From 7c1460331276ca2c0cc909a3e991dff177f86bca Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 6 Nov 2023 21:04:19 -0600 Subject: [PATCH 001/671] Change version number to 0.14.1-dev (#2760) --- CMakeLists.txt | 2 +- docs/source/conf.py | 2 +- include/openmc/version.h.in | 2 +- openmc/__init__.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d704dba39..24b855977 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ project(openmc C CXX) # Set version numbers set(OPENMC_VERSION_MAJOR 0) set(OPENMC_VERSION_MINOR 14) -set(OPENMC_VERSION_RELEASE 0) +set(OPENMC_VERSION_RELEASE 1) set(OPENMC_VERSION ${OPENMC_VERSION_MAJOR}.${OPENMC_VERSION_MINOR}.${OPENMC_VERSION_RELEASE}) configure_file(include/openmc/version.h.in "${CMAKE_BINARY_DIR}/include/openmc/version.h" @ONLY) diff --git a/docs/source/conf.py b/docs/source/conf.py index 735f01b2f..b2a4a6758 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -71,7 +71,7 @@ copyright = '2011-2023, Massachusetts Institute of Technology, UChicago Argonne # The short X.Y version. version = "0.14" # The full version, including alpha/beta/rc tags. -release = "0.14.0" +release = "0.14.1-dev" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/include/openmc/version.h.in b/include/openmc/version.h.in index a518e0d63..e1c2b0541 100644 --- a/include/openmc/version.h.in +++ b/include/openmc/version.h.in @@ -10,7 +10,7 @@ namespace openmc { constexpr int VERSION_MAJOR {@OPENMC_VERSION_MAJOR@}; constexpr int VERSION_MINOR {@OPENMC_VERSION_MINOR@}; constexpr int VERSION_RELEASE {@OPENMC_VERSION_RELEASE@}; -constexpr bool VERSION_DEV {false}; +constexpr bool VERSION_DEV {true}; constexpr std::array VERSION {VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE}; // clang-format on diff --git a/openmc/__init__.py b/openmc/__init__.py index 8369fd7a4..17b0abe2f 100644 --- a/openmc/__init__.py +++ b/openmc/__init__.py @@ -39,4 +39,4 @@ from .config import * from openmc.model import Model -__version__ = '0.14.0' +__version__ = '0.14.1-dev' From 910d1df1c9da70babab3e64a0e214b7bfc14637d Mon Sep 17 00:00:00 2001 From: nplinden <57541749+nplinden@users.noreply.github.com> Date: Thu, 9 Nov 2023 14:13:30 +0100 Subject: [PATCH 002/671] fix unit conversion in openmc.deplete.Results.get_mass (#2761) Co-authored-by: Nicolas Linden --- openmc/deplete/results.py | 2 +- tests/unit_tests/test_deplete_resultslist.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index d9e467322..2e537a1b7 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -318,7 +318,7 @@ class Results(list): # Divide by volume to get density mass /= self[0].volume[mat_id] elif mass_units == "kg": - mass *= 1e3 + mass /= 1e3 return times, mass diff --git a/tests/unit_tests/test_deplete_resultslist.py b/tests/unit_tests/test_deplete_resultslist.py index 190affea9..b22a78650 100644 --- a/tests/unit_tests/test_deplete_resultslist.py +++ b/tests/unit_tests/test_deplete_resultslist.py @@ -111,7 +111,7 @@ def test_get_mass(res): assert n_cm3 == pytest.approx(n_ref / volume) t_min, n_bcm = res.get_mass("1", "Xe135", mass_units="kg", time_units="min") - assert n_bcm == pytest.approx(n_ref * 1e3) + assert n_bcm == pytest.approx(n_ref / 1e3) assert t_min == pytest.approx(t_ref / 60) t_hour, _n = res.get_mass("1", "Xe135", time_units="h") From a3695c784eaddd74549f50b79567bc3d12254e79 Mon Sep 17 00:00:00 2001 From: Baptiste Mouginot <15145274+bam241@users.noreply.github.com> Date: Thu, 9 Nov 2023 17:05:42 +0100 Subject: [PATCH 003/671] Filter material from (#2750) Co-authored-by: Patrick Shriwise Co-authored-by: Paul Romano --- CMakeLists.txt | 1 + docs/source/pythonapi/base.rst | 1 + include/openmc/particle_data.h | 1 + include/openmc/tallies/filter.h | 1 + include/openmc/tallies/filter_material.h | 2 +- include/openmc/tallies/filter_materialfrom.h | 29 +++++++++++++++++ openmc/filter.py | 32 ++++++++++++++++--- openmc/lib/filter.py | 7 +++- src/tallies/filter.cpp | 3 ++ src/tallies/filter_materialfrom.cpp | 24 ++++++++++++++ .../surface_tally/inputs_true.dat | 2 +- tests/regression_tests/surface_tally/test.py | 6 ++-- 12 files changed, 99 insertions(+), 10 deletions(-) create mode 100644 include/openmc/tallies/filter_materialfrom.h create mode 100644 src/tallies/filter_materialfrom.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 24b855977..7269f4e63 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -406,6 +406,7 @@ list(APPEND libopenmc_SOURCES src/tallies/filter_energyfunc.cpp src/tallies/filter_legendre.cpp src/tallies/filter_material.cpp + src/tallies/filter_materialfrom.cpp src/tallies/filter_mesh.cpp src/tallies/filter_meshsurface.cpp src/tallies/filter_mu.cpp diff --git a/docs/source/pythonapi/base.rst b/docs/source/pythonapi/base.rst index e18423c58..ba86db117 100644 --- a/docs/source/pythonapi/base.rst +++ b/docs/source/pythonapi/base.rst @@ -118,6 +118,7 @@ Constructing Tallies openmc.Filter openmc.UniverseFilter openmc.MaterialFilter + openmc.MaterialFromFilter openmc.CellFilter openmc.CellFromFilter openmc.CellBornFilter diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index d0813f9c5..ee34c3052 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -435,6 +435,7 @@ public: int& material() { return material_; } const int& material() const { return material_; } int& material_last() { return material_last_; } + const int& material_last() const { return material_last_; } BoundaryInfo& boundary() { return boundary_; } diff --git a/include/openmc/tallies/filter.h b/include/openmc/tallies/filter.h index 8a17ee7ea..dc5872ce6 100644 --- a/include/openmc/tallies/filter.h +++ b/include/openmc/tallies/filter.h @@ -31,6 +31,7 @@ enum class FilterType { ENERGY_OUT, LEGENDRE, MATERIAL, + MATERIALFROM, MESH, MESH_SURFACE, MU, diff --git a/include/openmc/tallies/filter_material.h b/include/openmc/tallies/filter_material.h index 5da556d5e..aa5df5b3a 100644 --- a/include/openmc/tallies/filter_material.h +++ b/include/openmc/tallies/filter_material.h @@ -46,7 +46,7 @@ public: void set_materials(gsl::span materials); -private: +protected: //---------------------------------------------------------------------------- // Data members diff --git a/include/openmc/tallies/filter_materialfrom.h b/include/openmc/tallies/filter_materialfrom.h new file mode 100644 index 000000000..52039852a --- /dev/null +++ b/include/openmc/tallies/filter_materialfrom.h @@ -0,0 +1,29 @@ +#ifndef OPENMC_TALLIES_FILTER_MATERIALFROM_H +#define OPENMC_TALLIES_FILTER_MATERIALFROM_H + +#include + +#include "openmc/tallies/filter_material.h" + +namespace openmc { + +//============================================================================== +//! Specifies which material particles exit when crossing a surface. +//============================================================================== + +class MaterialFromFilter : public MaterialFilter { +public: + //---------------------------------------------------------------------------- + // Methods + + std::string type_str() const override { return "materialfrom"; } + FilterType type() const override { return FilterType::MATERIALFROM; } + + void get_all_bins(const Particle& p, TallyEstimator estimator, + FilterMatch& match) const override; + + std::string text_label(int bin) const override; +}; + +} // namespace openmc +#endif // OPENMC_TALLIES_FILTER_MATERIALFROM_H diff --git a/openmc/filter.py b/openmc/filter.py index c9457ceeb..8eb81432f 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -22,7 +22,7 @@ from ._xml import get_text _FILTER_TYPES = ( 'universe', 'material', 'cell', 'cellborn', 'surface', 'mesh', 'energy', 'energyout', 'mu', 'polar', 'azimuthal', 'distribcell', 'delayedgroup', - 'energyfunction', 'cellfrom', 'legendre', 'spatiallegendre', + 'energyfunction', 'cellfrom', 'materialfrom', 'legendre', 'spatiallegendre', 'sphericalharmonics', 'zernike', 'zernikeradial', 'particle', 'cellinstance', 'collision', 'time' ) @@ -451,7 +451,7 @@ class UniverseFilter(WithIDFilter): Parameters ---------- bins : openmc.UniverseBase, int, or iterable thereof - The Universes to tally. Either openmc.UniverseBase objects or their + The Universes to tally. Either :class:`openmc.UniverseBase` objects or their Integral ID numbers can be used. filter_id : int Unique identifier for the filter @@ -475,7 +475,31 @@ class MaterialFilter(WithIDFilter): Parameters ---------- bins : openmc.Material, Integral, or iterable thereof - The Materials to tally. Either openmc.Material objects or their + The material(s) to tally. Either :class:`openmc.Material` objects or their + Integral ID numbers can be used. + filter_id : int + Unique identifier for the filter + + Attributes + ---------- + bins : Iterable of Integral + openmc.Material IDs. + id : int + Unique identifier for the filter + num_bins : Integral + The number of filter bins + + """ + expected_type = Material + + +class MaterialFromFilter(WithIDFilter): + """Bins tally event locations based on the Material they occurred in. + + Parameters + ---------- + bins : openmc.Material, Integral, or iterable thereof + The material(s) to tally. Either :class:`openmc.Material` objects or their Integral ID numbers can be used. filter_id : int Unique identifier for the filter @@ -499,7 +523,7 @@ class CellFilter(WithIDFilter): Parameters ---------- bins : openmc.Cell, int, or iterable thereof - The cells to tally. Either openmc.Cell objects or their ID numbers can + The cells to tally. Either :class:`openmc.Cell` objects or their ID numbers can be used. filter_id : int Unique identifier for the filter diff --git a/openmc/lib/filter.py b/openmc/lib/filter.py index 8b175dd02..7c2e7e5c4 100644 --- a/openmc/lib/filter.py +++ b/openmc/lib/filter.py @@ -20,7 +20,7 @@ __all__ = [ 'Filter', 'AzimuthalFilter', 'CellFilter', 'CellbornFilter', 'CellfromFilter', 'CellInstanceFilter', 'CollisionFilter', 'DistribcellFilter', 'DelayedGroupFilter', 'EnergyFilter', 'EnergyoutFilter', 'EnergyFunctionFilter', 'LegendreFilter', - 'MaterialFilter', 'MeshFilter', 'MeshSurfaceFilter', 'MuFilter', 'ParticleFilter', + 'MaterialFilter', 'MaterialFromFilter', 'MeshFilter', 'MeshSurfaceFilter', 'MuFilter', 'ParticleFilter', 'PolarFilter', 'SphericalHarmonicsFilter', 'SpatialLegendreFilter', 'SurfaceFilter', 'UniverseFilter', 'ZernikeFilter', 'ZernikeRadialFilter', 'filters' ] @@ -342,6 +342,10 @@ class MaterialFilter(Filter): _dll.openmc_material_filter_set_bins(self._index, n, bins) +class MaterialFromFilter(Filter): + filter_type = 'materialfrom' + + class MeshFilter(Filter): filter_type = 'mesh' @@ -501,6 +505,7 @@ _FILTER_TYPE_MAP = { 'energyfunction': EnergyFunctionFilter, 'legendre': LegendreFilter, 'material': MaterialFilter, + 'materialfrom': MaterialFromFilter, 'mesh': MeshFilter, 'meshsurface': MeshSurfaceFilter, 'mu': MuFilter, diff --git a/src/tallies/filter.cpp b/src/tallies/filter.cpp index d8efd590f..5fae4cf60 100644 --- a/src/tallies/filter.cpp +++ b/src/tallies/filter.cpp @@ -21,6 +21,7 @@ #include "openmc/tallies/filter_energyfunc.h" #include "openmc/tallies/filter_legendre.h" #include "openmc/tallies/filter_material.h" +#include "openmc/tallies/filter_materialfrom.h" #include "openmc/tallies/filter_mesh.h" #include "openmc/tallies/filter_meshsurface.h" #include "openmc/tallies/filter_mu.h" @@ -121,6 +122,8 @@ Filter* Filter::create(const std::string& type, int32_t id) return Filter::create(id); } else if (type == "material") { return Filter::create(id); + } else if (type == "materialfrom") { + return Filter::create(id); } else if (type == "mesh") { return Filter::create(id); } else if (type == "meshsurface") { diff --git a/src/tallies/filter_materialfrom.cpp b/src/tallies/filter_materialfrom.cpp new file mode 100644 index 000000000..91f03aef8 --- /dev/null +++ b/src/tallies/filter_materialfrom.cpp @@ -0,0 +1,24 @@ +#include "openmc/tallies/filter_materialfrom.h" + +#include "openmc/cell.h" +#include "openmc/material.h" + +namespace openmc { + +void MaterialFromFilter::get_all_bins( + const Particle& p, TallyEstimator estimator, FilterMatch& match) const +{ + auto search = map_.find(p.material_last()); + if (search != map_.end()) { + match.bins_.push_back(search->second); + match.weights_.push_back(1.0); + } +} + +std::string MaterialFromFilter::text_label(int bin) const +{ + return "Material from " + + std::to_string(model::materials[materials_[bin]]->id_); +} + +} // namespace openmc diff --git a/tests/regression_tests/surface_tally/inputs_true.dat b/tests/regression_tests/surface_tally/inputs_true.dat index dc65e2a15..abd01266d 100644 --- a/tests/regression_tests/surface_tally/inputs_true.dat +++ b/tests/regression_tests/surface_tally/inputs_true.dat @@ -54,7 +54,7 @@ 1 - + 2 diff --git a/tests/regression_tests/surface_tally/test.py b/tests/regression_tests/surface_tally/test.py index 1224ebbb9..8968db9b6 100644 --- a/tests/regression_tests/surface_tally/test.py +++ b/tests/regression_tests/surface_tally/test.py @@ -106,19 +106,19 @@ class SurfaceTallyTestHarness(PyAPITestHarness): # Create partial current tallies from water to fuel # Filters - cell_from_filter = openmc.CellFromFilter(water) + mat_from_filter = openmc.MaterialFromFilter(borated_water) cell_filter = openmc.CellFilter(fuel) # Cell to cell filters for partial current cell_to_cell_tally = openmc.Tally(name=str('water_to_fuel_1')) - cell_to_cell_tally.filters = [cell_from_filter, cell_filter, \ + cell_to_cell_tally.filters = [mat_from_filter, cell_filter, \ energy_filter, polar_filter, azimuthal_filter] cell_to_cell_tally.scores = ['current'] tallies_file.append(cell_to_cell_tally) # Cell from + surface filters for partial current cell_to_cell_tally = openmc.Tally(name=str('water_to_fuel_2')) - cell_to_cell_tally.filters = [cell_from_filter, surface_filter, \ + cell_to_cell_tally.filters = [mat_from_filter, surface_filter, \ energy_filter, polar_filter, azimuthal_filter] cell_to_cell_tally.scores = ['current'] tallies_file.append(cell_to_cell_tally) From 24e1c951614d71fb7014a914615e55a697eef04c Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Wed, 15 Nov 2023 03:49:53 -0600 Subject: [PATCH 004/671] Fix lagrangian interpolation (#2775) --- include/openmc/interpolate.h | 5 ++- tests/cpp_unit_tests/CMakeLists.txt | 1 + tests/cpp_unit_tests/test_interpolate.cpp | 51 +++++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 tests/cpp_unit_tests/test_interpolate.cpp diff --git a/include/openmc/interpolate.h b/include/openmc/interpolate.h index 05353f130..e36fbb1d6 100644 --- a/include/openmc/interpolate.h +++ b/include/openmc/interpolate.h @@ -4,6 +4,9 @@ #include #include +#include + +#include "openmc/error.h" #include "openmc/search.h" namespace openmc { @@ -47,7 +50,7 @@ inline double interpolate_lagrangian(gsl::span xs, numerator *= (x - xs[idx + j]); denominator *= (xs[idx + i] - xs[idx + j]); } - output += (numerator / denominator) * ys[i]; + output += (numerator / denominator) * ys[idx + i]; } return output; diff --git a/tests/cpp_unit_tests/CMakeLists.txt b/tests/cpp_unit_tests/CMakeLists.txt index 50ae9a208..24ec1b7a9 100644 --- a/tests/cpp_unit_tests/CMakeLists.txt +++ b/tests/cpp_unit_tests/CMakeLists.txt @@ -2,6 +2,7 @@ set(TEST_NAMES test_distribution test_file_utils test_tally + test_interpolate # Add additional unit test files here ) diff --git a/tests/cpp_unit_tests/test_interpolate.cpp b/tests/cpp_unit_tests/test_interpolate.cpp new file mode 100644 index 000000000..4f19f2b63 --- /dev/null +++ b/tests/cpp_unit_tests/test_interpolate.cpp @@ -0,0 +1,51 @@ +#include +#include + +#include +#include + +#include "openmc/interpolate.h" +#include "openmc/search.h" + +using namespace openmc; + +TEST_CASE("Test Lagranian Interpolation") +{ + std::vector xs {0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0}; + std::vector ys {0.0, 1.0, 1.0, 2.0, 3.0, 3.0, 5.0}; + + // ensure we get data points back at the x values + for (int n = 1; n <= 6; n++) { + for (int i = 0; i < xs.size(); i++) { + double x = xs[i]; + double y = ys[i]; + + size_t idx = lower_bound_index(xs.begin(), xs.end(), x); + idx = std::min(idx, xs.size() - n - 1); + double out = interpolate_lagrangian(xs, ys, idx, x, n); + REQUIRE(out == y); + } + } + + // spot checks based on an independent implementation of Lagrangian + // interpolation + std::map>> checks; + checks[1] = {{0.5, 0.5}, {4.5, 3.0}, {2.5, 1.5}, {5.5, 4.0}}; + checks[2] = {{2.5, 1.5}, {4.5, 2.75}, {4.9999, 3.0}, {4.00001, 3.0}}; + checks[3] = {{2.5592, 1.5}, {4.5, 2.9375}, {4.9999, 3.0}, {4.00001, 3.0}}; + + for (auto check_set : checks) { + int order = check_set.first; + auto checks = check_set.second; + + for (auto check : checks) { + double input = check.first; + double exp_output = check.second; + + size_t idx = lower_bound_index(xs.begin(), xs.end(), input); + idx = std::min(idx, xs.size() - order - 1); + double out = interpolate_lagrangian(xs, ys, idx, input, order); + REQUIRE_THAT(out, Catch::Matchers::WithinAbs(exp_output, 1e-04)); + } + } +} \ No newline at end of file From 9830efaf2aadf68ffde9a054388bacef20ab5645 Mon Sep 17 00:00:00 2001 From: Ethan Peterson Date: Tue, 21 Nov 2023 17:22:42 -0500 Subject: [PATCH 005/671] change identity matrix format to csr in cram (#2771) --- openmc/deplete/cram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmc/deplete/cram.py b/openmc/deplete/cram.py index f5f80dfdc..8e7bb819d 100644 --- a/openmc/deplete/cram.py +++ b/openmc/deplete/cram.py @@ -77,7 +77,7 @@ class IPFCramSolver(DepSystemSolver): """ A = sp.csr_matrix(A * dt, dtype=np.float64) y = n0.copy() - ident = sp.eye(A.shape[0]) + ident = sp.eye(A.shape[0], format='csr') for alpha, theta in zip(self.alpha, self.theta): y += 2*np.real(alpha*sla.spsolve(A - theta*ident, y)) return y * self.alpha0 From cad9fdc338181798abdd6d8f3d4fcee4d20e73ee Mon Sep 17 00:00:00 2001 From: Ethan Peterson Date: Mon, 27 Nov 2023 14:16:34 -0500 Subject: [PATCH 006/671] Modifications to deplete_with_transfer_rates regression test suite (#2779) --- .../ref_depletion_with_feed.h5 | Bin 37328 -> 37328 bytes .../ref_depletion_with_removal.h5 | Bin 37328 -> 37328 bytes .../ref_depletion_with_transfer.h5 | Bin 37328 -> 37328 bytes .../ref_no_depletion_only_feed.h5 | Bin 37328 -> 37328 bytes .../ref_no_depletion_only_removal.h5 | Bin 37328 -> 37328 bytes .../ref_no_depletion_with_transfer.h5 | Bin 37328 -> 37328 bytes .../deplete_with_transfer_rates/test.py | 7 ++++--- 7 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_feed.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_feed.h5 index 4f270afab00ddb5651aeb22aed0ca9b32c173d06..b9be7634513373af572ead9d97e1b705ad454e83 100644 GIT binary patch delta 717 zcmcbxnCZe|rVR`F>bFfgBN4Zf-;Mza?D_t7R4sY~=Q}92!UgoEk!uh+GFMJU8P!Z1eoKfJi#n{kJZN1MwyRUz2@0~NezfMQp?#DvORoWjM z>+LR<9f*~E_{UbzBB}l1x~sM(|L?^s^eDg$IQ4Qe%Yiaxdzb@mxvg-E$zA|=P#OP` zbw@9&z|~LmKfd`=(0({S`_J>14;6-R{*Fspg44^Kp?pIIhL#s>2b*~;;0n%(#q`@Z z>B9Nz1=ef(wtlovSo)x<*MIip^Zl~kJ}OJ>FMpCLOYS#t4qdD(ujyRFNmQ)1=QueMBH z$RyoEpI_U%%K6~d#;D&}y5krkBD$~T+rRUf2>17Nze)|&UU9hkodyMYmuDj) zV*0Gri;ef_z~vvOxJrLJWnwokXV#QMjQ?!U+-VK457)5UllZZE)<0lGv~xe!X*=`Z zwwPn-!_WCwZKr(Rzqein5fLxe?y6B-%LI>zvm%bOzf3_y#Di-W4X&+GhFfU$NwtM1 zeIK0v%Ii=1k}XDXe*f9dVvaLzP(CmsG;}oc-Sf@i3R-g=Qg0!BF;|Yw_|{U+EnFTr}AIH`6q%|el6jhU<;FvebMps>I@OM z{70dvjR)hG!uf7Fi}jCmE5Z5d!Y55+y&UZlnj$R3_WiS6`KdX@lUKtoRqM!s!}lHQ z?Pe%9$o5A3vn@F5z|hHc&DO3lPx|`-MYsW5YuGnzYGtv9IpA#dDuIZSg>VO5P6*z6 zrCk%Qe(|<-%lxVi!1--Iznq!8+7Qm4?c-lI|BNq`Z^*##SRiTJ1}iJLf+c@eeaI6s zf%Chqcihnr`Dj0%WBrDZ3x$)<_se=Wl+CffxIX{b({=;reb#!3!5#+AAN&(rlDYhy z{R86^MKyJuqxYNXy62yAk~et!>6h9ar$gt&Oq%#lsfdBNXEt40r z$xpnXRd10~wAp4MA|l+@rF`Ol@DA>bJ2T!dj_R8T_qS?_kNzfONw~Zvw`rBqy%li2 zi?G?!L#3*4zPLk}=9eg2J7%uLeTDP?*%}3j#r`|0X7^=7=*IPLK@l;NO?G_{gI&GQ z-h_+iuG%`RFW7!#0U{>vZGHD=%LX=hL>#vDczjWE3EV+?XA6sqsu2UT{#BN4Zf-;MzaocR8BR4sY~=es;GXb+E@U<;GCeyzpw?}G$f z{`JaPUgoEk!uk7_FMJU8P!Z1eno;1g#n{kJYrW4uyRUz2@1HZgzfMQp?#DvORoWjM z>+P(BF+4;6-R{;o?}g44^Kp?pIIhOQTE2b*~;;0n%(#q`@Z z>B9Nz1=ef(wtlovSo)x<*MIip^Zl~kcO;hBU;ZRhmfUaPeCumoK-3Qv=Y{!CBOU9k zoc~x>o6r8H;;gJ`tJ-FA%4x;=g;6iR-gc6ld}&v!`bB3KFSF3K-)&tkof0dbezj%t zLN@t{7qsdP-MKt&^dTZbaJ9>hO^%3&n7cB&Cw&njA_T);FH!z24tJ)`EdRC1XO_eH zcKPW}>}INP{?ZEq6Eos$?Y8-L^liNG$9Dg(2kMWFHSDwkoMeuQJJs6-)U4mX{?k8O zmg5HO*B@Q6H9RrPH1m@Z+yHN`s`mP9Hh4sA^2v0WrL`0u5urEcnZ28jh`3`v>Wfw# zIs(^Uo$uv1y}}62Upc3OM>{qE$_GY7iNOz>P)|#^g0_iA_ZB}jh4VXEX5AFn^wGX+ SiIHj6&xMoMPn2Z=MFaqOH{S;U delta 717 zcmcbxnCZe|rVR`F>JLi^MVy_)Z^r-yx@pS0PUXLX^IrtB{93|0!4@VT|DxmR)fpmi z`Psrz8xO`Wh4ZK8EY?5Ltpw-C37<5P^>Vb6Y>u!H+xO3w_j7ZKC$ENGkk*j{hwnSq z+uc@fknN55XM69g14Ad*HQTVpJn8QT6yXNg*RpTe)XHKHbAUVPGNC@70 zrCk%Q-h11+Wqwr$;C$_0U(QTkZ3yQt_3pMI&`aXNHP%(PkkqBF03SL&wkwl2I64){;M+A?_| zoBYHJTJ^41(^o393Bawb7Rs3|q45FE-|Fb1=JW&+5&ajUBOSd2;qv>%zqlo9t%mbW z8$VofdY}#G&wH&e)pE(nZu!pNIi9co*iLTS_NT*7-A@BWm!NG0RYMr*X;lR diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_transfer.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_transfer.h5 index 791447253c5a3d6626037f202ab67dab218303a7..51173f778b945f5c666bd9a8ed9ace75f47eb0ec 100644 GIT binary patch delta 746 zcmcbxnCZe|rVR`F>bFfgBN4Zf-;Mza?D_t7R4sY~=Q}+xXb+E@U<;Eseyzpw?}G$f z{@Kb|UgoEk!uflbFMJU8P!Y~|pHbkl#n{kJeZ9{=yRUz2@0~NezfMQp?#DvORoWjM z>+LR;9f*~E_{UbzBB}l1x~sOv|L?^s^eDg$IQ4Qe%Yiaxdzb@mx~*`F$zA|=P#OP` zbw@9&z}5HrAK!c_Xg{2v`R94dhYCYDf6FB;!RckrP`)7pL(2=cgUvh^a0QpeV*2fy zbm9E<0_(MXTR++-EPYVb>py$)`F`1)1#=7SuY8gzOYS#tzWX&VAnJ#T^YZ+sk&bm% z&P-O-=Ci-4IBRLzsU)t5Ge$m<0%Pe&5cUzZBr^L#qUu|)L zc`WDNL+yr5Kb$@ZT)n?(fsXUm%4Z4&cRo8!Jg8CsDvke|ZU!IRO8!ta0PcX8oWm0W8BO5)RmX*1U)t^p@<)kSYpX}eNud_LzuyFGFiLxx9SONeIAnLII delta 746 zcmcbxnCZe|rVR`F>K{uAMVy_)Z^r-yQ&N?8oyva&=Wh>Y`L%?1f-Ouw?M27at20F4 z@>#-B8xO`Wh4YJY7V97BR)X_&gio5tdO6x9H$_;8?fYjd^|?94lUKtoUhBw#!}lHQ z?M^5+$o5A3vz>X?fuWP@nypJ?p7i$vif{ui*RXHc)XHKHbHJzSRRR$u3*ippP7L0A zrCk%Q{`9tW%lxVi!1;Q=zMPr7+7Qk^i^pdxzJ)=d3f><4p1EXB-O_+=XdD?FAI z47bdwEnNVQCC6_QZ&uusg Date: Tue, 28 Nov 2023 02:32:25 -0600 Subject: [PATCH 007/671] In volume calculation mode, only load atomic weight ratio from data files (#2741) --- src/nuclide.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/nuclide.cpp b/src/nuclide.cpp index d28630252..14d21bb09 100644 --- a/src/nuclide.cpp +++ b/src/nuclide.cpp @@ -62,6 +62,10 @@ Nuclide::Nuclide(hid_t group, const vector& temperature) read_attribute(group, "metastable", metastable_); read_attribute(group, "atomic_weight_ratio", awr_); + if (settings::run_mode == RunMode::VOLUME) { + return; + } + // Determine temperatures available hid_t kT_group = open_group(group, "kTs"); auto dset_names = dataset_names(kT_group); From 8b2698f5c0fda21a5ff467dada3e5e9ca1a8f6e7 Mon Sep 17 00:00:00 2001 From: Ethan Peterson Date: Tue, 28 Nov 2023 08:34:02 -0500 Subject: [PATCH 008/671] Change matrix format to CSC in depletion (#2764) --- openmc/deplete/abc.py | 6 +++--- openmc/deplete/chain.py | 10 +++++----- openmc/deplete/cram.py | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index d135b2dae..5d906efd8 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -543,7 +543,7 @@ class Integrator(ABC): User-supplied functions are expected to have the following signature: ``solver(A, n0, t) -> n1`` where - * ``A`` is a :class:`scipy.sparse.csr_matrix` making up the + * ``A`` is a :class:`scipy.sparse.csc_matrix` making up the depletion matrix * ``n0`` is a 1-D :class:`numpy.ndarray` of initial compositions for a given material in atoms/cm3 @@ -921,7 +921,7 @@ class SIIntegrator(Integrator): User-supplied functions are expected to have the following signature: ``solver(A, n0, t) -> n1`` where - * ``A`` is a :class:`scipy.sparse.csr_matrix` making up the + * ``A`` is a :class:`scipy.sparse.csc_matrix` making up the depletion matrix * ``n0`` is a 1-D :class:`numpy.ndarray` of initial compositions for a given material in atoms/cm3 @@ -1023,7 +1023,7 @@ class DepSystemSolver(ABC): Parameters ---------- - A : scipy.sparse.csr_matrix + A : scipy.sparse.csc_matrix Sparse transmutation matrix ``A[j, i]`` describing rates at which isotope ``i`` transmutes to isotope ``j`` n0 : numpy.ndarray diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index dc01a1b34..e06cb5725 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -596,7 +596,7 @@ class Chain: Returns ------- - scipy.sparse.csr_matrix + scipy.sparse.csc_matrix Sparse matrix representing depletion. See Also @@ -680,11 +680,11 @@ class Chain: # Clear set of reactions reactions.clear() - # Use DOK matrix as intermediate representation, then convert to CSR and return + # Use DOK matrix as intermediate representation, then convert to CSC and return n = len(self) matrix_dok = sp.dok_matrix((n, n)) dict.update(matrix_dok, matrix) - return matrix_dok.tocsr() + return matrix_dok.tocsc() def form_rr_term(self, tr_rates, mats): """Function to form the transfer rate term matrices. @@ -713,7 +713,7 @@ class Chain: Returns ------- - scipy.sparse.csr_matrix + scipy.sparse.csc_matrix Sparse matrix representing transfer term. """ @@ -746,7 +746,7 @@ class Chain: n = len(self) matrix_dok = sp.dok_matrix((n, n)) dict.update(matrix_dok, matrix) - return matrix_dok.tocsr() + return matrix_dok.tocsc() def get_branch_ratios(self, reaction="(n,gamma)"): """Return a dictionary with reaction branching ratios diff --git a/openmc/deplete/cram.py b/openmc/deplete/cram.py index 8e7bb819d..53de83bb6 100644 --- a/openmc/deplete/cram.py +++ b/openmc/deplete/cram.py @@ -75,9 +75,9 @@ class IPFCramSolver(DepSystemSolver): Final compositions after ``dt`` """ - A = sp.csr_matrix(A * dt, dtype=np.float64) + A = dt * sp.csc_matrix(A, dtype=np.float64) y = n0.copy() - ident = sp.eye(A.shape[0], format='csr') + ident = sp.eye(A.shape[0], format='csc') for alpha, theta in zip(self.alpha, self.theta): y += 2*np.real(alpha*sla.spsolve(A - theta*ident, y)) return y * self.alpha0 From e8faccdc93bee95360662b7bc36f7e79a03eb089 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Tue, 28 Nov 2023 13:56:21 -0600 Subject: [PATCH 009/671] Call simulation_finalize if needed when finalizing OpenMC (#2790) --- src/finalize.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/finalize.cpp b/src/finalize.cpp index b5b23b9d5..ea0470f9b 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -63,6 +63,9 @@ using namespace openmc; int openmc_finalize() { + if (simulation::initialized) + openmc_simulation_finalize(); + // Clear results openmc_reset(); From 1d4cd9b0aa290ef624d1689f47f2710d15e68dcc Mon Sep 17 00:00:00 2001 From: John Tramm Date: Tue, 28 Nov 2023 15:20:54 -0600 Subject: [PATCH 010/671] F90_NONE Removal (MGMC tallying optimization) (#2785) --- include/openmc/constants.h | 1 - include/openmc/particle_data.h | 8 ++++---- src/tallies/filter_energy.cpp | 4 ++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/include/openmc/constants.h b/include/openmc/constants.h index 2b9eaca29..ba558b040 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -317,7 +317,6 @@ enum class GlobalTally { K_COLLISION, K_ABSORPTION, K_TRACKLENGTH, LEAKAGE }; // Miscellaneous constexpr int C_NONE {-1}; -constexpr int F90_NONE {0}; // TODO: replace usage of this with C_NONE // Interpolation rules enum class Interpolation { diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index ee34c3052..37eb05bdb 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -257,10 +257,10 @@ private: vector cell_last_; //!< coordinates for all levels // Energy data - double E_; //!< post-collision energy in eV - double E_last_; //!< pre-collision energy in eV - int g_ {0}; //!< post-collision energy group (MG only) - int g_last_; //!< pre-collision energy group (MG only) + double E_; //!< post-collision energy in eV + double E_last_; //!< pre-collision energy in eV + int g_ {C_NONE}; //!< post-collision energy group (MG only) + int g_last_; //!< pre-collision energy group (MG only) // Other physical data double wgt_ {1.0}; //!< particle weight diff --git a/src/tallies/filter_energy.cpp b/src/tallies/filter_energy.cpp index 48448cb1f..825dd2ee5 100644 --- a/src/tallies/filter_energy.cpp +++ b/src/tallies/filter_energy.cpp @@ -59,7 +59,7 @@ void EnergyFilter::set_bins(gsl::span bins) void EnergyFilter::get_all_bins( const Particle& p, TallyEstimator estimator, FilterMatch& match) const { - if (p.g() != F90_NONE && matches_transport_groups_) { + 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 { @@ -98,7 +98,7 @@ std::string EnergyFilter::text_label(int bin) const void EnergyoutFilter::get_all_bins( const Particle& p, TallyEstimator estimator, FilterMatch& match) const { - if (p.g() != F90_NONE && matches_transport_groups_) { + if (p.g() != C_NONE && matches_transport_groups_) { match.bins_.push_back(data::mg.num_energy_groups_ - p.g() - 1); match.weights_.push_back(1.0); From 87e00f76ed6fd30b0dc10e8c86daba1f4ff7dff5 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 29 Nov 2023 05:20:01 -0600 Subject: [PATCH 011/671] Add inline to openmc::interpolate (#2789) --- include/openmc/interpolate.h | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/include/openmc/interpolate.h b/include/openmc/interpolate.h index e36fbb1d6..db501f71b 100644 --- a/include/openmc/interpolate.h +++ b/include/openmc/interpolate.h @@ -56,8 +56,9 @@ inline double interpolate_lagrangian(gsl::span xs, return output; } -double interpolate(gsl::span xs, gsl::span ys, - double x, Interpolation i = Interpolation::lin_lin) +inline double interpolate(gsl::span xs, + gsl::span ys, double x, + Interpolation i = Interpolation::lin_lin) { int idx = lower_bound_index(xs.begin(), xs.end(), x); @@ -95,4 +96,4 @@ double interpolate(gsl::span xs, gsl::span ys, } // namespace openmc -#endif \ No newline at end of file +#endif From 47c37f506daad78b596536ae37aa0d61e2efab33 Mon Sep 17 00:00:00 2001 From: nplinden <57541749+nplinden@users.noreply.github.com> Date: Wed, 29 Nov 2023 12:44:14 +0100 Subject: [PATCH 012/671] Correctly apply volumes to materials when using DAGMC geometries (#2787) Co-authored-by: Nicolas Linden --- openmc/model/model.py | 7 ++++++- openmc/summary.py | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index a40a79bc9..9da834e13 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -787,7 +787,12 @@ class Model: for i, vol_calc in enumerate(self.settings.volume_calculations): vol_calc.load_results(f"volume_{i + 1}.h5") # First add them to the Python side - self.geometry.add_volume_information(vol_calc) + if vol_calc.domain_type == "material" and self.materials: + for material in self.materials: + if material.id in vol_calc.volumes: + material.add_volume_information(vol_calc) + else: + self.geometry.add_volume_information(vol_calc) # And now repeat for the C API if self.is_initialized and vol_calc.domain_type == 'material': diff --git a/openmc/summary.py b/openmc/summary.py index 43224334d..6423f8ce1 100644 --- a/openmc/summary.py +++ b/openmc/summary.py @@ -240,4 +240,9 @@ class Summary: Results from a stochastic volume calculation """ - self.geometry.add_volume_information(volume_calc) + if volume_calc.domain_type == "material" and self.materials: + for material in self.materials: + if material.id in volume_calc.volumes: + material.add_volume_information(volume_calc) + else: + self.geometry.add_volume_information(volume_calc) From e0d03812b90dcc86dd4f2f48b055c25592e9a474 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Sat, 2 Dec 2023 11:35:23 -0600 Subject: [PATCH 013/671] Mesh Source Class (#2759) Co-authored-by: Paul Romano Co-authored-by: Jonathan Shimwell --- .github/workflows/format-check.yml | 9 +- docs/source/io_formats/settings.rst | 13 +- docs/source/pythonapi/base.rst | 1 + include/openmc/distribution.h | 7 +- include/openmc/distribution_multi.h | 2 + include/openmc/distribution_spatial.h | 15 ++- include/openmc/mesh.h | 75 +++++++++--- include/openmc/source.h | 42 ++++++- openmc/bounding_box.py | 4 + openmc/settings.py | 20 ++- openmc/source.py | 167 ++++++++++++++++++++++++-- openmc/stats/multivariate.py | 1 + src/distribution.cpp | 12 +- src/distribution_multi.cpp | 19 +++ src/distribution_spatial.cpp | 88 +++++++++++--- src/mesh.cpp | 78 ++++++++++-- src/settings.cpp | 23 +--- src/source.cpp | 155 ++++++++++++++++-------- tests/unit_tests/test_source_mesh.py | 167 +++++++++++++++++++++++++- 19 files changed, 752 insertions(+), 146 deletions(-) diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml index 4914d5e5e..36ccece3c 100644 --- a/.github/workflows/format-check.yml +++ b/.github/workflows/format-check.yml @@ -1,6 +1,13 @@ name: C++ Format Check -on: pull_request +on: + # allow workflow to be run manually + workflow_dispatch: + + pull_request: + branches: + - develop + - master jobs: cpp-linter: diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 03abe9a3c..9b855b250 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -473,6 +473,8 @@ pseudo-random number generator. *Default*: 1 +.. _source_element: + -------------------- ```` Element -------------------- @@ -491,7 +493,8 @@ attributes/sub-elements: *Default*: 1.0 :type: - Indicator of source type. One of ``independent``, ``file``, or ``compiled``. + Indicator of source type. One of ``independent``, ``file``, ``compiled``, or ``mesh``. + The type of the source will be determined by this attribute if it is present. :particle: The source particle type, either ``neutron`` or ``photon``. @@ -664,6 +667,14 @@ attributes/sub-elements: *Default*: false + :mesh: + For mesh sources, this indicates the ID of the corresponding mesh. + + :source: + For mesh sources, this sub-element specifies the source for an individual + mesh element and follows the format for :ref:`source_element`. The number of + ```` sub-elements should correspond to the number of mesh elements. + .. _univariate: Univariate Probability Distributions diff --git a/docs/source/pythonapi/base.rst b/docs/source/pythonapi/base.rst index ba86db117..705aecee9 100644 --- a/docs/source/pythonapi/base.rst +++ b/docs/source/pythonapi/base.rst @@ -25,6 +25,7 @@ Simulation Settings openmc.IndependentSource openmc.FileSource openmc.CompiledSource + openmc.MeshSource openmc.SourceParticle openmc.VolumeCalculation openmc.Settings diff --git a/include/openmc/distribution.h b/include/openmc/distribution.h index 65c68ad64..e70c803de 100644 --- a/include/openmc/distribution.h +++ b/include/openmc/distribution.h @@ -7,6 +7,7 @@ #include // for size_t #include "pugixml.hpp" +#include #include "openmc/constants.h" #include "openmc/memory.h" // for unique_ptr @@ -43,9 +44,9 @@ class DiscreteIndex { public: DiscreteIndex() {}; DiscreteIndex(pugi::xml_node node); - DiscreteIndex(const double* p, int n); + DiscreteIndex(gsl::span p); - void assign(const double* p, int n); + void assign(gsl::span p); //! Sample a value from the distribution //! \param seed Pseudorandom number seed pointer @@ -77,7 +78,7 @@ private: class Discrete : public Distribution { public: explicit Discrete(pugi::xml_node node); - Discrete(const double* x, const double* p, int n); + Discrete(const double* x, const double* p, size_t n); //! Sample a value from the distribution //! \param seed Pseudorandom number seed pointer diff --git a/include/openmc/distribution_multi.h b/include/openmc/distribution_multi.h index e171d58ab..9e84d03d5 100644 --- a/include/openmc/distribution_multi.h +++ b/include/openmc/distribution_multi.h @@ -22,6 +22,8 @@ public: explicit UnitSphereDistribution(pugi::xml_node node); virtual ~UnitSphereDistribution() = default; + static unique_ptr create(pugi::xml_node node); + //! Sample a direction from the distribution //! \param seed Pseudorandom number seed pointer //! \return Direction sampled diff --git a/include/openmc/distribution_spatial.h b/include/openmc/distribution_spatial.h index 9fba95060..bd10a2996 100644 --- a/include/openmc/distribution_spatial.h +++ b/include/openmc/distribution_spatial.h @@ -19,6 +19,8 @@ public: //! Sample a position from the distribution virtual Position sample(uint64_t* seed) const = 0; + + static unique_ptr create(pugi::xml_node node); }; //============================================================================== @@ -102,16 +104,27 @@ private: class MeshSpatial : public SpatialDistribution { public: explicit MeshSpatial(pugi::xml_node node); + explicit MeshSpatial(int32_t mesh_id, gsl::span strengths); //! Sample a position from the distribution //! \param seed Pseudorandom number seed pointer //! \return Sampled position Position sample(uint64_t* seed) const override; - const Mesh* mesh() const { return model::meshes.at(mesh_idx_).get(); } + //! Sample the mesh for an element and position within that element + //! \param seed Pseudorandom number seed pointer + //! \return Sampled element index and position within that element + std::pair sample_mesh(uint64_t* seed) const; + //! For unstructured meshes, ensure that elements are all linear tetrahedra + void check_element_types() const; + + // Accessors + const Mesh* mesh() const { return model::meshes.at(mesh_idx_).get(); } int32_t n_sources() const { return this->mesh()->n_bins(); } + double total_strength() { return this->elem_idx_dist_.integral(); } + private: int32_t mesh_idx_ {C_NONE}; DiscreteIndex elem_idx_dist_; //!< Distribution of diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index 47f54c023..de556321f 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -10,6 +10,7 @@ #include "pugixml.hpp" #include "xtensor/xtensor.hpp" +#include "openmc/error.h" #include "openmc/memory.h" // for unique_ptr #include "openmc/particle.h" #include "openmc/position.h" @@ -81,12 +82,12 @@ public: //! Return a position in the local coordinates of the mesh virtual Position local_coords(const Position& r) const { return r; }; - //! Sample a mesh volume using a certain seed + //! Sample a position within a mesh element // - //! \param[in] seed Seed to use for random sampling - //! \param[in] bin Bin value of the tet sampled - //! \return sampled position within tet - virtual Position sample(uint64_t* seed, int32_t bin) const = 0; + //! \param[in] bin Bin value of the mesh element sampled + //! \param[inout] seed Seed to use for random sampling + //! \return sampled position within mesh element + virtual Position sample_element(int32_t bin, uint64_t* seed) const = 0; //! Determine which bins were crossed by a particle // @@ -183,7 +184,12 @@ public: } }; - Position sample(uint64_t* seed, int32_t bin) const override; + Position sample_element(int32_t bin, uint64_t* seed) const override + { + return sample_element(get_indices_from_bin(bin), seed); + }; + + virtual Position sample_element(const MeshIndex& ijk, uint64_t* seed) const; int get_bin(Position r) const override; @@ -240,6 +246,30 @@ public: //! \param[in] i Direction index virtual int get_index_in_direction(double r, int i) const = 0; + //! Get the coordinate for the mesh grid boundary in the positive direction + //! + //! \param[in] ijk Array of mesh indices + //! \param[in] i Direction index + virtual double positive_grid_boundary(const MeshIndex& ijk, int i) const + { + auto msg = + fmt::format("Attempting to call positive_grid_boundary on a {} mesh.", + get_mesh_type()); + fatal_error(msg); + }; + + //! Get the coordinate for the mesh grid boundary in the negative direction + //! + //! \param[in] ijk Array of mesh indices + //! \param[in] i Direction index + virtual double negative_grid_boundary(const MeshIndex& ijk, int i) const + { + auto msg = + fmt::format("Attempting to call negative_grid_boundary on a {} mesh.", + get_mesh_type()); + fatal_error(msg); + }; + //! Get the closest distance from the coordinate r to the grid surface //! in i direction that bounds mesh cell ijk and that is larger than l //! The coordinate r does not have to be inside the mesh cell ijk. In @@ -322,18 +352,17 @@ public: void to_hdf5(hid_t group) const override; - // New methods //! Get the coordinate for the mesh grid boundary in the positive direction //! //! \param[in] ijk Array of mesh indices //! \param[in] i Direction index - double positive_grid_boundary(const MeshIndex& ijk, int i) const; + double positive_grid_boundary(const MeshIndex& ijk, int i) const override; //! Get the coordinate for the mesh grid boundary in the negative direction //! //! \param[in] ijk Array of mesh indices //! \param[in] i Direction index - double negative_grid_boundary(const MeshIndex& ijk, int i) const; + double negative_grid_boundary(const MeshIndex& ijk, int i) const override; //! Count number of bank sites in each mesh bin / energy bin // @@ -373,18 +402,17 @@ public: void to_hdf5(hid_t group) const override; - // New methods //! Get the coordinate for the mesh grid boundary in the positive direction //! //! \param[in] ijk Array of mesh indices //! \param[in] i Direction index - double positive_grid_boundary(const MeshIndex& ijk, int i) const; + double positive_grid_boundary(const MeshIndex& ijk, int i) const override; //! Get the coordinate for the mesh grid boundary in the negative direction //! //! \param[in] ijk Array of mesh indices //! \param[in] i Direction index - double negative_grid_boundary(const MeshIndex& ijk, int i) const; + double negative_grid_boundary(const MeshIndex& ijk, int i) const override; //! Return the volume for a given mesh index double volume(const MeshIndex& ijk) const override; @@ -410,6 +438,8 @@ public: static const std::string mesh_type; + Position sample_element(const MeshIndex& ijk, uint64_t* seed) const override; + MeshDistance distance_to_grid_boundary(const MeshIndex& ijk, int i, const Position& r0, const Direction& u, double l) const override; @@ -420,10 +450,16 @@ public: double volume(const MeshIndex& ijk) const override; - array, 3> grid_; + // grid accessors + double r(int i) const { return grid_[0][i]; } + double phi(int i) const { return grid_[1][i]; } + double z(int i) const { return grid_[2][i]; } int set_grid(); + // Data members + array, 3> grid_; + private: double find_r_crossing( const Position& r, const Direction& u, double l, int shell) const; @@ -466,6 +502,8 @@ public: static const std::string mesh_type; + Position sample_element(const MeshIndex& ijk, uint64_t* seed) const override; + MeshDistance distance_to_grid_boundary(const MeshIndex& ijk, int i, const Position& r0, const Direction& u, double l) const override; @@ -474,10 +512,15 @@ public: void to_hdf5(hid_t group) const override; - array, 3> grid_; + double r(int i) const { return grid_[0][i]; } + double theta(int i) const { return grid_[1][i]; } + double phi(int i) const { return grid_[2][i]; } int set_grid(); + // Data members + array, 3> grid_; + private: double find_r_crossing( const Position& r, const Direction& u, double l, int shell) const; @@ -621,7 +664,7 @@ public: // Overridden Methods - Position sample(uint64_t* seed, int32_t bin) const override; + Position sample_element(int32_t bin, uint64_t* seed) const override; void bins_crossed(Position r0, Position r1, const Direction& u, vector& bins, vector& lengths) const override; @@ -789,7 +832,7 @@ public: void bins_crossed(Position r0, Position r1, const Direction& u, vector& bins, vector& lengths) const override; - Position sample(uint64_t* seed, int32_t bin) const override; + Position sample_element(int32_t bin, uint64_t* seed) const override; int get_bin(Position r) const override; diff --git a/include/openmc/source.h b/include/openmc/source.h index ad2aae562..d5ea9bd1d 100644 --- a/include/openmc/source.h +++ b/include/openmc/source.h @@ -50,6 +50,8 @@ public: // Methods that can be overridden virtual double strength() const { return 1.0; } + + static unique_ptr create(pugi::xml_node node); }; //============================================================================== @@ -101,12 +103,13 @@ private: class FileSource : public Source { public: // Constructors - explicit FileSource(std::string path); - explicit FileSource(const vector& sites) : sites_ {sites} {} + explicit FileSource(pugi::xml_node node); + explicit FileSource(const std::string& path); // Methods SourceSite sample(uint64_t* seed) const override; - + void load_sites_from_file( + const std::string& path); //!< Load source sites from file private: vector sites_; //!< Source sites from a file }; @@ -118,7 +121,7 @@ private: class CompiledSourceWrapper : public Source { public: // Constructors, destructors - CompiledSourceWrapper(std::string path, std::string parameters); + CompiledSourceWrapper(pugi::xml_node node); ~CompiledSourceWrapper(); // Defer implementation to custom source library @@ -129,6 +132,8 @@ public: double strength() const override { return compiled_source_->strength(); } + void setup(const std::string& path, const std::string& parameters); + private: void* shared_library_; //!< library from dlopen unique_ptr compiled_source_; @@ -136,6 +141,35 @@ private: typedef unique_ptr create_compiled_source_t(std::string parameters); +//============================================================================== +//! Mesh-based source with different distributions for each element +//============================================================================== + +class MeshSource : public Source { +public: + // Constructors + explicit MeshSource(pugi::xml_node node); + + //! Sample from the external source distribution + //! \param[inout] seed Pseudorandom seed pointer + //! \return Sampled site + SourceSite sample(uint64_t* seed) const override; + + // Properties + double strength() const override { return space_->total_strength(); } + + // Accessors + const std::unique_ptr& source(int32_t i) const + { + return sources_.size() == 1 ? sources_[0] : sources_[i]; + } + +private: + // Data members + unique_ptr space_; //!< Mesh spatial + vector> sources_; //!< Source distributions +}; + //============================================================================== // Functions //============================================================================== diff --git a/openmc/bounding_box.py b/openmc/bounding_box.py index b74931d73..e5655cf8c 100644 --- a/openmc/bounding_box.py +++ b/openmc/bounding_box.py @@ -95,6 +95,10 @@ class BoundingBox: new |= other return new + def __contains__(self, point): + """Check whether or not a point is in the bounding box""" + return all(point > self.lower_left) and all(point < self.upper_right) + @property def center(self) -> np.ndarray: return (self[0] + self[1]) / 2 diff --git a/openmc/settings.py b/openmc/settings.py index d8f74dd11..5d0800ac1 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -12,7 +12,7 @@ import lxml.etree as ET import openmc.checkvalue as cv from openmc.stats.multivariate import MeshSpatial -from . import (RegularMesh, SourceBase, IndependentSource, +from . import (RegularMesh, SourceBase, MeshSource, IndependentSource, VolumeCalculation, WeightWindows, WeightWindowGenerator) from ._xml import clean_indentation, get_text, reorder_attributes from openmc.checkvalue import PathLike @@ -1072,13 +1072,19 @@ class Settings: element = ET.SubElement(root, "max_order") element.text = str(self._max_order) - def _create_source_subelement(self, root): + def _create_source_subelement(self, root, mesh_memo=None): for source in self.source: root.append(source.to_xml_element()) if isinstance(source, IndependentSource) and isinstance(source.space, MeshSpatial): path = f"./mesh[@id='{source.space.mesh.id}']" if root.find(path) is None: root.append(source.space.mesh.to_xml_element()) + if isinstance(source, MeshSource): + path = f"./mesh[@id='{source.mesh.id}']" + if root.find(path) is None: + root.append(source.mesh.to_xml_element()) + if mesh_memo is not None: + mesh_memo.add(source.mesh.id) def _create_volume_calcs_subelement(self, root): for calc in self.volume_calculations: @@ -1365,7 +1371,8 @@ class Settings: path = f"./mesh[@id='{ww.mesh.id}']" if root.find(path) is None: root.append(ww.mesh.to_xml_element()) - if mesh_memo is not None: mesh_memo.add(ww.mesh.id) + if mesh_memo is not None: + mesh_memo.add(ww.mesh.id) if self._weight_windows_on is not None: elem = ET.SubElement(root, "weight_windows_on") @@ -1787,7 +1794,7 @@ class Settings: self._create_max_write_lost_particles_subelement(element) self._create_generations_per_batch_subelement(element) self._create_keff_trigger_subelement(element) - self._create_source_subelement(element) + self._create_source_subelement(element, mesh_memo) self._create_output_subelement(element) self._create_statepoint_subelement(element) self._create_sourcepoint_subelement(element) @@ -1874,6 +1881,11 @@ class Settings: Settings object """ + # read all meshes under the settings node and update + settings_meshes = _read_meshes(elem) + meshes = {} if meshes is None else meshes + meshes.update(settings_meshes) + settings = cls() settings._eigenvalue_from_xml_element(elem) settings._run_mode_from_xml_element(elem) diff --git a/openmc/source.py b/openmc/source.py index 98b7673b1..c339d6ed8 100644 --- a/openmc/source.py +++ b/openmc/source.py @@ -18,7 +18,7 @@ from openmc.checkvalue import PathLike from openmc.stats.multivariate import UnitSphere, Spatial from openmc.stats.univariate import Univariate from ._xml import get_text -from .mesh import MeshBase +from .mesh import MeshBase, StructuredMesh, UnstructuredMesh class SourceBase(ABC): @@ -31,7 +31,7 @@ class SourceBase(ABC): Attributes ---------- - type : {'independent', 'file', 'compiled'} + type : {'independent', 'file', 'compiled', 'mesh'} Indicator of source type. strength : float Strength of the source @@ -61,7 +61,6 @@ class SourceBase(ABC): XML element containing source data """ - pass def to_xml_element(self) -> ET.Element: """Return XML representation of the source @@ -79,7 +78,7 @@ class SourceBase(ABC): return element @classmethod - def from_xml_element(cls, elem: ET.Element, meshes=None) -> openmc.SourceBase: + def from_xml_element(cls, elem: ET.Element, meshes=None) -> SourceBase: """Generate source from an XML element Parameters @@ -114,6 +113,8 @@ class SourceBase(ABC): return CompiledSource.from_xml_element(elem) elif source_type == 'file': return FileSource.from_xml_element(elem) + elif source_type == 'mesh': + return MeshSource.from_xml_element(elem, meshes) else: raise ValueError(f'Source type {source_type} is not recognized') @@ -292,13 +293,13 @@ class IndependentSource(SourceBase): def populate_xml_element(self, element): """Add necessary source information to an XML element + Returns ------- element : lxml.etree._Element XML element containing source data """ - super().populate_xml_element(element) element.set("particle", self.particle) if self.space is not None: element.append(self.space.to_xml_element()) @@ -315,7 +316,7 @@ class IndependentSource(SourceBase): id_elem.text = ' '.join(str(uid) for uid in self.domain_ids) @classmethod - def from_xml_element(cls, elem: ET.Element, meshes=None) -> 'openmc.SourceBase': + def from_xml_element(cls, elem: ET.Element, meshes=None) -> SourceBase: """Generate source from an XML element Parameters @@ -382,6 +383,154 @@ class IndependentSource(SourceBase): return source +class MeshSource(SourceBase): + """A source with a spatial distribution over mesh elements + + This class represents a mesh-based source in which random positions are + uniformly sampled within mesh elements and each element can have independent + angle, energy, and time distributions. The element sampled is chosen based + on the relative strengths of the sources applied to the elements. The + strength of the mesh source as a whole is the sum of all source strengths + applied to the elements. + + .. versionadded:: 0.14.1 + + Parameters + ---------- + mesh : openmc.MeshBase + The mesh over which source sites will be generated. + sources : iterable of openmc.SourceBase + Sources for each element in the mesh. If spatial distributions are set + on any of the source objects, they will be ignored during source site + sampling. + + Attributes + ---------- + mesh : openmc.MeshBase + The mesh over which source sites will be generated. + sources : numpy.ndarray or iterable of openmc.SourceBase + The set of sources to apply to each element. The shape of this array + must match the shape of the mesh with and exception in the case of + unstructured mesh, which allows for application of 1-D array or + iterable. + strength : float + Strength of the source + type : str + Indicator of source type: 'mesh' + + """ + def __init__(self, mesh: MeshBase, sources: Sequence[SourceBase]): + self.mesh = mesh + self.sources = sources + + @property + def type(self) -> str: + return "mesh" + + @property + def mesh(self) -> MeshBase: + return self._mesh + + @property + def strength(self) -> float: + return sum(s.strength for s in self.sources.flat) + + @property + def sources(self) -> np.ndarray: + return self._sources + + @mesh.setter + def mesh(self, m): + cv.check_type('source mesh', m, MeshBase) + self._mesh = m + + @sources.setter + def sources(self, s): + cv.check_iterable_type('mesh sources', s, SourceBase, max_depth=3) + + s = np.asarray(s) + + if isinstance(self.mesh, StructuredMesh) and s.shape != self.mesh.dimension: + raise ValueError('The shape of the source array' + f'({s.shape}) does not match the ' + f'dimensions of the structured mesh ({self.mesh.dimension})') + elif isinstance(self.mesh, UnstructuredMesh): + if len(s.shape) > 1: + raise ValueError('Sources must be a 1-D array for unstructured mesh') + + self._sources = s + for src in self._sources.flat: + if isinstance(src, IndependentSource) and src.space is not None: + warnings.warn('Some sources on the mesh have spatial ' + 'distributions that will be ignored at runtime.') + break + + @strength.setter + def strength(self, val): + cv.check_type('mesh source strength', val, Real) + self.set_total_strength(val) + + def set_total_strength(self, strength: float): + """Scales the element source strengths based on a desired total strength. + + Parameters + ---------- + strength : float + Total source strength + + """ + current_strength = self.strength if self.strength != 0.0 else 1.0 + + for s in self.sources.flat: + s.strength *= strength / current_strength + + def normalize_source_strengths(self): + """Update all element source strengths such that they sum to 1.0.""" + self.set_total_strength(1.0) + + def populate_xml_element(self, elem: ET.Element): + """Add necessary source information to an XML element + + Returns + ------- + element : lxml.etree._Element + XML element containing source data + + """ + elem.set("mesh", str(self.mesh.id)) + + # write in the order of mesh indices + for idx in self.mesh.indices: + idx = tuple(i - 1 for i in idx) + elem.append(self.sources[idx].to_xml_element()) + + @classmethod + def from_xml_element(cls, elem: ET.Element, meshes) -> openmc.MeshSource: + """ + Generate MeshSource from an XML element + + Parameters + ---------- + elem : lxml.etree._Element + XML element + meshes : dict + A dictionary with mesh IDs as keys and openmc.MeshBase instances as + values + + Returns + ------- + openmc.MeshSource + MeshSource generated from the XML element + """ + mesh_id = int(get_text(elem, 'mesh')) + + mesh = meshes[mesh_id] + + sources = [SourceBase.from_xml_element(e) for e in elem.iterchildren('source')] + sources = np.asarray(sources).reshape(mesh.dimension, order='F') + return cls(mesh, sources) + + def Source(*args, **kwargs): """ A function for backward compatibility of sources. Will be removed in the @@ -459,15 +608,13 @@ class CompiledSource(SourceBase): XML element containing source data """ - super().populate_xml_element(element) - element.set("library", self.library) if self.parameters is not None: element.set("parameters", self.parameters) @classmethod - def from_xml_element(cls, elem: ET.Element, meshes=None) -> openmc.CompiledSource: + def from_xml_element(cls, elem: ET.Element) -> openmc.CompiledSource: """Generate a compiled source from an XML element Parameters @@ -551,8 +698,6 @@ class FileSource(SourceBase): XML element containing source data """ - super().populate_xml_element(element) - if self.path is not None: element.set("file", self.path) diff --git a/openmc/stats/multivariate.py b/openmc/stats/multivariate.py index 09a2f8752..22c3d7ea8 100644 --- a/openmc/stats/multivariate.py +++ b/openmc/stats/multivariate.py @@ -710,6 +710,7 @@ class MeshSpatial(Spatial): """ element = ET.Element('space') + element.set('type', 'mesh') element.set("mesh_id", str(self.mesh.id)) element.set("volume_normalized", str(self.volume_normalized)) diff --git a/src/distribution.cpp b/src/distribution.cpp index 9c76147ee..3026630b3 100644 --- a/src/distribution.cpp +++ b/src/distribution.cpp @@ -27,17 +27,17 @@ DiscreteIndex::DiscreteIndex(pugi::xml_node node) auto params = get_node_array(node, "parameters"); std::size_t n = params.size() / 2; - assign(params.data() + n, n); + assign({params.data() + n, n}); } -DiscreteIndex::DiscreteIndex(const double* p, int n) +DiscreteIndex::DiscreteIndex(gsl::span p) { - assign(p, n); + assign(p); } -void DiscreteIndex::assign(const double* p, int n) +void DiscreteIndex::assign(gsl::span p) { - prob_.assign(p, p + n); + prob_.assign(p.begin(), p.end()); this->init_alias(); } @@ -126,7 +126,7 @@ Discrete::Discrete(pugi::xml_node node) : di_(node) x_.assign(params.begin(), params.begin() + n); } -Discrete::Discrete(const double* x, const double* p, int n) : di_(p, n) +Discrete::Discrete(const double* x, const double* p, size_t n) : di_({p, n}) { x_.assign(x, x + n); diff --git a/src/distribution_multi.cpp b/src/distribution_multi.cpp index 0735f0994..b7b3efe52 100644 --- a/src/distribution_multi.cpp +++ b/src/distribution_multi.cpp @@ -12,6 +12,25 @@ namespace openmc { +unique_ptr UnitSphereDistribution::create( + pugi::xml_node node) +{ + // Check for type of angular distribution + std::string type; + if (check_for_node(node, "type")) + type = get_node_value(node, "type", true, true); + if (type == "isotropic") { + return UPtrAngle {new Isotropic()}; + } else if (type == "monodirectional") { + return UPtrAngle {new Monodirectional(node)}; + } else if (type == "mu-phi") { + return UPtrAngle {new PolarAzimuthal(node)}; + } else { + fatal_error(fmt::format( + "Invalid angular distribution for external source: {}", type)); + } +} + //============================================================================== // UnitSphereDistribution implementation //============================================================================== diff --git a/src/distribution_spatial.cpp b/src/distribution_spatial.cpp index e1a4c20c2..8f34ea6b9 100644 --- a/src/distribution_spatial.cpp +++ b/src/distribution_spatial.cpp @@ -8,6 +8,36 @@ namespace openmc { +//============================================================================== +// SpatialDistribution implementation +//============================================================================== + +unique_ptr SpatialDistribution::create(pugi::xml_node node) +{ + // Check for type of spatial distribution and read + std::string type; + if (check_for_node(node, "type")) + type = get_node_value(node, "type", true, true); + if (type == "cartesian") { + return UPtrSpace {new CartesianIndependent(node)}; + } else if (type == "cylindrical") { + return UPtrSpace {new CylindricalIndependent(node)}; + } else if (type == "spherical") { + return UPtrSpace {new SphericalIndependent(node)}; + } else if (type == "mesh") { + return UPtrSpace {new MeshSpatial(node)}; + } else if (type == "box") { + return UPtrSpace {new SpatialBox(node)}; + } else if (type == "fission") { + return UPtrSpace {new SpatialBox(node, true)}; + } else if (type == "point") { + return UPtrSpace {new SpatialPoint(node)}; + } else { + fatal_error(fmt::format( + "Invalid spatial distribution for external source: {}", type)); + } +} + //============================================================================== // CartesianIndependent implementation //============================================================================== @@ -189,27 +219,23 @@ Position SphericalIndependent::sample(uint64_t* seed) const MeshSpatial::MeshSpatial(pugi::xml_node node) { + + if (get_node_value(node, "type", true, true) != "mesh") { + fatal_error(fmt::format( + "Incorrect spatial type '{}' for a MeshSpatial distribution")); + } + // No in-tet distributions implemented, could include distributions for the // barycentric coords Read in unstructured mesh from mesh_id value int32_t mesh_id = std::stoi(get_node_value(node, "mesh_id")); // Get pointer to spatial distribution mesh_idx_ = model::mesh_map.at(mesh_id); - auto mesh_ptr = - dynamic_cast(model::meshes.at(mesh_idx_).get()); - if (!mesh_ptr) { - fatal_error("Only unstructured mesh is supported for source sampling."); - } + const auto mesh_ptr = model::meshes.at(mesh_idx_).get(); - // ensure that the unstructured mesh contains only linear tets - for (int bin = 0; bin < mesh_ptr->n_bins(); bin++) { - if (mesh_ptr->element_type(bin) != ElementType::LINEAR_TET) { - fatal_error( - "Mesh specified for source must contain only linear tetrahedra."); - } - } + check_element_types(); - int32_t n_bins = this->n_sources(); + size_t n_bins = this->n_sources(); std::vector strengths(n_bins, 1.0); // Create cdfs for sampling for an element over a mesh @@ -227,18 +253,44 @@ MeshSpatial::MeshSpatial(pugi::xml_node node) if (get_node_value_bool(node, "volume_normalized")) { for (int i = 0; i < n_bins; i++) { - strengths[i] *= mesh()->volume(i); + strengths[i] *= this->mesh()->volume(i); } } - elem_idx_dist_.assign(strengths.data(), n_bins); + elem_idx_dist_.assign(strengths); +} + +MeshSpatial::MeshSpatial(int32_t mesh_idx, gsl::span strengths) + : mesh_idx_(mesh_idx) +{ + check_element_types(); + elem_idx_dist_.assign(strengths); +} + +void MeshSpatial::check_element_types() const +{ + const auto umesh_ptr = dynamic_cast(this->mesh()); + if (umesh_ptr) { + // ensure that the unstructured mesh contains only linear tets + for (int bin = 0; bin < umesh_ptr->n_bins(); bin++) { + if (umesh_ptr->element_type(bin) != ElementType::LINEAR_TET) { + fatal_error( + "Mesh specified for source must contain only linear tetrahedra."); + } + } + } +} + +std::pair MeshSpatial::sample_mesh(uint64_t* seed) const +{ + // Sample the CDF defined in initialization above + int32_t elem_idx = elem_idx_dist_.sample(seed); + return {elem_idx, mesh()->sample_element(elem_idx, seed)}; } Position MeshSpatial::sample(uint64_t* seed) const { - // Sample over the CDF defined in initialization above - int32_t elem_idx = elem_idx_dist_.sample(seed); - return mesh()->sample(seed, elem_idx); + return this->sample_mesh(seed).second; } //============================================================================== diff --git a/src/mesh.cpp b/src/mesh.cpp index 12052bed6..ea66552de 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -27,6 +27,7 @@ #include "openmc/hdf5_interface.h" #include "openmc/memory.h" #include "openmc/message_passing.h" +#include "openmc/random_dist.h" #include "openmc/search.h" #include "openmc/settings.h" #include "openmc/tallies/filter.h" @@ -165,6 +166,23 @@ xt::xtensor StructuredMesh::get_x_shape() const return xt::adapt(tmp_shape, {n_dimension_}); } +Position StructuredMesh::sample_element( + const MeshIndex& ijk, uint64_t* seed) const +{ + // lookup the lower/upper bounds for the mesh element + double x_min = negative_grid_boundary(ijk, 0); + double x_max = positive_grid_boundary(ijk, 0); + + double y_min = (n_dimension_ >= 2) ? negative_grid_boundary(ijk, 1) : 0.0; + double y_max = (n_dimension_ >= 2) ? positive_grid_boundary(ijk, 1) : 0.0; + + double z_min = (n_dimension_ == 3) ? negative_grid_boundary(ijk, 2) : 0.0; + double z_max = (n_dimension_ == 3) ? positive_grid_boundary(ijk, 2) : 0.0; + + return {x_min + (x_max - x_min) * prn(seed), + y_min + (y_max - y_min) * prn(seed), z_min + (z_max - z_min) * prn(seed)}; +} + //============================================================================== // Unstructured Mesh implementation //============================================================================== @@ -381,11 +399,6 @@ StructuredMesh::MeshIndex StructuredMesh::get_indices_from_bin(int bin) const return ijk; } -Position StructuredMesh::sample(uint64_t* seed, int32_t bin) const -{ - fatal_error("Position sampling on structured meshes is not yet implemented"); -} - int StructuredMesh::get_bin(Position r) const { // Determine indices @@ -1077,6 +1090,30 @@ StructuredMesh::MeshIndex CylindricalMesh::get_indices( return idx; } +Position CylindricalMesh::sample_element( + const MeshIndex& ijk, uint64_t* seed) const +{ + double r_min = this->r(ijk[0] - 1); + double r_max = this->r(ijk[0]); + + double phi_min = this->phi(ijk[1] - 1); + double phi_max = this->phi(ijk[1]); + + double z_min = this->z(ijk[2] - 1); + double z_max = this->z(ijk[2]); + + double r_min_sq = r_min * r_min; + double r_max_sq = r_max * r_max; + double r = std::sqrt(uniform_distribution(r_min_sq, r_max_sq, seed)); + double phi = uniform_distribution(phi_min, phi_max, seed); + double z = uniform_distribution(z_min, z_max, seed); + + double x = r * std::cos(phi); + double y = r * std::sin(phi); + + return origin_ + Position(x, y, z); +} + double CylindricalMesh::find_r_crossing( const Position& r, const Direction& u, double l, int shell) const { @@ -1338,6 +1375,33 @@ StructuredMesh::MeshIndex SphericalMesh::get_indices( return idx; } +Position SphericalMesh::sample_element( + const MeshIndex& ijk, uint64_t* seed) const +{ + double r_min = this->r(ijk[0] - 1); + double r_max = this->r(ijk[0]); + + double theta_min = this->theta(ijk[1] - 1); + double theta_max = this->theta(ijk[1]); + + double phi_min = this->phi(ijk[2] - 1); + double phi_max = this->phi(ijk[2]); + + double cos_theta = uniform_distribution(theta_min, theta_max, seed); + double sin_theta = std::sin(std::acos(cos_theta)); + double phi = uniform_distribution(phi_min, phi_max, seed); + double r_min_cub = std::pow(r_min, 3); + double r_max_cub = std::pow(r_max, 3); + // might be faster to do rejection here? + double r = std::cbrt(uniform_distribution(r_min_cub, r_max_cub, seed)); + + double x = r * std::cos(phi) * sin_theta; + double y = r * std::sin(phi) * sin_theta; + double z = r * cos_theta; + + return origin_ + Position(x, y, z); +} + double SphericalMesh::find_r_crossing( const Position& r, const Direction& u, double l, int shell) const { @@ -2185,7 +2249,7 @@ std::string MOABMesh::library() const } // Sample position within a tet for MOAB type tets -Position MOABMesh::sample(uint64_t* seed, int32_t bin) const +Position MOABMesh::sample_element(int32_t bin, uint64_t* seed) const { moab::EntityHandle tet_ent = get_ent_handle_from_bin(bin); @@ -2672,7 +2736,7 @@ void LibMesh::initialize() } // Sample position within a tet for LibMesh type tets -Position LibMesh::sample(uint64_t* seed, int32_t bin) const +Position LibMesh::sample_element(int32_t bin, uint64_t* seed) const { const auto& elem = get_element_from_bin(bin); // Get tet vertex coordinates from LibMesh diff --git a/src/settings.cpp b/src/settings.cpp index c0a3aacf3..a5256c9c2 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -457,28 +457,7 @@ void read_settings_xml(pugi::xml_node root) // Get point to list of elements and make sure there is at least one for (pugi::xml_node node : root.children("source")) { - if (check_for_node(node, "file")) { - auto path = get_node_value(node, "file", false, true); - if (ends_with(path, ".mcpl") || ends_with(path, ".mcpl.gz")) { - auto sites = mcpl_source_sites(path); - model::external_sources.push_back(make_unique(sites)); - } else { - model::external_sources.push_back(make_unique(path)); - } - } else if (check_for_node(node, "library")) { - // Get shared library path and parameters - auto path = get_node_value(node, "library", false, true); - std::string parameters; - if (check_for_node(node, "parameters")) { - parameters = get_node_value(node, "parameters", false, true); - } - - // Create custom source - model::external_sources.push_back( - make_unique(path, parameters)); - } else { - model::external_sources.push_back(make_unique(node)); - } + model::external_sources.push_back(Source::create(node)); } // Check if the user has specified to read surface source diff --git a/src/source.cpp b/src/source.cpp index 3ddd903bf..bf52b568a 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -22,6 +22,7 @@ #include "openmc/geometry.h" #include "openmc/hdf5_interface.h" #include "openmc/material.h" +#include "openmc/mcpl_interface.h" #include "openmc/memory.h" #include "openmc/message_passing.h" #include "openmc/mgxs_interface.h" @@ -31,6 +32,7 @@ #include "openmc/settings.h" #include "openmc/simulation.h" #include "openmc/state_point.h" +#include "openmc/string_utils.h" #include "openmc/xml_interface.h" namespace openmc { @@ -44,6 +46,39 @@ namespace model { vector> external_sources; } +//============================================================================== +// Source create implementation +//============================================================================== + +unique_ptr Source::create(pugi::xml_node node) +{ + // if the source type is present, use it to determine the type + // of object to create + if (check_for_node(node, "type")) { + std::string source_type = get_node_value(node, "type"); + if (source_type == "independent") { + return make_unique(node); + } else if (source_type == "file") { + return make_unique(node); + } else if (source_type == "compiled") { + return make_unique(node); + } else if (source_type == "mesh") { + return make_unique(node); + } else { + fatal_error(fmt::format("Invalid source type '{}' found.", source_type)); + } + } else { + // support legacy source format + if (check_for_node(node, "file")) { + return make_unique(node); + } else if (check_for_node(node, "library")) { + return make_unique(node); + } else { + return make_unique(node); + } + } +} + //============================================================================== // IndependentSource implementation //============================================================================== @@ -81,32 +116,7 @@ IndependentSource::IndependentSource(pugi::xml_node node) // Spatial distribution for external source if (check_for_node(node, "space")) { - // Get pointer to spatial distribution - pugi::xml_node node_space = node.child("space"); - - // Check for type of spatial distribution and read - std::string type; - if (check_for_node(node_space, "type")) - type = get_node_value(node_space, "type", true, true); - if (type == "cartesian") { - space_ = UPtrSpace {new CartesianIndependent(node_space)}; - } else if (type == "cylindrical") { - space_ = UPtrSpace {new CylindricalIndependent(node_space)}; - } else if (type == "spherical") { - space_ = UPtrSpace {new SphericalIndependent(node_space)}; - } else if (type == "mesh") { - space_ = UPtrSpace {new MeshSpatial(node_space)}; - } else if (type == "box") { - space_ = UPtrSpace {new SpatialBox(node_space)}; - } else if (type == "fission") { - space_ = UPtrSpace {new SpatialBox(node_space, true)}; - } else if (type == "point") { - space_ = UPtrSpace {new SpatialPoint(node_space)}; - } else { - fatal_error(fmt::format( - "Invalid spatial distribution for external source: {}", type)); - } - + space_ = SpatialDistribution::create(node.child("space")); } else { // If no spatial distribution specified, make it a point source space_ = UPtrSpace {new SpatialPoint()}; @@ -114,24 +124,7 @@ IndependentSource::IndependentSource(pugi::xml_node node) // Determine external source angular distribution if (check_for_node(node, "angle")) { - // Get pointer to angular distribution - pugi::xml_node node_angle = node.child("angle"); - - // Check for type of angular distribution - std::string type; - if (check_for_node(node_angle, "type")) - type = get_node_value(node_angle, "type", true, true); - if (type == "isotropic") { - angle_ = UPtrAngle {new Isotropic()}; - } else if (type == "monodirectional") { - angle_ = UPtrAngle {new Monodirectional(node_angle)}; - } else if (type == "mu-phi") { - angle_ = UPtrAngle {new PolarAzimuthal(node_angle)}; - } else { - fatal_error(fmt::format( - "Invalid angular distribution for external source: {}", type)); - } - + angle_ = UnitSphereDistribution::create(node.child("angle")); } else { angle_ = UPtrAngle {new Isotropic()}; } @@ -290,8 +283,22 @@ SourceSite IndependentSource::sample(uint64_t* seed) const //============================================================================== // FileSource implementation //============================================================================== +FileSource::FileSource(pugi::xml_node node) +{ + auto path = get_node_value(node, "file", false, true); + if (ends_with(path, ".mcpl") || ends_with(path, ".mcpl.gz")) { + sites_ = mcpl_source_sites(path); + } else { + this->load_sites_from_file(path); + } +} -FileSource::FileSource(std::string path) +FileSource::FileSource(const std::string& path) +{ + load_sites_from_file(path); +} + +void FileSource::load_sites_from_file(const std::string& path) { // Check if source file exists if (!file_exists(path)) { @@ -328,9 +335,19 @@ SourceSite FileSource::sample(uint64_t* seed) const //============================================================================== // CompiledSourceWrapper implementation //============================================================================== +CompiledSourceWrapper::CompiledSourceWrapper(pugi::xml_node node) +{ + // Get shared library path and parameters + auto path = get_node_value(node, "library", false, true); + std::string parameters; + if (check_for_node(node, "parameters")) { + parameters = get_node_value(node, "parameters", false, true); + } + setup(path, parameters); +} -CompiledSourceWrapper::CompiledSourceWrapper( - std::string path, std::string parameters) +void CompiledSourceWrapper::setup( + const std::string& path, const std::string& parameters) { #ifdef HAS_DYNAMIC_LINKING // Open the library @@ -378,6 +395,50 @@ CompiledSourceWrapper::~CompiledSourceWrapper() #endif } +//============================================================================== +// MeshSource implementation +//============================================================================== + +MeshSource::MeshSource(pugi::xml_node node) +{ + int32_t mesh_id = stoi(get_node_value(node, "mesh")); + int32_t mesh_idx = model::mesh_map.at(mesh_id); + const auto& mesh = model::meshes[mesh_idx]; + + std::vector strengths; + // read all source distributions and populate strengths vector for MeshSpatial + // object + for (auto source_node : node.children("source")) { + sources_.emplace_back(Source::create(source_node)); + strengths.push_back(sources_.back()->strength()); + } + + // the number of source distributions should either be one or equal to the + // number of mesh elements + if (sources_.size() > 1 && sources_.size() != mesh->n_bins()) { + fatal_error(fmt::format("Incorrect number of source distributions ({}) for " + "mesh source with {} elements.", + sources_.size(), mesh->n_bins())); + } + + space_ = std::make_unique(mesh_idx, strengths); +} + +SourceSite MeshSource::sample(uint64_t* seed) const +{ + // sample location and element from mesh + auto mesh_location = space_->sample_mesh(seed); + + // Sample source for the chosen element + int32_t element = mesh_location.first; + SourceSite site = source(element)->sample(seed); + + // Replace spatial position with the one already sampled + site.r = mesh_location.second; + + return site; +} + //============================================================================== // Non-member functions //============================================================================== diff --git a/tests/unit_tests/test_source_mesh.py b/tests/unit_tests/test_source_mesh.py index fd8b04148..7bec2d072 100644 --- a/tests/unit_tests/test_source_mesh.py +++ b/tests/unit_tests/test_source_mesh.py @@ -1,6 +1,5 @@ from itertools import product from pathlib import Path -from subprocess import call import pytest import numpy as np @@ -8,9 +7,11 @@ import openmc import openmc.lib from tests import cdtemp -from tests.regression_tests import config +################### +# MeshSpatial Tests +################### TETS_PER_VOXEL = 12 # This test uses a geometry file with cells that match a regular mesh. Each cell @@ -48,9 +49,9 @@ def model(): settings.particles = 100 settings.batches = 2 - return openmc.model.Model(geometry=geometry, - materials=materials, - settings=settings) + return openmc.Model(geometry=geometry, + materials=materials, + settings=settings) ### Setup test cases ### param_values = (['libmesh', 'moab'], # mesh libraries @@ -174,6 +175,7 @@ def test_strengths_size_failure(request, model): model.export_to_xml() openmc.run() + def test_roundtrip(run_in_tmpdir, model, request): if not openmc.lib._libmesh_enabled() and not openmc.lib._dagmc_enabled(): pytest.skip("Unstructured mesh is not enabled in this build.") @@ -201,3 +203,158 @@ def test_roundtrip(run_in_tmpdir, model, request): assert space_in.mesh.id == space_out.mesh.id assert space_in.volume_normalized == space_out.volume_normalized + + +################### +# MeshSource tests +################### +@pytest.mark.parametrize('mesh_type', ('rectangular', 'cylindrical')) +def test_mesh_source_independent(run_in_tmpdir, mesh_type): + """ + A void model containing a single box + """ + min, max = -10, 10 + box = openmc.model.RectangularParallelepiped( + min, max, min, max, min, max, boundary_type='vacuum') + + geometry = openmc.Geometry([openmc.Cell(region=-box)]) + + settings = openmc.Settings() + settings.particles = 100 + settings.batches = 10 + settings.run_mode = 'fixed source' + + model = openmc.Model(geometry=geometry, settings=settings) + + # define a 2 x 2 x 2 mesh + if mesh_type == 'rectangular': + mesh = openmc.RegularMesh.from_domain(model.geometry, (2, 2, 2)) + elif mesh_type == 'cylindrical': + mesh = openmc.CylindricalMesh.from_domain(model.geometry, (1, 4, 2)) + + energy = openmc.stats.Discrete([1.e6], [1.0]) + + # create sources with only one non-zero strength for the source in the mesh + # voxel occupying the lowest octant. Direct source particles straight out of + # the problem from there. This demonstrates that + # 1) particles are only being sourced within the intented mesh voxel based + # on source strength + # 2) particles are respecting the angle distributions assigned to each voxel + sources = np.empty(mesh.dimension, dtype=openmc.SourceBase) + centroids = mesh.centroids + x, y, z = np.swapaxes(mesh.centroids, -1, 0) + for i, j, k in mesh.indices: + # mesh.indices is currently one-indexed, adjust for Python arrays + ijk = (i-1, j-1, k-1) + + # get the centroid of the ijk mesh element and use it to set the + # direction of the source directly out of the problem + centroid = centroids[ijk] + vec = np.sign(centroid, dtype=float) + vec /= np.linalg.norm(vec) + angle = openmc.stats.Monodirectional(vec) + sources[ijk] = openmc.IndependentSource(energy=energy, angle=angle, strength=0.0) + + # create and apply the mesh source + mesh_source = openmc.MeshSource(mesh, sources) + model.settings.source = mesh_source + + # tally the flux on the mesh + mesh_filter = openmc.MeshFilter(mesh) + tally = openmc.Tally() + tally.filters = [mesh_filter] + tally.scores = ['flux'] + + model.tallies = openmc.Tallies([tally]) + + # for each element, set a single-non zero source with particles + # traveling out of the mesh (and geometry) w/o crossing any other + # mesh elements + for i, j, k in mesh.indices: + ijk = (i-1, j-1, k-1) + # zero-out all source strengths and set the strength + # on the element of interest + mesh_source.strength = 0.0 + mesh_source.sources[ijk].strength = 1.0 + + sp_file = model.run() + + with openmc.StatePoint(sp_file) as sp: + tally_out = sp.get_tally(id=tally.id) + mean = tally_out.get_reshaped_data(expand_dims=True) + + # remove nuclides and scores axes + mean = mean[..., 0, 0] + # the mesh elment with a non-zero source strength should have a value + assert mean[ijk] != 0 + # all other values should be zero + mean[ijk] = 0 + assert np.all(mean == 0), f'Failed on index {ijk} with centroid {mesh.centroids[ijk]}' + + # test roundtrip + xml_model = openmc.Model.from_model_xml() + xml_source = xml_model.settings.source[0] + assert isinstance(xml_source, openmc.MeshSource) + assert xml_source.strength == 1.0 + assert isinstance(xml_source.mesh, type(mesh_source.mesh)) + assert xml_source.mesh.dimension == mesh_source.mesh.dimension + assert xml_source.mesh.id == mesh_source.mesh.id + assert len(xml_source.sources) == len(mesh_source.sources) + + # check strength adjustment methods + assert mesh_source.strength == 1.0 + mesh_source.strength = 100.0 + assert mesh_source.strength == 100.0 + + mesh_source.normalize_source_strengths() + assert mesh_source.strength == 1.0 + + +def test_mesh_source_file(run_in_tmpdir): + # Creating a source file with a single particle + source_particle = openmc.SourceParticle(time=10.0) + openmc.write_source_file([source_particle], 'source.h5') + file_source = openmc.FileSource('source.h5') + + model = openmc.Model() + + rect_prism = openmc.model.RectangularParallelepiped( + -5.0, 5.0, -5.0, 5.0, -5.0, 5.0, boundary_type='vacuum') + + mat = openmc.Material() + mat.add_nuclide('H1', 1.0) + + model.geometry = openmc.Geometry([openmc.Cell(fill=mat, region=-rect_prism)]) + model.settings.particles = 1000 + model.settings.batches = 10 + model.settings.run_mode = 'fixed source' + + mesh = openmc.RegularMesh() + mesh.lower_left = (-1, -2, -3) + mesh.upper_right = (2, 3, 4) + mesh.dimension = (1, 1, 1) + + mesh_source_arr = np.asarray([file_source]).reshape(mesh.dimension) + source = openmc.MeshSource(mesh, mesh_source_arr) + + model.settings.source = source + + model.export_to_model_xml() + + openmc.lib.init() + openmc.lib.simulation_init() + sites = openmc.lib.sample_external_source(10) + openmc.lib.simulation_finalize() + openmc.lib.finalize() + + # The mesh bounds do not contain the point of the lone source site in the + # file source, so it should not appear in the set of source sites produced + # from the mesh source. Additionally, the source should be located within + # the mesh + bbox = mesh.bounding_box + for site in sites: + assert site.r != (0, 0, 0) + assert site.E == source_particle.E + assert site.u == source_particle.u + assert site.time == source_particle.time + assert site.r in bbox From ec8850deac2046668ab087f1f5f14a9df4614008 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Mon, 4 Dec 2023 12:08:47 -0600 Subject: [PATCH 014/671] OpenMPMutex "Copying" (#2794) --- include/openmc/openmp_interface.h | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/include/openmc/openmp_interface.h b/include/openmc/openmp_interface.h index dac03ac59..b489df0c9 100644 --- a/include/openmc/openmp_interface.h +++ b/include/openmc/openmp_interface.h @@ -29,11 +29,26 @@ public: #endif } - // Mutexes cannot be copied. We need to explicitly delete the copy - // constructor and copy assignment operator to ensure the compiler doesn't - // "help" us by implicitly trying to copy the underlying mutexes. - OpenMPMutex(const OpenMPMutex&) = delete; - OpenMPMutex& operator=(const OpenMPMutex&) = delete; + // omp_lock_t objects cannot be deep copied, they can only be shallow + // copied. Thus, while shallow copying of an omp_lock_t object is + // completely valid (provided no race conditions exist), true copying + // of an OpenMPMutex object is not valid due to the action of the + // destructor. However, since locks are fungible, we can simply replace + // copying operations with default construction. This allows storage of + // OpenMPMutex objects within containers that may need to move/copy them + // (e.g., std::vector). It is left to the caller to understand that + // copying of OpenMPMutex does not produce two handles to the same mutex, + // rather, it produces two different mutexes. + + // Copy constructor + OpenMPMutex(const OpenMPMutex& other) { OpenMPMutex(); } + + // Copy assignment operator + OpenMPMutex& operator=(const OpenMPMutex& other) + { + OpenMPMutex(); + return *this; + } //! Lock the mutex. // From 3901709141f6e1e85707fa1ce901199df081cc29 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 6 Dec 2023 12:35:10 -0600 Subject: [PATCH 015/671] Do not link against several transitive dependencies of HDF5 (#2797) --- CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7269f4e63..bf3555d84 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -155,6 +155,11 @@ if(NOT DEFINED HDF5_PREFER_PARALLEL) endif() find_package(HDF5 REQUIRED COMPONENTS C HL) + +# Remove HDF5 transitive dependencies that are system libraries +list(FILTER HDF5_LIBRARIES EXCLUDE REGEX ".*lib(pthread|dl|m).*") +message(STATUS "HDF5 Libraries: ${HDF5_LIBRARIES}") + if(HDF5_IS_PARALLEL) if(NOT OPENMC_USE_MPI) message(FATAL_ERROR "Parallel HDF5 was detected, but MPI was not enabled.\ From fe07c54bc6eca81cb92e81db2add78f30c2d35a7 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Tue, 12 Dec 2023 13:26:18 -0600 Subject: [PATCH 016/671] Pytest Update Documentation (#2801) --- docs/source/devguide/tests.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/source/devguide/tests.rst b/docs/source/devguide/tests.rst index 9a102d2dc..d8fc53b3a 100644 --- a/docs/source/devguide/tests.rst +++ b/docs/source/devguide/tests.rst @@ -134,6 +134,12 @@ following files to your new test directory: compiler options during openmc configuration and build (e.g., no MPI, no debug/optimization). +For tests using the Python API, both the **inputs_true.dat** and +**results_true.dat** files can be generated automatically in the correct format +via:: + + pytest --update + In addition to this description, please see the various types of tests that are already included in the test suite to see how to create them. If all is implemented correctly, the new test will automatically be discovered by pytest. From a833c176ab91622bf03789d2435f95bff82c01b0 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Tue, 12 Dec 2023 19:29:43 +0000 Subject: [PATCH 017/671] Adding get cylindrical mesh index at specified coordinates (#2782) Co-authored-by: Paul Romano --- openmc/mesh.py | 62 ++++++++++++++++++++++++++++++++++- tests/unit_tests/test_mesh.py | 48 +++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/openmc/mesh.py b/openmc/mesh.py index a14cec5dc..560d54042 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -2,7 +2,7 @@ import typing import warnings from abc import ABC, abstractmethod from collections.abc import Iterable -from math import pi +from math import pi, sqrt, atan2 from numbers import Integral, Real from pathlib import Path from typing import Optional, Sequence, Tuple @@ -1347,6 +1347,66 @@ class CylindricalMesh(StructuredMesh): string += fmt.format('\tZ Max:', '=\t', self._z_grid[-1]) return string + def get_indices_at_coords( + self, + coords: Sequence[float] + ) -> Tuple[int, int, int]: + """Finds the index of the mesh voxel at the specified x,y,z coordinates. + + Parameters + ---------- + coords : Sequence[float] + The x, y, z axis coordinates + + Returns + ------- + Tuple[int, int, int] + The r, phi, z indices + + """ + r_value_from_origin = sqrt((coords[0]-self.origin[0])**2 + (coords[1]-self.origin[1])**2) + + if r_value_from_origin < self.r_grid[0] or r_value_from_origin > self.r_grid[-1]: + raise ValueError( + f'The specified x, y ({coords[0]}, {coords[1]}) combine to give an r value of ' + f'{r_value_from_origin} from the origin of {self.origin}.which ' + f'is outside the origin absolute r grid values {self.r_grid}.' + ) + + r_index = np.searchsorted(self.r_grid, r_value_from_origin) - 1 + + z_grid_values = np.array(self.z_grid) + self.origin[2] + + if coords[2] < z_grid_values[0] or coords[2] > z_grid_values[-1]: + raise ValueError( + f'The specified z value ({coords[2]}) from the z origin of ' + f'{self.origin[-1]} is outside of the absolute z grid range {z_grid_values}.' + ) + + z_index = np.argmax(z_grid_values > coords[2]) - 1 + + delta_x = coords[0] - self.origin[0] + delta_y = coords[1] - self.origin[1] + # atan2 returns values in -pi to +pi range + phi_value = atan2(delta_y, delta_x) + if delta_x < 0 and delta_y < 0: + # returned phi_value anticlockwise and negative + phi_value += 2 * pi + if delta_x > 0 and delta_y < 0: + # returned phi_value anticlockwise and negative + phi_value += 2 * pi + + phi_grid_values = np.array(self.phi_grid) + + if phi_value < phi_grid_values[0] or phi_value > phi_grid_values[-1]: + raise ValueError( + f'The phi value ({phi_value}) resulting from the specified x, y ' + f'values is outside of the absolute phi grid range {phi_grid_values}.' + ) + phi_index = np.argmax(phi_grid_values > phi_value) - 1 + + return (r_index, phi_index, z_index) + @classmethod def from_hdf5(cls, group: h5py.Group): mesh_id = int(group.name.split('/')[-1].lstrip('mesh ')) diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index 06d543610..2a4ff6cbb 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -77,6 +77,7 @@ def test_cylindrical_mesh_bounding_box(): np.testing.assert_array_equal(mesh.lower_left, (2, 4, -3)) np.testing.assert_array_equal(mesh.upper_right, (4, 6, 17)) + def test_spherical_mesh_bounding_box(): # test with mesh at origin (0, 0, 0) mesh = openmc.SphericalMesh([0.1, 0.2, 0.5, 1.], origin=(0., 0., 0.)) @@ -239,4 +240,51 @@ def test_mesh_vertices(mesh_type): np.testing.assert_equal(mesh.vertices_spherical[ijk], exp_vert) +def test_CylindricalMesh_get_indices_at_coords(): + # default origin (0, 0, 0) and default phi grid (0, 2*pi) + mesh = openmc.CylindricalMesh(r_grid=(0, 5, 10), z_grid=(0, 5, 10)) + assert mesh.get_indices_at_coords([1, 0, 1]) == (0, 0, 0) + assert mesh.get_indices_at_coords([6, 0, 1]) == (1, 0, 0) + assert mesh.get_indices_at_coords([9, 0, 1]) == (1, 0, 0) + assert mesh.get_indices_at_coords([0, 6, 0]) == (1, 0, 0) + assert mesh.get_indices_at_coords([0, 9, 6]) == (1, 0, 1) + assert mesh.get_indices_at_coords([-2, -2, 9]) == (0, 0, 1) + with pytest.raises(ValueError): + assert mesh.get_indices_at_coords([8, 8, 1]) # resulting r value to large + with pytest.raises(ValueError): + assert mesh.get_indices_at_coords([-8, -8, 1]) # resulting r value to large + with pytest.raises(ValueError): + assert mesh.get_indices_at_coords([1, 0, -1]) # z value below range + with pytest.raises(ValueError): + assert mesh.get_indices_at_coords([1, 0, 11]) # z value above range + + assert mesh.get_indices_at_coords([1, 1, 1]) == (0, 0, 0) + + # negative range on z grid + mesh = openmc.CylindricalMesh( + r_grid=(0, 5, 10), + phi_grid=(0, 0.5 * pi, pi, 1.5 * pi, 1.9 * pi), + z_grid=(-5, 0, 5, 10), + ) + assert mesh.get_indices_at_coords([1, 1, 1]) == (0, 0, 1) # first angle quadrant + assert mesh.get_indices_at_coords([2, 2, 6]) == (0, 0, 2) # first angle quadrant + assert mesh.get_indices_at_coords([-2, 0.1, -1]) == (0, 1, 0) # second angle quadrant + assert mesh.get_indices_at_coords([-2, -0.1, -1]) == (0, 2, 0) # third angle quadrant + assert mesh.get_indices_at_coords([2, -0.9, -1]) == (0, 3, 0) # forth angle quadrant + + with pytest.raises(ValueError): + assert mesh.get_indices_at_coords([2, -0.1, 1]) # outside of phi range + + # origin of mesh not default + mesh = openmc.CylindricalMesh( + r_grid=(0, 5, 10), + phi_grid=(0, 0.5 * pi, pi, 1.5 * pi, 1.9 * pi), + z_grid=(-5, 0, 5, 10), + origin=(100, 200, 300), + ) + assert mesh.get_indices_at_coords([101, 201, 301]) == (0, 0, 1) # first angle quadrant + assert mesh.get_indices_at_coords([102, 202, 306]) == (0, 0, 2) # first angle quadrant + assert mesh.get_indices_at_coords([98, 200.1, 299]) == (0, 1, 0) # second angle quadrant + assert mesh.get_indices_at_coords([98, 199.9, 299]) == (0, 2, 0) # third angle quadrant + assert mesh.get_indices_at_coords([102, 199.1, 299]) == (0, 3, 0) # forth angle quadrant From 124e62fc58e7884123f1fdf7de7f4cacf6210195 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Tue, 12 Dec 2023 19:32:09 +0000 Subject: [PATCH 018/671] added path arg to integrator.integrate (#2784) --- openmc/deplete/abc.py | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 5d906efd8..dd4df21fb 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -18,7 +18,7 @@ from warnings import warn from numpy import nonzero, empty, asarray from uncertainties import ufloat -from openmc.checkvalue import check_type, check_greater_than +from openmc.checkvalue import check_type, check_greater_than, PathLike from openmc.mpi import comm from .stepresult import StepResult from .chain import Chain @@ -762,7 +762,12 @@ class Integrator(ABC): return (self.operator.prev_res[-1].time[-1], len(self.operator.prev_res) - 1) - def integrate(self, final_step=True, output=True): + def integrate( + self, + final_step: bool = True, + output: bool = True, + path: PathLike = 'depletion_results.h5' + ): """Perform the entire depletion process across all steps Parameters @@ -776,6 +781,10 @@ class Integrator(ABC): Indicate whether to display information about progress .. versionadded:: 0.13.1 + path : PathLike + Path to file to write. Defaults to 'depletion_results.h5'. + + .. versionadded:: 0.14.1 """ with change_directory(self.operator.output_dir): n = self.operator.initial_condition() @@ -802,7 +811,7 @@ class Integrator(ABC): n = n_list.pop() StepResult.save(self.operator, n_list, res_list, [t, t + dt], - source_rate, self._i_res + i, proc_time) + source_rate, self._i_res + i, proc_time, path) t += dt @@ -954,15 +963,21 @@ class SIIntegrator(Integrator): self.operator.settings.particles //= self.n_steps return inherited - def integrate(self, output=True): + def integrate( + self, + output: bool = True, + path: PathLike = "depletion_results.h5" + ): """Perform the entire depletion process across all steps Parameters ---------- output : bool, optional Indicate whether to display information about progress + path : PathLike + Path to file to write. Defaults to 'depletion_results.h5'. - .. versionadded:: 0.13.1 + .. versionadded:: 0.14.1 """ with change_directory(self.operator.output_dir): n = self.operator.initial_condition() @@ -992,13 +1007,13 @@ class SIIntegrator(Integrator): n = n_list.pop() StepResult.save(self.operator, n_list, res_list, [t, t + dt], - p, self._i_res + i, proc_time) + p, self._i_res + i, proc_time, path) t += dt # No final simulation for SIE, use last iteration results StepResult.save(self.operator, [n], [res_list[-1]], [t, t], - p, self._i_res + len(self), proc_time) + p, self._i_res + len(self), proc_time, path) self.operator.write_bos_data(self._i_res + len(self)) self.operator.finalize() From 15a2199c0d8496282dce1f88b41a7dab1155b897 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Tue, 12 Dec 2023 22:58:17 +0000 Subject: [PATCH 019/671] adding get_all_nuclides method to geometry class (#2796) Co-authored-by: Paul Romano --- openmc/geometry.py | 14 ++++++++++++++ tests/unit_tests/test_geometry.py | 12 ++++++++++++ 2 files changed, 26 insertions(+) diff --git a/openmc/geometry.py b/openmc/geometry.py index fb2488f78..60f4a390d 100644 --- a/openmc/geometry.py +++ b/openmc/geometry.py @@ -391,6 +391,20 @@ class Geometry: universes.update(self.root_universe.get_all_universes()) return universes + def get_all_nuclides(self) -> typing.List[str]: + """Return all nuclides within the geometry. + + Returns + ------- + list + Sorted list of all nuclides in materials appearing in the geometry + + """ + all_nuclides = set() + for material in self.get_all_materials().values(): + all_nuclides |= set(material.get_nuclides()) + return sorted(all_nuclides) + def get_all_materials(self) -> typing.Dict[int, openmc.Material]: """Return all materials within the geometry. diff --git a/tests/unit_tests/test_geometry.py b/tests/unit_tests/test_geometry.py index ff94bc932..f5a1bd7db 100644 --- a/tests/unit_tests/test_geometry.py +++ b/tests/unit_tests/test_geometry.py @@ -378,3 +378,15 @@ def test_remove_redundant_surfaces(): # There should be 0 remaining redundant surfaces n_redundant_surfs = len(geom.remove_redundant_surfaces().keys()) assert n_redundant_surfs == 0 + +def test_get_all_nuclides(): + m1 = openmc.Material() + m1.add_nuclide('Fe56', 1) + m1.add_nuclide('Be9', 1) + m2 = openmc.Material() + m2.add_nuclide('Be9', 1) + s = openmc.Sphere() + c1 = openmc.Cell(fill=m1, region=-s) + c2 = openmc.Cell(fill=m2, region=+s) + geom = openmc.Geometry([c1, c2]) + assert geom.get_all_nuclides() == ['Be9', 'Fe56'] From 552adc005cd46d737ace131d020852519898eb56 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Tue, 12 Dec 2023 23:37:53 +0000 Subject: [PATCH 020/671] added check to length of input args for IndependantOperator (#2799) Co-authored-by: Paul Romano --- openmc/deplete/independent_operator.py | 8 ++++++++ .../test_deplete_independent_operator.py | 18 +++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/openmc/deplete/independent_operator.py b/openmc/deplete/independent_operator.py index b45c8ad75..5469b28c7 100644 --- a/openmc/deplete/independent_operator.py +++ b/openmc/deplete/independent_operator.py @@ -130,6 +130,14 @@ class IndependentOperator(OpenMCOperator): # Validate micro-xs parameters check_type('materials', materials, openmc.Materials) check_type('micros', micros, Iterable, MicroXS) + check_type('fluxes', fluxes, Iterable, float) + + if not (len(fluxes) == len(micros) == len(materials)): + msg = (f'The length of fluxes ({len(fluxes)}) should be equal to ' + f'the length of micros ({len(micros)}) and the length of ' + f'materials ({len(materials)}).') + raise ValueError(msg) + if keff is not None: check_type('keff', keff, tuple, float) keff = ufloat(*keff) diff --git a/tests/unit_tests/test_deplete_independent_operator.py b/tests/unit_tests/test_deplete_independent_operator.py index 813ac06de..9129cf064 100644 --- a/tests/unit_tests/test_deplete_independent_operator.py +++ b/tests/unit_tests/test_deplete_independent_operator.py @@ -4,8 +4,10 @@ from pathlib import Path -from openmc.deplete import IndependentOperator, MicroXS +import pytest + from openmc import Material, Materials +from openmc.deplete import IndependentOperator, MicroXS CHAIN_PATH = Path(__file__).parents[1] / "chain_simple.xml" ONE_GROUP_XS = Path(__file__).parents[1] / "micro_xs_simple.csv" @@ -36,3 +38,17 @@ def test_operator_init(): fluxes = [1.0] micros = [micro_xs] IndependentOperator(materials, fluxes, micros, CHAIN_PATH) + + +def test_error_handling(): + micro_xs = MicroXS.from_csv(ONE_GROUP_XS) + fuel = Material(name="oxygen") + fuel.add_element("O", 2) + fuel.set_density("g/cc", 1) + fuel.depletable = True + fuel.volume = 1 + materials = Materials([fuel]) + fluxes = [1.0, 2.0] + micros = [micro_xs] + with pytest.raises(ValueError, match=r"The length of fluxes \(2\)"): + IndependentOperator(materials, fluxes, micros, CHAIN_PATH) From 85c963e223d7cf376ef5b888ce1225b241a635b5 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 13 Dec 2023 08:11:32 -0600 Subject: [PATCH 021/671] Move 'import lxml' to third-party block of imports (#2803) --- openmc/cell.py | 2 +- openmc/data/library.py | 2 +- openmc/deplete/nuclide.py | 2 +- openmc/element.py | 1 + openmc/filter.py | 2 +- openmc/filter_expansion.py | 1 + openmc/lattice.py | 2 +- openmc/material.py | 2 +- openmc/model/model.py | 2 +- openmc/plots.py | 2 +- openmc/settings.py | 3 +-- openmc/source.py | 2 +- openmc/surface.py | 2 +- openmc/tally_derivative.py | 1 + openmc/trigger.py | 1 + openmc/universe.py | 6 +----- openmc/volume.py | 4 ++-- openmc/weight_windows.py | 1 - tests/unit_tests/test_cell.py | 2 -- tests/unit_tests/test_deplete_nuclide.py | 3 ++- tests/unit_tests/test_lattice.py | 2 +- tests/unit_tests/test_universe.py | 1 - 22 files changed, 21 insertions(+), 25 deletions(-) diff --git a/openmc/cell.py b/openmc/cell.py index 956d727d7..0af6ea2b4 100644 --- a/openmc/cell.py +++ b/openmc/cell.py @@ -1,8 +1,8 @@ from collections.abc import Iterable from math import cos, sin, pi from numbers import Real -import lxml.etree as ET +import lxml.etree as ET import numpy as np from uncertainties import UFloat diff --git a/openmc/data/library.py b/openmc/data/library.py index 397be3faf..de9de2744 100644 --- a/openmc/data/library.py +++ b/openmc/data/library.py @@ -1,8 +1,8 @@ import os -import lxml.etree as ET import pathlib import h5py +import lxml.etree as ET import openmc from openmc._xml import clean_indentation, reorder_attributes diff --git a/openmc/deplete/nuclide.py b/openmc/deplete/nuclide.py index e2067a8e3..d055c2e37 100644 --- a/openmc/deplete/nuclide.py +++ b/openmc/deplete/nuclide.py @@ -8,8 +8,8 @@ from collections.abc import Mapping from collections import namedtuple, defaultdict from warnings import warn from numbers import Real -import lxml.etree as ET +import lxml.etree as ET import numpy as np from openmc.checkvalue import check_type diff --git a/openmc/element.py b/openmc/element.py index 01651bebb..f9cf102f7 100644 --- a/openmc/element.py +++ b/openmc/element.py @@ -1,4 +1,5 @@ import re + import lxml.etree as ET import openmc.checkvalue as cv diff --git a/openmc/filter.py b/openmc/filter.py index 8eb81432f..f2bbf2f50 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -3,9 +3,9 @@ from collections.abc import Iterable import hashlib from itertools import product from numbers import Real, Integral -import lxml.etree as ET import warnings +import lxml.etree as ET import numpy as np import pandas as pd diff --git a/openmc/filter_expansion.py b/openmc/filter_expansion.py index f05f39fe9..b9860846f 100644 --- a/openmc/filter_expansion.py +++ b/openmc/filter_expansion.py @@ -1,4 +1,5 @@ from numbers import Integral, Real + import lxml.etree as ET import openmc.checkvalue as cv diff --git a/openmc/lattice.py b/openmc/lattice.py index 926a016ef..d281f6e76 100644 --- a/openmc/lattice.py +++ b/openmc/lattice.py @@ -4,8 +4,8 @@ from copy import deepcopy from math import sqrt, floor from numbers import Real import types -import lxml.etree as ET +import lxml.etree as ET import numpy as np import openmc diff --git a/openmc/material.py b/openmc/material.py index 6b71f56a8..1f5496f15 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -8,8 +8,8 @@ import re import typing # imported separately as py3.8 requires typing.Iterable import warnings from typing import Optional, List, Union, Dict -import lxml.etree as ET +import lxml.etree as ET import numpy as np import h5py diff --git a/openmc/model/model.py b/openmc/model/model.py index 9da834e13..a60960b2b 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -7,10 +7,10 @@ from pathlib import Path from numbers import Integral from tempfile import NamedTemporaryFile import warnings -import lxml.etree as ET from typing import Optional, Dict import h5py +import lxml.etree as ET import openmc import openmc._xml as xml diff --git a/openmc/plots.py b/openmc/plots.py index 626056d79..b38b51a59 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -1,10 +1,10 @@ from collections.abc import Iterable, Mapping from numbers import Integral, Real from pathlib import Path -import lxml.etree as ET from typing import Optional import h5py +import lxml.etree as ET import numpy as np import openmc diff --git a/openmc/settings.py b/openmc/settings.py index 5d0800ac1..b4e615a2a 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -1,4 +1,3 @@ -import os from collections.abc import Iterable, Mapping, MutableSequence from enum import Enum import itertools @@ -7,11 +6,11 @@ from numbers import Integral, Real from pathlib import Path import typing # required to prevent typing.Union namespace overwriting Union from typing import Optional + import lxml.etree as ET import openmc.checkvalue as cv from openmc.stats.multivariate import MeshSpatial - from . import (RegularMesh, SourceBase, MeshSource, IndependentSource, VolumeCalculation, WeightWindows, WeightWindowGenerator) from ._xml import clean_indentation, get_text, reorder_attributes diff --git a/openmc/source.py b/openmc/source.py index c339d6ed8..1ba90c921 100644 --- a/openmc/source.py +++ b/openmc/source.py @@ -7,8 +7,8 @@ import warnings import typing # imported separately as py3.8 requires typing.Iterable # also required to prevent typing.Union namespace overwriting Union from typing import Optional, Sequence -import lxml.etree as ET +import lxml.etree as ET import numpy as np import h5py diff --git a/openmc/surface.py b/openmc/surface.py index a684f83fe..a16b6712d 100644 --- a/openmc/surface.py +++ b/openmc/surface.py @@ -3,9 +3,9 @@ from collections.abc import Iterable from copy import deepcopy import math from numbers import Real -import lxml.etree as ET from warnings import warn, catch_warnings, simplefilter +import lxml.etree as ET import numpy as np from .checkvalue import check_type, check_value, check_length, check_greater_than diff --git a/openmc/tally_derivative.py b/openmc/tally_derivative.py index 779ca619f..ff918c1b3 100644 --- a/openmc/tally_derivative.py +++ b/openmc/tally_derivative.py @@ -1,4 +1,5 @@ from numbers import Integral + import lxml.etree as ET import openmc.checkvalue as cv diff --git a/openmc/trigger.py b/openmc/trigger.py index 8a165526c..c7d9e9240 100644 --- a/openmc/trigger.py +++ b/openmc/trigger.py @@ -1,5 +1,6 @@ from collections.abc import Iterable from numbers import Real + import lxml.etree as ET import openmc.checkvalue as cv diff --git a/openmc/universe.py b/openmc/universe.py index bc122b03d..2e86ab05a 100644 --- a/openmc/universe.py +++ b/openmc/universe.py @@ -1,24 +1,20 @@ import math -import typing from abc import ABC, abstractmethod from collections.abc import Iterable -from copy import deepcopy from numbers import Integral, Real from pathlib import Path from tempfile import TemporaryDirectory -import lxml.etree as ET import warnings import h5py +import lxml.etree as ET import numpy as np import openmc import openmc.checkvalue as cv - from ._xml import get_text from .checkvalue import check_type, check_value from .mixin import IDManagerMixin -from .plots import _SVG_COLORS from .surface import _BOUNDARY_TYPES diff --git a/openmc/volume.py b/openmc/volume.py index 90882267d..df19def1e 100644 --- a/openmc/volume.py +++ b/openmc/volume.py @@ -1,11 +1,11 @@ from collections.abc import Iterable, Mapping from numbers import Real, Integral -import lxml.etree as ET import warnings +import h5py +import lxml.etree as ET import numpy as np import pandas as pd -import h5py from uncertainties import ufloat import openmc diff --git a/openmc/weight_windows.py b/openmc/weight_windows.py index 2b545a976..f59364389 100644 --- a/openmc/weight_windows.py +++ b/openmc/weight_windows.py @@ -12,7 +12,6 @@ from openmc.filter import _PARTICLES from openmc.mesh import MeshBase, RectilinearMesh, CylindricalMesh, SphericalMesh, UnstructuredMesh import openmc.checkvalue as cv from openmc.checkvalue import PathLike - from ._xml import get_text, clean_indentation from .mixin import IDManagerMixin diff --git a/tests/unit_tests/test_cell.py b/tests/unit_tests/test_cell.py index 888a0ef88..95c8249bb 100644 --- a/tests/unit_tests/test_cell.py +++ b/tests/unit_tests/test_cell.py @@ -1,11 +1,9 @@ import lxml.etree as ET - import numpy as np from uncertainties import ufloat import openmc import pytest - from tests.unit_tests import assert_unbounded from openmc.data import atomic_mass, AVOGADRO diff --git a/tests/unit_tests/test_deplete_nuclide.py b/tests/unit_tests/test_deplete_nuclide.py index 07db57bd6..6482d76a5 100644 --- a/tests/unit_tests/test_deplete_nuclide.py +++ b/tests/unit_tests/test_deplete_nuclide.py @@ -1,7 +1,8 @@ """Tests for the openmc.deplete.Nuclide class.""" -import lxml.etree as ET import copy + +import lxml.etree as ET import numpy as np import pytest from openmc.deplete import nuclide diff --git a/tests/unit_tests/test_lattice.py b/tests/unit_tests/test_lattice.py index 97cd89843..6fa32760e 100644 --- a/tests/unit_tests/test_lattice.py +++ b/tests/unit_tests/test_lattice.py @@ -1,6 +1,6 @@ from math import sqrt -import lxml.etree as ET +import lxml.etree as ET import openmc import pytest diff --git a/tests/unit_tests/test_universe.py b/tests/unit_tests/test_universe.py index 630e66df3..46d4ec3f7 100644 --- a/tests/unit_tests/test_universe.py +++ b/tests/unit_tests/test_universe.py @@ -1,5 +1,4 @@ import lxml.etree as ET - import numpy as np import openmc import pytest From 3efd24289ddf02746e5aa3154fe922bdf440df38 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 13 Dec 2023 08:41:51 -0600 Subject: [PATCH 022/671] Fix creation of meshes when from loading settings from XML (#2805) --- openmc/mesh.py | 2 +- openmc/settings.py | 35 +++++++++++++++-------------------- openmc/stats/multivariate.py | 2 +- openmc/weight_windows.py | 13 ++++++------- 4 files changed, 23 insertions(+), 29 deletions(-) diff --git a/openmc/mesh.py b/openmc/mesh.py index 560d54042..f3cd69a8f 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -2316,7 +2316,7 @@ def _read_meshes(elem): A dictionary with mesh IDs as keys and openmc.MeshBase instanaces as values """ - out = dict() + out = {} for mesh_elem in elem.findall('mesh'): mesh = MeshBase.from_xml_element(mesh_elem) out[mesh.id] = mesh diff --git a/openmc/settings.py b/openmc/settings.py index b4e615a2a..6486d1f2a 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -1603,15 +1603,14 @@ class Settings: if value is not None: self.cutoff[key] = float(value) - def _entropy_mesh_from_xml_element(self, root, meshes=None): + def _entropy_mesh_from_xml_element(self, root, meshes): text = get_text(root, 'entropy_mesh') - if text is not None: - path = f"./mesh[@id='{int(text)}']" - elem = root.find(path) - if elem is not None: - self.entropy_mesh = RegularMesh.from_xml_element(elem) - if meshes is not None and self.entropy_mesh is not None: - meshes[self.entropy_mesh.id] = self.entropy_mesh + if text is None: + return + mesh_id = int(text) + if mesh_id not in meshes: + raise ValueError(f'Could not locate mesh with ID "{mesh_id}"') + self.entropy_mesh = meshes[mesh_id] def _trigger_from_xml_element(self, root): elem = root.find('trigger') @@ -1671,15 +1670,14 @@ class Settings: values = [int(x) for x in text.split()] self.track = list(zip(values[::3], values[1::3], values[2::3])) - def _ufs_mesh_from_xml_element(self, root, meshes=None): + def _ufs_mesh_from_xml_element(self, root, meshes): text = get_text(root, 'ufs_mesh') - if text is not None: - path = f"./mesh[@id='{int(text)}']" - elem = root.find(path) - if elem is not None: - self.ufs_mesh = RegularMesh.from_xml_element(elem) - if meshes is not None and self.ufs_mesh is not None: - meshes[self.ufs_mesh.id] = self.ufs_mesh + if text is None: + return + mesh_id = int(text) + if mesh_id not in meshes: + raise ValueError(f'Could not locate mesh with ID "{mesh_id}"') + self.ufs_mesh = meshes[mesh_id] def _resonance_scattering_from_xml_element(self, root): elem = root.find('resonance_scattering') @@ -1743,16 +1741,13 @@ class Settings: def _weight_windows_from_xml_element(self, root, meshes=None): for elem in root.findall('weight_windows'): - ww = WeightWindows.from_xml_element(elem, root) + ww = WeightWindows.from_xml_element(elem, meshes) self.weight_windows.append(ww) text = get_text(root, 'weight_windows_on') if text is not None: self.weight_windows_on = text in ('true', '1') - if meshes is not None and self.weight_windows: - meshes.update({ww.mesh.id: ww.mesh for ww in self.weight_windows}) - def _weight_window_checkpoints_from_xml_element(self, root): elem = root.find('weight_window_checkpoints') if elem is None: diff --git a/openmc/stats/multivariate.py b/openmc/stats/multivariate.py index 22c3d7ea8..3922d601a 100644 --- a/openmc/stats/multivariate.py +++ b/openmc/stats/multivariate.py @@ -744,7 +744,7 @@ class MeshSpatial(Spatial): # check if this mesh has been read in from another location already if mesh_id not in meshes: - raise RuntimeError(f'Could not locate mesh with ID "{mesh_id}"') + raise ValueError(f'Could not locate mesh with ID "{mesh_id}"') volume_normalized = elem.get("volume_normalized") volume_normalized = get_text(elem, 'volume_normalized').lower() == 'true' diff --git a/openmc/weight_windows.py b/openmc/weight_windows.py index f59364389..96f1db892 100644 --- a/openmc/weight_windows.py +++ b/openmc/weight_windows.py @@ -353,15 +353,15 @@ class WeightWindows(IDManagerMixin): return element @classmethod - def from_xml_element(cls, elem: ET.Element, root: ET.Element) -> WeightWindows: + def from_xml_element(cls, elem: ET.Element, meshes: Dict[int, MeshBase]) -> WeightWindows: """Generate weight window settings from an XML element Parameters ---------- elem : lxml.etree._Element XML element - root : lxml.etree._Element - Root element for the file where meshes can be found + meshes : dict + Dictionary mapping IDs to mesh objects Returns ------- @@ -370,10 +370,9 @@ class WeightWindows(IDManagerMixin): """ # Get mesh for weight windows mesh_id = int(get_text(elem, 'mesh')) - path = f"./mesh[@id='{mesh_id}']" - mesh_elem = root.find(path) - if mesh_elem is not None: - mesh = MeshBase.from_xml_element(mesh_elem) + if mesh_id not in meshes: + raise ValueError(f'Could not locate mesh with ID "{mesh_id}"') + mesh = meshes[mesh_id] # Read all other parameters lower_ww_bounds = [float(l) for l in get_text(elem, 'lower_ww_bounds').split()] From fb65dfd53cf8de6a360e23234bcc77f6a1d1f3c2 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 14 Dec 2023 10:49:59 -0600 Subject: [PATCH 023/671] Avoid high memory use when writing unstructured mesh VTK files (#2806) --- openmc/mesh.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openmc/mesh.py b/openmc/mesh.py index f3cd69a8f..435a44d2c 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -2181,7 +2181,6 @@ class UnstructuredMesh(MeshBase): grid.SetPoints(vtk_pnts) n_skipped = 0 - elems = [] for elem_type, conn in zip(self.element_types, self.connectivity): if elem_type == self._LINEAR_TET: elem = vtk.vtkTetra() @@ -2195,15 +2194,13 @@ class UnstructuredMesh(MeshBase): if c == -1: break elem.GetPointIds().SetId(i, c) - elems.append(elem) + + grid.InsertNextCell(elem.GetCellType(), elem.GetPointIds()) if n_skipped > 0: warnings.warn(f'{n_skipped} elements were not written because ' 'they are not of type linear tet/hex') - for elem in elems: - grid.InsertNextCell(elem.GetCellType(), elem.GetPointIds()) - # check that datasets are the correct size datasets_out = [] if datasets is not None: From 53363da3ccb7af3c46820fa17558ed0d040d0c6a Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Mon, 18 Dec 2023 21:31:29 -0600 Subject: [PATCH 024/671] Consolidating number of threads and thread number queries into the openmp interface header (#2809) --- include/openmc/openmp_interface.h | 21 +++++++++++++++++++++ src/initialize.cpp | 9 ++------- src/mesh.cpp | 21 ++++----------------- src/plot.cpp | 17 ++++------------- src/volume_calc.cpp | 13 +++---------- 5 files changed, 34 insertions(+), 47 deletions(-) diff --git a/include/openmc/openmp_interface.h b/include/openmc/openmp_interface.h index b489df0c9..fe517415b 100644 --- a/include/openmc/openmp_interface.h +++ b/include/openmc/openmp_interface.h @@ -7,6 +7,27 @@ namespace openmc { +//============================================================================== +//! Accessor functions related to number of threads and thread number +//============================================================================== +inline int num_threads() +{ +#ifdef _OPENMP + return omp_get_max_threads(); +#else + return 1; +#endif +} + +inline int thread_num() +{ +#ifdef _OPENMP + return omp_get_thread_num(); +#else + return 0; +#endif +} + //============================================================================== //! An object used to prevent concurrent access to a piece of data. // diff --git a/src/initialize.cpp b/src/initialize.cpp index cafcd5828..9e3c88603 100644 --- a/src/initialize.cpp +++ b/src/initialize.cpp @@ -23,6 +23,7 @@ #include "openmc/message_passing.h" #include "openmc/mgxs_interface.h" #include "openmc/nuclide.h" +#include "openmc/openmp_interface.h" #include "openmc/output.h" #include "openmc/plot.h" #include "openmc/random_lcg.h" @@ -63,13 +64,7 @@ int openmc_init(int argc, char* argv[], const void* intracomm) return err; #ifdef LIBMESH - -#ifdef _OPENMP - int n_threads = omp_get_max_threads(); -#else - int n_threads = 1; -#endif - + const int n_threads = num_threads(); // initialize libMesh if it hasn't been initialized already // (if initialized externally, the libmesh_init object needs to be provided // also) diff --git a/src/mesh.cpp b/src/mesh.cpp index ea66552de..34510b614 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -8,9 +8,7 @@ #ifdef OPENMC_MPI #include "mpi.h" #endif -#ifdef _OPENMP -#include -#endif + #include "xtensor/xbuilder.hpp" #include "xtensor/xeval.hpp" #include "xtensor/xmath.hpp" @@ -27,6 +25,7 @@ #include "openmc/hdf5_interface.h" #include "openmc/memory.h" #include "openmc/message_passing.h" +#include "openmc/openmp_interface.h" #include "openmc/random_dist.h" #include "openmc/search.h" #include "openmc/settings.h" @@ -2715,13 +2714,7 @@ void LibMesh::initialize() libMesh::ExplicitSystem& eq_sys = equation_systems_->add_system(eq_system_name_); -#ifdef _OPENMP - int n_threads = omp_get_max_threads(); -#else - int n_threads = 1; -#endif - - for (int i = 0; i < n_threads; i++) { + for (int i = 0; i < num_threads(); i++) { pl_.emplace_back(m_->sub_point_locator()); pl_.back()->set_contains_point_tol(FP_COINCIDENT); pl_.back()->enable_out_of_mesh_mode(); @@ -2896,13 +2889,7 @@ int LibMesh::get_bin(Position r) const return -1; } -#ifdef _OPENMP - int thread_num = omp_get_thread_num(); -#else - int thread_num = 0; -#endif - - const auto& point_locator = pl_.at(thread_num); + const auto& point_locator = pl_.at(thread_num()); const auto elem_ptr = (*point_locator)(p); return elem_ptr ? get_bin_from_element(elem_ptr) : -1; diff --git a/src/plot.cpp b/src/plot.cpp index e2f07c47c..8d995b0a9 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -23,6 +23,7 @@ #include "openmc/material.h" #include "openmc/mesh.h" #include "openmc/message_passing.h" +#include "openmc/openmp_interface.h" #include "openmc/output.h" #include "openmc/particle.h" #include "openmc/progress_bar.h" @@ -1218,11 +1219,7 @@ void ProjectionPlot::create_output() const * Note that a vector of vectors is required rather than a 2-tensor, * since the stack size varies within each column. */ -#ifdef _OPENMP - const int n_threads = omp_get_max_threads(); -#else - const int n_threads = 1; -#endif + const int n_threads = num_threads(); std::vector>> this_line_segments( n_threads); for (int t = 0; t < n_threads; ++t) { @@ -1234,14 +1231,8 @@ void ProjectionPlot::create_output() const #pragma omp parallel { - -#ifdef _OPENMP - const int n_threads = omp_get_max_threads(); - const int tid = omp_get_thread_num(); -#else - int n_threads = 1; - int tid = 0; -#endif + const int n_threads = num_threads(); + const int tid = thread_num(); SourceSite s; // Where particle starts from (camera) s.E = 1; diff --git a/src/volume_calc.cpp b/src/volume_calc.cpp index 8cd697a9d..859a0d5ac 100644 --- a/src/volume_calc.cpp +++ b/src/volume_calc.cpp @@ -10,18 +10,16 @@ #include "openmc/message_passing.h" #include "openmc/mgxs_interface.h" #include "openmc/nuclide.h" +#include "openmc/openmp_interface.h" #include "openmc/output.h" #include "openmc/random_lcg.h" #include "openmc/settings.h" #include "openmc/timer.h" #include "openmc/xml_interface.h" -#include -#ifdef _OPENMP -#include -#endif #include "xtensor/xadapt.hpp" #include "xtensor/xview.hpp" +#include #include // for copy #include // for pow, sqrt @@ -205,12 +203,7 @@ vector VolumeCalculation::execute() const // At this point, each thread has its own pair of index/hits lists and we // now need to reduce them. OpenMP is not nearly smart enough to do this // on its own, so we have to manually reduce them - -#ifdef _OPENMP - int n_threads = omp_get_num_threads(); -#else - int n_threads = 1; -#endif + const int n_threads = num_threads(); #pragma omp for ordered schedule(static) for (int i = 0; i < n_threads; ++i) { From f5900293fa9debff910301e1ac9304ab9aea4b3a Mon Sep 17 00:00:00 2001 From: Jin Whan Bae Date: Thu, 21 Dec 2023 22:21:14 -0500 Subject: [PATCH 025/671] microxs from mg flux and chain file (#2755) Co-authored-by: Jin Whan Bae Co-authored-by: shimwell Co-authored-by: Jonathan Shimwell Co-authored-by: Olek <45364492+yardasol@users.noreply.github.com> Co-authored-by: Paul Romano --- openmc/deplete/abc.py | 3 +- openmc/deplete/coupled_operator.py | 14 ++- openmc/deplete/microxs.py | 148 +++++++++++++++++++++-- openmc/lib/nuclide.py | 2 +- tests/unit_tests/test_deplete_microxs.py | 51 ++++++++ 5 files changed, 198 insertions(+), 20 deletions(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index dd4df21fb..60774871c 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -68,8 +68,9 @@ def change_directory(output_dir): Directory to switch to. """ orig_dir = os.getcwd() + output_dir = Path(output_dir) + output_dir.mkdir(parents=True, exist_ok=True) try: - output_dir.mkdir(parents=True, exist_ok=True) os.chdir(output_dir) yield finally: diff --git a/openmc/deplete/coupled_operator.py b/openmc/deplete/coupled_operator.py index 0d778c745..287a4e0d3 100644 --- a/openmc/deplete/coupled_operator.py +++ b/openmc/deplete/coupled_operator.py @@ -10,6 +10,7 @@ filesystem. import copy from warnings import warn +from typing import Optional import numpy as np from uncertainties import ufloat @@ -33,18 +34,19 @@ from .helpers import ( __all__ = ["CoupledOperator", "Operator", "OperatorResult"] -def _find_cross_sections(model): +def _find_cross_sections(model: Optional[str] = None): """Determine cross sections to use for depletion Parameters ---------- - model : openmc.model.Model + model : openmc.model.Model, optional Reactor model """ - if model.materials and model.materials.cross_sections is not None: - # Prefer info from Model class if available - return model.materials.cross_sections + if model: + if model.materials and model.materials.cross_sections is not None: + # Prefer info from Model class if available + return model.materials.cross_sections # otherwise fallback to environment variable cross_sections = openmc.config.get("cross_sections") @@ -67,7 +69,7 @@ def _get_nuclides_with_data(cross_sections): Returns ------- nuclides : set of str - Set of nuclide names that have cross secton data + Set of nuclide names that have cross section data """ nuclides = set() diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index e145bce87..0721535ca 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -5,8 +5,8 @@ IndependentOperator class for depletion. """ from __future__ import annotations -import tempfile -from typing import List, Tuple, Iterable, Optional, Union +from tempfile import TemporaryDirectory +from typing import List, Tuple, Iterable, Optional, Union, Sequence import pandas as pd import numpy as np @@ -14,14 +14,30 @@ import numpy as np from openmc.checkvalue import check_type, check_value, check_iterable_type, PathLike from openmc.exceptions import DataError from openmc import StatePoint +from openmc.mgxs import GROUP_STRUCTURES +from openmc.data import REACTION_MT import openmc +from .abc import change_directory from .chain import Chain, REACTIONS from .coupled_operator import _find_cross_sections, _get_nuclides_with_data +import openmc.lib _valid_rxns = list(REACTIONS) _valid_rxns.append('fission') +def _resolve_chain_file_path(chain_file: str): + # Determine what reactions and nuclides are available in chain + if chain_file is None: + chain_file = openmc.config.get('chain_file') + if 'chain_file' in openmc.config: + raise DataError( + "No depletion chain specified and could not find depletion " + "chain in openmc.config['chain_file']" + ) + return chain_file + + def get_microxs_and_flux( model: openmc.Model, domains, @@ -69,13 +85,7 @@ def get_microxs_and_flux( original_tallies = model.tallies # Determine what reactions and nuclides are available in chain - if chain_file is None: - chain_file = openmc.config.get('chain_file') - if chain_file is None: - raise DataError( - "No depletion chain specified and could not find depletion " - "chain in openmc.config['chain_file']" - ) + chain_file = _resolve_chain_file_path(chain_file) chain = Chain.from_xml(chain_file) if reactions is None: reactions = chain.reactions @@ -115,7 +125,7 @@ def get_microxs_and_flux( model.tallies = openmc.Tallies([rr_tally, flux_tally]) # create temporary run - with tempfile.TemporaryDirectory() as temp_dir: + with TemporaryDirectory() as temp_dir: if run_kwargs is None: run_kwargs = {} else: @@ -192,8 +202,122 @@ class MicroXS: self._index_nuc = {nuc: i for i, nuc in enumerate(nuclides)} self._index_rx = {rx: i for i, rx in enumerate(reactions)} - # TODO: Add a classmethod for generating MicroXS directly from cross section - # data using a known flux spectrum + @classmethod + def from_multigroup_flux( + cls, + energies: Union[Sequence[float], str], + multigroup_flux: Sequence[float], + chain_file: Optional[PathLike] = None, + temperature: float = 293.6, + nuclides: Optional[Sequence[str]] = None, + reactions: Optional[Sequence[str]] = None, + **init_kwargs: dict, + ) -> MicroXS: + """Generated microscopic cross sections from a known flux. + + The size of the MicroXS matrix depends on the chain file and cross + sections available. MicroXS entry will be 0 if the nuclide cross section + is not found. + + .. versionadded:: 0.14.1 + + Parameters + ---------- + energies : iterable of float or str + Energy group boundaries in [eV] or the name of the group structure + multi_group_flux : iterable of float + Energy-dependent multigroup flux values + chain_file : str, optional + Path to the depletion chain XML file that will be used in depletion + simulation. Defaults to ``openmc.config['chain_file']``. + temperature : int, optional + Temperature for cross section evaluation in [K]. + nuclides : list of str, optional + Nuclides to get cross sections for. If not specified, all burnable + nuclides from the depletion chain file are used. + reactions : list of str, optional + Reactions to get cross sections for. If not specified, all neutron + reactions listed in the depletion chain file are used. + **init_kwargs : dict + Keyword arguments passed to :func:`openmc.lib.init` + + Returns + ------- + MicroXS + """ + + check_type("temperature", temperature, (int, float)) + # if energy is string then use group structure of that name + if isinstance(energies, str): + energies = GROUP_STRUCTURES[energies] + else: + # if user inputs energies check they are ascending (low to high) as + # some depletion codes use high energy to low energy. + if not np.all(np.diff(energies) > 0): + raise ValueError('Energy group boundaries must be in ascending order') + + # check dimension consistency + if len(multigroup_flux) != len(energies) - 1: + raise ValueError('Length of flux array should be len(energies)-1') + + chain_file_path = _resolve_chain_file_path(chain_file) + chain = Chain.from_xml(chain_file_path) + + cross_sections = _find_cross_sections(model=None) + nuclides_with_data = _get_nuclides_with_data(cross_sections) + + # If no nuclides were specified, default to all nuclides from the chain + if not nuclides: + nuclides = chain.nuclides + nuclides = [nuc.name for nuc in nuclides] + + # Get reaction MT values. If no reactions specified, default to the + # reactions available in the chain file + if reactions is None: + reactions = chain.reactions + mts = [REACTION_MT[name] for name in reactions] + + # Normalize multigroup flux + multigroup_flux = np.asarray(multigroup_flux) + multigroup_flux /= multigroup_flux.sum() + + # Create 2D array for microscopic cross sections + microxs_arr = np.zeros((len(nuclides), len(mts))) + + with TemporaryDirectory() as tmpdir: + # Create a material with all nuclides + mat_all_nucs = openmc.Material() + for nuc in nuclides: + if nuc in nuclides_with_data: + mat_all_nucs.add_nuclide(nuc, 1.0) + mat_all_nucs.set_density("atom/b-cm", 1.0) + + # Create simple model containing the above material + surf1 = openmc.Sphere(boundary_type="vacuum") + surf1_cell = openmc.Cell(fill=mat_all_nucs, region=-surf1) + model = openmc.Model() + model.geometry = openmc.Geometry([surf1_cell]) + model.settings = openmc.Settings( + particles=1, batches=1, output={'summary': False}) + + with change_directory(tmpdir): + # Export model within temporary directory + model.export_to_model_xml() + + with openmc.lib.run_in_memory(**init_kwargs): + # For each nuclide and reaction, compute the flux-averaged + # cross section + for nuc_index, nuc in enumerate(nuclides): + if nuc not in nuclides_with_data: + continue + lib_nuc = openmc.lib.nuclides[nuc] + for mt_index, mt in enumerate(mts): + xs = lib_nuc.collapse_rate( + mt, temperature, energies, multigroup_flux + ) + microxs_arr[nuc_index, mt_index] = xs + + return cls(microxs_arr, nuclides, reactions) @classmethod def from_csv(cls, csv_file, **kwargs): diff --git a/openmc/lib/nuclide.py b/openmc/lib/nuclide.py index 399bb3465..8078882cf 100644 --- a/openmc/lib/nuclide.py +++ b/openmc/lib/nuclide.py @@ -91,7 +91,7 @@ class Nuclide(_FortranObject): energy : iterable of float Energy group boundaries in [eV] flux : iterable of float - Flux in each energt group (not normalized per eV) + Flux in each energy group (not normalized per eV) Returns ------- diff --git a/tests/unit_tests/test_deplete_microxs.py b/tests/unit_tests/test_deplete_microxs.py index 582c58465..ad54026f0 100644 --- a/tests/unit_tests/test_deplete_microxs.py +++ b/tests/unit_tests/test_deplete_microxs.py @@ -51,6 +51,7 @@ def test_from_array(): r'match dimensions of data array of shape \(\d*\, \d*\)'): MicroXS(data[:, 0], nuclides, reactions) + def test_csv(): ref_xs = MicroXS.from_csv(ONE_GROUP_XS) ref_xs.to_csv('temp_xs.csv') @@ -58,3 +59,53 @@ def test_csv(): assert np.all(ref_xs.data == temp_xs.data) remove('temp_xs.csv') + +def test_from_multigroup_flux(): + energies = [0., 6.25e-1, 5.53e3, 8.21e5, 2.e7] + flux = [1.1e-7, 1.2e-6, 1.3e-5, 1.4e-4] + chain_file = Path(__file__).parents[1] / 'chain_simple.xml' + kwargs = {'multigroup_flux': flux, 'chain_file': chain_file} + + # test with energy group structure from string + microxs = MicroXS.from_multigroup_flux(energies='CASMO-4', **kwargs) + assert isinstance(microxs, MicroXS) + + # test with energy group structure as floats + microxs = MicroXS.from_multigroup_flux(energies=energies, **kwargs) + assert isinstance(microxs, MicroXS) + + # test with nuclides provided + microxs = MicroXS.from_multigroup_flux( + energies=energies, nuclides=['Gd157', 'H1'], **kwargs + ) + assert isinstance(microxs, MicroXS) + assert microxs.nuclides == ['Gd157', 'H1'] + + # test with reactions provided + microxs = MicroXS.from_multigroup_flux( + energies=energies, reactions=['fission', '(n,2n)'], **kwargs + ) + assert isinstance(microxs, MicroXS) + assert microxs.reactions == ['fission', '(n,2n)'] + + +def test_multigroup_flux_same(): + chain_file = Path(__file__).parents[1] / 'chain_simple.xml' + + # Generate micro XS based on 4-group flux + energies = [0., 6.25e-1, 5.53e3, 8.21e5, 2.e7] + flux_per_ev = [0.3, 0.3, 1.0, 1.0] + flux = flux_per_ev * np.diff(energies) + microxs_4g = MicroXS.from_multigroup_flux( + energies=energies, multigroup_flux=flux, chain_file=chain_file) + + # Generate micro XS based on 2-group flux, where the boundaries line up with + # the 4 group flux and have the same flux per eV across the full energy + # range + energies = [0., 5.53e3, 2.0e7] + flux_per_ev = [0.3, 1.0] + flux = flux_per_ev * np.diff(energies) + microxs_2g = MicroXS.from_multigroup_flux( + energies=energies, multigroup_flux=flux, chain_file=chain_file) + + assert microxs_4g.data == pytest.approx(microxs_2g.data) From 411656668f4c3f35eb97046fcc309e619436e733 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Wed, 27 Dec 2023 08:44:48 -0600 Subject: [PATCH 026/671] Provide error message if a cell path can't be determined (#2812) --- src/geometry_aux.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/geometry_aux.cpp b/src/geometry_aux.cpp index 98dfc12a7..b0e88e8e3 100644 --- a/src/geometry_aux.cpp +++ b/src/geometry_aux.cpp @@ -544,6 +544,15 @@ std::string distribcell_path_inner(int32_t target_cell, int32_t map, } } + // if we get through the loop without finding an appropriate entry, throw + // an error + if (cell_it == search_univ.cells_.crend()) { + fatal_error( + fmt::format("Failed to generate a text label for distribcell with ID {}." + "The current label is: '{}'", + model::cells[target_cell]->id_, path.str())); + } + // Add the cell to the path string. Cell& c = *model::cells[*cell_it]; path << "c" << c.id_ << "->"; From 2e4811b26b94365c5927e4ec732effd308328008 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 28 Dec 2023 14:37:21 -0600 Subject: [PATCH 027/671] Ensure particle direction is normalized for plotting / volume calculations (#2816) --- include/openmc/plot.h | 3 ++- src/volume_calc.cpp | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/include/openmc/plot.h b/include/openmc/plot.h index 3e97e2544..ba82f43e2 100644 --- a/include/openmc/plot.h +++ b/include/openmc/plot.h @@ -1,6 +1,7 @@ #ifndef OPENMC_PLOT_H #define OPENMC_PLOT_H +#include #include #include @@ -200,7 +201,7 @@ T SlicePlotBase::get_map() const xyz[out_i] = origin_[out_i] + width_[1] / 2. - out_pixel / 2.; // arbitrary direction - Direction dir = {0.7071, 0.7071, 0.0}; + Direction dir = {1. / std::sqrt(2.), 1. / std::sqrt(2.), 0.0}; #pragma omp parallel { diff --git a/src/volume_calc.cpp b/src/volume_calc.cpp index 859a0d5ac..6a47e9490 100644 --- a/src/volume_calc.cpp +++ b/src/volume_calc.cpp @@ -159,7 +159,7 @@ vector VolumeCalculation::execute() const p.n_coord() = 1; Position xi {prn(&seed), prn(&seed), prn(&seed)}; p.r() = lower_left_ + xi * (upper_right_ - lower_left_); - p.u() = {0.5, 0.5, 0.5}; + p.u() = {1. / std::sqrt(3.), 1. / std::sqrt(3.), 1. / std::sqrt(3.)}; // If this location is not in the geometry at all, move on to next block if (!exhaustive_find_cell(p)) From b0926ea22c21c350b6d227bfcfb06953c43ee433 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 3 Jan 2024 08:25:33 -0600 Subject: [PATCH 028/671] Prevent underflow in calculation of speed (#2811) --- src/particle.cpp | 17 +++- .../mgxs_library_condense/results_true.dat | 8 +- .../mgxs_library_distribcell/results_true.dat | 2 +- .../mgxs_library_hdf5/results_true.dat | 4 +- .../mgxs_library_no_nuclides/results_true.dat | 4 +- .../mgxs_library_nuclides/results_true.dat | 2 +- .../results_true.dat | 2 +- .../regression_tests/tallies/results_true.dat | 2 +- .../track_output/results_true.dat | 88 +++++++++---------- 9 files changed, 69 insertions(+), 60 deletions(-) diff --git a/src/particle.cpp b/src/particle.cpp index e26e25e67..8992bc868 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -59,11 +59,20 @@ double Particle::speed() const break; } - // Calculate inverse of Lorentz factor - const double inv_gamma = mass / (this->E() + mass); + if (this->E() < 1.0e-9 * mass) { + // If the energy is much smaller than the mass, revert to non-relativistic + // formula. The 1e-9 criterion is specifically chosen as the point below + // which the error from using the non-relativistic formula is less than the + // round-off eror when using the relativistic formula (see analysis at + // https://gist.github.com/paulromano/da3b473fe3df33de94b265bdff0c7817) + return C_LIGHT * std::sqrt(2 * this->E() / mass); + } else { + // Calculate inverse of Lorentz factor + const double inv_gamma = mass / (this->E() + mass); - // Calculate speed via v = c * sqrt(1 - γ^-2) - return C_LIGHT * std::sqrt(1 - inv_gamma * inv_gamma); + // Calculate speed via v = c * sqrt(1 - γ^-2) + return C_LIGHT * std::sqrt(1 - inv_gamma * inv_gamma); + } } void Particle::move_distance(double length) diff --git a/tests/regression_tests/mgxs_library_condense/results_true.dat b/tests/regression_tests/mgxs_library_condense/results_true.dat index 0792d92da..ae5bff6e3 100644 --- a/tests/regression_tests/mgxs_library_condense/results_true.dat +++ b/tests/regression_tests/mgxs_library_condense/results_true.dat @@ -168,10 +168,10 @@ 3 2 2 1 1 total 1.0 0.130701 mesh 1 group in nuclide mean std. dev. x y z -0 1 1 1 1 total 5.304284e-07 2.560239e-08 -2 1 2 1 1 total 4.940320e-07 2.410416e-08 -1 2 1 1 1 total 5.587366e-07 3.382787e-08 -3 2 2 1 1 total 5.232209e-07 2.482800e-08 +0 1 1 1 1 total 5.304285e-07 2.560239e-08 +2 1 2 1 1 total 4.940321e-07 2.410417e-08 +1 2 1 1 1 total 5.587365e-07 3.382787e-08 +3 2 2 1 1 total 5.232210e-07 2.482800e-08 mesh 1 group in nuclide mean std. dev. x y z 0 1 1 1 1 total 0.026032 0.001172 diff --git a/tests/regression_tests/mgxs_library_distribcell/results_true.dat b/tests/regression_tests/mgxs_library_distribcell/results_true.dat index ad2aef475..ef7cb2b96 100644 --- a/tests/regression_tests/mgxs_library_distribcell/results_true.dat +++ b/tests/regression_tests/mgxs_library_distribcell/results_true.dat @@ -51,7 +51,7 @@ sum(distribcell) group out nuclide mean std. dev. 0 ((0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, ...),) 1 total 1.0 0.084331 sum(distribcell) group in nuclide mean std. dev. -0 ((0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, ...),) 1 total 5.253873e-07 2.168461e-08 +0 ((0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, ...),) 1 total 5.253873e-07 2.168462e-08 sum(distribcell) group in nuclide mean std. dev. 0 ((0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, ...),) 1 total 0.094147 0.003701 sum(distribcell) group in group out nuclide mean std. dev. diff --git a/tests/regression_tests/mgxs_library_hdf5/results_true.dat b/tests/regression_tests/mgxs_library_hdf5/results_true.dat index 28a352fdf..b2ef8eae9 100644 --- a/tests/regression_tests/mgxs_library_hdf5/results_true.dat +++ b/tests/regression_tests/mgxs_library_hdf5/results_true.dat @@ -97,8 +97,8 @@ domain=1 type=chi-prompt [1.00000000e+00 0.00000000e+00] [1.03333203e-01 0.00000000e+00] domain=1 type=inverse-velocity -[5.72461488e-08 3.00999716e-06] -[2.80644406e-09 1.80993425e-07] +[5.72461488e-08 3.00999757e-06] +[2.80644407e-09 1.80993426e-07] domain=1 type=prompt-nu-fission [5.64594959e-03 1.32859920e-01] [3.68364598e-04 7.79430310e-03] diff --git a/tests/regression_tests/mgxs_library_no_nuclides/results_true.dat b/tests/regression_tests/mgxs_library_no_nuclides/results_true.dat index 7ab85f600..c0dba7c0f 100644 --- a/tests/regression_tests/mgxs_library_no_nuclides/results_true.dat +++ b/tests/regression_tests/mgxs_library_no_nuclides/results_true.dat @@ -143,7 +143,7 @@ chi-prompt inverse-velocity material group in nuclide mean std. dev. 1 1 1 total 5.956290e-08 2.255751e-09 -0 1 2 total 2.886630e-06 7.418455e-08 +0 1 2 total 2.886630e-06 7.418452e-08 prompt-nu-fission material group in nuclide mean std. dev. 1 1 1 total 0.018067 0.000817 @@ -449,7 +449,7 @@ chi-prompt inverse-velocity material group in nuclide mean std. dev. 1 2 1 total 6.076563e-08 2.727519e-09 -0 2 2 total 2.996176e-06 1.110140e-07 +0 2 2 total 2.996176e-06 1.110139e-07 prompt-nu-fission material group in nuclide mean std. dev. 1 2 1 total 0.0 0.0 diff --git a/tests/regression_tests/mgxs_library_nuclides/results_true.dat b/tests/regression_tests/mgxs_library_nuclides/results_true.dat index c23890323..c2188997d 100644 --- a/tests/regression_tests/mgxs_library_nuclides/results_true.dat +++ b/tests/regression_tests/mgxs_library_nuclides/results_true.dat @@ -1 +1 @@ -c4a4cb4e00f09ef62222a0e66df817af87bf324a2ac0e57a82ff1337535a223c7077d660a60d0ce9ac7113c47d37b3296347f3ba212558ff09ef5fc586e1dd28 \ No newline at end of file +4ae3b5a70ad72b1be261aee3ab19e0261d1c12f4d4ca50b712d1ba76041bd5387c69fa9fa326619ad588db206cd9aaf464b0025d71ea9b8b1137af0102bca87f \ No newline at end of file diff --git a/tests/regression_tests/mgxs_library_specific_nuclides/results_true.dat b/tests/regression_tests/mgxs_library_specific_nuclides/results_true.dat index 05ee40902..eaf56b966 100644 --- a/tests/regression_tests/mgxs_library_specific_nuclides/results_true.dat +++ b/tests/regression_tests/mgxs_library_specific_nuclides/results_true.dat @@ -1 +1 @@ -3e86542d1166b8a0bcc61742b88c9e931d63733477f3274b32b4da64d8f05413fa6330d5615f44f24735c2c2f77d3071f3dce38faba284c321ceab81c1064480 \ No newline at end of file +08d5c199c51496f86fdd739bf7ee0e143a9a159da0f4d364ec970557e5c1fc92a202d906dcae91812e665fd2e88dd7db1e4913ef6b91f456f23b52093c83f483 \ No newline at end of file diff --git a/tests/regression_tests/tallies/results_true.dat b/tests/regression_tests/tallies/results_true.dat index 673d2143b..3dda0f9b2 100644 --- a/tests/regression_tests/tallies/results_true.dat +++ b/tests/regression_tests/tallies/results_true.dat @@ -1 +1 @@ -d1decdbec6cb59df91ba5c42cb37a04f413a34fa7faf7ad1eecfd7d53a14af8cb65b58335c4ede4e88f6d9ab35a1746251983cc991d74eab3e678887c84183bd \ No newline at end of file +ddfbb0a6f5498eb8ff33bb10beb64e244b015861919eb4837bd82855e5fd87c3ff97dfa382d3d60afa81c48d9f857fba8e0b99263e7b35587f8adb75be1fc0ec \ No newline at end of file diff --git a/tests/regression_tests/track_output/results_true.dat b/tests/regression_tests/track_output/results_true.dat index 746f1448f..148b54a30 100644 --- a/tests/regression_tests/track_output/results_true.dat +++ b/tests/regression_tests/track_output/results_true.dat @@ -97,22 +97,22 @@ neutron [((-9.663085e-01, -6.616522e-01, -9.863690e-01), (-7.513921e-01, 4.14097 ((-1.585080e+01, 1.545037e+00, -2.159072e+01), (6.457458e-01, -1.162934e-01, -7.546444e-01), 4.958073e-01, 5.061748e-06, 1.000000e+00, 22, 2129, 3) ((-1.576406e+01, 1.529415e+00, -2.169209e+01), (6.457458e-01, -1.162934e-01, -7.546444e-01), 4.958073e-01, 5.199673e-06, 1.000000e+00, 23, 2129, 1) ((-1.553972e+01, 1.489015e+00, -2.195425e+01), (6.457458e-01, -1.162934e-01, -7.546444e-01), 4.958073e-01, 5.556377e-06, 1.000000e+00, 23, 2115, 1) - ((-1.546081e+01, 1.474803e+00, -2.204648e+01), (3.257038e-01, -9.420596e-01, -8.025439e-02), 3.076319e-01, 5.681852e-06, 1.000000e+00, 23, 2115, 1) + ((-1.546081e+01, 1.474803e+00, -2.204648e+01), (3.257038e-01, -9.420596e-01, -8.025439e-02), 3.076319e-01, 5.681853e-06, 1.000000e+00, 23, 2115, 1) ((-1.543563e+01, 1.401988e+00, -2.205268e+01), (-3.677195e-01, -8.784218e-01, -3.052172e-01), 8.422973e-02, 5.782605e-06, 1.000000e+00, 23, 2115, 1) ((-1.553972e+01, 1.153338e+00, -2.213907e+01), (-3.677195e-01, -8.784218e-01, -3.052172e-01), 8.422973e-02, 6.487752e-06, 1.000000e+00, 23, 2129, 1) ((-1.565955e+01, 8.670807e-01, -2.223854e+01), (1.611814e-01, 2.692337e-01, -9.494913e-01), 8.559448e-02, 7.299552e-06, 1.000000e+00, 23, 2129, 1) ((-1.563263e+01, 9.120446e-01, -2.239711e+01), (-6.773989e-01, 6.985688e-01, -2.305048e-01), 3.418159e-02, 7.712256e-06, 1.000000e+00, 23, 2129, 1) - ((-1.592604e+01, 1.214620e+00, -2.249695e+01), (-6.773989e-01, 6.985688e-01, -2.305048e-01), 3.418159e-02, 9.406031e-06, 1.000000e+00, 22, 2129, 3) + ((-1.592604e+01, 1.214620e+00, -2.249695e+01), (-6.773989e-01, 6.985688e-01, -2.305048e-01), 3.418159e-02, 9.406032e-06, 1.000000e+00, 22, 2129, 3) ((-1.598742e+01, 1.277922e+00, -2.251784e+01), (-6.773989e-01, 6.985688e-01, -2.305048e-01), 3.418159e-02, 9.760391e-06, 1.000000e+00, 21, 2129, 2) ((-1.670388e+01, 2.016770e+00, -2.276163e+01), (-6.773989e-01, 6.985688e-01, -2.305048e-01), 3.418159e-02, 1.389636e-05, 1.000000e+00, 22, 2129, 3) ((-1.676526e+01, 2.080073e+00, -2.278252e+01), (-6.773989e-01, 6.985688e-01, -2.305048e-01), 3.418159e-02, 1.425072e-05, 1.000000e+00, 23, 2129, 1) ((-1.681575e+01, 2.132139e+00, -2.279970e+01), (4.286076e-02, -9.409799e-01, -3.357375e-01), 1.813618e-02, 1.454218e-05, 1.000000e+00, 23, 2129, 1) ((-1.681374e+01, 2.087933e+00, -2.281547e+01), (-7.741702e-02, -7.222450e-01, -6.872909e-01), 1.529603e-02, 1.479439e-05, 1.000000e+00, 23, 2129, 1) ((-1.682083e+01, 2.021795e+00, -2.287841e+01), (-7.741702e-02, -7.222450e-01, -6.872909e-01), 1.529603e-02, 1.532970e-05, 1.000000e+00, 22, 2129, 3) - ((-1.684417e+01, 1.804084e+00, -2.308558e+01), (-7.741702e-02, -7.222450e-01, -6.872909e-01), 1.529603e-02, 1.709182e-05, 1.000000e+00, 21, 2129, 2) + ((-1.684417e+01, 1.804084e+00, -2.308558e+01), (-7.741702e-02, -7.222450e-01, -6.872909e-01), 1.529603e-02, 1.709181e-05, 1.000000e+00, 21, 2129, 2) ((-1.686879e+01, 1.574384e+00, -2.330417e+01), (-7.741702e-02, -7.222450e-01, -6.872909e-01), 1.529603e-02, 1.895097e-05, 1.000000e+00, 22, 2129, 3) - ((-1.689212e+01, 1.356673e+00, -2.351134e+01), (-7.741702e-02, -7.222450e-01, -6.872909e-01), 1.529603e-02, 2.071309e-05, 1.000000e+00, 23, 2129, 1) - ((-1.689233e+01, 1.354754e+00, -2.351317e+01), (6.002458e-01, 4.967948e-01, 6.268172e-01), 8.602342e-03, 2.072862e-05, 1.000000e+00, 23, 2129, 1) + ((-1.689212e+01, 1.356673e+00, -2.351134e+01), (-7.741702e-02, -7.222450e-01, -6.872909e-01), 1.529603e-02, 2.071308e-05, 1.000000e+00, 23, 2129, 1) + ((-1.689233e+01, 1.354754e+00, -2.351317e+01), (6.002458e-01, 4.967948e-01, 6.268172e-01), 8.602342e-03, 2.072861e-05, 1.000000e+00, 23, 2129, 1) ((-1.689148e+01, 1.355453e+00, -2.351229e+01), (6.002458e-01, 4.967948e-01, 6.268172e-01), 8.602342e-03, 2.073958e-05, 1.000000e+00, 22, 2129, 3) ((-1.682182e+01, 1.413107e+00, -2.343954e+01), (6.002458e-01, 4.967948e-01, 6.268172e-01), 8.602342e-03, 2.164421e-05, 1.000000e+00, 21, 2129, 2) ((-1.636364e+01, 1.792326e+00, -2.296107e+01), (9.641593e-01, -1.162836e-01, 2.384846e-01), 7.837617e-03, 2.759442e-05, 1.000000e+00, 21, 2129, 2) @@ -145,60 +145,60 @@ neutron [((-9.469716e-01, -2.580266e-01, 1.414357e-01), (1.438873e-01, 2.365819e ((-3.744933e+00, -1.031047e+00, 2.831062e+00), (-2.916959e-01, -6.494657e-01, -7.022164e-01), 8.590591e+00, 1.876753e-07, 0.000000e+00, 23, 2387, 1)] neutron [((6.474155e+00, -5.192870e+00, 5.413003e+00), (-9.112559e-01, 3.950037e-01, 1.165536e-01), 2.052226e+06, 4.450859e-05, 1.000000e+00, 21, 2367, 2) ((6.037250e+00, -5.003484e+00, 5.468885e+00), (-9.112559e-01, 3.950037e-01, 1.165536e-01), 2.052226e+06, 4.450883e-05, 1.000000e+00, 22, 2367, 3) - ((5.942573e+00, -4.962444e+00, 5.480995e+00), (-9.112559e-01, 3.950037e-01, 1.165536e-01), 2.052226e+06, 4.450888e-05, 1.000000e+00, 23, 2367, 1) - ((5.861800e+00, -4.927431e+00, 5.491326e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.450892e-05, 1.000000e+00, 23, 2367, 1) - ((5.725160e+00, -4.971424e+00, 5.491002e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.450902e-05, 1.000000e+00, 23, 2366, 1) + ((5.942573e+00, -4.962444e+00, 5.480995e+00), (-9.112559e-01, 3.950037e-01, 1.165536e-01), 2.052226e+06, 4.450889e-05, 1.000000e+00, 23, 2367, 1) + ((5.861800e+00, -4.927431e+00, 5.491326e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.450893e-05, 1.000000e+00, 23, 2367, 1) + ((5.725160e+00, -4.971424e+00, 5.491002e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.450903e-05, 1.000000e+00, 23, 2366, 1) ((5.494150e+00, -5.045802e+00, 5.490454e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.450919e-05, 1.000000e+00, 22, 2366, 3) - ((5.392865e+00, -5.078412e+00, 5.490213e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.450926e-05, 1.000000e+00, 21, 2366, 2) - ((4.612759e+00, -5.329579e+00, 5.488362e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.450981e-05, 1.000000e+00, 22, 2366, 3) + ((5.392865e+00, -5.078412e+00, 5.490213e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.450927e-05, 1.000000e+00, 21, 2366, 2) + ((4.612759e+00, -5.329579e+00, 5.488362e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.450982e-05, 1.000000e+00, 22, 2366, 3) ((4.511474e+00, -5.362189e+00, 5.488121e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.450989e-05, 1.000000e+00, 23, 2366, 1) ((4.089400e+00, -5.498082e+00, 5.487119e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.451019e-05, 1.000000e+00, 23, 2365, 1) ((3.384113e+00, -5.725160e+00, 5.485445e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.451069e-05, 1.000000e+00, 23, 2351, 1) - ((2.453640e+00, -6.024740e+00, 5.483237e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.451135e-05, 1.000000e+00, 23, 2350, 1) - ((2.086813e+00, -6.142846e+00, 5.482366e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.451161e-05, 1.000000e+00, 22, 2350, 3) + ((2.453640e+00, -6.024740e+00, 5.483237e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.451136e-05, 1.000000e+00, 23, 2350, 1) + ((2.086813e+00, -6.142846e+00, 5.482366e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.451162e-05, 1.000000e+00, 22, 2350, 3) ((1.993594e+00, -6.172859e+00, 5.482145e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.451168e-05, 1.000000e+00, 21, 2350, 2) - ((1.325704e+00, -6.387896e+00, 5.480560e+00), (8.986086e-01, -4.380574e-01, -2.466265e-02), 9.775839e+05, 4.451215e-05, 1.000000e+00, 21, 2350, 2) - ((2.061403e+00, -6.746538e+00, 5.460368e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451275e-05, 1.000000e+00, 21, 2350, 2) - ((1.122794e+00, -6.498940e+00, 4.743664e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451374e-05, 1.000000e+00, 22, 2350, 3) - ((1.036483e+00, -6.476172e+00, 4.677758e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451383e-05, 1.000000e+00, 23, 2350, 1) - ((8.178800e-01, -6.418507e+00, 4.510837e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451406e-05, 1.000000e+00, 23, 2349, 1) + ((1.325704e+00, -6.387896e+00, 5.480560e+00), (8.986086e-01, -4.380574e-01, -2.466265e-02), 9.775839e+05, 4.451216e-05, 1.000000e+00, 21, 2350, 2) + ((2.061403e+00, -6.746538e+00, 5.460368e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451276e-05, 1.000000e+00, 21, 2350, 2) + ((1.122794e+00, -6.498940e+00, 4.743664e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451375e-05, 1.000000e+00, 22, 2350, 3) + ((1.036483e+00, -6.476172e+00, 4.677758e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451384e-05, 1.000000e+00, 23, 2350, 1) + ((8.178800e-01, -6.418507e+00, 4.510837e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451407e-05, 1.000000e+00, 23, 2349, 1) ((5.725264e-01, -6.353784e+00, 4.323490e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451432e-05, 1.000000e+00, 22, 2349, 3) - ((4.668297e-01, -6.325902e+00, 4.242782e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451443e-05, 1.000000e+00, 21, 2349, 2) + ((4.668297e-01, -6.325902e+00, 4.242782e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451444e-05, 1.000000e+00, 21, 2349, 2) ((-2.989816e-01, -6.123888e+00, 3.658023e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451524e-05, 1.000000e+00, 22, 2349, 3) ((-4.046782e-01, -6.096006e+00, 3.577315e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451535e-05, 1.000000e+00, 23, 2349, 1) ((-8.040445e-01, -5.990656e+00, 3.272367e+00), (-7.989001e-01, -1.890553e-01, -5.709787e-01), 6.637114e+05, 4.451577e-05, 1.000000e+00, 23, 2349, 1) - ((-8.178800e-01, -5.993930e+00, 3.262478e+00), (-7.989001e-01, -1.890553e-01, -5.709787e-01), 6.637114e+05, 4.451578e-05, 1.000000e+00, 23, 2348, 1) - ((-1.077910e+00, -6.055465e+00, 3.076633e+00), (-4.487118e-01, 1.670254e-01, -8.779295e-01), 4.550608e+05, 4.451607e-05, 1.000000e+00, 23, 2348, 1) - ((-1.215611e+00, -6.004208e+00, 2.807214e+00), (7.872329e-01, 4.481433e-01, -4.235941e-01), 3.544207e+03, 4.451640e-05, 1.000000e+00, 23, 2348, 1) - ((-1.100296e+00, -5.938564e+00, 2.745166e+00), (8.468456e-01, 5.310954e-01, 2.811230e-02), 2.812308e+03, 4.451818e-05, 1.000000e+00, 23, 2348, 1) + ((-8.178800e-01, -5.993930e+00, 3.262478e+00), (-7.989001e-01, -1.890553e-01, -5.709787e-01), 6.637114e+05, 4.451579e-05, 1.000000e+00, 23, 2348, 1) + ((-1.077910e+00, -6.055465e+00, 3.076633e+00), (-4.487118e-01, 1.670254e-01, -8.779295e-01), 4.550608e+05, 4.451608e-05, 1.000000e+00, 23, 2348, 1) + ((-1.215611e+00, -6.004208e+00, 2.807214e+00), (7.872329e-01, 4.481433e-01, -4.235941e-01), 3.544207e+03, 4.451641e-05, 1.000000e+00, 23, 2348, 1) + ((-1.100296e+00, -5.938564e+00, 2.745166e+00), (8.468456e-01, 5.310954e-01, 2.811230e-02), 2.812308e+03, 4.451819e-05, 1.000000e+00, 23, 2348, 1) ((-8.178800e-01, -5.761448e+00, 2.754541e+00), (8.468456e-01, 5.310954e-01, 2.811230e-02), 2.812308e+03, 4.452273e-05, 1.000000e+00, 23, 2349, 1) ((-7.600184e-01, -5.725160e+00, 2.756462e+00), (8.468456e-01, 5.310954e-01, 2.811230e-02), 2.812308e+03, 4.452366e-05, 1.000000e+00, 23, 2363, 1) ((-2.947156e-01, -5.433347e+00, 2.771908e+00), (8.468456e-01, 5.310954e-01, 2.811230e-02), 2.812308e+03, 4.453115e-05, 1.000000e+00, 22, 2363, 3) - ((-2.073333e-01, -5.378546e+00, 2.774809e+00), (8.468456e-01, 5.310954e-01, 2.811230e-02), 2.812308e+03, 4.453255e-05, 1.000000e+00, 21, 2363, 2) - ((-1.746705e-01, -5.358062e+00, 2.775893e+00), (5.278774e-01, 5.459205e-01, 6.506276e-01), 2.806481e+03, 4.453308e-05, 1.000000e+00, 21, 2363, 2) - ((-8.681718e-02, -5.267205e+00, 2.884176e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.453535e-05, 1.000000e+00, 21, 2363, 2) + ((-2.073333e-01, -5.378546e+00, 2.774809e+00), (8.468456e-01, 5.310954e-01, 2.811230e-02), 2.812308e+03, 4.453256e-05, 1.000000e+00, 21, 2363, 2) + ((-1.746705e-01, -5.358062e+00, 2.775893e+00), (5.278774e-01, 5.459205e-01, 6.506276e-01), 2.806481e+03, 4.453309e-05, 1.000000e+00, 21, 2363, 2) + ((-8.681718e-02, -5.267205e+00, 2.884176e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.453536e-05, 1.000000e+00, 21, 2363, 2) ((-3.684221e-01, -5.266924e+00, 2.745840e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.453967e-05, 1.000000e+00, 22, 2363, 3) ((-4.840903e-01, -5.266809e+00, 2.689019e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.454144e-05, 1.000000e+00, 23, 2363, 1) - ((-8.178800e-01, -5.266475e+00, 2.525049e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.454655e-05, 1.000000e+00, 23, 2362, 1) + ((-8.178800e-01, -5.266475e+00, 2.525049e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.454656e-05, 1.000000e+00, 23, 2362, 1) ((-1.151175e+00, -5.266142e+00, 2.361321e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.455166e-05, 1.000000e+00, 22, 2362, 3) - ((-1.266464e+00, -5.266027e+00, 2.304687e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.455342e-05, 1.000000e+00, 21, 2362, 2) + ((-1.266464e+00, -5.266027e+00, 2.304687e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.455343e-05, 1.000000e+00, 21, 2362, 2) ((-2.005772e+00, -5.265288e+00, 1.941509e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.456475e-05, 1.000000e+00, 22, 2362, 3) - ((-2.121061e+00, -5.265173e+00, 1.884875e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.456651e-05, 1.000000e+00, 23, 2362, 1) - ((-2.393759e+00, -5.264900e+00, 1.750915e+00), (-9.607392e-01, -1.749373e-01, 2.153533e-01), 1.619469e+03, 4.457069e-05, 1.000000e+00, 23, 2362, 1) - ((-2.453640e+00, -5.275804e+00, 1.764337e+00), (-9.607392e-01, -1.749373e-01, 2.153533e-01), 1.619469e+03, 4.457181e-05, 1.000000e+00, 23, 2361, 1) - ((-2.470658e+00, -5.278903e+00, 1.768152e+00), (-6.728606e-01, -2.916408e-01, -6.798561e-01), 4.873672e+02, 4.457213e-05, 1.000000e+00, 23, 2361, 1) - ((-3.463692e+00, -5.709318e+00, 7.647939e-01), (-2.249302e-01, -9.330150e-01, -2.808728e-01), 1.790901e+02, 4.462046e-05, 1.000000e+00, 23, 2361, 1) - ((-3.467511e+00, -5.725160e+00, 7.600247e-01), (-2.249302e-01, -9.330150e-01, -2.808728e-01), 1.790901e+02, 4.462138e-05, 1.000000e+00, 23, 2347, 1) + ((-2.121061e+00, -5.265173e+00, 1.884875e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.456652e-05, 1.000000e+00, 23, 2362, 1) + ((-2.393759e+00, -5.264900e+00, 1.750915e+00), (-9.607392e-01, -1.749373e-01, 2.153533e-01), 1.619469e+03, 4.457070e-05, 1.000000e+00, 23, 2362, 1) + ((-2.453640e+00, -5.275804e+00, 1.764337e+00), (-9.607392e-01, -1.749373e-01, 2.153533e-01), 1.619469e+03, 4.457182e-05, 1.000000e+00, 23, 2361, 1) + ((-2.470658e+00, -5.278903e+00, 1.768152e+00), (-6.728606e-01, -2.916408e-01, -6.798561e-01), 4.873672e+02, 4.457214e-05, 1.000000e+00, 23, 2361, 1) + ((-3.463692e+00, -5.709318e+00, 7.647939e-01), (-2.249302e-01, -9.330150e-01, -2.808728e-01), 1.790901e+02, 4.462047e-05, 1.000000e+00, 23, 2361, 1) + ((-3.467511e+00, -5.725160e+00, 7.600247e-01), (-2.249302e-01, -9.330150e-01, -2.808728e-01), 1.790901e+02, 4.462139e-05, 1.000000e+00, 23, 2347, 1) ((-3.533785e+00, -6.000066e+00, 6.772677e-01), (-2.249302e-01, -9.330150e-01, -2.808728e-01), 1.790901e+02, 4.463730e-05, 1.000000e+00, 22, 2347, 3) - ((-3.562245e+00, -6.118119e+00, 6.417291e-01), (-2.249302e-01, -9.330150e-01, -2.808728e-01), 1.790901e+02, 4.464413e-05, 1.000000e+00, 21, 2347, 2) + ((-3.562245e+00, -6.118119e+00, 6.417291e-01), (-2.249302e-01, -9.330150e-01, -2.808728e-01), 1.790901e+02, 4.464414e-05, 1.000000e+00, 21, 2347, 2) ((-3.723934e+00, -6.788806e+00, 4.398272e-01), (-2.249302e-01, -9.330150e-01, -2.808728e-01), 1.790901e+02, 4.468297e-05, 1.000000e+00, 22, 2347, 3) - ((-3.752394e+00, -6.906859e+00, 4.042885e-01), (-2.249302e-01, -9.330150e-01, -2.808728e-01), 1.790901e+02, 4.468980e-05, 1.000000e+00, 23, 2347, 1) - ((-3.848515e+00, -7.305571e+00, 2.842613e-01), (6.498448e-01, -3.243413e-01, -6.873896e-01), 2.328979e+01, 4.471289e-05, 1.000000e+00, 23, 2347, 1) + ((-3.752394e+00, -6.906859e+00, 4.042885e-01), (-2.249302e-01, -9.330150e-01, -2.808728e-01), 1.790901e+02, 4.468981e-05, 1.000000e+00, 23, 2347, 1) + ((-3.848515e+00, -7.305571e+00, 2.842613e-01), (6.498448e-01, -3.243413e-01, -6.873896e-01), 2.328979e+01, 4.471290e-05, 1.000000e+00, 23, 2347, 1) ((-3.737619e+00, -7.360920e+00, 1.669579e-01), (6.498448e-01, -3.243413e-01, -6.873896e-01), 2.328979e+01, 4.473846e-05, 1.000000e+00, 11, 1750, 1) - ((-3.574308e+00, -7.442429e+00, -5.788041e-03), (6.794505e-01, 3.157106e-01, -6.623246e-01), 1.590416e+01, 4.477610e-05, 1.000000e+00, 11, 1750, 1) - ((-3.398889e+00, -7.360920e+00, -1.767852e-01), (6.794505e-01, 3.157106e-01, -6.623246e-01), 1.590416e+01, 4.482291e-05, 1.000000e+00, 23, 2347, 1) - ((-2.904712e+00, -7.131298e+00, -6.585068e-01), (-4.836985e-02, -2.819627e-01, -9.582053e-01), 2.610202e+00, 4.495476e-05, 1.000000e+00, 23, 2347, 1) - ((-2.923579e+00, -7.241285e+00, -1.032280e+00), (-6.089807e-01, -2.347428e-01, -7.576532e-01), 1.671268e+00, 4.512932e-05, 1.000000e+00, 23, 2347, 1) + ((-3.574308e+00, -7.442429e+00, -5.788041e-03), (6.794505e-01, 3.157106e-01, -6.623246e-01), 1.590416e+01, 4.477611e-05, 1.000000e+00, 11, 1750, 1) + ((-3.398889e+00, -7.360920e+00, -1.767852e-01), (6.794505e-01, 3.157106e-01, -6.623246e-01), 1.590416e+01, 4.482292e-05, 1.000000e+00, 23, 2347, 1) + ((-2.904712e+00, -7.131298e+00, -6.585068e-01), (-4.836985e-02, -2.819627e-01, -9.582053e-01), 2.610202e+00, 4.495477e-05, 1.000000e+00, 23, 2347, 1) + ((-2.923579e+00, -7.241285e+00, -1.032280e+00), (-6.089807e-01, -2.347428e-01, -7.576532e-01), 1.671268e+00, 4.512933e-05, 1.000000e+00, 23, 2347, 1) ((-3.012516e+00, -7.275567e+00, -1.142930e+00), (3.221931e-01, -5.406104e-01, -7.771306e-01), 1.275709e-01, 4.521100e-05, 1.000000e+00, 23, 2347, 1) ((-2.984032e+00, -7.323360e+00, -1.211633e+00), (-8.629564e-01, -1.719529e-01, -4.751195e-01), 1.897424e-02, 4.538995e-05, 1.000000e+00, 23, 2347, 1) ((-3.172528e+00, -7.360920e+00, -1.315413e+00), (-8.629564e-01, -1.719529e-01, -4.751195e-01), 1.897424e-02, 4.653641e-05, 1.000000e+00, 11, 1750, 1) @@ -206,13 +206,13 @@ neutron [((6.474155e+00, -5.192870e+00, 5.413003e+00), (-9.112559e-01, 3.950037e ((-3.625236e+00, -7.552741e+00, -1.449021e+00), (-1.976867e-01, 2.826475e-01, 9.386322e-01), 2.145548e-02, 5.009730e-05, 1.000000e+00, 11, 1750, 1) ((-3.636290e+00, -7.536936e+00, -1.396537e+00), (1.266676e-01, 2.265353e-01, -9.657314e-01), 1.815564e-02, 5.037329e-05, 1.000000e+00, 11, 1750, 1) ((-3.621792e+00, -7.511008e+00, -1.507069e+00), (-4.275550e-01, 8.441443e-01, 3.234457e-01), 1.848049e-02, 5.098741e-05, 1.000000e+00, 11, 1750, 1) - ((-3.697811e+00, -7.360920e+00, -1.449560e+00), (-4.275550e-01, 8.441443e-01, 3.234457e-01), 1.848049e-02, 5.193299e-05, 1.000000e+00, 23, 2347, 1) - ((-3.703436e+00, -7.349814e+00, -1.445305e+00), (-7.832167e-01, 5.713670e-01, 2.451762e-01), 1.861705e-02, 5.200296e-05, 1.000000e+00, 23, 2347, 1) + ((-3.697811e+00, -7.360920e+00, -1.449560e+00), (-4.275550e-01, 8.441443e-01, 3.234457e-01), 1.848049e-02, 5.193300e-05, 1.000000e+00, 23, 2347, 1) + ((-3.703436e+00, -7.349814e+00, -1.445305e+00), (-7.832167e-01, 5.713670e-01, 2.451762e-01), 1.861705e-02, 5.200297e-05, 1.000000e+00, 23, 2347, 1) ((-3.823243e+00, -7.262414e+00, -1.407801e+00), (-9.917106e-01, 5.069164e-02, 1.180695e-01), 4.703334e-02, 5.281350e-05, 1.000000e+00, 23, 2347, 1) ((-4.089400e+00, -7.248809e+00, -1.376113e+00), (-9.917106e-01, 5.069164e-02, 1.180695e-01), 4.703334e-02, 5.370820e-05, 1.000000e+00, 23, 2346, 1) ((-4.131081e+00, -7.246679e+00, -1.371151e+00), (9.108632e-01, -4.018526e-01, 9.403556e-02), 8.057393e-02, 5.384831e-05, 1.000000e+00, 23, 2346, 1) ((-4.089400e+00, -7.265067e+00, -1.366848e+00), (9.108632e-01, -4.018526e-01, 9.403556e-02), 8.057393e-02, 5.396486e-05, 1.000000e+00, 23, 2347, 1) - ((-3.872134e+00, -7.360920e+00, -1.344418e+00), (9.108632e-01, -4.018526e-01, 9.403556e-02), 8.057393e-02, 5.457239e-05, 1.000000e+00, 11, 1750, 1) + ((-3.872134e+00, -7.360920e+00, -1.344418e+00), (9.108632e-01, -4.018526e-01, 9.403556e-02), 8.057393e-02, 5.457240e-05, 1.000000e+00, 11, 1750, 1) ((-3.673062e+00, -7.448746e+00, -1.323866e+00), (2.976906e-01, 8.174443e-01, -4.931178e-01), 5.538060e-02, 5.512905e-05, 1.000000e+00, 11, 1750, 1) ((-3.658580e+00, -7.408978e+00, -1.347855e+00), (-5.409120e-01, 8.345378e-01, -1.046943e-01), 8.122576e-02, 5.527851e-05, 1.000000e+00, 11, 1750, 1) - ((-3.682981e+00, -7.371331e+00, -1.352578e+00), (-5.409120e-01, 8.345378e-01, -1.046943e-01), 8.122576e-02, 5.539294e-05, 0.000000e+00, 11, 1750, 1)] + ((-3.682981e+00, -7.371331e+00, -1.352578e+00), (-5.409120e-01, 8.345378e-01, -1.046943e-01), 8.122576e-02, 5.539295e-05, 0.000000e+00, 11, 1750, 1)] From 63cb7c23a69987c703f4291c24bd70c0d41a915b Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Thu, 4 Jan 2024 19:12:09 +0000 Subject: [PATCH 029/671] added missing meshes to lib docs (#2820) --- docs/source/pythonapi/capi.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/source/pythonapi/capi.rst b/docs/source/pythonapi/capi.rst index bce647ccb..ef713801c 100644 --- a/docs/source/pythonapi/capi.rst +++ b/docs/source/pythonapi/capi.rst @@ -54,11 +54,14 @@ Classes :template: myclass.rst Cell + CylindricalMesh EnergyFilter MaterialFilter Material MeshFilter MeshSurfaceFilter Nuclide + RectilinearMesh RegularMesh + SphericalMesh Tally From cc15f5e5dfdd50c5b6f6d16a034e7e5e1c11180c Mon Sep 17 00:00:00 2001 From: Gavin Ridley Date: Thu, 11 Jan 2024 08:52:33 -0600 Subject: [PATCH 030/671] Fix config change not propagating through to decay energies (#2825) --- openmc/config.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openmc/config.py b/openmc/config.py index 3b8992536..b823d6b06 100644 --- a/openmc/config.py +++ b/openmc/config.py @@ -4,7 +4,7 @@ from pathlib import Path import warnings from openmc.data import DataLibrary -from openmc.data.decay import _DECAY_PHOTON_ENERGY +from openmc.data.decay import _DECAY_ENERGY, _DECAY_PHOTON_ENERGY __all__ = ["config"] @@ -41,6 +41,7 @@ class _Config(MutableMapping): os.environ['OPENMC_CHAIN_FILE'] = str(value) # Reset photon source data since it relies on chain file _DECAY_PHOTON_ENERGY.clear() + _DECAY_ENERGY.clear() else: raise KeyError(f'Unrecognized config key: {key}. Acceptable keys ' 'are "cross_sections", "mg_cross_sections" and ' @@ -76,7 +77,7 @@ def _default_config(): if (chain_file is None and config.get('cross_sections') is not None and config['cross_sections'].exists() - ): + ): # Check for depletion chain in cross_sections.xml data = DataLibrary.from_xml(config['cross_sections']) for lib in reversed(data.libraries): From d8e9d58c5e461b3b16345f25c4741027c915a297 Mon Sep 17 00:00:00 2001 From: Gavin Ridley Date: Thu, 11 Jan 2024 08:56:44 -0600 Subject: [PATCH 031/671] Reset timers at correct place in deplete (#2821) --- openmc/deplete/coupled_operator.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/openmc/deplete/coupled_operator.py b/openmc/deplete/coupled_operator.py index 287a4e0d3..d591e956f 100644 --- a/openmc/deplete/coupled_operator.py +++ b/openmc/deplete/coupled_operator.py @@ -265,6 +265,9 @@ class CoupledOperator(OpenMCOperator): 'fission_yield_opts': fission_yield_opts } + # Records how many times the operator has been called + self._n_calls = 0 + super().__init__( materials=model.materials, cross_sections=cross_sections, @@ -429,6 +432,16 @@ class CoupledOperator(OpenMCOperator): # Reset results in OpenMC openmc.lib.reset() + # The timers are reset only if the operator has been called before. + # This is because we call this method after loading cross sections, and + # no transport has taken place yet. As a result, we only reset the + # timers after the first step so as to correctly report the time spent + # reading cross sections in the first depletion step, and from there + # correctly report all particle tracking rates in multistep depletion + # solvers. + if self._n_calls > 0: + openmc.lib.reset_timers() + self._update_materials_and_nuclides(vec) # If the source rate is zero, return zero reaction rates without running @@ -449,6 +462,8 @@ class CoupledOperator(OpenMCOperator): op_result = OperatorResult(keff, rates) + self._n_calls += 1 + return copy.deepcopy(op_result) def _update_materials(self): @@ -508,8 +523,6 @@ class CoupledOperator(OpenMCOperator): "openmc_simulation_n{}.h5".format(step), write_source=False) - openmc.lib.reset_timers() - def finalize(self): """Finalize a depletion simulation and release resources.""" if self.cleanup_when_done: From 971c5f77a5ebc276c6aa5c24879f115d1a49a8ed Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 15 Jan 2024 14:33:56 -0600 Subject: [PATCH 032/671] Ensure that implicit complement cells appear last in DAGMC universes (#2838) --- src/cell.cpp | 30 ++++++++++++++++--- .../regression_tests/dagmc/external/main.cpp | 16 +++++++++- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/src/cell.cpp b/src/cell.cpp index c96abf39b..cba52f1b5 100644 --- a/src/cell.cpp +++ b/src/cell.cpp @@ -1020,19 +1020,41 @@ void read_cells(pugi::xml_node node) void populate_universes() { + // Used to map universe index to the index of an implicit complement cell for + // DAGMC universes + std::unordered_map implicit_comp_cells; + // Populate the Universe vector and map. - for (int i = 0; i < model::cells.size(); i++) { - int32_t uid = model::cells[i]->universe_; + for (int index_cell = 0; index_cell < model::cells.size(); index_cell++) { + int32_t uid = model::cells[index_cell]->universe_; auto it = model::universe_map.find(uid); if (it == model::universe_map.end()) { model::universes.push_back(make_unique()); model::universes.back()->id_ = uid; - model::universes.back()->cells_.push_back(i); + model::universes.back()->cells_.push_back(index_cell); model::universe_map[uid] = model::universes.size() - 1; } else { - model::universes[it->second]->cells_.push_back(i); +#ifdef DAGMC + // Skip implicit complement cells for now + Universe* univ = model::universes[it->second].get(); + DAGUniverse* dag_univ = dynamic_cast(univ); + if (dag_univ && (dag_univ->implicit_complement_idx() == index_cell)) { + implicit_comp_cells[it->second] = index_cell; + continue; + } +#endif + + model::universes[it->second]->cells_.push_back(index_cell); } } + + // Add DAGUniverse implicit complement cells last + for (const auto& it : implicit_comp_cells) { + int index_univ = it.first; + int index_cell = it.second; + model::universes[index_univ]->cells_.push_back(index_cell); + } + model::universes.shrink_to_fit(); } diff --git a/tests/regression_tests/dagmc/external/main.cpp b/tests/regression_tests/dagmc/external/main.cpp index 42f7d97c9..bebe088f6 100644 --- a/tests/regression_tests/dagmc/external/main.cpp +++ b/tests/regression_tests/dagmc/external/main.cpp @@ -33,7 +33,7 @@ int main(int argc, char* argv[]) // Initialize acceleration data structures rval = dag_ptr->init_OBBTree(); if (rval != moab::MB_SUCCESS) { - fatal_error("Failed to initialise OBB tree"); + fatal_error("Failed to initialize OBB tree"); } // Get rid of existing geometry @@ -62,6 +62,20 @@ int main(int argc, char* argv[]) // Add cells to universes openmc::populate_universes(); + // Make sure implicit complement appears last + auto dag_univ = dynamic_cast(model::universes.back().get()); + int n = dag_univ->cells_.size(); + for (int i = 0; i < n - 1; ++i) { + if (dag_univ->cells_[i] == dag_univ->implicit_complement_idx()) { + fatal_error("Implicit complement cell should appear last in vector of " + "cells for DAGMC universe."); + } + } + if (dag_univ->cells_.back() != dag_univ->implicit_complement_idx()) { + fatal_error( + "Last cell in DAGMC universe is not an implicit complement cell."); + } + // Set root universe openmc::model::root_universe = openmc::find_root_universe(); openmc::check_dagmc_root_univ(); From 5549b58e1fbebf0f584341d039fede2a44ae0889 Mon Sep 17 00:00:00 2001 From: Gavin Ridley Date: Tue, 16 Jan 2024 10:35:57 -0600 Subject: [PATCH 033/671] Geometron (#2744) --- include/openmc/cell.h | 14 +- include/openmc/dagmc.h | 8 +- include/openmc/geometry.h | 16 +- include/openmc/particle.h | 14 +- include/openmc/particle_data.h | 440 ++++++++++++++++++++------------- include/openmc/plot.h | 8 +- include/openmc/surface.h | 7 +- include/openmc/universe.h | 3 +- src/cell.cpp | 7 +- src/dagmc.cpp | 12 +- src/geometry.cpp | 67 ++--- src/particle.cpp | 18 +- src/particle_data.cpp | 23 +- src/plot.cpp | 43 ++-- src/surface.cpp | 4 +- src/universe.cpp | 3 +- 16 files changed, 403 insertions(+), 284 deletions(-) diff --git a/include/openmc/cell.h b/include/openmc/cell.h index 39acaf067..c405f536b 100644 --- a/include/openmc/cell.h +++ b/include/openmc/cell.h @@ -40,6 +40,7 @@ constexpr int32_t OP_UNION {std::numeric_limits::max() - 4}; //============================================================================== class Cell; +class GeometryState; class ParentCell; class CellInstance; class Universe; @@ -82,7 +83,7 @@ public: //! Find the oncoming boundary of this cell. std::pair distance( - Position r, Direction u, int32_t on_surface, Particle* p) const; + Position r, Direction u, int32_t on_surface) const; //! Get the BoundingBox for this cell. BoundingBox bounding_box(int32_t cell_id) const; @@ -183,7 +184,7 @@ public: //! Find the oncoming boundary of this cell. virtual std::pair distance( - Position r, Direction u, int32_t on_surface, Particle* p) const = 0; + Position r, Direction u, int32_t on_surface, GeometryState* p) const = 0; //! Write all information needed to reconstruct the cell to an HDF5 group. //! \param group_id An HDF5 group id. @@ -260,7 +261,8 @@ protected: //! \param[in] instance of the cell to find parent cells for //! \param[in] p particle used to do a fast search for parent cells //! \return parent cells - vector find_parent_cells(int32_t instance, Particle& p) const; + vector find_parent_cells( + int32_t instance, GeometryState& p) const; //! Determine the path to this cell instance in the geometry hierarchy //! \param[in] instance of the cell to find parent cells for @@ -332,10 +334,10 @@ public: // Methods vector surfaces() const override { return region_.surfaces(); } - std::pair distance( - Position r, Direction u, int32_t on_surface, Particle* p) const override + std::pair distance(Position r, Direction u, + int32_t on_surface, GeometryState* p) const override { - return region_.distance(r, u, on_surface, p); + return region_.distance(r, u, on_surface); } bool contains(Position r, Direction u, int32_t on_surface) const override diff --git a/include/openmc/dagmc.h b/include/openmc/dagmc.h index 5ed426e5a..0b23e567a 100644 --- a/include/openmc/dagmc.h +++ b/include/openmc/dagmc.h @@ -42,7 +42,7 @@ public: double evaluate(Position r) const override; double distance(Position r, Direction u, bool coincident) const override; Direction normal(Position r) const override; - Direction reflect(Position r, Direction u, Particle* p) const override; + Direction reflect(Position r, Direction u, GeometryState* p) const override; inline void to_hdf5_inner(hid_t group_id) const override {}; @@ -63,8 +63,8 @@ public: bool contains(Position r, Direction u, int32_t on_surface) const override; - std::pair distance( - Position r, Direction u, int32_t on_surface, Particle* p) const override; + std::pair distance(Position r, Direction u, + int32_t on_surface, GeometryState* p) const override; BoundingBox bounding_box() const override; @@ -143,7 +143,7 @@ public: //! string of the ID ranges for entities of dimension \p dim std::string dagmc_ids_for_dim(int dim) const; - bool find_cell(Particle& p) const override; + bool find_cell(GeometryState& p) const override; void to_hdf5(hid_t universes_group) const override; diff --git a/include/openmc/geometry.h b/include/openmc/geometry.h index 001e58c4c..107cc7d1f 100644 --- a/include/openmc/geometry.h +++ b/include/openmc/geometry.h @@ -11,7 +11,7 @@ namespace openmc { class BoundaryInfo; -class Particle; +class GeometryState; //============================================================================== // Global variables @@ -39,7 +39,7 @@ inline bool coincident(double d1, double d2) //! Check for overlapping cells at a particle's position. //============================================================================== -bool check_cell_overlap(Particle& p, bool error = true); +bool check_cell_overlap(GeometryState& p, bool error = true); //============================================================================== //! Get the cell instance for a particle at the specified universe level @@ -50,7 +50,7 @@ bool check_cell_overlap(Particle& p, bool error = true); //! should be computed. \return The instance of the cell at the specified level. //============================================================================== -int cell_instance_at_level(const Particle& p, int level); +int cell_instance_at_level(const GeometryState& p, int level); //============================================================================== //! Locate a particle in the geometry tree and set its geometry data fields. @@ -60,20 +60,22 @@ int cell_instance_at_level(const Particle& p, int level); //! \return True if the particle's location could be found and ascribed to a //! valid geometry coordinate stack. //============================================================================== -bool exhaustive_find_cell(Particle& p); -bool neighbor_list_find_cell(Particle& p); // Only usable on surface crossings +bool exhaustive_find_cell(GeometryState& p, bool verbose = false); +bool neighbor_list_find_cell( + GeometryState& p, bool verbose = false); // Only usable on surface crossings //============================================================================== //! Move a particle into a new lattice tile. //============================================================================== -void cross_lattice(Particle& p, const BoundaryInfo& boundary); +void cross_lattice( + GeometryState& p, const BoundaryInfo& boundary, bool verbose = false); //============================================================================== //! Find the next boundary a particle will intersect. //============================================================================== -BoundaryInfo distance_to_boundary(Particle& p); +BoundaryInfo distance_to_boundary(GeometryState& p); } // namespace openmc diff --git a/include/openmc/particle.h b/include/openmc/particle.h index 80a561f4d..e423880f8 100644 --- a/include/openmc/particle.h +++ b/include/openmc/particle.h @@ -5,7 +5,6 @@ //! \brief Particle type #include -#include #include #include "openmc/constants.h" @@ -103,17 +102,8 @@ public: //! mark a particle as lost and create a particle restart file //! \param message A warning message to display - void mark_as_lost(const char* message); - - void mark_as_lost(const std::string& message) - { - mark_as_lost(message.c_str()); - } - - void mark_as_lost(const std::stringstream& message) - { - mark_as_lost(message.str()); - } + virtual void mark_as_lost(const char* message) override; + using GeometryState::mark_as_lost; //! create a particle restart HDF5 file void write_restart() const; diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index 37eb05bdb..cb68fc457 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -194,6 +194,157 @@ struct BoundaryInfo { lattice_translation {}; //!< which way lattice indices will change }; +/* + * Contains all geometry state information for a particle. + */ +class GeometryState { +public: + GeometryState(); + + /* + * GeometryState does not store any ID info, so give some reasonable behavior + * here. The Particle class redefines this. This is only here for the error + * reporting behavior that occurs in geometry.cpp. The explanation for + * mark_as_lost is the same. + */ + virtual void mark_as_lost(const char* message); + void mark_as_lost(const std::string& message); + void mark_as_lost(const std::stringstream& message); + + // resets all coordinate levels for the particle + void clear() + { + for (auto& level : coord_) + level.reset(); + n_coord_ = 1; + } + + // Initialize all internal state from position and direction + void init_from_r_u(Position r_a, Direction u_a) + { + clear(); + surface() = 0; + material() = C_NONE; + r() = r_a; + u() = u_a; + r_last_current() = r_a; + r_last() = r_a; + u_last() = u_a; + } + + // Unique ID. This is not geometric info, but the + // error reporting in geometry.cpp requires this. + // We could save this to implement it in Particle, + // but that would require virtuals. + int64_t& id() { return id_; } + const int64_t& id() const { return id_; } + + // Number of current coordinate levels + int& n_coord() { return n_coord_; } + const int& n_coord() const { return n_coord_; } + + // Offset for distributed properties + int& cell_instance() { return cell_instance_; } + const int& cell_instance() const { return cell_instance_; } + + // Coordinates for all nesting levels + LocalCoord& coord(int i) { return coord_[i]; } + const LocalCoord& coord(int i) const { return coord_[i]; } + const vector& coord() const { return coord_; } + + // Innermost universe nesting coordinates + LocalCoord& lowest_coord() { return coord_[n_coord_ - 1]; } + const LocalCoord& lowest_coord() const { return coord_[n_coord_ - 1]; } + + // Last coordinates on all nesting levels, before crossing a surface + int& n_coord_last() { return n_coord_last_; } + const int& n_coord_last() const { return n_coord_last_; } + int& cell_last(int i) { return cell_last_[i]; } + const int& cell_last(int i) const { return cell_last_[i]; } + + // Coordinates of last collision or reflective/periodic surface + // crossing for current tallies + Position& r_last_current() { return r_last_current_; } + const Position& r_last_current() const { return r_last_current_; } + + // Previous direction and spatial coordinates before a collision + Position& r_last() { return r_last_; } + const Position& r_last() const { return r_last_; } + Position& u_last() { return u_last_; } + const Position& u_last() const { return u_last_; } + + // Accessors for position in global coordinates + Position& r() { return coord_[0].r; } + const Position& r() const { return coord_[0].r; } + + // Accessors for position in local coordinates + Position& r_local() { return coord_[n_coord_ - 1].r; } + const Position& r_local() const { return coord_[n_coord_ - 1].r; } + + // Accessors for direction in global coordinates + Direction& u() { return coord_[0].u; } + const Direction& u() const { return coord_[0].u; } + + // Accessors for direction in local coordinates + Direction& u_local() { return coord_[n_coord_ - 1].u; } + const Direction& u_local() const { return coord_[n_coord_ - 1].u; } + + // Surface that the particle is on + int& surface() { return surface_; } + const int& surface() const { return surface_; } + + // Boundary information + BoundaryInfo& boundary() { return boundary_; } + +#ifdef DAGMC + // DagMC state variables + moab::DagMC::RayHistory& history() { return history_; } + Direction& last_dir() { return last_dir_; } +#endif + + // material of current and last cell + int& material() { return material_; } + const int& material() const { return material_; } + int& material_last() { return material_last_; } + const int& material_last() const { return material_last_; } + + // temperature of current and last cell + double& sqrtkT() { return sqrtkT_; } + const double& sqrtkT() const { return sqrtkT_; } + double& sqrtkT_last() { return sqrtkT_last_; } + +private: + int64_t id_ {-1}; //!< Unique ID + + int n_coord_ {1}; //!< number of current coordinate levels + int cell_instance_; //!< offset for distributed properties + vector coord_; //!< coordinates for all levels + + int n_coord_last_ {1}; //!< number of current coordinates + vector cell_last_; //!< coordinates for all levels + + Position r_last_current_; //!< coordinates of the last collision or + //!< reflective/periodic surface crossing for + //!< current tallies + Position r_last_; //!< previous coordinates + Direction u_last_; //!< previous direction coordinates + + int surface_ {0}; //!< index for surface particle is on + + BoundaryInfo boundary_; //!< Info about the next intersection + + int material_ {-1}; //!< index for current material + int material_last_ {-1}; //!< index for last material + + double sqrtkT_ {-1.0}; //!< sqrt(k_Boltzmann * temperature) in eV + double sqrtkT_last_ {0.0}; //!< last temperature + +#ifdef DAGMC + moab::DagMC::RayHistory history_; + Direction last_dir_; +#endif +}; + //============================================================================ //! Defines how particle data is laid out in memory //============================================================================ @@ -229,163 +380,112 @@ struct BoundaryInfo { * Algorithms.” Annals of Nuclear Energy 113 (March 2018): 506–18. * https://doi.org/10.1016/j.anucene.2017.11.032. */ -class ParticleData { -public: - //---------------------------------------------------------------------------- - // Constructors - ParticleData(); - +class ParticleData : public GeometryState { private: //========================================================================== - // Data members (accessor methods are below) + // Data members -- see public: below for descriptions - // Cross section caches - vector neutron_xs_; //!< Microscopic neutron cross sections - vector photon_xs_; //!< Microscopic photon cross sections - MacroXS macro_xs_; //!< Macroscopic cross sections - CacheDataMG mg_xs_cache_; //!< Multigroup XS cache + vector neutron_xs_; + vector photon_xs_; + MacroXS macro_xs_; + CacheDataMG mg_xs_cache_; - int64_t id_; //!< Unique ID - ParticleType type_ {ParticleType::neutron}; //!< Particle type (n, p, e, etc.) + ParticleType type_ {ParticleType::neutron}; - int n_coord_ {1}; //!< number of current coordinate levels - int cell_instance_; //!< offset for distributed properties - vector coord_; //!< coordinates for all levels + double E_; + double E_last_; + int g_ {0}; + int g_last_; - // Particle coordinates before crossing a surface - int n_coord_last_ {1}; //!< number of current coordinates - vector cell_last_; //!< coordinates for all levels + double wgt_ {1.0}; + double mu_; + double time_ {0.0}; + double time_last_ {0.0}; + double wgt_last_ {1.0}; - // Energy data - double E_; //!< post-collision energy in eV - double E_last_; //!< pre-collision energy in eV - int g_ {C_NONE}; //!< post-collision energy group (MG only) - int g_last_; //!< pre-collision energy group (MG only) + bool fission_ {false}; + TallyEvent event_; + int event_nuclide_; + int event_mt_; + int delayed_group_ {0}; - // Other physical data - double wgt_ {1.0}; //!< particle weight - double mu_; //!< angle of scatter - double time_ {0.0}; //!< time in [s] - double time_last_ {0.0}; //!< previous time in [s] + int n_bank_ {0}; + int n_bank_second_ {0}; + double wgt_bank_ {0.0}; + int n_delayed_bank_[MAX_DELAYED_GROUPS]; - // Other physical data - Position r_last_current_; //!< coordinates of the last collision or - //!< reflective/periodic surface crossing for - //!< current tallies - Position r_last_; //!< previous coordinates - Direction u_last_; //!< previous direction coordinates - double wgt_last_ {1.0}; //!< pre-collision particle weight + int cell_born_ {-1}; - // What event took place - bool fission_ {false}; //!< did particle cause implicit fission - TallyEvent event_; //!< scatter, absorption - int event_nuclide_; //!< index in nuclides array - int event_mt_; //!< reaction MT - int delayed_group_ {0}; //!< delayed group + int n_collision_ {0}; - // Post-collision physical data - int n_bank_ {0}; //!< number of fission sites banked - int n_bank_second_ {0}; //!< number of secondary particles banked - double wgt_bank_ {0.0}; //!< weight of fission sites banked - int n_delayed_bank_[MAX_DELAYED_GROUPS]; //!< number of delayed fission - //!< sites banked - - // Indices for various arrays - int surface_ {0}; //!< index for surface particle is on - int cell_born_ {-1}; //!< index for cell particle was born in - int material_ {-1}; //!< index for current material - int material_last_ {-1}; //!< index for last material - - // Boundary information - BoundaryInfo boundary_; - - // Temperature of current cell - double sqrtkT_ {-1.0}; //!< sqrt(k_Boltzmann * temperature) in eV - double sqrtkT_last_ {0.0}; //!< last temperature - - // Statistical data - int n_collision_ {0}; //!< number of collisions - - // Track output bool write_track_ {false}; - // Current PRNG state - uint64_t seeds_[N_STREAMS]; // current seeds - int stream_; // current RNG stream + uint64_t seeds_[N_STREAMS]; + int stream_; - // Secondary particle bank vector secondary_bank_; - int64_t current_work_; // current work index + int64_t current_work_; - vector flux_derivs_; // for derivatives for this particle + vector flux_derivs_; - vector filter_matches_; // tally filter matches + vector filter_matches_; - vector tracks_; // tracks for outputting to file + vector tracks_; - vector nu_bank_; // bank of most recently fissioned particles + vector nu_bank_; - vector pht_storage_; // interim pulse-height results + vector pht_storage_; - // Global tally accumulators double keff_tally_absorption_ {0.0}; double keff_tally_collision_ {0.0}; double keff_tally_tracklength_ {0.0}; double keff_tally_leakage_ {0.0}; - bool trace_ {false}; //!< flag to show debug information + bool trace_ {false}; - double collision_distance_; // distance to particle's next closest collision + double collision_distance_; - int n_event_ {0}; // number of events executed in this particle's history + int n_event_ {0}; - // Weight window information - int n_split_ {0}; // Number of times this particle has been split - double ww_factor_ { - 0.0}; // Particle-specific factor for on-the-fly weight window adjustment + int n_split_ {0}; + double ww_factor_ {0.0}; -// DagMC state variables -#ifdef DAGMC - moab::DagMC::RayHistory history_; - Direction last_dir_; -#endif - - int64_t n_progeny_ {0}; // Number of progeny produced by this particle + int64_t n_progeny_ {0}; public: + //---------------------------------------------------------------------------- + // Constructors + ParticleData(); + //========================================================================== // Methods and accessors - NuclideMicroXS& neutron_xs(int i) { return neutron_xs_[i]; } + // Cross section caches + NuclideMicroXS& neutron_xs(int i) + { + return neutron_xs_[i]; + } // Microscopic neutron cross sections const NuclideMicroXS& neutron_xs(int i) const { return neutron_xs_[i]; } + + // Microscopic photon cross sections ElementMicroXS& photon_xs(int i) { return photon_xs_[i]; } + + // Macroscopic cross sections MacroXS& macro_xs() { return macro_xs_; } const MacroXS& macro_xs() const { return macro_xs_; } + + // Multigroup macroscopic cross sections CacheDataMG& mg_xs_cache() { return mg_xs_cache_; } const CacheDataMG& mg_xs_cache() const { return mg_xs_cache_; } - int64_t& id() { return id_; } - const int64_t& id() const { return id_; } + // Particle type (n, p, e, gamma, etc) ParticleType& type() { return type_; } const ParticleType& type() const { return type_; } - int& n_coord() { return n_coord_; } - const int& n_coord() const { return n_coord_; } - int& cell_instance() { return cell_instance_; } - const int& cell_instance() const { return cell_instance_; } - LocalCoord& coord(int i) { return coord_[i]; } - const LocalCoord& coord(int i) const { return coord_[i]; } - const vector& coord() const { return coord_; } - - LocalCoord& lowest_coord() { return coord_[n_coord_ - 1]; } - const LocalCoord& lowest_coord() const { return coord_[n_coord_ - 1]; } - - int& n_coord_last() { return n_coord_last_; } - const int& n_coord_last() const { return n_coord_last_; } - int& cell_last(int i) { return cell_last_[i]; } - const int& cell_last(int i) const { return cell_last_[i]; } - + // Current particle energy, energy before collision, + // and corresponding multigroup group indices. Energy + // units are eV. double& E() { return E_; } const double& E() const { return E_; } double& E_last() { return E_last_; } @@ -395,113 +495,119 @@ public: int& g_last() { return g_last_; } const int& g_last() const { return g_last_; } + // Statistic weight of particle. Setting to zero + // indicates that the particle is dead. double& wgt() { return wgt_; } double wgt() const { return wgt_; } + double& wgt_last() { return wgt_last_; } + const double& wgt_last() const { return wgt_last_; } + bool alive() const { return wgt_ != 0.0; } + + // Polar scattering angle after a collision double& mu() { return mu_; } const double& mu() const { return mu_; } + + // Tracks the time of a particle as it traverses the problem. + // Units are seconds. double& time() { return time_; } const double& time() const { return time_; } double& time_last() { return time_last_; } const double& time_last() const { return time_last_; } - bool alive() const { return wgt_ != 0.0; } - Position& r_last_current() { return r_last_current_; } - const Position& r_last_current() const { return r_last_current_; } - Position& r_last() { return r_last_; } - const Position& r_last() const { return r_last_; } - Position& u_last() { return u_last_; } - const Position& u_last() const { return u_last_; } - double& wgt_last() { return wgt_last_; } - const double& wgt_last() const { return wgt_last_; } - - bool& fission() { return fission_; } + // What event took place, described in greater detail below TallyEvent& event() { return event_; } const TallyEvent& event() const { return event_; } - int& event_nuclide() { return event_nuclide_; } + bool& fission() { return fission_; } // true if implicit fission + int& event_nuclide() { return event_nuclide_; } // index of collision nuclide const int& event_nuclide() const { return event_nuclide_; } - int& event_mt() { return event_mt_; } - int& delayed_group() { return delayed_group_; } + int& event_mt() { return event_mt_; } // MT number of collision + int& delayed_group() { return delayed_group_; } // delayed group - int& n_bank() { return n_bank_; } - int& n_bank_second() { return n_bank_second_; } - double& wgt_bank() { return wgt_bank_; } - int* n_delayed_bank() { return n_delayed_bank_; } - int& n_delayed_bank(int i) { return n_delayed_bank_[i]; } + // Post-collision data + int& n_bank() { return n_bank_; } // number of banked fission sites + int& n_bank_second() + { + return n_bank_second_; + } // number of secondaries banked + double& wgt_bank() { return wgt_bank_; } // weight of banked fission sites + int* n_delayed_bank() + { + return n_delayed_bank_; + } // number of delayed fission sites + int& n_delayed_bank(int i) + { + return n_delayed_bank_[i]; + } // number of delayed fission sites - int& surface() { return surface_; } - const int& surface() const { return surface_; } + // Index of cell particle is born in int& cell_born() { return cell_born_; } const int& cell_born() const { return cell_born_; } - int& material() { return material_; } - const int& material() const { return material_; } - int& material_last() { return material_last_; } - const int& material_last() const { return material_last_; } - - BoundaryInfo& boundary() { return boundary_; } - - double& sqrtkT() { return sqrtkT_; } - const double& sqrtkT() const { return sqrtkT_; } - double& sqrtkT_last() { return sqrtkT_last_; } + // index of the current and last material + // Total number of collisions suffered by particle int& n_collision() { return n_collision_; } const int& n_collision() const { return n_collision_; } + // whether this track is to be written bool& write_track() { return write_track_; } + + // RNG state uint64_t& seeds(int i) { return seeds_[i]; } uint64_t* seeds() { return seeds_; } int& stream() { return stream_; } + // secondary particle bank SourceSite& secondary_bank(int i) { return secondary_bank_[i]; } decltype(secondary_bank_)& secondary_bank() { return secondary_bank_; } + + // Current simulation work index int64_t& current_work() { return current_work_; } const int64_t& current_work() const { return current_work_; } + + // Used in tally derivatives double& flux_derivs(int i) { return flux_derivs_[i]; } const double& flux_derivs(int i) const { return flux_derivs_[i]; } + + // Matches of tallies decltype(filter_matches_)& filter_matches() { return filter_matches_; } FilterMatch& filter_matches(int i) { return filter_matches_[i]; } + + // Tracks to output to file decltype(tracks_)& tracks() { return tracks_; } + + // Bank of recently fissioned particles decltype(nu_bank_)& nu_bank() { return nu_bank_; } NuBank& nu_bank(int i) { return nu_bank_[i]; } + + // Interim pulse height tally storage vector& pht_storage() { return pht_storage_; } + // Global tally accumulators double& keff_tally_absorption() { return keff_tally_absorption_; } double& keff_tally_collision() { return keff_tally_collision_; } double& keff_tally_tracklength() { return keff_tally_tracklength_; } double& keff_tally_leakage() { return keff_tally_leakage_; } + // Shows debug info bool& trace() { return trace_; } + + // Distance to the next collision double& collision_distance() { return collision_distance_; } + + // Number of events particle has undergone int& n_event() { return n_event_; } + // Number of times variance reduction has caused a particle split int n_split() const { return n_split_; } int& n_split() { return n_split_; } + // Particle-specific factor for on-the-fly weight window adjustment double ww_factor() const { return ww_factor_; } double& ww_factor() { return ww_factor_; } -#ifdef DAGMC - moab::DagMC::RayHistory& history() { return history_; } - Direction& last_dir() { return last_dir_; } -#endif - + // Number of progeny produced by this particle int64_t& n_progeny() { return n_progeny_; } - // Accessors for position in global coordinates - Position& r() { return coord_[0].r; } - const Position& r() const { return coord_[0].r; } - - // Accessors for position in local coordinates - Position& r_local() { return coord_[n_coord_ - 1].r; } - const Position& r_local() const { return coord_[n_coord_ - 1].r; } - - // Accessors for direction in global coordinates - Direction& u() { return coord_[0].u; } - const Direction& u() const { return coord_[0].u; } - - // Accessors for direction in local coordinates - Direction& u_local() { return coord_[n_coord_ - 1].u; } - const Direction& u_local() const { return coord_[n_coord_ - 1].u; } - //! Gets the pointer to the particle's current PRN seed uint64_t* current_seed() { return seeds_ + stream_; } const uint64_t* current_seed() const { return seeds_ + stream_; } @@ -513,14 +619,6 @@ public: micro.last_E = 0.0; } - //! resets all coordinate levels for the particle - void clear() - { - for (auto& level : coord_) - level.reset(); - n_coord_ = 1; - } - //! Get track information based on particle's current state TrackState get_track_state() const; diff --git a/include/openmc/plot.h b/include/openmc/plot.h index ba82f43e2..7b66036fb 100644 --- a/include/openmc/plot.h +++ b/include/openmc/plot.h @@ -121,7 +121,7 @@ struct IdData { IdData(size_t h_res, size_t v_res); // Methods - void set_value(size_t y, size_t x, const Particle& p, int level); + void set_value(size_t y, size_t x, const GeometryState& p, int level); void set_overlap(size_t y, size_t x); // Members @@ -133,7 +133,7 @@ struct PropertyData { PropertyData(size_t h_res, size_t v_res); // Methods - void set_value(size_t y, size_t x, const Particle& p, int level); + void set_value(size_t y, size_t x, const GeometryState& p, int level); void set_overlap(size_t y, size_t x); // Members @@ -205,7 +205,7 @@ T SlicePlotBase::get_map() const #pragma omp parallel { - Particle p; + GeometryState p; p.r() = xyz; p.u() = dir; p.coord(0).universe = model::root_universe; @@ -291,7 +291,7 @@ private: * find a distance to the boundary in a non-standard surface intersection * check. It's an exhaustive search over surfaces in the top-level universe. */ - static int advance_to_boundary_from_void(Particle& p); + static int advance_to_boundary_from_void(GeometryState& p); /* Checks if a vector of two TrackSegments is equivalent. We define this * to mean not having matching intersection lengths, but rather having diff --git a/include/openmc/surface.h b/include/openmc/surface.h index 3bb84c6e0..350775123 100644 --- a/include/openmc/surface.h +++ b/include/openmc/surface.h @@ -105,12 +105,13 @@ public: //! Determine the direction of a ray reflected from the surface. //! \param[in] r The point at which the ray is incident. //! \param[in] u Incident direction of the ray - //! \param[inout] p Pointer to the particle + //! \param[inout] p Pointer to the particle. Only DAGMC uses this. //! \return Outgoing direction of the ray - virtual Direction reflect(Position r, Direction u, Particle* p) const; + virtual Direction reflect( + Position r, Direction u, GeometryState* p = nullptr) const; virtual Direction diffuse_reflect( - Position r, Direction u, uint64_t* seed) const; + Position r, Direction u, uint64_t* seed, GeometryState* p = nullptr) const; //! Evaluate the equation describing the surface. //! diff --git a/include/openmc/universe.h b/include/openmc/universe.h index b98d2d57f..26f33cb38 100644 --- a/include/openmc/universe.h +++ b/include/openmc/universe.h @@ -9,6 +9,7 @@ namespace openmc { class DAGUniverse; #endif +class GeometryState; class Universe; class UniversePartitioner; @@ -32,7 +33,7 @@ public: //! \param group_id An HDF5 group id. virtual void to_hdf5(hid_t group_id) const; - virtual bool find_cell(Particle& p) const; + virtual bool find_cell(GeometryState& p) const; BoundingBox bounding_box() const; diff --git a/src/cell.cpp b/src/cell.cpp index cba52f1b5..00083d967 100644 --- a/src/cell.cpp +++ b/src/cell.cpp @@ -784,7 +784,7 @@ std::string Region::str() const //============================================================================== std::pair Region::distance( - Position r, Direction u, int32_t on_surface, Particle* p) const + Position r, Direction u, int32_t on_surface) const { double min_dist {INFTY}; int32_t i_surf {std::numeric_limits::max()}; @@ -1301,14 +1301,15 @@ vector Cell::find_parent_cells( { // create a temporary particle - Particle dummy_particle {}; + GeometryState dummy_particle {}; dummy_particle.r() = r; dummy_particle.u() = {0., 0., 1.}; return find_parent_cells(instance, dummy_particle); } -vector Cell::find_parent_cells(int32_t instance, Particle& p) const +vector Cell::find_parent_cells( + int32_t instance, GeometryState& p) const { // look up the particle's location exhaustive_find_cell(p); diff --git a/src/dagmc.cpp b/src/dagmc.cpp index 4ffe4e652..561fdca81 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -404,7 +404,7 @@ int32_t DAGUniverse::implicit_complement_idx() const return cell_idx_offset_ + dagmc_instance_->index_by_handle(ic) - 1; } -bool DAGUniverse::find_cell(Particle& p) const +bool DAGUniverse::find_cell(GeometryState& p) const { // if the particle isn't in any of the other DagMC // cells, place it in the implicit complement @@ -583,9 +583,8 @@ DAGCell::DAGCell(std::shared_ptr dag_ptr, int32_t dag_idx) }; std::pair DAGCell::distance( - Position r, Direction u, int32_t on_surface, Particle* p) const + Position r, Direction u, int32_t on_surface, GeometryState* p) const { - Expects(p); // if we've changed direction or we're not on a surface, // reset the history and update last direction if (u != p->last_dir()) { @@ -635,10 +634,9 @@ std::pair DAGCell::distance( p->material() == MATERIAL_VOID ? "-1 (VOID)" : std::to_string(model::materials[p->material()]->id()); - auto lost_particle_msg = fmt::format( + p->mark_as_lost(fmt::format( "No intersection found with DAGMC cell {}, filled with material {}", id_, - material_id); - p->mark_as_lost(lost_particle_msg); + material_id)); } return {dist, surf_idx}; @@ -723,7 +721,7 @@ Direction DAGSurface::normal(Position r) const return dir; } -Direction DAGSurface::reflect(Position r, Direction u, Particle* p) const +Direction DAGSurface::reflect(Position r, Direction u, GeometryState* p) const { Expects(p); p->history().reset_to_last_intersection(); diff --git a/src/geometry.cpp b/src/geometry.cpp index 47152e172..a16addd48 100644 --- a/src/geometry.cpp +++ b/src/geometry.cpp @@ -32,7 +32,7 @@ vector overlap_check_count; // Non-member functions //============================================================================== -bool check_cell_overlap(Particle& p, bool error) +bool check_cell_overlap(GeometryState& p, bool error) { int n_coord = p.n_coord(); @@ -63,7 +63,7 @@ bool check_cell_overlap(Particle& p, bool error) //============================================================================== -int cell_instance_at_level(const Particle& p, int level) +int cell_instance_at_level(const GeometryState& p, int level) { // throw error if the requested level is too deep for the geometry if (level > model::n_coord_levels) { @@ -99,7 +99,8 @@ int cell_instance_at_level(const Particle& p, int level) //============================================================================== -bool find_cell_inner(Particle& p, const NeighborList* neighbor_list) +bool find_cell_inner( + GeometryState& p, const NeighborList* neighbor_list, bool verbose) { // Find which cell of this universe the particle is in. Use the neighbor list // to shorten the search if one was provided. @@ -156,7 +157,7 @@ bool find_cell_inner(Particle& p, const NeighborList* neighbor_list) i_cell = p.lowest_coord().cell; // Announce the cell that the particle is entering. - if (found && (settings::verbosity >= 10 || p.trace())) { + if (found && verbose) { auto msg = fmt::format(" Entering cell {}", model::cells[i_cell]->id_); write_message(msg, 1); } @@ -243,10 +244,9 @@ bool find_cell_inner(Particle& p, const NeighborList* neighbor_list) if (lat.outer_ != NO_OUTER_UNIVERSE) { coord.universe = lat.outer_; } else { - warning(fmt::format("Particle {} is outside lattice {} but the " - "lattice has no defined outer universe.", + p.mark_as_lost(fmt::format( + "Particle {} left lattice {}, but it has no outer definition.", p.id(), lat.id_)); - return false; } } } @@ -259,7 +259,7 @@ bool find_cell_inner(Particle& p, const NeighborList* neighbor_list) //============================================================================== -bool neighbor_list_find_cell(Particle& p) +bool neighbor_list_find_cell(GeometryState& p, bool verbose) { // Reset all the deeper coordinate levels. @@ -274,20 +274,20 @@ bool neighbor_list_find_cell(Particle& p) // Search for the particle in that cell's neighbor list. Return if we // found the particle. - bool found = find_cell_inner(p, &c.neighbors_); + bool found = find_cell_inner(p, &c.neighbors_, verbose); if (found) return found; // The particle could not be found in the neighbor list. Try searching all // cells in this universe, and update the neighbor list if we find a new // neighboring cell. - found = find_cell_inner(p, nullptr); + found = find_cell_inner(p, nullptr, verbose); if (found) c.neighbors_.push_back(p.coord(coord_lvl).cell); return found; } -bool exhaustive_find_cell(Particle& p) +bool exhaustive_find_cell(GeometryState& p, bool verbose) { int i_universe = p.lowest_coord().universe; if (i_universe == C_NONE) { @@ -299,17 +299,17 @@ bool exhaustive_find_cell(Particle& p) for (int i = p.n_coord(); i < model::n_coord_levels; i++) { p.coord(i).reset(); } - return find_cell_inner(p, nullptr); + return find_cell_inner(p, nullptr, verbose); } //============================================================================== -void cross_lattice(Particle& p, const BoundaryInfo& boundary) +void cross_lattice(GeometryState& p, const BoundaryInfo& boundary, bool verbose) { auto& coord {p.lowest_coord()}; auto& lat {*model::lattices[coord.lattice]}; - if (settings::verbosity >= 10 || p.trace()) { + if (verbose) { write_message( fmt::format(" Crossing lattice {}. Current position ({},{},{}). r={}", lat.id_, coord.lattice_i[0], coord.lattice_i[1], coord.lattice_i[2], @@ -336,10 +336,11 @@ void cross_lattice(Particle& p, const BoundaryInfo& boundary) // The particle is outside the lattice. Search for it from the base coords. p.n_coord() = 1; bool found = exhaustive_find_cell(p); - if (!found && p.alive()) { - p.mark_as_lost(fmt::format("Could not locate particle {} after " - "crossing a lattice boundary", - p.id())); + + if (!found) { + p.mark_as_lost(fmt::format("Particle {} could not be located after " + "crossing a boundary of lattice {}", + p.id(), lat.id_)); } } else { @@ -352,10 +353,10 @@ void cross_lattice(Particle& p, const BoundaryInfo& boundary) // this case, search for it from the base coords. p.n_coord() = 1; bool found = exhaustive_find_cell(p); - if (!found && p.alive()) { - p.mark_as_lost(fmt::format("Could not locate particle {} after " - "crossing a lattice boundary", - p.id())); + if (!found) { + p.mark_as_lost(fmt::format("Particle {} could not be located after " + "crossing a boundary of lattice {}", + p.id(), lat.id_)); } } } @@ -363,7 +364,7 @@ void cross_lattice(Particle& p, const BoundaryInfo& boundary) //============================================================================== -BoundaryInfo distance_to_boundary(Particle& p) +BoundaryInfo distance_to_boundary(GeometryState& p) { BoundaryInfo info; double d_lat = INFINITY; @@ -408,8 +409,9 @@ BoundaryInfo distance_to_boundary(Particle& p) level_lat_trans = lattice_distance.second; if (d_lat < 0) { - p.mark_as_lost(fmt::format( - "Particle {} had a negative distance to a lattice boundary", p.id())); + p.mark_as_lost(fmt::format("Particle {} had a negative distance " + "to a lattice boundary.", + p.id())); } } @@ -463,18 +465,19 @@ BoundaryInfo distance_to_boundary(Particle& p) extern "C" int openmc_find_cell( const double* xyz, int32_t* index, int32_t* instance) { - Particle p; + GeometryState geom_state; - p.r() = Position {xyz}; - p.u() = {0.0, 0.0, 1.0}; + geom_state.r() = Position {xyz}; + geom_state.u() = {0.0, 0.0, 1.0}; - if (!exhaustive_find_cell(p)) { - set_errmsg(fmt::format("Could not find cell at position {}.", p.r())); + if (!exhaustive_find_cell(geom_state)) { + set_errmsg( + fmt::format("Could not find cell at position {}.", geom_state.r())); return OPENMC_E_GEOMETRY; } - *index = p.lowest_coord().cell; - *instance = p.cell_instance(); + *index = geom_state.lowest_coord().cell; + *instance = geom_state.cell_instance(); return 0; } diff --git a/src/particle.cpp b/src/particle.cpp index 8992bc868..549de5cff 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -277,7 +277,9 @@ void Particle::event_cross_surface() boundary().lattice_translation[1] != 0 || boundary().lattice_translation[2] != 0) { // Particle crosses lattice boundary - cross_lattice(*this, boundary()); + + bool verbose = settings::verbosity >= 10 || trace(); + cross_lattice(*this, boundary(), verbose); event() = TallyEvent::LATTICE; } else { // Particle crosses surface @@ -406,7 +408,8 @@ void Particle::event_revive_from_secondary() // have to determine it before the energy of the secondary particle can be // removed from the pulse-height of this cell. if (lowest_coord().cell == C_NONE) { - if (!exhaustive_find_cell(*this)) { + bool verbose = settings::verbosity >= 10 || trace(); + if (!exhaustive_find_cell(*this, verbose)) { mark_as_lost("Could not find the cell containing particle " + std::to_string(id())); return; @@ -556,7 +559,8 @@ void Particle::cross_surface() } #endif - if (neighbor_list_find_cell(*this)) + bool verbose = settings::verbosity >= 10 || trace(); + if (neighbor_list_find_cell(*this, verbose)) return; // ========================================================================== @@ -564,7 +568,7 @@ void Particle::cross_surface() // Remove lower coordinate levels n_coord() = 1; - bool found = exhaustive_find_cell(*this); + bool found = exhaustive_find_cell(*this, verbose); if (settings::run_mode != RunMode::PLOTTING && (!found)) { // If a cell is still not found, there are two possible causes: 1) there is @@ -579,7 +583,7 @@ void Particle::cross_surface() // Couldn't find next cell anywhere! This probably means there is an actual // undefined region in the geometry. - if (!exhaustive_find_cell(*this)) { + if (!exhaustive_find_cell(*this, verbose)) { mark_as_lost("After particle " + std::to_string(id()) + " crossed surface " + std::to_string(surf->id_) + " it could not be located in any cell and it did not leak."); @@ -654,8 +658,8 @@ void Particle::cross_reflective_bc(const Surface& surf, Direction new_u) // (unless we're using a dagmc model, which has exactly one universe) n_coord() = 1; if (surf.geom_type_ != GeometryType::DAG && !neighbor_list_find_cell(*this)) { - this->mark_as_lost("Couldn't find particle after reflecting from surface " + - std::to_string(surf.id_) + "."); + mark_as_lost("Couldn't find particle after reflecting from surface " + + std::to_string(surf.id_) + "."); return; } diff --git a/src/particle_data.cpp b/src/particle_data.cpp index 0fc7202c5..d8a5bb9d9 100644 --- a/src/particle_data.cpp +++ b/src/particle_data.cpp @@ -1,6 +1,9 @@ #include "openmc/particle_data.h" +#include + #include "openmc/cell.h" +#include "openmc/error.h" #include "openmc/geometry.h" #include "openmc/material.h" #include "openmc/nuclide.h" @@ -12,6 +15,21 @@ namespace openmc { +void GeometryState::mark_as_lost(const std::string& message) +{ + mark_as_lost(message.c_str()); +} + +void GeometryState::mark_as_lost(const std::stringstream& message) +{ + mark_as_lost(message.str()); +} + +void GeometryState::mark_as_lost(const char* message) +{ + fatal_error(message); +} + void LocalCoord::rotate(const vector& rotation) { r = r.rotate(rotation); @@ -30,13 +48,16 @@ void LocalCoord::reset() rotated = false; } -ParticleData::ParticleData() +GeometryState::GeometryState() { // Create and clear coordinate levels coord_.resize(model::n_coord_levels); cell_last_.resize(model::n_coord_levels); clear(); +} +ParticleData::ParticleData() +{ zero_delayed_bank(); // Every particle starts with no accumulated flux derivative. Note that in diff --git a/src/plot.cpp b/src/plot.cpp index 8d995b0a9..bf733cff4 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -45,7 +45,7 @@ constexpr int32_t OVERLAP {-3}; IdData::IdData(size_t h_res, size_t v_res) : data_({v_res, h_res, 3}, NOT_FOUND) {} -void IdData::set_value(size_t y, size_t x, const Particle& p, int level) +void IdData::set_value(size_t y, size_t x, const GeometryState& p, int level) { // set cell data if (p.n_coord() <= level) { @@ -78,7 +78,8 @@ PropertyData::PropertyData(size_t h_res, size_t v_res) : data_({v_res, h_res, 2}, NOT_FOUND) {} -void PropertyData::set_value(size_t y, size_t x, const Particle& p, int level) +void PropertyData::set_value( + size_t y, size_t x, const GeometryState& p, int level) { Cell* c = model::cells.at(p.lowest_coord().cell).get(); data_(y, x, 0) = (p.sqrtkT() * p.sqrtkT()) / K_BOLTZMANN; @@ -1084,7 +1085,7 @@ void ProjectionPlot::set_output_path(pugi::xml_node node) // Advances to the next boundary from outside the geometry // Returns -1 if no intersection found, and the surface index // if an intersection was found. -int ProjectionPlot::advance_to_boundary_from_void(Particle& p) +int ProjectionPlot::advance_to_boundary_from_void(GeometryState& p) { constexpr double scoot = 1e-5; double min_dist = {INFINITY}; @@ -1234,20 +1235,8 @@ void ProjectionPlot::create_output() const const int n_threads = num_threads(); const int tid = thread_num(); - SourceSite s; // Where particle starts from (camera) - s.E = 1; - s.wgt = 1; - s.delayed_group = 0; - s.particle = ParticleType::photon; // just has to be something reasonable - s.parent_id = 1; - s.progeny_id = 2; - s.r = camera_position_; - - Particle p; - s.u.x = 1.0; - s.u.y = 0.0; - s.u.z = 0.0; - p.from_source(&s); + GeometryState p; + p.u() = {1.0, 0.0, 0.0}; int vert = tid; for (int iter = 0; iter <= pixels_[1] / n_threads; iter++) { @@ -1263,6 +1252,10 @@ void ProjectionPlot::create_output() const for (int horiz = 0; horiz < pixels_[0]; ++horiz) { + // Projection mode below decides ray starting conditions + Position init_r; + Direction init_u; + // Generate the starting position/direction of the ray if (orthographic_width_ == 0.0) { // perspective projection double this_phi = @@ -1273,18 +1266,22 @@ void ProjectionPlot::create_output() const camera_local_vec.x = std::cos(this_phi) * std::sin(this_mu); camera_local_vec.y = std::sin(this_phi) * std::sin(this_mu); camera_local_vec.z = std::cos(this_mu); - s.u = camera_local_vec.rotate(camera_to_model); + init_u = camera_local_vec.rotate(camera_to_model); + init_r = camera_position_; } else { // orthographic projection - s.u = looking_direction; + init_u = looking_direction; double x_pix_coord = (static_cast(horiz) - p0 / 2.0) / p0; double y_pix_coord = (static_cast(vert) - p1 / 2.0) / p0; - s.r = camera_position_ + - cam_yaxis * x_pix_coord * orthographic_width_ + - cam_zaxis * y_pix_coord * orthographic_width_; + + init_r = camera_position_; + init_r += cam_yaxis * x_pix_coord * orthographic_width_; + init_r += cam_zaxis * y_pix_coord * orthographic_width_; } - p.from_source(&s); // put particle at camera + // Resets internal geometry state of particle + p.init_from_r_u(init_r, init_u); + bool hitsomething = false; bool intersection_found = true; int loop_counter = 0; diff --git a/src/surface.cpp b/src/surface.cpp index 394a4b366..12fef070e 100644 --- a/src/surface.cpp +++ b/src/surface.cpp @@ -129,7 +129,7 @@ bool Surface::sense(Position r, Direction u) const return f > 0.0; } -Direction Surface::reflect(Position r, Direction u, Particle* p) const +Direction Surface::reflect(Position r, Direction u, GeometryState* p) const { // Determine projection of direction onto normal and squared magnitude of // normal. @@ -140,7 +140,7 @@ Direction Surface::reflect(Position r, Direction u, Particle* p) const } Direction Surface::diffuse_reflect( - Position r, Direction u, uint64_t* seed) const + Position r, Direction u, uint64_t* seed, GeometryState* p) const { // Diffuse reflect direction according to the normal. // cosine distribution diff --git a/src/universe.cpp b/src/universe.cpp index 67e1d1354..b4ef6b4b2 100644 --- a/src/universe.cpp +++ b/src/universe.cpp @@ -3,6 +3,7 @@ #include #include "openmc/hdf5_interface.h" +#include "openmc/particle.h" namespace openmc { @@ -36,7 +37,7 @@ void Universe::to_hdf5(hid_t universes_group) const close_group(group); } -bool Universe::find_cell(Particle& p) const +bool Universe::find_cell(GeometryState& p) const { const auto& cells { !partitioner_ ? cells_ : partitioner_->get_cells(p.r_local(), p.u_local())}; From d7d2230e5f550473bb9121f12b04267c71c636ed Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 16 Jan 2024 11:12:44 -0600 Subject: [PATCH 034/671] Fix locating h5m files references in DAGMC universes (#2842) --- include/openmc/file_utils.h | 5 +++ src/dagmc.cpp | 5 ++- src/file_utils.cpp | 6 ++++ src/finalize.cpp | 11 +++++++ src/initialize.cpp | 18 ++++------ tests/unit_tests/dagmc/test_h5m_subdir.py | 40 +++++++++++++++++++++++ 6 files changed, 70 insertions(+), 15 deletions(-) create mode 100644 tests/unit_tests/dagmc/test_h5m_subdir.py diff --git a/include/openmc/file_utils.h b/include/openmc/file_utils.h index 11b20b4aa..6e9812d5e 100644 --- a/include/openmc/file_utils.h +++ b/include/openmc/file_utils.h @@ -16,6 +16,11 @@ bool dir_exists(const std::string& path); //! \return Whether file exists bool file_exists(const std::string& filename); +//! Determine directory containing given file +//! \param[in] filename Path to file +//! \return Name of directory containing file +std::string dir_name(const std::string& filename); + // Gets the file extension of whatever string is passed in. This is defined as // a sequence of strictly alphanumeric characters which follow the last period, // i.e. at least one alphabet character is present, and zero or more numbers. diff --git a/src/dagmc.cpp b/src/dagmc.cpp index 561fdca81..56722fde6 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -49,8 +49,8 @@ DAGUniverse::DAGUniverse(pugi::xml_node node) if (check_for_node(node, "filename")) { filename_ = get_node_value(node, "filename"); - if (!file_exists(filename_)) { - fatal_error(fmt::format("DAGMC file '{}' could not be found", filename_)); + if (!starts_with(filename_, "/")) { + filename_ = dir_name(settings::path_input) + filename_; } } else { fatal_error("Must specify a file for the DAGMC universe"); @@ -123,7 +123,6 @@ void DAGUniverse::init_dagmc() dagmc_instance_ = std::make_shared(); // load the DAGMC geometry - filename_ = settings::path_input + filename_; if (!file_exists(filename_)) { fatal_error("Geometry DAGMC file '" + filename_ + "' does not exist!"); } diff --git a/src/file_utils.cpp b/src/file_utils.cpp index 8a078fe18..197c9767c 100644 --- a/src/file_utils.cpp +++ b/src/file_utils.cpp @@ -26,6 +26,12 @@ bool file_exists(const std::string& filename) return s.good(); } +std::string dir_name(const std::string& filename) +{ + size_t pos = filename.find_last_of("\\/"); + return (std::string::npos == pos) ? "" : filename.substr(0, pos + 1); +} + std::string get_file_extension(const std::string& filename) { // try our best to work on windows... diff --git a/src/finalize.cpp b/src/finalize.cpp index ea0470f9b..a41c2f783 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -88,17 +88,27 @@ int openmc_finalize() settings::legendre_to_tabular = true; settings::legendre_to_tabular_points = -1; settings::material_cell_offsets = true; + settings::max_lost_particles = 10; + settings::max_order = 0; settings::max_particles_in_flight = 100000; settings::max_splits = 1000; settings::max_tracks = 1000; settings::max_write_lost_particles = -1; + settings::n_log_bins = 8000; settings::n_inactive = 0; settings::n_particles = -1; settings::output_summary = true; settings::output_tallies = true; settings::particle_restart_run = false; + settings::path_cross_sections.clear(); + settings::path_input.clear(); + settings::path_output.clear(); + settings::path_particle_restart.clear(); + settings::path_sourcepoint.clear(); + settings::path_statepoint.clear(); settings::photon_transport = false; settings::reduce_tallies = true; + settings::rel_max_lost_particles = 1.0e-6; settings::res_scat_on = false; settings::res_scat_method = ResScatMethod::rvs; settings::res_scat_energy_min = 0.01; @@ -123,6 +133,7 @@ int openmc_finalize() settings::verbosity = 7; settings::weight_cutoff = 0.25; settings::weight_survive = 1.0; + settings::weight_windows_file.clear(); settings::weight_windows_on = false; settings::write_all_tracks = false; settings::write_initial_source = false; diff --git a/src/initialize.cpp b/src/initialize.cpp index 9e3c88603..cc1eac9cf 100644 --- a/src/initialize.cpp +++ b/src/initialize.cpp @@ -304,8 +304,9 @@ int parse_command_line(int argc, char* argv[]) settings::path_input)); } - // Add slash at end of directory if it isn't the - if (!ends_with(settings::path_input, "/")) { + // Add slash at end of directory if it isn't there + if (!ends_with(settings::path_input, "/") && + dir_exists(settings::path_input)) { settings::path_input += "/"; } } @@ -315,18 +316,11 @@ int parse_command_line(int argc, char* argv[]) bool read_model_xml() { - std::string model_filename = - settings::path_input.empty() ? "." : settings::path_input; - - // some string cleanup - // a trailing "/" is applied to path_input if it's specified, - // remove it for the first attempt at reading the input file - if (ends_with(model_filename, "/")) - model_filename.pop_back(); + std::string model_filename = settings::path_input; // if the current filename is a directory, append the default model filename - if (dir_exists(model_filename)) - model_filename += "/model.xml"; + if (model_filename.empty() || dir_exists(model_filename)) + model_filename += "model.xml"; // if this file doesn't exist, stop here if (!file_exists(model_filename)) diff --git a/tests/unit_tests/dagmc/test_h5m_subdir.py b/tests/unit_tests/dagmc/test_h5m_subdir.py new file mode 100644 index 000000000..dd9c6b043 --- /dev/null +++ b/tests/unit_tests/dagmc/test_h5m_subdir.py @@ -0,0 +1,40 @@ +import shutil +from pathlib import Path + +import openmc +import openmc.lib +import pytest + +pytestmark = pytest.mark.skipif( + not openmc.lib._dagmc_enabled(), reason="DAGMC CAD geometry is not enabled." +) + + +@pytest.mark.parametrize("absolute", [True, False]) +def test_model_h5m_in_subdirectory(run_in_tmpdir, request, absolute): + # Create new subdirectory and copy h5m file there + h5m = Path(request.fspath).parent / "dagmc.h5m" + subdir = Path("h5m") + subdir.mkdir() + shutil.copy(h5m, subdir) + + # Create simple model with h5m file in subdirectory + if absolute: + dag_univ = openmc.DAGMCUniverse((subdir / "dagmc.h5m").absolute()) + else: + dag_univ = openmc.DAGMCUniverse(subdir / "dagmc.h5m") + model = openmc.Model() + model.geometry = openmc.Geometry(dag_univ.bounded_universe()) + mat1 = openmc.Material(name="41") + mat1.add_nuclide("H1", 1.0) + mat2 = openmc.Material(name="no-void fuel") + mat2.add_nuclide("U235", 1.0) + model.materials = [mat1, mat2] + model.settings.batches = 10 + model.settings.inactive = 5 + model.settings.particles = 1000 + + # Make sure model can load + model.export_to_model_xml() + openmc.lib.init(["model.xml"]) + openmc.lib.finalize() From 63916067ee74414f99c46dbf1480c894f097b916 Mon Sep 17 00:00:00 2001 From: yrrepy <100789850+yrrepy@users.noreply.github.com> Date: Tue, 16 Jan 2024 22:54:03 -0800 Subject: [PATCH 035/671] Use huge_tree=True in lxml parsing (#2791) Co-authored-by: Paul Romano --- openmc/geometry.py | 3 ++- openmc/material.py | 3 ++- openmc/model/model.py | 3 ++- openmc/plots.py | 3 ++- openmc/settings.py | 3 ++- openmc/tallies.py | 3 ++- 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/openmc/geometry.py b/openmc/geometry.py index 60f4a390d..8815f2c83 100644 --- a/openmc/geometry.py +++ b/openmc/geometry.py @@ -293,7 +293,8 @@ class Geometry: if isinstance(materials, (str, os.PathLike)): materials = openmc.Materials.from_xml(materials) - tree = ET.parse(path) + parser = ET.XMLParser(huge_tree=True) + tree = ET.parse(path, parser=parser) root = tree.getroot() return cls.from_xml_element(root, materials) diff --git a/openmc/material.py b/openmc/material.py index 1f5496f15..d42c489ae 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -1754,7 +1754,8 @@ class Materials(cv.CheckedList): Materials collection """ - tree = ET.parse(path) + parser = ET.XMLParser(huge_tree=True) + tree = ET.parse(path, parser=parser) root = tree.getroot() return cls.from_xml_element(root) diff --git a/openmc/model/model.py b/openmc/model/model.py index a60960b2b..5194fec1d 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -253,7 +253,8 @@ class Model: path : str or PathLike Path to model.xml file """ - tree = ET.parse(path) + parser = ET.XMLParser(huge_tree=True) + tree = ET.parse(path, parser=parser) root = tree.getroot() model = cls() diff --git a/openmc/plots.py b/openmc/plots.py index b38b51a59..c73e3cdf7 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -1508,6 +1508,7 @@ class Plots(cv.CheckedList): Plots collection """ - tree = ET.parse(path) + parser = ET.XMLParser(huge_tree=True) + tree = ET.parse(path, parser=parser) root = tree.getroot() return cls.from_xml_element(root) diff --git a/openmc/settings.py b/openmc/settings.py index 6486d1f2a..a6d273901 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -1952,7 +1952,8 @@ class Settings: Settings object """ - tree = ET.parse(path) + parser = ET.XMLParser(huge_tree=True) + tree = ET.parse(path, parser=parser) root = tree.getroot() meshes = _read_meshes(root) return cls.from_xml_element(root, meshes) diff --git a/openmc/tallies.py b/openmc/tallies.py index b6cd4d139..ddc5d68ee 100644 --- a/openmc/tallies.py +++ b/openmc/tallies.py @@ -3304,6 +3304,7 @@ class Tallies(cv.CheckedList): Tallies object """ - tree = ET.parse(path) + parser = ET.XMLParser(huge_tree=True) + tree = ET.parse(path, parser=parser) root = tree.getroot() return cls.from_xml_element(root) From 4fa3fbcb193a785fa0314f83c54cdcbad860d5e0 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Wed, 17 Jan 2024 12:32:05 -0600 Subject: [PATCH 036/671] Make creation of spatial trees based on usage for unstructured mesh. (#2815) --- include/openmc/mesh.h | 5 +++++ src/mesh.cpp | 7 +++++++ src/tallies/filter_mesh.cpp | 2 ++ 3 files changed, 14 insertions(+) diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index de556321f..c88ffc350 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -75,6 +75,8 @@ public: virtual ~Mesh() = default; // Methods + //! Perform any preparation needed to support use in mesh filters + virtual void prepare_for_tallies() {}; //! Update a position to the local coordinates of the mesh virtual void local_coords(Position& r) const {}; @@ -664,6 +666,9 @@ public: // Overridden Methods + //! Perform any preparation needed to support use in mesh filters + void prepare_for_tallies() override; + Position sample_element(int32_t bin, uint64_t* seed) const override; void bins_crossed(Position r0, Position r1, const Direction& u, diff --git a/src/mesh.cpp b/src/mesh.cpp index 34510b614..cb6851d04 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -2057,6 +2057,13 @@ void MOABMesh::initialize() } } } +} + +void MOABMesh::prepare_for_tallies() +{ + // if the KDTree has already been constructed, do nothing + if (kdtree_) + return; // build acceleration data structures compute_barycentric_data(ehs_); diff --git a/src/tallies/filter_mesh.cpp b/src/tallies/filter_mesh.cpp index deb143346..ed9c71667 100644 --- a/src/tallies/filter_mesh.cpp +++ b/src/tallies/filter_mesh.cpp @@ -77,8 +77,10 @@ std::string MeshFilter::text_label(int bin) const void MeshFilter::set_mesh(int32_t mesh) { + // perform any additional perparation for mesh tallies here mesh_ = mesh; n_bins_ = model::meshes[mesh_]->n_bins(); + model::meshes[mesh_]->prepare_for_tallies(); } void MeshFilter::set_translation(const Position& translation) From 057c33a48e523e54fce25f10ba608f1747520f55 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Wed, 17 Jan 2024 14:46:37 -0600 Subject: [PATCH 037/671] Expose `Material::depletable` in the CAPI (#2843) Co-authored-by: Paul Romano --- include/openmc/capi.h | 2 ++ include/openmc/material.h | 17 +++++++++++------ openmc/lib/material.py | 20 +++++++++++++++++++- src/event.cpp | 2 +- src/material.cpp | 30 +++++++++++++++++++++++++++--- src/mgxs_interface.cpp | 2 +- src/physics_mg.cpp | 2 +- src/source.cpp | 2 +- tests/unit_tests/test_lib.py | 4 ++++ 9 files changed, 67 insertions(+), 14 deletions(-) diff --git a/include/openmc/capi.h b/include/openmc/capi.h index a444814f5..1d4dc1a4e 100644 --- a/include/openmc/capi.h +++ b/include/openmc/capi.h @@ -93,6 +93,8 @@ int openmc_material_set_id(int32_t index, int32_t id); int openmc_material_get_name(int32_t index, const char** name); int openmc_material_set_name(int32_t index, const char* name); int openmc_material_set_volume(int32_t index, double volume); +int openmc_material_get_depletable(int32_t index, bool* depletable); +int openmc_material_set_depletable(int32_t index, bool depletable); int openmc_material_filter_get_bins( int32_t index, const int32_t** bins, size_t* n); int openmc_material_filter_set_bins( diff --git a/include/openmc/material.h b/include/openmc/material.h index deaee2469..9235be356 100644 --- a/include/openmc/material.h +++ b/include/openmc/material.h @@ -94,7 +94,7 @@ public: // which will get auto-assigned to the next available ID. After creating // the new material, it is added to openmc::model::materials. //! \return reference to the cloned material - Material & clone(); + Material& clone(); //---------------------------------------------------------------------------- // Accessors @@ -149,6 +149,7 @@ public: //! Get whether material is fissionable //! \return Whether material is fissionable bool fissionable() const { return fissionable_; } + bool& fissionable() { return fissionable_; } //! Get volume of material //! \return Volume in [cm^3] @@ -158,6 +159,10 @@ public: //! \return Temperature in [K] double temperature() const; + //! Whether or not the material is depletable + bool depletable() const { return depletable_; } + bool& depletable() { return depletable_; } + //! Get pointer to NCrystal material object //! \return Pointer to NCrystal material object const NCrystalMat& ncrystal_mat() const { return ncrystal_mat_; }; @@ -173,11 +178,8 @@ public: double density_; //!< Total atom density in [atom/b-cm] double density_gpcc_; //!< Total atom density in [g/cm^3] double volume_ {-1.0}; //!< Volume in [cm^3] - bool fissionable_ { - false}; //!< Does this material contain fissionable nuclides - bool depletable_ {false}; //!< Is the material depletable? - vector p0_; //!< Indicate which nuclides are to be treated with - //!< iso-in-lab scattering + vector p0_; //!< Indicate which nuclides are to be treated with + //!< iso-in-lab scattering // To improve performance of tallying, we store an array (direct address // table) that indicates for each nuclide in data::nuclides the index of the @@ -210,6 +212,9 @@ private: // Private data members gsl::index index_; + bool depletable_ {false}; //!< Is the material depletable? + bool fissionable_ { + false}; //!< Does this material contain fissionable nuclides //! \brief Default temperature for cells containing this material. //! //! A negative value indicates no default temperature was specified. diff --git a/openmc/lib/material.py b/openmc/lib/material.py index fde197d3d..0ed8932da 100644 --- a/openmc/lib/material.py +++ b/openmc/lib/material.py @@ -1,5 +1,5 @@ from collections.abc import Mapping -from ctypes import c_int, c_int32, c_double, c_char_p, POINTER, c_size_t +from ctypes import c_bool, c_int, c_int32, c_double, c_char_p, POINTER, c_size_t from weakref import WeakValueDictionary import numpy as np @@ -60,6 +60,12 @@ _dll.openmc_material_set_name.errcheck = _error_handler _dll.openmc_material_set_volume.argtypes = [c_int32, c_double] _dll.openmc_material_set_volume.restype = c_int _dll.openmc_material_set_volume.errcheck = _error_handler +_dll.openmc_material_get_depletable.argtypes = [c_int32, POINTER(c_bool)] +_dll.openmc_material_get_depletable.restype = c_int +_dll.openmc_material_get_depletable.errcheck = _error_handler +_dll.openmc_material_set_depletable.argtypes = [c_int32, c_bool] +_dll.openmc_material_set_depletable.restype = c_int +_dll.openmc_material_set_depletable.errcheck = _error_handler _dll.n_materials.argtypes = [] _dll.n_materials.restype = c_size_t @@ -89,6 +95,8 @@ class Material(_FortranObjectWithID): List of nuclides in the material densities : numpy.ndarray Array of densities in atom/b-cm + depletable : bool + Whether this material is marked as depletable name : str Name of the material temperature : float @@ -169,6 +177,16 @@ class Material(_FortranObjectWithID): def volume(self, volume): _dll.openmc_material_set_volume(self._index, volume) + @property + def depletable(self): + depletable = c_bool() + _dll.openmc_material_get_depletable(self._index, depletable) + return depletable.value + + @depletable.setter + def depletable(self, depletable): + _dll.openmc_material_set_depletable(self._index, depletable) + @property def nuclides(self): return self._get_densities()[0] diff --git a/src/event.cpp b/src/event.cpp index e9490a77f..ae914c8be 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -51,7 +51,7 @@ void dispatch_xs_event(int64_t buffer_idx) { Particle& p = simulation::particles[buffer_idx]; if (p.material() == MATERIAL_VOID || - !model::materials[p.material()]->fissionable_) { + !model::materials[p.material()]->fissionable()) { simulation::calculate_nonfuel_xs_queue.thread_safe_append({p, buffer_idx}); } else { simulation::calculate_fuel_xs_queue.thread_safe_append({p, buffer_idx}); diff --git a/src/material.cpp b/src/material.cpp index 7466f1981..aad7008e7 100644 --- a/src/material.cpp +++ b/src/material.cpp @@ -372,8 +372,8 @@ Material& Material::clone() mat->density_ = density_; mat->density_gpcc_ = density_gpcc_; mat->volume_ = volume_; - mat->fissionable_ = fissionable_; - mat->depletable_ = depletable_; + mat->fissionable() = fissionable_; + mat->depletable() = depletable_; mat->p0_ = p0_; mat->mat_nuclide_index_ = mat_nuclide_index_; mat->thermal_tables_ = thermal_tables_; @@ -1068,7 +1068,7 @@ void Material::to_hdf5(hid_t group) const { hid_t material_group = create_group(group, "material " + std::to_string(id_)); - write_attribute(material_group, "depletable", static_cast(depletable_)); + write_attribute(material_group, "depletable", static_cast(depletable())); if (volume_ > 0.0) { write_attribute(material_group, "volume", volume_); } @@ -1550,6 +1550,30 @@ extern "C" int openmc_material_set_volume(int32_t index, double volume) } } +extern "C" int openmc_material_get_depletable(int32_t index, bool* depletable) +{ + if (index < 0 || index >= model::materials.size()) { + set_errmsg("Index in materials array is out of bounds."); + return OPENMC_E_OUT_OF_BOUNDS; + } + + *depletable = model::materials[index]->depletable(); + + return 0; +} + +extern "C" int openmc_material_set_depletable(int32_t index, bool depletable) +{ + if (index < 0 || index >= model::materials.size()) { + set_errmsg("Index in materials array is out of bounds."); + return OPENMC_E_OUT_OF_BOUNDS; + } + + model::materials[index]->depletable() = depletable; + + return 0; +} + extern "C" int openmc_extend_materials( int32_t n, int32_t* index_start, int32_t* index_end) { diff --git a/src/mgxs_interface.cpp b/src/mgxs_interface.cpp index 77085d57c..d54211eca 100644 --- a/src/mgxs_interface.cpp +++ b/src/mgxs_interface.cpp @@ -284,7 +284,7 @@ void mark_fissionable_mgxs_materials() for (const auto& mat : model::materials) { for (int i_nuc : mat->nuclide_) { if (data::mg.nuclides_[i_nuc].fissionable) { - mat->fissionable_ = true; + mat->fissionable() = true; } } } diff --git a/src/physics_mg.cpp b/src/physics_mg.cpp index 43e3cfa74..361cf5aff 100644 --- a/src/physics_mg.cpp +++ b/src/physics_mg.cpp @@ -43,7 +43,7 @@ void sample_reaction(Particle& p) // change when sampling fission sites. The following block handles all // absorption (including fission) - if (model::materials[p.material()]->fissionable_) { + if (model::materials[p.material()]->fissionable()) { if (settings::run_mode == RunMode::EIGENVALUE || (settings::run_mode == RunMode::FIXED_SOURCE && settings::create_fission_neutrons)) { diff --git a/src/source.cpp b/src/source.cpp index bf52b568a..778962667 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -201,7 +201,7 @@ SourceSite IndependentSource::sample(uint64_t* seed) const if (mat_index == MATERIAL_VOID) { found = false; } else { - found = model::materials[mat_index]->fissionable_; + found = model::materials[mat_index]->fissionable(); } } } diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index 5f74c7c23..11d6b12c9 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -206,6 +206,10 @@ def test_material(lib_init): m.name = "Not hot borated water" assert m.name == "Not hot borated water" + assert m.depletable == False + m.depletable = True + assert m.depletable == True + def test_properties_density(lib_init): m = openmc.lib.materials[1] From 2c5b7407380d8d073e01836bbb3ca6324f9cdb87 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 17 Jan 2024 16:10:39 -0600 Subject: [PATCH 038/671] Update copyright to 2024 (#2846) --- 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 81c1f5f7b..1f9198c1e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2011-2023 Massachusetts Institute of Technology, UChicago Argonne +Copyright (c) 2011-2024 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 b2a4a6758..53d529f3d 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-2023, Massachusetts Institute of Technology, UChicago Argonne LLC, and OpenMC contributors' +copyright = '2011-2024, 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 fb29afbc0..23315e8fe 100644 --- a/docs/source/license.rst +++ b/docs/source/license.rst @@ -4,7 +4,7 @@ License Agreement ================= -Copyright © 2011-2023 Massachusetts Institute of Technology, UChicago Argonne +Copyright © 2011-2024 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 331e42ea0..d121a8956 100644 --- a/man/man1/openmc.1 +++ b/man/man1/openmc.1 @@ -63,7 +63,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-2023 Massachusetts Institute of Technology, UChicago +Copyright \(co 2011-2024 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 17e158d3f..9b4171d8d 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -74,7 +74,7 @@ void title() // Write version information fmt::print( " | The OpenMC Monte Carlo Code\n" - " Copyright | 2011-2023 MIT, UChicago Argonne LLC, and contributors\n" + " Copyright | 2011-2024 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" : ""); @@ -295,7 +295,7 @@ void print_version() #ifdef GIT_SHA1 fmt::print("Git SHA1: {}\n", GIT_SHA1); #endif - fmt::print("Copyright (c) 2011-2023 MIT, UChicago Argonne LLC, and " + fmt::print("Copyright (c) 2011-2024 MIT, UChicago Argonne LLC, and " "contributors\nMIT/X license at " "\n"); } From 187160d0822ef9f4ceb23e489c96fd62336a1fb2 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 19 Jan 2024 09:01:18 -0600 Subject: [PATCH 039/671] Ability to compute material volume fractions over mesh elements (#2802) --- include/openmc/capi.h | 3 + include/openmc/mesh.h | 30 +++++++- include/openmc/volume_calc.h | 36 +++++++++- openmc/lib/core.py | 5 +- openmc/lib/mesh.py | 84 +++++++++++++++++++++- src/mesh.cpp | 136 ++++++++++++++++++++++++++++++++++- src/volume_calc.cpp | 29 +------- tests/unit_tests/test_lib.py | 76 ++++++++++++++++++-- 8 files changed, 360 insertions(+), 39 deletions(-) diff --git a/include/openmc/capi.h b/include/openmc/capi.h index 1d4dc1a4e..063ed8d9f 100644 --- a/include/openmc/capi.h +++ b/include/openmc/capi.h @@ -105,6 +105,9 @@ int openmc_mesh_filter_get_translation(int32_t index, double translation[3]); int openmc_mesh_filter_set_translation(int32_t index, double translation[3]); int openmc_mesh_get_id(int32_t index, int32_t* id); int openmc_mesh_set_id(int32_t index, int32_t id); +int openmc_mesh_get_n_elements(int32_t index, size_t* n); +int openmc_mesh_material_volumes(int32_t index, int n_sample, int bin, + int result_size, void* result, int* hits, uint64_t* seed); int openmc_meshsurface_filter_get_mesh(int32_t index, int32_t* index_mesh); int openmc_meshsurface_filter_set_mesh(int32_t index, int32_t index_mesh); int openmc_new_filter(const char* type, int32_t* index); diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index c88ffc350..37b771832 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -9,6 +9,7 @@ #include "hdf5.h" #include "pugixml.hpp" #include "xtensor/xtensor.hpp" +#include #include "openmc/error.h" #include "openmc/memory.h" // for unique_ptr @@ -69,6 +70,12 @@ extern const libMesh::Parallel::Communicator* libmesh_comm; class Mesh { public: + // Types, aliases + struct MaterialVolume { + int32_t material; //!< material index + double volume; //!< volume in [cm^3] + }; + // Constructors and destructor Mesh() = default; Mesh(pugi::xml_node node); @@ -159,9 +166,28 @@ public: virtual std::string get_mesh_type() const = 0; + //! Determine volume of materials within a single mesh elemenet + // + //! \param[in] n_sample Number of samples within each element + //! \param[in] bin Index of mesh element + //! \param[out] Array of (material index, volume) for desired element + //! \param[inout] seed Pseudorandom number seed + //! \return Number of materials within element + int material_volumes(int n_sample, int bin, gsl::span volumes, + uint64_t* seed) const; + + //! Determine volume of materials within a single mesh elemenet + // + //! \param[in] n_sample Number of samples within each element + //! \param[in] bin Index of mesh element + //! \param[inout] seed Pseudorandom number seed + //! \return Vector of (material index, volume) for desired element + vector material_volumes( + int n_sample, int bin, uint64_t* seed) const; + // Data members - int id_ {-1}; //!< User-specified ID - int n_dimension_; //!< Number of dimensions + int id_ {-1}; //!< User-specified ID + int n_dimension_ {-1}; //!< Number of dimensions }; class StructuredMesh : public Mesh { diff --git a/include/openmc/volume_calc.h b/include/openmc/volume_calc.h index 2773a39fa..376b8c707 100644 --- a/include/openmc/volume_calc.h +++ b/include/openmc/volume_calc.h @@ -1,18 +1,23 @@ #ifndef OPENMC_VOLUME_CALC_H #define OPENMC_VOLUME_CALC_H +#include // for find #include #include +#include #include "openmc/array.h" +#include "openmc/openmp_interface.h" #include "openmc/position.h" #include "openmc/tallies/trigger.h" #include "openmc/vector.h" #include "pugixml.hpp" #include "xtensor/xtensor.hpp" - #include +#ifdef _OPENMP +#include +#endif namespace openmc { @@ -89,6 +94,35 @@ extern vector volume_calcs; // Non-member functions //============================================================================== +//! Reduce vector of indices and hits from each thread to a single copy +// +//! \param[in] local_indices Indices specific to each thread +//! \param[in] local_hits Hit count specific to each thread +//! \param[out] indices Reduced vector of indices +//! \param[out] hits Reduced vector of hits +template +void reduce_indices_hits(const vector& local_indices, + const vector& local_hits, vector& indices, vector& hits) +{ + const int n_threads = num_threads(); + +#pragma omp for ordered schedule(static, 1) + for (int i = 0; i < n_threads; ++i) { +#pragma omp ordered + for (int j = 0; j < local_indices.size(); ++j) { + // Check if this material has been added to the master list and if + // so, accumulate the number of hits + auto it = std::find(indices.begin(), indices.end(), local_indices[j]); + if (it == indices.end()) { + indices.push_back(local_indices[j]); + hits.push_back(local_hits[j]); + } else { + hits[it - indices.begin()] += local_hits[j]; + } + } + } +} + void free_memory_volume(); } // namespace openmc diff --git a/openmc/lib/core.py b/openmc/lib/core.py index 25dc65fe8..4ea1c86d0 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -611,7 +611,7 @@ class _DLLGlobal: class _FortranObject: def __repr__(self): - return "{}[{}]".format(type(self).__name__, self._index) + return "<{}(index={})>".format(type(self).__name__, self._index) class _FortranObjectWithID(_FortranObject): @@ -622,6 +622,9 @@ class _FortranObjectWithID(_FortranObject): # OutOfBoundsError will be raised here by virtue of referencing self.id self.id + def __repr__(self): + return "<{}(id={})>".format(type(self).__name__, self.id) + @contextmanager def quiet_dll(output=True): diff --git a/openmc/lib/mesh.py b/openmc/lib/mesh.py index 72d61cc66..8a9953754 100644 --- a/openmc/lib/mesh.py +++ b/openmc/lib/mesh.py @@ -1,6 +1,8 @@ from collections.abc import Mapping -from ctypes import (c_int, c_int32, c_char_p, c_double, POINTER, - create_string_buffer) +from ctypes import (c_int, c_int32, c_char_p, c_double, POINTER, Structure, + create_string_buffer, c_uint64, c_size_t) +from random import getrandbits +from typing import Optional, List, Tuple from weakref import WeakValueDictionary import numpy as np @@ -10,9 +12,18 @@ from ..exceptions import AllocationError, InvalidIDError from . import _dll from .core import _FortranObjectWithID from .error import _error_handler +from .material import Material __all__ = ['RegularMesh', 'RectilinearMesh', 'CylindricalMesh', 'SphericalMesh', 'UnstructuredMesh', 'meshes'] + +class _MaterialVolume(Structure): + _fields_ = [ + ("material", c_int32), + ("volume", c_double) + ] + + # Mesh functions _dll.openmc_extend_meshes.argtypes = [c_int32, c_char_p, POINTER(c_int32), POINTER(c_int32)] @@ -24,6 +35,14 @@ _dll.openmc_mesh_get_id.errcheck = _error_handler _dll.openmc_mesh_set_id.argtypes = [c_int32, c_int32] _dll.openmc_mesh_set_id.restype = c_int _dll.openmc_mesh_set_id.errcheck = _error_handler +_dll.openmc_mesh_get_n_elements.argtypes = [c_int32, POINTER(c_size_t)] +_dll.openmc_mesh_get_n_elements.restype = c_int +_dll.openmc_mesh_get_n_elements.errcheck = _error_handler +_dll.openmc_mesh_material_volumes.argtypes = [ + c_int32, c_int, c_int, c_int, POINTER(_MaterialVolume), + POINTER(c_int), POINTER(c_uint64)] +_dll.openmc_mesh_material_volumes.restype = c_int +_dll.openmc_mesh_material_volumes.errcheck = _error_handler _dll.openmc_get_mesh_index.argtypes = [c_int32, POINTER(c_int32)] _dll.openmc_get_mesh_index.restype = c_int _dll.openmc_get_mesh_index.errcheck = _error_handler @@ -123,6 +142,67 @@ class Mesh(_FortranObjectWithID): def id(self, mesh_id): _dll.openmc_mesh_set_id(self._index, mesh_id) + @property + def n_elements(self): + n = c_size_t() + _dll.openmc_mesh_get_n_elements(self._index, n) + return n.value + + def material_volumes( + self, + n_samples: int = 10_000, + prn_seed: Optional[int] = None + ) -> List[List[Tuple[Material, float]]]: + """Determine volume of materials in each mesh element + + .. versionadded:: 0.14.1 + + Parameters + ---------- + n_samples : int + Number of samples in each mesh element + prn_seed : int + Pseudorandom number generator (PRNG) seed; if None, one will be + generated randomly. + + Returns + ------- + List of tuple of (material, volume) for each mesh element. Void volume + is represented by having a value of None in the first element of a + tuple. + + """ + if n_samples <= 0: + raise ValueError("Number of samples must be positive") + if prn_seed is None: + prn_seed = getrandbits(63) + prn_seed = c_uint64(prn_seed) + + # Preallocate space for MaterialVolume results + size = 16 + result = (_MaterialVolume * size)() + + hits = c_int() # Number of materials hit in a given element + volumes = [] + for i_element in range(self.n_elements): + while True: + try: + _dll.openmc_mesh_material_volumes( + self._index, n_samples, i_element, size, result, hits, prn_seed) + except AllocationError: + # Increase size of result array and try again + size *= 2 + result = (_MaterialVolume * size)() + else: + # If no error, break out of loop + break + + volumes.append([ + (Material(index=r.material), r.volume) + for r in result[:hits.value] + ]) + return volumes + class RegularMesh(Mesh): """RegularMesh stored internally. diff --git a/src/mesh.cpp b/src/mesh.cpp index cb6851d04..b70692347 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -22,15 +22,19 @@ #include "openmc/container_util.h" #include "openmc/error.h" #include "openmc/file_utils.h" +#include "openmc/geometry.h" #include "openmc/hdf5_interface.h" +#include "openmc/material.h" #include "openmc/memory.h" #include "openmc/message_passing.h" #include "openmc/openmp_interface.h" +#include "openmc/particle_data.h" #include "openmc/random_dist.h" #include "openmc/search.h" #include "openmc/settings.h" #include "openmc/tallies/filter.h" #include "openmc/tallies/tally.h" +#include "openmc/volume_calc.h" #include "openmc/xml_interface.h" #ifdef LIBMESH @@ -141,6 +145,92 @@ vector Mesh::volumes() const return volumes; } +int Mesh::material_volumes( + int n_sample, int bin, gsl::span result, uint64_t* seed) const +{ + vector materials; + vector hits; + +#pragma omp parallel + { + vector local_materials; + vector local_hits; + GeometryState geom; + +#pragma omp for + for (int i = 0; i < n_sample; ++i) { + // Get seed for i-th sample + uint64_t seed_i = future_seed(3 * i, *seed); + + // Sample position and set geometry state + geom.r() = this->sample_element(bin, &seed_i); + geom.u() = {1., 0., 0.}; + geom.n_coord() = 1; + + // If this location is not in the geometry at all, move on to next block + if (!exhaustive_find_cell(geom)) + continue; + + int i_material = geom.material(); + + // Check if this material was previously hit and if so, increment count + auto it = + std::find(local_materials.begin(), local_materials.end(), i_material); + if (it == local_materials.end()) { + local_materials.push_back(i_material); + local_hits.push_back(1); + } else { + local_hits[it - local_materials.begin()]++; + } + } // omp for + + // Reduce index/hits lists from each thread into a single copy + reduce_indices_hits(local_materials, local_hits, materials, hits); + } // omp parallel + + // Advance RNG seed + advance_prn_seed(3 * n_sample, seed); + + // Make sure span passed in is large enough + if (hits.size() > result.size()) { + return -1; + } + + // Convert hits to fractions + for (int i_mat = 0; i_mat < hits.size(); ++i_mat) { + double fraction = double(hits[i_mat]) / n_sample; + result[i_mat].material = materials[i_mat]; + result[i_mat].volume = fraction * this->volume(bin); + } + return hits.size(); +} + +vector Mesh::material_volumes( + int n_sample, int bin, uint64_t* seed) const +{ + // Create result vector with space for 8 pairs + vector result; + result.reserve(8); + + int size = -1; + while (true) { + // Get material volumes + size = this->material_volumes( + n_sample, bin, {result.data(), result.data() + result.capacity()}, seed); + + // If capacity was sufficient, resize the vector and return + if (size >= 0) { + result.resize(size); + break; + } + + // Otherwise, increase capacity of the vector + result.reserve(2 * result.capacity()); + } + + return result; +} + //============================================================================== // Structured Mesh implementation //============================================================================== @@ -736,7 +826,7 @@ RegularMesh::RegularMesh(pugi::xml_node node) : StructuredMesh {node} fatal_error("Must specify either or on a mesh."); } - // Set volume fraction + // Set material volumes volume_frac_ = 1.0 / xt::prod(shape)(); element_volume_ = 1.0; @@ -1035,7 +1125,7 @@ double RectilinearMesh::volume(const MeshIndex& ijk) const double vol {1.0}; for (int i = 0; i < n_dimension_; i++) { - vol *= grid_[i][ijk[i] + 1] - grid_[i][ijk[i]]; + vol *= grid_[i][ijk[i]] - grid_[i][ijk[i] - 1]; } return vol; } @@ -1781,6 +1871,33 @@ extern "C" int openmc_mesh_set_id(int32_t index, int32_t id) return 0; } +//! Get the number of elements in a mesh +extern "C" int openmc_mesh_get_n_elements(int32_t index, size_t* n) +{ + if (int err = check_mesh(index)) + return err; + *n = model::meshes[index]->n_bins(); + return 0; +} + +extern "C" int openmc_mesh_material_volumes(int32_t index, int n_sample, + int bin, int result_size, void* result, int* hits, uint64_t* seed) +{ + auto result_ = reinterpret_cast(result); + if (!result_) { + set_errmsg("Invalid result pointer passed to openmc_mesh_material_volumes"); + return OPENMC_E_INVALID_ARGUMENT; + } + + if (int err = check_mesh(index)) + return err; + + int n = model::meshes[index]->material_volumes( + n_sample, bin, {result_, result_ + result_size}, seed); + *hits = n; + return (n == -1) ? OPENMC_E_ALLOCATE : 0; +} + //! Get the dimension of a regular mesh extern "C" int openmc_regular_mesh_get_dimension( int32_t index, int** dims, int* n) @@ -1835,6 +1952,11 @@ extern "C" int openmc_regular_mesh_set_params( return err; RegularMesh* m = dynamic_cast(model::meshes[index].get()); + if (m->n_dimension_ == -1) { + set_errmsg("Need to set mesh dimension before setting parameters."); + return OPENMC_E_UNASSIGNED; + } + vector shape = {static_cast(n)}; if (ll && ur) { m->lower_left_ = xt::adapt(ll, n, xt::no_ownership(), shape); @@ -1853,6 +1975,16 @@ extern "C" int openmc_regular_mesh_set_params( return OPENMC_E_INVALID_ARGUMENT; } + // Set material volumes + + // 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->element_volume_ = 1.0; + for (int i = 0; i < m->n_dimension_; i++) { + m->element_volume_ *= m->width_[i]; + } + return 0; } diff --git a/src/volume_calc.cpp b/src/volume_calc.cpp index 6a47e9490..8b5c27f14 100644 --- a/src/volume_calc.cpp +++ b/src/volume_calc.cpp @@ -203,32 +203,9 @@ vector VolumeCalculation::execute() const // At this point, each thread has its own pair of index/hits lists and we // now need to reduce them. OpenMP is not nearly smart enough to do this // on its own, so we have to manually reduce them - const int n_threads = num_threads(); - -#pragma omp for ordered schedule(static) - for (int i = 0; i < n_threads; ++i) { -#pragma omp ordered - for (int i_domain = 0; i_domain < n; ++i_domain) { - for (int j = 0; j < indices[i_domain].size(); ++j) { - // Check if this material has been added to the master list and if - // so, accumulate the number of hits - bool already_added = false; - for (int k = 0; k < master_indices[i_domain].size(); k++) { - if (indices[i_domain][j] == master_indices[i_domain][k]) { - master_hits[i_domain][k] += hits[i_domain][j]; - already_added = true; - break; - } - } - if (!already_added) { - // If we made it here, the material hasn't yet been added to the - // master list, so add entries to the master indices and master - // hits lists - master_indices[i_domain].push_back(indices[i_domain][j]); - master_hits[i_domain].push_back(hits[i_domain][j]); - } - } - } + for (int i_domain = 0; i_domain < n; ++i_domain) { + reduce_indices_hits(indices[i_domain], hits[i_domain], + master_indices[i_domain], master_hits[i_domain]); } } // omp parallel diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index 11d6b12c9..8aa2ea6e9 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -1,4 +1,5 @@ from collections.abc import Mapping +from math import pi import os import numpy as np @@ -126,7 +127,7 @@ def test_cell(lib_init): cell = openmc.lib.cells[1] assert isinstance(cell.fill, openmc.lib.Material) cell.fill = openmc.lib.materials[1] - assert str(cell) == 'Cell[0]' + assert str(cell) == '' assert cell.name == "Fuel" cell.name = "Not fuel" assert cell.name == "Not fuel" @@ -584,6 +585,25 @@ def test_regular_mesh(lib_init): msf.translation = translation assert msf.translation == translation + # Test material volumes + mesh = openmc.lib.RegularMesh() + mesh.dimension = (2, 2, 1) + mesh.set_parameters(lower_left=(-0.63, -0.63, -0.5), + upper_right=(0.63, 0.63, 0.5)) + vols = mesh.material_volumes() + assert len(vols) == 4 + for elem_vols in vols: + assert sum(f[1] for f in elem_vols) == pytest.approx(1.26 * 1.26 / 4) + + # If the mesh extends beyond the boundaries of the model, the volumes should + # still be reported correctly + mesh.dimension = (1, 1, 1) + mesh.set_parameters(lower_left=(-1.0, -1.0, -0.5), + upper_right=(1.0, 1.0, 0.5)) + vols = mesh.material_volumes(100_000) + for elem_vols in vols: + assert sum(f[1] for f in elem_vols) == pytest.approx(1.26 * 1.26, 1e-2) + def test_rectilinear_mesh(lib_init): mesh = openmc.lib.RectilinearMesh() @@ -604,7 +624,7 @@ def test_rectilinear_mesh(lib_init): meshes = openmc.lib.meshes assert isinstance(meshes, Mapping) - assert len(meshes) == 2 + assert len(meshes) == 3 mesh = meshes[mesh.id] assert isinstance(mesh, openmc.lib.RectilinearMesh) @@ -615,8 +635,21 @@ def test_rectilinear_mesh(lib_init): msf = openmc.lib.MeshSurfaceFilter(mesh) assert msf.mesh == mesh + # Test material volumes + mesh = openmc.lib.RectilinearMesh() + w = 1.26 + mesh.set_grid([-w/2, -w/4, w/2], [-w/2, -w/4, w/2], [-0.5, 0.5]) + + vols = mesh.material_volumes() + assert len(vols) == 4 + assert sum(f[1] for f in vols[0]) == pytest.approx(w/4 * w/4) + assert sum(f[1] for f in vols[1]) == pytest.approx(w/4 * 3*w/4) + assert sum(f[1] for f in vols[2]) == pytest.approx(3*w/4 * w/4) + assert sum(f[1] for f in vols[3]) == pytest.approx(3*w/4 * 3*w/4) + + def test_cylindrical_mesh(lib_init): - deg2rad = lambda deg: deg*np.pi/180 + deg2rad = lambda deg: deg*pi/180 mesh = openmc.lib.CylindricalMesh() r_grid = [0., 5., 10.] phi_grid = np.radians([0., 10., 20.]) @@ -635,7 +668,7 @@ def test_cylindrical_mesh(lib_init): meshes = openmc.lib.meshes assert isinstance(meshes, Mapping) - assert len(meshes) == 3 + assert len(meshes) == 5 mesh = meshes[mesh.id] assert isinstance(mesh, openmc.lib.CylindricalMesh) @@ -646,6 +679,21 @@ def test_cylindrical_mesh(lib_init): msf = openmc.lib.MeshSurfaceFilter(mesh) assert msf.mesh == mesh + # Test material volumes + mesh = openmc.lib.CylindricalMesh() + r_grid = (0., 0.25, 0.5) + phi_grid = np.linspace(0., 2.0*pi, 4) + z_grid = (-0.5, 0.5) + mesh.set_grid(r_grid, phi_grid, z_grid) + + vols = mesh.material_volumes() + assert len(vols) == 6 + for i in range(0, 6, 2): + assert sum(f[1] for f in vols[i]) == pytest.approx(pi * 0.25**2 / 3) + for i in range(1, 6, 2): + assert sum(f[1] for f in vols[i]) == pytest.approx(pi * (0.5**2 - 0.25**2) / 3) + + def test_spherical_mesh(lib_init): deg2rad = lambda deg: deg*np.pi/180 mesh = openmc.lib.SphericalMesh() @@ -666,7 +714,7 @@ def test_spherical_mesh(lib_init): meshes = openmc.lib.meshes assert isinstance(meshes, Mapping) - assert len(meshes) == 4 + assert len(meshes) == 7 mesh = meshes[mesh.id] assert isinstance(mesh, openmc.lib.SphericalMesh) @@ -677,6 +725,24 @@ def test_spherical_mesh(lib_init): msf = openmc.lib.MeshSurfaceFilter(mesh) assert msf.mesh == mesh + # Test material volumes + mesh = openmc.lib.SphericalMesh() + r_grid = (0., 0.25, 0.5) + theta_grid = np.linspace(0., pi, 3) + phi_grid = np.linspace(0., 2.0*pi, 4) + mesh.set_grid(r_grid, theta_grid, phi_grid) + + vols = mesh.material_volumes() + assert len(vols) == 12 + d_theta = theta_grid[1] - theta_grid[0] + d_phi = phi_grid[1] - phi_grid[0] + for i in range(0, 12, 2): + assert sum(f[1] for f in vols[i]) == pytest.approx( + 0.25**3 / 3 * d_theta * d_phi * 2/pi) + for i in range(1, 12, 2): + assert sum(f[1] for f in vols[i]) == pytest.approx( + (0.5**3 - 0.25**3) / 3 * d_theta * d_phi * 2/pi) + def test_restart(lib_init, mpi_intracomm): # Finalize and re-init to make internal state consistent with XML. From 0785500bc418d5bf7407fe65835492dbe95baa20 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Fri, 19 Jan 2024 15:32:43 -0600 Subject: [PATCH 040/671] Updating file extension for Excel files when exporting MGXS data (#2852) --- openmc/mgxs/mdgxs.py | 4 ++-- openmc/mgxs/mgxs.py | 4 ++-- setup.py | 2 +- tests/regression_tests/mgxs_library_hdf5/test.py | 10 +++++++--- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/openmc/mgxs/mdgxs.py b/openmc/mgxs/mdgxs.py index c9d76bc10..58f6a2d2f 100644 --- a/openmc/mgxs/mdgxs.py +++ b/openmc/mgxs/mdgxs.py @@ -743,9 +743,9 @@ class MDGXS(MGXS): df.to_csv(filename + '.csv', index=False) elif format == 'excel': if self.domain_type == 'mesh': - df.to_excel(filename + '.xls') + df.to_excel(filename + '.xlsx') else: - df.to_excel(filename + '.xls', index=False) + df.to_excel(filename + '.xlsx', index=False) elif format == 'pickle': df.to_pickle(filename + '.pkl') elif format == 'latex': diff --git a/openmc/mgxs/mgxs.py b/openmc/mgxs/mgxs.py index b6840d079..2eb7f2756 100644 --- a/openmc/mgxs/mgxs.py +++ b/openmc/mgxs/mgxs.py @@ -2001,9 +2001,9 @@ class MGXS: df.to_csv(filename + '.csv', index=False) elif format == 'excel': if self.domain_type == 'mesh': - df.to_excel(filename + '.xls') + df.to_excel(filename + '.xlsx') else: - df.to_excel(filename + '.xls', index=False) + df.to_excel(filename + '.xlsx', index=False) elif format == 'pickle': df.to_pickle(filename + '.pkl') elif format == 'latex': diff --git a/setup.py b/setup.py index 29c129d7b..eeb0b3f29 100755 --- a/setup.py +++ b/setup.py @@ -71,7 +71,7 @@ kwargs = { 'depletion-mpi': ['mpi4py'], 'docs': ['sphinx', 'sphinxcontrib-katex', 'sphinx-numfig', 'jupyter', 'sphinxcontrib-svg2pdfconverter', 'sphinx-rtd-theme'], - 'test': ['pytest', 'pytest-cov', 'colorama'], + 'test': ['pytest', 'pytest-cov', 'colorama', 'openpyxl'], 'vtk': ['vtk'], }, # Cython is used to add resonance reconstruction and fast float_endf diff --git a/tests/regression_tests/mgxs_library_hdf5/test.py b/tests/regression_tests/mgxs_library_hdf5/test.py index 2f3c9d149..06625c25f 100644 --- a/tests/regression_tests/mgxs_library_hdf5/test.py +++ b/tests/regression_tests/mgxs_library_hdf5/test.py @@ -54,6 +54,11 @@ class MGXSTestHarness(PyAPITestHarness): # Export the MGXS Library to an HDF5 file self.mgxs_lib.build_hdf5_store(directory='.') + # Test export of the MGXS Library to an Excel spreadsheet + for mgxs in self.mgxs_lib.all_mgxs.values(): + for xs in mgxs.values(): + xs.export_xs_data('mgxs', xs_type='macro', format='excel') + # Open the MGXS HDF5 file with h5py.File('mgxs.h5', 'r') as f: @@ -76,9 +81,8 @@ class MGXSTestHarness(PyAPITestHarness): def _cleanup(self): super()._cleanup() - f = 'mgxs.h5' - if os.path.exists(f): - os.remove(f) + files = ['mgxs.h5', 'mgxs.xlsx'] + (os.remove(f) for f in files if os.path.exists(f)) def test_mgxs_library_hdf5(): From b97149ddf817d2f4bcb41db5dbc8f77dd1dd4113 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Sat, 20 Jan 2024 19:06:45 +0000 Subject: [PATCH 041/671] removed error raising when calling warn (#2853) --- openmc/deplete/openmc_operator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py index 74dbf8fee..ad7a3924b 100644 --- a/openmc/deplete/openmc_operator.py +++ b/openmc/deplete/openmc_operator.py @@ -213,7 +213,7 @@ class OpenMCOperator(TransportOperator): msg = (f"Nuclilde {nuclide} in material {mat.id} is not " "present in the depletion chain and has no cross " "section data.") - raise warn(msg) + warn(msg) if mat.depletable: burnable_mats.add(str(mat.id)) if mat.volume is None: From fca4da4bda59c532134a791fe0cd938d08e2e15c Mon Sep 17 00:00:00 2001 From: Gavin Ridley Date: Tue, 23 Jan 2024 06:53:11 -0600 Subject: [PATCH 042/671] Export model.tallies to XML in CoupledOperator (#2840) --- openmc/deplete/coupled_operator.py | 46 +++++++++++++++---- .../test_deplete_coupled_operator.py | 8 ++-- tests/unit_tests/test_model.py | 18 ++++++++ 3 files changed, 59 insertions(+), 13 deletions(-) diff --git a/openmc/deplete/coupled_operator.py b/openmc/deplete/coupled_operator.py index d591e956f..856a26958 100644 --- a/openmc/deplete/coupled_operator.py +++ b/openmc/deplete/coupled_operator.py @@ -178,10 +178,6 @@ class CoupledOperator(OpenMCOperator): ---------- model : openmc.model.Model OpenMC model object - geometry : openmc.Geometry - OpenMC geometry object - settings : openmc.Settings - OpenMC settings object output_dir : pathlib.Path Path to output directory to save results. round_number : bool @@ -242,8 +238,6 @@ class CoupledOperator(OpenMCOperator): warn("Fission Q dictionary will not be used") fission_q = None self.model = model - self.settings = model.settings - self.geometry = model.geometry # determine set of materials in the model if not model.materials: @@ -358,7 +352,7 @@ class CoupledOperator(OpenMCOperator): if normalization_mode == "fission-q": self._normalization_helper = ChainFissionHelper() elif normalization_mode == "energy-deposition": - score = "heating" if self.settings.photon_transport else "heating-local" + score = "heating" if self.model.settings.photon_transport else "heating-local" self._normalization_helper = EnergyScoreHelper(score) else: self._normalization_helper = SourceRateHelper() @@ -380,8 +374,12 @@ class CoupledOperator(OpenMCOperator): # Create XML files if comm.rank == 0: - self.geometry.export_to_xml() - self.settings.export_to_xml() + self.model.geometry.export_to_xml() + self.model.settings.export_to_xml() + if self.model.plots: + self.model.plots.export_to_xml() + if self.model.tallies: + self.model.tallies.export_to_xml() self._generate_materials_xml() # Initialize OpenMC library @@ -528,6 +526,36 @@ class CoupledOperator(OpenMCOperator): if self.cleanup_when_done: openmc.lib.finalize() + # The next few class variables and methods should be removed after one + # release cycle or so. For now, we will provide compatibility to + # accessing CoupledOperator.settings and CoupledOperator.geometry. In + # the future these should stay on the Model class. + + var_warning_msg = "The CoupledOperator.{0} variable should be \ +accessed through CoupledOperator.model.{0}." + geometry_warning_msg = var_warning_msg.format("geometry") + settings_warning_msg = var_warning_msg.format("settings") + + @property + def settings(self): + warn(self.settings_warning_msg, FutureWarning) + return self.model.settings + + @settings.setter + def settings(self, new_settings): + warn(self.settings_warning_msg, FutureWarning) + self.model.settings = new_settings + + @property + def geometry(self): + warn(self.geometry_warning_msg, FutureWarning) + return self.model.geometry + + @geometry.setter + def geometry(self, new_geometry): + warn(self.geometry_warning_msg, FutureWarning) + self.model.geometry = new_geometry + # Retain deprecated name for the time being def Operator(*args, **kwargs): diff --git a/tests/unit_tests/test_deplete_coupled_operator.py b/tests/unit_tests/test_deplete_coupled_operator.py index 8765020de..fe79d621b 100644 --- a/tests/unit_tests/test_deplete_coupled_operator.py +++ b/tests/unit_tests/test_deplete_coupled_operator.py @@ -38,7 +38,8 @@ def model(): pin_surfaces = [openmc.ZCylinder(r=r) for r in radii] pin_univ = openmc.model.pin(pin_surfaces, materials) - bound_box = openmc.model.RectangularPrism(1.24, 1.24, boundary_type="reflective") + bound_box = openmc.model.RectangularPrism( + 1.24, 1.24, boundary_type="reflective") root_cell = openmc.Cell(fill=pin_univ, region=-bound_box) geometry = openmc.Geometry([root_cell]) @@ -95,7 +96,7 @@ def test_diff_volume_method_match_cell(model_with_volumes): chain_file=CHAIN_PATH ) - all_cells = list(operator.geometry.get_all_cells().values()) + all_cells = list(operator.model.geometry.get_all_cells().values()) assert all_cells[0].fill.volume == 4.19 assert all_cells[1].fill.volume == 33.51 # mat2 is not depletable @@ -112,9 +113,8 @@ def test_diff_volume_method_divide_equally(model_with_volumes): chain_file=CHAIN_PATH ) - all_cells = list(operator.geometry.get_all_cells().values()) + all_cells = list(operator.model.geometry.get_all_cells().values()) assert all_cells[0].fill[0].volume == 51 assert all_cells[1].fill[0].volume == 51 # mat2 is not depletable assert all_cells[2].fill.volume is None - diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 682afb41a..24f136c8c 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -457,6 +457,20 @@ def test_deplete(run_in_tmpdir, pin_model_attributes, mpi_intracomm): assert after_xe + after_u == pytest.approx(initial_u, abs=1e-15) assert test_model.is_initialized is False + # check the tally output + def check_tally_output(): + with openmc.StatePoint('openmc_simulation_n0.h5') as sp: + flux = sp.get_tally(id=1).get_values(scores=['flux'])[0, 0, 0] + fission = sp.get_tally(id=1).get_values( + scores=['fission'])[0, 0, 0] + + # we're mainly just checking that the result was produced, + # so a rough numerical comparison doesn't hurt to have. + assert flux == pytest.approx(13.1, abs=0.2) + assert fission == pytest.approx(0.47, abs=0.2) + + check_tally_output() + # Reset the initial material densities mats[0].nuclides.clear() densities = initial_mat.get_nuclide_atom_densities() @@ -481,6 +495,8 @@ def test_deplete(run_in_tmpdir, pin_model_attributes, mpi_intracomm): assert after_xe == pytest.approx(after_lib_xe, abs=1e-15) assert after_u == pytest.approx(after_lib_u, abs=1e-15) + check_tally_output() + test_model.finalize_lib() @@ -531,6 +547,7 @@ def test_calc_volumes(run_in_tmpdir, pin_model_attributes, mpi_intracomm): test_model.finalize_lib() + def test_model_xml(run_in_tmpdir): # load a model from examples @@ -549,6 +566,7 @@ def test_model_xml(run_in_tmpdir): # XML files new_model.export_to_xml() + def test_single_xml_exec(run_in_tmpdir): pincell_model = openmc.examples.pwr_pin_cell() From e6a36ff7960e71cbe3a83a53138b61b61ad32bc5 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 23 Jan 2024 06:54:08 -0600 Subject: [PATCH 043/671] Add C API function for getting mesh bins for rasterized plot (#2854) --- openmc/lib/mesh.py | 48 ++++++++++++++++++++++++++++- setup.py | 2 +- src/mesh.cpp | 58 ++++++++++++++++++++++++++++++++++++ tests/unit_tests/test_lib.py | 25 ++++++++++++++++ 4 files changed, 131 insertions(+), 2 deletions(-) diff --git a/openmc/lib/mesh.py b/openmc/lib/mesh.py index 8a9953754..599b750fb 100644 --- a/openmc/lib/mesh.py +++ b/openmc/lib/mesh.py @@ -2,7 +2,7 @@ from collections.abc import Mapping from ctypes import (c_int, c_int32, c_char_p, c_double, POINTER, Structure, create_string_buffer, c_uint64, c_size_t) from random import getrandbits -from typing import Optional, List, Tuple +from typing import Optional, List, Tuple, Sequence from weakref import WeakValueDictionary import numpy as np @@ -13,6 +13,7 @@ from . import _dll from .core import _FortranObjectWithID from .error import _error_handler from .material import Material +from .plot import _Position __all__ = ['RegularMesh', 'RectilinearMesh', 'CylindricalMesh', 'SphericalMesh', 'UnstructuredMesh', 'meshes'] @@ -43,6 +44,11 @@ _dll.openmc_mesh_material_volumes.argtypes = [ POINTER(c_int), POINTER(c_uint64)] _dll.openmc_mesh_material_volumes.restype = c_int _dll.openmc_mesh_material_volumes.errcheck = _error_handler +_dll.openmc_mesh_get_plot_bins.argtypes = [ + c_int32, _Position, _Position, c_int, POINTER(c_int), POINTER(c_int32) +] +_dll.openmc_mesh_get_plot_bins.restype = c_int +_dll.openmc_mesh_get_plot_bins.errcheck = _error_handler _dll.openmc_get_mesh_index.argtypes = [c_int32, POINTER(c_int32)] _dll.openmc_get_mesh_index.restype = c_int _dll.openmc_get_mesh_index.errcheck = _error_handler @@ -203,6 +209,46 @@ class Mesh(_FortranObjectWithID): ]) return volumes + def get_plot_bins( + self, + origin: Sequence[float], + width: Sequence[float], + basis: str, + pixels: Sequence[int] + ) -> np.ndarray: + """Get mesh bin indices for a rasterized plot. + + .. versionadded:: 0.14.1 + + Parameters + ---------- + origin : iterable of float + Origin of the plotting view. Should have length 3. + width : iterable of float + Width of the plotting view. Should have length 2. + basis : {'xy', 'xz', 'yz'} + Plotting basis. + pixels : iterable of int + Number of pixels in each direction. Should have length 2. + + Returns + ------- + 2D numpy array with mesh bin indices corresponding to each pixel within + the plotting view. + + """ + origin = _Position(*origin) + width = _Position(*width) + basis = {'xy': 1, 'xz': 2, 'yz': 3}[basis] + pixel_array = (c_int*2)(*pixels) + img_data = np.zeros((pixels[1], pixels[0]), dtype=np.dtype('int32')) + + _dll.openmc_mesh_get_plot_bins( + self._index, origin, width, basis, pixel_array, + img_data.ctypes.data_as(POINTER(c_int32)) + ) + return img_data + class RegularMesh(Mesh): """RegularMesh stored internally. diff --git a/setup.py b/setup.py index eeb0b3f29..84a2ad367 100755 --- a/setup.py +++ b/setup.py @@ -64,7 +64,7 @@ kwargs = { # Dependencies 'python_requires': '>=3.7', 'install_requires': [ - 'numpy>=1.9', 'h5py', 'scipy', 'ipython', 'matplotlib', + 'numpy>=1.9', 'h5py', 'scipy<1.12', 'ipython', 'matplotlib', 'pandas', 'lxml', 'uncertainties' ], 'extras_require': { diff --git a/src/mesh.cpp b/src/mesh.cpp index b70692347..f343b27e3 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -29,6 +29,7 @@ #include "openmc/message_passing.h" #include "openmc/openmp_interface.h" #include "openmc/particle_data.h" +#include "openmc/plot.h" #include "openmc/random_dist.h" #include "openmc/search.h" #include "openmc/settings.h" @@ -1898,6 +1899,63 @@ extern "C" int openmc_mesh_material_volumes(int32_t index, int n_sample, return (n == -1) ? OPENMC_E_ALLOCATE : 0; } +extern "C" int openmc_mesh_get_plot_bins(int32_t index, Position origin, + Position width, int basis, int* pixels, int32_t* data) +{ + if (int err = check_mesh(index)) + return err; + const auto& mesh = model::meshes[index].get(); + + int pixel_width = pixels[0]; + int pixel_height = pixels[1]; + + // get pixel size + double in_pixel = (width[0]) / static_cast(pixel_width); + double out_pixel = (width[1]) / static_cast(pixel_height); + + // setup basis indices and initial position centered on pixel + int in_i, out_i; + Position xyz = origin; + enum class PlotBasis { xy = 1, xz = 2, yz = 3 }; + PlotBasis basis_enum = static_cast(basis); + switch (basis_enum) { + case PlotBasis::xy: + in_i = 0; + out_i = 1; + break; + case PlotBasis::xz: + in_i = 0; + out_i = 2; + break; + case PlotBasis::yz: + in_i = 1; + out_i = 2; + break; + default: + UNREACHABLE(); + } + + // set initial position + xyz[in_i] = origin[in_i] - width[0] / 2. + in_pixel / 2.; + xyz[out_i] = origin[out_i] + width[1] / 2. - out_pixel / 2.; + +#pragma omp parallel + { + Position r = xyz; + +#pragma omp for + for (int y = 0; y < pixel_height; y++) { + r[out_i] = xyz[out_i] - out_pixel * y; + for (int x = 0; x < pixel_width; x++) { + r[in_i] = xyz[in_i] + in_pixel * x; + data[pixel_width * y + x] = mesh->get_bin(r); + } + } + } + + return 0; +} + //! Get the dimension of a regular mesh extern "C" int openmc_regular_mesh_get_dimension( int32_t index, int** dims, int* n) diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index 8aa2ea6e9..6ae13ef76 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -605,6 +605,31 @@ def test_regular_mesh(lib_init): assert sum(f[1] for f in elem_vols) == pytest.approx(1.26 * 1.26, 1e-2) +def test_regular_mesh_get_plot_bins(lib_init): + mesh: openmc.lib.RegularMesh = openmc.lib.meshes[2] + mesh.dimension = (2, 2, 1) + mesh.set_parameters(lower_left=(-1.0, -1.0, -0.5), + upper_right=(1.0, 1.0, 0.5)) + + # Get bins for a plot view covering only a single mesh bin + mesh_bins = mesh.get_plot_bins((-0.5, -0.5, 0.), (0.1, 0.1), 'xy', (20, 20)) + assert (mesh_bins == 0).all() + mesh_bins = mesh.get_plot_bins((0.5, 0.5, 0.), (0.1, 0.1), 'xy', (20, 20)) + assert (mesh_bins == 3).all() + + # Get bins for a plot view covering all mesh bins. Note that the y direction + # (first dimension) is flipped for plotting purposes + mesh_bins = mesh.get_plot_bins((0., 0., 0.), (2., 2.), 'xy', (20, 20)) + assert (mesh_bins[:10, :10] == 2).all() + assert (mesh_bins[:10, 10:] == 3).all() + assert (mesh_bins[10:, :10] == 0).all() + assert (mesh_bins[10:, 10:] == 1).all() + + # Get bins for a plot view outside of the mesh + mesh_bins = mesh.get_plot_bins((100., 100., 0.), (2., 2.), 'xy', (20, 20)) + assert (mesh_bins == -1).all() + + def test_rectilinear_mesh(lib_init): mesh = openmc.lib.RectilinearMesh() x_grid = [-10., 0., 10.] From 09eb33ac217dc7695049e5b7f76d45f6332a1499 Mon Sep 17 00:00:00 2001 From: Davide Mancusi Date: Tue, 23 Jan 2024 16:48:21 +0100 Subject: [PATCH 044/671] Fix compilation on CentOS 7 (missing link to libdl) (#2849) Co-authored-by: Paul Romano --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index bf3555d84..ec6ea6179 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -497,7 +497,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 gsl::gsl-lite-v1 fmt::fmt) + xtensor gsl::gsl-lite-v1 fmt::fmt ${CMAKE_DL_LIBS}) if(TARGET pugixml::pugixml) target_link_libraries(libopenmc pugixml::pugixml) From cb7ef009b9f6865267b7f683abc5b4f16673669a Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Mon, 29 Jan 2024 18:59:16 -0600 Subject: [PATCH 045/671] Setting surf_source_ attribute for DAGMC surfaces. (#2857) Co-authored-by: Paul Romano --- src/dagmc.cpp | 4 +++ tests/regression_tests/dagmc/legacy/test.py | 36 +++++++++++++++++++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/dagmc.cpp b/src/dagmc.cpp index 56722fde6..6a4865c39 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -280,6 +280,10 @@ void DAGUniverse::init_geometry() s->id_ = adjust_geometry_ids_ ? next_surf_id++ : dagmc_instance_->id_by_index(2, i + 1); + // set surface source attribute if needed + if (contains(settings::source_write_surf_id, s->id_)) + s->surf_source_ = true; + // set BCs std::string bc_value = dmd_ptr->get_surface_property("boundary", surf_handle); diff --git a/tests/regression_tests/dagmc/legacy/test.py b/tests/regression_tests/dagmc/legacy/test.py index 7e4e6e134..884264f30 100644 --- a/tests/regression_tests/dagmc/legacy/test.py +++ b/tests/regression_tests/dagmc/legacy/test.py @@ -1,9 +1,13 @@ +from pathlib import Path + import openmc import openmc.lib -from pathlib import Path +import h5py +import numpy as np import pytest -from tests.testing_harness import PyAPITestHarness + +from tests.testing_harness import PyAPITestHarness, config pytestmark = pytest.mark.skipif( not openmc.lib._dagmc_enabled(), @@ -13,7 +17,7 @@ pytestmark = pytest.mark.skipif( def model(): openmc.reset_auto_ids() - model = openmc.model.Model() + model = openmc.Model() # settings model.settings.batches = 5 @@ -73,6 +77,32 @@ def test_missing_material_name(model): assert exp_error_msg in str(exec_info.value) +def test_surf_source(model): + # create a surface source read on this model to ensure + # particles are being generated correctly + n = 100 + model.settings.surf_source_write = {'surface_ids': [1], 'max_particles': n} + + # If running in MPI mode, setup proper keyword arguments for run() + kwargs = {'openmc_exec': config['exe']} + if config['mpi']: + kwargs['mpi_args'] = [config['mpiexec'], '-n', config['mpi_np']] + model.run(**kwargs) + + with h5py.File('surface_source.h5') as fh: + assert fh.attrs['filetype'] == b'source' + arr = fh['source_bank'][...] + expected_size = n * int(config['mpi_np']) if config['mpi'] else n + assert arr.size == expected_size + + # check that all particles are on surface 1 (radius = 7) + xs = arr[:]['r']['x'] + ys = arr[:]['r']['y'] + rad = np.sqrt(xs**2 + ys**2) + assert np.allclose(rad, 7.0) + + def test_dagmc(model): harness = PyAPITestHarness('statepoint.5.h5', model) harness.main() + From f14fc55e60463710d3f165b0a9cdc2ef9fee1534 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 30 Jan 2024 21:24:30 -0600 Subject: [PATCH 046/671] Add bounding_box property to RectilinearMesh and UnstructuredMesh (#2861) --- openmc/mesh.py | 65 ++++++++++++++++++++++------------- tests/unit_tests/test_mesh.py | 12 ++++++- 2 files changed, 52 insertions(+), 25 deletions(-) diff --git a/openmc/mesh.py b/openmc/mesh.py index 435a44d2c..a12bd5853 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -1,3 +1,4 @@ +from __future__ import annotations import typing import warnings from abc import ABC, abstractmethod @@ -35,6 +36,9 @@ class MeshBase(IDManagerMixin, ABC): Unique identifier for the mesh name : str Name of the mesh + bounding_box : openmc.BoundingBox + Axis-aligned bounding box of the mesh as defined by the upper-right and + lower-left coordinates. """ @@ -58,6 +62,10 @@ class MeshBase(IDManagerMixin, ABC): else: self._name = '' + @property + def bounding_box(self) -> openmc.BoundingBox: + return openmc.BoundingBox(self.lower_left, self.upper_right) + def __repr__(self): string = type(self).__name__ + '\n' string += '{0: <16}{1}{2}\n'.format('\tID', '=\t', self._id) @@ -507,9 +515,9 @@ class RegularMesh(StructuredMesh): upper_right : Iterable of float The upper-right corner of the structured mesh. If only two coordinate are given, it is assumed that the mesh is an x-y mesh. - bounding_box: openmc.BoundingBox - Axis-aligned bounding box of the cell defined by the upper-right and lower- - left coordinates + bounding_box : openmc.BoundingBox + Axis-aligned bounding box of the mesh as defined by the upper-right and + lower-left coordinates. width : Iterable of float The width of mesh cells in each direction. indices : Iterable of tuple @@ -660,12 +668,6 @@ class RegularMesh(StructuredMesh): x1, = self.upper_right return (np.linspace(x0, x1, nx + 1),) - @property - def bounding_box(self): - return openmc.BoundingBox( - np.array(self.lower_left), np.array(self.upper_right) - ) - def __repr__(self): string = super().__repr__() string += '{0: <16}{1}{2}\n'.format('\tDimensions', '=\t', self.n_dimension) @@ -1005,6 +1007,9 @@ class RectilinearMesh(StructuredMesh): indices : Iterable of tuple An iterable of mesh indices for each mesh element, e.g. [(1, 1, 1), (2, 1, 1), ...] + bounding_box : openmc.BoundingBox + Axis-aligned bounding box of the mesh as defined by the upper-right and + lower-left coordinates. """ @@ -1056,6 +1061,14 @@ class RectilinearMesh(StructuredMesh): def _grids(self): return (self.x_grid, self.y_grid, self.z_grid) + @property + def lower_left(self): + return np.array([self.x_grid[0], self.y_grid[0], self.z_grid[0]]) + + @property + def upper_right(self): + return np.array([self.x_grid[-1], self.y_grid[-1], self.z_grid[-1]]) + @property def volumes(self): """Return Volumes for every mesh cell @@ -1222,9 +1235,9 @@ class CylindricalMesh(StructuredMesh): upper_right : Iterable of float The upper-right corner of the structured mesh. If only two coordinate are given, it is assumed that the mesh is an x-y mesh. - bounding_box: openmc.OpenMC - Axis-aligned cartesian bounding box of cell defined by upper-right and lower- - left coordinates + bounding_box : openmc.BoundingBox + Axis-aligned bounding box of the mesh as defined by the upper-right and + lower-left coordinates. """ @@ -1321,10 +1334,6 @@ class CylindricalMesh(StructuredMesh): self.origin[2] + self.z_grid[-1] )) - @property - def bounding_box(self): - return openmc.BoundingBox(self.lower_left, self.upper_right) - def __repr__(self): fmt = '{0: <16}{1}{2}\n' string = super().__repr__() @@ -1666,8 +1675,8 @@ class SphericalMesh(StructuredMesh): The upper-right corner of the structured mesh. If only two coordinate are given, it is assumed that the mesh is an x-y mesh. bounding_box : openmc.BoundingBox - Axis-aligned bounding box of the cell defined by the upper-right and lower- - left coordinates + Axis-aligned bounding box of the mesh as defined by the upper-right and + lower-left coordinates. """ @@ -1758,10 +1767,6 @@ class SphericalMesh(StructuredMesh): r = self.r_grid[-1] return np.array((self.origin[0] + r, self.origin[1] + r, self.origin[2] + r)) - @property - def bounding_box(self): - return openmc.BoundingBox(self.lower_left, self.upper_right) - def __repr__(self): fmt = '{0: <16}{1}{2}\n' string = super().__repr__() @@ -1943,8 +1948,8 @@ class UnstructuredMesh(MeshBase): library : {'moab', 'libmesh'} Mesh library used for the unstructured mesh tally output : bool - Indicates whether or not automatic tally output should - be generated for this mesh + Indicates whether or not automatic tally output should be generated for + this mesh volumes : Iterable of float Volumes of the unstructured mesh elements centroids : numpy.ndarray @@ -1964,6 +1969,10 @@ class UnstructuredMesh(MeshBase): .. versionadded:: 0.13.1 total_volume : float Volume of the unstructured mesh in total + bounding_box : openmc.BoundingBox + Axis-aligned bounding box of the mesh as defined by the upper-right and + lower-left coordinates. + """ _UNSUPPORTED_ELEM = -1 @@ -2096,6 +2105,14 @@ class UnstructuredMesh(MeshBase): self.length_multiplier) return string + @property + def lower_left(self): + return self.vertices.min(axis=0) + + @property + def upper_right(self): + return self.vertices.max(axis=0) + def centroid(self, bin: int): """Return the vertex averaged centroid of an element diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index 2a4ff6cbb..50e1236a8 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -4,7 +4,6 @@ import numpy as np import pytest import openmc -from openmc import RegularMesh, RectilinearMesh, CylindricalMesh, SphericalMesh @pytest.mark.parametrize("val_left,val_right", [(0, 0), (-1., -1.), (2.0, 2)]) def test_raises_error_when_flat(val_left, val_right): @@ -49,6 +48,17 @@ def test_regular_mesh_bounding_box(): np.testing.assert_array_equal(bb.upper_right, (2, 3, 5)) +def test_rectilinear_mesh_bounding_box(): + mesh = openmc.RectilinearMesh() + mesh.x_grid = [0., 1., 5., 10.] + mesh.y_grid = [-10., -5., 0.] + mesh.z_grid = [-100., 0., 100.] + bb = mesh.bounding_box + assert isinstance(bb, openmc.BoundingBox) + np.testing.assert_array_equal(bb.lower_left, (0., -10. ,-100.)) + np.testing.assert_array_equal(bb.upper_right, (10., 0., 100.)) + + def test_cylindrical_mesh_bounding_box(): # test with mesh at origin (0, 0, 0) mesh = openmc.CylindricalMesh( From bf33a9e11bf288639df7d6aba9c6b39aeff0afdd Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 13 Feb 2024 06:44:05 -0600 Subject: [PATCH 047/671] Add `openmc_mesh_get_volumes` C API function (#2869) --- include/openmc/capi.h | 1 + openmc/lib/mesh.py | 28 +++++++++++++++++++++++++++- openmc/material.py | 2 +- src/mesh.cpp | 11 +++++++++++ tests/unit_tests/test_lib.py | 14 ++++++++++++++ 5 files changed, 54 insertions(+), 2 deletions(-) diff --git a/include/openmc/capi.h b/include/openmc/capi.h index 063ed8d9f..5f98152cd 100644 --- a/include/openmc/capi.h +++ b/include/openmc/capi.h @@ -106,6 +106,7 @@ int openmc_mesh_filter_set_translation(int32_t index, double translation[3]); int openmc_mesh_get_id(int32_t index, int32_t* id); int openmc_mesh_set_id(int32_t index, int32_t id); int openmc_mesh_get_n_elements(int32_t index, size_t* n); +int openmc_mesh_get_volumes(int32_t index, double* volumes); int openmc_mesh_material_volumes(int32_t index, int n_sample, int bin, int result_size, void* result, int* hits, uint64_t* seed); int openmc_meshsurface_filter_get_mesh(int32_t index, int32_t* index_mesh); diff --git a/openmc/lib/mesh.py b/openmc/lib/mesh.py index 599b750fb..7f17dd81f 100644 --- a/openmc/lib/mesh.py +++ b/openmc/lib/mesh.py @@ -39,6 +39,9 @@ _dll.openmc_mesh_set_id.errcheck = _error_handler _dll.openmc_mesh_get_n_elements.argtypes = [c_int32, POINTER(c_size_t)] _dll.openmc_mesh_get_n_elements.restype = c_int _dll.openmc_mesh_get_n_elements.errcheck = _error_handler +_dll.openmc_mesh_get_volumes.argtypes = [c_int32, POINTER(c_double)] +_dll.openmc_mesh_get_volumes.restype = c_int +_dll.openmc_mesh_get_volumes.errcheck = _error_handler _dll.openmc_mesh_material_volumes.argtypes = [ c_int32, c_int, c_int, c_int, POINTER(_MaterialVolume), POINTER(c_int), POINTER(c_uint64)] @@ -149,11 +152,18 @@ class Mesh(_FortranObjectWithID): _dll.openmc_mesh_set_id(self._index, mesh_id) @property - def n_elements(self): + def n_elements(self) -> int: n = c_size_t() _dll.openmc_mesh_get_n_elements(self._index, n) return n.value + @property + def volumes(self) -> np.ndarray: + volumes = np.empty((self.n_elements,)) + _dll.openmc_mesh_get_volumes( + self._index, volumes.ctypes.data_as(POINTER(c_double))) + return volumes + def material_volumes( self, n_samples: int = 10_000, @@ -276,6 +286,10 @@ class RegularMesh(Mesh): are given, it is assumed that the mesh is an x-y mesh. width : numpy.ndarray The width of mesh cells in each direction. + n_elements : int + Total number of mesh elements. + volumes : numpy.ndarray + Volume of each mesh element in [cm^3] """ mesh_type = 'regular' @@ -358,6 +372,10 @@ class RectilinearMesh(Mesh): The upper-right corner of the structrued mesh. width : numpy.ndarray The width of mesh cells in each direction. + n_elements : int + Total number of mesh elements. + volumes : numpy.ndarray + Volume of each mesh element in [cm^3] """ mesh_type = 'rectilinear' @@ -457,6 +475,10 @@ class CylindricalMesh(Mesh): The upper-right corner of the structrued mesh. width : numpy.ndarray The width of mesh cells in each direction. + n_elements : int + Total number of mesh elements. + volumes : numpy.ndarray + Volume of each mesh element in [cm^3] """ mesh_type = 'cylindrical' @@ -555,6 +577,10 @@ class SphericalMesh(Mesh): The upper-right corner of the structrued mesh. width : numpy.ndarray The width of mesh cells in each direction. + n_elements : int + Total number of mesh elements. + volumes : numpy.ndarray + Volume of each mesh element in [cm^3] """ mesh_type = 'spherical' diff --git a/openmc/material.py b/openmc/material.py index d42c489ae..c31862a18 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -571,7 +571,7 @@ class Material(IDManagerMixin): for component, params in components.items(): cv.check_type('component', component, str) - if isinstance(params, float): + if isinstance(params, Real): params = {'percent': params} else: diff --git a/src/mesh.cpp b/src/mesh.cpp index f343b27e3..bc6781ad3 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -1881,6 +1881,17 @@ extern "C" int openmc_mesh_get_n_elements(int32_t index, size_t* n) return 0; } +//! Get the volume of each element in the mesh +extern "C" int openmc_mesh_get_volumes(int32_t index, double* volumes) +{ + if (int err = check_mesh(index)) + return err; + for (int i = 0; i < model::meshes[index]->n_bins(); ++i) { + volumes[i] = model::meshes[index]->volume(i); + } + return 0; +} + extern "C" int openmc_mesh_material_volumes(int32_t index, int n_sample, int bin, int result_size, void* result, int* hits, uint64_t* seed) { diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index 6ae13ef76..1310a6a7f 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -566,6 +566,8 @@ def test_regular_mesh(lib_init): assert mesh.upper_right == pytest.approx(ur) assert mesh.width == pytest.approx(width) + np.testing.assert_allclose(mesh.volumes, 1.0) + meshes = openmc.lib.meshes assert isinstance(meshes, Mapping) assert len(meshes) == 1 @@ -644,6 +646,8 @@ def test_rectilinear_mesh(lib_init): for k, diff_z in enumerate(np.diff(z_grid)): assert np.all(mesh.width[i, j, k, :] == (10, 10, 10)) + np.testing.assert_allclose(mesh.volumes, 1000.0) + with pytest.raises(exc.AllocationError): mesh2 = openmc.lib.RectilinearMesh(mesh.id) @@ -688,6 +692,9 @@ def test_cylindrical_mesh(lib_init): for k, _ in enumerate(np.diff(z_grid)): assert np.allclose(mesh.width[i, j, k, :], (5, deg2rad(10), 10)) + np.testing.assert_allclose(mesh.volumes[::2], 10/360 * pi * 5**2 * 10) + np.testing.assert_allclose(mesh.volumes[1::2], 10/360 * pi * (10**2 - 5**2) * 10) + with pytest.raises(exc.AllocationError): mesh2 = openmc.lib.CylindricalMesh(mesh.id) @@ -734,6 +741,13 @@ def test_spherical_mesh(lib_init): for k, _ in enumerate(np.diff(phi_grid)): assert np.allclose(mesh.width[i, j, k, :], (5, deg2rad(10), deg2rad(10))) + dtheta = lambda d1, d2: np.cos(deg2rad(d1)) - np.cos(deg2rad(d2)) + f = 1/3 * deg2rad(10.) + np.testing.assert_allclose(mesh.volumes[::4], f * 5**3 * dtheta(0., 10.)) + np.testing.assert_allclose(mesh.volumes[1::4], f * (10**3 - 5**3) * dtheta(0., 10.)) + np.testing.assert_allclose(mesh.volumes[2::4], f * 5**3 * dtheta(10., 20.)) + np.testing.assert_allclose(mesh.volumes[3::4], f * (10**3 - 5**3) * dtheta(10., 20.)) + with pytest.raises(exc.AllocationError): mesh2 = openmc.lib.SphericalMesh(mesh.id) From 33c910ddd3cb99bab974184343b37d90034140a6 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Wed, 14 Feb 2024 23:19:10 -0600 Subject: [PATCH 048/671] Fix issue with Cell::get_contained_cells() utility function (#2873) --- src/cell.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/cell.cpp b/src/cell.cpp index 00083d967..a46f05687 100644 --- a/src/cell.cpp +++ b/src/cell.cpp @@ -1272,6 +1272,9 @@ struct ParentCellStack { //! compute an instance for the provided distribcell index int32_t compute_instance(int32_t distribcell_index) const { + if (distribcell_index == C_NONE) + return 0; + int32_t instance = 0; for (const auto& parent_cell : this->parent_cells_) { auto& cell = model::cells[parent_cell.cell_index]; From 5005c3cdc6fba0ee9eb945ff6f9816f50a8d724e Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Thu, 15 Feb 2024 07:30:46 +0000 Subject: [PATCH 049/671] type hinting openmc.deplete.abc.py (#2866) Co-authored-by: Paul Romano --- openmc/deplete/abc.py | 76 ++++++++++++++++++++++++++++++++----------- 1 file changed, 57 insertions(+), 19 deletions(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 60774871c..1dea8440c 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -3,6 +3,7 @@ This module contains Abstract Base Classes for implementing operator, integrator, depletion system solver, and operator helper classes """ +from __future__ import annotations from abc import ABC, abstractmethod from collections import namedtuple, defaultdict from collections.abc import Iterable, Callable @@ -13,17 +14,20 @@ from contextlib import contextmanager import os from pathlib import Path import time +from typing import Optional, Union, Sequence from warnings import warn -from numpy import nonzero, empty, asarray +import numpy as np from uncertainties import ufloat from openmc.checkvalue import check_type, check_greater_than, PathLike from openmc.mpi import comm +from openmc import Material from .stepresult import StepResult from .chain import Chain from .results import Results from .pool import deplete +from .reaction_rates import ReactionRates from .transfer_rates import TransferRates @@ -176,7 +180,7 @@ class TransportOperator(ABC): pass @abstractmethod - def write_bos_data(self, step): + def write_bos_data(self, step: int): """Document beginning of step data for a given step Called at the beginning of a depletion step and at @@ -215,7 +219,7 @@ class ReactionRateHelper(ABC): def __init__(self, n_nucs, n_react): self._nuclides = None - self._results_cache = empty((n_nucs, n_react)) + self._results_cache = np.empty((n_nucs, n_react)) @abstractmethod def generate_tallies(self, materials, scores): @@ -232,7 +236,12 @@ class ReactionRateHelper(ABC): self._nuclides = nuclides @abstractmethod - def get_material_rates(self, mat_id, nuc_index, react_index): + def get_material_rates( + self, + mat_id: int, + nuc_index: Sequence[str], + react_index: Sequence[str] + ): """Return 2D array of [nuclide, reaction] reaction rates Parameters @@ -245,7 +254,7 @@ class ReactionRateHelper(ABC): Ordering of reactions """ - def divide_by_atoms(self, number): + def divide_by_atoms(self, number: Sequence[float]): """Normalize reaction rates by number of atoms Acts on the current material examined by :meth:`get_material_rates` @@ -262,7 +271,7 @@ class ReactionRateHelper(ABC): normalized by the number of nuclides """ - mask = nonzero(number) + mask = np.nonzero(number) results = self._results_cache for col in range(results.shape[1]): results[mask, col] /= number[mask] @@ -294,7 +303,7 @@ class NormalizationHelper(ABC): """Reset state for normalization""" @abstractmethod - def prepare(self, chain_nucs, rate_index): + def prepare(self, chain_nucs: Sequence[str], rate_index: dict): """Perform work needed to obtain energy produced This method is called prior to calculating the reaction rates @@ -333,7 +342,7 @@ class NormalizationHelper(ABC): self._nuclides = nuclides @abstractmethod - def factor(self, source_rate): + def factor(self, source_rate: float): """Return normalization factor Parameters @@ -436,7 +445,7 @@ class FissionYieldHelper(ABC): in parallel mode. """ - def update_tally_nuclides(self, nuclides): + def update_tally_nuclides(self, nuclides: Sequence[str]) -> list: """Return nuclides with non-zero densities and yield data Parameters @@ -559,8 +568,16 @@ class Integrator(ABC): """ - def __init__(self, operator, timesteps, power=None, power_density=None, - source_rates=None, timestep_units='s', solver="cram48"): + def __init__( + self, + operator: TransportOperator, + timesteps: Sequence[float], + power: Optional[Union[float, Sequence[float]]] = None, + power_density: Optional[Union[float, Sequence[float]]] = None, + source_rates: Optional[Sequence[float]] = None, + timestep_units: str = 's', + solver: str = "cram48" + ): # Check number of stages previously used if operator.prev_res is not None: res = operator.prev_res[-1] @@ -632,8 +649,8 @@ class Integrator(ABC): else: raise ValueError("Invalid timestep unit '{}'".format(unit)) - self.timesteps = asarray(seconds) - self.source_rates = asarray(source_rates) + self.timesteps = np.asarray(seconds) + self.source_rates = np.asarray(source_rates) self.transfer_rates = None @@ -692,7 +709,14 @@ class Integrator(ABC): return time.time() - start, results @abstractmethod - def __call__(self, n, rates, dt, source_rate, i): + def __call__( + self, + n: Sequence[np.ndarray], + rates: ReactionRates, + dt: float, + source_rate: float, + i: int + ): """Perform the integration across one time step Parameters @@ -829,8 +853,14 @@ class Integrator(ABC): self.operator.finalize() - def add_transfer_rate(self, material, components, transfer_rate, - transfer_rate_units='1/s', destination_material=None): + def add_transfer_rate( + self, + material: Union[str, int, Material], + components: Sequence[str], + transfer_rate: float, + transfer_rate_units: str = '1/s', + destination_material: Optional[Union[str, int, Material]] = None + ): """Add transfer rates to depletable material. Parameters @@ -943,9 +973,17 @@ class SIIntegrator(Integrator): """ - def __init__(self, operator, timesteps, power=None, power_density=None, - source_rates=None, timestep_units='s', n_steps=10, - solver="cram48"): + def __init__( + self, + operator: TransportOperator, + timesteps: Sequence[float], + power: Optional[Union[float, Sequence[float]]] = None, + power_density: Optional[Union[float, Sequence[float]]] = None, + source_rates: Optional[Sequence[float]] = None, + timestep_units: str = 's', + n_steps: int = 10, + solver: str = "cram48" + ): check_type("n_steps", n_steps, Integral) check_greater_than("n_steps", n_steps, 0) super().__init__( From 3b575a4daa343a8598dff5f0b191da5b7c9b3019 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Fri, 16 Feb 2024 15:16:36 -0600 Subject: [PATCH 050/671] Adding openmc.read_source_file (#2858) Co-authored-by: Paul Romano --- docs/source/pythonapi/base.rst | 1 + openmc/source.py | 34 ++++++++++++++++++++++++++++ tests/unit_tests/test_source_file.py | 24 ++++++++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/docs/source/pythonapi/base.rst b/docs/source/pythonapi/base.rst index 705aecee9..7b924e374 100644 --- a/docs/source/pythonapi/base.rst +++ b/docs/source/pythonapi/base.rst @@ -35,6 +35,7 @@ Simulation Settings :nosignatures: :template: myfunction.rst + openmc.read_source_file openmc.write_source_file openmc.wwinp_to_wws diff --git a/openmc/source.py b/openmc/source.py index 1ba90c921..441bf4e76 100644 --- a/openmc/source.py +++ b/openmc/source.py @@ -885,3 +885,37 @@ def write_source_file( with h5py.File(filename, **kwargs) as fh: fh.attrs['filetype'] = np.string_("source") fh.create_dataset('source_bank', data=arr, dtype=source_dtype) + + +def read_source_file(filename: PathLike) -> typing.List[SourceParticle]: + """Read a source file and return a list of source particles. + + .. versionadded:: 0.14.1 + + Parameters + ---------- + filename : str or path-like + Path to source file to read + + Returns + ------- + list of SourceParticle + Source particles read from file + + See Also + -------- + openmc.SourceParticle + + """ + with h5py.File(filename, 'r') as fh: + filetype = fh.attrs['filetype'] + arr = fh['source_bank'][...] + + if filetype != b'source': + raise ValueError(f'File {filename} is not a source file') + + source_particles = [] + for *params, particle in arr: + source_particles.append(SourceParticle(*params, ParticleType(particle))) + + return source_particles diff --git a/tests/unit_tests/test_source_file.py b/tests/unit_tests/test_source_file.py index ec10260ba..d9fb3c1f9 100644 --- a/tests/unit_tests/test_source_file.py +++ b/tests/unit_tests/test_source_file.py @@ -45,6 +45,30 @@ def test_source_file(run_in_tmpdir): assert np.all(arr['particle'] == 0) + # Ensure sites read in are consistent + sites = openmc.read_source_file('test_source.h5') + + assert filetype == b'source' + xs = np.array([site.r[0] for site in sites]) + ys = np.array([site.r[1] for site in sites]) + zs = np.array([site.r[2] for site in sites]) + assert np.all((xs > 0.0) & (xs < 1.0)) + assert np.all(ys == np.arange(1000)) + assert np.all(zs == 0.0) + u = np.array([s.u for s in sites]) + assert np.all(u[..., 0] == 0.0) + assert np.all(u[..., 1] == 0.0) + assert np.all(u[..., 2] == 1.0) + E = np.array([s.E for s in sites]) + assert np.all(E == n - np.arange(n)) + wgt = np.array([s.wgt for s in sites]) + assert np.all(wgt == 1.0) + dgs = np.array([s.delayed_group for s in sites]) + assert np.all(dgs == 0) + p_types = np.array([s.particle for s in sites]) + assert np.all(p_types == 0) + + def test_wrong_source_attributes(run_in_tmpdir): # Create a source file with animal attributes source_dtype = np.dtype([ From e8f68a0159302f69ad04f0ca56e062f88324a411 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Sat, 17 Feb 2024 18:21:52 +0000 Subject: [PATCH 051/671] adding resulting nuclide to cross section plot legend (#2851) Co-authored-by: Paul Romano --- openmc/data/data.py | 87 +++++++++++++++++++ openmc/deplete/chain.py | 188 +++++++++++++++++++--------------------- openmc/plotter.py | 5 ++ 3 files changed, 183 insertions(+), 97 deletions(-) diff --git a/openmc/data/data.py b/openmc/data/data.py index 609a901d9..7caf31e26 100644 --- a/openmc/data/data.py +++ b/openmc/data/data.py @@ -179,6 +179,93 @@ ATOMIC_SYMBOL = {0: 'n', 1: 'H', 2: 'He', 3: 'Li', 4: 'Be', 5: 'B', 6: 'C', 118: 'Og'} ATOMIC_NUMBER = {value: key for key, value in ATOMIC_SYMBOL.items()} +DADZ = { + '(n,2nd)': (-3, -1), + '(n,2n)': (-1, 0), + '(n,3n)': (-2, 0), + '(n,na)': (-4, -2), + '(n,n3a)': (-12, -6), + '(n,2na)': (-5, -2), + '(n,3na)': (-6, -2), + '(n,np)': (-1, -1), + '(n,n2a)': (-8, -4), + '(n,2n2a)': (-9, -4), + '(n,nd)': (-2, -1), + '(n,nt)': (-3, -1), + '(n,n3He)': (-3, -2), + '(n,nd2a)': (-10, -5), + '(n,nt2a)': (-11, -5), + '(n,4n)': (-3, 0), + '(n,2np)': (-2, -1), + '(n,3np)': (-3, -1), + '(n,n2p)': (-2, -2), + '(n,npa)': (-5, -3), + '(n,gamma)': (1, 0), + '(n,p)': (0, -1), + '(n,d)': (-1, -1), + '(n,t)': (-2, -1), + '(n,3He)': (-2, -2), + '(n,a)': (-3, -2), + '(n,2a)': (-7, -4), + '(n,3a)': (-11, -6), + '(n,2p)': (-1, -2), + '(n,pa)': (-4, -3), + '(n,t2a)': (-10, -5), + '(n,d2a)': (-9, -5), + '(n,pd)': (-2, -2), + '(n,pt)': (-3, -2), + '(n,da)': (-5, -3), + '(n,5n)': (-4, 0), + '(n,6n)': (-5, 0), + '(n,2nt)': (-4, -1), + '(n,ta)': (-6, -3), + '(n,4np)': (-4, -1), + '(n,3nd)': (-4, -1), + '(n,nda)': (-6, -3), + '(n,2npa)': (-6, -3), + '(n,7n)': (-6, 0), + '(n,8n)': (-7, 0), + '(n,5np)': (-5, -1), + '(n,6np)': (-6, -1), + '(n,7np)': (-7, -1), + '(n,4na)': (-7, -2), + '(n,5na)': (-8, -2), + '(n,6na)': (-9, -2), + '(n,7na)': (-10, -2), + '(n,4nd)': (-5, -1), + '(n,5nd)': (-6, -1), + '(n,6nd)': (-7, -1), + '(n,3nt)': (-5, -1), + '(n,4nt)': (-6, -1), + '(n,5nt)': (-7, -1), + '(n,6nt)': (-8, -1), + '(n,2n3He)': (-4, -2), + '(n,3n3He)': (-5, -2), + '(n,4n3He)': (-6, -2), + '(n,3n2p)': (-4, -2), + '(n,3n2a)': (-10, -4), + '(n,3npa)': (-7, -3), + '(n,dt)': (-4, -2), + '(n,npd)': (-3, -2), + '(n,npt)': (-4, -2), + '(n,ndt)': (-5, -2), + '(n,np3He)': (-4, -3), + '(n,nd3He)': (-5, -3), + '(n,nt3He)': (-6, -3), + '(n,nta)': (-7, -3), + '(n,2n2p)': (-3, -2), + '(n,p3He)': (-4, -3), + '(n,d3He)': (-5, -3), + '(n,3Hea)': (-6, -4), + '(n,4n2p)': (-5, -2), + '(n,4n2a)': (-11, -4), + '(n,4npa)': (-8, -3), + '(n,3p)': (-2, -3), + '(n,n3p)': (-3, -3), + '(n,3n2pa)': (-8, -4), + '(n,5n2p)': (-6, -2), +} + # Values here are from the Committee on Data for Science and Technology # (CODATA) 2018 recommendation (https://physics.nist.gov/cuu/Constants/). diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index e06cb5725..607168955 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -7,115 +7,109 @@ loaded from an .xml file and all the nuclides are linked together. from io import StringIO from itertools import chain import math -import os import re from collections import defaultdict, namedtuple from collections.abc import Mapping, Iterable from numbers import Real, Integral from warnings import warn -from openmc.checkvalue import check_type, check_greater_than -from openmc.data import gnds_name, zam, DataLibrary -from openmc.exceptions import DataError -from .nuclide import FissionYieldDistribution - import lxml.etree as ET import scipy.sparse as sp +from openmc.checkvalue import check_type, check_greater_than +from openmc.data import gnds_name, zam +from .nuclide import FissionYieldDistribution, Nuclide import openmc.data -from openmc._xml import clean_indentation -from .nuclide import Nuclide, DecayTuple, ReactionTuple -# tuple of (possible MT values, (dA, dZ), secondaries) where dA is the change in -# the mass number and dZ is the change in the atomic number -ReactionInfo = namedtuple('ReactionInfo', ('mts', 'dadz', 'secondaries')) +# tuple of (possible MT values, secondaries) +ReactionInfo = namedtuple('ReactionInfo', ('mts', 'secondaries')) REACTIONS = { - '(n,2nd)': ReactionInfo({11}, (-3, -1), ('H2',)), - '(n,2n)': ReactionInfo(set(chain([16], range(875, 892))), (-1, 0), ()), - '(n,3n)': ReactionInfo({17}, (-2, 0), ()), - '(n,na)': ReactionInfo({22}, (-4, -2), ('He4',)), - '(n,n3a)': ReactionInfo({23}, (-12, -6), ('He4', 'He4', 'He4')), - '(n,2na)': ReactionInfo({24}, (-5, -2), ('He4',)), - '(n,3na)': ReactionInfo({25}, (-6, -2), ('He4',)), - '(n,np)': ReactionInfo({28}, (-1, -1), ('H1',)), - '(n,n2a)': ReactionInfo({29}, (-8, -4), ('He4', 'He4')), - '(n,2n2a)': ReactionInfo({30}, (-9, -4), ('He4', 'He4')), - '(n,nd)': ReactionInfo({32}, (-2, -1), ('H2',)), - '(n,nt)': ReactionInfo({33}, (-3, -1), ('H3',)), - '(n,n3He)': ReactionInfo({34}, (-3, -2), ('He3',)), - '(n,nd2a)': ReactionInfo({35}, (-10, -5), ('H2', 'He4', 'He4')), - '(n,nt2a)': ReactionInfo({36}, (-11, -5), ('H3', 'He4', 'He4')), - '(n,4n)': ReactionInfo({37}, (-3, 0), ()), - '(n,2np)': ReactionInfo({41}, (-2, -1), ('H1',)), - '(n,3np)': ReactionInfo({42}, (-3, -1), ('H1',)), - '(n,n2p)': ReactionInfo({44}, (-2, -2), ('H1', 'H1')), - '(n,npa)': ReactionInfo({45}, (-5, -3), ('H1', 'He4')), - '(n,gamma)': ReactionInfo({102}, (1, 0), ()), - '(n,p)': ReactionInfo(set(chain([103], range(600, 650))), (0, -1), ('H1',)), - '(n,d)': ReactionInfo(set(chain([104], range(650, 700))), (-1, -1), ('H2',)), - '(n,t)': ReactionInfo(set(chain([105], range(700, 750))), (-2, -1), ('H3',)), - '(n,3He)': ReactionInfo(set(chain([106], range(750, 800))), (-2, -2), ('He3',)), - '(n,a)': ReactionInfo(set(chain([107], range(800, 850))), (-3, -2), ('He4',)), - '(n,2a)': ReactionInfo({108}, (-7, -4), ('He4', 'He4')), - '(n,3a)': ReactionInfo({109}, (-11, -6), ('He4', 'He4', 'He4')), - '(n,2p)': ReactionInfo({111}, (-1, -2), ('H1', 'H1')), - '(n,pa)': ReactionInfo({112}, (-4, -3), ('H1', 'He4')), - '(n,t2a)': ReactionInfo({113}, (-10, -5), ('H3', 'He4', 'He4')), - '(n,d2a)': ReactionInfo({114}, (-9, -5), ('H2', 'He4', 'He4')), - '(n,pd)': ReactionInfo({115}, (-2, -2), ('H1', 'H2')), - '(n,pt)': ReactionInfo({116}, (-3, -2), ('H1', 'H3')), - '(n,da)': ReactionInfo({117}, (-5, -3), ('H2', 'He4')), - '(n,5n)': ReactionInfo({152}, (-4, 0), ()), - '(n,6n)': ReactionInfo({153}, (-5, 0), ()), - '(n,2nt)': ReactionInfo({154}, (-4, -1), ('H3',)), - '(n,ta)': ReactionInfo({155}, (-6, -3), ('H3', 'He4')), - '(n,4np)': ReactionInfo({156}, (-4, -1), ('H1',)), - '(n,3nd)': ReactionInfo({157}, (-4, -1), ('H2',)), - '(n,nda)': ReactionInfo({158}, (-6, -3), ('H2', 'He4')), - '(n,2npa)': ReactionInfo({159}, (-6, -3), ('H1', 'He4')), - '(n,7n)': ReactionInfo({160}, (-6, 0), ()), - '(n,8n)': ReactionInfo({161}, (-7, 0), ()), - '(n,5np)': ReactionInfo({162}, (-5, -1), ('H1',)), - '(n,6np)': ReactionInfo({163}, (-6, -1), ('H1',)), - '(n,7np)': ReactionInfo({164}, (-7, -1), ('H1',)), - '(n,4na)': ReactionInfo({165}, (-7, -2), ('He4',)), - '(n,5na)': ReactionInfo({166}, (-8, -2), ('He4',)), - '(n,6na)': ReactionInfo({167}, (-9, -2), ('He4',)), - '(n,7na)': ReactionInfo({168}, (-10, -2), ('He4',)), - '(n,4nd)': ReactionInfo({169}, (-5, -1), ('H2',)), - '(n,5nd)': ReactionInfo({170}, (-6, -1), ('H2',)), - '(n,6nd)': ReactionInfo({171}, (-7, -1), ('H2',)), - '(n,3nt)': ReactionInfo({172}, (-5, -1), ('H3',)), - '(n,4nt)': ReactionInfo({173}, (-6, -1), ('H3',)), - '(n,5nt)': ReactionInfo({174}, (-7, -1), ('H3',)), - '(n,6nt)': ReactionInfo({175}, (-8, -1), ('H3',)), - '(n,2n3He)': ReactionInfo({176}, (-4, -2), ('He3',)), - '(n,3n3He)': ReactionInfo({177}, (-5, -2), ('He3',)), - '(n,4n3He)': ReactionInfo({178}, (-6, -2), ('He3',)), - '(n,3n2p)': ReactionInfo({179}, (-4, -2), ('H1', 'H1')), - '(n,3n2a)': ReactionInfo({180}, (-10, -4), ('He4', 'He4')), - '(n,3npa)': ReactionInfo({181}, (-7, -3), ('H1', 'He4')), - '(n,dt)': ReactionInfo({182}, (-4, -2), ('H2', 'H3')), - '(n,npd)': ReactionInfo({183}, (-3, -2), ('H1', 'H2')), - '(n,npt)': ReactionInfo({184}, (-4, -2), ('H1', 'H3')), - '(n,ndt)': ReactionInfo({185}, (-5, -2), ('H2', 'H3')), - '(n,np3He)': ReactionInfo({186}, (-4, -3), ('H1', 'He3')), - '(n,nd3He)': ReactionInfo({187}, (-5, -3), ('H2', 'He3')), - '(n,nt3He)': ReactionInfo({188}, (-6, -3), ('H3', 'He3')), - '(n,nta)': ReactionInfo({189}, (-7, -3), ('H3', 'He4')), - '(n,2n2p)': ReactionInfo({190}, (-3, -2), ('H1', 'H1')), - '(n,p3He)': ReactionInfo({191}, (-4, -3), ('H1', 'He3')), - '(n,d3He)': ReactionInfo({192}, (-5, -3), ('H2', 'He3')), - '(n,3Hea)': ReactionInfo({193}, (-6, -4), ('He3', 'He4')), - '(n,4n2p)': ReactionInfo({194}, (-5, -2), ('H1', 'H1')), - '(n,4n2a)': ReactionInfo({195}, (-11, -4), ('He4', 'He4')), - '(n,4npa)': ReactionInfo({196}, (-8, -3), ('H1', 'He4')), - '(n,3p)': ReactionInfo({197}, (-2, -3), ('H1', 'H1', 'H1')), - '(n,n3p)': ReactionInfo({198}, (-3, -3), ('H1', 'H1', 'H1')), - '(n,3n2pa)': ReactionInfo({199}, (-8, -4), ('H1', 'H1', 'He4')), - '(n,5n2p)': ReactionInfo({200}, (-6, -2), ('H1', 'H1')), + '(n,2nd)': ReactionInfo({11}, ('H2',)), + '(n,2n)': ReactionInfo(set(chain([16], range(875, 892))), ()), + '(n,3n)': ReactionInfo({17}, ()), + '(n,na)': ReactionInfo({22}, ('He4',)), + '(n,n3a)': ReactionInfo({23}, ('He4', 'He4', 'He4')), + '(n,2na)': ReactionInfo({24}, ('He4',)), + '(n,3na)': ReactionInfo({25}, ('He4',)), + '(n,np)': ReactionInfo({28}, ('H1',)), + '(n,n2a)': ReactionInfo({29}, ('He4', 'He4')), + '(n,2n2a)': ReactionInfo({30}, ('He4', 'He4')), + '(n,nd)': ReactionInfo({32}, ('H2',)), + '(n,nt)': ReactionInfo({33}, ('H3',)), + '(n,n3He)': ReactionInfo({34}, ('He3',)), + '(n,nd2a)': ReactionInfo({35}, ('H2', 'He4', 'He4')), + '(n,nt2a)': ReactionInfo({36}, ('H3', 'He4', 'He4')), + '(n,4n)': ReactionInfo({37}, ()), + '(n,2np)': ReactionInfo({41}, ('H1',)), + '(n,3np)': ReactionInfo({42}, ('H1',)), + '(n,n2p)': ReactionInfo({44}, ('H1', 'H1')), + '(n,npa)': ReactionInfo({45}, ('H1', 'He4')), + '(n,gamma)': ReactionInfo({102}, ()), + '(n,p)': ReactionInfo(set(chain([103], range(600, 650))), ('H1',)), + '(n,d)': ReactionInfo(set(chain([104], range(650, 700))), ('H2',)), + '(n,t)': ReactionInfo(set(chain([105], range(700, 750))), ('H3',)), + '(n,3He)': ReactionInfo(set(chain([106], range(750, 800))), ('He3',)), + '(n,a)': ReactionInfo(set(chain([107], range(800, 850))), ('He4',)), + '(n,2a)': ReactionInfo({108}, ('He4', 'He4')), + '(n,3a)': ReactionInfo({109}, ('He4', 'He4', 'He4')), + '(n,2p)': ReactionInfo({111}, ('H1', 'H1')), + '(n,pa)': ReactionInfo({112}, ('H1', 'He4')), + '(n,t2a)': ReactionInfo({113}, ('H3', 'He4', 'He4')), + '(n,d2a)': ReactionInfo({114}, ('H2', 'He4', 'He4')), + '(n,pd)': ReactionInfo({115}, ('H1', 'H2')), + '(n,pt)': ReactionInfo({116}, ('H1', 'H3')), + '(n,da)': ReactionInfo({117}, ('H2', 'He4')), + '(n,5n)': ReactionInfo({152}, ()), + '(n,6n)': ReactionInfo({153}, ()), + '(n,2nt)': ReactionInfo({154}, ('H3',)), + '(n,ta)': ReactionInfo({155}, ('H3', 'He4')), + '(n,4np)': ReactionInfo({156}, ('H1',)), + '(n,3nd)': ReactionInfo({157}, ('H2',)), + '(n,nda)': ReactionInfo({158}, ('H2', 'He4')), + '(n,2npa)': ReactionInfo({159}, ('H1', 'He4')), + '(n,7n)': ReactionInfo({160}, ()), + '(n,8n)': ReactionInfo({161}, ()), + '(n,5np)': ReactionInfo({162}, ('H1',)), + '(n,6np)': ReactionInfo({163}, ('H1',)), + '(n,7np)': ReactionInfo({164}, ('H1',)), + '(n,4na)': ReactionInfo({165}, ('He4',)), + '(n,5na)': ReactionInfo({166}, ('He4',)), + '(n,6na)': ReactionInfo({167}, ('He4',)), + '(n,7na)': ReactionInfo({168}, ('He4',)), + '(n,4nd)': ReactionInfo({169}, ('H2',)), + '(n,5nd)': ReactionInfo({170}, ('H2',)), + '(n,6nd)': ReactionInfo({171}, ('H2',)), + '(n,3nt)': ReactionInfo({172}, ('H3',)), + '(n,4nt)': ReactionInfo({173}, ('H3',)), + '(n,5nt)': ReactionInfo({174}, ('H3',)), + '(n,6nt)': ReactionInfo({175}, ('H3',)), + '(n,2n3He)': ReactionInfo({176}, ('He3',)), + '(n,3n3He)': ReactionInfo({177}, ('He3',)), + '(n,4n3He)': ReactionInfo({178}, ('He3',)), + '(n,3n2p)': ReactionInfo({179}, ('H1', 'H1')), + '(n,3n2a)': ReactionInfo({180}, ('He4', 'He4')), + '(n,3npa)': ReactionInfo({181}, ('H1', 'He4')), + '(n,dt)': ReactionInfo({182}, ('H2', 'H3')), + '(n,npd)': ReactionInfo({183}, ('H1', 'H2')), + '(n,npt)': ReactionInfo({184}, ('H1', 'H3')), + '(n,ndt)': ReactionInfo({185}, ('H2', 'H3')), + '(n,np3He)': ReactionInfo({186}, ('H1', 'He3')), + '(n,nd3He)': ReactionInfo({187}, ('H2', 'He3')), + '(n,nt3He)': ReactionInfo({188}, ('H3', 'He3')), + '(n,nta)': ReactionInfo({189}, ('H3', 'He4')), + '(n,2n2p)': ReactionInfo({190}, ('H1', 'H1')), + '(n,p3He)': ReactionInfo({191}, ('H1', 'He3')), + '(n,d3He)': ReactionInfo({192}, ('H2', 'He3')), + '(n,3Hea)': ReactionInfo({193}, ('He3', 'He4')), + '(n,4n2p)': ReactionInfo({194}, ('H1', 'H1')), + '(n,4n2a)': ReactionInfo({195}, ('He4', 'He4')), + '(n,4npa)': ReactionInfo({196}, ('H1', 'He4')), + '(n,3p)': ReactionInfo({197}, ('H1', 'H1', 'H1')), + '(n,n3p)': ReactionInfo({198}, ('H1', 'H1', 'H1')), + '(n,3n2pa)': ReactionInfo({199}, ('H1', 'H1', 'He4')), + '(n,5n2p)': ReactionInfo({200}, ('H1', 'H1')), } __all__ = ["Chain", "REACTIONS"] @@ -418,9 +412,9 @@ class Chain: if parent in reactions: reactions_available = set(reactions[parent].keys()) for name in transmutation_reactions: - mts, changes, _ = REACTIONS[name] + mts = REACTIONS[name].mts + delta_A, delta_Z = openmc.data.DADZ[name] if mts & reactions_available: - delta_A, delta_Z = changes A = data.nuclide['mass_number'] + delta_A Z = data.nuclide['atomic_number'] + delta_Z daughter = '{}{}'.format(openmc.data.ATOMIC_SYMBOL[Z], A) diff --git a/openmc/plotter.py b/openmc/plotter.py index 193ba9cbe..899c46c5c 100644 --- a/openmc/plotter.py +++ b/openmc/plotter.py @@ -58,6 +58,11 @@ ELEMENT_NAMES = list(openmc.data.ELEMENT_SYMBOL.values())[1:] def _get_legend_label(this, type): """Gets a label for the element or nuclide or material and reaction plotted""" if isinstance(this, str): + if type in openmc.data.DADZ: + z, a, m = openmc.data.zam(this) + da, dz = openmc.data.DADZ[type] + gnds_name = openmc.data.gnds_name(z + dz, a + da, m) + return f'{this} {type} {gnds_name}' return f'{this} {type}' elif this.name == '': return f'Material {this.id} {type}' From b3a2456f2b2033f8eeee6f84ce69e757022918e3 Mon Sep 17 00:00:00 2001 From: rlbarker Date: Mon, 26 Feb 2024 22:15:50 +0000 Subject: [PATCH 052/671] Added checks that tolerance value is between 0 and 1 (#2884) --- openmc/stats/univariate.py | 3 +++ tests/unit_tests/test_stats.py | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index b33f88af8..e7ff64257 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -280,6 +280,9 @@ class Discrete(Univariate): Discrete distribution with low-importance points removed """ + cv.check_less_than("tolerance", tolerance, 1.0, equality=True) + cv.check_greater_than("tolerance", tolerance, 0.0, equality=True) + # Determine (reversed) sorted order of probabilities intensity = self.p * self.x index_sort = np.argsort(intensity)[::-1] diff --git a/tests/unit_tests/test_stats.py b/tests/unit_tests/test_stats.py index 8438535ff..f5368e912 100644 --- a/tests/unit_tests/test_stats.py +++ b/tests/unit_tests/test_stats.py @@ -89,6 +89,12 @@ def test_clip_discrete(): d_same = d.clip(1e-6, inplace=True) assert d_same is d + with pytest.raises(ValueError): + d.clip(-1.) + + with pytest.raises(ValueError): + d.clip(5) + def test_uniform(): a, b = 10.0, 20.0 From a8f7a61acb498951ee22abacfc335895519959c5 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Wed, 28 Feb 2024 22:13:45 +0000 Subject: [PATCH 053/671] removed unused step_index arg from restart (#2867) --- openmc/deplete/abc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 1dea8440c..a574d4855 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -769,7 +769,7 @@ class Integrator(ABC): self.operator.write_bos_data(step_index + self._i_res) return x, res - def _get_bos_data_from_restart(self, step_index, source_rate, bos_conc): + def _get_bos_data_from_restart(self, source_rate, bos_conc): """Get beginning of step concentrations, reaction rates from restart""" res = self.operator.prev_res[-1] # Depletion methods expect list of arrays @@ -823,7 +823,7 @@ class Integrator(ABC): if i > 0 or self.operator.prev_res is None: n, res = self._get_bos_data_from_operator(i, source_rate, n) else: - n, res = self._get_bos_data_from_restart(i, source_rate, n) + n, res = self._get_bos_data_from_restart(source_rate, n) # Solve Bateman equations over time interval proc_time, n_list, res_list = self(n, res.rates, dt, source_rate, i) @@ -1030,7 +1030,7 @@ class SIIntegrator(Integrator): if self.operator.prev_res is None: n, res = self._get_bos_data_from_operator(i, p, n) else: - n, res = self._get_bos_data_from_restart(i, p, n) + n, res = self._get_bos_data_from_restart(p, n) else: # Pull rates, k from previous iteration w/o # re-running transport From 02c0a092814cec0ae721c1444e9fee09a60c497f Mon Sep 17 00:00:00 2001 From: Gavin Ridley Date: Wed, 28 Feb 2024 18:11:52 -0600 Subject: [PATCH 054/671] fix expansion filter merging (#2882) Co-authored-by: Paul Romano --- openmc/filter_expansion.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/openmc/filter_expansion.py b/openmc/filter_expansion.py index b9860846f..f8e677578 100644 --- a/openmc/filter_expansion.py +++ b/openmc/filter_expansion.py @@ -53,6 +53,34 @@ class ExpansionFilter(Filter): order = int(elem.find('order').text) return cls(order, filter_id=filter_id) + def merge(self, other): + """Merge this filter with another. + + This overrides the behavior of the parent Filter class, since its + merging technique is to take the union of the set of bins of each + filter. That technique does not apply to expansion filters, since the + argument should be the maximum filter order rather than the list of all + bins. + + Parameters + ---------- + other : openmc.Filter + Filter to merge with + + Returns + ------- + merged_filter : openmc.Filter + Filter resulting from the merge + + """ + + if not self.can_merge(other): + msg = f'Unable to merge "{type(self)}" with "{type(other)}"' + raise ValueError(msg) + + # Create a new filter with these bins and a new auto-generated ID + return type(self)(max(self.order, other.order)) + class LegendreFilter(ExpansionFilter): r"""Score Legendre expansion moments up to specified order. From b75ccd598e8442c97eed315b965736a31b6a67c8 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Thu, 29 Feb 2024 01:31:54 +0000 Subject: [PATCH 055/671] Adding energy axis units to plot xs (#2876) Co-authored-by: Paul Romano --- openmc/plotter.py | 18 +++++++++++++++--- tests/unit_tests/test_plotter.py | 11 ++++++++++- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/openmc/plotter.py b/openmc/plotter.py index 899c46c5c..849672fce 100644 --- a/openmc/plotter.py +++ b/openmc/plotter.py @@ -105,7 +105,7 @@ def _get_title(reactions): def plot_xs(reactions, divisor_types=None, temperature=294., axis=None, sab_name=None, ce_cross_sections=None, mg_cross_sections=None, enrichment=None, plot_CE=True, orders=None, divisor_orders=None, - **kwargs): + energy_axis_units="eV", **kwargs): """Creates a figure of continuous-energy cross sections for this item. Parameters @@ -148,6 +148,10 @@ def plot_xs(reactions, divisor_types=None, temperature=294., axis=None, **kwargs : All keyword arguments are passed to :func:`matplotlib.pyplot.figure`. + energy_axis_units : {'eV', 'keV', 'MeV'} + Units used on the plot energy axis + + .. versionadded:: 0.14.1 Returns ------- @@ -161,6 +165,9 @@ def plot_xs(reactions, divisor_types=None, temperature=294., axis=None, import matplotlib.pyplot as plt cv.check_type("plot_CE", plot_CE, bool) + cv.check_value("energy_axis_units", energy_axis_units, {"eV", "keV", "MeV"}) + + axis_scaling_factor = {"eV": 1.0, "keV": 1e-3, "MeV": 1e-6} # Generate the plot if axis is None: @@ -217,6 +224,8 @@ def plot_xs(reactions, divisor_types=None, temperature=294., axis=None, if divisor_types[line] != 'unity': types[line] += ' / ' + divisor_types[line] + E *= axis_scaling_factor[energy_axis_units] + # Plot the data for i in range(len(data)): data[i, :] = np.nan_to_num(data[i, :]) @@ -232,9 +241,12 @@ def plot_xs(reactions, divisor_types=None, temperature=294., axis=None, ax.set_xscale('log') ax.set_yscale('log') - ax.set_xlabel('Energy [eV]') + ax.set_xlabel(f"Energy [{energy_axis_units}]") if plot_CE: - ax.set_xlim(_MIN_E, _MAX_E) + ax.set_xlim( + _MIN_E * axis_scaling_factor[energy_axis_units], + _MAX_E * axis_scaling_factor[energy_axis_units], + ) else: ax.set_xlim(E[-1], E[0]) diff --git a/tests/unit_tests/test_plotter.py b/tests/unit_tests/test_plotter.py index 8a1147b4a..fd635d89d 100644 --- a/tests/unit_tests/test_plotter.py +++ b/tests/unit_tests/test_plotter.py @@ -77,10 +77,19 @@ def test_plot_xs(this): from matplotlib.figure import Figure assert isinstance(openmc.plot_xs({this: ['total', 'elastic']}), Figure) + def test_plot_xs_mat(test_mat): from matplotlib.figure import Figure assert isinstance(openmc.plot_xs({test_mat: ['total']}), Figure) + +@pytest.mark.parametrize("units", ["eV", "keV", "MeV"]) +def test_plot_xs_energy_axis(units): + plot = openmc.plot_xs({'Be9': ['(n,2n)']}, energy_axis_units=units) + axis_text = plot.get_axes()[0].get_xaxis().get_label().get_text() + assert axis_text == f'Energy [{units}]' + + def test_plot_axes_labels(): # just nuclides axis_label = openmc.plotter._get_yaxis_label( @@ -156,4 +165,4 @@ def test_get_title(): mat1.set_density('g/cm3', 1) mat1.name = 'my_mat' title = openmc.plotter._get_title(reactions={mat1: [205]}) - assert title == 'Cross Section Plot For my_mat' \ No newline at end of file + assert title == 'Cross Section Plot For my_mat' From aa0516f06bb9ab1bc2faaf343deefa94f9c5c8c2 Mon Sep 17 00:00:00 2001 From: Gavin Ridley Date: Tue, 5 Mar 2024 23:55:45 -0500 Subject: [PATCH 056/671] abort on cmake config if openmp requested but not found (#2893) --- CMakeLists.txt | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ec6ea6179..1280daf2b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -193,12 +193,10 @@ endif() if(NOT MSVC) if(OPENMC_USE_OPENMP) - find_package(OpenMP) - if(OPENMP_FOUND) - # In CMake 3.9+, can use the OpenMP::OpenMP_CXX imported target - list(APPEND cxxflags ${OpenMP_CXX_FLAGS}) - list(APPEND ldflags ${OpenMP_CXX_FLAGS}) - endif() + find_package(OpenMP REQUIRED) + # In CMake 3.9+, can use the OpenMP::OpenMP_CXX imported target + list(APPEND cxxflags ${OpenMP_CXX_FLAGS}) + list(APPEND ldflags ${OpenMP_CXX_FLAGS}) endif() set(CMAKE_POSITION_INDEPENDENT_CODE ON) From 7ed12788df9cc9c1e6582ab4f634a91fde88be89 Mon Sep 17 00:00:00 2001 From: Micah Gale Date: Sat, 9 Mar 2024 12:49:28 -0600 Subject: [PATCH 057/671] Clarifying documentation for Cones (#2892) --- openmc/model/surface_composite.py | 24 +++++++++++++-------- openmc/surface.py | 35 ++++++++++++++++++++++++++----- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index e4b7020d8..8f48b2f71 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -617,7 +617,7 @@ class RectangularParallelepiped(CompositeSurface): class XConeOneSided(CompositeSurface): """One-sided cone parallel the x-axis - A one-sided cone is composed of a normal cone surface and an "ambiguity" + A one-sided cone is composed of a normal cone surface and a "disambiguation" surface that eliminates the ambiguity as to which region of space is included. This class acts as a proper surface, meaning that unary `+` and `-` operators applied to it will produce a half-space. The negative side is @@ -634,7 +634,9 @@ class XConeOneSided(CompositeSurface): z0 : float, optional z-coordinate of the apex. Defaults to 0. r2 : float, optional - Parameter related to the aperature. Defaults to 1. + Parameter related to the aperture [:math:`\\rm cm^2`]. + It can be interpreted as the increase in the radius squared per cm along + the cone's axis of revolution. up : bool Whether to select the side of the cone that extends to infinity in the positive direction of the coordinate axis (the positive half-space of @@ -647,7 +649,7 @@ class XConeOneSided(CompositeSurface): cone : openmc.XCone Regular two-sided cone plane : openmc.XPlane - Ambiguity surface + Disambiguation surface up : bool Whether to select the side of the cone that extends to infinity in the positive direction of the coordinate axis (the positive half-space of @@ -675,7 +677,7 @@ class XConeOneSided(CompositeSurface): class YConeOneSided(CompositeSurface): """One-sided cone parallel the y-axis - A one-sided cone is composed of a normal cone surface and an "ambiguity" + A one-sided cone is composed of a normal cone surface and a "disambiguation" surface that eliminates the ambiguity as to which region of space is included. This class acts as a proper surface, meaning that unary `+` and `-` operators applied to it will produce a half-space. The negative side is @@ -692,7 +694,9 @@ class YConeOneSided(CompositeSurface): z0 : float, optional z-coordinate of the apex. Defaults to 0. r2 : float, optional - Parameter related to the aperature. Defaults to 1. + Parameter related to the aperture [:math:`\\rm cm^2`]. + It can be interpreted as the increase in the radius squared per cm along + the cone's axis of revolution. up : bool Whether to select the side of the cone that extends to infinity in the positive direction of the coordinate axis (the positive half-space of @@ -705,7 +709,7 @@ class YConeOneSided(CompositeSurface): cone : openmc.YCone Regular two-sided cone plane : openmc.YPlane - Ambiguity surface + Disambiguation surface up : bool Whether to select the side of the cone that extends to infinity in the positive direction of the coordinate axis (the positive half-space of @@ -727,7 +731,7 @@ class YConeOneSided(CompositeSurface): class ZConeOneSided(CompositeSurface): """One-sided cone parallel the z-axis - A one-sided cone is composed of a normal cone surface and an "ambiguity" + A one-sided cone is composed of a normal cone surface and a "disambiguation" surface that eliminates the ambiguity as to which region of space is included. This class acts as a proper surface, meaning that unary `+` and `-` operators applied to it will produce a half-space. The negative side is @@ -744,7 +748,9 @@ class ZConeOneSided(CompositeSurface): z0 : float, optional z-coordinate of the apex. Defaults to 0. r2 : float, optional - Parameter related to the aperature. Defaults to 1. + Parameter related to the aperture [:math:`\\rm cm^2`]. + It can be interpreted as the increase in the radius squared per cm along + the cone's axis of revolution. up : bool Whether to select the side of the cone that extends to infinity in the positive direction of the coordinate axis (the positive half-space of @@ -757,7 +763,7 @@ class ZConeOneSided(CompositeSurface): cone : openmc.ZCone Regular two-sided cone plane : openmc.ZPlane - Ambiguity surface + Disambiguation surface up : bool Whether to select the side of the cone that extends to infinity in the positive direction of the coordinate axis (the positive half-space of diff --git a/openmc/surface.py b/openmc/surface.py index a16b6712d..bc10f7e8a 100644 --- a/openmc/surface.py +++ b/openmc/surface.py @@ -1748,6 +1748,11 @@ class Sphere(QuadricMixin, Surface): class Cone(QuadricMixin, Surface): """A conical surface parallel to the x-, y-, or z-axis. + .. Note:: + This creates a double cone, which is two one-sided cones that meet at their apex. + For a one-sided cone see :class:`~openmc.model.XConeOneSided`, + :class:`~openmc.model.YConeOneSided`, and :class:`~openmc.model.ZConeOneSided`. + Parameters ---------- x0 : float, optional @@ -1757,7 +1762,9 @@ class Cone(QuadricMixin, Surface): z0 : float, optional z-coordinate of the apex in [cm]. Defaults to 0. r2 : float, optional - Parameter related to the aperature. Defaults to 1. + Parameter related to the aperture [:math:`\\rm cm^2`]. + It can be interpreted as the increase in the radius squared per cm along + the cone's axis of revolution. dx : float, optional x-component of the vector representing the axis of the cone. Defaults to 0. @@ -1791,7 +1798,7 @@ class Cone(QuadricMixin, Surface): z0 : float z-coordinate of the apex in [cm] r2 : float - Parameter related to the aperature + Parameter related to the aperature [cm^2] dx : float x-component of the vector representing the axis of the cone. dy : float @@ -1900,6 +1907,10 @@ class XCone(QuadricMixin, Surface): """A cone parallel to the x-axis of the form :math:`(y - y_0)^2 + (z - z_0)^2 = r^2 (x - x_0)^2`. + .. Note:: + This creates a double cone, which is two one-sided cones that meet at their apex. + For a one-sided cone see :class:`~openmc.model.XConeOneSided`. + Parameters ---------- x0 : float, optional @@ -1909,7 +1920,9 @@ class XCone(QuadricMixin, Surface): z0 : float, optional z-coordinate of the apex in [cm]. Defaults to 0. r2 : float, optional - Parameter related to the aperature. Defaults to 1. + Parameter related to the aperture [:math:`\\rm cm^2`]. + It can be interpreted as the increase in the radius squared per cm along + the cone's axis of revolution. boundary_type : {'transmission, 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles @@ -1995,6 +2008,10 @@ class YCone(QuadricMixin, Surface): """A cone parallel to the y-axis of the form :math:`(x - x_0)^2 + (z - z_0)^2 = r^2 (y - y_0)^2`. + .. Note:: + This creates a double cone, which is two one-sided cones that meet at their apex. + For a one-sided cone see :class:`~openmc.model.YConeOneSided`. + Parameters ---------- x0 : float, optional @@ -2004,7 +2021,9 @@ class YCone(QuadricMixin, Surface): z0 : float, optional z-coordinate of the apex in [cm]. Defaults to 0. r2 : float, optional - Parameter related to the aperature. Defaults to 1. + Parameter related to the aperture [:math:`\\rm cm^2`]. + It can be interpreted as the increase in the radius squared per cm along + the cone's axis of revolution. boundary_type : {'transmission, 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles @@ -2090,6 +2109,10 @@ class ZCone(QuadricMixin, Surface): """A cone parallel to the z-axis of the form :math:`(x - x_0)^2 + (y - y_0)^2 = r^2 (z - z_0)^2`. + .. Note:: + This creates a double cone, which is two one-sided cones that meet at their apex. + For a one-sided cone see :class:`~openmc.model.ZConeOneSided`. + Parameters ---------- x0 : float, optional @@ -2099,7 +2122,9 @@ class ZCone(QuadricMixin, Surface): z0 : float, optional z-coordinate of the apex in [cm]. Defaults to 0. r2 : float, optional - Parameter related to the aperature. Defaults to 1. + Parameter related to the aperature [cm^2]. + This is the square of the radius of the cone 1 cm from. + This can also be treated as the square of the slope of the cone relative to its axis. boundary_type : {'transmission, 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles From c4a75f706256d575c798f9f4046d6d3b41490695 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 11 Mar 2024 22:35:32 -0500 Subject: [PATCH 058/671] Ensure that Model.run() works when specifying a custom XML path (#2889) --- openmc/model/model.py | 10 +++++++--- tests/unit_tests/test_deplete_chain.py | 5 +++-- tests/unit_tests/test_deplete_nuclide.py | 5 +++-- tests/unit_tests/test_model.py | 6 ++++++ 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index 5194fec1d..abef9bb7a 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -488,8 +488,11 @@ class Model: # if the provided path doesn't end with the XML extension, assume the # input path is meant to be a directory. If the directory does not # exist, create it and place a 'model.xml' file there. - if not str(xml_path).endswith('.xml') and not xml_path.exists(): - os.mkdir(xml_path) + if not str(xml_path).endswith('.xml'): + if not xml_path.exists(): + os.mkdir(xml_path) + elif not xml_path.is_dir(): + raise FileExistsError(f"File exists and is not a directory: '{xml_path}'") xml_path /= 'model.xml' # if this is an XML file location and the file's parent directory does # not exist, create it before continuing @@ -709,9 +712,10 @@ class Model: self.export_to_model_xml(**export_kwargs) else: self.export_to_xml(**export_kwargs) + path_input = export_kwargs.get("path", None) openmc.run(particles, threads, geometry_debug, restart_file, tracks, output, Path('.'), openmc_exec, mpi_args, - event_based) + event_based, path_input) # Get output directory and return the last statepoint written if self.settings.output and 'path' in self.settings.output: diff --git a/tests/unit_tests/test_deplete_chain.py b/tests/unit_tests/test_deplete_chain.py index e4e023135..9753a53f7 100644 --- a/tests/unit_tests/test_deplete_chain.py +++ b/tests/unit_tests/test_deplete_chain.py @@ -5,6 +5,7 @@ from itertools import product from math import log import os from pathlib import Path +import warnings import numpy as np from openmc.mpi import comm @@ -438,9 +439,9 @@ def test_validate(simple_chain): simple_chain["C"].yield_data = {0.0253: {"A": 1.4, "B": 0.6}} assert simple_chain.validate(strict=True, tolerance=0.0) - with pytest.warns(None) as record: + with warnings.catch_warnings(): + warnings.simplefilter("error") assert simple_chain.validate(strict=False, quiet=False, tolerance=0.0) - assert len(record) == 0 # Mess up "earlier" nuclide's reactions decay_mode = simple_chain["A"].decay_modes.pop() diff --git a/tests/unit_tests/test_deplete_nuclide.py b/tests/unit_tests/test_deplete_nuclide.py index 6482d76a5..f2bb7d1b6 100644 --- a/tests/unit_tests/test_deplete_nuclide.py +++ b/tests/unit_tests/test_deplete_nuclide.py @@ -1,6 +1,7 @@ """Tests for the openmc.deplete.Nuclide class.""" import copy +import warnings import lxml.etree as ET import numpy as np @@ -277,9 +278,9 @@ def test_validate(): } # nuclide is good and should have no warnings raise - with pytest.warns(None) as record: + with warnings.catch_warnings(): + warnings.simplefilter("error") assert nuc.validate(strict=True, quiet=False, tolerance=0.0) - assert len(record) == 0 # invalidate decay modes decay = nuc.decay_modes.pop() diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 24f136c8c..16fa18d45 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -585,3 +585,9 @@ def test_single_xml_exec(run_in_tmpdir): with pytest.raises(RuntimeError, match='input_dir'): openmc.run(path_input='input_dir/pincell.xml') + + # Make sure path can be specified with run + pincell_model.run(path='my_model.xml') + + os.mkdir('subdir') + pincell_model.run(path='subdir') From 0f07420af217e2f5c6c526edbbcb45a0a6ff9394 Mon Sep 17 00:00:00 2001 From: Yue JIN <40021217+kingyue737@users.noreply.github.com> Date: Tue, 12 Mar 2024 20:31:34 +0800 Subject: [PATCH 059/671] docs: add missing max_splits in settings specification (#2910) --- docs/source/io_formats/settings.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 9b855b250..da71e4e60 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -276,6 +276,13 @@ then, OpenMC will only use up to the :math:`P_1` data. .. note:: This element is not used in the continuous-energy :ref:`energy_mode`. +--------------------------- +```` Element +--------------------------- + +The ```` element indicates the number of times a particle can split during a history. + + *Default*: 1000 -------------------------------------- ```` Element From 539f58d3b127ef2f1119d759ea2229eedb2b5c19 Mon Sep 17 00:00:00 2001 From: Micah Gale Date: Wed, 13 Mar 2024 11:08:13 -0500 Subject: [PATCH 060/671] Changed CI to use latest actions to get away from the Node 16 deprecation. (#2912) Co-authored-by: Paul Romano --- .github/workflows/ci.yml | 8 ++++---- .github/workflows/dockerhub-publish-dagmc-libmesh.yml | 8 ++++---- .github/workflows/dockerhub-publish-dagmc.yml | 8 ++++---- .github/workflows/dockerhub-publish-dev.yml | 8 ++++---- .../dockerhub-publish-develop-dagmc-libmesh.yml | 8 ++++---- .github/workflows/dockerhub-publish-develop-dagmc.yml | 8 ++++---- .../workflows/dockerhub-publish-develop-libmesh.yml | 8 ++++---- .github/workflows/dockerhub-publish-libmesh.yml | 8 ++++---- .../dockerhub-publish-release-dagmc-libmesh.yml | 10 +++++----- .github/workflows/dockerhub-publish-release-dagmc.yml | 10 +++++----- .../workflows/dockerhub-publish-release-libmesh.yml | 10 +++++----- .github/workflows/dockerhub-publish-release.yml | 10 +++++----- .github/workflows/dockerhub-publish.yml | 8 ++++---- .github/workflows/format-check.yml | 2 +- 14 files changed, 57 insertions(+), 57 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab2121b9c..99d2155fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,10 +93,10 @@ jobs: RDMAV_FORK_SAFE: 1 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -128,7 +128,7 @@ jobs: $GITHUB_WORKSPACE/tools/ci/gha-install.sh - name: cache-xs - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ~/nndc_hdf5 @@ -156,7 +156,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Coveralls Finished - uses: coverallsapp/github-action@master + uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.github_token }} parallel-finished: true diff --git a/.github/workflows/dockerhub-publish-dagmc-libmesh.yml b/.github/workflows/dockerhub-publish-dagmc-libmesh.yml index 3f0b5ab9b..813596953 100644 --- a/.github/workflows/dockerhub-publish-dagmc-libmesh.yml +++ b/.github/workflows/dockerhub-publish-dagmc-libmesh.yml @@ -10,20 +10,20 @@ jobs: steps: - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: push: true tags: openmc/openmc:latest-dagmc-libmesh diff --git a/.github/workflows/dockerhub-publish-dagmc.yml b/.github/workflows/dockerhub-publish-dagmc.yml index b6ba2f075..6757f7727 100644 --- a/.github/workflows/dockerhub-publish-dagmc.yml +++ b/.github/workflows/dockerhub-publish-dagmc.yml @@ -10,20 +10,20 @@ jobs: steps: - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: push: true tags: openmc/openmc:latest-dagmc diff --git a/.github/workflows/dockerhub-publish-dev.yml b/.github/workflows/dockerhub-publish-dev.yml index d2603ead4..7a81363a7 100644 --- a/.github/workflows/dockerhub-publish-dev.yml +++ b/.github/workflows/dockerhub-publish-dev.yml @@ -10,20 +10,20 @@ jobs: steps: - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: push: true tags: openmc/openmc:develop diff --git a/.github/workflows/dockerhub-publish-develop-dagmc-libmesh.yml b/.github/workflows/dockerhub-publish-develop-dagmc-libmesh.yml index 354f0a020..a219f2a91 100644 --- a/.github/workflows/dockerhub-publish-develop-dagmc-libmesh.yml +++ b/.github/workflows/dockerhub-publish-develop-dagmc-libmesh.yml @@ -10,20 +10,20 @@ jobs: steps: - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: push: true tags: openmc/openmc:develop-dagmc-libmesh diff --git a/.github/workflows/dockerhub-publish-develop-dagmc.yml b/.github/workflows/dockerhub-publish-develop-dagmc.yml index 36ec7a337..a901b8d3f 100644 --- a/.github/workflows/dockerhub-publish-develop-dagmc.yml +++ b/.github/workflows/dockerhub-publish-develop-dagmc.yml @@ -10,20 +10,20 @@ jobs: steps: - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: push: true tags: openmc/openmc:develop-dagmc diff --git a/.github/workflows/dockerhub-publish-develop-libmesh.yml b/.github/workflows/dockerhub-publish-develop-libmesh.yml index a89417316..22e9aa68f 100644 --- a/.github/workflows/dockerhub-publish-develop-libmesh.yml +++ b/.github/workflows/dockerhub-publish-develop-libmesh.yml @@ -10,20 +10,20 @@ jobs: steps: - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: push: true tags: openmc/openmc:develop-libmesh diff --git a/.github/workflows/dockerhub-publish-libmesh.yml b/.github/workflows/dockerhub-publish-libmesh.yml index e592ccb8e..843ce0f6f 100644 --- a/.github/workflows/dockerhub-publish-libmesh.yml +++ b/.github/workflows/dockerhub-publish-libmesh.yml @@ -10,20 +10,20 @@ jobs: steps: - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: push: true tags: openmc/openmc:latest-libmesh diff --git a/.github/workflows/dockerhub-publish-release-dagmc-libmesh.yml b/.github/workflows/dockerhub-publish-release-dagmc-libmesh.yml index c90302af4..db62bb53e 100644 --- a/.github/workflows/dockerhub-publish-release-dagmc-libmesh.yml +++ b/.github/workflows/dockerhub-publish-release-dagmc-libmesh.yml @@ -8,25 +8,25 @@ jobs: main: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set env run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: push: true tags: openmc/openmc:${{ env.RELEASE_VERSION }}-dagmc-libmesh diff --git a/.github/workflows/dockerhub-publish-release-dagmc.yml b/.github/workflows/dockerhub-publish-release-dagmc.yml index ef66f6ead..de9593782 100644 --- a/.github/workflows/dockerhub-publish-release-dagmc.yml +++ b/.github/workflows/dockerhub-publish-release-dagmc.yml @@ -8,25 +8,25 @@ jobs: main: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set env run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: push: true tags: openmc/openmc:${{ env.RELEASE_VERSION }}-dagmc diff --git a/.github/workflows/dockerhub-publish-release-libmesh.yml b/.github/workflows/dockerhub-publish-release-libmesh.yml index 72edfbc68..e8ea98aeb 100644 --- a/.github/workflows/dockerhub-publish-release-libmesh.yml +++ b/.github/workflows/dockerhub-publish-release-libmesh.yml @@ -8,25 +8,25 @@ jobs: main: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set env run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: push: true tags: openmc/openmc:${{ env.RELEASE_VERSION }}-libmesh diff --git a/.github/workflows/dockerhub-publish-release.yml b/.github/workflows/dockerhub-publish-release.yml index 22d21b4c9..fab030192 100644 --- a/.github/workflows/dockerhub-publish-release.yml +++ b/.github/workflows/dockerhub-publish-release.yml @@ -8,25 +8,25 @@ jobs: main: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set env run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: push: true tags: openmc/openmc:${{ env.RELEASE_VERSION }} diff --git a/.github/workflows/dockerhub-publish.yml b/.github/workflows/dockerhub-publish.yml index 00a3ba316..fd51a9fa7 100644 --- a/.github/workflows/dockerhub-publish.yml +++ b/.github/workflows/dockerhub-publish.yml @@ -10,20 +10,20 @@ jobs: steps: - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: push: true tags: openmc/openmc:latest diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml index 36ccece3c..cef14ca2c 100644 --- a/.github/workflows/format-check.yml +++ b/.github/workflows/format-check.yml @@ -13,7 +13,7 @@ jobs: cpp-linter: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: cpp-linter/cpp-linter-action@v2 id: linter env: From d10e128e76912f26936b4922fa1f9026084c6576 Mon Sep 17 00:00:00 2001 From: Micah Gale Date: Wed, 13 Mar 2024 18:13:02 -0500 Subject: [PATCH 061/671] Fixed small sphinx typo (#2915) --- openmc/cell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmc/cell.py b/openmc/cell.py index 0af6ea2b4..998640930 100644 --- a/openmc/cell.py +++ b/openmc/cell.py @@ -722,7 +722,7 @@ class Cell(IDManagerMixin): Dictionary mapping surface IDs to :class:`openmc.Surface` instances materials : dict Dictionary mapping material ID strings to :class:`openmc.Material` - instances (defined in :math:`openmc.Geometry.from_xml`) + instances (defined in :meth:`openmc.Geometry.from_xml`) get_universe : function Function returning universe (defined in :meth:`openmc.Geometry.from_xml`) From 231fcb651b82b02eec86ff56bb14d3abfe9b4767 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Sun, 17 Mar 2024 15:23:24 +0000 Subject: [PATCH 062/671] Mkdir to always allow parents and exist ok (#2914) --- openmc/model/model.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index abef9bb7a..2be6fcefa 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -449,7 +449,7 @@ class Model: # Create directory if required d = Path(directory) if not d.is_dir(): - d.mkdir(parents=True) + d.mkdir(parents=True, exist_ok=True) self.settings.export_to_xml(d) self.geometry.export_to_xml(d, remove_surfs=remove_surfs) @@ -490,14 +490,14 @@ class Model: # exist, create it and place a 'model.xml' file there. if not str(xml_path).endswith('.xml'): if not xml_path.exists(): - os.mkdir(xml_path) + xml_path.mkdir(parents=True, exist_ok=True) elif not xml_path.is_dir(): raise FileExistsError(f"File exists and is not a directory: '{xml_path}'") xml_path /= 'model.xml' # if this is an XML file location and the file's parent directory does # not exist, create it before continuing elif not xml_path.parent.exists(): - os.mkdir(xml_path.parent) + xml_path.parent.mkdir(parents=True, exist_ok=True) if remove_surfs: warnings.warn("remove_surfs kwarg will be deprecated soon, please " From 23f19a0310f462c81313d4e21f3cb08a3695e85f Mon Sep 17 00:00:00 2001 From: Micah Gale Date: Thu, 21 Mar 2024 14:22:35 -0500 Subject: [PATCH 063/671] Implemented contains for BoundingBox containing other BoundingBox (#2906) Co-authored-by: Jonathan Shimwell --- openmc/bounding_box.py | 20 +++++++++++--- tests/unit_tests/test_bounding_box.py | 38 ++++++++++++++++++++++++--- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/openmc/bounding_box.py b/openmc/bounding_box.py index e5655cf8c..6e58ca8ba 100644 --- a/openmc/bounding_box.py +++ b/openmc/bounding_box.py @@ -95,9 +95,23 @@ class BoundingBox: new |= other return new - def __contains__(self, point): - """Check whether or not a point is in the bounding box""" - return all(point > self.lower_left) and all(point < self.upper_right) + def __contains__(self, other): + """Check whether or not a point or another bounding box is in the bounding box. + + For another bounding box to be in the parent it must lie fully inside of it. + """ + # test for a single point + if isinstance(other, (tuple, list, np.ndarray)): + point = other + check_length("Point", point, 3, 3) + return all(point > self.lower_left) and all(point < self.upper_right) + elif isinstance(other, BoundingBox): + return all([p in self for p in [other.lower_left, other.upper_right]]) + else: + raise TypeError( + f"Unable to determine if {other} is in the bounding box." + f" Expected a tuple or a bounding box, but {type(other)} given" + ) @property def center(self) -> np.ndarray: diff --git a/tests/unit_tests/test_bounding_box.py b/tests/unit_tests/test_bounding_box.py index 89e504271..57c880092 100644 --- a/tests/unit_tests/test_bounding_box.py +++ b/tests/unit_tests/test_bounding_box.py @@ -78,9 +78,9 @@ def test_bounding_box_input_checking(): def test_bounding_box_extents(): - assert test_bb_1.extent['xy'] == (-10., 1., -20., 2.) - assert test_bb_1.extent['xz'] == (-10., 1., -30., 3.) - assert test_bb_1.extent['yz'] == (-20., 2., -30., 3.) + assert test_bb_1.extent["xy"] == (-10.0, 1.0, -20.0, 2.0) + assert test_bb_1.extent["xz"] == (-10.0, 1.0, -30.0, 3.0) + assert test_bb_1.extent["yz"] == (-20.0, 2.0, -30.0, 3.0) def test_bounding_box_methods(): @@ -156,3 +156,35 @@ def test_bounding_box_methods(): assert all(test_bb[0] == [-50.1, -50.1, -12.1]) assert all(test_bb[1] == [50.1, 14.1, 50.1]) + + +@pytest.mark.parametrize( + "bb, other, expected", + [ + (test_bb_1, (0, 0, 0), True), + (test_bb_2, (3, 3, 3), False), + # completely disjoint + (test_bb_1, test_bb_2, False), + # contained but touching border + (test_bb_1, test_bb_3, False), + # Fully contained + (test_bb_1, openmc.BoundingBox((-9, -19, -29), (0, 0, 0)), True), + # intersecting boxes + (test_bb_1, openmc.BoundingBox((-9, -19, -29), (1, 2, 5)), False), + ], +) +def test_bounding_box_contains(bb, other, expected): + assert (other in bb) == expected + + +@pytest.mark.parametrize( + "invalid, ex", + [ + ((1, 0), ValueError), + ((1, 2, 3, 4), ValueError), + ("foo", TypeError), + ], +) +def test_bounding_box_contains_checking(invalid, ex): + with pytest.raises(ex): + invalid in test_bb_1 From ce7efa415e2b8fae049266f72c714d543575de96 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Sat, 23 Mar 2024 17:29:09 -0500 Subject: [PATCH 064/671] Fix Chain.form_matrix to work with scipy 1.12 (#2922) --- openmc/deplete/chain.py | 23 ++++++++++++----------- setup.py | 2 +- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index 607168955..e15b127d0 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -597,9 +597,12 @@ class Chain: -------- :meth:`get_default_fission_yields` """ - matrix = defaultdict(float) reactions = set() + # Use DOK matrix as intermediate representation for matrix + n = len(self) + matrix = sp.dok_matrix((n, n)) + if fission_yields is None: fission_yields = self.get_default_fission_yields() @@ -674,11 +677,8 @@ class Chain: # Clear set of reactions reactions.clear() - # Use DOK matrix as intermediate representation, then convert to CSC and return - n = len(self) - matrix_dok = sp.dok_matrix((n, n)) - dict.update(matrix_dok, matrix) - return matrix_dok.tocsc() + # Return CSC representation instead of DOK + return matrix.tocsc() def form_rr_term(self, tr_rates, mats): """Function to form the transfer rate term matrices. @@ -711,7 +711,9 @@ class Chain: Sparse matrix representing transfer term. """ - matrix = defaultdict(float) + # Use DOK as intermediate representation + n = len(self) + matrix = sp.dok_matrix((n, n)) for i, nuc in enumerate(self.nuclides): elm = re.split(r'\d+', nuc.name)[0] @@ -737,10 +739,9 @@ class Chain: else: matrix[i, i] = 0.0 #Nothing else is allowed - n = len(self) - matrix_dok = sp.dok_matrix((n, n)) - dict.update(matrix_dok, matrix) - return matrix_dok.tocsc() + + # Return CSC instead of DOK + return matrix.tocsc() def get_branch_ratios(self, reaction="(n,gamma)"): """Return a dictionary with reaction branching ratios diff --git a/setup.py b/setup.py index 84a2ad367..eeb0b3f29 100755 --- a/setup.py +++ b/setup.py @@ -64,7 +64,7 @@ kwargs = { # Dependencies 'python_requires': '>=3.7', 'install_requires': [ - 'numpy>=1.9', 'h5py', 'scipy<1.12', 'ipython', 'matplotlib', + 'numpy>=1.9', 'h5py', 'scipy', 'ipython', 'matplotlib', 'pandas', 'lxml', 'uncertainties' ], 'extras_require': { From 9fee6534b6e873d24a4df7fe42d0bf5a988e207a Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 26 Mar 2024 11:00:06 -0500 Subject: [PATCH 065/671] Prepare for NumPy 2.0 (#2845) --- openmc/data/correlated.py | 2 +- openmc/data/energy_distribution.py | 14 +++---- openmc/data/function.py | 6 +-- openmc/data/kalbach_mann.py | 2 +- openmc/data/multipole.py | 2 +- openmc/data/nbody.py | 2 +- openmc/data/neutron.py | 2 +- openmc/data/photon.py | 2 +- openmc/data/product.py | 4 +- openmc/data/reaction.py | 4 +- openmc/data/resonance_covariance.py | 7 ++-- openmc/data/thermal.py | 6 +-- openmc/data/thermal_angle_energy.py | 10 ++--- openmc/data/uncorrelated.py | 2 +- openmc/deplete/stepresult.py | 2 +- openmc/mgxs_library.py | 8 ++-- openmc/plots.py | 4 +- openmc/source.py | 2 +- openmc/stats/univariate.py | 44 +++++++++++---------- tests/unit_tests/test_deplete_integrator.py | 16 ++++---- tests/unit_tests/test_source_file.py | 2 +- tests/unit_tests/test_stats.py | 3 +- 22 files changed, 74 insertions(+), 72 deletions(-) diff --git a/openmc/data/correlated.py b/openmc/data/correlated.py index f131ce30d..2ff095a5c 100644 --- a/openmc/data/correlated.py +++ b/openmc/data/correlated.py @@ -113,7 +113,7 @@ class CorrelatedAngleEnergy(AngleEnergy): HDF5 group to write to """ - group.attrs['type'] = np.string_(self._name) + group.attrs['type'] = np.bytes_(self._name) dset = group.create_dataset('energy', data=self.energy) dset.attrs['interpolation'] = np.vstack((self.breakpoints, diff --git a/openmc/data/energy_distribution.py b/openmc/data/energy_distribution.py index a13893a68..b3566e999 100644 --- a/openmc/data/energy_distribution.py +++ b/openmc/data/energy_distribution.py @@ -277,7 +277,7 @@ class MaxwellEnergy(EnergyDistribution): """ - group.attrs['type'] = np.string_('maxwell') + group.attrs['type'] = np.bytes_('maxwell') group.attrs['u'] = self.u self.theta.to_hdf5(group, 'theta') @@ -410,7 +410,7 @@ class Evaporation(EnergyDistribution): """ - group.attrs['type'] = np.string_('evaporation') + group.attrs['type'] = np.bytes_('evaporation') group.attrs['u'] = self.u self.theta.to_hdf5(group, 'theta') @@ -556,7 +556,7 @@ class WattEnergy(EnergyDistribution): """ - group.attrs['type'] = np.string_('watt') + group.attrs['type'] = np.bytes_('watt') group.attrs['u'] = self.u self.a.to_hdf5(group, 'a') self.b.to_hdf5(group, 'b') @@ -728,7 +728,7 @@ class MadlandNix(EnergyDistribution): """ - group.attrs['type'] = np.string_('madland-nix') + group.attrs['type'] = np.bytes_('madland-nix') group.attrs['efl'] = self.efl group.attrs['efh'] = self.efh self.tm.to_hdf5(group) @@ -846,7 +846,7 @@ class DiscretePhoton(EnergyDistribution): """ - group.attrs['type'] = np.string_('discrete_photon') + group.attrs['type'] = np.bytes_('discrete_photon') group.attrs['primary_flag'] = self.primary_flag group.attrs['energy'] = self.energy group.attrs['atomic_weight_ratio'] = self.atomic_weight_ratio @@ -945,7 +945,7 @@ class LevelInelastic(EnergyDistribution): """ - group.attrs['type'] = np.string_('level') + group.attrs['type'] = np.bytes_('level') group.attrs['threshold'] = self.threshold group.attrs['mass_ratio'] = self.mass_ratio @@ -1074,7 +1074,7 @@ class ContinuousTabular(EnergyDistribution): """ - group.attrs['type'] = np.string_('continuous') + group.attrs['type'] = np.bytes_('continuous') dset = group.create_dataset('energy', data=self.energy) dset.attrs['interpolation'] = np.vstack((self.breakpoints, diff --git a/openmc/data/function.py b/openmc/data/function.py index 299924b37..23fd5e9d4 100644 --- a/openmc/data/function.py +++ b/openmc/data/function.py @@ -364,7 +364,7 @@ class Tabulated1D(Function1D): """ dataset = group.create_dataset(name, data=np.vstack( [self.x, self.y])) - dataset.attrs['type'] = np.string_(type(self).__name__) + dataset.attrs['type'] = np.bytes_(type(self).__name__) dataset.attrs['breakpoints'] = self.breakpoints dataset.attrs['interpolation'] = self.interpolation @@ -460,7 +460,7 @@ class Polynomial(np.polynomial.Polynomial, Function1D): """ dataset = group.create_dataset(name, data=self.coef) - dataset.attrs['type'] = np.string_(type(self).__name__) + dataset.attrs['type'] = np.bytes_(type(self).__name__) @classmethod def from_hdf5(cls, dataset): @@ -592,7 +592,7 @@ class Sum(Function1D): """ sum_group = group.create_group(name) - sum_group.attrs['type'] = np.string_(type(self).__name__) + sum_group.attrs['type'] = np.bytes_(type(self).__name__) sum_group.attrs['n'] = len(self.functions) for i, f in enumerate(self.functions): f.to_hdf5(sum_group, f'func_{i+1}') diff --git a/openmc/data/kalbach_mann.py b/openmc/data/kalbach_mann.py index b49399139..d92bf9c21 100644 --- a/openmc/data/kalbach_mann.py +++ b/openmc/data/kalbach_mann.py @@ -366,7 +366,7 @@ class KalbachMann(AngleEnergy): HDF5 group to write to """ - group.attrs['type'] = np.string_('kalbach-mann') + group.attrs['type'] = np.bytes_('kalbach-mann') dset = group.create_dataset('energy', data=self.energy) dset.attrs['interpolation'] = np.vstack((self.breakpoints, diff --git a/openmc/data/multipole.py b/openmc/data/multipole.py index 9fd6c9a9b..d45d2beb6 100644 --- a/openmc/data/multipole.py +++ b/openmc/data/multipole.py @@ -1273,7 +1273,7 @@ class WindowedMultipole(EqualityMixin): # Open file and write version. with h5py.File(str(path), mode, libver=libver) as f: - f.attrs['filetype'] = np.string_('data_wmp') + f.attrs['filetype'] = np.bytes_('data_wmp') f.attrs['version'] = np.array(WMP_VERSION) g = f.create_group(self.name) diff --git a/openmc/data/nbody.py b/openmc/data/nbody.py index 1f2ff5b4d..ec1ac25c0 100644 --- a/openmc/data/nbody.py +++ b/openmc/data/nbody.py @@ -91,7 +91,7 @@ class NBodyPhaseSpace(AngleEnergy): HDF5 group to write to """ - group.attrs['type'] = np.string_('nbody') + group.attrs['type'] = np.bytes_('nbody') group.attrs['total_mass'] = self.total_mass group.attrs['n_particles'] = self.n_particles group.attrs['atomic_weight_ratio'] = self.atomic_weight_ratio diff --git a/openmc/data/neutron.py b/openmc/data/neutron.py index e0c0032f0..94833d4b3 100644 --- a/openmc/data/neutron.py +++ b/openmc/data/neutron.py @@ -425,7 +425,7 @@ class IncidentNeutron(EqualityMixin): # Open file and write version with h5py.File(str(path), mode, libver=libver) as f: - f.attrs['filetype'] = np.string_('data_neutron') + f.attrs['filetype'] = np.bytes_('data_neutron') f.attrs['version'] = np.array(HDF5_VERSION) # Write basic data diff --git a/openmc/data/photon.py b/openmc/data/photon.py index 7c96e9fac..ba4bf5c88 100644 --- a/openmc/data/photon.py +++ b/openmc/data/photon.py @@ -764,7 +764,7 @@ class IncidentPhoton(EqualityMixin): """ with h5py.File(str(path), mode, libver=libver) as f: # Write filetype and version - f.attrs['filetype'] = np.string_('data_photon') + f.attrs['filetype'] = np.bytes_('data_photon') if 'version' not in f.attrs: f.attrs['version'] = np.array(HDF5_VERSION) diff --git a/openmc/data/product.py b/openmc/data/product.py index 93697a9e7..4a9b07aee 100644 --- a/openmc/data/product.py +++ b/openmc/data/product.py @@ -124,8 +124,8 @@ class Product(EqualityMixin): HDF5 group to write to """ - group.attrs['particle'] = np.string_(self.particle) - group.attrs['emission_mode'] = np.string_(self.emission_mode) + group.attrs['particle'] = np.bytes_(self.particle) + group.attrs['emission_mode'] = np.bytes_(self.emission_mode) if self.decay_rate > 0.0: group.attrs['decay_rate'] = self.decay_rate diff --git a/openmc/data/reaction.py b/openmc/data/reaction.py index 04a68d739..8ba478782 100644 --- a/openmc/data/reaction.py +++ b/openmc/data/reaction.py @@ -920,9 +920,9 @@ class Reaction(EqualityMixin): group.attrs['mt'] = self.mt if self.mt in REACTION_NAME: - group.attrs['label'] = np.string_(REACTION_NAME[self.mt]) + group.attrs['label'] = np.bytes_(REACTION_NAME[self.mt]) else: - group.attrs['label'] = np.string_(self.mt) + group.attrs['label'] = np.bytes_(self.mt) group.attrs['Q_value'] = self.q_value group.attrs['center_of_mass'] = 1 if self.center_of_mass else 0 group.attrs['redundant'] = 1 if self.redundant else 0 diff --git a/openmc/data/resonance_covariance.py b/openmc/data/resonance_covariance.py index 9ba429cb4..709657044 100644 --- a/openmc/data/resonance_covariance.py +++ b/openmc/data/resonance_covariance.py @@ -239,14 +239,14 @@ class ResonanceCovarianceRange: samples = [] # Handling MLBW/SLBW sampling + rng = np.random.default_rng() if formalism == 'mlbw' or formalism == 'slbw': params = ['energy', 'neutronWidth', 'captureWidth', 'fissionWidth', 'competitiveWidth'] param_list = params[:mpar] mean_array = parameters[param_list].values mean = mean_array.flatten() - par_samples = np.random.multivariate_normal(mean, cov, - size=n_samples) + par_samples = rng.multivariate_normal(mean, cov, size=n_samples) spin = parameters['J'].values l_value = parameters['L'].values for sample in par_samples: @@ -277,8 +277,7 @@ class ResonanceCovarianceRange: param_list = params[:mpar] mean_array = parameters[param_list].values mean = mean_array.flatten() - par_samples = np.random.multivariate_normal(mean, cov, - size=n_samples) + par_samples = rng.multivariate_normal(mean, cov, size=n_samples) spin = parameters['J'].values l_value = parameters['L'].values for sample in par_samples: diff --git a/openmc/data/thermal.py b/openmc/data/thermal.py index 994a8ed2a..f8dd43ed9 100644 --- a/openmc/data/thermal.py +++ b/openmc/data/thermal.py @@ -221,7 +221,7 @@ class CoherentElastic(Function1D): """ dataset = group.create_dataset(name, data=np.vstack( [self.bragg_edges, self.factors])) - dataset.attrs['type'] = np.string_(type(self).__name__) + dataset.attrs['type'] = np.bytes_(type(self).__name__) @classmethod def from_hdf5(cls, dataset): @@ -294,7 +294,7 @@ class IncoherentElastic(Function1D): """ data = np.array([self.bound_xs, self.debye_waller]) dataset = group.create_dataset(name, data=data) - dataset.attrs['type'] = np.string_(type(self).__name__) + dataset.attrs['type'] = np.bytes_(type(self).__name__) @classmethod def from_hdf5(cls, dataset): @@ -464,7 +464,7 @@ class ThermalScattering(EqualityMixin): """ # Open file and write version with h5py.File(str(path), mode, libver=libver) as f: - f.attrs['filetype'] = np.string_('data_thermal') + f.attrs['filetype'] = np.bytes_('data_thermal') f.attrs['version'] = np.array(HDF5_VERSION) # Write basic data diff --git a/openmc/data/thermal_angle_energy.py b/openmc/data/thermal_angle_energy.py index 17a560092..0dd2a0b7a 100644 --- a/openmc/data/thermal_angle_energy.py +++ b/openmc/data/thermal_angle_energy.py @@ -43,7 +43,7 @@ class CoherentElasticAE(AngleEnergy): HDF5 group to write to """ - group.attrs['type'] = np.string_('coherent_elastic') + group.attrs['type'] = np.bytes_('coherent_elastic') self.coherent_xs.to_hdf5(group, 'coherent_xs') @classmethod @@ -104,7 +104,7 @@ class IncoherentElasticAE(AngleEnergy): HDF5 group to write to """ - group.attrs['type'] = np.string_('incoherent_elastic') + group.attrs['type'] = np.bytes_('incoherent_elastic') group.create_dataset('debye_waller', data=self.debye_waller) @classmethod @@ -146,7 +146,7 @@ class IncoherentElasticAEDiscrete(AngleEnergy): HDF5 group to write to """ - group.attrs['type'] = np.string_('incoherent_elastic_discrete') + group.attrs['type'] = np.bytes_('incoherent_elastic_discrete') group.create_dataset('mu_out', data=self.mu_out) @classmethod @@ -203,7 +203,7 @@ class IncoherentInelasticAEDiscrete(AngleEnergy): HDF5 group to write to """ - group.attrs['type'] = np.string_('incoherent_inelastic_discrete') + group.attrs['type'] = np.bytes_('incoherent_inelastic_discrete') group.create_dataset('energy_out', data=self.energy_out) group.create_dataset('mu_out', data=self.mu_out) group.create_dataset('skewed', data=self.skewed) @@ -266,7 +266,7 @@ class MixedElasticAE(AngleEnergy): HDF5 group to write to """ - group.attrs['type'] = np.string_('mixed_elastic') + group.attrs['type'] = np.bytes_('mixed_elastic') coherent_group = group.create_group('coherent') self.coherent.to_hdf5(coherent_group) incoherent_group = group.create_group('incoherent') diff --git a/openmc/data/uncorrelated.py b/openmc/data/uncorrelated.py index 9dcb18bf6..141007b70 100644 --- a/openmc/data/uncorrelated.py +++ b/openmc/data/uncorrelated.py @@ -63,7 +63,7 @@ class UncorrelatedAngleEnergy(AngleEnergy): HDF5 group to write to """ - group.attrs['type'] = np.string_('uncorrelated') + group.attrs['type'] = np.bytes_('uncorrelated') if self.angle is not None: angle_group = group.create_group('angle') self.angle.to_hdf5(angle_group) diff --git a/openmc/deplete/stepresult.py b/openmc/deplete/stepresult.py index 372f7cf25..9cf33898f 100644 --- a/openmc/deplete/stepresult.py +++ b/openmc/deplete/stepresult.py @@ -291,7 +291,7 @@ class StepResult: # Store concentration mat and nuclide dictionaries (along with volumes) handle.attrs['version'] = np.array(VERSION_RESULTS) - handle.attrs['filetype'] = np.string_('depletion results') + handle.attrs['filetype'] = np.bytes_('depletion results') mat_list = sorted(self.mat_to_hdf5_ind, key=int) nuc_list = sorted(self.index_nuc) diff --git a/openmc/mgxs_library.py b/openmc/mgxs_library.py index 85e15f8eb..642da83d1 100644 --- a/openmc/mgxs_library.py +++ b/openmc/mgxs_library.py @@ -1964,7 +1964,7 @@ class XSdata: grp.attrs['fissionable'] = self.fissionable if self.representation is not None: - grp.attrs['representation'] = np.string_(self.representation) + grp.attrs['representation'] = np.bytes_(self.representation) if self.representation == REPRESENTATION_ANGLE: if self.num_azimuthal is not None: grp.attrs['num_azimuthal'] = self.num_azimuthal @@ -1972,9 +1972,9 @@ class XSdata: if self.num_polar is not None: grp.attrs['num_polar'] = self.num_polar - grp.attrs['scatter_shape'] = np.string_("[G][G'][Order]") + grp.attrs['scatter_shape'] = np.bytes_("[G][G'][Order]") if self.scatter_format is not None: - grp.attrs['scatter_format'] = np.string_(self.scatter_format) + grp.attrs['scatter_format'] = np.bytes_(self.scatter_format) if self.order is not None: grp.attrs['order'] = self.order @@ -2516,7 +2516,7 @@ class MGXSLibrary: # Create and write to the HDF5 file file = h5py.File(filename, "w", libver=libver) - file.attrs['filetype'] = np.string_(_FILETYPE_MGXS_LIBRARY) + file.attrs['filetype'] = np.bytes_(_FILETYPE_MGXS_LIBRARY) file.attrs['version'] = [_VERSION_MGXS_LIBRARY, 0] file.attrs['energy_groups'] = self.energy_groups.num_groups file.attrs['delayed_groups'] = self.num_delayed_groups diff --git a/openmc/plots.py b/openmc/plots.py index c73e3cdf7..5552b18a8 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -465,11 +465,11 @@ class PlotBase(IDManagerMixin): domains = geometry.get_all_cells().values() # Set the seed for the random number generator - np.random.seed(seed) + rng = np.random.RandomState(seed) # Generate random colors for each feature for domain in domains: - self.colors[domain] = np.random.randint(0, 256, (3,)) + self.colors[domain] = rng.randint(0, 256, (3,)) def to_xml_element(self): """Save common plot attributes to XML element diff --git a/openmc/source.py b/openmc/source.py index 441bf4e76..5942a3107 100644 --- a/openmc/source.py +++ b/openmc/source.py @@ -883,7 +883,7 @@ def write_source_file( # Write array to file kwargs.setdefault('mode', 'w') with h5py.File(filename, **kwargs) as fh: - fh.attrs['filetype'] = np.string_("source") + fh.attrs['filetype'] = np.bytes_("source") fh.create_dataset('source_bank', data=arr, dtype=source_dtype) diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index e7ff64257..3d5f647ac 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -10,6 +10,7 @@ from warnings import warn import lxml.etree as ET import numpy as np +from scipy.integrate import trapezoid import openmc.checkvalue as cv from .._xml import get_text @@ -155,9 +156,9 @@ class Discrete(Univariate): return np.insert(np.cumsum(self.p), 0, 0.0) def sample(self, n_samples=1, seed=None): - np.random.seed(seed) + rng = np.random.RandomState(seed) p = self.p / self.p.sum() - return np.random.choice(self.x, n_samples, p=p) + return rng.choice(self.x, n_samples, p=p) def normalize(self): """Normalize the probabilities stored on the distribution""" @@ -363,8 +364,8 @@ class Uniform(Univariate): return t def sample(self, n_samples=1, seed=None): - np.random.seed(seed) - return np.random.uniform(self.a, self.b, n_samples) + rng = np.random.RandomState(seed) + return rng.uniform(self.a, self.b, n_samples) def to_xml_element(self, element_name: str): """Return XML representation of the uniform distribution @@ -468,8 +469,8 @@ class PowerLaw(Univariate): self._n = n def sample(self, n_samples=1, seed=None): - np.random.seed(seed) - xi = np.random.rand(n_samples) + rng = np.random.RandomState(seed) + xi = rng.random(n_samples) pwr = self.n + 1 offset = self.a**pwr span = self.b**pwr - offset @@ -549,12 +550,14 @@ class Maxwell(Univariate): self._theta = theta def sample(self, n_samples=1, seed=None): - np.random.seed(seed) - return self.sample_maxwell(self.theta, n_samples) + rng = np.random.RandomState(seed) + return self.sample_maxwell(self.theta, n_samples, rng=rng) @staticmethod - def sample_maxwell(t, n_samples: int): - r1, r2, r3 = np.random.rand(3, n_samples) + def sample_maxwell(t, n_samples: int, rng=None): + if rng is None: + rng = np.random.default_rng() + r1, r2, r3 = rng.random((3, n_samples)) c = np.cos(0.5 * np.pi * r3) return -t * (np.log(r1) + np.log(r2) * c * c) @@ -647,9 +650,9 @@ class Watt(Univariate): self._b = b def sample(self, n_samples=1, seed=None): - np.random.seed(seed) - w = Maxwell.sample_maxwell(self.a, n_samples) - u = np.random.uniform(-1., 1., n_samples) + rng = np.random.RandomState(seed) + w = Maxwell.sample_maxwell(self.a, n_samples, rng=rng) + u = rng.uniform(-1., 1., n_samples) aab = self.a * self.a * self.b return w + 0.25*aab + u*np.sqrt(aab*w) @@ -740,8 +743,8 @@ class Normal(Univariate): self._std_dev = std_dev def sample(self, n_samples=1, seed=None): - np.random.seed(seed) - return np.random.normal(self.mean_value, self.std_dev, n_samples) + rng = np.random.RandomState(seed) + return rng.normal(self.mean_value, self.std_dev, n_samples) def to_xml_element(self, element_name: str): """Return XML representation of the Normal distribution @@ -952,8 +955,8 @@ class Tabular(Univariate): self.p /= self.cdf().max() def sample(self, n_samples: int = 1, seed: typing.Optional[int] = None): - np.random.seed(seed) - xi = np.random.rand(n_samples) + rng = np.random.RandomState(seed) + xi = rng.random(n_samples) # always use normalized probabilities when sampling cdf = self.cdf() @@ -1069,7 +1072,7 @@ class Tabular(Univariate): if self.interpolation == 'histogram': return np.sum(np.diff(self.x) * self.p[:-1]) elif self.interpolation == 'linear-linear': - return np.trapz(self.p, self.x) + return trapezoid(self.p, self.x) else: raise NotImplementedError( f'integral() not supported for {self.inteprolation} interpolation') @@ -1185,7 +1188,7 @@ class Mixture(Univariate): return np.insert(np.cumsum(self.probability), 0, 0.0) def sample(self, n_samples=1, seed=None): - np.random.seed(seed) + rng = np.random.RandomState(seed) # Get probability of each distribution accounting for its intensity p = np.array([prob*dist.integral() for prob, dist in @@ -1193,8 +1196,7 @@ class Mixture(Univariate): p /= p.sum() # Sample from the distributions - idx = np.random.choice(range(len(self.distribution)), - n_samples, p=p) + idx = rng.choice(range(len(self.distribution)), n_samples, p=p) # Draw samples from the distributions sampled above out = np.empty_like(idx, dtype=float) diff --git a/tests/unit_tests/test_deplete_integrator.py b/tests/unit_tests/test_deplete_integrator.py index ba625a999..b1d2cb950 100644 --- a/tests/unit_tests/test_deplete_integrator.py +++ b/tests/unit_tests/test_deplete_integrator.py @@ -40,7 +40,7 @@ def test_results_save(run_in_tmpdir): stages = 3 - np.random.seed(comm.rank) + rng = np.random.RandomState(comm.rank) # Mock geometry op = MagicMock() @@ -68,26 +68,26 @@ def test_results_save(run_in_tmpdir): x2 = [] for i in range(stages): - x1.append([np.random.rand(2), np.random.rand(2)]) - x2.append([np.random.rand(2), np.random.rand(2)]) + x1.append([rng.random(2), rng.random(2)]) + x2.append([rng.random(2), rng.random(2)]) # Construct r r1 = ReactionRates(burn_list, ["na", "nb"], ["ra", "rb"]) - r1[:] = np.random.rand(2, 2, 2) + r1[:] = rng.random((2, 2, 2)) rate1 = [] rate2 = [] for i in range(stages): rate1.append(copy.deepcopy(r1)) - r1[:] = np.random.rand(2, 2, 2) + r1[:] = rng.random((2, 2, 2)) rate2.append(copy.deepcopy(r1)) - r1[:] = np.random.rand(2, 2, 2) + r1[:] = rng.random((2, 2, 2)) # Create global terms # Col 0: eig, Col 1: uncertainty - eigvl1 = np.random.rand(stages, 2) - eigvl2 = np.random.rand(stages, 2) + eigvl1 = rng.random((stages, 2)) + eigvl2 = rng.random((stages, 2)) eigvl1 = comm.bcast(eigvl1, root=0) eigvl2 = comm.bcast(eigvl2, root=0) diff --git a/tests/unit_tests/test_source_file.py b/tests/unit_tests/test_source_file.py index d9fb3c1f9..1b5549b00 100644 --- a/tests/unit_tests/test_source_file.py +++ b/tests/unit_tests/test_source_file.py @@ -78,7 +78,7 @@ def test_wrong_source_attributes(run_in_tmpdir): ]) arr = np.array([(1.0, 2.0, 3), (4.0, 5.0, 6), (7.0, 8.0, 9)], dtype=source_dtype) with h5py.File('animal_source.h5', 'w') as fh: - fh.attrs['filetype'] = np.string_("source") + fh.attrs['filetype'] = np.bytes_("source") fh.create_dataset('source_bank', data=arr) # Create a simple model that uses this lovely animal source diff --git a/tests/unit_tests/test_stats.py b/tests/unit_tests/test_stats.py index f5368e912..761f26ab3 100644 --- a/tests/unit_tests/test_stats.py +++ b/tests/unit_tests/test_stats.py @@ -4,6 +4,7 @@ import numpy as np import pytest import openmc import openmc.stats +from scipy.integrate import trapezoid def assert_sample_mean(samples, expected_mean): @@ -226,7 +227,7 @@ def test_legendre(): # Integrating distribution should yield one mu = np.linspace(-1., 1., 1000) - assert np.trapz(d(mu), mu) == pytest.approx(1.0, rel=1e-4) + assert trapezoid(d(mu), mu) == pytest.approx(1.0, rel=1e-4) with pytest.raises(NotImplementedError): d.to_xml_element('distribution') From 1a34ddf121cf3ec4929ba7cef160c96a3896dbd9 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 4 Apr 2024 02:54:13 +0200 Subject: [PATCH 066/671] Fix CMFD to work with scipy 1.13 (#2936) --- openmc/cmfd.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/openmc/cmfd.py b/openmc/cmfd.py index 1b4bfa5e2..9b0b4ca83 100644 --- a/openmc/cmfd.py +++ b/openmc/cmfd.py @@ -982,14 +982,16 @@ class CMFDRun: temp_data = np.ones(len(loss_row)) temp_loss = sparse.csr_matrix((temp_data, (loss_row, loss_col)), shape=(n, n)) + temp_loss.sort_indices() # Pass coremap as 1-d array of 32-bit integers coremap = np.swapaxes(self._coremap, 0, 2).flatten().astype(np.int32) - args = temp_loss.indptr, len(temp_loss.indptr), \ - temp_loss.indices, len(temp_loss.indices), n, \ + return openmc.lib._dll.openmc_initialize_linsolver( + temp_loss.indptr.astype(np.int32), len(temp_loss.indptr), + temp_loss.indices.astype(np.int32), len(temp_loss.indices), n, self._spectral, coremap, self._use_all_threads - return openmc.lib._dll.openmc_initialize_linsolver(*args) + ) def _write_cmfd_output(self): """Write CMFD output to buffer at the end of each batch""" @@ -1585,6 +1587,7 @@ class CMFDRun: loss_row = self._loss_row loss_col = self._loss_col loss = sparse.csr_matrix((data, (loss_row, loss_col)), shape=(n, n)) + loss.sort_indices() return loss def _build_prod_matrix(self, adjoint): @@ -1611,6 +1614,7 @@ class CMFDRun: prod_row = self._prod_row prod_col = self._prod_col prod = sparse.csr_matrix((data, (prod_row, prod_col)), shape=(n, n)) + prod.sort_indices() return prod def _execute_power_iter(self, loss, prod): @@ -2307,8 +2311,8 @@ class CMFDRun: constant_values=_CMFD_NOACCEL)[:,:,1:] # Create empty row and column vectors to store for loss matrix - row = np.array([]) - col = np.array([]) + row = np.array([], dtype=int) + col = np.array([], dtype=int) # Store all indices used to populate production and loss matrix is_accel = self._coremap != _CMFD_NOACCEL From cc848effe77c90decdcb453679f0f5ee18e872f7 Mon Sep 17 00:00:00 2001 From: Joffrey Dorville <54550047+JoffreyDorville@users.noreply.github.com> Date: Thu, 4 Apr 2024 16:32:21 -0500 Subject: [PATCH 067/671] Meshborn filter (#2925) Co-authored-by: Paul Romano --- CMakeLists.txt | 1 + docs/source/capi/index.rst | 102 ++++++++++++ docs/source/pythonapi/base.rst | 1 + docs/source/pythonapi/capi.rst | 1 + include/openmc/particle_data.h | 5 + include/openmc/tallies/filter.h | 1 + include/openmc/tallies/filter_meshborn.h | 26 +++ openmc/filter.py | 28 ++++ openmc/lib/filter.py | 151 ++++++++++++++++-- src/particle.cpp | 1 + src/tallies/filter.cpp | 3 + src/tallies/filter_mesh.cpp | 2 + src/tallies/filter_meshborn.cpp | 61 +++++++ src/tallies/filter_meshsurface.cpp | 12 ++ src/tallies/tally.cpp | 1 + .../filter_meshborn/__init__.py | 0 .../filter_meshborn/inputs_true.dat | 51 ++++++ .../filter_meshborn/results_true.dat | 54 +++++++ .../regression_tests/filter_meshborn/test.py | 107 +++++++++++++ tests/unit_tests/test_filter_meshborn.py | 116 ++++++++++++++ tests/unit_tests/test_tallies.py | 7 +- 21 files changed, 720 insertions(+), 11 deletions(-) create mode 100644 include/openmc/tallies/filter_meshborn.h create mode 100644 src/tallies/filter_meshborn.cpp create mode 100644 tests/regression_tests/filter_meshborn/__init__.py create mode 100644 tests/regression_tests/filter_meshborn/inputs_true.dat create mode 100644 tests/regression_tests/filter_meshborn/results_true.dat create mode 100644 tests/regression_tests/filter_meshborn/test.py create mode 100644 tests/unit_tests/test_filter_meshborn.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 1280daf2b..6922e6b89 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -411,6 +411,7 @@ list(APPEND libopenmc_SOURCES src/tallies/filter_material.cpp src/tallies/filter_materialfrom.cpp src/tallies/filter_mesh.cpp + src/tallies/filter_meshborn.cpp src/tallies/filter_meshsurface.cpp src/tallies/filter_mu.cpp src/tallies/filter_particle.cpp diff --git a/docs/source/capi/index.rst b/docs/source/capi/index.rst index 52c0b2406..d9ac0d1e0 100644 --- a/docs/source/capi/index.rst +++ b/docs/source/capi/index.rst @@ -420,6 +420,16 @@ Functions :return: Return status (negative if an error occurred) :rtype: int +.. c:function:: int openmc_mesh_filter_get_mesh(int32_t index, int32_t* index_mesh) + + Get the mesh for a mesh filter + + :param int32_t index: Index in the filters array + :param index_mesh: Index in the meshes array + :type index_mesh: int32_t* + :return: Return status (negative if an error occurred) + :rtype: int + .. c:function:: int openmc_mesh_filter_set_mesh(int32_t index, int32_t index_mesh) Set the mesh for a mesh filter @@ -429,6 +439,98 @@ Functions :return: Return status (negative if an error occurred) :rtype: int +.. c:function:: int openmc_mesh_filter_get_translation(int32_t index, double translation[3]) + + Get the 3-D translation coordinates for a mesh filter + + :param int32_t index: Index in the filters array + :param double[3] translation: 3-D translation coordinates + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_mesh_filter_set_translation(int32_t index, double translation[3]) + + Set the 3-D translation coordinates for a mesh filter + + :param int32_t index: Index in the filters array + :param double[3] translation: 3-D translation coordinates + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_meshborn_filter_get_mesh(int32_t index, int32_t* index_mesh) + + Get the mesh for a meshborn filter + + :param int32_t index: Index in the filters array + :param index_mesh: Index in the meshes array + :type index_mesh: int32_t* + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_meshborn_filter_set_mesh(int32_t index, int32_t index_mesh) + + Set the mesh for a meshborn filter + + :param int32_t index: Index in the filters array + :param int32_t index_mesh: Index in the meshes array + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_meshborn_filter_get_translation(int32_t index, double translation[3]) + + Get the 3-D translation coordinates for a meshborn filter + + :param int32_t index: Index in the filters array + :param double[3] translation: 3-D translation coordinates + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_meshborn_filter_set_translation(int32_t index, double translation[3]) + + Set the 3-D translation coordinates for a meshborn filter + + :param int32_t index: Index in the filters array + :param double[3] translation: 3-D translation coordinates + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_meshsurface_filter_get_mesh(int32_t index, int32_t* index_mesh) + + Get the mesh for a mesh surface filter + + :param int32_t index: Index in the filters array + :param index_mesh: Index in the meshes array + :type index_mesh: int32_t* + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_meshsurface_filter_set_mesh(int32_t index, int32_t index_mesh) + + Set the mesh for a mesh surface filter + + :param int32_t index: Index in the filters array + :param int32_t index_mesh: Index in the meshes array + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_meshsurface_filter_get_translation(int32_t index, double translation[3]) + + Get the 3-D translation coordinates for a mesh surface filter + + :param int32_t index: Index in the filters array + :param double[3] translation: 3-D translation coordinates + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_meshsurface_filter_set_translation(int32_t index, double translation[3]) + + Set the 3-D translation coordinates for a mesh surface filter + + :param int32_t index: Index in the filters array + :param double[3] translation: 3-D translation coordinates + :return: Return status (negative if an error occurred) + :rtype: int + .. c:function:: int openmc_next_batch() Simulate next batch of particles. Must be called after openmc_simulation_init(). diff --git a/docs/source/pythonapi/base.rst b/docs/source/pythonapi/base.rst index 7b924e374..5ae9f20ed 100644 --- a/docs/source/pythonapi/base.rst +++ b/docs/source/pythonapi/base.rst @@ -128,6 +128,7 @@ Constructing Tallies openmc.CollisionFilter openmc.SurfaceFilter openmc.MeshFilter + openmc.MeshBornFilter openmc.MeshSurfaceFilter openmc.EnergyFilter openmc.EnergyoutFilter diff --git a/docs/source/pythonapi/capi.rst b/docs/source/pythonapi/capi.rst index ef713801c..16bb68ca4 100644 --- a/docs/source/pythonapi/capi.rst +++ b/docs/source/pythonapi/capi.rst @@ -59,6 +59,7 @@ Classes MaterialFilter Material MeshFilter + MeshBornFilter MeshSurfaceFilter Nuclide RectilinearMesh diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index cb68fc457..cceab1d11 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -262,6 +262,10 @@ public: int& cell_last(int i) { return cell_last_[i]; } const int& cell_last(int i) const { return cell_last_[i]; } + // Coordinates at birth + Position& r_born() { return r_born_; } + const Position& r_born() const { return r_born_; } + // Coordinates of last collision or reflective/periodic surface // crossing for current tallies Position& r_last_current() { return r_last_current_; } @@ -323,6 +327,7 @@ private: int n_coord_last_ {1}; //!< number of current coordinates vector cell_last_; //!< coordinates for all levels + Position r_born_; //!< coordinates at birth Position r_last_current_; //!< coordinates of the last collision or //!< reflective/periodic surface crossing for //!< current tallies diff --git a/include/openmc/tallies/filter.h b/include/openmc/tallies/filter.h index dc5872ce6..1166c0eee 100644 --- a/include/openmc/tallies/filter.h +++ b/include/openmc/tallies/filter.h @@ -33,6 +33,7 @@ enum class FilterType { MATERIAL, MATERIALFROM, MESH, + MESHBORN, MESH_SURFACE, MU, PARTICLE, diff --git a/include/openmc/tallies/filter_meshborn.h b/include/openmc/tallies/filter_meshborn.h new file mode 100644 index 000000000..8ab7a8c76 --- /dev/null +++ b/include/openmc/tallies/filter_meshborn.h @@ -0,0 +1,26 @@ +#ifndef OPENMC_TALLIES_FILTER_MESHBORN_H +#define OPENMC_TALLIES_FILTER_MESHBORN_H + +#include + +#include "openmc/position.h" +#include "openmc/tallies/filter_mesh.h" + +namespace openmc { + +class MeshBornFilter : public MeshFilter { +public: + //---------------------------------------------------------------------------- + // Methods + + std::string type_str() const override { return "meshborn"; } + FilterType type() const override { return FilterType::MESHBORN; } + + void get_all_bins(const Particle& p, TallyEstimator estimator, + FilterMatch& match) const override; + + std::string text_label(int bin) const override; +}; + +} // namespace openmc +#endif // OPENMC_TALLIES_FILTER_MESHBORN_H diff --git a/openmc/filter.py b/openmc/filter.py index f2bbf2f50..04b30261d 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -982,6 +982,34 @@ class MeshFilter(Filter): if translation: out.translation = [float(x) for x in translation.split()] return out + + +class MeshBornFilter(MeshFilter): + """Filter events by the mesh cell a particle originated from. + + Parameters + ---------- + mesh : openmc.MeshBase + The mesh object that events will be tallied onto + filter_id : int + Unique identifier for the filter + + Attributes + ---------- + mesh : openmc.MeshBase + The mesh object that events will be tallied onto + id : int + Unique identifier for the filter + translation : Iterable of float + This array specifies a vector that is used to translate (shift) + the mesh for this filter + bins : list of tuple + A list of mesh indices for each filter bin, e.g. [(1, 1, 1), (2, 1, 1), + ...] + num_bins : Integral + The number of filter bins + + """ class MeshSurfaceFilter(MeshFilter): diff --git a/openmc/lib/filter.py b/openmc/lib/filter.py index 7c2e7e5c4..340c2fa34 100644 --- a/openmc/lib/filter.py +++ b/openmc/lib/filter.py @@ -20,7 +20,8 @@ __all__ = [ 'Filter', 'AzimuthalFilter', 'CellFilter', 'CellbornFilter', 'CellfromFilter', 'CellInstanceFilter', 'CollisionFilter', 'DistribcellFilter', 'DelayedGroupFilter', 'EnergyFilter', 'EnergyoutFilter', 'EnergyFunctionFilter', 'LegendreFilter', - 'MaterialFilter', 'MaterialFromFilter', 'MeshFilter', 'MeshSurfaceFilter', 'MuFilter', 'ParticleFilter', + 'MaterialFilter', 'MaterialFromFilter', 'MeshFilter', 'MeshBornFilter', + 'MeshSurfaceFilter', 'MuFilter', 'ParticleFilter', 'PolarFilter', 'SphericalHarmonicsFilter', 'SpatialLegendreFilter', 'SurfaceFilter', 'UniverseFilter', 'ZernikeFilter', 'ZernikeRadialFilter', 'filters' ] @@ -89,18 +90,36 @@ _dll.openmc_mesh_filter_get_mesh.errcheck = _error_handler _dll.openmc_mesh_filter_set_mesh.argtypes = [c_int32, c_int32] _dll.openmc_mesh_filter_set_mesh.restype = c_int _dll.openmc_mesh_filter_set_mesh.errcheck = _error_handler -_dll.openmc_meshsurface_filter_get_mesh.argtypes = [c_int32, POINTER(c_int32)] -_dll.openmc_meshsurface_filter_get_mesh.restype = c_int -_dll.openmc_meshsurface_filter_get_mesh.errcheck = _error_handler -_dll.openmc_meshsurface_filter_set_mesh.argtypes = [c_int32, c_int32] -_dll.openmc_meshsurface_filter_set_mesh.restype = c_int -_dll.openmc_meshsurface_filter_set_mesh.errcheck = _error_handler _dll.openmc_mesh_filter_get_translation.argtypes = [c_int32, POINTER(c_double*3)] _dll.openmc_mesh_filter_get_translation.restype = c_int _dll.openmc_mesh_filter_get_translation.errcheck = _error_handler _dll.openmc_mesh_filter_set_translation.argtypes = [c_int32, POINTER(c_double*3)] _dll.openmc_mesh_filter_set_translation.restype = c_int _dll.openmc_mesh_filter_set_translation.errcheck = _error_handler +_dll.openmc_meshborn_filter_get_mesh.argtypes = [c_int32, POINTER(c_int32)] +_dll.openmc_meshborn_filter_get_mesh.restype = c_int +_dll.openmc_meshborn_filter_get_mesh.errcheck = _error_handler +_dll.openmc_meshborn_filter_set_mesh.argtypes = [c_int32, c_int32] +_dll.openmc_meshborn_filter_set_mesh.restype = c_int +_dll.openmc_meshborn_filter_set_mesh.errcheck = _error_handler +_dll.openmc_meshborn_filter_get_translation.argtypes = [c_int32, POINTER(c_double*3)] +_dll.openmc_meshborn_filter_get_translation.restype = c_int +_dll.openmc_meshborn_filter_get_translation.errcheck = _error_handler +_dll.openmc_meshborn_filter_set_translation.argtypes = [c_int32, POINTER(c_double*3)] +_dll.openmc_meshborn_filter_set_translation.restype = c_int +_dll.openmc_meshborn_filter_set_translation.errcheck = _error_handler +_dll.openmc_meshsurface_filter_get_mesh.argtypes = [c_int32, POINTER(c_int32)] +_dll.openmc_meshsurface_filter_get_mesh.restype = c_int +_dll.openmc_meshsurface_filter_get_mesh.errcheck = _error_handler +_dll.openmc_meshsurface_filter_set_mesh.argtypes = [c_int32, c_int32] +_dll.openmc_meshsurface_filter_set_mesh.restype = c_int +_dll.openmc_meshsurface_filter_set_mesh.errcheck = _error_handler +_dll.openmc_meshsurface_filter_get_translation.argtypes = [c_int32, POINTER(c_double*3)] +_dll.openmc_meshsurface_filter_get_translation.restype = c_int +_dll.openmc_meshsurface_filter_get_translation.errcheck = _error_handler +_dll.openmc_meshsurface_filter_set_translation.argtypes = [c_int32, POINTER(c_double*3)] +_dll.openmc_meshsurface_filter_set_translation.restype = c_int +_dll.openmc_meshsurface_filter_set_translation.errcheck = _error_handler _dll.openmc_new_filter.argtypes = [c_char_p, POINTER(c_int32)] _dll.openmc_new_filter.restype = c_int _dll.openmc_new_filter.errcheck = _error_handler @@ -347,6 +366,34 @@ class MaterialFromFilter(Filter): class MeshFilter(Filter): + """Mesh filter stored internally. + + This class exposes a Mesh filter that is stored internally in the OpenMC + library. To obtain a view of a Mesh filter with a given ID, use the + :data:`openmc.lib.filters` mapping. + + Parameters + ---------- + mesh : openmc.lib.Mesh + Mesh to use for the filter + uid : int or None + Unique ID of the Mesh filter + new : bool + When `index` is None, this argument controls whether a new object is + created or a view of an existing object is returned. + index : int + Index in the `filters` array. + + Attributes + ---------- + filter_type : str + Type of filter + mesh : openmc.lib.Mesh + Mesh used for the filter + translation : Iterable of float + 3-D coordinates of the translation vector + + """ filter_type = 'mesh' def __init__(self, mesh=None, uid=None, new=True, index=None): @@ -375,7 +422,92 @@ class MeshFilter(Filter): _dll.openmc_mesh_filter_set_translation(self._index, (c_double*3)(*translation)) +class MeshBornFilter(Filter): + """MeshBorn filter stored internally. + + This class exposes a MeshBorn filter that is stored internally in the OpenMC + library. To obtain a view of a MeshBorn filter with a given ID, use the + :data:`openmc.lib.filters` mapping. + + Parameters + ---------- + mesh : openmc.lib.Mesh + Mesh to use for the filter + uid : int or None + Unique ID of the MeshBorn filter + new : bool + When `index` is None, this argument controls whether a new object is + created or a view of an existing object is returned. + index : int + Index in the `filters` array. + + Attributes + ---------- + filter_type : str + Type of filter + mesh : openmc.lib.Mesh + Mesh used for the filter + translation : Iterable of float + 3-D coordinates of the translation vector + + """ + filter_type = 'meshborn' + + def __init__(self, mesh=None, uid=None, new=True, index=None): + super().__init__(uid, new, index) + if mesh is not None: + self.mesh = mesh + + @property + def mesh(self): + index_mesh = c_int32() + _dll.openmc_meshborn_filter_get_mesh(self._index, index_mesh) + return _get_mesh(index_mesh.value) + + @mesh.setter + def mesh(self, mesh): + _dll.openmc_meshborn_filter_set_mesh(self._index, mesh._index) + + @property + def translation(self): + translation = (c_double*3)() + _dll.openmc_meshborn_filter_get_translation(self._index, translation) + return tuple(translation) + + @translation.setter + def translation(self, translation): + _dll.openmc_meshborn_filter_set_translation(self._index, (c_double*3)(*translation)) + + class MeshSurfaceFilter(Filter): + """MeshSurface filter stored internally. + + This class exposes a MeshSurface filter that is stored internally in the + OpenMC library. To obtain a view of a MeshSurface filter with a given ID, + use the :data:`openmc.lib.filters` mapping. + + Parameters + ---------- + mesh : openmc.lib.Mesh + Mesh to use for the filter + uid : int or None + Unique ID of the MeshSurface filter + new : bool + When `index` is None, this argument controls whether a new object is + created or a view of an existing object is returned. + index : int + Index in the `filters` array. + + Attributes + ---------- + filter_type : str + Type of filter + mesh : openmc.lib.Mesh + Mesh used for the filter + translation : Iterable of float + 3-D coordinates of the translation vector + + """ filter_type = 'meshsurface' def __init__(self, mesh=None, uid=None, new=True, index=None): @@ -396,12 +528,12 @@ class MeshSurfaceFilter(Filter): @property def translation(self): translation = (c_double*3)() - _dll.openmc_mesh_filter_get_translation(self._index, translation) + _dll.openmc_meshsurface_filter_get_translation(self._index, translation) return tuple(translation) @translation.setter def translation(self, translation): - _dll.openmc_mesh_filter_set_translation(self._index, (c_double*3)(*translation)) + _dll.openmc_meshsurface_filter_set_translation(self._index, (c_double*3)(*translation)) class MuFilter(Filter): @@ -507,6 +639,7 @@ _FILTER_TYPE_MAP = { 'material': MaterialFilter, 'materialfrom': MaterialFromFilter, 'mesh': MeshFilter, + 'meshborn': MeshBornFilter, 'meshsurface': MeshSurfaceFilter, 'mu': MuFilter, 'particle': ParticleFilter, diff --git a/src/particle.cpp b/src/particle.cpp index 549de5cff..c2e340201 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -121,6 +121,7 @@ void Particle::from_source(const SourceSite* src) wgt_last() = src->wgt; r() = src->r; u() = src->u; + r_born() = src->r; r_last_current() = src->r; r_last() = src->r; u_last() = src->u; diff --git a/src/tallies/filter.cpp b/src/tallies/filter.cpp index 5fae4cf60..ff7a3416b 100644 --- a/src/tallies/filter.cpp +++ b/src/tallies/filter.cpp @@ -23,6 +23,7 @@ #include "openmc/tallies/filter_material.h" #include "openmc/tallies/filter_materialfrom.h" #include "openmc/tallies/filter_mesh.h" +#include "openmc/tallies/filter_meshborn.h" #include "openmc/tallies/filter_meshsurface.h" #include "openmc/tallies/filter_mu.h" #include "openmc/tallies/filter_particle.h" @@ -126,6 +127,8 @@ Filter* Filter::create(const std::string& type, int32_t id) return Filter::create(id); } else if (type == "mesh") { return Filter::create(id); + } else if (type == "meshborn") { + return Filter::create(id); } else if (type == "meshsurface") { return Filter::create(id); } else if (type == "mu") { diff --git a/src/tallies/filter_mesh.cpp b/src/tallies/filter_mesh.cpp index ed9c71667..5b01da1f6 100644 --- a/src/tallies/filter_mesh.cpp +++ b/src/tallies/filter_mesh.cpp @@ -161,6 +161,7 @@ extern "C" int openmc_mesh_filter_get_translation( // Check the filter type const auto& filter = model::tally_filters[index]; if (filter->type() != FilterType::MESH && + filter->type() != FilterType::MESHBORN && filter->type() != FilterType::MESH_SURFACE) { set_errmsg("Tried to get a translation from a non-mesh-based filter."); return OPENMC_E_INVALID_TYPE; @@ -186,6 +187,7 @@ extern "C" int openmc_mesh_filter_set_translation( const auto& filter = model::tally_filters[index]; // Check the filter type if (filter->type() != FilterType::MESH && + filter->type() != FilterType::MESHBORN && filter->type() != FilterType::MESH_SURFACE) { set_errmsg("Tried to set mesh on a non-mesh-based filter."); return OPENMC_E_INVALID_TYPE; diff --git a/src/tallies/filter_meshborn.cpp b/src/tallies/filter_meshborn.cpp new file mode 100644 index 000000000..c95dc3dc7 --- /dev/null +++ b/src/tallies/filter_meshborn.cpp @@ -0,0 +1,61 @@ +#include "openmc/tallies/filter_meshborn.h" + +#include "openmc/capi.h" +#include "openmc/constants.h" +#include "openmc/error.h" +#include "openmc/mesh.h" + +namespace openmc { + +void MeshBornFilter::get_all_bins( + const Particle& p, TallyEstimator estimator, FilterMatch& match) const +{ + Position r_born = p.r_born(); + + // apply translation if present + if (translated_) { + r_born -= translation(); + } + + auto bin = model::meshes[mesh_]->get_bin(r_born); + if (bin >= 0) { + match.bins_.push_back(bin); + match.weights_.push_back(1.0); + } +} + +std::string MeshBornFilter::text_label(int bin) const +{ + auto& mesh = *model::meshes.at(mesh_); + return mesh.bin_label(bin) + " (born)"; +} + +//============================================================================== +// C-API functions +//============================================================================== + +extern "C" int openmc_meshborn_filter_get_mesh( + int32_t index, int32_t* index_mesh) +{ + return openmc_mesh_filter_get_mesh(index, index_mesh); +} + +extern "C" int openmc_meshborn_filter_set_mesh( + int32_t index, int32_t index_mesh) +{ + return openmc_mesh_filter_set_mesh(index, index_mesh); +} + +extern "C" int openmc_meshborn_filter_get_translation( + int32_t index, double translation[3]) +{ + return openmc_mesh_filter_get_translation(index, translation); +} + +extern "C" int openmc_meshborn_filter_set_translation( + int32_t index, double translation[3]) +{ + return openmc_mesh_filter_set_translation(index, translation); +} + +} // namespace openmc diff --git a/src/tallies/filter_meshsurface.cpp b/src/tallies/filter_meshsurface.cpp index b22085ebb..b26cd198b 100644 --- a/src/tallies/filter_meshsurface.cpp +++ b/src/tallies/filter_meshsurface.cpp @@ -100,4 +100,16 @@ extern "C" int openmc_meshsurface_filter_set_mesh( return openmc_mesh_filter_set_mesh(index, index_mesh); } +extern "C" int openmc_meshsurface_filter_get_translation( + int32_t index, double translation[3]) +{ + return openmc_mesh_filter_get_translation(index, translation); +} + +extern "C" int openmc_meshsurface_filter_set_translation( + int32_t index, double translation[3]) +{ + return openmc_mesh_filter_set_translation(index, translation); +} + } // namespace openmc diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index 7fd444a58..df1ec5ea7 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -26,6 +26,7 @@ #include "openmc/tallies/filter_energy.h" #include "openmc/tallies/filter_legendre.h" #include "openmc/tallies/filter_mesh.h" +#include "openmc/tallies/filter_meshborn.h" #include "openmc/tallies/filter_meshsurface.h" #include "openmc/tallies/filter_particle.h" #include "openmc/tallies/filter_sph_harm.h" diff --git a/tests/regression_tests/filter_meshborn/__init__.py b/tests/regression_tests/filter_meshborn/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/filter_meshborn/inputs_true.dat b/tests/regression_tests/filter_meshborn/inputs_true.dat new file mode 100644 index 000000000..3ba38d56e --- /dev/null +++ b/tests/regression_tests/filter_meshborn/inputs_true.dat @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + fixed source + 2000 + 8 + + + 0.0 -10.0 -10.0 10.0 10.0 10.0 + + + + + + 2 2 1 + -10.0 -10.0 -10.0 + 10.0 10.0 10.0 + + + 1 + + + 1 + + + 1 2 + scatter + + + 1 + scatter + + + 2 + scatter + + + scatter + + + diff --git a/tests/regression_tests/filter_meshborn/results_true.dat b/tests/regression_tests/filter_meshborn/results_true.dat new file mode 100644 index 000000000..62f2707ff --- /dev/null +++ b/tests/regression_tests/filter_meshborn/results_true.dat @@ -0,0 +1,54 @@ +tally 1: +0.000000E+00 +0.000000E+00 +2.631246E+01 +8.845079E+01 +0.000000E+00 +0.000000E+00 +2.450265E+00 +9.462266E-01 +0.000000E+00 +0.000000E+00 +3.878380E+02 +1.881752E+04 +0.000000E+00 +0.000000E+00 +2.932956E+01 +1.091674E+02 +0.000000E+00 +0.000000E+00 +1.837753E+00 +5.195343E-01 +0.000000E+00 +0.000000E+00 +2.944919E+01 +1.095819E+02 +0.000000E+00 +0.000000E+00 +2.921731E+01 +1.097387E+02 +0.000000E+00 +0.000000E+00 +4.019442E+02 +2.021184E+04 +tally 2: +2.876273E+01 +1.060244E+02 +4.171676E+02 +2.176683E+04 +3.128695E+01 +1.238772E+02 +4.311615E+02 +2.325871E+04 +tally 3: +0.000000E+00 +0.000000E+00 +4.452055E+02 +2.478148E+04 +0.000000E+00 +0.000000E+00 +4.631732E+02 +2.683862E+04 +tally 4: +9.083787E+02 +1.031695E+05 diff --git a/tests/regression_tests/filter_meshborn/test.py b/tests/regression_tests/filter_meshborn/test.py new file mode 100644 index 000000000..ff4adbc9f --- /dev/null +++ b/tests/regression_tests/filter_meshborn/test.py @@ -0,0 +1,107 @@ +"""Test the meshborn filter using a fixed source calculation on a H1 sphere. + +""" + +from numpy.testing import assert_allclose +import numpy as np +import openmc +import pytest + +from tests.testing_harness import PyAPITestHarness + + +RTOL = 1.0e-7 +ATOL = 0.0 + + +@pytest.fixture +def model(): + """Sphere of H1 with one hemisphere containing the source (x>0) and one + hemisphere with no source (x<0). + + """ + openmc.reset_auto_ids() + model = openmc.Model() + + # Materials + h1 = openmc.Material() + h1.add_nuclide("H1", 1.0) + h1.set_density("g/cm3", 1.0) + model.materials = openmc.Materials([h1]) + + # Core geometry + r = 10.0 + sphere = openmc.Sphere(r=r, boundary_type="reflective") + core = openmc.Cell(fill=h1, region=-sphere) + model.geometry = openmc.Geometry([core]) + + # Settings + model.settings.run_mode = 'fixed source' + model.settings.particles = 2000 + model.settings.batches = 8 + distribution = openmc.stats.Box((0., -r, -r), (r, r, r)) + model.settings.source = openmc.IndependentSource(space=distribution) + + # Tallies + mesh = openmc.RegularMesh() + mesh.dimension = (2, 2, 1) + mesh.lower_left = (-r, -r, -r) + mesh.upper_right = (r, r, r) + + f_1 = openmc.MeshFilter(mesh) + f_2 = openmc.MeshBornFilter(mesh) + + t_1 = openmc.Tally(name="scatter") + t_1.filters = [f_1, f_2] + t_1.scores = ["scatter"] + + t_2 = openmc.Tally(name="scatter-mesh") + t_2.filters = [f_1] + t_2.scores = ["scatter"] + + t_3 = openmc.Tally(name="scatter-meshborn") + t_3.filters = [f_2] + t_3.scores = ["scatter"] + + t_4 = openmc.Tally(name="scatter-total") + t_4.scores = ["scatter"] + + model.tallies = [t_1, t_2, t_3, t_4] + + return model + + +class MeshBornFilterTest(PyAPITestHarness): + + def _compare_results(self): + """Additional unit tests on the tally results to check consistency.""" + with openmc.StatePoint(self.statepoint_name) as sp: + + t1 = sp.get_tally(name="scatter").mean.reshape(4, 4) + t2 = sp.get_tally(name="scatter-mesh").mean.reshape(4) + t3 = sp.get_tally(name="scatter-meshborn").mean.reshape(4) + t4 = sp.get_tally(name="scatter-total").mean.reshape(1) + + # Consistency between mesh+meshborn matrix tally and meshborn tally + for i in range(4): + assert_allclose(t1[:, i].sum(), t3[i], rtol=RTOL, atol=ATOL) + + # Consistency between mesh+meshborn matrix tally and mesh tally + for i in range(4): + assert_allclose(t1[i, :].sum(), t2[i], rtol=RTOL, atol=ATOL) + + # Mesh cells in x<0 do not contribute to meshborn + assert_allclose(t1[:, 0].sum(), np.zeros(4), rtol=RTOL, atol=ATOL) + assert_allclose(t1[:, 2].sum(), np.zeros(4), rtol=RTOL, atol=ATOL) + + # Consistency with total scattering + assert_allclose(t1.sum(), t4, rtol=RTOL, atol=ATOL) + assert_allclose(t2.sum(), t4, rtol=RTOL, atol=ATOL) + assert_allclose(t3.sum(), t4, rtol=RTOL, atol=ATOL) + + super()._compare_results() + + +def test_filter_meshborn(model): + harness = MeshBornFilterTest("statepoint.8.h5", model) + harness.main() diff --git a/tests/unit_tests/test_filter_meshborn.py b/tests/unit_tests/test_filter_meshborn.py new file mode 100644 index 000000000..62fa1174e --- /dev/null +++ b/tests/unit_tests/test_filter_meshborn.py @@ -0,0 +1,116 @@ +"""Test the meshborn filter using a fixed source calculation on a H1 sphere. + +""" + +import numpy as np +from uncertainties import unumpy +import openmc +import pytest + + +@pytest.fixture +def model(): + """Sphere of H1 with one hemisphere containing the source (x>0) and one + hemisphere with no source (x<0). + + """ + openmc.reset_auto_ids() + model = openmc.Model() + + # Materials + h1 = openmc.Material() + h1.add_nuclide("H1", 1.0) + h1.set_density("g/cm3", 1.0) + model.materials = openmc.Materials([h1]) + + # Core geometry + r = 10.0 + sphere = openmc.Sphere(r=r, boundary_type="reflective") + core = openmc.Cell(fill=h1, region=-sphere) + model.geometry = openmc.Geometry([core]) + + # Settings + model.settings.run_mode = 'fixed source' + model.settings.particles = 2000 + model.settings.batches = 8 + + distribution = openmc.stats.Box((0., -r, -r), (r, r, r)) + model.settings.source = openmc.IndependentSource(space=distribution) + + # ============================================================================= + # Tallies + # ============================================================================= + + mesh = openmc.RegularMesh() + mesh.dimension = (2, 2, 1) + mesh.lower_left = (-r, -r, -r) + mesh.upper_right = (r, r, r) + + f = openmc.MeshBornFilter(mesh) + t_1 = openmc.Tally(name="scatter-collision") + t_1.filters = [f] + t_1.scores = ["scatter"] + t_1.estimator = "collision" + + t_2 = openmc.Tally(name="scatter-tracklength") + t_2.filters = [f] + t_2.scores = ["scatter"] + t_2.estimator = "tracklength" + + model.tallies = [t_1, t_2] + + return model + + +def test_estimator_consistency(model, run_in_tmpdir): + """Test that resuts obtained from a tracklength estimator are + consistent with results obtained from a collision estimator. + + """ + # Run OpenMC + sp_filename = model.run() + + # Get radial flux distribution + with openmc.StatePoint(sp_filename) as sp: + scatter_collision = sp.get_tally(name="scatter-collision").mean.ravel() + scatter_collision_std_dev = sp.get_tally(name="scatter-collision").std_dev.ravel() + scatter_tracklength = sp.get_tally(name="scatter-tracklength").mean.ravel() + scatter_tracklength_std_dev = sp.get_tally(name="scatter-tracklength").std_dev.ravel() + + collision = unumpy.uarray(scatter_collision, scatter_collision_std_dev) + tracklength = unumpy.uarray(scatter_tracklength, scatter_tracklength_std_dev) + delta = abs(collision - tracklength) + + diff = unumpy.nominal_values(delta) + std_dev = unumpy.std_devs(delta) + assert np.all(diff <= 3 * std_dev) + + +def test_xml_serialization(): + """Test xml serialization of the meshborn filter.""" + openmc.reset_auto_ids() + + mesh = openmc.RegularMesh() + mesh.dimension = (1, 1, 1) + mesh.lower_left = (0.0, 0.0, 0.0) + mesh.upper_right = (1.0, 1.0, 1.0) + + filter = openmc.MeshBornFilter(mesh) + filter.translation = (2.0, 2.0, 2.0) + assert filter.mesh.id == 1 + assert filter.mesh.dimension == (1, 1, 1) + assert filter.mesh.lower_left == (0.0, 0.0, 0.0) + assert filter.mesh.upper_right == (1.0, 1.0, 1.0) + + repr(filter) + + elem = filter.to_xml_element() + assert elem.tag == 'filter' + assert elem.attrib['type'] == 'meshborn' + assert elem[0].text == "1" + assert elem.get("translation") == "2.0 2.0 2.0" + + meshes = {1: mesh} + new_filter = openmc.Filter.from_xml_element(elem, meshes=meshes) + assert new_filter.bins == filter.bins + np.testing.assert_equal(new_filter.translation, [2.0, 2.0, 2.0]) diff --git a/tests/unit_tests/test_tallies.py b/tests/unit_tests/test_tallies.py index 14319a0a2..544443312 100644 --- a/tests/unit_tests/test_tallies.py +++ b/tests/unit_tests/test_tallies.py @@ -10,8 +10,9 @@ def test_xml_roundtrip(run_in_tmpdir): mesh.upper_right = (10., 10., 10.,) mesh.dimension = (5, 5, 5) mesh_filter = openmc.MeshFilter(mesh) + meshborn_filter = openmc.MeshBornFilter(mesh) tally = openmc.Tally() - tally.filters = [mesh_filter] + tally.filters = [mesh_filter, meshborn_filter] tally.nuclides = ['U235', 'I135', 'Li6'] tally.scores = ['total', 'fission', 'heating'] tally.derivative = openmc.TallyDerivative( @@ -27,9 +28,11 @@ def test_xml_roundtrip(run_in_tmpdir): assert len(new_tallies) == 1 new_tally = new_tallies[0] assert new_tally.id == tally.id - assert len(new_tally.filters) == 1 + assert len(new_tally.filters) == 2 assert isinstance(new_tally.filters[0], openmc.MeshFilter) assert np.allclose(new_tally.filters[0].mesh.lower_left, mesh.lower_left) + assert isinstance(new_tally.filters[1], openmc.MeshBornFilter) + assert np.allclose(new_tally.filters[1].mesh.lower_left, mesh.lower_left) assert new_tally.nuclides == tally.nuclides assert new_tally.scores == tally.scores assert new_tally.derivative.variable == tally.derivative.variable From 0aad22d54158271cd33a3c245936d7164a17e2f6 Mon Sep 17 00:00:00 2001 From: Ethan Peterson Date: Fri, 5 Apr 2024 02:35:15 -0400 Subject: [PATCH 068/671] Allow get_microxs_and_flux to use OPENMC_CHAIN_FILE environment variable (#2934) --- openmc/deplete/microxs.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index 0721535ca..c1c4cb7ac 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -27,10 +27,9 @@ _valid_rxns.append('fission') def _resolve_chain_file_path(chain_file: str): - # Determine what reactions and nuclides are available in chain if chain_file is None: chain_file = openmc.config.get('chain_file') - if 'chain_file' in openmc.config: + if 'chain_file' not in openmc.config: raise DataError( "No depletion chain specified and could not find depletion " "chain in openmc.config['chain_file']" From 27bd315f0f2eb61b447603514bd790cc04309a91 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Fri, 5 Apr 2024 10:37:46 +0100 Subject: [PATCH 069/671] changing y axis label for heating plots (#2859) Co-authored-by: Paul Romano --- openmc/plotter.py | 35 ++++++++++++++++++++++---------- tests/unit_tests/test_plotter.py | 15 ++++++++++++++ 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/openmc/plotter.py b/openmc/plotter.py index 849672fce..97ca5f929 100644 --- a/openmc/plotter.py +++ b/openmc/plotter.py @@ -73,24 +73,37 @@ def _get_legend_label(this, type): def _get_yaxis_label(reactions, divisor_types): """Gets a y axis label for the type of data plotted""" - if all(isinstance(item, str) for item in reactions.keys()): - stem = 'Microscopic' - if divisor_types: - mid, units = 'Data', '' - else: - mid, units = 'Cross Section', '[b]' + heat_values = {"heating", "heating-local", "damage-energy"} + + # if all the types are heating a different stem and unit is needed + if all(set(value).issubset(heat_values) for value in reactions.values()): + stem = "Heating" + elif all(isinstance(item, str) for item in reactions.keys()): + for nuc_reactions in reactions.values(): + for reaction in nuc_reactions: + if reaction in heat_values: + raise TypeError( + "Mixture of heating and Microscopic reactions. " + "Invalid type for plotting" + ) + stem = "Microscopic" elif all(isinstance(item, openmc.Material) for item in reactions.keys()): stem = 'Macroscopic' - if divisor_types: - mid, units = 'Data', '' - else: - mid, units = 'Cross Section', '[1/cm]' else: msg = "Mixture of openmc.Material and elements/nuclides. Invalid type for plotting" raise TypeError(msg) - return f'{stem} {mid} {units}' + if divisor_types: + mid, units = "Data", "" + else: + mid = "Cross Section" + units = { + "Macroscopic": "[1/cm]", + "Microscopic": "[b]", + "Heating": "[eV-barn]", + }[stem] + return f'{stem} {mid} {units}' def _get_title(reactions): """Gets a title for the type of data plotted""" diff --git a/tests/unit_tests/test_plotter.py b/tests/unit_tests/test_plotter.py index fd635d89d..2c195c5e1 100644 --- a/tests/unit_tests/test_plotter.py +++ b/tests/unit_tests/test_plotter.py @@ -118,6 +118,21 @@ def test_plot_axes_labels(): ) assert axis_label == 'Microscopic Cross Section [b]' + axis_label = openmc.plotter._get_yaxis_label( + reactions={ + "Li": ["heating", "heating-local"], + "Li7": ["heating"], + "Be": ["damage-energy"], + }, + divisor_types=False, + ) + assert axis_label == "Heating Cross Section [eV-barn]" + + with pytest.raises(TypeError): + axis_label = openmc.plotter.plot_xs( + reactions={"Li": ["heating", "heating-local"], "Be9": ["(n,2n)"]} + ) + # just materials mat1 = openmc.Material() mat1.add_nuclide('Fe56', 1) From 704cfbf1afb11a03d4dc6223d737e40680b49f39 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Sun, 7 Apr 2024 16:27:51 +0100 Subject: [PATCH 070/671] Updating docker file base to bookworm (#2890) --- Dockerfile | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index fa223bd22..0b4742ada 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,7 @@ ARG compile_cores=1 ARG build_dagmc=off ARG build_libmesh=off -FROM debian:bullseye-slim AS dependencies +FROM debian:bookworm-slim AS dependencies ARG compile_cores ARG build_dagmc @@ -71,9 +71,13 @@ 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 && \ + libpng-dev python3-venv && \ apt-get autoremove +# create virtual enviroment to avoid externally managed environment error +RUN python3 -m venv openmc_venv +ENV PATH=/openmc_venv/bin:$PATH + # Update system-provided pip RUN pip install --upgrade pip From db3b6f3e9ef30af313c372b48915ab7e301e9ab0 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Sun, 7 Apr 2024 17:50:44 +0100 Subject: [PATCH 071/671] added missing functions and classes to openmc.lib docs (#2847) Co-authored-by: Paul Romano --- docs/source/pythonapi/capi.rst | 76 ++++++++++++++++++++++++++++++++-- openmc/arithmetic.py | 12 +++--- openmc/lib/mesh.py | 6 ++- 3 files changed, 84 insertions(+), 10 deletions(-) diff --git a/docs/source/pythonapi/capi.rst b/docs/source/pythonapi/capi.rst index 16bb68ca4..313543199 100644 --- a/docs/source/pythonapi/capi.rst +++ b/docs/source/pythonapi/capi.rst @@ -16,7 +16,6 @@ Functions current_batch export_properties export_weight_windows - import_weight_windows finalize find_cell find_material @@ -25,6 +24,7 @@ Functions hard_reset id_map import_properties + import_weight_windows init is_statepoint_batch iter_batches @@ -40,8 +40,8 @@ Functions run run_in_memory sample_external_source - simulation_init simulation_finalize + simulation_init source_bank statepoint_write @@ -53,16 +53,86 @@ Classes :nosignatures: :template: myclass.rst + AzimuthalFilter Cell + CellFilter + CellInstanceFilter + CellbornFilter + CellfromFilter + CollisionFilter CylindricalMesh + DelayedGroupFilter + DistribcellFilter EnergyFilter - MaterialFilter + EnergyFunctionFilter + EnergyoutFilter + Filter + LegendreFilter Material + MaterialFilter + MaterialFromFilter + Mesh MeshFilter MeshBornFilter MeshSurfaceFilter + MuFilter Nuclide + ParticleFilter + PolarFilter RectilinearMesh RegularMesh + SpatialLegendreFilter + SphericalHarmonicsFilter SphericalMesh + SurfaceFilter Tally + UniverseFilter + UnstructuredMesh + WeightWindows + ZernikeFilter + ZernikeRadialFilter + +Data +---- + +.. data:: cells + + Mapping of cell ID to :class:`openmc.lib.Cell` instances. + + :type: dict + +.. data:: filters + + Mapping of filter ID to :class:`openmc.lib.Filter` instances. + + :type: dict + +.. data:: materials + + Mapping of material ID to :class:`openmc.lib.Material` instances. + + :type: dict + +.. data:: meshes + + Mapping of mesh ID to :class:`openmc.lib.Mesh` instances. + + :type: dict + +.. data:: nuclides + + Mapping of nuclide name to :class:`openmc.lib.Nuclide` instances. + + :type: dict + +.. data:: tallies + + Mapping of tally ID to :class:`openmc.lib.Tally` instances. + + :type: dict + +.. data:: weight_windows + + Mapping of weight window ID to :class:`openmc.lib.WeightWindows` instances. + + :type: dict diff --git a/openmc/arithmetic.py b/openmc/arithmetic.py index 5ca7cc666..c66efe5b8 100644 --- a/openmc/arithmetic.py +++ b/openmc/arithmetic.py @@ -188,9 +188,9 @@ class CrossFilter: Parameters ---------- - left_filter : Filter or CrossFilter + left_filter : openmc.Filter or CrossFilter The left filter in the outer product - right_filter : Filter or CrossFilter + right_filter : openmc.Filter or CrossFilter The right filter in the outer product binary_op : str The tally arithmetic binary operator (e.g., '+', '-', etc.) used to @@ -200,9 +200,9 @@ class CrossFilter: ---------- type : str The type of the crossfilter (e.g., 'energy / energy') - left_filter : Filter or CrossFilter + left_filter : openmc.Filter or CrossFilter The left filter in the outer product - right_filter : Filter or CrossFilter + right_filter : openmc.Filter or CrossFilter The right filter in the outer product binary_op : str The tally arithmetic binary operator (e.g., '+', '-', etc.) used to @@ -517,7 +517,7 @@ class AggregateFilter: Parameters ---------- - aggregate_filter : Filter or CrossFilter + aggregate_filter : openmc.Filter or CrossFilter The filter included in the aggregation bins : Iterable of tuple The filter bins included in the aggregation @@ -529,7 +529,7 @@ class AggregateFilter: ---------- type : str The type of the aggregatefilter (e.g., 'sum(energy)', 'sum(cell)') - aggregate_filter : filter + aggregate_filter : openmc.Filter The filter included in the aggregation aggregate_op : str The tally aggregation operator (e.g., 'sum', 'avg', etc.) used diff --git a/openmc/lib/mesh.py b/openmc/lib/mesh.py index 7f17dd81f..4da7baba7 100644 --- a/openmc/lib/mesh.py +++ b/openmc/lib/mesh.py @@ -15,7 +15,10 @@ from .error import _error_handler from .material import Material from .plot import _Position -__all__ = ['RegularMesh', 'RectilinearMesh', 'CylindricalMesh', 'SphericalMesh', 'UnstructuredMesh', 'meshes'] +__all__ = [ + 'Mesh', 'RegularMesh', 'RectilinearMesh', 'CylindricalMesh', + 'SphericalMesh', 'UnstructuredMesh', 'meshes' +] class _MaterialVolume(Structure): @@ -553,6 +556,7 @@ class CylindricalMesh(Mesh): _dll.openmc_cylindrical_mesh_set_grid(self._index, r_grid, nr, phi_grid, nphi, z_grid, nz) + class SphericalMesh(Mesh): """SphericalMesh stored internally. From c85ba93040433ee2d4be7f17d37d0831da14d2f0 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Mon, 8 Apr 2024 05:46:59 -0500 Subject: [PATCH 072/671] Fix distribcell labels for lattices used as fill in multiple cells (#2813) Co-authored-by: Paul Romano --- src/geometry_aux.cpp | 8 ++++---- src/tallies/filter_distribcell.cpp | 3 ++- tests/unit_tests/test_cell_instance.py | 19 +++++++++++++++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/geometry_aux.cpp b/src/geometry_aux.cpp index b0e88e8e3..10b239be8 100644 --- a/src/geometry_aux.cpp +++ b/src/geometry_aux.cpp @@ -539,7 +539,7 @@ std::string distribcell_path_inner(int32_t target_cell, int32_t map, // The desired cell is the first cell that gives an offset smaller or // equal to the target offset. - if (temp_offset <= target_offset) + if (temp_offset <= target_offset - c.offset_[map]) break; } } @@ -570,11 +570,11 @@ std::string distribcell_path_inner(int32_t target_cell, int32_t map, for (ReverseLatticeIter it = lat.rbegin(); it != lat.rend(); ++it) { int32_t indx = lat.universes_.size() * map + it.indx_; int32_t temp_offset = offset + lat.offsets_[indx]; - if (temp_offset <= target_offset) { + if (temp_offset <= target_offset - c.offset_[map]) { offset = temp_offset; path << "(" << lat.index_to_string(it.indx_) << ")->"; - path << distribcell_path_inner( - target_cell, map, target_offset, *model::universes[*it], offset); + path << distribcell_path_inner(target_cell, map, target_offset, + *model::universes[*it], offset + c.offset_[map]); return path.str(); } } diff --git a/src/tallies/filter_distribcell.cpp b/src/tallies/filter_distribcell.cpp index 89349b61f..c754dbd44 100644 --- a/src/tallies/filter_distribcell.cpp +++ b/src/tallies/filter_distribcell.cpp @@ -48,7 +48,8 @@ void DistribcellFilter::get_all_bins( auto& lat {*model::lattices[p.coord(i + 1).lattice]}; const auto& i_xyz {p.coord(i + 1).lattice_i}; if (lat.are_valid_indices(i_xyz)) { - offset += lat.offset(distribcell_index, i_xyz); + offset += + lat.offset(distribcell_index, i_xyz) + c.offset_[distribcell_index]; } } if (cell_ == p.coord(i).cell) { diff --git a/tests/unit_tests/test_cell_instance.py b/tests/unit_tests/test_cell_instance.py index 00424f9c5..25c20cfef 100644 --- a/tests/unit_tests/test_cell_instance.py +++ b/tests/unit_tests/test_cell_instance.py @@ -9,6 +9,7 @@ from tests import cdtemp @pytest.fixture(scope='module', autouse=True) def double_lattice_model(): + openmc.reset_auto_ids() model = openmc.Model() # Create a single material @@ -39,6 +40,18 @@ def double_lattice_model(): cell_with_lattice2.translation = (2., 0., 0.) model.geometry = openmc.Geometry([cell_with_lattice1, cell_with_lattice2]) + tally = openmc.Tally() + tally.filters = [openmc.DistribcellFilter(c)] + tally.scores = ['flux'] + model.tallies = [tally] + + # Add box source that covers the model space well + bbox = model.geometry.bounding_box + bbox[0][2] = -0.5 + bbox[1][2] = 0.5 + space = openmc.stats.Box(*bbox) + model.settings.source = openmc.IndependentSource(space=space) + # Add necessary settings and export model.settings.batches = 10 model.settings.inactive = 0 @@ -71,3 +84,9 @@ expected_results = [ def test_cell_instance_multilattice(r, expected_cell_instance): _, cell_instance = openmc.lib.find_cell(r) assert cell_instance == expected_cell_instance + + +def test_cell_instance_multilattice_results(): + openmc.lib.run() + tally_results = openmc.lib.tallies[1].mean + assert (tally_results != 0.0).all() From 463299d04aa15c87baedaf33bf84c07b77090eed Mon Sep 17 00:00:00 2001 From: Ethan Peterson Date: Mon, 8 Apr 2024 09:45:01 -0400 Subject: [PATCH 073/671] Polygon fix to better handle colinear points (#2935) Co-authored-by: Paul Romano --- openmc/model/surface_composite.py | 26 +++++++++++++--------- tests/unit_tests/test_surface_composite.py | 26 ++++++++++++++++++++++ 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index 8f48b2f71..5543a88e8 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -635,7 +635,7 @@ class XConeOneSided(CompositeSurface): z-coordinate of the apex. Defaults to 0. r2 : float, optional Parameter related to the aperture [:math:`\\rm cm^2`]. - It can be interpreted as the increase in the radius squared per cm along + It can be interpreted as the increase in the radius squared per cm along the cone's axis of revolution. up : bool Whether to select the side of the cone that extends to infinity in the @@ -695,7 +695,7 @@ class YConeOneSided(CompositeSurface): z-coordinate of the apex. Defaults to 0. r2 : float, optional Parameter related to the aperture [:math:`\\rm cm^2`]. - It can be interpreted as the increase in the radius squared per cm along + It can be interpreted as the increase in the radius squared per cm along the cone's axis of revolution. up : bool Whether to select the side of the cone that extends to infinity in the @@ -749,7 +749,7 @@ class ZConeOneSided(CompositeSurface): z-coordinate of the apex. Defaults to 0. r2 : float, optional Parameter related to the aperture [:math:`\\rm cm^2`]. - It can be interpreted as the increase in the radius squared per cm along + It can be interpreted as the increase in the radius squared per cm along the cone's axis of revolution. up : bool Whether to select the side of the cone that extends to infinity in the @@ -1029,9 +1029,9 @@ class Polygon(CompositeSurface): ------- None """ - # Only attempt the triangulation up to 3 times. - if depth > 2: - raise RuntimeError('Could not create a valid triangulation after 3' + # Only attempt the triangulation up to 5 times. + if depth > 4: + raise RuntimeError('Could not create a valid triangulation after 5' ' attempts') tri = Delaunay(points, qhull_options='QJ') @@ -1039,7 +1039,7 @@ class Polygon(CompositeSurface): # included in the triangulation, break it into two line segments. n = len(points) new_pts = [] - for i, j in zip(range(n), range(1, n +1)): + for i, j in zip(range(n), range(1, n + 1)): # If both vertices of any edge are not found in any simplex, insert # a new point between them. if not any([i in s and j % n in s for s in tri.simplices]): @@ -1077,7 +1077,8 @@ class Polygon(CompositeSurface): return group # If group is empty, grab the next simplex in the dictionary and recurse if group is None: - sidx = next(iter(neighbor_map)) + # Start with smallest neighbor lists + sidx = sorted(neighbor_map.items(), key=lambda item: len(item[1]))[0][0] return self._group_simplices(neighbor_map, group=[sidx]) # Otherwise use the last simplex in the group else: @@ -1087,13 +1088,16 @@ class Polygon(CompositeSurface): # For each neighbor check if it is part of the same convex # hull as the rest of the group. If yes, recurse. If no, continue on. for n in neighbors: - if n in group or neighbor_map.get(n, None) is None: + if n in group or neighbor_map.get(n) is None: continue test_group = group + [n] test_point_idx = np.unique(self._tri.simplices[test_group, :]) test_points = self.points[test_point_idx] - # If test_points are convex keep adding to this group - if len(test_points) == len(ConvexHull(test_points).vertices): + test_hull = ConvexHull(test_points, qhull_options='Qc') + pts_on_hull = len(test_hull.vertices) + len(test_hull.coplanar) + # If test_points are convex (including coplanar) keep adding to + # this group + if len(test_points) == pts_on_hull: group = self._group_simplices(neighbor_map, group=test_group) return group diff --git a/tests/unit_tests/test_surface_composite.py b/tests/unit_tests/test_surface_composite.py index 73519c383..19221212e 100644 --- a/tests/unit_tests/test_surface_composite.py +++ b/tests/unit_tests/test_surface_composite.py @@ -399,6 +399,32 @@ def test_polygon(): with pytest.raises(ValueError): openmc.model.Polygon(rz_points) + # Test "M" shaped polygon + points = np.array([[8.5151581, -17.988337], + [10.381711000000001, -17.988337], + [12.744357, -24.288728000000003], + [15.119406000000001, -17.988337], + [16.985959, -17.988337], + [16.985959, -27.246687], + [15.764328, -27.246687], + [15.764328, -19.116951], + [13.376877, -25.466951], + [12.118039, -25.466951], + [9.7305877, -19.116951], + [9.7305877, -27.246687], + [8.5151581, -27.246687]]) + + # Test points inside and outside by using offset method + m_polygon = openmc.model.Polygon(points, basis='xz') + inner_pts = m_polygon.offset(-0.1).points + assert all([(pt[0], 0, pt[1]) in -m_polygon for pt in inner_pts]) + outer_pts = m_polygon.offset(0.1).points + assert all([(pt[0], 0, pt[1]) in +m_polygon for pt in outer_pts]) + + # Offset of -0.2 will cause self-intersection + with pytest.raises(ValueError): + m_polygon.offset(-0.2) + @pytest.mark.parametrize("axis", ["x", "y", "z"]) def test_cruciform_prism(axis): From 70ba23a3672740db9e92fe3fc7cd6d8628edd036 Mon Sep 17 00:00:00 2001 From: Hunter Belanger Date: Mon, 8 Apr 2024 10:08:18 -0400 Subject: [PATCH 074/671] Update xtl and xtensor submodules (#2941) --- vendor/xtensor | 2 +- vendor/xtl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/vendor/xtensor b/vendor/xtensor index 31acec1e9..3634f2ded 160000 --- a/vendor/xtensor +++ b/vendor/xtensor @@ -1 +1 @@ -Subproject commit 31acec1e90bbea6d4bc17af0710a123bd5da6689 +Subproject commit 3634f2ded19e0cf38208c8b86cea9e1d7c8e397d diff --git a/vendor/xtl b/vendor/xtl index c19750fb1..a7c1c5444 160000 --- a/vendor/xtl +++ b/vendor/xtl @@ -1 +1 @@ -Subproject commit c19750fb1488369dc41f6069bc2b8446fc093e75 +Subproject commit a7c1c5444dfc57f76620391af4c94785ff82c8d6 From fea90e0926be94627b67f9d9d2500d0def7d17ce Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Mon, 8 Apr 2024 16:08:15 +0100 Subject: [PATCH 075/671] updated package versions in Dockerfile (#2946) --- Dockerfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 0b4742ada..107df4afe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,21 +34,21 @@ ARG build_libmesh ENV HOME=/root # Embree variables -ENV EMBREE_TAG='v3.12.2' +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.3.0' +ENV MOAB_TAG='5.5.1' ENV MOAB_REPO='https://bitbucket.org/fathomteam/moab/' # Double-Down variables -ENV DD_TAG='v1.0.0' +ENV DD_TAG='v1.1.0' ENV DD_REPO='https://github.com/pshriwise/double-down' ENV DD_INSTALL_DIR=$HOME/Double_down # DAGMC variables -ENV DAGMC_BRANCH='v3.2.1' +ENV DAGMC_BRANCH='v3.2.3' ENV DAGMC_REPO='https://github.com/svalinn/DAGMC' ENV DAGMC_INSTALL_DIR=$HOME/DAGMC/ From 569c087597adf873a4869d7785884b94f539b543 Mon Sep 17 00:00:00 2001 From: Lorenzo Chierici Date: Tue, 9 Apr 2024 21:26:03 +0200 Subject: [PATCH 076/671] Depletion restart with mpi (#2778) Co-authored-by: Paul Romano --- openmc/deplete/coupled_operator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmc/deplete/coupled_operator.py b/openmc/deplete/coupled_operator.py index 856a26958..8d43e2b2c 100644 --- a/openmc/deplete/coupled_operator.py +++ b/openmc/deplete/coupled_operator.py @@ -291,7 +291,7 @@ class CoupledOperator(OpenMCOperator): # on this process if comm.size != 1: prev_results = self.prev_res - self.prev_res = Results() + self.prev_res = Results(filename=None) mat_indexes = _distribute(range(len(self.burnable_mats))) for res_obj in prev_results: new_res = res_obj.distribute(self.local_mats, mat_indexes) From 256150f5675f4d1c61f9c07b5e29cdb8293cffc0 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 11 Apr 2024 15:00:41 +0200 Subject: [PATCH 077/671] Ensure that two surfaces with different boundary type are not considered redundant (#2942) --- openmc/geometry.py | 2 +- tests/unit_tests/test_geometry.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/openmc/geometry.py b/openmc/geometry.py index 8815f2c83..175cef2bf 100644 --- a/openmc/geometry.py +++ b/openmc/geometry.py @@ -701,7 +701,7 @@ class Geometry: coeffs = tuple(round(surf._coefficients[k], self.surface_precision) for k in surf._coeff_keys) - key = (surf._type,) + coeffs + key = (surf._type, surf._boundary_type) + coeffs redundancies[key].append(surf) redundant_surfaces = {replace.id: keep diff --git a/tests/unit_tests/test_geometry.py b/tests/unit_tests/test_geometry.py index f5a1bd7db..6cc577c82 100644 --- a/tests/unit_tests/test_geometry.py +++ b/tests/unit_tests/test_geometry.py @@ -390,3 +390,16 @@ def test_get_all_nuclides(): c2 = openmc.Cell(fill=m2, region=+s) geom = openmc.Geometry([c1, c2]) assert geom.get_all_nuclides() == ['Be9', 'Fe56'] + + +def test_redundant_surfaces(): + # Make sure boundary condition is accounted for + s1 = openmc.Sphere(r=5.0) + s2 = openmc.Sphere(r=5.0, boundary_type="vacuum") + c1 = openmc.Cell(region=-s1) + c2 = openmc.Cell(region=+s1) + u_lower = openmc.Universe(cells=[c1, c2]) + c3 = openmc.Cell(fill=u_lower, region=-s2) + geom = openmc.Geometry([c3]) + redundant_surfs = geom.remove_redundant_surfaces() + assert len(redundant_surfs) == 0 From 26280bc9d514568d7439885f02267aa40176b710 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Thu, 11 Apr 2024 08:10:45 -0500 Subject: [PATCH 078/671] Add MPI calls to DAGMC external test. (#2948) --- tests/regression_tests/dagmc/external/main.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/regression_tests/dagmc/external/main.cpp b/tests/regression_tests/dagmc/external/main.cpp index bebe088f6..3765cf79a 100644 --- a/tests/regression_tests/dagmc/external/main.cpp +++ b/tests/regression_tests/dagmc/external/main.cpp @@ -5,6 +5,7 @@ #include "openmc/geometry.h" #include "openmc/geometry_aux.h" #include "openmc/material.h" +#include "openmc/message_passing.h" #include "openmc/nuclide.h" #include @@ -14,7 +15,12 @@ int main(int argc, char* argv[]) int openmc_err; // Initialise OpenMC +#ifdef OPENMC_MPI + MPI_Comm world = MPI_COMM_WORLD; + openmc_err = openmc_init(argc, argv, &world); +#else openmc_err = openmc_init(argc, argv, nullptr); +#endif if (openmc_err == -1) { // This happens for the -h and -v flags return EXIT_SUCCESS; @@ -104,5 +110,9 @@ int main(int argc, char* argv[]) if (openmc_err) fatal_error(openmc_err_msg); +#ifdef OPENMC_MPI + MPI_Finalize(); +#endif + return EXIT_SUCCESS; } From cfebe16127c0f6636d0370ac141df8a769bebded Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Thu, 11 Apr 2024 16:35:29 +0100 Subject: [PATCH 079/671] added check for length of value passed into EnergyFilter (#2887) --- openmc/filter.py | 4 ++++ tests/unit_tests/test_filters.py | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/openmc/filter.py b/openmc/filter.py index 04b30261d..c1d86126a 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -1377,6 +1377,10 @@ class EnergyFilter(RealFilter): """ units = 'eV' + def __init__(self, values, filter_id=None): + cv.check_length('values', values, 2) + super().__init__(values, filter_id) + def get_bin_index(self, filter_bin): # Use lower energy bound to find index for RealFilters deltas = np.abs(self.bins[:, 1] - filter_bin[1]) / filter_bin[1] diff --git a/tests/unit_tests/test_filters.py b/tests/unit_tests/test_filters.py index 4ace59e72..55bb62075 100644 --- a/tests/unit_tests/test_filters.py +++ b/tests/unit_tests/test_filters.py @@ -1,6 +1,6 @@ import numpy as np import openmc -from pytest import fixture, approx +from pytest import fixture, approx, raises @fixture(scope='module') @@ -248,6 +248,11 @@ def test_energy(): assert len(f.values) == 710 +def test_energyfilter_error_handling(): + with raises(ValueError): + openmc.EnergyFilter([1e6]) + + def test_lethargy_bin_width(): f = openmc.EnergyFilter.from_group_structure('VITAMIN-J-175') assert len(f.lethargy_bin_width) == 175 From 9fd096b84338fe468c848b6c7bb992374c8ff072 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Thu, 11 Apr 2024 12:09:23 -0500 Subject: [PATCH 080/671] Add a `max_events` setting. (#2945) Co-authored-by: Paul Romano --- docs/source/io_formats/settings.rst | 22 +++++++++++++++------- include/openmc/particle_data.h | 3 --- include/openmc/settings.h | 4 ++-- openmc/settings.py | 27 +++++++++++++++++++++++++++ src/finalize.cpp | 1 + src/particle.cpp | 2 +- src/settings.cpp | 7 +++++++ tests/unit_tests/test_settings.py | 9 ++++++--- 8 files changed, 59 insertions(+), 16 deletions(-) diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index da71e4e60..9a339fb47 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -252,9 +252,9 @@ to false. *Default*: true ----------------------------------------- +------------------------------------- ```` Element ----------------------------------------- +------------------------------------- This element indicates the number of neutrons to run in flight concurrently when using event-based parallelism. A higher value uses more memory, but @@ -262,9 +262,17 @@ may be more efficient computationally. *Default*: 100000 ---------------------------- +--------------------------------- +```` Element +--------------------------------- + +This element indicates the maximum number of events a particle can undergo. + + *Default*: 1000000 + +----------------------- ```` Element ---------------------------- +----------------------- The ```` element allows the user to set a maximum scattering order to apply to every nuclide/material in the problem. That is, if the data @@ -276,11 +284,11 @@ then, OpenMC will only use up to the :math:`P_1` data. .. note:: This element is not used in the continuous-energy :ref:`energy_mode`. ---------------------------- +------------------------ ```` Element ---------------------------- +------------------------ -The ```` element indicates the number of times a particle can split during a history. +The ```` element indicates the number of times a particle can split during a history. *Default*: 1000 diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index cceab1d11..5c765e2e6 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -27,9 +27,6 @@ constexpr int MAX_DELAYED_GROUPS {8}; constexpr double CACHE_INVALID {-1.0}; -// Maximum number of collisions/crossings -constexpr int MAX_EVENTS {1000000}; - //========================================================================== // Aliases and type definitions diff --git a/include/openmc/settings.h b/include/openmc/settings.h index 69a8d7d13..5e60009bc 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -93,8 +93,8 @@ extern "C" int32_t gen_per_batch; //!< number of generations per batch extern "C" int64_t n_particles; //!< number of particles per generation extern int64_t - max_particles_in_flight; //!< Max num. event-based particles in flight - + max_particles_in_flight; //!< Max num. event-based particles in flight +extern int max_particle_events; //!< Maximum number of particle events extern ElectronTreatment electron_treatment; //!< how to treat secondary electrons extern array diff --git a/openmc/settings.py b/openmc/settings.py index a6d273901..828d5aef4 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -109,6 +109,10 @@ class Settings: parallelism. .. versionadded:: 0.12 + max_particle_events : int + Maximum number of allowed particle events per source particle. + + .. versionadded:: 0.14.1 max_order : None or int Maximum scattering order to apply globally when in multi-group mode. max_splits : int @@ -334,6 +338,7 @@ class Settings: self._event_based = None self._max_particles_in_flight = None + self._max_particle_events = None self._write_initial_source = None self._weight_windows = cv.CheckedList(WeightWindows, 'weight windows') self._weight_window_generators = cv.CheckedList(WeightWindowGenerator, 'weight window generators') @@ -938,6 +943,16 @@ class Settings: cv.check_greater_than('max particles in flight', value, 0) self._max_particles_in_flight = value + @property + def max_particle_events(self) -> int: + return self._max_particle_events + + @max_particle_events.setter + def max_particle_events(self, value: int): + cv.check_type('max particle events', value, Integral) + cv.check_greater_than('max particle events', value, 0) + self._max_particle_events = value + @property def write_initial_source(self) -> bool: return self._write_initial_source @@ -1341,6 +1356,11 @@ class Settings: elem = ET.SubElement(root, "max_particles_in_flight") elem.text = str(self._max_particles_in_flight).lower() + def _create_max_events_subelement(self, root): + if self._max_particle_events is not None: + elem = ET.SubElement(root, "max_particle_events") + elem.text = str(self._max_particle_events).lower() + def _create_material_cell_offsets_subelement(self, root): if self._material_cell_offsets is not None: elem = ET.SubElement(root, "material_cell_offsets") @@ -1719,6 +1739,11 @@ class Settings: if text is not None: self.max_particles_in_flight = int(text) + def _max_particle_events_from_xml_element(self, root): + text = get_text(root, 'max_particle_events') + if text is not None: + self.max_particle_events = int(text) + def _material_cell_offsets_from_xml_element(self, root): text = get_text(root, 'material_cell_offsets') if text is not None: @@ -1820,6 +1845,7 @@ class Settings: self._create_delayed_photon_scaling_subelement(element) self._create_event_based_subelement(element) self._create_max_particles_in_flight_subelement(element) + self._create_max_events_subelement(element) self._create_material_cell_offsets_subelement(element) self._create_log_grid_bins_subelement(element) self._create_write_initial_source_subelement(element) @@ -1923,6 +1949,7 @@ class Settings: settings._delayed_photon_scaling_from_xml_element(elem) settings._event_based_from_xml_element(elem) settings._max_particles_in_flight_from_xml_element(elem) + settings._max_particle_events_from_xml_element(elem) settings._material_cell_offsets_from_xml_element(elem) settings._log_grid_bins_from_xml_element(elem) settings._write_initial_source_from_xml_element(elem) diff --git a/src/finalize.cpp b/src/finalize.cpp index a41c2f783..26efc9723 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -91,6 +91,7 @@ int openmc_finalize() settings::max_lost_particles = 10; settings::max_order = 0; settings::max_particles_in_flight = 100000; + settings::max_particle_events = 1000000; settings::max_splits = 1000; settings::max_tracks = 1000; settings::max_write_lost_particles = -1; diff --git a/src/particle.cpp b/src/particle.cpp index c2e340201..a91113c61 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -381,7 +381,7 @@ void Particle::event_revive_from_secondary() { // If particle has too many events, display warning and kill it ++n_event(); - if (n_event() == MAX_EVENTS) { + if (n_event() == settings::max_particle_events) { warning("Particle " + std::to_string(id()) + " underwent maximum number of events."); wgt() = 0.0; diff --git a/src/settings.cpp b/src/settings.cpp index a5256c9c2..9f183a6c1 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -96,6 +96,7 @@ int32_t gen_per_batch {1}; int64_t n_particles {-1}; int64_t max_particles_in_flight {100000}; +int max_particle_events {1000000}; ElectronTreatment electron_treatment {ElectronTreatment::TTB}; array energy_cutoff {0.0, 1000.0, 0.0, 0.0}; @@ -156,6 +157,12 @@ void get_run_parameters(pugi::xml_node node_base) std::stoll(get_node_value(node_base, "max_particles_in_flight")); } + // Get maximum number of events allowed per particle + if (check_for_node(node_base, "max_particle_events")) { + max_particle_events = + std::stoll(get_node_value(node_base, "max_particle_events")); + } + // Get number of basic batches if (check_for_node(node_base, "batches")) { n_batches = std::stoi(get_node_value(node_base, "batches")); diff --git a/tests/unit_tests/test_settings.py b/tests/unit_tests/test_settings.py index b1737c461..30930ecdf 100644 --- a/tests/unit_tests/test_settings.py +++ b/tests/unit_tests/test_settings.py @@ -27,8 +27,8 @@ def test_export_to_xml(run_in_tmpdir): s.survival_biasing = True s.cutoff = {'weight': 0.25, 'weight_avg': 0.5, 'energy_neutron': 1.0e-5, 'energy_photon': 1000.0, 'energy_electron': 1.0e-5, - 'energy_positron': 1.0e-5, 'time_neutron': 1.0e-5, - 'time_photon': 1.0e-5, 'time_electron': 1.0e-5, + 'energy_positron': 1.0e-5, 'time_neutron': 1.0e-5, + 'time_photon': 1.0e-5, 'time_electron': 1.0e-5, 'time_positron': 1.0e-5} mesh = openmc.RegularMesh() mesh.lower_left = (-10., -10., -10.) @@ -59,6 +59,8 @@ def test_export_to_xml(run_in_tmpdir): s.write_initial_source = True s.weight_window_checkpoints = {'surface': True, 'collision': False} + s.max_particle_events = 100 + # Make sure exporting XML works s.export_to_xml() @@ -92,7 +94,7 @@ def test_export_to_xml(run_in_tmpdir): assert s.cutoff == {'weight': 0.25, 'weight_avg': 0.5, 'energy_neutron': 1.0e-5, 'energy_photon': 1000.0, 'energy_electron': 1.0e-5, 'energy_positron': 1.0e-5, - 'time_neutron': 1.0e-5, 'time_photon': 1.0e-5, + 'time_neutron': 1.0e-5, 'time_photon': 1.0e-5, 'time_electron': 1.0e-5, 'time_positron': 1.0e-5} assert isinstance(s.entropy_mesh, openmc.RegularMesh) assert s.entropy_mesh.lower_left == [-10., -10., -10.] @@ -128,3 +130,4 @@ def test_export_to_xml(run_in_tmpdir): assert vol.lower_left == (-10., -10., -10.) assert vol.upper_right == (10., 10., 10.) assert s.weight_window_checkpoints == {'surface': True, 'collision': False} + assert s.max_particle_events == 100 From 4ba053ca472a983a773abf706979455e2bf79a43 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Fri, 12 Apr 2024 11:03:19 -0500 Subject: [PATCH 081/671] Generate Region Plots Directly (#2895) Co-authored-by: Paul Romano --- openmc/cell.py | 6 ++-- openmc/region.py | 55 +++++++++++++++++++++++++++++++++ tests/unit_tests/test_region.py | 10 ++++++ 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/openmc/cell.py b/openmc/cell.py index 998640930..6de8eadec 100644 --- a/openmc/cell.py +++ b/openmc/cell.py @@ -594,7 +594,7 @@ class Cell(IDManagerMixin): # Make water blue water = openmc.Cell(fill=h2o) - universe.plot(..., colors={water: (0., 0., 1.)) + water.plot(colors={water: (0., 0., 1.)}) seed : int Seed for the random number generator openmc_exec : str @@ -619,8 +619,10 @@ class Cell(IDManagerMixin): """ # Create dummy universe but preserve used_ids - u = openmc.Universe(cells=[self], universe_id=openmc.Universe.next_id + 1) + next_id = openmc.Universe.next_id + u = openmc.Universe(cells=[self]) openmc.Universe.used_ids.remove(u.id) + openmc.Universe.next_id = next_id return u.plot(*args, **kwargs) def create_xml_subelement(self, xml_element, memo=None): diff --git a/openmc/region.py b/openmc/region.py index 534ee9b82..d3c03b898 100644 --- a/openmc/region.py +++ b/openmc/region.py @@ -1,9 +1,11 @@ from abc import ABC, abstractmethod from collections.abc import MutableSequence from copy import deepcopy +import warnings import numpy as np +import openmc from .bounding_box import BoundingBox @@ -334,6 +336,59 @@ class Region(ABC): return type(self)(n.rotate(rotation, pivot=pivot, order=order, inplace=inplace, memo=memo) for n in self) + def plot(self, *args, **kwargs): + """Display a slice plot of the region. + + .. versionadded:: 0.14.1 + + Parameters + ---------- + origin : iterable of float + Coordinates at the origin of the plot. If left as None then the + bounding box center will be used to attempt to ascertain the origin. + Defaults to (0, 0, 0) if the bounding box is not finite + width : iterable of float + Width of the plot in each basis direction. If left as none then the + bounding box width will be used to attempt to ascertain the plot + width. Defaults to (10, 10) if the bounding box is not finite + pixels : Iterable of int or int + If iterable of ints provided, then this directly sets the number of + pixels to use in each basis direction. If int provided, then this + sets the total number of pixels in the plot and the number of pixels + in each basis direction is calculated from this total and the image + aspect ratio. + basis : {'xy', 'xz', 'yz'} + The basis directions for the plot + seed : int + Seed for the random number generator + openmc_exec : str + Path to OpenMC executable. + axes : matplotlib.Axes + Axes to draw to + outline : bool + Whether outlines between color boundaries should be drawn + axis_units : {'km', 'm', 'cm', 'mm'} + Units used on the plot axis + **kwargs + Keyword arguments passed to :func:`matplotlib.pyplot.imshow` + + Returns + ------- + matplotlib.axes.Axes + Axes containing resulting image + + """ + for key in ('color_by', 'colors', 'legend', 'legend_kwargs'): + if key in kwargs: + warnings.warn(f"The '{key}' argument is present but won't be applied in a region plot") + + # Create cell while not perturbing use of autogenerated IDs + next_id = openmc.Cell.next_id + c = openmc.Cell(region=self) + openmc.Cell.used_ids.remove(c.id) + openmc.Cell.next_id = next_id + return c.plot(*args, **kwargs) + class Intersection(Region, MutableSequence): r"""Intersection of two or more regions. diff --git a/tests/unit_tests/test_region.py b/tests/unit_tests/test_region.py index 3197a8d72..8c9e0afe4 100644 --- a/tests/unit_tests/test_region.py +++ b/tests/unit_tests/test_region.py @@ -241,3 +241,13 @@ def test_invalid_operands(): with pytest.raises(ValueError, match='must be of type Region'): openmc.Complement(z) + +def test_plot(): + # Create region and plot + region = -openmc.Sphere() & +openmc.XPlane() + c_before = openmc.Cell() + region.plot() + + # Ensure that calling plot doesn't affect cell ID space + c_after = openmc.Cell() + assert c_after.id - 1 == c_before.id From e77a5247b67a230f4fa7e9294f460e6d43f16cde Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Fri, 12 Apr 2024 12:14:13 -0500 Subject: [PATCH 082/671] Support UnstructuredMesh for IndependentSource (#2949) Co-authored-by: Jonathan Shimwell Co-authored-by: Paul Romano --- openmc/filter.py | 8 ++-- openmc/mesh.py | 42 ++++++++++++++++++- openmc/source.py | 10 +++-- tests/unit_tests/test_mesh.py | 19 +++++++++ tests/unit_tests/test_source_mesh.py | 63 +++++++++++++++++++++++----- 5 files changed, 123 insertions(+), 19 deletions(-) diff --git a/openmc/filter.py b/openmc/filter.py index c1d86126a..66d832c69 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -864,10 +864,10 @@ class MeshFilter(Filter): cv.check_type('filter mesh', mesh, openmc.MeshBase) self._mesh = mesh if isinstance(mesh, openmc.UnstructuredMesh): - if mesh.volumes is None: - self.bins = [] - else: + if mesh.has_statepoint_data: self.bins = list(range(len(mesh.volumes))) + else: + self.bins = [] else: self.bins = list(mesh.indices) @@ -982,7 +982,7 @@ class MeshFilter(Filter): if translation: out.translation = [float(x) for x in translation.split()] return out - + class MeshBornFilter(MeshFilter): """Filter events by the mesh cell a particle originated from. diff --git a/openmc/mesh.py b/openmc/mesh.py index a12bd5853..280f447fa 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -3,6 +3,7 @@ import typing import warnings from abc import ABC, abstractmethod from collections.abc import Iterable +from functools import wraps from math import pi, sqrt, atan2 from numbers import Integral, Real from pathlib import Path @@ -39,7 +40,8 @@ class MeshBase(IDManagerMixin, ABC): bounding_box : openmc.BoundingBox Axis-aligned bounding box of the mesh as defined by the upper-right and lower-left coordinates. - + indices : Iterable of tuple + An iterable of mesh indices for each mesh element, e.g. [(1, 1, 1), (2, 1, 1), ...] """ next_id = 1 @@ -66,6 +68,11 @@ class MeshBase(IDManagerMixin, ABC): def bounding_box(self) -> openmc.BoundingBox: return openmc.BoundingBox(self.lower_left, self.upper_right) + @property + @abstractmethod + def indices(self): + pass + def __repr__(self): string = type(self).__name__ + '\n' string += '{0: <16}{1}{2}\n'.format('\tID', '=\t', self._id) @@ -1914,6 +1921,17 @@ class SphericalMesh(StructuredMesh): return arr +def require_statepoint_data(func): + @wraps(func) + def wrapper(self: UnstructuredMesh, *args, **kwargs): + if not self._has_statepoint_data: + raise AttributeError(f'The "{func.__name__}" property requires ' + 'information about this mesh to be loaded ' + 'from a statepoint file.') + return func(self, *args, **kwargs) + return wrapper + + class UnstructuredMesh(MeshBase): """A 3D unstructured mesh @@ -1990,6 +2008,7 @@ class UnstructuredMesh(MeshBase): self.library = library self._output = False self.length_multiplier = length_multiplier + self._has_statepoint_data = False @property def filename(self): @@ -2010,6 +2029,7 @@ class UnstructuredMesh(MeshBase): self._library = lib @property + @require_statepoint_data def size(self): return self._size @@ -2028,6 +2048,7 @@ class UnstructuredMesh(MeshBase): self._output = val @property + @require_statepoint_data def volumes(self): """Return Volumes for every mesh cell if populated by a StatePoint file @@ -2046,26 +2067,32 @@ class UnstructuredMesh(MeshBase): self._volumes = volumes @property + @require_statepoint_data def total_volume(self): return np.sum(self.volumes) @property + @require_statepoint_data def vertices(self): return self._vertices @property + @require_statepoint_data def connectivity(self): return self._connectivity @property + @require_statepoint_data def element_types(self): return self._element_types @property + @require_statepoint_data def centroids(self): return np.array([self.centroid(i) for i in range(self.n_elements)]) @property + @require_statepoint_data def n_elements(self): if self._n_elements is None: raise RuntimeError("No information about this mesh has " @@ -2096,6 +2123,15 @@ class UnstructuredMesh(MeshBase): def n_dimension(self): return 3 + @property + @require_statepoint_data + def indices(self): + return [(i,) for i in range(self.n_elements)] + + @property + def has_statepoint_data(self) -> bool: + return self._has_statepoint_data + def __repr__(self): string = super().__repr__() string += '{: <16}=\t{}\n'.format('\tFilename', self.filename) @@ -2106,13 +2142,16 @@ class UnstructuredMesh(MeshBase): return string @property + @require_statepoint_data def lower_left(self): return self.vertices.min(axis=0) @property + @require_statepoint_data def upper_right(self): return self.vertices.max(axis=0) + @require_statepoint_data def centroid(self, bin: int): """Return the vertex averaged centroid of an element @@ -2257,6 +2296,7 @@ class UnstructuredMesh(MeshBase): library = group['library'][()].decode() mesh = cls(filename=filename, library=library, mesh_id=mesh_id) + mesh._has_statepoint_data = True vol_data = group['volumes'][()] mesh.volumes = np.reshape(vol_data, (vol_data.shape[0],)) mesh.n_elements = mesh.volumes.size diff --git a/openmc/source.py b/openmc/source.py index 5942a3107..a77ca8b04 100644 --- a/openmc/source.py +++ b/openmc/source.py @@ -500,9 +500,13 @@ class MeshSource(SourceBase): elem.set("mesh", str(self.mesh.id)) # write in the order of mesh indices - for idx in self.mesh.indices: - idx = tuple(i - 1 for i in idx) - elem.append(self.sources[idx].to_xml_element()) + if isinstance(self.mesh, openmc.UnstructuredMesh): + for s in self.sources: + elem.append(s.to_xml_element()) + else: + for idx in self.mesh.indices: + idx = tuple(i - 1 for i in idx) + elem.append(self.sources[idx].to_xml_element()) @classmethod def from_xml_element(cls, elem: ET.Element, meshes) -> openmc.MeshSource: diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index 50e1236a8..d50d2968e 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -298,3 +298,22 @@ def test_CylindricalMesh_get_indices_at_coords(): assert mesh.get_indices_at_coords([98, 200.1, 299]) == (0, 1, 0) # second angle quadrant assert mesh.get_indices_at_coords([98, 199.9, 299]) == (0, 2, 0) # third angle quadrant assert mesh.get_indices_at_coords([102, 199.1, 299]) == (0, 3, 0) # forth angle quadrant + +def test_umesh_roundtrip(run_in_tmpdir, request): + umesh = openmc.UnstructuredMesh(request.path.parent / 'test_mesh_tets.e', 'moab') + umesh.output = True + + # create a tally using this mesh + mf = openmc.MeshFilter(umesh) + tally = openmc.Tally() + tally.filters = [mf] + tally.scores = ['flux'] + + tallies = openmc.Tallies([tally]) + tallies.export_to_xml() + + xml_tallies = openmc.Tallies.from_xml() + xml_tally = xml_tallies[0] + xml_mesh = xml_tally.filters[0].mesh + + assert umesh.id == xml_mesh.id diff --git a/tests/unit_tests/test_source_mesh.py b/tests/unit_tests/test_source_mesh.py index 7bec2d072..f0813d2f9 100644 --- a/tests/unit_tests/test_source_mesh.py +++ b/tests/unit_tests/test_source_mesh.py @@ -208,23 +208,29 @@ def test_roundtrip(run_in_tmpdir, model, request): ################### # MeshSource tests ################### -@pytest.mark.parametrize('mesh_type', ('rectangular', 'cylindrical')) -def test_mesh_source_independent(run_in_tmpdir, mesh_type): +@pytest.fixture +def void_model(): """ A void model containing a single box """ - min, max = -10, 10 - box = openmc.model.RectangularParallelepiped( - min, max, min, max, min, max, boundary_type='vacuum') + model = openmc.Model() - geometry = openmc.Geometry([openmc.Cell(region=-box)]) + box = openmc.model.RectangularParallelepiped(*[-10, 10]*3, boundary_type='vacuum') + model.geometry = openmc.Geometry([openmc.Cell(region=-box)]) - settings = openmc.Settings() - settings.particles = 100 - settings.batches = 10 - settings.run_mode = 'fixed source' + model.settings.particles = 100 + model.settings.batches = 10 + model.settings.run_mode = 'fixed source' - model = openmc.Model(geometry=geometry, settings=settings) + return model + + +@pytest.mark.parametrize('mesh_type', ('rectangular', 'cylindrical')) +def test_mesh_source_independent(run_in_tmpdir, void_model, mesh_type): + """ + A void model containing a single box + """ + model = void_model # define a 2 x 2 x 2 mesh if mesh_type == 'rectangular': @@ -310,6 +316,41 @@ def test_mesh_source_independent(run_in_tmpdir, mesh_type): assert mesh_source.strength == 1.0 +@pytest.mark.parametrize("library", ('moab', 'libmesh')) +def test_umesh_source_independent(run_in_tmpdir, request, void_model, library): + import openmc.lib + # skip the test if the library is not enabled + if library == 'moab' and not openmc.lib._dagmc_enabled(): + pytest.skip("DAGMC (and MOAB) mesh not enabled in this build.") + + if library == 'libmesh' and not openmc.lib._libmesh_enabled(): + pytest.skip("LibMesh is not enabled in this build.") + + model = void_model + + mesh_filename = Path(request.fspath).parent / "test_mesh_tets.e" + uscd_mesh = openmc.UnstructuredMesh(mesh_filename, library) + ind_source = openmc.IndependentSource() + n_elements = 12_000 + model.settings.source = openmc.MeshSource(uscd_mesh, n_elements*[ind_source]) + model.export_to_model_xml() + try: + openmc.lib.init() + openmc.lib.simulation_init() + sites = openmc.lib.sample_external_source(10) + openmc.lib.statepoint_write('statepoint.h5') + finally: + openmc.lib.finalize() + + with openmc.StatePoint('statepoint.h5') as sp: + uscd_mesh = sp.meshes[uscd_mesh.id] + + # ensure at least that all sites are inside the mesh + bounding_box = uscd_mesh.bounding_box + for site in sites: + assert site.r in bounding_box + + def test_mesh_source_file(run_in_tmpdir): # Creating a source file with a single particle source_particle = openmc.SourceParticle(time=10.0) From 2974d53b3c07dc1a822f2b31ecd27b6553cbd458 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 15 Apr 2024 09:25:32 -0500 Subject: [PATCH 083/671] Update minimum Python version to 3.8 (#2958) --- .github/workflows/ci.yml | 6 +++--- docs/source/devguide/styleguide.rst | 2 +- docs/source/usersguide/install.rst | 2 +- setup.py | 7 +++---- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99d2155fa..c5accc5e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,9 +35,6 @@ jobs: vectfit: [n] include: - - python-version: "3.7" - omp: n - mpi: n - python-version: "3.8" omp: n mpi: n @@ -47,6 +44,9 @@ jobs: - python-version: "3.11" omp: n mpi: n + - python-version: "3.12" + omp: n + mpi: n - dagmc: y python-version: "3.10" mpi: y diff --git a/docs/source/devguide/styleguide.rst b/docs/source/devguide/styleguide.rst index 9f6f82f06..3c71d14ad 100644 --- a/docs/source/devguide/styleguide.rst +++ b/docs/source/devguide/styleguide.rst @@ -146,7 +146,7 @@ Style for Python code should follow PEP8_. Docstrings for functions and methods should follow numpydoc_ style. -Python code should work with Python 3.7+. +Python code should work with Python 3.8+. Use of third-party Python packages should be limited to numpy_, scipy_, matplotlib_, pandas_, and h5py_. Use of other third-party packages must be diff --git a/docs/source/usersguide/install.rst b/docs/source/usersguide/install.rst index ab2cbe645..25329a77f 100644 --- a/docs/source/usersguide/install.rst +++ b/docs/source/usersguide/install.rst @@ -540,7 +540,7 @@ to install the Python package in :ref:`"editable" mode `. Prerequisites ------------- -The Python API works with Python 3.7+. In addition to Python itself, the API +The Python API works with Python 3.8+. In addition to Python itself, the API relies on a number of third-party packages. All prerequisites can be installed using Conda_ (recommended), pip_, or through the package manager in most Linux distributions. diff --git a/setup.py b/setup.py index eeb0b3f29..a33037ad3 100755 --- a/setup.py +++ b/setup.py @@ -53,19 +53,18 @@ kwargs = { 'Topic :: Scientific/Engineering' 'Programming Language :: C++', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', ], # Dependencies - 'python_requires': '>=3.7', + 'python_requires': '>=3.8', 'install_requires': [ 'numpy>=1.9', 'h5py', 'scipy', 'ipython', 'matplotlib', - 'pandas', 'lxml', 'uncertainties' + 'pandas', 'lxml', 'uncertainties', 'setuptools' ], 'extras_require': { 'depletion-mpi': ['mpi4py'], From d53155d234f681548534be5085383d423ecf88d6 Mon Sep 17 00:00:00 2001 From: Aidan Crilly <20111148+aidancrilly@users.noreply.github.com> Date: Thu, 18 Apr 2024 17:46:18 +0100 Subject: [PATCH 084/671] Added fix to cfloat_endf for length 11 endf floats (#2967) Co-authored-by: aidancrilly --- openmc/data/endf.c | 2 +- tests/unit_tests/test_endf.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/openmc/data/endf.c b/openmc/data/endf.c index 936fd3bbb..dd5eee541 100644 --- a/openmc/data/endf.c +++ b/openmc/data/endf.c @@ -14,7 +14,7 @@ double cfloat_endf(const char* buffer, int n) { - char arr[12]; // 11 characters plus a null terminator + char arr[13]; // 11 characters plus e and a null terminator int j = 0; // current position in arr int found_significand = 0; int found_exponent = 0; diff --git a/tests/unit_tests/test_endf.py b/tests/unit_tests/test_endf.py index 9e6970867..1d4982054 100644 --- a/tests/unit_tests/test_endf.py +++ b/tests/unit_tests/test_endf.py @@ -23,6 +23,7 @@ def test_float_endf(): assert endf.float_endf('-1.+2') == approx(-100.0) assert endf.float_endf(' ') == 0.0 assert endf.float_endf('9.876540000000000') == approx(9.87654) + assert endf.float_endf('-2.225002+6') == approx(-2.225002e+6) def test_int_endf(): From 5111aa2621ec74280ca9b762ccc149cde70c531d Mon Sep 17 00:00:00 2001 From: John Tramm Date: Thu, 18 Apr 2024 17:10:16 -0500 Subject: [PATCH 085/671] Random Ray Transport (#2823) Co-authored-by: Gavin Ridley Co-authored-by: Paul Romano --- CMakeLists.txt | 3 + docs/source/_images/2x2_fsrs.jpeg | Bin 0 -> 271045 bytes docs/source/_images/2x2_materials.jpeg | Bin 0 -> 59424 bytes docs/source/io_formats/settings.rst | 26 + docs/source/methods/index.rst | 1 + docs/source/methods/random_ray.rst | 804 ++++++++++++++++++ docs/source/usersguide/data.rst | 2 + docs/source/usersguide/index.rst | 2 + docs/source/usersguide/random_ray.rst | 480 +++++++++++ examples/pincell_random_ray/build_xml.py | 203 +++++ include/openmc/boundary_condition.h | 1 + include/openmc/cell.h | 36 + include/openmc/constants.h | 2 + include/openmc/mgxs.h | 3 +- include/openmc/output.h | 4 +- include/openmc/plot.h | 1 + .../openmc/random_ray/flat_source_domain.h | 141 +++ include/openmc/random_ray/random_ray.h | 55 ++ .../openmc/random_ray/random_ray_simulation.h | 59 ++ include/openmc/settings.h | 6 +- include/openmc/timer.h | 1 + openmc/settings.py | 90 +- src/boundary_condition.cpp | 14 +- src/eigenvalue.cpp | 23 +- src/geometry.cpp | 12 +- src/main.cpp | 11 +- src/mesh.cpp | 2 +- src/output.cpp | 3 +- src/random_ray/flat_source_domain.cpp | 686 +++++++++++++++ src/random_ray/random_ray.cpp | 289 +++++++ src/random_ray/random_ray_simulation.cpp | 397 +++++++++ src/settings.cpp | 42 + src/simulation.cpp | 28 +- src/source.cpp | 52 +- src/tallies/tally.cpp | 7 +- src/timer.cpp | 2 + .../random_ray_basic/__init__.py | 0 .../random_ray_basic/inputs_true.dat | 108 +++ .../random_ray_basic/results_true.dat | 171 ++++ .../regression_tests/random_ray_basic/test.py | 232 +++++ .../random_ray_vacuum/__init__.py | 0 .../random_ray_vacuum/inputs_true.dat | 108 +++ .../random_ray_vacuum/results_true.dat | 171 ++++ .../random_ray_vacuum/test.py | 235 +++++ tests/testing_harness.py | 57 ++ tests/unit_tests/test_settings.py | 11 + 46 files changed, 4512 insertions(+), 69 deletions(-) create mode 100644 docs/source/_images/2x2_fsrs.jpeg create mode 100644 docs/source/_images/2x2_materials.jpeg create mode 100644 docs/source/methods/random_ray.rst create mode 100644 docs/source/usersguide/random_ray.rst create mode 100644 examples/pincell_random_ray/build_xml.py create mode 100644 include/openmc/random_ray/flat_source_domain.h create mode 100644 include/openmc/random_ray/random_ray.h create mode 100644 include/openmc/random_ray/random_ray_simulation.h create mode 100644 src/random_ray/flat_source_domain.cpp create mode 100644 src/random_ray/random_ray.cpp create mode 100644 src/random_ray/random_ray_simulation.cpp create mode 100644 tests/regression_tests/random_ray_basic/__init__.py create mode 100644 tests/regression_tests/random_ray_basic/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_basic/results_true.dat create mode 100644 tests/regression_tests/random_ray_basic/test.py create mode 100644 tests/regression_tests/random_ray_vacuum/__init__.py create mode 100644 tests/regression_tests/random_ray_vacuum/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_vacuum/results_true.dat create mode 100644 tests/regression_tests/random_ray_vacuum/test.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 6922e6b89..3f87cbd7e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -380,6 +380,9 @@ list(APPEND libopenmc_SOURCES src/progress_bar.cpp src/random_dist.cpp src/random_lcg.cpp + src/random_ray/random_ray_simulation.cpp + src/random_ray/random_ray.cpp + src/random_ray/flat_source_domain.cpp src/reaction.cpp src/reaction_product.cpp src/scattdata.cpp diff --git a/docs/source/_images/2x2_fsrs.jpeg b/docs/source/_images/2x2_fsrs.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..1c8e474d0e8f7b37adeb6e832b7197f6458f071e GIT binary patch literal 271045 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

o)5qG>Rvezi1bqK8 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%0Arnvrsjl~`G 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%0AmZMe zM68)g!#Cr^&bKOr$Re0Xf{&j1Pr64|L$TgZMqcn$*?CC1(oPWm z0R|))<)xCV*WeE>d_ZTs0g1-K>OBT-FC&^*4`{@R1Mb-@;^JM})bK&cl|Dlo%FZcz zCA_Z(us)TO{hXmPqeUi+a4Y%xI%cQssT|6mm{nTr#X-T<&?JWm2Nz{=kz_3B$V&EA zaoj&o`ryBuG>LSiCh_Vbr!1cFUB|dI2vPU#Zbq-$`~vr!+xCQHha|wmEDh^);1Z(m zpxb;KvF|MsZy)d@{8}&kX=wNH!se7uj1*fK!e5yK4hLdkq|Xw=FhcMwzDGJJBx2v~ zRQvjoh)d0Hz)?Pl2y-?2oWIchK3`7!Bc^hcv7N_`nn6nzfEMLG zeMUhE$gl!b0=>yaPKpIZg;XR9uWCNBj?Jg1od3lQG}T9Jn=~DSki4oZ}s-zQX^f-OvB0 z-RtlCWdz1xuk_CD@x%x2J2%pBWWA1#r;~`S* z;x}^A>Qs){6;%{A4~ggJzsWnWLJAk{UFng$IpT#yiaJqk>@OAoaP4lmZVNHA1G@lV zBL|B=_16XA&^R)v-(-i(D*YxgX(quy%Xy&Lv;7y9ay;oKC-}MOnx8hTZaWsyv&PVHAs}E4>)Af?s zu~D^i>|yLz|DQ#;|EEP<)AIMn85q2W<{8^_rwL3eL6jQ+UZTQ-LQwaNK&jE;oANyU zxp;2cZ?>LUn>UbO{yOXyd_WT>H>W_OlH{7?3-?cIkuW71jx$Lc$+*m^xar)uqeCgP z>U$+#sFM|(#Rc#TV8dhkO9dAGp#n^xHqaq%+$;XxpN_f!q3`rG7`eAcue#`=nMH@5 zp9ukITaBxDqTPg;ZX^M@@$qS6Gr{0BSbc@QX!``7oGQapGn7`gc5^h}rNUCI)q8kV z#S1TaQB?Ou7KKpiU(vaI{g;8p}htL7f?6z1*(Z|3B|Hj$SM(qowAb*S~#!&Xg3Fa@W)E&u=m%wji|a#U8gH3WWrtvQ&z!QrpoA*?62 zE#5O%qE1Q0KC-|}7P-KNR&3_1zR{Rn3IXGRL5<>emfrVK?uuPO&hY3qdMWj$*bl_{ z4=C5c6>2;c#+cCDX-#?b)Hi|S+;OH)4lipuGz-Gr7im98jXft!oTtK96u!cY$CeyguA z=oC8ui64Fek)8Eu%i(Ikx^vVsp}W)L5F#IQ~f6>ph?`% zCqvX*)?I~KiIu;#3MvyZ^8(BP0Hp|*cfjRUZ}+NaF|)~@dSnPy&*7&xEZ(GGv|eJP zpk5{}khJ!ms6Rpc(bZk5t1OJxCVyO{G~tg4>Xa~8g#Ra1dnor5Z5YCw&Yg9&e^2|K z%E~=lCu;_4y>W}({^?Q|yWGjqlaz*sW<;At`k$+Az&8 zwO%p|47UW9oRdANAtrDWa@=xf!O6&z%fRMfS|r0~^txqpxR5sVyQ1uIg*{UHWmoao z1n!Mc524)?l_T2lbQdX^B4YF%Fg4>^eJkkMy=dh{9e(I_8%rr|1gHf7Jd{jOr0+*@ zMRG`H2?3G4W69wcULk?Nmm5OE!Ube^Usl?i5Xh*t`Hu*`B0EtU@dwu+TK-LCqG_#& zv`Lyota^-T7#jS6VTJd=Q6J;%R_6fyZFW9&5yk5p1q6Vp^+z{jrgSL062UafOqyj3%8tRpQ}Y7 zix51yBrcRHCD?54FEHq|4hBOCu-O!()1!q6DL9)W$vCHIeGV3loBP44aN<^bru?VU zJou(P+Ug)r_a7Cz1b{?26>#5;Z{5V*TI-cJDY+Iu|2lb{M)IQ85M4m)+ErZO0VOHW z5$yy9897T5bI?Vb-j!EQFCcs0=QY8;>>WPDU|)x(T}K!(B@w6k&!cmOGQZs@j}rN| z{x8d`U}KMjli+$vp?)9`YN_$)uXXf{k2l& zbGaFH23{yZ4`%O9;>Gjr4v-nl(uB8L>8(@A6UY} zSyc84U;j8QG|vRaisR#OmMyVq2_vjf|4I7({upIln2(!HoFxo)O~&_gZC^a^OLo7Q znkWb$0t`=WH5V(e>6S$p!U2R)`7d<{cs)dJa@Y)C!Y?_#j^_f!I||+sm_}gf+r~y4 z;W{^QgLvYUVJ*J_wEWQhwnU^YDO-SJnTL1Yg&V}zZ0_`m#eX%N6l)Q>@J+W(lfZeX zDfptB4S;Ue+mC9}bsD+$#^oMjy9>9)No5dBl>kL5{VhumNjl3&_~lAwK|~@oM&M5; zMm9nqr@E#{+$ug@yW;MRdE`wS*6pANd(9cnX>tQ~I&=mOay1uJFYi7YF6QbNW2*nQ z^IwcSG&=Sk!saAaj3GvpIm+Ulka6tlJt4_=Rulec}S7f4EbR>7-T{t zw5vyNu-8}WA}3<_A(E>C3G8Q8G?I&|hoWxaD%fKW?MZwii6qcK^s)Xce zTglbPd&&(9FL}UR)ipl|mM+uK|EtMjyOcZy3DRxCHJKKRhXh9!H)Z4VBW9`SfTsm` zmGToGiS5^!s^Tz=<_u@ghv$GLs@r%Jg6yyB$eKwh2qpie(_aV!95I+}%J&8Cf3>-6 z#ZrLKEXE(;KxoIN=I!08myH&``0;ns=S@9{ho3E?@7 zRvW4-EVN3aCi!`@dKl}zu%^`ZbLdhG0|P!Xv}=0XMRM3Li%P;>&f*jly278c=R-T2?~o)DLSh z%YJ&{>*o^@{AqxLBszV$lQyT+Zqk}qgzIaP8x+xzML|mfNu<#XKmF(p3upa$rbB8R zZ5UR!t<`cjDD z!2zm?G`_}8N=wh=Tq*9C8H<{3{9f{f5_OYco8w|qASAMVMMBqDV)Oghp(g6d-K3ifdNX!WkgdFp61@hGbUf@fgT3A+C^}FG38m=cl>WQ#8 z*q=?j&qn3j;H@o{2tQxU5{!S$NV%6;KCN1Xm`cbJSihW;^?)Va^hmQid%o%n+ zwUQ(6Tn&x!vju1Et`fz-C&1-`aCi1@7Zdr)ZffX4s*bn9>(x&lxrPQ7GABLne7D4R z;6=9Bnco0OD3B5+re)sQ_+b5to4yGM6(q?P-|NH;QnUBS1wBbeX!eGz4pKXLuO537 zza;}O-st653G-=}_L=%XA`#oH6~(2lqRI4plex?rz-bkS=)poQBDB`ig(8T$0T?oh zEgkY+S{$P4{yUz6;HX~eHW+LjykicS5EWzN=+`h`YkdT6(L!2i6K>pJ^B=&9+w!h6 zvb+{vBtP+canikv4i|>lfQKQ-AE2m56s0X@ly*)%bWlS4Mu>+gdptcBaX&QeKZIpX zh-9~Ey>2tHP;qrv48GBOeM94wb#6P;8)65&f+?1})qWe%zEI5&x2f#^)la1{Eh;9h z$KkqiTbS1vVM0H`&U8~9A&l}MS8K9z!rg^m8?~0{E~*!tZ<66!+*~rwb<#`~!XB(Kg+@KXpyP;} zG5Bm?pbJeun-HFa2iT#{yZaNHeRqVUn_V2?qAQzrA?(KP-S6KuPyCELCQ}3?e_D~h z=H}=LFPs{pzdFes_gj1epC7@hU*8^#j1M8A#%#(LL^$it2RD=c!ffMX5dI()kO{My z9X!(@<|(73xE0oqiZg`ulo6Or45iY}qUo1{2F2aY$Hn_Ygt9<=O#tKmkux`L+OzY0x zs9Sr$%GUUkw6X^)O87sG(Mor>B zy7zp2rOn_a2!A+-iiwoRgLFcT8nmZON-S!5+r}4KZ==E_S*M2gNfKH*6_cRA2*rxI zKgj$f5DCI{RGLfUv5tFC$CgHmyk4vJ%=M&V8)8{{#Hd|Lut1Fz3M+sT^o(Ggg^#7b zVp*7A)DBx`@QG2wiVn93vhR$~c`(P>0K6VoJ5yfJM8p&r-taT->C<-sys8?BuSI_gOrJKGJjZbrU#uQ%)WP9}d~MC+zD{ReTuG?;epc*Zj#o7{Cfmr_LW3 zdnWNOuSa}6y>g(o<=>$zQ5ma&UnB?{5g74tPH$z(%N!|LW%MyiEXfP)mPyuq`-{wG zzS}Aey3Gnkh#utv*=_~t81veO@l|Eq%GgfVz%at&Na-+<7z33_4E$yzB&N}0R4cf! zi|t3}iVHqm^fiBz%iFPzde-rVQEX^&{zfTj3ptVm&?Ltgh!Kf>6p?76y&bFFBiW`5 zz8&WESMBB5=`AocK};OdCN_P&Kih};7$m&$kzP0%a5{_kYMq+miO-gD^pC_kzPDm|rARfN62 zzSndWPG)nqt2re&l{q87$bp}wV$`BRq|C`69u&C6PR)TV)1VHxo*4o=zfrVf2EBzQK0C5pQA4@@*T)t=F{a)PY)6 zw(LA;I{iD`&p1GAUHp?OL@3}Jt1bjHr#F*(3o-+DHfBGXe*@g+6?NCtwpEh;DpsmI z)VEV%8x3FCCqbp9TNLVtdEDiK2D|Ol{Z|* zfkjG}RaYxZ?d-_)d#_owaXF61uZie@-20!!i;H+qKV6iK`FWj=buLMm7rK{z(u!0F zo)OV0$+pU1AYkTe15!9j~9g@})cWMl#XkXnxdTt<4c;lW#x zyho1(82H7JkruZmxYNvp3ZaDvFpprk*mF`YP~LH z7Ipm;XYH5c<+w{aS3Z*nBZ32Aq|>cSHX9t<13pz)BpzIc$bL$|up#fWf65XJijIks z7g%JT5qTGpmsTd>Gvub<{^VGSB>+pG8lzs+S-=#WK`OfV9Q14JXEbNJO`mD>jgDDWaigIa zDX@6wP3!`n%U5mUC!iDur!T=#k?KfiN7jKQbIyBlLl4#AdbE6UDPTkXHp{Yun%hGc z*qSXLTD{G3B(>)8$xSFq8^?8d0U*FZhh2&73sOI)_F3d1&!F@?T>!!QL5@UpLAL#j zfwb~)F2SpJDrwMuKz8F320%X$K#%uB40=E=2yUu1M|B%E4w5FuY={z3!t}UP3v7&P zKF_x}i903p;2-P+$@1>mjp{62noa+jKMVCiuyIE4EPXaV>N}@NIiW!$2K$>7ADP@$ z#9VI28L4FRasqOv$59M9l1#MChO~{4ZYa=ZBfv|*mQMg!rMju71I`wzDC!?hxi%iD z$})&CNMWc=hCB}~`Kx(-{U+GlwEg@>esGxSn5nm}HVrsRYSQ=>{+ZCCNhPF&Y|a`p zRduG)jYDQpY_B)rNwf%wS{fC5YCMSbe@coU#lgyTt7V-3^=;pw-S8(`Lv0}=3Se1F z&c_)9^a6$(oD39~SrzZTYphQ-y}0AS!tpJBQyHqIxIb4={F&3<6Vv{~#sx}j`w7B| zSq_Tdl^{f@ws~c;pw4(N4RLXQ17$;WtJcsDqJw`^w-1g+VdJ-)v~IV)>1iz zPZ=bygMv{GACsCqnG*=>hbovhvhNjSLAubl9NDFKri~~b9YjWg*gch^?JGE5NlvB2 z&>A3sF}TzNAyLB*`q-x`MM1=Gh!?YD@rXGh-NyDhBw>Xkv-Unrva_0>tix+QF^2IT z>uhmoOnW1PaG7pYa4zl@z}UFgEs`)jb5GxCinWj(T;CdNgUke3vxlC?I@^q?0&3<` zO;A(r+CY0svU#va`G2#ct01R#jh!E7k`R!iTGYp?XnjF;j7mm*DBV=Mtx*XgS;{aF zPvzA#F69HXkv2P-6_2!~0T&h6DKq450lh`*!uKfB zrhSu|l~93DEjZc!9wnQ9+lBUnL6>=11IFQIpId?5o)*57WOyds_;)}vh(RM{wj`>T zxnSeJsr{8KRuNVI+T42l(gc9!;#@2o61ZbU^b%Y$R|Uawqbai$kp7vdCq0ouJ`Dzf zVK?ILj3>5g@nJ|+@+hTO;gNCAxVIyw`49$jPc)XC+X_{;DLe(9uS0G)Qt){Yyi{dJ z#}@3g@kH*_PQJ$H=R%NsWYQ2a3l+<|l7O?iMG!n=Dpc9&Rynwjn za90~>9HC4PqHlBEYxOF=%E4c#|MF@=iLii;rIkJ0<2Qibhp$L#iIXI=$gs$X8H;Ku zr}wh8$-AR(M5P{e3Reki*!pgoz{%rl-#6V=&362n>8siM!gn-bS67uZzX4(Eup=`6 zkssF= zO&jc2_6!WoQNDZ6ijxz+m#vi_%2m!qVm~ubrzxrWi0!rx`%*1#8f}E%R!+oGdDthczITPTLYnVG8WqDL}`I*QNYR3vym@4m9Nd&ul;HMLH6%ft9L zpln!>1bn&+kAJUnbdZIB3#|jX3ArI?Tp#MQE_AYi`|a1s2RO*lC8hueIeK=9rT{}C zYy<{o9bsNOf760c<~ED4wVSasr~w7rRGCe7rPzJxo~g&(Q? z(g>$HZOvXnLp&A1ia2sC>Qp&}s~0loXoF-Lb)bW)P$Y7iOx zozOcqQ*gNM#I}Gm`CFTabeU5NL)$~LQcMT9VHiMM4SeaZJ;L#Gsj&eG03aDd30LOE zb$^w=cWS})5GmkClM7saK|`zfTCX8aob<%4pkZZ7%qfvCCA&8S00Zmkj@ozACvkUD zqSPq&VPLMrG{!q`ix<3Q)KSx0DW8fmTGEjeg5@0)o`sI4ltJK_FEq1HzMkxtM|<$W z$=-Knpazz2`ceNZ(nL;!xz(v3Lkdn_0lc^w23~$5VH*md5hdjWc^%Oj&4PKnVlsuY z9?f7N6kFDDe-Lsja#^WizmNtv^qFojHGvmQAt$o_Vg2~hpQhboCH$dJ?n*RbHZ1w& zdIm4j57gK}c@MH``9$IAL3kASQ^_4kNwO#K3`SQ_{};|f0TeON&}3B4w8MaFyy8AH(blugq^_LC&eeSc_3GiR_hqgmM7qSU0?kd_3;_(I2g=s@pyimI)JbzUaXu>(%ZNf7X2y#^N<#?6Xg!hs!Od)A$6ytMRE} zK)cmdf1kaIKLDw|R$RS*4j+W8_b*H#9oeXsTJCWD6qv}sv8}$M{y`_OP<&aYr-4?| z*E4=8LnjX6R)6&iSt3V#`0$P(Wu03$wn3?sTR_f=AN#Xv?W0^IH|yMzm``c3`uHc!?s7@bYeU zz5rJka+t)Rs<-3Zo3Vq^o2|{dxLI%;xL(0sbC1o*n!lRT^SSU8B(YPLq$}c;Iaoqu zYIhMJEm=l*X=Yu9%BWmfs1O64)8=7^2AyMHL2?Cw-dhyfesuXb? z?)^*}v$s12CW2?js4-0eRKBCLt8JZ;D=z#mO?YA$nwh2`WUcD&-c#Fx>C8V}Uwr$| zqsas2M(E$)aLxJDAbHEd!K(u+d{aQ;@gu>>?>@f)9nI?-xS4}|L zp;wX-y6b8KoHVNG7O=j_&d{JhazMnQZf*8y9@kSjcG5NKvJvt62fGfw6|V$Xjh(|{ z7Uwc;;u}D2P>kB^j2xS6Kr91T}Dxas76!F?YaP_W9ISKpI9c>>@8~i~~MSJ0pVim+-98&C+D4q-?f1-Cu zYU#e~UuAH=X3>whkwKq1PB6jwjyf+-D`^FtO)RwySR-Gu6e;Z+lSr!-Gitl{&AWy~ zc|vtF-7)jaHdc}{f&4|#z1Bn|d{^bAgT52~yV5I4gH+`@UMa99aVjTKnL3VZgo(N3 z-;A2}Uq+o3UL3HLFES^%viR#D;?##N!_)^W zBMG0(&`~LWg12H>U*)LW1jirN3WKm5y(!A;MUbJuqxZhbv?O1F=ET$~Ld9M`=(in)R#u_)kU;S%%*>c2!#IX;joTU@Hi@ZRGrRNLfiD`~H)oe)cN9HxBX{eu128rM~9uYa{xJy3%LUm53FY*rS*kSpA>oS zQc)7C=Yg?o)xnEDM@_?NIV9 zx9W>#Yd2hW4y{MVgjYy^2-z=H$WadpH{^nxKANV@t zU-YPtcFUKtQ;yVAqJs}*<3Ev4Jov0=A(*R4^cX%HX%+C z%;Bvz*tb+t zwJ^~9Z5Z{e?Bl11j?+){L=WgJly(Zt^8jEZ*U_50!3ql9nNBzHuGC4tN1QC zU4QTdiHlRJg)Qo^GU`wiOd3C$4F+^RLoY1LU`Sqa-sdng(f$p%O_>dijL^)luiBfB z7IeApqDG*HHexcRXdOUWMnbcGH&-DctQN zOvV!UjGJ|z`xj3HJc90V+oeJv-ycMq#}N*5{|$)5VeTc?v9b$bL7}2=x3?_+x~!eO zT0S>ic4;PT-1kn;zaLik4DelBF~yz0V<-B!Gr`>0F_fBkjn2IytgYaI;i;ns=CROe zSzPa(n%V7Oo*3e@3ObYc&rb}oFTr7{Ia)79TZ08^!xn$33qB%mM2IsHT-6kvJ8g^_ za3m*e#6oOURrGCdpk$fHh>oV>UVfB!5G*ZjK=Y3$=kFjy@?#EQI3hu6z!#;%-N+W1 zk@G))@#$8tceE5dqnUhXyQF$;CR@W$Df{HvQ@*gW=0SPlfDL9vCo#?-}d( z4mRwKD;g_c=&U0NenRfrq?H{=WV|q5ryZ!Cb}0AV*;yvHgi{WV@KFe8JG_Bh6Zfy#+b53m(Q;Nu z4_Jck&1J^PLIv;>Z)C)9T7_C4wF~nqzegYoD;Cs}e5cmvqR`9m(*Wxkh9RbS_&0!h zUAjF$DfMNB*@V<<*}I|jcfa1%DR)MXT+df{bMrCLkxUWWsV>C?SI)E${6GZF0aQhW zg|6P|(yUegDyfVfv;KZnitcyiIqix|mkLy7=EA*@-~nSgv>0>lv0&e{<@?j zHtz;1kH5=qv&W6*&=P%h4UE)hbp6?J$$jHN@$;GjX~Qtbj>p;lF% zTQ3A$a)gYF3mGxF00>}eH*$_PKm+5v5~Q&%o7p0w>JK>oR$)2e&s`n#H?3cCg!7HZ z-$weKiOwFrjkOjq(aHIhJ*N}1a=%X&kw2>vv$t^+edjACW$H%W9!7hE`D?gSj0&6b z=y$vQRNh@rK8sszzX9v!CMRW+?UO~G-`f-jn!BmfV3}0qs@aQ;oYuSd@GCI)3-8B= z(>n@zy?0~oTRLWm2)Z8z877h?N-CCB^R;vG#cMTcWC$=l_&9gWLa7|B#dupwT>aZw zKR7y5O1Tv8^59!^Pc=vC$}TchhBC+X&CvNcSrHo~l8}EXz6@Rdw7(KJH{mw`Yim51 zqxeNJcIf@ynd}FEk&e2#ULHAm7Zfc*@=_h4dNUw5I^oG6;4eV_>)ZQ-6Q7^J3r95G zr1rFpvX(B4R~i~xhfrO*hdd|i`onquP@MGM^qD>Ty~Y$8r8e6NxT?qU^zqUeu2F#U z>lgyhSL{5{DtkGV38exfCO2!K(@D`6i#5VACgK>eddm|egV%zol3q{my?$NAVzQcc zLYOG|5rvOHJQG1S=(+r+08^yzS)U1v& zllT_=z3g7O#szWqepki+YwIh(qUzSRXNDQNK{|%+ZX|{tx+O#!q(KBE1cn~EOS+^R zQ96|tP!t3agh2u6ZvXLp&-woEyyx3+U7NMpvuDM#_F7Ne_u~_uW~+a8lPP37$CzmZ zcdODB25# z!%|eY9lOi=7wy9b_HhCE8d=63eHW_tWSm>~jyLmz882ueIWyS?qWD6cil6!kr`Ht0b)P0o=UYd2*6cpVW*{Z9)hbsf=L=sL|M$Sgr zs&x&@NC~XIck6E-1iVYZH5J%x>Uk^-WnsY@#8HmzV+W^>W-{Yl9S#N~NLwy|cE+gI zg&GKcO^1mUtlvJ}Qk{CP$k=c)nr!9DBCp*F7Ek%PxwM6RwZWZHXg1_JcDr`C- zPy3b`?IUlzT3@vE*DRy@StP*1bPa;$p5)eTae!tUvo@*;pYlJ%!-p~Mc^7YIa6Op3 zqRNYpD$ZdNuVIWBelbm160)%}EFZBh9S#vJ@Qoj)x{1=jwRl#eQR(h(w#7n$Vw+EmAPXZPcyFv$U<#KP){%K|F#bV|&M2%kNIN3G~1U!0XbT6?6|)l4-*IYZNQo z(;v@8m0%@B-0e%N<5V;=G_~b0nL)D+U5p|j^H=x*hQYdx& zbGk^L!ewLHc}@}emg?PtP0V7GQs4Rf$57vsmE~ZGb1UuJe-#{L+^@^*Nwl$5!nmAv z_Tw#k0#Dyst5x_hjbQu++_R3c^9_CsHS)Uu;!>Tq*?^o;GW60}u$LWmt zj76^HX74b0h5XdN@I}0NkDuyu0k&h?!c5jo;FCogEN^OCc&QD!+oxFhC&@t%zXAS%++o+`1to#wOOmsR<2BUnL@ z-~*ryw&+M;d*O^hqFl3KuE4}O>^pEtIr_0({?w1EafkK(6IgD(Fz%(ogU8T3`TO0VP40yL^-4cWzn}yf-iU!iPa2+$j1z{;g}OYyaOPYPGlY+mDgPM0 zc{{cs-02zM7UyE@@0F8|LqRIT@gY{3rALdtajur zXG~|F{Ah+!laL{s*wRH|iE(cQ2{vY4sx+6aSSOd=$1j7csTG~_TS~pvfkiH=syI~T zcbi7ET!AK$?})!=QE~R0Fn_^ZpyA`y&@i!lY_>k$hZ|;tUNq5+>8506`v+r{cZEau zZX;ttRXEgEk@toDB?uc-hOfO7AMVJshoQ|gDa>deouEI>fKU8Swqt2DEDtCB64RRO zS$b96cW3Ny1w&fb!~e75xE(Nc=thXS3+d z@lhNONIz9H^rZdoNy19<*aDJmPAtpmfp*mZoJYuHRMIq4vL-#woRJ&wgvKN51F`Te z%J-T-eEx*fvnlpx8?&Zxe{g6RfCyh;TGW2nlM;4|Z7r#zc_UUGzec1W_^Mfom7nva z>sU{qbu)mW$u4#%ErW}2*F{^ckqv-|eNtAf5X9uc4>#pIF@>Rnc`F093l*aoZ=Si{|ph%FESr4oR`>bPS!tn}LAbI)I=UVGnv1NuWP z@;~-+S{1TpVwT_Q`h(QbTqH;l`NZ6_>mk;bwrQ%jX>I~LRM9X717HLIj3|kOCRLRI z(cD(+QUtd~wSH$qcdkTpyCXaR>J$v=P{^W-d!5&~(TfUXRzoOt`mrND5A4``f zp8kBR>EVs%P)l@1Zi+1LRQ|~UFUY+WVz4x+;cg2=^VWQ znE#JLRM)3jzN4TcY+@)Z3d@4azysjv?dheDVt8?43X=n(SME@wSlCQrD-BbVSO&n)M>UQ?r$J`%I zBy9ww9P?|#uxYC~C7EqII*>IDj+KaT!jYI{Mw};^p6DPUh#p0uXtl#@I^6m*fc&5m zU0;=}wx;BMm@PWVdGh`R_R$kg7%5~Ppa>cKy3=e8(nP3IlYm{0bM!tki|eKEN{%k~ z$YG3eyifs$agM%_c~_2ewJHU^Wa02JM2GBl2J6*C4cbWSvJoUkEZ@r2mE{({{wy`E zc+UZ>ozoS+LmTG}WPJ}+XQ|@#n0Sp{f6o*W&Mw%cKrS5iAg8I>x7iNs*c*ubOqPL` zKVcj);LfIj=VkSr-GE=!D-)aV?Pe-vneum*Lj%~O`}Yr4K*Q*maK@D*1Q@PnuEM>b zLOhg-2THVH)pm>~#TVdhWi=%wMt0baB9kozT{&M0?iBNv^a)h~tfc@8q%^9dEmtMR zOfU6!kJROQ>u{x-eZC!rbhObsCOts-8gUsSc~nz;0R($(hlBNFUAWA&Y^}3Pk;}|D zt|d92fGt#UBqC0N@TPt?ckY^QN+@y6)7d-Cc7NqYb`@cREnJ}1Kx+LD8dPX+6-j1k+jVEdAAQ+f~~ zu;nT>%?nVg4oHZOeF@Q6#>6|cKq%5rtLCE>!0D1n1*{Cq@Mm##Uf#8UN5+8oE3$NkS@|1{PGN?S(`VS|of(T$ZCyverfF-$fFHsNv}f<;xPC+& z7_`^-NzbfNs1xC+3@0SW!Jf~SPf`UHBAAt@Mz$OOB~^)+`BtE@8C}OU=7cLQ8@|ym zn4&o!!==fdLk(E4%}55>G(X!Za-h?`;v|VJ+a!#KKp4@X=!GuaRm!+>83MP4qZrH8 zdm0xKMRlTpkBLnHrF6Wx{$GRhA5auZ;njt(@I6IAO_iLGDuJfda%Tui=7UNqWF^yj z|3Tq_s^6QQx1STvq1a!ziHsh6Nl0&5SW}=WBK8WxS4%6zKPR#e%F2|o(Pd64oDTfT zK}SzDBEEVCZXaMgR6k*DIC&)QvzsKQ+yXW&*yL;C$AcGOV=K?Ds!U*(q6RV&_$N7~ z^#=qsp~zeYHhXV-MUpCaeN41jHexRHxL(B=J523E_@<@@c)C!+U2-lqUxy^*Yu7+M zvv*z1vOhL82%lsZFcS!k;{tbRayM9t7P`E4SH(v!B{#u}>V-BNfal=L5Bh6aXIY=d zqs7R5%h<*aEppx?WQkQA&qV{cSx1Qj$Oq5joKI$%5d zCtTATD>9B6^s6)2pYGGG5Mpf!@QGqQWGv64(_@N?Phr@P`VA=fS8rpohI`b=meXUA zvH;LnrVM1hj#9`?mqH}~jYAug1Q=I02yYsQ7?_JK9xiF}m6jG0d;L|V$n0Z4!Sk4VCZ=)glS%e#xM5P$yrs5TviAE-RD&46BK)9s4&R?tN&g z9x^elat36#E3tArLOa{r>*AKcY@r|oGs_^xOztFg8mk1z#6U@06>ae0sB)byr0uYt zV4IQ-i_@43(z%q1u3Z0v?k(}(QzIJtWEpujBi&j_G_M}9L2qGgnHc65Q`>Ov-&?{u zEqH*aiWXwOGoDz@)$3tJbAp%BDl-p*EPj3h)6J)?YzG!eR(wn#qP0&r^~x2pJq@jV zRV(!!VY(yBki?lYlV&h_JNTqRu{s8-(SM)9bBHAtmZOSf#dQXs;to1t-*_=bbOTLu5-oq3qbmXt#H&prx|Vm48pY^;_iLYVIe7iLu{+fFJbAD8@b+KG1UU1@Dm^U$8J#8f_Br8h zL%Z)}{iN-s*V+&RO%@GWKw!R}O*EC$N%wC+jNt?U5^0P*z=JU5RD8^`mU1Gib+(2L z4ab3Qaoms>gJOll(zJAhqk==1|4usf>GU_i_diPl0cOH@_4tJ`*Pb-L8ajVP$=jmz z$9s9K*x*PUuBu$+)?OpHUmj`pMKcZA0xvvZBlYNy}BStsEW}(iH=tRK6M}a-8kEuWf zm)vlz&wnM_!br{*C)&S*OhSahX!gzyldhhd|Tt zWPDt@asHlQF9lWA95I6_c&$kB8K(l0W&Uc{s! zfMFU)%`2xl$L=GPbwUWGqdPMglAkT~K4_&ucl1an(<<+DOkH3B<8=FgzxK`5Ll0Fp zgst;MMCWSEqcr6ENr$R?HNz*L|AG;!TAniaY0Jt9NDsPPXARme%TLPl4@hy64R zXrq=ElMtl%v0Ew7l%uEZA@DRg-AhJgFF%IH{!SB15k@2mNt7BfCF*w!AMf?&*I39&H(J$!) zM2MFE;duP;*`P`fzgiHiiX_xv(^g!-xx`vnN16pWEBib25ulT51}#_QqxE*?uq-qq z_nqDof69hM4WX@=(#r5GZWxfe0r2^y5RGQNXjI@tBl#>TM!nNsS6A(r+S)Jp%_I2~ zY~<(*JsjV@CjG+rHbJsqAWPhBx``c}A&E%xm%t$yKCtqLoZG2Np@ls|iio?3d8~y) zpKjEfT46$wod#h8rom)PNA6T%lQw-dco~{r>HA5y5Ocxu&2B!A(5IoBILwDP$(IUm zS1p@T-r#Gc|In48xXkRi*dk!w9SO9K`gGSPQ=9vE+LHJmHQ5gT@0#FOPq7OXq|&`p z5o{}eB`+-dUPC_RCneuE%MmIBZW-Cj9sZGUpAPs#{>Xim2d?gF9mMh|V&N53=va~f z=yXI`wX-TFYc7|#I&*biTPF?P*6-8NM{Q*$=NF)SF#O zK)D>&cdW)JL71lSrm_gkTPYEWwv9-EDR@&;2~ssW%e*G_d(`=Bb+j&KQhT;!WMDdA zFmiK_d({Va8u3h0WCDn7hBmb;gr>kILQ{VO*d@F1J|7EFj0gZj8ioKZALOYOoYP~& z9q41OZ@ld=>c8oMmF=~;6;96qKnXk1mW3i!Qae)aBPu9ct`9SHu@uw06i`16UFj-` z;0Vf$A#Xj65>>Wy@g&YaRm7`j{Ml~*!MKQ%?4}pE9FPJ3^ac;D_xQOfh| zcza--R-TXf%Wny+aNMuDl#-1&NM2$8$w62okbSCI3@1Hir-In9=-G5xf>iPQep zW`JDAO@ng(z%fL_Eq&vD?91t{C$nqi3fn(iG$Lg^LYpbeR1@i;Xo4}^G|C}F{L~`h ztL(OT;}>hv65pRNDMsOv^A@>iyY^kCr=9MOn!j@SRQupIGT14&BGW*ngy7eX(65oH zF2CKSmjk<@gyq^-c6*(_%w3m4C>i!w`a*82f`0>0MsfX!e7oTMcdo}{e?9bGvj#?U zNVA?o{S``RB`NcdYDXVun0lsq&mp2zycsg$7f@-Zh2gF^L(vjeoyAAW;r z+>#R>n9DrRNeF*Zj!v2qBSB&f$GKl4$B`DM?3-UBXMgzC%_3v6acD5u4BST(%CXb# z4aA|5$hiF=m|59*cT2D|bWL+)O>e1v%6a5?`?_ELdEVE;mkTi;XNj8>b@6eq4x-rC$?qV zLkk^zU_OGG?8 z0VGWH&fOMMABtEk14s_if*8lv+ZP*|R!J3k^8)JpHE(XSrf*J7$?CG6kPvlXykX47 zsG+oRoVn$FvQSUdgU(PH3wCCR|96x0{;wu&s=$jMzde-kz9*@dZ0=ef99hWYs<6j% zIH0`4KFHYdJXGGH^*2EF0r(eNzA}s;?L1W3%X~-4uTX@@sfaE*obF0uwud-r_H#BJ zBCTKBL>&Us6rV|RVq!bgL$?E~6f33n^_@jZN+#Pj@KUl1&xkBJ<~8V+P?7gHVC3kU zujq2SiNlO{jfrB`A0Rtkk@0h5f4Fk9NG6xrx(I%{>W)F-u0E9d#8EO_6DanGl^ln6 zNm0ExGW#tW(gR>f_>#pSYJjSI_p^nc6XSuK%srU_$kGkv<^O*&OXO~IeeYXmH#6c=n@oeyl>e*l<=o6TVsY0;R(7hP&lN9YwgzGkppr%N z|IsH_1oAek=i}}Eg&(sg(k~d3wY_%Fcr0q!>K>3}XXTx=e+h%sQ;tW`DM=7?FdTUl@O)W!VD7NvkMt8NIxG>n=-9gm+7J zjr!6lkbXMWajwf6AE=`>^HZ=}Njym$8H2gz<3ihk6NH;OBu+O-zaFV&$#y4v?>OR0 z&ze}&z<$`2rShTl_m%L;A=4nE84e}u*3U&TUt}V3b93`jbRFDd@%0MVL)1Q?%l|sc zm401A^qo}URiE|aj0i^Cgh6oPRj_I*JTFiYO`Z7n(HWxQAZRV(7&bR-^PcF7h4%x zsJBYTee=pW*$>eUed4bIY`oD@4oKB!$j(DNUH_F2F_h~rH8o{F+pPioB)-hU=*pyv zujO@hl8Kb}g;!kjEW9JNR0_ybR9oy@dL(1Hw|DpY)WjwD|+!Pc{rzPH5lkhhi#*p_*#xYUlbDzA{H&% z!OoH5=-~UpX4pwgoHSjL;Zr@kPuwArAeEpjN2m1_pJw$^>GJyau4ekoA*6@&B7ix* zd;ZzjcA!P}*U^*jVC=x3eb=ku99xvvYm~W{$lIewTWYkr6%+sdCOsJt7z3^2*2?wA zMx+I$EvFgZF9BljT&WKT(G#OyfF@7W)@97zNdM#bX2BER7nh83t+T z8$MtauU?k7fJ>ZUAL$lO@LxBo+lAqC&bI#?sKG+O5Y_~e#zof317osd9%I}EGl)pP zVCtMSQo)!=U%EVF%tflT!`0MMysG(dtDpPmZcG^LWjun7;fD*GdTkOFf?YI5l+I$P zbJox4B6UDnih&~keEz3@KA+ODWjLj%)>|JHGswGzMIi(3VfC{)kU)3UZ@{`>W6Vv8 zuo5&ZPbi;+-sr+!4kYsgq4mw5<#g1p6yL}BxI2*NA00IIAUKQ+i}jqXi?!yje6=S$ zklJR$kJEChtUN$A%K;7UW{2n94ZQ0Jx!n*2NXSe1qP~?o5!{Q=F$`8=Y^>EvFZI2A za55}L;h4_;u@60M^2F|F`!h`I^sk%|)5DYV;g!h>|7e+%%XAa7HL4Kx+CEcf6Q+1y zE*!dKrcpmH0vAOoWVX`-*<_WMvI!}uM0B=f&+bCdnKdsadC=x$R{WGvsIrCxr|vW0 zXo{kA=8?2vNWP(A~vy{y?8Sc@+GYt~L$^)fcL+T$0A2}m-@ zd@$*OIPDSm-C$Gy>=#c~fG$b13`;E)>9+lE0LT9M5rMnO`mCzjzLX^U@a=9N2N7#D z8P|sZ)|m%|JMt^+?8{QDDGsvp=hG~W9RZCSU-1i|JfwKjL&J!aghZ_G)ErPmBeN<7 zHC5!CImyhrImZK~njfaCO1Pr#9S1I9tHe+$3|+;#YY;o3`ktnry3t-Fn5A_BuKD&w-dP;^Js&8-T7G8bJ0vf zf@ZSapYLBt;~>w=#A!?Q%_+4Y_!?HYDej*8(bD@uDtlITY-cmm}vK<4f!KZ5cQ%X^G7JEw-Hqp=HQ#eJ30B4 z**D@?ETR^xCTyAsZI&=WGag*c(TK7TkjAk4AiEO=>uh=FVqL$o8;TANSUaL^M)MggU&pcR0E%MjX+Muene)!`O*m9 zTa(3>mscEG|c{M`K2^_`I*pX z>{Ho4tX9~6Sl>pRz9svCW`B%~mt9A^lzgK|w%Gdh^DluBa!yLNHq<4{G}|Mk0;d8T z!nhaa(?9`CQvFhyynnn5w*G6P02ud4tpS}4nQY7{0;vTWk&zF_p!%Z*Wv$emZ7Z=F z-5@C&GyiXyO<#WlX5S?14sU^cztY4Y<<=RTK-g*Q-Ix#TAZbn49G1{3xE32Rsm?!z^xZem7obqHz9@e25$;q~umghoq;~zS;e5Nz%poTO ztrU(U=nU^Sp!{Fo{__``sd`|QUm+g85(H_p<+Jr}_!Z$~;YyO``ISbw;+n(@iN~C9 zQw&NAQv!~-e-2#cKL?Kb))rgnfPAxF?H4&PIiu1zK#2BhaTFz%3zo8fVLn*Q0Uph3 zNdx(ikrE8{P1I{dODTL!R-UXJXMWoDGsa|wk=sout6)M4ak6(MOoo{Q74b6`6rR+> z1ximUkYbmU=0$1|hw*vk@u_pB@!Lm5W;n$T{NsfwYu@^gHS;KU29W8;^#(<2OKU^N zJTgW!^cZdFw#J*%B@S1Yd89qepO*0i{GJlx`Vw< z340;}`^-Q61o40#rCp#KVi;}Umv8b`+Wf&8=in3;>VICYjSo1_^+4PMtmsKKS^3#o z1UebtL^mUa=B^M&rSdMm+3x2)DaKNy^2TS!A3t69J^+AV^iM5g?#+}f4eLai0zmyw zE$qY+j)0~_H<2MB9O{&f>|?f6`_i~>9PsbrHPvn74UcGCmiUsp!qv{qDTx`WT8)@OR)xl3A~G*gc~1>DfzYM z4gZTdqycU%Bz((s*-kfwH}|z<14J>B*g2Q)mGt|i(F_J!en$!gJENs#~JBV8Zg@quJiWC9+! z`-YB$8vh2+-REbOl5Af90E(t$osME2auviWrN~nPi=+$ zqXQ=d7!WkWAJ@!WN1yy#V4@Lv*ccw*=#cl|!D0Vmu)&O)(2Zr%}V0jD7*! z#0^}`MQUk30m4TD$F&Z6HsG#<^btO9{v}4j(Wezc6HO_kb%6T&d~g^dmkdyyLFYn9 z9{bb?FRTA4cMC@xAa~yRfKZwI?NF;I28s2KT8RQx$61JG*veUGBoXNT$5qA#t^8H= z`Y?GboUdg+yJqEW&&xEYP|~ZA3P@RIb`vAMGs90F>u8anLy8HxWGKHrE?L(wP_CoL z++Zk)syg`3CUM0YHgOXrI;*Ev)$6e|86`y7(#=NmBEP*KW5|8j0Dz*&N4X`1s~+mS z{Q!wG|E%#5Ow`1dnQ0Qc&#h!hyhB?tQ1TbpU{%2Rf{wRq_#u)As$sV36tO1K2_X<1 zY%3d}!NgG>!pj135^_EZ;%IPU3MAiLX$H-iYg!#V@0~ZfOgVr$7BxGzmM{H0ey62E z&Py~Ci1>zu-*oQ)AY+SrOzuy7giD$lPW{4OD^b%_q>6w>*#1%f zJ1VkI(3e6|Dh3X8N!bKV*P{TkVB8^o>@^@B)>qTE%}7T(9h1=h(1?w*&c4Z*FbvTf z%1#;)kL%O*{wt<1-|SWGVy_`m9OdDrO|DlH3Nr2<7wrU;I-UA#%!~hjMo(r|=~u@e z_~4jR55)V4doXHH4R9yhN1k(PixOT?=v0cCcHu$ZmposD(Ub%}wja<26PA_c6=RO5 z3W<}%Bcv0$=ew&3{FRzKvN4ez8d)Bi^W^&_hv5&dB=LL)e~ifGbta3;Uk;< zX?;qvc(!E4?+e8A9BN0>O6|1n%_d1;pK9Al>ab41-M>fFT+hi?EzLo0gF)f;*iSgF zKlYHN>@lV`J}(u_*Q9jT?8m8fVRC>0jA50u)YffdQ!?rm$hECH{7={BK9m<%vPG+x z!3~Ya3ahXw`OLJi5BbjVdo(i6Th7F%P&fQ0GScrpaHVGu0Z0)(r(rRsd&rb>i^!1Y z-9-3v+HSmY2xC4v?{>sR!Ryzmf8x!ypnMbGPipLIZRtBt>waDUn?@$(RC|t)gI;4E zRAhoweDfF;VH(fv)49y%w+AD5fG={ms4VenNF#}D2YZtrU&vXx*4`}m6N~6Bp4`Vu zSqBj6W9O)rIPn-dk@+^6cJ@m?-oPI0-^cD~j>k_VK7FrY#(`bZB@4u$eDT;b^>Sz!HX z9Nh+zQ4)^foT&+8LVDPp8|M9J+XetY^mFsfo4CbLXBBl?$0+uB=88e}q1{0M`*HLy zx_p6a)Ljobw&%Ipei~MX?$h(>n?j2&zeLT%e*fTJ5|+eNR2`wAP7M-*8SQJd{`9+xd86=y29xmL+I&4 zPt_={_j`}R_vFPWI6Nbm)dMQoUAVWN?_OpB(xX(VS&k_7wy)Vt}nuQ zhSXm_NQw$}?MD2$5dtry!e$LG5K@h>+pPzyF`!+uORkCnQPEGd1`0YrXoo8{w8`_8 z3#Q#PC$?K^l$7jLh$ZyTM;(e(+Uj)Zl zMRO7XfUSw&O_N_rk&L_7;91w0umYa;5I54K;) z(UR*VG|Wrda-tI|>7x@Wjd7zX@S-OIRrOwQ)LA28=rreE?p$WecQsm@;^GWas*2d8 zwvSS>$%&(>9QmoO0T4tnh_jzp-rg zn$Ckvt|#I*g*jNv+H&g@FAp0Ojwogya+rI`(VPg8N zV0kD&$V4!rE(`c;!i8CpKiXsfux1;Fo5bR>I~LJgV~^UK+)hH9QkB~oJL0jRTbrvz zVOH*Yn7wwl+box@67o*}A|#0=Pe6!C}`z{RSsUoo)*LxB28-Z`@7K*A>w z_5dRusgzPyKOcU8+9k8Wwj33~Oec`KdBshpZR z==Hu6LDCXWm^8-|d}hx7g-5FX)sKN1-zuaZ<6cF0JG{)nQsAPp466$1S%uHGmsKekY@IW8N+akI8nn1tZB?(LA1pvu_@uU zZ(`2PsoNbTcqV!Zup#{k;NX5o+5GX@rnL%MQ@PX6zW)R^sHVTh>RQ1!+pV2myjuSb zUeV&tuB@y`AuKLJc(Xwrq!0toc*C)n^`g++oV~2p< z*2i4H@q+NWw8KaC6cNTVnrT?0a|Y-{&xDlUTfdW5G(mpk*psL#K%GY`Y%5`#Mj8#q zcaOX)H{_;OmcnN+5cLf(OQ#vdSDaCtHF{<&@^B!p}wLP6VRS?ALS51x!vsPE7Daonag)BG+ zsUETg?jRuUpS^{@7-mXZOl^c?K1?PrxeR>wO%nMlQ=aQAkpC+Mf({f*GAKX%$Uz}} z66mFot9fTaYeRh@j z&+7q)f;sES3njo2qx6REcyM9-Ye?xdo*-8@j)5MOob`Q_a&6aU8Gf2KIZl8CP6pwA zY0mUg+O1h)La|I?7>UE=s|&=vE)-wP*Q{(gNJbz(<`O#-GqRk7HW{j)8aZghRB|qg z&+5w{TK8*=QStGl?~4)Z`^DI2faS)hh(j($GtpNVu7c>;Ze_6OT>wGG*Hjp3lEewr zMm>z<-HrHjW}|jM_owukntCL5XSr5nLc<_mtaTFz06=|5y!m(~FG?o}-_oDd?40rj6VrWlOsT4y6 z3bZDWj|qoHyYgZly)%0u8N-4TMKBz2AGTpzT!EJ|$xWxw0+(h{93yl2eSyQx>sE%nqL~`IC&Oh>_jJ94N3H~Lodu;k?n z=gMvFO`iIhrElNL5^T*Vq|bjb2%_(DmXq$=dqq$@pl+XACa@;QjCKSSTlO(-=uYu| zN#xUOcjCOZ!iq;n5EYxrp5|n!&1%>$=+n1l3H0$;A%PEj6&HfXoP1zT4o2*NzC9${(D~phOSYX&Le@u62`bkDN)~7c0Od>;t+2mY2XcWkQBkIs@D(ca!JCR zl&__oi$5+p8Jb4nhkHkJnN{ux>sNU7;@O#{l!1N;d`LBP`H|97!|X0J9p7_KhOgays~a=J)3 zcREy!8}e_@!5S3ZTia{{+!0CB%bH}9j#k^)SZ7fQZ;A%zIr@Hy6HCZ50BF?5SDIN^=|@*})DMykmRgUzvS z0vxwM(VW;w5Un!dIw|o2)^l%|D1vixGArh8)!0FGyRLQuV$J47{yZCOYE|iWRssft z02}Mi=2mVJGZSr4&1h#7x$C!>{}xkGYfW2S17YcEhSgYoG>vAHJX^cv(_{W9vB>?} z(}%aO+WtVkrKEoBsfSvmakcpG72i9yV_$!y?~=)Do+zQ=bm?q{ZPTt+IJ-nV!PD#i zO1e>e3)sFVin~Kfbn>*hYuocqar|A=kty1z7%R4~)Hn>NUtw~SL$ygmMGXKHfRBYI zPy{?rA2j$RQZQ92FTF%WyC&@`=1b|w{6ke$G8{uG+LdEPPl9-Zyzd>{B$0s?TC1L( zQ=vmfZzBY!5B$wRYUDT%otz^R6X$H3Kgr!sk4tpa1y}dZZ3nJi`>#3`MX=p)q?NuD zNH~?3i>6fgR>FHOUt{=EuE*kToW#0EoGW|DcVwlIlB$9)C`@rIP?ca6zuT|w zySV0|85*SWu?5@+8P)L^HrR=gvGd@V|I#p{3>~t*(j4 z@HR<3!;ES7_9oR(W$$ZBB-$0m;%o3mTruT;_1%XF8W6~{;JI`nFQ?G~4GG&9!nRdH z%EKac=H=%c<(O?S(l-C;NcDA%H|Gy6LoM@YdtaW!r!@HSYjP{*wuz+Ao;^RJ3lHFn zBG0+{=%It2FCm)kg9Vp@!`7a`W6hhWwgNIav>PsTtORPuz^Ud3$p zmRvPfp#hkTHUClQ?!ePPYbmDNAI7tJv-`;Z#3+HN1v4Yr1P8A5s0L|yc)|RQg|c~Q Z=w}mN=0uEx=pg>ynj3}uW#8{F{}0?HuQ31s literal 0 HcmV?d00001 diff --git a/docs/source/_images/2x2_materials.jpeg b/docs/source/_images/2x2_materials.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..b76607eca66106f083c2c8fc2feb044f60d90a7a GIT binary patch literal 59424 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

N1Gkv&N5JUzVL7}n>CVY1IDMtkdfQfL<^Jc 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 literal 0 HcmV?d00001 diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 9a339fb47..6318a9916 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -413,6 +413,32 @@ or sub-elements and can be set to either "false" or "true". .. note:: This element is not used in the multi-group :ref:`energy_mode`. +------------------------ +```` Element +------------------------ + +The ```` element enables random ray mode and contains a number of +settings relevant to the solver. Tips for selecting these parameters can be +found in the :ref:`random ray user guide `. + + :distance_inactive: + The inactive ray length (dead zone length) in [cm]. + + *Default*: None + + :distance_active: + The active ray length in [cm]. + + *Default*: None + + :source: + Specifies the starting ray distribution, and follows the format for + :ref:`source_element`. It must be uniform in space and angle and cover the + full domain. It does not represent a physical neutron or photon source -- it + is only used to sample integrating ray starting locations and directions. + + *Default*: None + ---------------------------------- ```` Element ---------------------------------- diff --git a/docs/source/methods/index.rst b/docs/source/methods/index.rst index 59892ac27..08aa7d053 100644 --- a/docs/source/methods/index.rst +++ b/docs/source/methods/index.rst @@ -20,3 +20,4 @@ Theory and Methodology energy_deposition parallelization cmfd + random_ray \ No newline at end of file diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst new file mode 100644 index 000000000..aa4367847 --- /dev/null +++ b/docs/source/methods/random_ray.rst @@ -0,0 +1,804 @@ +.. _methods_random_ray: + +========== +Random Ray +========== + +.. _methods_random_ray_intro: + +------------------- +What is Random Ray? +------------------- + +`Random ray `_ is a stochastic transport method, closely related to +the deterministic Method of Characteristics (MOC) [Askew-1972]_. Rather than +each ray representing a single neutron as in Monte Carlo, it represents a +characteristic line through the simulation geometry upon which the transport +equation can be written as an ordinary differential equation that can be solved +analytically (although with discretization required in energy, making it a +multigroup method). The behavior of the governing transport equation can be +approximated by solving along many characteristic tracks (rays) through the +system. Unlike particles in Monte Carlo, rays in random ray or MOC are not +affected by the material characteristics of the simulated problem---rays are +selected so as to explore the full simulation problem with a statistically equal +distribution in space and angle. + +.. raw:: html + + + +The above animation is an example of the random ray integration process at work, +showing a series of random rays being sampled and transported through the +geometry. In the following sections, we will discuss how the random ray solver +works. + +---------------------------------------------- +Why is a Random Ray Solver Included in OpenMC? +---------------------------------------------- + +* One area that Monte Carlo struggles with is maintaining numerical efficiency + in regions of low physical particle flux. Random ray, on the other hand, has + approximately even variance throughout the entire global simulation domain, + such that areas with low neutron flux are no less well known that areas of + high neutron flux. Absent weight windows in MC, random ray can be several + orders of magnitude faster than multigroup Monte Carlo in classes of problems + where areas with low physical neutron flux need to be resolved. While MC + uncertainty can be greatly improved with variance reduction techniques, they + add some user complexity, and weight windows can often be expensive to + generate via MC transport alone (e.g., via the `MAGIC method + `_). The random ray solver + may be used in future versions of OpenMC as a fast way to generate weight + windows for subsequent usage by the MC solver in OpenMC. + +* In practical implementation terms, random ray is mechanically very similar to + how Monte Carlo works, in terms of the process of ray tracing on constructive + solid geometry (CSG) and handling stochastic convergence, etc. In the original + 1972 paper by Askew that introduces MOC (which random ray is a variant of), he + stated: + + .. epigraph:: + + "One of the features of the method proposed [MoC] is that ... the + tracking process needed to perform this operation is common to the + proposed method ... and to Monte Carlo methods. Thus a single tracking + routine capable of recognizing a geometric arrangement could be utilized + to service all types of solution, choice being made depending which was + more appropriate to the problem size and required accuracy." + + -- Askew [Askew-1972]_ + + This prediction holds up---the additional requirements needed in OpenMC to + handle random ray transport turned out to be fairly small. + +* It amortizes the code complexity in OpenMC for representing multigroup cross + sections. There is a significant amount of interface code, documentation, and + complexity in allowing OpenMC to generate and use multigroup XS data in its + MGMC mode. Random ray allows the same multigroup data to be used, making full + reuse of these existing capabilities. + +------------------------------- +Random Ray Numerical Derivation +------------------------------- + +In this section, we will derive the numerical basis for the random ray solver +mode in OpenMC. The derivation of random ray is also discussed in several papers +(`1 `_, `2 `_, `3 `_), and some of those +derivations are reproduced here verbatim. Several extensions are also made to +add clarity, particularly on the topic of OpenMC's treatment of cell volumes in +the random ray solver. + +~~~~~~~~~~~~~~~~~~~~~~~~~ +Method of Characteristics +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The Boltzmann neutron transport equation is a partial differential equation +(PDE) that describes the angular flux within a system. It is a balance equation, +with the streaming and absorption terms typically appearing on the left hand +side, which are balanced by the scattering source and fission source terms on +the right hand side. + +.. math:: + :label: transport + + \begin{align*} + \mathbf{\Omega} \cdot \mathbf{\nabla} \psi(\mathbf{r},\mathbf{\Omega},E) & + \Sigma_t(\mathbf{r},E) \psi(\mathbf{r},\mathbf{\Omega},E) = \\ + & \int_0^\infty d E^\prime \int_{4\pi} d \Omega^{\prime} \Sigma_s(\mathbf{r},\mathbf{\Omega}^\prime \rightarrow \mathbf{\Omega}, E^\prime \rightarrow E) \psi(\mathbf{r},\mathbf{\Omega}^\prime, E^\prime) \\ + & + \frac{\chi(\mathbf{r}, E)}{4\pi k_{eff}} \int_0^\infty dE^\prime \nu \Sigma_f(\mathbf{r},E^\prime) \int_{4\pi}d \Omega^\prime \psi(\mathbf{r},\mathbf{\Omega}^\prime,E^\prime) + \end{align*} + +In Equation :eq:`transport`, :math:`\psi` is the angular neutron flux. This +parameter represents the total distance traveled by all neutrons in a particular +direction inside of a control volume per second, and is often given in units of +:math:`1/(\text{cm}^{2} \text{s})`. As OpenMC does not support time dependence +in the random ray solver mode, we consider the steady state equation, where the +units of flux become :math:`1/\text{cm}^{2}`. The angular direction unit vector, +:math:`\mathbf{\Omega}`, represents the direction of travel for the neutron. The +spatial position vector, :math:`\mathbf{r}`, represents the location within the +simulation. The neutron energy, :math:`E`, or speed in continuous space, is +often given in units of electron volts. The total macroscopic neutron cross +section is :math:`\Sigma_t`. This value represents the total probability of +interaction between a neutron traveling at a certain speed (i.e., neutron energy +:math:`E`) and a target nucleus (i.e., the material through which the neutron is +traveling) per unit path length, typically given in units of +:math:`1/\text{cm}`. Macroscopic cross section data is a combination of +empirical data and quantum mechanical modeling employed in order to generate an +evaluation represented either in pointwise form or resonance parameters for each +target isotope of interest in a material, as well as the density of the +material, and is provided as input to a simulation. The scattering neutron cross +section, :math:`\Sigma_s`, is similar to the total cross section but only +measures scattering interactions between the neutron and the target nucleus, and +depends on the change in angle and energy the neutron experiences as a result of +the interaction. Several additional reactions like (n,2n) and (n,3n) are +included in the scattering transfer cross section. The fission neutron cross +section, :math:`\Sigma_f`, is also similar to the total cross section but only +measures the fission interaction between a neutron and a target nucleus. The +energy spectrum for neutrons born from fission, :math:`\chi`, represents a known +distribution of outgoing neutron energies based on the material that fissioned, +which is taken as input data to a computation. The average number of neutrons +born per fission is :math:`\nu`. The eigenvalue of the equation, +:math:`k_{eff}`, represents the effective neutron multiplication factor. If the +right hand side of Equation :eq:`transport` is condensed into a single term, +represented by the total neutron source term :math:`Q(\mathbf{r}, \mathbf{\Omega},E)`, +the form given in Equation :eq:`transport_simple` is reached. + +.. math:: + :label: transport_simple + + \overbrace{\mathbf{\Omega} \cdot \mathbf{\nabla} \psi(\mathbf{r},\mathbf{\Omega},E)}^{\text{streaming term}} + \overbrace{\Sigma_t(\mathbf{r},E) \psi(\mathbf{r},\mathbf{\Omega},E)}^{\text{absorption term}} = \overbrace{Q(\mathbf{r}, \mathbf{\Omega},E)}^{\text{total neutron source term}} + +Fundamentally, MOC works by solving Equation :eq:`transport_simple` along a +single characteristic line, thus altering the full spatial and angular scope of +the transport equation into something that holds true only for a particular +linear path (or track) through the reactor. These tracks are linear for neutral +particles that are not subject to field effects. With our transport equation in +hand, we will now derive the solution along a track. To accomplish this, we +parameterize :math:`\mathbf{r}` with respect to some reference location +:math:`\mathbf{r}_0` such that :math:`\mathbf{r} = \mathbf{r}_0 + s\mathbf{\Omega}`. In this +manner, Equation :eq:`transport_simple` can be rewritten for a specific segment +length :math:`s` at a specific angle :math:`\mathbf{\Omega}` through a constant +cross section region of the reactor geometry as in Equation :eq:`char_long`. + +.. math:: + :label: char_long + + \mathbf{\Omega} \cdot \mathbf{\nabla} \psi(\mathbf{r}_0 + s\mathbf{\Omega},\mathbf{\Omega},E) + \Sigma_t(\mathbf{r}_0 + s\mathbf{\Omega},E) \psi(\mathbf{r}_0 + s\mathbf{\Omega},\mathbf{\Omega},E) = Q(\mathbf{r}_0 + s\mathbf{\Omega}, \mathbf{\Omega},E) + +As this equation holds along a one dimensional path, we can assume the +dependence of :math:`s` on :math:`\mathbf{r}_0` and :math:`\mathbf{\Omega}` such that +:math:`\mathbf{r}_0 + s\mathbf{\Omega}` simplifies to :math:`s`. When the differential +operator is also applied to the angular flux :math:`\psi`, we arrive at the +characteristic form of the Boltzmann Neutron Transport Equation given in +Equation :eq:`char`. + +.. math:: + :label: char + + \frac{d}{ds} \psi(s,\mathbf{\Omega},E) + \Sigma_t(s,E) \psi(s,\mathbf{\Omega},E) = Q(s, \mathbf{\Omega},E) + +An analytical solution to this characteristic equation can be achieved with the +use of an integrating factor: + +.. math:: + :label: int_factor + + e^{ \int_0^s ds' \Sigma_t (s', E)} + +to arrive at the final form of the characteristic equation shown in Equation +:eq:`full_char`. + +.. math:: + :label: full_char + + \psi(s,\mathbf{\Omega},E) = \psi(\mathbf{r}_0,\mathbf{\Omega},E) e^{-\int_0^s ds^\prime \Sigma_t(s^\prime,E)} + \int_0^s ds^{\prime\prime} Q(s^{\prime\prime},\mathbf{\Omega}, E) e^{-\int_{s^{\prime\prime}}^s ds^\prime \Sigma_t(s^\prime,E)} + +With this characteristic form of the transport equation, we now have an +analytical solution along a linear path through any constant cross section +region of a system. While the solution only holds along a linear track, no +discretizations have yet been made. + +Similar to many other solution approaches to the Boltzmann neutron transport +equation, the MOC approach also uses a "multigroup" approximation in order to +discretize the continuous energy spectrum of neutrons traveling through the +system into fixed set of energy groups :math:`G`, where each group :math:`g \in +G` has its own specific cross section parameters. This makes the difficult +non-linear continuous energy dependence much more manageable as group wise cross +section data can be precomputed and fed into a simulation as input data. The +computation of multigroup cross section data is not a trivial task and can +introduce errors in the simulation. However, this is an active field of research +common to all multigroup methods, and there are numerous generation methods +available that are capable of reducing the biases introduced by the multigroup +approximation. Commonly used methods include the subgroup self-shielding method +and use of fast (unconverged) Monte Carlo simulations to produce cross section +estimates. It is important to note that Monte Carlo methods are capable of +treating the energy variable of the neutron continuously, meaning that they do +not need to make this approximation and are therefore not subject to any +multigroup errors. + +Following the multigroup discretization, another assumption made is that a large +and complex problem can be broken up into small constant cross section regions, +and that these regions have group dependent, flat, isotropic sources (fission +and scattering), :math:`Q_g`. Anisotropic as well as higher order sources are +also possible with MOC-based methods but are not used yet in OpenMC for +simplicity. With these key assumptions, the multigroup MOC form of the neutron +transport equation can be written as in Equation :eq:`moc_final`. + +.. math:: + :label: moc_final + + \psi_g(s, \mathbf{\Omega}) = \psi_g(\mathbf{r_0}, \mathbf{\Omega}) e^{-\int_0^s ds^\prime \Sigma_{t_g}(s^\prime)} + \int_0^s ds^{\prime\prime} Q_g(s^{\prime\prime},\mathbf{\Omega}) e^{-\int_{s^{\prime\prime}}^s ds^\prime \Sigma_{t_g}(s^\prime)} + +The CSG definition of the system is used to create spatially defined source +regions (each region being denoted as :math:`i`). These neutron source regions +are often approximated as being constant +(flat) in source intensity but can also be defined using a higher order source +(linear, quadratic, etc.) that allows for fewer source regions to be required to +achieve a specified solution fidelity. In OpenMC, the approximation of a +spatially constant isotropic fission and scattering source :math:`Q_{i,g}` in +cell :math:`i` leads +to simple exponential attenuation along an individual characteristic of length +:math:`s` given by Equation :eq:`fsr_attenuation`. + +.. math:: + :label: fsr_attenuation + + \psi_g(s) = \psi_g(0) e^{-\Sigma_{t,i,g} s} + \frac{Q_{i,g}}{\Sigma_{t,i,g}} \left( 1 - e^{-\Sigma_{t,i,g} s} \right) + +For convenience, we can also write this equation in terms of the incoming and +outgoing angular flux (:math:`\psi_g^{in}` and :math:`\psi_g^{out}`), and +consider a specific tracklength for a particular ray :math:`r` crossing cell +:math:`i` as :math:`\ell_r`, as in: + +.. math:: + :label: fsr_attenuation_in_out + + \psi_g^{out} = \psi_g^{in} e^{-\Sigma_{t,i,g} \ell_r} + \frac{Q_{i,g}}{\Sigma_{t,i,g}} \left( 1 - e^{-\Sigma_{t,i,g} \ell_r} \right) . + +We can then define the average angular flux of a single ray passing through the +cell as: + +.. math:: + :label: average + + \overline{\psi}_{r,i,g} = \frac{1}{\ell_r} \int_0^{\ell_r} \psi_{g}(s)ds . + +We can then substitute in Equation :eq:`fsr_attenuation` and solve, resulting +in: + +.. math:: + :label: average_solved + + \overline{\psi}_{r,i,g} = \frac{Q_{i,g}}{\Sigma_{t,i,g}} - \frac{\psi_{r,g}^{out} - \psi_{r,g}^{in}}{\ell_r \Sigma_{t,i,g}} . + +By rearranging Equation :eq:`fsr_attenuation_in_out`, we can then define +:math:`\Delta \psi_{r,g}` as the change in angular flux for ray :math:`r` +passing through region :math:`i` as: + +.. math:: + :label: delta_psi + + \Delta \psi_{r,g} = \psi_{r,g}^{in} - \psi_{r,g}^{out} = \left(\psi_{r,g}^{in} - \frac{Q_{i,g}}{\Sigma_{t,i,g}} \right) \left( 1 - e^{-\Sigma_{t,i,g} \ell_r} \right) . + +Equation :eq:`delta_psi` is a useful expression as it is easily computed with +the known inputs for a ray crossing through the region. + +By substituting :eq:`delta_psi` into :eq:`average_solved`, we can arrive at a +final expression for the average angular flux for a ray crossing a region as: + +.. math:: + :label: average_psi_final + + \overline{\psi}_{r,i,g} = \frac{Q_{i,g}}{\Sigma_{t,i,g}} + \frac{\Delta \psi_{r,g}}{\ell_r \Sigma_{t,i,g}} + +~~~~~~~~~~~ +Random Rays +~~~~~~~~~~~ + +In the previous subsection, the governing characteristic equation along a 1D +line through the system was written, such that an analytical solution for the +ODE can be computed. If enough characteristic tracks (ODEs) are solved, then the +behavior of the governing PDE can be numerically approximated. In traditional +deterministic MOC, the selection of tracks is chosen deterministically, where +azimuthal and polar quadratures are defined along with even track spacing in +three dimensions. This is the point at which random ray diverges from +deterministic MOC numerically. In the random ray method, rays are randomly +sampled from a uniform distribution in space and angle and tracked along a +predefined distance through the geometry before terminating. **Importantly, +different rays are sampled each power iteration, leading to a fully stochastic +convergence process.** This results in a need to utilize both inactive and +active batches as in the Monte Carlo method. + +While Monte Carlo implicitly converges the scattering source fully within each +iteration, random ray (and MOC) solvers are not typically written to fully +converge the scattering source within a single iteration. Rather, both the +fission and scattering sources are updated each power iteration, thus requiring +enough outer iterations to reach a stationary distribution in both the fission +source and scattering source. So, even in a low dominance ratio problem like a +2D pincell, several hundred inactive batches may still be required with random +ray to allow the scattering source to fully develop, as neutrons undergoing +hundreds of scatters may constitute a non-trivial contribution to the fission +source. We note that use of a two-level second iteration scheme is sometimes +used by some MOC or random ray solvers so as to fully converge the scattering +source with many inner iterations before updating the fission source in the +outer iteration. It is typically more efficient to use the single level +iteration scheme, as there is little reason to spend so much work converging the +scattering source if the fission source is not yet converged. + +Overall, the difference in how random ray and Monte Carlo converge the +scattering source means that in practice, random ray typically requires more +inactive iterations than are required in Monte Carlo. While a Monte Carlo +simulation may need 100 inactive iterations to reach a stationary source +distribution for many problems, a random ray solve will likely require 1,000 +iterations or more. Source convergence metrics (e.g., Shannon entropy) are thus +recommended when performing random ray simulations to ascertain when the source +has fully developed. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Converting Angular Flux to Scalar Flux +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Thus far in our derivation, we have been able to write analytical equations that +solve for the change in angular flux of a ray crossing a flat source region +(Equation :eq:`delta_psi`) as well as the ray's average angular flux through +that region (Equation :eq:`average_psi_final`). To determine the source for the +next power iteration, we need to assemble our estimates of angular fluxes from +all the sampled rays into scalar fluxes within each FSR. + +We can define the scalar flux in region :math:`i` as: + +.. math:: + :label: integral + + \phi_i = \frac{\int_{V_i} \int_{4\pi} \psi(r, \Omega) d\Omega d\mathbf{r}}{\int_{V_i} d\mathbf{r}} . + +The integral in the numerator: + +.. math:: + :label: numerator + + \int_{V_i} \int_{4\pi} \psi(r, \Omega) d\Omega d\mathbf{r} . + +is not known analytically, but with random ray, we are going the numerically +approximate it by discretizing over a finite number of tracks (with a finite +number of locations and angles) crossing the domain. We can then use the +characteristic method to determine the total angular flux along that line. + +Conceptually, this can be thought of as taking a volume-weighted sum of angular +fluxes for all :math:`N_i` rays that happen to pass through cell :math:`i` that +iteration. When written in discretized form (with the discretization happening +in terms of individual ray segments :math:`r` that pass through region +:math:`i`), we arrive at: + +.. math:: + :label: discretized + + \phi_{i,g} = \frac{\int_{V_i} \int_{4\pi} \psi(r, \Omega) d\Omega d\mathbf{r}}{\int_{V_i} d\mathbf{r}} = \overline{\overline{\psi}}_{i,g} \approx \frac{\sum\limits_{r=1}^{N_i} \ell_r w_r \overline{\psi}_{r,i,g}}{\sum\limits_{r=1}^{N_i} \ell_r w_r} . + +Here we introduce the term :math:`w_r`, which represents the "weight" of the ray +(its 2D area), such that the volume that a ray is responsible for can be +determined by multiplying its length :math:`\ell` by its weight :math:`w`. As +the scalar flux vector is a shape function only, we are actually free to +multiply all ray weights :math:`w` by any constant such that the overall shape +is still maintained, even if the magnitude of the shape function changes. Thus, +we can simply set :math:`w_r` to be unity for all rays, such that: + +.. math:: + :label: weights + + \text{Volume of cell } i = V_i \approx \sum\limits_{r=1}^{N_i} \ell_r w_r = \sum\limits_{r=1}^{N_i} \ell_r . + +We can then rewrite our discretized equation as: + +.. math:: + :label: discretized_2 + + \phi_{i,g} \approx \frac{\sum\limits_{r=1}^{N_i} \ell_r w_r \overline{\psi}_{r,i,g}}{\sum\limits_{r=1}^{N_i} \ell_r w_r} = \frac{\sum\limits_{r=1}^{N_i} \ell_r \overline{\psi}_{r,i,g}}{\sum\limits_{r=1}^{N_i} \ell_r} . + +Thus, the scalar flux can be inferred if we know the volume weighted sum of the +average angular fluxes that pass through the cell. Substituting +:eq:`average_psi_final` into :eq:`discretized_2`, we arrive at: + +.. math:: + :label: scalar_full + + \phi_{i,g} = \frac{\int_{V_i} \int_{4\pi} \psi(r, \Omega) d\Omega d\mathbf{r}}{\int_{V_i} d\mathbf{r}} = \overline{\overline{\psi}}_{i,g} = \frac{\sum\limits_{r=1}^{N_i} \ell_r \overline{\psi}_{r,i,g}}{\sum\limits_{r=1}^{N_i} \ell_r} = \frac{\sum\limits_{r=1}^{N_i} \ell_r \frac{Q_{i,g}}{\Sigma_{t,i,g}} + \frac{\Delta \psi_{r,g}}{\ell_r \Sigma_{t,i,g}}}{\sum\limits_{r=1}^{N_i} \ell_r}, + +which when partially simplified becomes: + +.. math:: + :label: scalar_four_vols + + \phi = \frac{Q_{i,g} \sum\limits_{r=1}^{N_i} \ell_r}{\Sigma_{t,i,g} \sum\limits_{r=1}^{N_i} \ell_r} + \frac{\sum\limits_{r=1}^{N_i} \ell_r \frac{\Delta \psi_i}{\ell_r}}{\Sigma_{t,i,g} \sum\limits_{r=1}^{N_i} \ell_r} . + +Note that there are now four (seemingly identical) volume terms in this equation. + +~~~~~~~~~~~~~~ +Volume Dilemma +~~~~~~~~~~~~~~ + +At first glance, Equation :eq:`scalar_four_vols` appears ripe for cancellation +of terms. Mathematically, such cancellation allows us to arrive at the following +"naive" estimator for the scalar flux: + +.. math:: + :label: phi_naive + + \phi_{i,g}^{naive} = \frac{Q_{i,g} }{\Sigma_{t,i,g}} + \frac{\sum\limits_{r=1}^{N_i} \Delta \psi_{r,g}}{\Sigma_{t,i,g} \sum\limits_{r=1}^{N_i} \ell_r} . + +This derivation appears mathematically sound at first glance but unfortunately +raises a serious issue as discussed in more depth by `Tramm et al. +`_ and `Cosgrove and Tramm `_. Namely, the second +term: + +.. math:: + :label: ratio_estimator + + \frac{\sum\limits_{r=1}^{N_i} \Delta \psi_{r,g}}{\Sigma_{t,i,g} \sum\limits_{r=1}^{N_i} \ell_r} + +features stochastic variables (the sums over random ray lengths and angular +fluxes) in both the numerator and denominator, making it a stochastic ratio +estimator, which is inherently biased. In practice, usage of the naive estimator +does result in a biased, but "consistent" estimator (i.e., it is biased, but +the bias tends towards zero as the sample size increases). Experimentally, the +right answer can be obtained with this estimator, though a very fine ray density +is required to eliminate the bias. + +How might we solve the biased ratio estimator problem? While there is no obvious +way to alter the numerator term (which arises from the characteristic +integration approach itself), there is potentially more flexibility in how we +treat the stochastic term in the denominator, :math:`\sum\limits_{r=1}^{N_i} +\ell_r` . From Equation :eq:`weights` we know that this term can be directly +inferred from the volume of the problem, which does not actually change between +iterations. Thus, an alternative treatment for this "volume" term in the +denominator is to replace the actual stochastically sampled total track length +with the expected value of the total track length. For instance, if the true +volume of the FSR is known (as is the total volume of the full simulation domain +and the total tracklength used for integration that iteration), then we know the +true expected value of the tracklength in that FSR. That is, if a FSR accounts +for 2% of the overall volume of a simulation domain, then we know that the +expected value of tracklength in that FSR will be 2% of the total tracklength +for all rays that iteration. This is a key insight, as it allows us to the +replace the actual tracklength that was accumulated inside that FSR each +iteration with the expected value. + +If we know the analytical volumes, then those can be used to directly compute +the expected value of the tracklength in each cell. However, as the analytical +volumes are not typically known in OpenMC due to the usage of user-defined +constructive solid geometry, we need to source this quantity from elsewhere. An +obvious choice is to simply accumulate the total tracklength through each FSR +across all iterations (batches) and to use that sum to compute the expected +average length per iteration, as: + +.. math:: + :label: sim_estimator + + \sum\limits^{}_{i} \ell_i \approx \frac{\sum\limits^{B}_{b}\sum\limits^{N_i}_{r} \ell_{b,r} }{B} + +where :math:`b` is a single batch in :math:`B` total batches simulated so far. + +In this manner, the expected value of the tracklength will become more refined +as iterations continue, until after many iterations the variance of the +denominator term becomes trivial compared to the numerator term, essentially +eliminating the presence of the stochastic ratio estimator. A "simulation +averaged" estimator is therefore: + +.. math:: + :label: phi_sim + + \phi_{i,g}^{simulation} = \frac{Q_{i,g} }{\Sigma_{t,i,g}} + \frac{\sum\limits_{r=1}^{N_i} \Delta \psi_{r,g}}{\Sigma_{t,i,g} \frac{\sum\limits^{B}_{b}\sum\limits^{N_i}_{r} \ell_{b,r} }{B}} + +In practical terms, the "simulation averaged" estimator is virtually +indistinguishable numerically from use of the true analytical volume to estimate +this term. Note also that the term "simulation averaged" refers only to the +volume/length treatment, the scalar flux estimate itself is computed fully again +each iteration. + +There are some drawbacks to this method. Recall, this denominator volume term +originally stemmed from taking a volume weighted integral of the angular flux, +in which case the denominator served as a normalization term for the numerator +integral in Equation :eq:`integral`. Essentially, we have now used a different +term for the volume in the numerator as compared to the normalizing volume in +the denominator. The inevitable mismatch (due to noise) between these two +quantities results in a significant increase in variance. Notably, the same +problem occurs if using a tracklength estimate based on the analytical volume, +as again the numerator integral and the normalizing denominator integral no +longer match on a per-iteration basis. + +In practice, the simulation averaged method does completely remove the bias, +though at the cost of a notable increase in variance. Empirical testing reveals +that on most problems, the simulation averaged estimator does win out overall in +numerical performance, as a much coarser quadrature can be used resulting in +faster runtimes overall. Thus, OpenMC uses the simulation averaged estimator in +its random ray mode. + +~~~~~~~~~~~~~~~ +Power Iteration +~~~~~~~~~~~~~~~ + +Given a starting source term, we now have a way of computing an estimate of the +scalar flux in each cell by way of transporting rays randomly through the +domain, recording the change in angular flux for the rays into each cell as they +make their traversals, and summing these contributions up as in Equation +:eq:`phi_sim`. How then do we turn this into an iterative process such that we +improve the estimate of the source and scalar flux over many iterations, given +that our initial starting source will just be a guess? + +The source :math:`Q^{n}` for iteration :math:`n` can be inferred +from the scalar flux from the previous iteration :math:`n-1` as: + +.. math:: + :label: source_update + + Q^{n}(i, g) = \frac{\chi}{k^{n-1}_{eff}} \nu \Sigma_f(i, g) \phi^{n-1}(g) + \sum\limits^{G}_{g'} \Sigma_{s}(i,g,g') \phi^{n-1}(g') + +where :math:`Q^{n}(i, g)` is the total source (fission + scattering) in region +:math:`i` and energy group :math:`g`. Notably, the in-scattering source in group +:math:`g` must be computed by summing over the contributions from all groups +:math:`g' \in G`. + +In a similar manner, the eigenvalue for iteration :math:`n` can be computed as: + +.. math:: + :label: eigenvalue_update + + k^{n}_{eff} = k^{n-1}_{eff} \frac{F^n}{F^{n-1}}, + +where the total spatial- and energy-integrated fission rate :math:`F^n` in +iteration :math:`n` can be computed as: + +.. math:: + :label: fission_source + + F^n = \sum\limits^{M}_{i} \left( V_i \sum\limits^{G}_{g} \nu \Sigma_f(i, g) \phi^{n}(g) \right) + +where :math:`M` is the total number of FSRs in the simulation. Similarly, the +total spatial- and energy-integrated fission rate :math:`F^{n-1}` in iteration +:math:`n-1` can be computed as: + +.. math:: + :label: fission_source_prev + + F^{n-1} = \sum\limits^{M}_{i} \left( V_i \sum\limits^{G}_{g} \nu \Sigma_f(i, g) \phi^{n-1}(g) \right) + +Notably, the volume term :math:`V_i` appears in the eigenvalue update equation. +The same logic applies to the treatment of this term as was discussed earlier. +In OpenMC, we use the "simulation averaged" volume derived from summing over all +ray tracklength contributions to a FSR over all iterations and dividing by the +total integration tracklength to date. Thus, Equation :eq:`fission_source` +becomes: + +.. math:: + :label: fission_source_volumed + + F^n = \sum\limits^{M}_{i} \left( \frac{\sum\limits^{B}_{b}\sum\limits^{N_i}_{r} \ell_{b,r} }{B} \sum\limits^{G}_{g} \nu \Sigma_f(i, g) \phi^{n}(g) \right) + +and a similar substitution can be made to update Equation +:eq:`fission_source_prev` . In OpenMC, the most up-to-date version of the volume +estimate is used, such that the total fission source from the previous iteration +(:math:`n-1`) is also recomputed each iteration. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Ray Starting Conditions and Inactive Length +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Another key area of divergence between deterministic MOC and random ray is the +starting conditions for rays. In deterministic MOC, the angular flux spectrum +for rays are stored at any reflective or periodic boundaries so as to provide a +starting condition for the next iteration. As there are many tracks, storage of +angular fluxes can become costly in terms of memory consumption unless there are +only vacuum boundaries present. + +In random ray, as the starting locations of rays are sampled anew each +iteration, the initial angular flux spectrum for the ray is unknown. While a +guess can be made by taking the isotropic source from the FSR the ray was +sampled in, direct usage of this quantity would result in significant bias and +error being imparted on the simulation. + +Thus, an `on-the-fly approximation method `_ was developed (known +as the "dead zone"), where the first several mean free paths of a ray are +considered to be "inactive" or "read only". In this sense, the angular flux is +solved for using the MOC equation, but the ray does not "tally" any scalar flux +back to the FSRs that it travels through. After several mean free paths have +been traversed, the ray's angular flux spectrum typically becomes dominated by +the accumulated source terms from the cells it has traveled through, while the +(incorrect) starting conditions have been attenuated away. In the animation in +the :ref:`introductory section on this page `, the +yellow portion of the ray lengths is the dead zone. As can be seen in this +animation, the tallied :math:`\sum\limits_{r=1}^{N_i} \Delta \psi_{r,g}` term +that is plotted is not affected by the ray when the ray is within its inactive +length. Only when the ray enters its active mode does the ray contribute to the +:math:`\sum\limits_{r=1}^{N_i} \Delta \psi_{r,g}` sum for the iteration. + +~~~~~~~~~~~~~~~~~~~~~ +Ray Ending Conditions +~~~~~~~~~~~~~~~~~~~~~ + +To ensure that a uniform density of rays is integrated in space and angle +throughout the simulation domain, after exiting the initial inactive "dead zone" +portion of the ray, the rays are run for a user-specified distance. Typically, a +choice of at least several times the length of the inactive "dead zone" is made +so as to amortize the cost of the dead zone. For example, if a dead zone of 30 +cm is selected, then an active length of 300 cm might be selected so that the +cost of the dead zone is at most 10% of the overall runtime. + +-------------------- +Simplified Algorithm +-------------------- + +A simplified set of functions that execute a single random ray power iteration +are given below. Not all global variables are defined in this illustrative +example, but the high level components of the algorithm are shown. A number of +significant simplifications are made for clarity---for example, no inactive +"dead zone" length is shown, geometry operations are abstracted, no parallelism +(or thread safety) is expressed, a naive exponential treatment is used, and rays +are not halted at their exact termination distances, among other subtleties. +Nonetheless, the below algorithms may be useful for gaining intuition on the +basic components of the random ray process. Rather than expressing the algorithm +in abstract pseudocode, C++ is used to make the control flow easier to +understand. + +The first block below shows the logic for a single power iteration (batch): + +.. code-block:: C++ + + double power_iteration(double k_eff) { + + // Update source term (scattering + fission) + update_neutron_source(k_eff); + + // Reset scalar fluxes to zero + fill(global::scalar_flux_new, 0.0f); + + // Transport sweep over all random rays for the iteration + for (int i = 0; i < nrays; i++) { + RandomRay ray; + initialize_ray(ray); + transport_single_ray(ray); + } + + // Normalize scalar flux and update volumes + normalize_scalar_flux_and_volumes(); + + // Add source to scalar flux, compute number of FSR hits + add_source_to_scalar_flux(); + + // Compute k-eff using updated scalar flux + k_eff = compute_k_eff(k_eff); + + // Set phi_old = phi_new + global::scalar_flux_old.swap(global::scalar_flux_new); + + return k_eff; + } + +The second function shows the logic for transporting a single ray within the +transport loop: + +.. code-block:: C++ + + void transport_single_ray(RandomRay& ray) { + + // Reset distance to zero + double distance = 0.0; + + // Continue transport of ray until active length is reached + while (distance < user_setting::active_length) { + // Ray trace to find distance to next surface (i.e., segment length) + double s = distance_to_nearest_boundary(ray); + + // Attenuate flux (and accumulate source/attenuate) on segment + attenuate_flux(ray, s); + + // Advance particle to next surface + ray.location = ray.location + s * ray.direction; + + // Move ray across the surface + cross_surface(ray); + + // Add segment length "s" to total distance traveled + distance += s; + } + } + +The final function below shows the logic for solving for the characteristic MOC +equation (and accumulating the scalar flux contribution of the ray into the +scalar flux value for the FSR). + +.. code-block:: C++ + + void attenuate_flux(RandomRay& ray, double s) { + + // Determine which flat source region (FSR) the ray is currently in + int fsr = get_fsr_id(ray.location); + + // Determine material type + int material = get_material_type(fsr); + + // MOC incoming flux attenuation + source contribution/attenuation equation + for (int e = 0; e < global::n_energy_groups; e++) { + float sigma_t = global::macro_xs[material].total; + float tau = sigma_t * s; + float delta_psi = (ray.angular_flux[e] - global::source[fsr][e] / sigma_t) * (1 - exp(-tau)); + ray.angular_flux_[e] -= delta_psi; + global::scalar_flux_new[fsr][e] += delta_psi; + } + + // Record total tracklength in this FSR (to compute volume) + global::volume[fsr] += s; + } + +------------------------ +How are Tallies Handled? +------------------------ + +Most tallies, filters, and scores that you would expect to work with a +multigroup solver like random ray should work. For example, you can define 3D +mesh tallies with energy filters and flux, fission, and nu-fission scores, etc. +There are some restrictions though. For starters, it is assumed that all filter +mesh boundaries will conform to physical surface boundaries (or lattice +boundaries) in the simulation geometry. It is acceptable for multiple cells +(FSRs) to be contained within a filter mesh cell (e.g., pincell-level or +assembly-level tallies should work), but it is currently left as undefined +behavior if a single simulation cell is able to score to multiple filter mesh +cells. In the future, the capability to fully support mesh tallies may be added +to OpenMC, but for now this restriction needs to be respected. + +--------------------------- +Fundamental Sources of Bias +--------------------------- + +Compared to continuous energy Monte Carlo simulations, the known sources of bias +in random ray particle transport are: + + - **Multigroup Energy Discretization:** The multigroup treatment of flux and + cross sections incurs a significant bias, as a reaction rate (:math:`R_g = + V \phi_g \Sigma_g`) for an energy group :math:`g` can only be conserved + for a given choice of multigroup cross section :math:`\Sigma_g` if the + flux (:math:`\phi_g`) is known a priori. If the flux was already known, + then there would be no point to the simulation, resulting in a fundamental + need for approximating this quantity. There are numerous methods for + generating relatively accurate multigroup cross section libraries that can + each be applied to a narrow design area reliably, although there are + always limitations and/or complexities that arise with a multigroup energy + treatment. This is by far the most significant source of simulation bias + between Monte Carlo and random ray for most problems. While the other + areas typically have solutions that are highly effective at mitigating + bias, error stemming from multigroup energy discretization is much harder + to remedy. + - **Flat Source Approximation:**. In OpenMC, a "flat" (0th order) source + approximation is made, wherein the scattering and fission sources within a + cell are assumed to be spatially uniform. As the source in reality is a + continuous function, this leads to bias, although the bias can be reduced + to acceptable levels if the flat source regions are sufficiently small. + The bias can also be mitigated by assuming a higher-order source (e.g., + linear or quadratic), although OpenMC does not yet have this capability. + In practical terms, this source of bias can become very large if cells are + large (with dimensions beyond that of a typical particle mean free path), + but the subdivision of cells can often reduce this bias to trivial levels. + - **Anisotropic Source Approximation:** In OpenMC, the source is not only + assumed to be flat but also isotropic, leading to bias. It is possible for + MOC (and likely random ray) to treat anisotropy explicitly, but this is + not currently supported in OpenMC. This source of bias is not significant + for some problems, but becomes more problematic for others. Even in the + absence of explicit treatment of anistropy, use of transport-corrected + multigroup cross sections can often mitigate this bias, particularly for + light water reactor simulation problems. + - **Angular Flux Initial Conditions:** Each time a ray is sampled, its + starting angular flux is unknown, so a guess must be made (typically the + source term for the cell it starts in). Usage of an adequate inactive ray + length (dead zone) mitigates this error. As the starting guess is + attenuated at a rate of :math:`\exp(-\Sigma_t \ell)`, this bias can driven + below machine precision in a low cost manner on many problems. + +.. _Tramm-2017a: https://doi.org/10.1016/j.jcp.2017.04.038 +.. _Tramm-2017b: https://doi.org/10.1016/j.anucene.2017.10.015 +.. _Tramm-2018: https://dspace.mit.edu/handle/1721.1/119038 +.. _Tramm-2020: https://doi.org/10.1051/EPJCONF/202124703021 +.. _Cosgrove-2023: https://doi.org/10.1080/00295639.2023.2270618 + +.. only:: html + + .. rubric:: References + +.. [Askew-1972] Askew, “A Characteristics Formulation of the Neutron Transport + Equation in Complicated Geometries.” Technical Report AAEW-M 1108, UK Atomic + Energy Establishment (1972). diff --git a/docs/source/usersguide/data.rst b/docs/source/usersguide/data.rst index 48003ecb7..6af297338 100644 --- a/docs/source/usersguide/data.rst +++ b/docs/source/usersguide/data.rst @@ -279,6 +279,8 @@ The `official ENDF/B-VII.1 HDF5 library multipole library, so if you are using this library, the windowed multipole data will already be available to you. +.. _create_mgxs: + ------------------------- Multigroup Cross Sections ------------------------- diff --git a/docs/source/usersguide/index.rst b/docs/source/usersguide/index.rst index 511bdc6ce..03a77e870 100644 --- a/docs/source/usersguide/index.rst +++ b/docs/source/usersguide/index.rst @@ -25,4 +25,6 @@ essential aspects of using OpenMC to perform simulations. processing parallel volume + random_ray troubleshoot + \ No newline at end of file diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst new file mode 100644 index 000000000..dad86c826 --- /dev/null +++ b/docs/source/usersguide/random_ray.rst @@ -0,0 +1,480 @@ +.. _random_ray: + +================= +Random Ray Solver +================= + +In general, the random ray solver mode uses most of the same settings and +:ref:`run strategies ` as the standard Monte Carlo solver +mode. For instance, random ray solves are also split up into :ref:`inactive and +active batches `. However, there are a couple of settings +that are unique to the random ray solver and a few areas that the random ray +run strategy differs, both of which will be described in this section. + +------------------------ +Enabling Random Ray Mode +------------------------ + +To utilize the random ray solver, the :attr:`~openmc.Settings.random_ray` +dictionary must be present in the :class:`openmc.Settings` Python class. There +are a number of additional settings that must be specified within this +dictionary that will be discussed below. Additionally, the "multi-group" energy +mode must be specified. + +------- +Batches +------- + +In Monte Carlo simulations, inactive batches are used to let the fission source +develop into a stationary distribution before active batches are performed that +actually accumulate statistics. While this is true of random ray as well, in the +random ray mode the inactive batches are also used to let the scattering source +develop. Monte Carlo fully represents the scattering source within each +iteration (by its nature of fully simulating particles from birth to death +through any number of physical scattering events), whereas the scattering source +in random ray can only represent as many scattering events as batches have been +completed. For example, by iteration 10 in random ray, the scattering source +only captures the behavior of neutrons through their 10th scattering event. +Thus, while inactive batches are only required in an eigenvalue solve in Monte +Carlo, **inactive batches are required for both eigenvalue and fixed source +solves in random ray mode** due to this additional need to converge the +scattering source. + +The additional burden of converging the scattering source generally results in a +higher requirement for the number of inactive batches---often by an order of +magnitude or more. For instance, it may be reasonable to only use 50 inactive +batches for a light water reactor simulation with Monte Carlo, but random ray +might require 500 or more inactive batches. Similar to Monte Carlo, +:ref:`Shannon entropy ` can be used to gauge whether the +combined scattering and fission source has fully developed. + +Similar to Monte Carlo, active batches are used in the random ray solver mode to +accumulate and converge statistics on unknown quantities (i.e., the random ray +sources, scalar fluxes, as well as any user-specified tallies). + +The batch parameters are set in the same manner as with the regular Monte Carlo +solver:: + + settings = openmc.Settings() + settings.energy_mode = "multi-group" + settings.batches = 1200 + settings.inactive = 600 + +------------------------------- +Inactive Ray Length (Dead Zone) +------------------------------- + +A major issue with random ray is that the starting angular flux distribution for +each sampled ray is unknown. Thus, an on-the-fly method is used to build a high +quality approximation of the angular flux of the ray each iteration. This is +accomplished by running the ray through an inactive length (also known as a dead +zone length), where the ray is moved through the geometry and its angular flux +is solved for via the normal :ref:`MOC ` equation, but +no information is written back to the system. Thus, the ray is run in a "read +only" mode for the set inactive length. This parameter can be adjusted, in units +of cm, as:: + + settings.random_ray['distance_inactive'] = 40.0 + +After several mean free paths are traversed, the angular flux spectrum of the +ray becomes dominated by the in-scattering and fission source components that it +picked up when travelling through the geometry, while its original (incorrect) +starting angular flux is attenuated toward zero. Thus, longer selections of +inactive ray length will asymptotically approach the true angular flux. + +In practice, 10 mean free paths are sufficient (with light water reactors often +requiring only about 10--50 cm of inactive ray length for the error to become +undetectable). However, we caution that certain models with large quantities of +void regions (even if just limited to a few streaming channels) may require +significantly longer inactive ray lengths to ensure that the angular flux is +accurate before the conclusion of the inactive ray length. Additionally, +problems where a sensitive estimate of the uncollided flux is required (e.g., +the detector response to fast neutrons is required, and the detected is located +far away from the source in a moderator region) may require the user to specify +an inactive length that is derived from the pyhsical geometry of the simulation +problem rather than its material properties. For instance, consider a detector +placed 30 cm outside of a reactor core, with a moderator region separating the +detector from the core. In this case, rays sampled in the moderator region and +heading toward the detector will begin life with a highly scattered thermal +spectrum and will have an inaccurate fast spectrum. If the dead zone length is +only 20 cm, we might imagine such rays writing to the detector tally within +their active lengths, despite their innaccurate estimate of the uncollided fast +angular flux. Thus, an inactive length of 100--200 cm would ensure that any such +rays would still be within their inactive regions, and only rays that have +actually traversed through the core (and thus have an accurate representation of +the core's emitted fast flux) will score to the detector region while in their +active phase. + + +------------------------------------ +Active Ray Length and Number of Rays +------------------------------------ + +Once the inactive length of the ray has completed, the active region of the ray +begins. The ray is now run in regular mode, where changes in angular flux as it +traverses through each flat source region are written back to the system, so as +to contribute to the estimate for the iteration scalar flux (which is used to +compute the source for the next iteration). The active ray length can be +adjusted, in units of [cm], as:: + + settings.random_ray['distance_active'] = 400.0 + +Assuming that a sufficient inactive ray length is used so that the starting +angular flux is highly accurate, any selection of active length greater than +zero is theoretically acceptable. However, in order to adequately sample the +full integration domain, a selection of a very short track length would require +a very high number of rays to be selected. Due to the static costs per ray of +computing the starting angular flux in the dead zone, typically very short ray +lengths are undesireable. Thus, to amortize the per-ray cost of the inactive +region of the ray, it is desirable to select a very long inactive ray length. +For example, if the inactive length is set to 20 cm, a 200 cm active ray length +ensures that only about 10% of the overall simulation runtime is spent in the +inactive ray phase integration, making the dead zone a relatively inexpensive +way of estimating the angular flux. + +Thus, to fully amortize the cost of the dead zone integration, one might ask why +not simply run a single ray per iteration with an extremely long active length? +While this is also theoretically possible, this results in two issues. The first +problem is that each ray only represents a single angular sample. As we want to +sample the angular phase space of the simulation with similar fidelity to the +spatial phase space, we naturally want a lot of angles. This means in practice, +we want to balance the need to amortize the cost of the inactive region of the +ray with the need to sample lots of angles. The second problem is that +parallelism in OpenMC is expressed in terms of rays, with each being processed +by an independent MPI rank and/or OpenMP thread, thus we want to ensure each +thread has many rays to process. + +In practical terms, the best strategy is typically to set an active ray length +that is about 10 times that of the inactive ray length. This is often the right +balance between ensuring not too much time is spent in the dead zone, while +still adequately sampling the angular phase space. However, as discussed in the +previous section, some types of simulation may demand that additional thought be +applied to this parameter. For instance, in the same example where we have a +detector region far outside a reactor core, we want to make sure that there is +enough active ray length that rays exiting the core can reach the detector +region. For example, if the detector were to be 30 cm outside of the core, then +we would need to ensure that at least a few hundred cm of active length were +used so as to ensure even rays with indirect angles will be able to reach the +target region. + +The number of rays each iteration can be set by reusing the normal Monte Carlo +particle count selection parameter, as:: + + settings.particles = 2000 + +----------- +Ray Density +----------- + +In the preceding sections, it was argued that for most use cases, the inactive +length for a ray can be determined by taking a multiple of the mean free path +for the limiting energy group. The active ray length could then be set by taking +a multiple of the inactive length. With these parameters set, how many rays per +iteration should be run? + +There are three basic settings that control the density of the stochastic +quadrature being used to integrate the domain each iteration. These three +variables are: + +- The number of rays (in OpenMC settings parlance, "particles") +- The inactive distance per ray +- The active distance per ray + +While the inactive and active ray lengths can usually be chosen by simply +examining the geometry, tallies, and cross section data, one has much more +flexibility in the choice of the number of rays to run. Consider a few +scenarios: + +- If a choice of zero rays is made, then no information is gained by the system + after each batch. +- If a choice of rays close to zero is made, then some information is gained + after each batch, but many source regions may not have been visited that + iteration, which is not ideal numerically and can result in instability. + Empirically, we have found that the simulation can remain stable and produce + accurate results even when on average 20% or more of the cells have zero rays + passing through them each iteration. However, besides the cost of transporting + rays, a new neutron source must be computed based on the scalar flux at each + iteration. This cost is dictated only by the number of source regions and + energy groups---it is independent of the number of rays. Thus, in practical + terms, if too few rays are run, then the simulation runtime becomes dominated + by the fixed cost of source updates, making it inefficient overall given that + a huge number of active batches will likely be required to converge statistics + to acceptable levels. Additionally, if many cells are missed each iteration, + then the fission and scattering sources may not develop very quickly, + resulting in a need for far more inactive batches than might otherwise be + required. +- If a choice of running a very large number of rays is made such that you + guarantee that all cells are hit each iteration, this avoids any issues with + numerical instability. As even more rays are run, this reduces the number of + active batches that must be used to converge statistics and therefore + minimizes the fixed per-iteration source update costs. While this seems + advantageous, it has the same practical downside as with Monte Carlo---namely, + that the inactive batches tend to be overly well integrated, resulting in a + lot of wasted time. This issue is actually much more serious than in Monte + Carlo (where typically only tens of inactive batches are needed), as random + ray often requires hundreds or even thousands of inactive batches. Thus, + minimizing the cost of the source updates in the active phase needs to be + balanced against the increased cost of the inactive phase of the simulation. +- If a choice of rays is made such that relatively few (e.g., around 0.1%) of + cells are missed each iteration, the cost of the inactive batches of the + simulation is minimized. In this "goldilocks" regime, there is very little + chance of numerical instability, and enough information is gained by each cell + to progress the fission and scattering sources forward at their maximum rate. + However, the inactive batches can proceed with minimal cost. While this will + result in the active phase of the simulation requiring more batches (and + correspondingly higher source update costs), the added cost is typically far + less than the savings by making the inactive phase much cheaper. + +To help you set this parameter, OpenMC will report the average flat source +region miss rate at the end of the simulation. Additionally, OpenMC will alert +you if very high miss rates are detected, indicating that more rays and/or a +longer active ray length might improve numerical performance. Thus, a "guess and +check" approach to this parameter is recommended, where a very low guess is +made, a few iterations are performed, and then the simulation is restarted with +a larger value until the "low ray density" messages go away. + +.. note:: + In summary, the user should select an inactive length corresponding to many + times the mean free path of a particle, generally O(10--100) cm, to ensure accuracy of + the starting angular flux. The active length should be 10× the inactive + length to amortize its cost. The number of rays should be enough so that + nearly all :ref:`FSRs ` are hit at least once each power iteration (the hit fraction + is reported by OpenMC for empirical user adjustment). + +.. warning:: + For simulations where long range uncollided flux estimates need to be + accurately resolved (e.g., shielding, detector response, and problems with + significant void areas), make sure that selections for inactive and active + ray lengths are sufficiently long to allow for transport to occur between + source and target regions of interest. + +---------- +Ray Source +---------- + +Random ray requires that the ray source be uniform in space and isotropic in +angle. To facilitate sampling, the user must specify a single random ray source +for sampling rays in both eigenvalue and fixed source solver modes. The random +ray integration source should be of type :class:`openmc.IndependentSource`, and +is specified as part of the :attr:`openmc.Settings.random_ray` dictionary. Note +that the source must not be limited to only fissionable regions. Additionally, +the source box must cover the entire simulation domain. In the case of a +simulation domain that is not box shaped, a box source should still be used to +bound the domain but with the source limited to rejection sampling the actual +simulation universe (which can be specified via the ``domains`` field of the +:class:`openmc.IndependentSource` Python class). Similar to Monte Carlo sources, +for two-dimensional problems (e.g., a 2D pincell) it is desirable to make the +source bounded near the origin of the infinite dimension. An example of an +acceptable ray source for a two-dimensional 2x2 lattice would look like: + +:: + + pitch = 1.26 + lower_left = (-pitch, -pitch, -pitch) + upper_right = ( pitch, pitch, pitch) + uniform_dist = openmc.stats.Box(lower_left, upper_right) + settings.random_ray['ray_source'] = openmc.IndependentSource(space=uniform_dist) + +.. note:: + The random ray source is not related to the underlying particle flux or + source distribution of the simulation problem. It is akin to the selection + of an integration quadrature. Thus, in fixed source mode, the ray source + still needs to be provided and still needs to be uniform in space and angle + throughout the simulation domain. In fixed source mode, the user will + provide physical particle fixed sources in addition to the random ray + source. + +.. _subdivision_fsr: + +---------------------------------- +Subdivision of Flat Source Regions +---------------------------------- + +While the scattering and fission sources in Monte Carlo +are treated continuously, they are assumed to be invariant (flat) within a +MOC or random ray flat source region (FSR). This introduces bias into the +simulation, which can be remedied by reducing the physical size of the FSR +to dimensions below that of typical mean free paths of particles. + +In OpenMC, this subdivision currently must be done manually. The level of +subdivision needed will be dependent on the fidelity the user requires. For +typical light water reactor analysis, consider the following example subdivision +of a two-dimensional 2x2 reflective pincell lattice: + +.. figure:: ../_images/2x2_materials.jpeg + :class: with-border + :width: 400 + + Material definition for an asymmetrical 2x2 lattice (1.26 cm pitch) + +.. figure:: ../_images/2x2_fsrs.jpeg + :class: with-border + :width: 400 + + FSR decomposition for an asymmetrical 2x2 lattice (1.26 cm pitch) + +In the future, automated subdivision of FSRs via mesh overlay may be supported. + +------- +Tallies +------- + +Most tallies, filters, and scores that you would expect to work with a +multigroup solver like random ray are supported. For example, you can define 3D +mesh tallies with energy filters and flux, fission, and nu-fission scores, etc. +There are some restrictions though. For starters, it is assumed that all filter +mesh boundaries will conform to physical surface boundaries (or lattice +boundaries) in the simulation geometry. It is acceptable for multiple cells +(FSRs) to be contained within a mesh element (e.g., pincell-level or +assembly-level tallies should work), but it is currently left as undefined +behavior if a single simulation cell is contained in multiple mesh elements. + +Supported scores: + - flux + - total + - fission + - nu-fission + - events + +Supported Estimators: + - tracklength + +Supported Filters: + - cell + - cell instance + - distribcell + - energy + - material + - mesh + - universe + +Note that there is no difference between the analog, tracklength, and collision +estimators in random ray mode as individual particles are not being simulated. +Tracklength-style tally estimation is inherent to the random ray method. + +-------- +Plotting +-------- + +Visualization of geometry is handled in the same way as normal with OpenMC (see +:ref:`plotting guide ` for more details). That is, ``openmc +--plot`` is handled without any modifications, as the random ray solver uses the +same geometry definition as in Monte Carlo. + +In addition to OpenMC's standard geometry plotting mode, the random ray solver +also features an additional method of data visualization. If a ``plots.xml`` +file is present, any voxel plots that are defined will be output at the end of a +random ray simulation. Rather than being stored in HDF5 file format, the random +ray plotting will generate ``.vtk`` files that can be directly read and plotted +with `Paraview `_. + +In fixed source Monte Carlo (MC) simulations, by default the only thing global +tally provided is the leakage fraction. In a k-eigenvalue MC simulation, by +default global tallies are collected for the eigenvalue and leakage fraction. +Spatial flux information must be manually requested, and often fine-grained +spatial meshes are considered costly/unnecessary, so it is impractical in MC +mode to plot spatial flux or power info by default. Conversely, in random ray, +the solver functions by estimating the multigroup source and flux spectrums in +every fine-grained FSR each iteration. Thus, for random ray, in both fixed +source and eigenvalue simulations, the simulation always finishes with a well +converged flux estimate for all areas. As such, it is much more common in random +ray, MOC, and other deterministic codes to provide spatial flux information by +default. In the future, all FSR data will be made available in the statepoint +file, which facilitates plotting and manipulation through the Python API; at +present, statepoint support is not available. + +Only voxel plots will be used to generate output; other plot types present in +the ``plots.xml`` file will be ignored. The following fields will be written to +the VTK structured grid file: + + - material + - FSR index + - flux spectrum (for each energy group) + - total fission source (integrated across all energy groups) + +------------------------------------------ +Inputting Multigroup Cross Sections (MGXS) +------------------------------------------ + +Multigroup cross sections for use with OpenMC's random ray solver are input the +same way as with OpenMC's traditional multigroup Monte Carlo mode. There is more +information on generating multigroup cross sections via OpenMC in the +:ref:`multigroup materials ` user guide. You may also wish to +use an existing multigroup library. An example of using OpenMC's Python +interface to generate a correctly formatted ``mgxs.h5`` input file is given +in the `OpenMC Jupyter notebook collection +`_. + +.. note:: + Currently only isotropic and isothermal multigroup cross sections are + supported in random ray mode. To represent multiple material temperatures, + separate materials can be defined each with a separate multigroup dataset + corresponding to a given temperature. + +--------------------------------------- +Putting it All Together: Example Inputs +--------------------------------------- + +An example of a settings definition for random ray is given below:: + + # Geometry and MGXS material definition of 2x2 lattice (not shown) + pitch = 1.26 + group_edges = [1e-5, 0.0635, 10.0, 1.0e2, 1.0e3, 0.5e6, 1.0e6, 20.0e6] + ... + + # Instantiate a settings object for a random ray solve + settings = openmc.Settings() + settings.energy_mode = "multi-group" + settings.batches = 1200 + settings.inactive = 600 + settings.particles = 2000 + + settings.random_ray['distance_inactive'] = 40.0 + settings.random_ray['distance_active'] = 400.0 + + # Create an initial uniform spatial source distribution for sampling rays + lower_left = (-pitch, -pitch, -pitch) + upper_right = ( pitch, pitch, pitch) + uniform_dist = openmc.stats.Box(lower_left, upper_right) + settings.random_ray['ray_source'] = openmc.IndependentSource(space=uniform_dist) + + settings.export_to_xml() + + # Define tallies + + # Create a mesh filter + mesh = openmc.RegularMesh() + mesh.dimension = (2, 2) + mesh.lower_left = (-pitch/2, -pitch/2) + mesh.upper_right = (pitch/2, pitch/2) + mesh_filter = openmc.MeshFilter(mesh) + + # Create a multigroup energy filter + energy_filter = openmc.EnergyFilter(group_edges) + + # Create tally using our two filters and add scores + tally = openmc.Tally() + tally.filters = [mesh_filter, energy_filter] + tally.scores = ['flux', 'fission', 'nu-fission'] + + # Instantiate a Tallies collection and export to XML + tallies = openmc.Tallies([tally]) + tallies.export_to_xml() + + # Create voxel plot + plot = openmc.Plot() + plot.origin = [0, 0, 0] + plot.width = [2*pitch, 2*pitch, 1] + plot.pixels = [1000, 1000, 1] + plot.type = 'voxel' + + # Instantiate a Plots collection and export to XML + plots = openmc.Plots([plot]) + plots.export_to_xml() + +All other inputs (e.g., geometry, materials) will be unchanged from a typical +Monte Carlo run (see the :ref:`geometry ` and +:ref:`multigroup materials ` user guides for more information). + +There is also a complete example of a pincell available in the +``openmc/examples/pincell_random_ray`` folder. diff --git a/examples/pincell_random_ray/build_xml.py b/examples/pincell_random_ray/build_xml.py new file mode 100644 index 000000000..b3dd8020a --- /dev/null +++ b/examples/pincell_random_ray/build_xml.py @@ -0,0 +1,203 @@ +import numpy as np +import openmc +import openmc.mgxs + +############################################################################### +# Create multigroup data + +# Instantiate the energy group data +group_edges = [1e-5, 0.0635, 10.0, 1.0e2, 1.0e3, 0.5e6, 1.0e6, 20.0e6] +groups = openmc.mgxs.EnergyGroups(group_edges) + +# Instantiate the 7-group (C5G7) cross section data +uo2_xsdata = openmc.XSdata('UO2', groups) +uo2_xsdata.order = 0 +uo2_xsdata.set_total( + [0.1779492, 0.3298048, 0.4803882, 0.5543674, 0.3118013, 0.3951678, + 0.5644058]) +uo2_xsdata.set_absorption([8.0248e-03, 3.7174e-03, 2.6769e-02, 9.6236e-02, + 3.0020e-02, 1.1126e-01, 2.8278e-01]) +scatter_matrix = np.array( + [[[0.1275370, 0.0423780, 0.0000094, 0.0000000, 0.0000000, 0.0000000, 0.0000000], + [0.0000000, 0.3244560, 0.0016314, 0.0000000, 0.0000000, 0.0000000, 0.0000000], + [0.0000000, 0.0000000, 0.4509400, 0.0026792, 0.0000000, 0.0000000, 0.0000000], + [0.0000000, 0.0000000, 0.0000000, 0.4525650, 0.0055664, 0.0000000, 0.0000000], + [0.0000000, 0.0000000, 0.0000000, 0.0001253, 0.2714010, 0.0102550, 0.0000000], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0012968, 0.2658020, 0.0168090], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0085458, 0.2730800]]]) +scatter_matrix = np.rollaxis(scatter_matrix, 0, 3) +uo2_xsdata.set_scatter_matrix(scatter_matrix) +uo2_xsdata.set_fission([7.21206e-03, 8.19301e-04, 6.45320e-03, + 1.85648e-02, 1.78084e-02, 8.30348e-02, + 2.16004e-01]) +uo2_xsdata.set_nu_fission([2.005998e-02, 2.027303e-03, 1.570599e-02, + 4.518301e-02, 4.334208e-02, 2.020901e-01, + 5.257105e-01]) +uo2_xsdata.set_chi([5.8791e-01, 4.1176e-01, 3.3906e-04, 1.1761e-07, 0.0000e+00, + 0.0000e+00, 0.0000e+00]) + +h2o_xsdata = openmc.XSdata('LWTR', groups) +h2o_xsdata.order = 0 +h2o_xsdata.set_total([0.15920605, 0.412969593, 0.59030986, 0.58435, + 0.718, 1.2544497, 2.650379]) +h2o_xsdata.set_absorption([6.0105e-04, 1.5793e-05, 3.3716e-04, + 1.9406e-03, 5.7416e-03, 1.5001e-02, + 3.7239e-02]) +scatter_matrix = np.array( + [[[0.0444777, 0.1134000, 0.0007235, 0.0000037, 0.0000001, 0.0000000, 0.0000000], + [0.0000000, 0.2823340, 0.1299400, 0.0006234, 0.0000480, 0.0000074, 0.0000010], + [0.0000000, 0.0000000, 0.3452560, 0.2245700, 0.0169990, 0.0026443, 0.0005034], + [0.0000000, 0.0000000, 0.0000000, 0.0910284, 0.4155100, 0.0637320, 0.0121390], + [0.0000000, 0.0000000, 0.0000000, 0.0000714, 0.1391380, 0.5118200, 0.0612290], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0022157, 0.6999130, 0.5373200], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.1324400, 2.4807000]]]) +scatter_matrix = np.rollaxis(scatter_matrix, 0, 3) +h2o_xsdata.set_scatter_matrix(scatter_matrix) + +mg_cross_sections_file = openmc.MGXSLibrary(groups) +mg_cross_sections_file.add_xsdatas([uo2_xsdata, h2o_xsdata]) +mg_cross_sections_file.export_to_hdf5() + +############################################################################### +# Create materials for the problem + +# Instantiate some Materials and register the appropriate macroscopic data +uo2 = openmc.Material(name='UO2 fuel') +uo2.set_density('macro', 1.0) +uo2.add_macroscopic('UO2') + +water = openmc.Material(name='Water') +water.set_density('macro', 1.0) +water.add_macroscopic('LWTR') + +# Instantiate a Materials collection and export to XML +materials_file = openmc.Materials([uo2, water]) +materials_file.cross_sections = "mgxs.h5" +materials_file.export_to_xml() + +############################################################################### +# Define problem geometry + +# The geometry we will define a simplified pincell with fuel radius 0.54 cm +# surrounded by moderator (same as in the multigroup example). +# In random ray, we typically want several radial regions and azimuthal +# sectors in both the fuel and moderator areas of the pincell. This is +# due to the flat source approximation requiring that source regions are +# small compared to the typical mean free path of a neutron. Below we +# sudivide the basic pincell into 8 aziumthal sectors (pizza slices) and +# 5 concentric rings in both the fuel and moderator. + +# TODO: When available in OpenMC, use cylindrical lattice instead to +# simplify definition and improve runtime performance. + +pincell_base = openmc.Universe() + +# These are the subdivided radii (creating 5 concentric regions in the +# fuel and moderator) +ring_radii = [0.241, 0.341, 0.418, 0.482, 0.54, 0.572, 0.612, 0.694, 0.786] +fills = [uo2, uo2, uo2, uo2, uo2, water, water, water, water, water] + +# We then create cells representing the bounded rings, with special +# treatment for both the innermost and outermost cells +cells = [] +for r in range(10): + cell = [] + if r == 0: + outer_bound = openmc.ZCylinder(r=ring_radii[r]) + cell = openmc.Cell(fill=fills[r], region=-outer_bound) + elif r == 9: + inner_bound = openmc.ZCylinder(r=ring_radii[r-1]) + cell = openmc.Cell(fill=fills[r], region=+inner_bound) + else: + inner_bound = openmc.ZCylinder(r=ring_radii[r-1]) + outer_bound = openmc.ZCylinder(r=ring_radii[r]) + cell = openmc.Cell(fill=fills[r], region=+inner_bound & -outer_bound) + pincell_base.add_cell(cell) + +# We then generate 8 planes to bound 8 azimuthal sectors +azimuthal_planes = [] +for i in range(8): + angle = 2 * i * openmc.pi / 8 + normal_vector = (-openmc.sin(angle), openmc.cos(angle), 0) + azimuthal_planes.append(openmc.Plane(a=normal_vector[0], b=normal_vector[1], c=normal_vector[2], d=0)) + +# Create a cell for each azimuthal sector using the pincell base class +azimuthal_cells = [] +for i in range(8): + azimuthal_cell = openmc.Cell(name=f'azimuthal_cell_{i}') + azimuthal_cell.fill = pincell_base + azimuthal_cell.region = +azimuthal_planes[i] & -azimuthal_planes[(i+1) % 8] + azimuthal_cells.append(azimuthal_cell) + +# Create the (subdivided) geometry with the azimuthal universes +pincell = openmc.Universe(cells=azimuthal_cells) + +# Create a region represented as the inside of a rectangular prism +pitch = 1.26 +box = openmc.model.RectangularPrism(pitch, pitch, boundary_type='reflective') +pincell_bounded = openmc.Cell(fill=pincell, region=-box, name='pincell') + +# Create a geometry (specifying merge surfaces option to remove +# all the redundant cylinder/plane surfaces) and export to XML +geometry = openmc.Geometry([pincell_bounded], merge_surfaces=True) +geometry.export_to_xml() + +############################################################################### +# Define problem settings + +# Instantiate a Settings object, set all runtime parameters, and export to XML +settings = openmc.Settings() +settings.energy_mode = "multi-group" +settings.batches = 600 +settings.inactive = 300 +settings.particles = 50 + +# Create an initial uniform spatial source distribution for sampling rays. +# Note that this must be uniform in space and angle. +lower_left = (-pitch/2, -pitch/2, -1) +upper_right = (pitch/2, pitch/2, 1) +uniform_dist = openmc.stats.Box(lower_left, upper_right) +settings.random_ray['ray_source'] = openmc.IndependentSource(space=uniform_dist) +settings.random_ray['distance_inactive'] = 40.0 +settings.random_ray['distance_active'] = 400.0 + +settings.export_to_xml() + +############################################################################### +# Define tallies + +# Create a mesh that will be used for tallying +mesh = openmc.RegularMesh() +mesh.dimension = (2, 2) +mesh.lower_left = (-pitch/2, -pitch/2) +mesh.upper_right = (pitch/2, pitch/2) + +# Create a mesh filter that can be used in a tally +mesh_filter = openmc.MeshFilter(mesh) + +# Let's also create a filter to measure each group +# indepdendently +energy_filter = openmc.EnergyFilter(group_edges) + +# Now use the mesh filter in a tally and indicate what scores are desired +tally = openmc.Tally(name="Mesh and Energy tally") +tally.filters = [mesh_filter, energy_filter] +tally.scores = ['flux', 'fission', 'nu-fission'] + +# Instantiate a Tallies collection and export to XML +tallies = openmc.Tallies([tally]) +tallies.export_to_xml() + +############################################################################### +# Exporting to OpenMC plots.xml file +############################################################################### + +plot = openmc.Plot() +plot.origin = [0, 0, 0] +plot.width = [pitch, pitch, pitch] +plot.pixels = [1000, 1000, 1] +plot.type = 'voxel' + +# Instantiate a Plots collection and export to XML +plots = openmc.Plots([plot]) +plots.export_to_xml() diff --git a/include/openmc/boundary_condition.h b/include/openmc/boundary_condition.h index e63e232e9..cd8988162 100644 --- a/include/openmc/boundary_condition.h +++ b/include/openmc/boundary_condition.h @@ -10,6 +10,7 @@ namespace openmc { // Forward declare some types used in function arguments. class Particle; +class RandomRay; class Surface; //============================================================================== diff --git a/include/openmc/cell.h b/include/openmc/cell.h index c405f536b..e68443a32 100644 --- a/include/openmc/cell.h +++ b/include/openmc/cell.h @@ -249,6 +249,42 @@ public: std::unordered_map> get_contained_cells( int32_t instance = 0, Position* hint = nullptr) const; + //! Determine the material index corresponding to a specific cell instance, + //! taking into account presence of distribcell material + //! \param[in] instance of the cell + //! \return material index + int32_t material(int32_t instance) const + { + // If distributed materials are used, then each instance has its own + // material definition. If distributed materials are not used, then + // all instances used the same material stored at material_[0]. The + // presence of distributed materials is inferred from the size of + // the material_ vector being greater than one. + if (material_.size() > 1) { + return material_[instance]; + } else { + return material_[0]; + } + } + + //! Determine the temperature index corresponding to a specific cell instance, + //! taking into account presence of distribcell temperature + //! \param[in] instance of the cell + //! \return temperature index + double sqrtkT(int32_t instance) const + { + // If distributed materials are used, then each instance has its own + // temperature definition. If distributed materials are not used, then + // all instances used the same temperature stored at sqrtkT_[0]. The + // presence of distributed materials is inferred from the size of + // the sqrtkT_ vector being greater than one. + if (sqrtkT_.size() > 1) { + return sqrtkT_[instance]; + } else { + return sqrtkT_[0]; + } + } + protected: //! Determine the path to this cell instance in the geometry hierarchy //! \param[in] instance of the cell to find parent cells for diff --git a/include/openmc/constants.h b/include/openmc/constants.h index ba558b040..1c683e5f5 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -340,6 +340,8 @@ enum class RunMode { VOLUME }; +enum class SolverType { MONTE_CARLO, RANDOM_RAY }; + //============================================================================== // Geometry Constants diff --git a/include/openmc/mgxs.h b/include/openmc/mgxs.h index d77c34bd5..3fb060810 100644 --- a/include/openmc/mgxs.h +++ b/include/openmc/mgxs.h @@ -29,7 +29,6 @@ private: int num_delayed_groups; // number of delayed neutron groups vector xs; // Cross section data // MGXS Incoming Flux Angular grid information - bool is_isotropic; // used to skip search for angle indices if isotropic int n_pol; int n_azi; vector polar; @@ -85,6 +84,8 @@ public: std::string name; // name of dataset, e.g., UO2 double awr; // atomic weight ratio bool fissionable; // Is this fissionable + bool is_isotropic { + true}; // used to skip search for angle indices if isotropic Mgxs() = default; diff --git a/include/openmc/output.h b/include/openmc/output.h index 1ef95a887..1ece3de96 100644 --- a/include/openmc/output.h +++ b/include/openmc/output.h @@ -57,6 +57,8 @@ void print_results(); void write_tallies(); +void show_time(const char* label, double secs, int indent_level = 0); + } // namespace openmc #endif // OPENMC_OUTPUT_H @@ -80,4 +82,4 @@ struct formatter> { } }; -} // namespace fmt \ No newline at end of file +} // namespace fmt diff --git a/include/openmc/plot.h b/include/openmc/plot.h index 7b66036fb..ee6c3fbec 100644 --- a/include/openmc/plot.h +++ b/include/openmc/plot.h @@ -100,6 +100,7 @@ public: virtual void print_info() const = 0; const std::string& path_plot() const { return path_plot_; } + std::string& path_plot() { return path_plot_; } int id() const { return id_; } int level() const { return level_; } diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h new file mode 100644 index 000000000..5f64914ed --- /dev/null +++ b/include/openmc/random_ray/flat_source_domain.h @@ -0,0 +1,141 @@ +#ifndef OPENMC_RANDOM_RAY_FLAT_SOURCE_DOMAIN_H +#define OPENMC_RANDOM_RAY_FLAT_SOURCE_DOMAIN_H + +#include "openmc/openmp_interface.h" +#include "openmc/position.h" + +namespace openmc { + +/* + * The FlatSourceDomain class encompasses data and methods for storing + * scalar flux and source region for all flat source regions in a + * random ray simulation domain. + */ + +class FlatSourceDomain { +public: + //---------------------------------------------------------------------------- + // Helper Structs + + // A mapping object that is used to map between a specific random ray + // source region and an OpenMC native tally bin that it should score to + // every iteration. + struct TallyTask { + int tally_idx; + int filter_idx; + int score_idx; + int score_type; + TallyTask(int tally_idx, int filter_idx, int score_idx, int score_type) + : tally_idx(tally_idx), filter_idx(filter_idx), score_idx(score_idx), + score_type(score_type) + {} + }; + + //---------------------------------------------------------------------------- + // Constructors + FlatSourceDomain(); + + //---------------------------------------------------------------------------- + // Methods + void update_neutron_source(double k_eff); + double compute_k_eff(double k_eff_old) const; + void normalize_scalar_flux_and_volumes( + double total_active_distance_per_iteration); + int64_t add_source_to_scalar_flux(); + void batch_reset(); + void convert_source_regions_to_tallies(); + void random_ray_tally() const; + void accumulate_iteration_flux(); + void output_to_vtk() const; + void all_reduce_replicated_source_regions(); + + //---------------------------------------------------------------------------- + // Public Data members + + bool mapped_all_tallies_ {false}; // If all source regions have been visited + + int64_t n_source_regions_ {0}; // Total number of source regions in the model + + // 1D array representing source region starting offset for each OpenMC Cell + // in model::cells + vector source_region_offsets_; + + // 1D arrays representing values for all source regions + vector lock_; + vector was_hit_; + vector volume_; + vector position_recorded_; + vector position_; + + // 2D arrays stored in 1D representing values for all source regions x energy + // groups + vector scalar_flux_old_; + vector scalar_flux_new_; + vector source_; + + //---------------------------------------------------------------------------- + // Private data members +private: + int negroups_; // Number of energy groups in simulation + int64_t n_source_elements_ {0}; // Total number of source regions in the model + // times the number of energy groups + + // 2D array representing values for all source regions x energy groups x tally + // tasks + vector> tally_task_; + + // 1D arrays representing values for all source regions + vector material_; + vector volume_t_; + + // 2D arrays stored in 1D representing values for all source regions x energy + // groups + vector scalar_flux_final_; + +}; // class FlatSourceDomain + +//============================================================================ +//! Non-member functions +//============================================================================ + +// Returns the inputted value in big endian byte ordering. If the system is +// little endian, the byte ordering is flipped. If the system is big endian, +// the inputted value is returned as is. This function is necessary as +// .vtk binary files use big endian byte ordering. +template +T convert_to_big_endian(T in) +{ + // 4 byte integer + uint32_t test = 1; + + // 1 byte pointer to first byte of test integer + uint8_t* ptr = reinterpret_cast(&test); + + // If the first byte of test is 0, then the system is big endian. In this + // case, we don't have to do anything as .vtk files are big endian + if (*ptr == 0) + return in; + + // Otherwise, the system is in little endian, so we need to flip the + // endianness + uint8_t* orig = reinterpret_cast(&in); + uint8_t swapper[sizeof(T)]; + for (int i = 0; i < sizeof(T); i++) { + swapper[i] = orig[sizeof(T) - i - 1]; + } + T out = *reinterpret_cast(&swapper); + return out; +} + +template +void parallel_fill(vector& arr, T value) +{ +#pragma omp parallel for schedule(static) + for (int i = 0; i < arr.size(); i++) { + arr[i] = value; + } +} + +} // namespace openmc + +#endif // OPENMC_RANDOM_RAY_FLAT_SOURCE_DOMAIN_H diff --git a/include/openmc/random_ray/random_ray.h b/include/openmc/random_ray/random_ray.h new file mode 100644 index 000000000..c114007c6 --- /dev/null +++ b/include/openmc/random_ray/random_ray.h @@ -0,0 +1,55 @@ +#ifndef OPENMC_RANDOM_RAY_H +#define OPENMC_RANDOM_RAY_H + +#include "openmc/memory.h" +#include "openmc/particle.h" +#include "openmc/random_ray/flat_source_domain.h" +#include "openmc/source.h" + +namespace openmc { + +/* + * The RandomRay class encompasses data and methods for transporting random rays + * through the model. It is a small extension of the Particle class. + */ + +// TODO: Inherit from GeometryState instead of Particle +class RandomRay : public Particle { +public: + //---------------------------------------------------------------------------- + // Constructors + RandomRay(); + RandomRay(uint64_t ray_id, FlatSourceDomain* domain); + + //---------------------------------------------------------------------------- + // Methods + void event_advance_ray(); + void attenuate_flux(double distance, bool is_active); + void initialize_ray(uint64_t ray_id, FlatSourceDomain* domain); + uint64_t transport_history_based_single_ray(); + + //---------------------------------------------------------------------------- + // Static data members + static double distance_inactive_; // Inactive (dead zone) ray length + static double distance_active_; // Active ray length + static unique_ptr ray_source_; // Starting source for ray sampling + + //---------------------------------------------------------------------------- + // Public data members + vector angular_flux_; + + //---------------------------------------------------------------------------- + // Private data members +private: + FlatSourceDomain* domain_ {nullptr}; // pointer to domain that has flat source + // data needed for ray transport + vector delta_psi_; + double distance_travelled_ {0}; + int negroups_; + bool is_active_ {false}; + bool is_alive_ {true}; +}; // class RandomRay + +} // namespace openmc + +#endif // OPENMC_RANDOM_RAY_H diff --git a/include/openmc/random_ray/random_ray_simulation.h b/include/openmc/random_ray/random_ray_simulation.h new file mode 100644 index 000000000..cde0d27df --- /dev/null +++ b/include/openmc/random_ray/random_ray_simulation.h @@ -0,0 +1,59 @@ +#ifndef OPENMC_RANDOM_RAY_SIMULATION_H +#define OPENMC_RANDOM_RAY_SIMULATION_H + +#include "openmc/random_ray/flat_source_domain.h" + +namespace openmc { + +/* + * The RandomRaySimulation class encompasses data and methods for running a + * random ray simulation. + */ + +class RandomRaySimulation { +public: + //---------------------------------------------------------------------------- + // Constructors + RandomRaySimulation(); + + //---------------------------------------------------------------------------- + // Methods + void simulate(); + void reduce_simulation_statistics(); + void output_simulation_results() const; + void instability_check( + int64_t n_hits, double k_eff, double& avg_miss_rate) const; + void print_results_random_ray(uint64_t total_geometric_intersections, + double avg_miss_rate, int negroups, int64_t n_source_regions) const; + + //---------------------------------------------------------------------------- + // Data members +private: + // Contains all flat source region data + FlatSourceDomain domain_; + + // Random ray eigenvalue + double k_eff_ {1.0}; + + // Tracks the average FSR miss rate for analysis and reporting + double avg_miss_rate_ {0.0}; + + // Tracks the total number of geometric intersections by all rays for + // reporting + uint64_t total_geometric_intersections_ {0}; + + // Number of energy groups + int negroups_; + +}; // class RandomRaySimulation + +//============================================================================ +//! Non-member functions +//============================================================================ + +void openmc_run_random_ray(); +void validate_random_ray_inputs(); + +} // namespace openmc + +#endif // OPENMC_RANDOM_RAY_SIMULATION_H diff --git a/include/openmc/settings.h b/include/openmc/settings.h index 5e60009bc..b71ec3649 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -112,8 +112,9 @@ extern ResScatMethod res_scat_method; //!< resonance upscattering method extern double res_scat_energy_min; //!< Min energy in [eV] for res. upscattering extern double res_scat_energy_max; //!< Max energy in [eV] for res. upscattering extern vector - res_scat_nuclides; //!< Nuclides using res. upscattering treatment -extern RunMode run_mode; //!< Run mode (eigenvalue, fixed src, etc.) + res_scat_nuclides; //!< Nuclides using res. upscattering treatment +extern RunMode run_mode; //!< Run mode (eigenvalue, fixed src, etc.) +extern SolverType solver_type; //!< Solver Type (Monte Carlo or Random Ray) extern std::unordered_set sourcepoint_batch; //!< Batches when source should be written extern std::unordered_set @@ -139,6 +140,7 @@ extern int trigger_batch_interval; //!< Batch interval for triggers extern "C" int verbosity; //!< How verbose to make output extern double weight_cutoff; //!< Weight cutoff for Russian roulette extern double weight_survive; //!< Survival weight after Russian roulette + } // namespace settings //============================================================================== diff --git a/include/openmc/timer.h b/include/openmc/timer.h index 62b97883f..d928aad45 100644 --- a/include/openmc/timer.h +++ b/include/openmc/timer.h @@ -31,6 +31,7 @@ extern Timer time_event_advance_particle; extern Timer time_event_surface_crossing; extern Timer time_event_collision; extern Timer time_event_death; +extern Timer time_update_src; } // namespace simulation diff --git a/openmc/settings.py b/openmc/settings.py index 828d5aef4..631246029 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -86,7 +86,8 @@ class Settings: .. versionadded:: 0.12 rel_max_lost_particles : float - Maximum number of lost particles, relative to the total number of particles + Maximum number of lost particles, relative to the total number of + particles .. versionadded:: 0.12 inactive : int @@ -146,6 +147,18 @@ class Settings: Initial seed for randomly generated plot colors. ptables : bool Determine whether probability tables are used. + random_ray : dict + Options for configuring the random ray solver. Acceptable keys are: + + :distance_inactive: + Indicates the total active distance in [cm] a ray should travel + :distance_active: + Indicates the total active distance in [cm] a ray should travel + :ray_source: + Starting ray distribution (must be uniform in space and angle) as + specified by a :class:`openmc.SourceBase` object. + + .. versionadded:: 0.14.1 resonance_scattering : dict Settings for resonance elastic scattering. Accepted keys are 'enable' (bool), 'method' (str), 'energy_min' (float), 'energy_max' (float), and @@ -153,10 +166,10 @@ class Settings: rejection correction) or 'rvs' (relative velocity sampling). If not specified, 'rvs' is the default method. The 'energy_min' and 'energy_max' values indicate the minimum and maximum energies above and - below which the resonance elastic scattering method is to be - applied. The 'nuclides' list indicates what nuclides the method should - be applied to. In its absence, the method will be applied to all - nuclides with 0 K elastic scattering data present. + below which the resonance elastic scattering method is to be applied. + The 'nuclides' list indicates what nuclides the method should be applied + to. In its absence, the method will be applied to all nuclides with 0 K + elastic scattering data present. run_mode : {'eigenvalue', 'fixed source', 'plot', 'volume', 'particle restart'} The type of calculation to perform (default is 'eigenvalue') seed : int @@ -185,26 +198,26 @@ class Settings: :surface_ids: List of surface ids at which crossing particles are to be banked (int) - :max_particles: Maximum number of particles to be banked on - surfaces per process (int) + :max_particles: Maximum number of particles to be banked on surfaces per + process (int) :mcpl: Output in the form of an MCPL-file (bool) survival_biasing : bool Indicate whether survival biasing is to be used tabular_legendre : dict Determines if a multi-group scattering moment kernel expanded via Legendre polynomials is to be converted to a tabular distribution or - not. Accepted keys are 'enable' and 'num_points'. The value for - 'enable' is a bool stating whether the conversion to tabular is - performed; the value for 'num_points' sets the number of points to use - in the tabular distribution, should 'enable' be True. + not. Accepted keys are 'enable' and 'num_points'. The value for 'enable' + is a bool stating whether the conversion to tabular is performed; the + value for 'num_points' sets the number of points to use in the tabular + distribution, should 'enable' be True. temperature : dict Defines a default temperature and method for treating intermediate temperatures at which nuclear data doesn't exist. Accepted keys are 'default', 'method', 'range', 'tolerance', and 'multipole'. The value for 'default' should be a float representing the default temperature in Kelvin. The value for 'method' should be 'nearest' or 'interpolation'. - If the method is 'nearest', 'tolerance' indicates a range of - temperature within which cross sections may be used. If the method is + If the method is 'nearest', 'tolerance' indicates a range of temperature + within which cross sections may be used. If the method is 'interpolation', 'tolerance' indicates the range of temperatures outside of the available cross section temperatures where cross sections will evaluate to the nearer bound. The value for 'range' should be a pair of @@ -348,6 +361,8 @@ class Settings: self._max_splits = None self._max_tracks = None + self._random_ray = {} + for key, value in kwargs.items(): setattr(self, key, value) @@ -1030,6 +1045,31 @@ class Settings: wwgs = [wwgs] self._weight_window_generators = cv.CheckedList(WeightWindowGenerator, 'weight window generators', wwgs) + @property + def random_ray(self) -> dict: + return self._random_ray + + @random_ray.setter + def random_ray(self, random_ray: dict): + if not isinstance(random_ray, Mapping): + raise ValueError(f'Unable to set random_ray from "{random_ray}" ' + 'which is not a dict.') + for key in random_ray: + if key == 'distance_active': + cv.check_type('active ray length', random_ray[key], Real) + cv.check_greater_than('active ray length', random_ray[key], 0.0) + elif key == 'distance_inactive': + cv.check_type('inactive ray length', random_ray[key], Real) + cv.check_greater_than('inactive ray length', + random_ray[key], 0.0, True) + elif key == 'ray_source': + cv.check_type('random ray source', random_ray[key], SourceBase) + else: + raise ValueError(f'Unable to set random ray to "{key}" which is ' + 'unsupported by OpenMC') + + self._random_ray = random_ray + def _create_run_mode_subelement(self, root): elem = ET.SubElement(root, "run_mode") elem.text = self._run_mode.value @@ -1442,6 +1482,17 @@ class Settings: elem = ET.SubElement(root, "max_tracks") elem.text = str(self._max_tracks) + def _create_random_ray_subelement(self, root): + if self._random_ray: + element = ET.SubElement(root, "random_ray") + for key, value in self._random_ray.items(): + if key == 'ray_source' and isinstance(value, SourceBase): + source_element = value.to_xml_element() + element.append(source_element) + else: + subelement = ET.SubElement(element, key) + subelement.text = str(value) + def _eigenvalue_from_xml_element(self, root): elem = root.find('eigenvalue') if elem is not None: @@ -1793,6 +1844,17 @@ class Settings: if text is not None: self.max_tracks = int(text) + def _random_ray_from_xml_element(self, root): + elem = root.find('random_ray') + if elem is not None: + self.random_ray = {} + for child in elem: + if child.tag in ('distance_inactive', 'distance_active'): + self.random_ray[child.tag] = float(child.text) + elif child.tag == 'source': + source = SourceBase.from_xml_element(child) + self.random_ray['ray_source'] = source + def to_xml_element(self, mesh_memo=None): """Create a 'settings' element to be written to an XML file. @@ -1855,6 +1917,7 @@ class Settings: self._create_weight_window_checkpoints_subelement(element) self._create_max_splits_subelement(element) self._create_max_tracks_subelement(element) + self._create_random_ray_subelement(element) # Clean the indentation in the file to be user-readable clean_indentation(element) @@ -1958,6 +2021,7 @@ class Settings: settings._weight_window_checkpoints_from_xml_element(elem) settings._max_splits_from_xml_element(elem) settings._max_tracks_from_xml_element(elem) + settings._random_ray_from_xml_element(elem) # TODO: Get volume calculations return settings diff --git a/src/boundary_condition.cpp b/src/boundary_condition.cpp index 42a8e27a1..b58054dce 100644 --- a/src/boundary_condition.cpp +++ b/src/boundary_condition.cpp @@ -6,6 +6,7 @@ #include "openmc/constants.h" #include "openmc/error.h" +#include "openmc/random_ray/random_ray.h" #include "openmc/surface.h" namespace openmc { @@ -16,7 +17,18 @@ namespace openmc { void VacuumBC::handle_particle(Particle& p, const Surface& surf) const { - p.cross_vacuum_bc(surf); + // Random ray and Monte Carlo need different treatments at vacuum BCs + if (settings::solver_type == SolverType::RANDOM_RAY) { + // Reflect ray off of the surface + ReflectiveBC().handle_particle(p, surf); + + // Set ray's angular flux spectrum to vacuum conditions (zero) + RandomRay* r = static_cast(&p); + std::fill(r->angular_flux_.begin(), r->angular_flux_.end(), 0.0); + + } else { + p.cross_vacuum_bc(surf); + } } //============================================================================== diff --git a/src/eigenvalue.cpp b/src/eigenvalue.cpp index 5584210a1..d5410094b 100644 --- a/src/eigenvalue.cpp +++ b/src/eigenvalue.cpp @@ -57,16 +57,28 @@ void calculate_generation_keff() double keff_reduced; #ifdef OPENMC_MPI - // Combine values across all processors - MPI_Allreduce(&simulation::keff_generation, &keff_reduced, 1, MPI_DOUBLE, - MPI_SUM, mpi::intracomm); + if (settings::solver_type != SolverType::RANDOM_RAY) { + // Combine values across all processors + MPI_Allreduce(&simulation::keff_generation, &keff_reduced, 1, MPI_DOUBLE, + MPI_SUM, mpi::intracomm); + } else { + // If using random ray, MPI parallelism is provided by domain replication. + // As such, all fluxes will be reduced at the end of each transport sweep, + // such that all ranks have identical scalar flux vectors, and will all + // independently compute the same value of k. Thus, there is no need to + // perform any additional MPI reduction here. + keff_reduced = simulation::keff_generation; + } #else keff_reduced = simulation::keff_generation; #endif // Normalize single batch estimate of k // TODO: This should be normalized by total_weight, not by n_particles - keff_reduced /= settings::n_particles; + if (settings::solver_type != SolverType::RANDOM_RAY) { + keff_reduced /= settings::n_particles; + } + simulation::k_generation.push_back(keff_reduced); } @@ -370,7 +382,8 @@ int openmc_get_keff(double* k_combined) // Special case for n <=3. Notice that at the end, // there is a N-3 term in a denominator. - if (simulation::n_realizations <= 3) { + if (simulation::n_realizations <= 3 || + settings::solver_type == SolverType::RANDOM_RAY) { k_combined[0] = simulation::keff; k_combined[1] = simulation::keff_std; if (simulation::n_realizations <= 1) { diff --git a/src/geometry.cpp b/src/geometry.cpp index a16addd48..445b19faa 100644 --- a/src/geometry.cpp +++ b/src/geometry.cpp @@ -174,17 +174,9 @@ bool find_cell_inner( // Set the material and temperature. p.material_last() = p.material(); - if (c.material_.size() > 1) { - p.material() = c.material_[p.cell_instance()]; - } else { - p.material() = c.material_[0]; - } + p.material() = c.material(p.cell_instance()); p.sqrtkT_last() = p.sqrtkT(); - if (c.sqrtkT_.size() > 1) { - p.sqrtkT() = c.sqrtkT_[p.cell_instance()]; - } else { - p.sqrtkT() = c.sqrtkT_[0]; - } + p.sqrtkT() = c.sqrtkT(p.cell_instance()); return true; diff --git a/src/main.cpp b/src/main.cpp index 02a850ead..88251ac72 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,6 +6,7 @@ #include "openmc/error.h" #include "openmc/message_passing.h" #include "openmc/particle_restart.h" +#include "openmc/random_ray/random_ray_simulation.h" #include "openmc/settings.h" int main(int argc, char* argv[]) @@ -31,7 +32,15 @@ int main(int argc, char* argv[]) switch (settings::run_mode) { case RunMode::FIXED_SOURCE: case RunMode::EIGENVALUE: - err = openmc_run(); + switch (settings::solver_type) { + case SolverType::MONTE_CARLO: + err = openmc_run(); + break; + case SolverType::RANDOM_RAY: + openmc_run_random_ray(); + err = 0; + break; + } break; case RunMode::PLOTTING: err = openmc_plot_geometry(); diff --git a/src/mesh.cpp b/src/mesh.cpp index bc6781ad3..c6e421b42 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -582,7 +582,7 @@ void StructuredMesh::raytrace_mesh( // Compute the length of the entire track. double total_distance = (r1 - r0).norm(); - if (total_distance == 0.0) + if (total_distance == 0.0 && settings::solver_type != SolverType::RANDOM_RAY) return; const int n = n_dimension_; diff --git a/src/output.cpp b/src/output.cpp index 9b4171d8d..b1b963f7d 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -31,6 +31,7 @@ #include "openmc/mgxs_interface.h" #include "openmc/nuclide.h" #include "openmc/plot.h" +#include "openmc/random_ray/flat_source_domain.h" #include "openmc/reaction.h" #include "openmc/settings.h" #include "openmc/simulation.h" @@ -409,7 +410,7 @@ void print_generation() //============================================================================== -void show_time(const char* label, double secs, int indent_level = 0) +void show_time(const char* label, double secs, int indent_level) { int width = 33 - indent_level * 2; fmt::print("{0:{1}} {2:<{3}} = {4:>10.4e} seconds\n", "", 2 * indent_level, diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp new file mode 100644 index 000000000..5e1194aa9 --- /dev/null +++ b/src/random_ray/flat_source_domain.cpp @@ -0,0 +1,686 @@ +#include "openmc/random_ray/flat_source_domain.h" + +#include "openmc/cell.h" +#include "openmc/geometry.h" +#include "openmc/message_passing.h" +#include "openmc/mgxs_interface.h" +#include "openmc/output.h" +#include "openmc/plot.h" +#include "openmc/random_ray/random_ray.h" +#include "openmc/simulation.h" +#include "openmc/tallies/filter.h" +#include "openmc/tallies/tally.h" +#include "openmc/tallies/tally_scoring.h" +#include "openmc/timer.h" + +#include + +namespace openmc { + +//============================================================================== +// FlatSourceDomain implementation +//============================================================================== + +FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) +{ + // Count the number of source regions, compute the cell offset + // indices, and store the material type The reason for the offsets is that + // some cell types may not have material fills, and therefore do not + // produce FSRs. Thus, we cannot index into the global arrays directly + for (const auto& c : model::cells) { + if (c->type_ != Fill::MATERIAL) { + source_region_offsets_.push_back(-1); + } else { + source_region_offsets_.push_back(n_source_regions_); + n_source_regions_ += c->n_instances_; + n_source_elements_ += c->n_instances_ * negroups_; + } + } + + // Initialize cell-wise arrays + lock_.resize(n_source_regions_); + material_.resize(n_source_regions_); + position_recorded_.assign(n_source_regions_, 0); + position_.resize(n_source_regions_); + volume_.assign(n_source_regions_, 0.0); + volume_t_.assign(n_source_regions_, 0.0); + was_hit_.assign(n_source_regions_, 0); + + // Initialize element-wise arrays + scalar_flux_new_.assign(n_source_elements_, 0.0); + scalar_flux_old_.assign(n_source_elements_, 1.0); + scalar_flux_final_.assign(n_source_elements_, 0.0); + source_.resize(n_source_elements_); + tally_task_.resize(n_source_elements_); + + // Initialize material array + int64_t source_region_id = 0; + for (int i = 0; i < model::cells.size(); i++) { + Cell& cell = *model::cells[i]; + if (cell.type_ == Fill::MATERIAL) { + for (int j = 0; j < cell.n_instances_; j++) { + material_[source_region_id++] = cell.material(j); + } + } + } + + // Sanity check + if (source_region_id != n_source_regions_) { + fatal_error("Unexpected number of source regions"); + } +} + +void FlatSourceDomain::batch_reset() +{ + // Reset scalar fluxes, iteration volume tallies, and region hit flags to + // zero + parallel_fill(scalar_flux_new_, 0.0f); + parallel_fill(volume_, 0.0); + parallel_fill(was_hit_, 0); +} + +void FlatSourceDomain::accumulate_iteration_flux() +{ +#pragma omp parallel for + for (int64_t se = 0; se < n_source_elements_; se++) { + scalar_flux_final_[se] += scalar_flux_new_[se]; + } +} + +// Compute new estimate of scattering + fission sources in each source region +// based on the flux estimate from the previous iteration. +void FlatSourceDomain::update_neutron_source(double k_eff) +{ + simulation::time_update_src.start(); + + double inverse_k_eff = 1.0 / k_eff; + + // Temperature and angle indices, if using multiple temperature + // data sets and/or anisotropic data sets. + // TODO: Currently assumes we are only using single temp/single + // angle data. + const int t = 0; + const int a = 0; + +#pragma omp parallel for + for (int sr = 0; sr < n_source_regions_; sr++) { + int material = material_[sr]; + + for (int e_out = 0; e_out < negroups_; e_out++) { + float sigma_t = data::mg.macro_xs_[material].get_xs( + MgxsType::TOTAL, e_out, nullptr, nullptr, nullptr, t, a); + float scatter_source = 0.0f; + float fission_source = 0.0f; + + for (int e_in = 0; e_in < negroups_; e_in++) { + float scalar_flux = scalar_flux_old_[sr * negroups_ + e_in]; + float sigma_s = data::mg.macro_xs_[material].get_xs( + MgxsType::NU_SCATTER, e_in, &e_out, nullptr, nullptr, t, a); + float nu_sigma_f = data::mg.macro_xs_[material].get_xs( + MgxsType::NU_FISSION, e_in, nullptr, nullptr, nullptr, t, a); + float chi = data::mg.macro_xs_[material].get_xs( + MgxsType::CHI_PROMPT, e_in, &e_out, nullptr, nullptr, t, a); + scatter_source += sigma_s * scalar_flux; + fission_source += nu_sigma_f * scalar_flux * chi; + } + + fission_source *= inverse_k_eff; + float new_isotropic_source = (scatter_source + fission_source) / sigma_t; + source_[sr * negroups_ + e_out] = new_isotropic_source; + } + } + + simulation::time_update_src.stop(); +} + +// Normalizes flux and updates simulation-averaged volume estimate +void FlatSourceDomain::normalize_scalar_flux_and_volumes( + double total_active_distance_per_iteration) +{ + float normalization_factor = 1.0 / total_active_distance_per_iteration; + double volume_normalization_factor = + 1.0 / (total_active_distance_per_iteration * simulation::current_batch); + +// Normalize scalar flux to total distance travelled by all rays this iteration +#pragma omp parallel for + for (int64_t e = 0; e < scalar_flux_new_.size(); e++) { + scalar_flux_new_[e] *= normalization_factor; + } + +// Accumulate cell-wise ray length tallies collected this iteration, then +// update the simulation-averaged cell-wise volume estimates +#pragma omp parallel for + for (int64_t sr = 0; sr < n_source_regions_; sr++) { + volume_t_[sr] += volume_[sr]; + volume_[sr] = volume_t_[sr] * volume_normalization_factor; + } +} + +// Combine transport flux contributions and flat source contributions from the +// previous iteration to generate this iteration's estimate of scalar flux. +int64_t FlatSourceDomain::add_source_to_scalar_flux() +{ + int64_t n_hits = 0; + + // Temperature and angle indices, if using multiple temperature + // data sets and/or anisotropic data sets. + // TODO: Currently assumes we are only using single temp/single + // angle data. + const int t = 0; + const int a = 0; + +#pragma omp parallel for reduction(+ : n_hits) + for (int sr = 0; sr < n_source_regions_; sr++) { + + // Check if this cell was hit this iteration + int was_cell_hit = was_hit_[sr]; + if (was_cell_hit) { + n_hits++; + } + + double volume = volume_[sr]; + int material = material_[sr]; + for (int g = 0; g < negroups_; g++) { + int64_t idx = (sr * negroups_) + g; + + // There are three scenarios we need to consider: + if (was_cell_hit) { + // 1. If the FSR was hit this iteration, then the new flux is equal to + // the flat source from the previous iteration plus the contributions + // from rays passing through the source region (computed during the + // transport sweep) + float sigma_t = data::mg.macro_xs_[material].get_xs( + MgxsType::TOTAL, g, nullptr, nullptr, nullptr, t, a); + scalar_flux_new_[idx] /= (sigma_t * volume); + scalar_flux_new_[idx] += source_[idx]; + } else if (volume > 0.0) { + // 2. If the FSR was not hit this iteration, but has been hit some + // previous iteration, then we simply set the new scalar flux to be + // equal to the contribution from the flat source alone. + scalar_flux_new_[idx] = source_[idx]; + } else { + // If the FSR was not hit this iteration, and it has never been hit in + // any iteration (i.e., volume is zero), then we want to set this to 0 + // to avoid dividing anything by a zero volume. + scalar_flux_new_[idx] = 0.0f; + } + } + } + + // Return the number of source regions that were hit this iteration + return n_hits; +} + +// Generates new estimate of k_eff based on the differences between this +// iteration's estimate of the scalar flux and the last iteration's estimate. +double FlatSourceDomain::compute_k_eff(double k_eff_old) const +{ + double fission_rate_old = 0; + double fission_rate_new = 0; + + // Temperature and angle indices, if using multiple temperature + // data sets and/or anisotropic data sets. + // TODO: Currently assumes we are only using single temp/single + // angle data. + const int t = 0; + const int a = 0; + +#pragma omp parallel for reduction(+ : fission_rate_old, fission_rate_new) + for (int sr = 0; sr < n_source_regions_; sr++) { + + // If simulation averaged volume is zero, don't include this cell + double volume = volume_[sr]; + if (volume == 0.0) { + continue; + } + + int material = material_[sr]; + + double sr_fission_source_old = 0; + double sr_fission_source_new = 0; + + for (int g = 0; g < negroups_; g++) { + int64_t idx = (sr * negroups_) + g; + double nu_sigma_f = data::mg.macro_xs_[material].get_xs( + MgxsType::NU_FISSION, g, nullptr, nullptr, nullptr, t, a); + sr_fission_source_old += nu_sigma_f * scalar_flux_old_[idx]; + sr_fission_source_new += nu_sigma_f * scalar_flux_new_[idx]; + } + + fission_rate_old += sr_fission_source_old * volume; + fission_rate_new += sr_fission_source_new * volume; + } + + double k_eff_new = k_eff_old * (fission_rate_new / fission_rate_old); + + return k_eff_new; +} + +// This function is responsible for generating a mapping between random +// ray flat source regions (cell instances) and tally bins. The mapping +// takes the form of a "TallyTask" object, which accounts for one single +// score being applied to a single tally. Thus, a single source region +// may have anywhere from zero to many tally tasks associated with it --- +// meaning that the global "tally_task" data structure is in 2D. The outer +// dimension corresponds to the source element (i.e., each entry corresponds +// to a specific energy group within a specific source region), and the +// inner dimension corresponds to the tallying task itself. Mechanically, +// the mapping between FSRs and spatial filters is done by considering +// the location of a single known ray midpoint that passed through the +// FSR. I.e., during transport, the first ray to pass through a given FSR +// will write down its midpoint for use with this function. This is a cheap +// and easy way of mapping FSRs to spatial tally filters, but comes with +// the downside of adding the restriction that spatial tally filters must +// share boundaries with the physical geometry of the simulation (so as +// not to subdivide any FSR). It is acceptable for a spatial tally region +// to contain multiple FSRs, but not the other way around. + +// TODO: In future work, it would be preferable to offer a more general +// (but perhaps slightly more expensive) option for handling arbitrary +// spatial tallies that would be allowed to subdivide FSRs. + +// Besides generating the mapping structure, this function also keeps track +// of whether or not all flat source regions have been hit yet. This is +// required, as there is no guarantee that all flat source regions will +// be hit every iteration, such that in the first few iterations some FSRs +// may not have a known position within them yet to facilitate mapping to +// spatial tally filters. However, after several iterations, if all FSRs +// have been hit and have had a tally map generated, then this status will +// be passed back to the caller to alert them that this function doesn't +// need to be called for the remainder of the simulation. + +void FlatSourceDomain::convert_source_regions_to_tallies() +{ + openmc::simulation::time_tallies.start(); + + // Tracks if we've generated a mapping yet for all source regions. + bool all_source_regions_mapped = true; + +// Attempt to generate mapping for all source regions +#pragma omp parallel for + for (int sr = 0; sr < n_source_regions_; sr++) { + + // If this source region has not been hit by a ray yet, then + // we aren't going to be able to map it, so skip it. + if (!position_recorded_[sr]) { + all_source_regions_mapped = false; + continue; + } + + // A particle located at the recorded midpoint of a ray + // crossing through this source region is used to estabilish + // the spatial location of the source region + Particle p; + p.r() = position_[sr]; + p.r_last() = position_[sr]; + bool found = exhaustive_find_cell(p); + + // Loop over energy groups (so as to support energy filters) + for (int g = 0; g < negroups_; g++) { + + // Set particle to the current energy + p.g() = g; + p.g_last() = g; + p.E() = data::mg.energy_bin_avg_[p.g()]; + p.E_last() = p.E(); + + int64_t source_element = sr * negroups_ + g; + + // If this task has already been populated, we don't need to do + // it again. + if (tally_task_[source_element].size() > 0) { + continue; + } + + // Loop over all active tallies. This logic is essentially identical + // to what happens when scanning for applicable tallies during + // MC transport. + for (auto i_tally : model::active_tallies) { + Tally& 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]; + // If a valid tally, filter, and score cobination has been found, + // then add it to the list of tally tasks for this source element. + tally_task_[source_element].emplace_back( + i_tally, filter_index, score_index, score_bin); + } + } + } + // Reset all the filter matches for the next tally event. + for (auto& match : p.filter_matches()) + match.bins_present_ = false; + } + } + openmc::simulation::time_tallies.stop(); + + mapped_all_tallies_ = all_source_regions_mapped; +} + +// Tallying in random ray is not done directly during transport, rather, +// it is done only once after each power iteration. This is made possible +// by way of a mapping data structure that relates spatial source regions +// (FSRs) to tally/filter/score combinations. The mechanism by which the +// mapping is done (and the limitations incurred) is documented in the +// "convert_source_regions_to_tallies()" function comments above. The present +// tally function simply traverses the mapping data structure and executes +// the scoring operations to OpenMC's native tally result arrays. + +void FlatSourceDomain::random_ray_tally() const +{ + openmc::simulation::time_tallies.start(); + + // Temperature and angle indices, if using multiple temperature + // data sets and/or anisotropic data sets. + // TODO: Currently assumes we are only using single temp/single + // angle data. + const int t = 0; + const int a = 0; + +// We loop over all source regions and energy groups. For each +// element, we check if there are any scores needed and apply +// them. +#pragma omp parallel for + for (int sr = 0; sr < n_source_regions_; sr++) { + double volume = volume_[sr]; + double material = material_[sr]; + for (int g = 0; g < negroups_; g++) { + int idx = sr * negroups_ + g; + double flux = scalar_flux_new_[idx] * volume; + for (auto& task : tally_task_[idx]) { + double score; + switch (task.score_type) { + + case SCORE_FLUX: + score = flux; + break; + + case SCORE_TOTAL: + score = flux * data::mg.macro_xs_[material].get_xs( + MgxsType::TOTAL, g, NULL, NULL, NULL, t, a); + break; + + case SCORE_FISSION: + score = flux * data::mg.macro_xs_[material].get_xs( + MgxsType::FISSION, g, NULL, NULL, NULL, t, a); + break; + + case SCORE_NU_FISSION: + score = flux * data::mg.macro_xs_[material].get_xs( + MgxsType::NU_FISSION, g, NULL, NULL, NULL, t, a); + break; + + case SCORE_EVENTS: + score = 1.0; + break; + + default: + fatal_error("Invalid score specified in tallies.xml. Only flux, " + "total, fission, nu-fission, and events are supported in " + "random ray mode."); + break; + } + Tally& tally {*model::tallies[task.tally_idx]}; +#pragma omp atomic + tally.results_(task.filter_idx, task.score_idx, TallyResult::VALUE) += + score; + } + } + } +} + +void FlatSourceDomain::all_reduce_replicated_source_regions() +{ +#ifdef OPENMC_MPI + + // If we only have 1 MPI rank, no need + // to reduce anything. + if (mpi::n_procs <= 1) + return; + + simulation::time_bank_sendrecv.start(); + + // The "position_recorded" variable needs to be allreduced (and maxed), + // as whether or not a cell was hit will affect some decisions in how the + // source is calculated in the next iteration so as to avoid dividing + // by zero. We take the max rather than the sum as the hit values are + // expected to be zero or 1. + MPI_Allreduce(MPI_IN_PLACE, position_recorded_.data(), n_source_regions_, + MPI_INT, MPI_MAX, mpi::intracomm); + + // The position variable is more complicated to reduce than the others, + // as we do not want the sum of all positions in each cell, rather, we + // want to just pick any single valid position. Thus, we perform a gather + // and then pick the first valid position we find for all source regions + // that have had a position recorded. This operation does not need to + // be broadcast back to other ranks, as this value is only used for the + // tally conversion operation, which is only performed on the master rank. + // While this is expensive, it only needs to be done for active batches, + // and only if we have not mapped all the tallies yet. Once tallies are + // fully mapped, then the position vector is fully populated, so this + // operation can be skipped. + + // First, we broadcast the fully mapped tally status variable so that + // all ranks are on the same page + int mapped_all_tallies_i = static_cast(mapped_all_tallies_); + MPI_Bcast(&mapped_all_tallies_i, 1, MPI_INT, 0, mpi::intracomm); + + // Then, we perform the gather of position data, if needed + if (simulation::current_batch > settings::n_inactive && + !mapped_all_tallies_i) { + + // Master rank will gather results and pick valid positions + if (mpi::master) { + // Initialize temporary vector for receiving positions + vector> all_position; + all_position.resize(mpi::n_procs); + for (int i = 0; i < mpi::n_procs; i++) { + all_position[i].resize(n_source_regions_); + } + + // Copy master rank data into gathered vector for convenience + all_position[0] = position_; + + // Receive all data into gather vector + for (int i = 1; i < mpi::n_procs; i++) { + MPI_Recv(all_position[i].data(), n_source_regions_ * 3, MPI_DOUBLE, i, + 0, mpi::intracomm, MPI_STATUS_IGNORE); + } + + // Scan through gathered data and pick first valid cell posiiton + for (int sr = 0; sr < n_source_regions_; sr++) { + if (position_recorded_[sr] == 1) { + for (int i = 0; i < mpi::n_procs; i++) { + if (all_position[i][sr].x != 0.0 || all_position[i][sr].y != 0.0 || + all_position[i][sr].z != 0.0) { + position_[sr] = all_position[i][sr]; + break; + } + } + } + } + } else { + // Other ranks just send in their data + MPI_Send(position_.data(), n_source_regions_ * 3, MPI_DOUBLE, 0, 0, + mpi::intracomm); + } + } + + // For the rest of the source region data, we simply perform an all reduce, + // as these values will be needed on all ranks for transport during the + // next iteration. + MPI_Allreduce(MPI_IN_PLACE, volume_.data(), n_source_regions_, MPI_DOUBLE, + MPI_SUM, mpi::intracomm); + + MPI_Allreduce(MPI_IN_PLACE, was_hit_.data(), n_source_regions_, MPI_INT, + MPI_SUM, mpi::intracomm); + + MPI_Allreduce(MPI_IN_PLACE, scalar_flux_new_.data(), n_source_elements_, + MPI_FLOAT, MPI_SUM, mpi::intracomm); + + simulation::time_bank_sendrecv.stop(); +#endif +} + +// Outputs all basic material, FSR ID, multigroup flux, and +// fission source data to .vtk file that can be directly +// loaded and displayed by Paraview. Note that .vtk binary +// files require big endian byte ordering, so endianness +// is checked and flipped if necessary. +void FlatSourceDomain::output_to_vtk() const +{ + // Rename .h5 plot filename(s) to .vtk filenames + for (int p = 0; p < model::plots.size(); p++) { + PlottableInterface* plot = model::plots[p].get(); + plot->path_plot() = + plot->path_plot().substr(0, plot->path_plot().find_last_of('.')) + ".vtk"; + } + + // Print header information + print_plot(); + + // Outer loop over plots + for (int p = 0; p < model::plots.size(); p++) { + + // Get handle to OpenMC plot object and extract params + Plot* openmc_plot = dynamic_cast(model::plots[p].get()); + + // Random ray plots only support voxel plots + if (!openmc_plot) { + warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting " + "is allowed in random ray mode.", + p)); + continue; + } else if (openmc_plot->type_ != Plot::PlotType::voxel) { + warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting " + "is allowed in random ray mode.", + p)); + continue; + } + + int Nx = openmc_plot->pixels_[0]; + int Ny = openmc_plot->pixels_[1]; + int Nz = openmc_plot->pixels_[2]; + Position origin = openmc_plot->origin_; + Position width = openmc_plot->width_; + Position ll = origin - width / 2.0; + double x_delta = width.x / Nx; + double y_delta = width.y / Ny; + double z_delta = width.z / Nz; + std::string filename = openmc_plot->path_plot(); + + // Perform sanity checks on file size + uint64_t bytes = Nx * Ny * Nz * (negroups_ + 1 + 1 + 1) * sizeof(float); + write_message(5, "Processing plot {}: {}... (Estimated size is {} MB)", + openmc_plot->id(), filename, bytes / 1.0e6); + if (bytes / 1.0e9 > 1.0) { + warning("Voxel plot specification is very large (>1 GB). Plotting may be " + "slow."); + } else if (bytes / 1.0e9 > 100.0) { + fatal_error("Voxel plot specification is too large (>100 GB). Exiting."); + } + + // Relate voxel spatial locations to random ray source regions + vector voxel_indices(Nx * Ny * Nz); + +#pragma omp parallel for collapse(3) + for (int z = 0; z < Nz; z++) { + for (int y = 0; y < Ny; y++) { + for (int x = 0; x < Nx; x++) { + Position sample; + sample.z = ll.z + z_delta / 2.0 + z * z_delta; + sample.y = ll.y + y_delta / 2.0 + y * y_delta; + sample.x = ll.x + x_delta / 2.0 + x * x_delta; + Particle p; + p.r() = sample; + bool found = exhaustive_find_cell(p); + int i_cell = p.lowest_coord().cell; + int64_t source_region_idx = + source_region_offsets_[i_cell] + p.cell_instance(); + voxel_indices[z * Ny * Nx + y * Nx + x] = source_region_idx; + } + } + } + + // Open file for writing + std::FILE* plot = std::fopen(filename.c_str(), "wb"); + + // Write vtk metadata + std::fprintf(plot, "# vtk DataFile Version 2.0\n"); + std::fprintf(plot, "Dataset File\n"); + std::fprintf(plot, "BINARY\n"); + std::fprintf(plot, "DATASET STRUCTURED_POINTS\n"); + std::fprintf(plot, "DIMENSIONS %d %d %d\n", Nx, Ny, Nz); + std::fprintf(plot, "ORIGIN 0 0 0\n"); + std::fprintf(plot, "SPACING %lf %lf %lf\n", x_delta, y_delta, z_delta); + std::fprintf(plot, "POINT_DATA %d\n", Nx * Ny * Nz); + + // Plot multigroup flux data + for (int g = 0; g < negroups_; g++) { + std::fprintf(plot, "SCALARS flux_group_%d float\n", g); + std::fprintf(plot, "LOOKUP_TABLE default\n"); + for (int fsr : voxel_indices) { + int64_t source_element = fsr * negroups_ + g; + float flux = scalar_flux_final_[source_element]; + flux /= (settings::n_batches - settings::n_inactive); + flux = convert_to_big_endian(flux); + std::fwrite(&flux, sizeof(float), 1, plot); + } + } + + // Plot FSRs + std::fprintf(plot, "SCALARS FSRs float\n"); + std::fprintf(plot, "LOOKUP_TABLE default\n"); + for (int fsr : voxel_indices) { + float value = future_prn(10, fsr); + value = convert_to_big_endian(value); + std::fwrite(&value, sizeof(float), 1, plot); + } + + // Plot Materials + std::fprintf(plot, "SCALARS Materials int\n"); + std::fprintf(plot, "LOOKUP_TABLE default\n"); + for (int fsr : voxel_indices) { + int mat = material_[fsr]; + mat = convert_to_big_endian(mat); + std::fwrite(&mat, sizeof(int), 1, plot); + } + + // Plot fission source + std::fprintf(plot, "SCALARS total_fission_source float\n"); + std::fprintf(plot, "LOOKUP_TABLE default\n"); + for (int fsr : voxel_indices) { + float total_fission = 0.0; + int mat = material_[fsr]; + for (int g = 0; g < negroups_; g++) { + int64_t source_element = fsr * negroups_ + g; + float flux = scalar_flux_final_[source_element]; + flux /= (settings::n_batches - settings::n_inactive); + float Sigma_f = data::mg.macro_xs_[mat].get_xs( + MgxsType::FISSION, g, nullptr, nullptr, nullptr, 0, 0); + total_fission += Sigma_f * flux; + } + total_fission = convert_to_big_endian(total_fission); + std::fwrite(&total_fission, sizeof(float), 1, plot); + } + + std::fclose(plot); + } +} + +} // namespace openmc diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp new file mode 100644 index 000000000..f471c7551 --- /dev/null +++ b/src/random_ray/random_ray.cpp @@ -0,0 +1,289 @@ +#include "openmc/random_ray/random_ray.h" + +#include "openmc/geometry.h" +#include "openmc/message_passing.h" +#include "openmc/mgxs_interface.h" +#include "openmc/random_ray/flat_source_domain.h" +#include "openmc/search.h" +#include "openmc/settings.h" +#include "openmc/simulation.h" +#include "openmc/source.h" + +namespace openmc { + +//============================================================================== +// Non-method functions +//============================================================================== + +// returns 1 - exp(-tau) +// Equivalent to -(_expm1f(-tau)), but faster +// Written by Colin Josey. +float cjosey_exponential(float tau) +{ + constexpr float c1n = -1.0000013559236386308f; + constexpr float c2n = 0.23151368626911062025f; + constexpr float c3n = -0.061481916409314966140f; + constexpr float c4n = 0.0098619906458127653020f; + constexpr float c5n = -0.0012629460503540849940f; + constexpr float c6n = 0.00010360973791574984608f; + constexpr float c7n = -0.000013276571933735820960f; + + constexpr float c0d = 1.0f; + constexpr float c1d = -0.73151337729389001396f; + constexpr float c2d = 0.26058381273536471371f; + constexpr float c3d = -0.059892419041316836940f; + constexpr float c4d = 0.0099070188241094279067f; + constexpr float c5d = -0.0012623388962473160860f; + constexpr float c6d = 0.00010361277635498731388f; + constexpr float c7d = -0.000013276569500666698498f; + + float x = -tau; + + float den = c7d; + den = den * x + c6d; + den = den * x + c5d; + den = den * x + c4d; + den = den * x + c3d; + den = den * x + c2d; + den = den * x + c1d; + den = den * x + c0d; + + float num = c7n; + num = num * x + c6n; + num = num * x + c5n; + num = num * x + c4n; + num = num * x + c3n; + num = num * x + c2n; + num = num * x + c1n; + num = num * x; + + return num / den; +} + +//============================================================================== +// RandomRay implementation +//============================================================================== + +// Static Variable Declarations +double RandomRay::distance_inactive_; +double RandomRay::distance_active_; +unique_ptr RandomRay::ray_source_; + +RandomRay::RandomRay() + : negroups_(data::mg.num_energy_groups_), + angular_flux_(data::mg.num_energy_groups_), + delta_psi_(data::mg.num_energy_groups_) +{} + +RandomRay::RandomRay(uint64_t ray_id, FlatSourceDomain* domain) : RandomRay() +{ + initialize_ray(ray_id, domain); +} + +// Transports ray until termination criteria are met +uint64_t RandomRay::transport_history_based_single_ray() +{ + using namespace openmc; + while (alive()) { + event_advance_ray(); + if (!alive()) + break; + event_cross_surface(); + } + + return n_event(); +} + +// Transports ray across a single source region +void RandomRay::event_advance_ray() +{ + // Find the distance to the nearest boundary + boundary() = distance_to_boundary(*this); + double distance = boundary().distance; + + if (distance <= 0.0) { + mark_as_lost("Negative transport distance detected for particle " + + std::to_string(id())); + return; + } + + if (is_active_) { + // If the ray is in the active length, need to check if it has + // reached its maximum termination distance. If so, reduce + // the ray traced length so that the ray does not overrun the + // maximum numerical length (so as to avoid numerical bias). + if (distance_travelled_ + distance >= distance_active_) { + distance = distance_active_ - distance_travelled_; + wgt() = 0.0; + } + + distance_travelled_ += distance; + attenuate_flux(distance, true); + } else { + // If the ray is still in the dead zone, need to check if it + // has entered the active phase. If so, split into two segments (one + // representing the final part of the dead zone, the other representing the + // first part of the active length) and attenuate each. Otherwise, if the + // full length of the segment is within the dead zone, attenuate as normal. + if (distance_travelled_ + distance >= distance_inactive_) { + is_active_ = true; + double distance_dead = distance_inactive_ - distance_travelled_; + attenuate_flux(distance_dead, false); + + double distance_alive = distance - distance_dead; + + // Ensure we haven't travelled past the active phase as well + if (distance_alive > distance_active_) { + distance_alive = distance_active_; + wgt() = 0.0; + } + + attenuate_flux(distance_alive, true); + distance_travelled_ = distance_alive; + } else { + distance_travelled_ += distance; + attenuate_flux(distance, false); + } + } + + // Advance particle + for (int j = 0; j < n_coord(); ++j) { + coord(j).r += distance * coord(j).u; + } +} + +// This function forms the inner loop of the random ray transport process. +// It is responsible for several tasks. Based on the incoming angular flux +// of the ray and the source term in the region, the outgoing angular flux +// is computed. The delta psi between the incoming and outgoing fluxes is +// contributed to the estimate of the total scalar flux in the source region. +// Additionally, the contribution of the ray path to the stochastically +// estimated volume is also kept track of. All tasks involving writing +// to the data for the source region are done with a lock over the entire +// source region. Locks are used instead of atomics as all energy groups +// must be written, such that locking once is typically much more efficient +// than use of many atomic operations corresponding to each energy group +// individually (at least on CPU). Several other bookkeeping tasks are also +// performed when inside the lock. +void RandomRay::attenuate_flux(double distance, bool is_active) +{ + // The number of geometric intersections is counted for reporting purposes + n_event()++; + + // Determine source region index etc. + int i_cell = lowest_coord().cell; + + // The source region is the spatial region index + int64_t source_region = + domain_->source_region_offsets_[i_cell] + cell_instance(); + + // The source element is the energy-specific region index + int64_t source_element = source_region * negroups_; + int material = this->material(); + + // Temperature and angle indices, if using multiple temperature + // data sets and/or anisotropic data sets. + // TODO: Currently assumes we are only using single temp/single + // angle data. + const int t = 0; + const int a = 0; + + // MOC incoming flux attenuation + source contribution/attenuation equation + for (int g = 0; g < negroups_; g++) { + float sigma_t = data::mg.macro_xs_[material].get_xs( + MgxsType::TOTAL, g, NULL, NULL, NULL, t, a); + float tau = sigma_t * distance; + float exponential = cjosey_exponential(tau); // exponential = 1 - exp(-tau) + float new_delta_psi = + (angular_flux_[g] - domain_->source_[source_element + g]) * exponential; + delta_psi_[g] = new_delta_psi; + angular_flux_[g] -= new_delta_psi; + } + + // If ray is in the active phase (not in dead zone), make contributions to + // source region bookkeeping + if (is_active) { + + // Aquire lock for source region + domain_->lock_[source_region].lock(); + + // Accumulate delta psi into new estimate of source region flux for + // this iteration + for (int g = 0; g < negroups_; g++) { + domain_->scalar_flux_new_[source_element + g] += delta_psi_[g]; + } + + // If the source region hasn't been hit yet this iteration, + // indicate that it now has + if (domain_->was_hit_[source_region] == 0) { + domain_->was_hit_[source_region] = 1; + } + + // Accomulate volume (ray distance) into this iteration's estimate + // of the source region's volume + domain_->volume_[source_region] += distance; + + // Tally valid position inside the source region (e.g., midpoint of + // the ray) if not done already + if (!domain_->position_recorded_[source_region]) { + Position midpoint = r() + u() * (distance / 2.0); + domain_->position_[source_region] = midpoint; + domain_->position_recorded_[source_region] = 1; + } + + // Release lock + domain_->lock_[source_region].unlock(); + } +} + +void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain) +{ + domain_ = domain; + + // Reset particle event counter + n_event() = 0; + + is_active_ = (distance_inactive_ <= 0.0); + + wgt() = 1.0; + + // set identifier for particle + id() = simulation::work_index[mpi::rank] + ray_id; + + // 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; + + // Sample from ray source distribution + SourceSite site {ray_source_->sample(current_seed())}; + site.E = lower_bound_index( + data::mg.rev_energy_bins_.begin(), data::mg.rev_energy_bins_.end(), site.E); + site.E = negroups_ - site.E - 1.; + this->from_source(&site); + + // Locate ray + if (lowest_coord().cell == C_NONE) { + if (!exhaustive_find_cell(*this)) { + this->mark_as_lost( + "Could not find the cell containing particle " + std::to_string(id())); + } + + // Set birth cell attribute + if (cell_born() == C_NONE) + cell_born() = lowest_coord().cell; + } + + // Initialize ray's starting angular flux to starting location's isotropic + // source + int i_cell = lowest_coord().cell; + int64_t source_region_idx = + domain_->source_region_offsets_[i_cell] + cell_instance(); + + for (int g = 0; g < negroups_; g++) { + angular_flux_[g] = domain_->source_[source_region_idx * negroups_ + g]; + } +} + +} // namespace openmc diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp new file mode 100644 index 000000000..b9fd93a48 --- /dev/null +++ b/src/random_ray/random_ray_simulation.cpp @@ -0,0 +1,397 @@ +#include "openmc/random_ray/random_ray_simulation.h" + +#include "openmc/eigenvalue.h" +#include "openmc/geometry.h" +#include "openmc/message_passing.h" +#include "openmc/mgxs_interface.h" +#include "openmc/output.h" +#include "openmc/plot.h" +#include "openmc/random_ray/random_ray.h" +#include "openmc/simulation.h" +#include "openmc/source.h" +#include "openmc/tallies/filter.h" +#include "openmc/tallies/tally.h" +#include "openmc/tallies/tally_scoring.h" +#include "openmc/timer.h" + +namespace openmc { + +//============================================================================== +// Non-member functions +//============================================================================== + +void openmc_run_random_ray() +{ + // Initialize OpenMC general data structures + openmc_simulation_init(); + + // Validate that inputs meet requirements for random ray mode + if (mpi::master) + validate_random_ray_inputs(); + + // Initialize Random Ray Simulation Object + RandomRaySimulation sim; + + // Begin main simulation timer + simulation::time_total.start(); + + // Execute random ray simulation + sim.simulate(); + + // End main simulation timer + openmc::simulation::time_total.stop(); + + // Finalize OpenMC + openmc_simulation_finalize(); + + // Reduce variables across MPI ranks + sim.reduce_simulation_statistics(); + + // Output all simulation results + sim.output_simulation_results(); +} + +// Enforces restrictions on inputs in random ray mode. While there are +// many features that don't make sense in random ray mode, and are therefore +// unsupported, we limit our testing/enforcement operations only to inputs +// that may cause erroneous/misleading output or crashes from the solver. +void validate_random_ray_inputs() +{ + // Validate tallies + /////////////////////////////////////////////////////////////////// + for (auto& tally : model::tallies) { + + // Validate score types + for (auto score_bin : tally->scores_) { + switch (score_bin) { + case SCORE_FLUX: + case SCORE_TOTAL: + case SCORE_FISSION: + case SCORE_NU_FISSION: + case SCORE_EVENTS: + break; + default: + fatal_error( + "Invalid score specified. Only flux, total, fission, nu-fission, and " + "event scores are supported in random ray mode."); + } + } + + // Validate filter types + for (auto f : tally->filters()) { + auto& filter = *model::tally_filters[f]; + + switch (filter.type()) { + case FilterType::CELL: + case FilterType::CELL_INSTANCE: + case FilterType::DISTRIBCELL: + case FilterType::ENERGY: + case FilterType::MATERIAL: + case FilterType::MESH: + case FilterType::UNIVERSE: + break; + default: + fatal_error("Invalid filter specified. Only cell, cell_instance, " + "distribcell, energy, material, mesh, and universe filters " + "are supported in random ray mode."); + } + } + } + + // Validate MGXS data + /////////////////////////////////////////////////////////////////// + for (auto& material : data::mg.macro_xs_) { + if (!material.is_isotropic) { + fatal_error("Anisotropic MGXS detected. Only isotropic XS data sets " + "supported in random ray mode."); + } + if (material.get_xsdata().size() > 1) { + fatal_error("Non-isothermal MGXS detected. Only isothermal XS data sets " + "supported in random ray mode."); + } + } + + // Validate solver mode + /////////////////////////////////////////////////////////////////// + if (settings::run_mode == RunMode::FIXED_SOURCE) { + fatal_error( + "Invalid run mode. Fixed source not yet supported in random ray mode."); + } + + // Validate ray source + /////////////////////////////////////////////////////////////////// + + // Check for independent source + IndependentSource* is = + dynamic_cast(RandomRay::ray_source_.get()); + if (!is) { + fatal_error( + "Invalid ray source definition. Ray source must be IndependentSource."); + } + + // Check for box source + SpatialDistribution* space_dist = is->space(); + SpatialBox* sb = dynamic_cast(space_dist); + if (!sb) { + fatal_error( + "Invalid source definition -- only box sources are allowed in random " + "ray " + "mode. If no source is specified, OpenMC default is an isotropic point " + "source at the origin, which is invalid in random ray mode."); + } + + // Check that box source is not restricted to fissionable areas + if (sb->only_fissionable()) { + fatal_error("Invalid source definition -- fissionable spatial distribution " + "not allowed for random ray source."); + } + + // Check for isotropic source + UnitSphereDistribution* angle_dist = is->angle(); + Isotropic* id = dynamic_cast(angle_dist); + if (!id) { + fatal_error("Invalid source definition -- only isotropic sources are " + "allowed for random ray source."); + } + + // Validate plotting files + /////////////////////////////////////////////////////////////////// + for (int p = 0; p < model::plots.size(); p++) { + + // Get handle to OpenMC plot object + Plot* openmc_plot = dynamic_cast(model::plots[p].get()); + + // Random ray plots only support voxel plots + if (!openmc_plot) { + warning(fmt::format( + "Plot {} will not be used for end of simulation data plotting -- only " + "voxel plotting is allowed in random ray mode.", + p)); + continue; + } else if (openmc_plot->type_ != Plot::PlotType::voxel) { + warning(fmt::format( + "Plot {} will not be used for end of simulation data plotting -- only " + "voxel plotting is allowed in random ray mode.", + p)); + continue; + } + } + + // Warn about slow MPI domain replication, if detected + /////////////////////////////////////////////////////////////////// +#ifdef OPENMC_MPI + if (mpi::n_procs > 1) { + warning( + "Domain replication in random ray is supported, but suffers from poor " + "scaling of source all-reduce operations. Performance may severely " + "degrade beyond just a few MPI ranks. Domain decomposition may be " + "implemented in the future to provide efficient scaling."); + } +#endif +} + +//============================================================================== +// RandomRaySimulation implementation +//============================================================================== + +RandomRaySimulation::RandomRaySimulation() + : negroups_(data::mg.num_energy_groups_) +{ + // There are no source sites in random ray mode, so be sure to disable to + // ensure we don't attempt to write source sites to statepoint + settings::source_write = false; + + // Random ray mode does not have an inner loop over generations within a + // batch, so set the current gen to 1 + simulation::current_gen = 1; +} + +void RandomRaySimulation::simulate() +{ + // Random ray power iteration loop + while (simulation::current_batch < settings::n_batches) { + + // Initialize the current batch + initialize_batch(); + initialize_generation(); + + // Reset total starting particle weight used for normalizing tallies + simulation::total_weight = 1.0; + + // Update source term (scattering + fission) + domain_.update_neutron_source(k_eff_); + + // Reset scalar fluxes, iteration volume tallies, and region hit flags to + // zero + domain_.batch_reset(); + + // Start timer for transport + simulation::time_transport.start(); + +// Transport sweep over all random rays for the iteration +#pragma omp parallel for schedule(dynamic) \ + reduction(+ : total_geometric_intersections_) + for (int i = 0; i < simulation::work_per_rank; i++) { + RandomRay ray(i, &domain_); + total_geometric_intersections_ += + ray.transport_history_based_single_ray(); + } + + simulation::time_transport.stop(); + + // If using multiple MPI ranks, perform all reduce on all transport results + domain_.all_reduce_replicated_source_regions(); + + // Normalize scalar flux and update volumes + domain_.normalize_scalar_flux_and_volumes( + settings::n_particles * RandomRay::distance_active_); + + // Add source to scalar flux, compute number of FSR hits + int64_t n_hits = domain_.add_source_to_scalar_flux(); + + // Compute random ray k-eff + k_eff_ = domain_.compute_k_eff(k_eff_); + + // Store random ray k-eff into OpenMC's native k-eff variable + global_tally_tracklength = k_eff_; + + // Execute all tallying tasks, if this is an active batch + if (simulation::current_batch > settings::n_inactive && mpi::master) { + + // Generate mapping between source regions and tallies + if (!domain_.mapped_all_tallies_) { + domain_.convert_source_regions_to_tallies(); + } + + // Use above mapping to contribute FSR flux data to appropriate tallies + domain_.random_ray_tally(); + + // Add this iteration's scalar flux estimate to final accumulated estimate + domain_.accumulate_iteration_flux(); + } + + // Set phi_old = phi_new + domain_.scalar_flux_old_.swap(domain_.scalar_flux_new_); + + // Check for any obvious insabilities/nans/infs + instability_check(n_hits, k_eff_, avg_miss_rate_); + + // Finalize the current batch + finalize_generation(); + finalize_batch(); + } // End random ray power iteration loop +} + +void RandomRaySimulation::reduce_simulation_statistics() +{ + // Reduce number of intersections +#ifdef OPENMC_MPI + if (mpi::n_procs > 1) { + uint64_t total_geometric_intersections_reduced = 0; + MPI_Reduce(&total_geometric_intersections_, + &total_geometric_intersections_reduced, 1, MPI_UNSIGNED_LONG, MPI_SUM, 0, + mpi::intracomm); + total_geometric_intersections_ = total_geometric_intersections_reduced; + } +#endif +} + +void RandomRaySimulation::output_simulation_results() const +{ + // Print random ray results + if (mpi::master) { + print_results_random_ray(total_geometric_intersections_, + avg_miss_rate_ / settings::n_batches, negroups_, + domain_.n_source_regions_); + if (model::plots.size() > 0) { + domain_.output_to_vtk(); + } + } +} + +// Apply a few sanity checks to catch obvious cases of numerical instability. +// Instability typically only occurs if ray density is extremely low. +void RandomRaySimulation::instability_check( + int64_t n_hits, double k_eff, double& avg_miss_rate) const +{ + double percent_missed = ((domain_.n_source_regions_ - n_hits) / + static_cast(domain_.n_source_regions_)) * + 100.0; + avg_miss_rate += percent_missed; + + if (percent_missed > 10.0) { + warning(fmt::format( + "Very high FSR miss rate detected ({:.3f}%). Instability may occur. " + "Increase ray density by adding more rays and/or active distance.", + percent_missed)); + } else if (percent_missed > 0.01) { + warning(fmt::format("Elevated FSR miss rate detected ({:.3f}%). Increasing " + "ray density by adding more rays and/or active " + "distance may improve simulation efficiency.", + percent_missed)); + } + + if (k_eff > 10.0 || k_eff < 0.01 || !(std::isfinite(k_eff))) { + fatal_error("Instability detected"); + } +} + +// Print random ray simulation results +void RandomRaySimulation::print_results_random_ray( + uint64_t total_geometric_intersections, double avg_miss_rate, int negroups, + int64_t n_source_regions) const +{ + using namespace simulation; + + if (settings::verbosity >= 6) { + double total_integrations = total_geometric_intersections * negroups; + double time_per_integration = + simulation::time_transport.elapsed() / total_integrations; + double misc_time = time_total.elapsed() - time_update_src.elapsed() - + time_transport.elapsed() - time_tallies.elapsed() - + time_bank_sendrecv.elapsed(); + + header("Simulation Statistics", 4); + fmt::print( + " Total Iterations = {}\n", settings::n_batches); + fmt::print(" Flat Source Regions (FSRs) = {}\n", n_source_regions); + fmt::print(" Total Geometric Intersections = {:.4e}\n", + static_cast(total_geometric_intersections)); + fmt::print(" Avg per Iteration = {:.4e}\n", + static_cast(total_geometric_intersections) / settings::n_batches); + fmt::print(" Avg per Iteration per FSR = {:.2f}\n", + static_cast(total_geometric_intersections) / + static_cast(settings::n_batches) / n_source_regions); + fmt::print(" Avg FSR Miss Rate per Iteration = {:.4f}%\n", avg_miss_rate); + fmt::print(" Energy Groups = {}\n", negroups); + fmt::print( + " Total Integrations = {:.4e}\n", total_integrations); + fmt::print(" Avg per Iteration = {:.4e}\n", + total_integrations / settings::n_batches); + + header("Timing Statistics", 4); + show_time("Total time for initialization", time_initialize.elapsed()); + show_time("Reading cross sections", time_read_xs.elapsed(), 1); + show_time("Total simulation time", time_total.elapsed()); + show_time("Transport sweep only", time_transport.elapsed(), 1); + show_time("Source update only", time_update_src.elapsed(), 1); + show_time("Tally conversion only", time_tallies.elapsed(), 1); + show_time("MPI source reductions only", time_bank_sendrecv.elapsed(), 1); + show_time("Other iteration routines", misc_time, 1); + if (settings::run_mode == RunMode::EIGENVALUE) { + show_time("Time in inactive batches", time_inactive.elapsed()); + } + show_time("Time in active batches", time_active.elapsed()); + show_time("Time writing statepoints", time_statepoint.elapsed()); + show_time("Total time for finalization", time_finalize.elapsed()); + show_time("Time per integration", time_per_integration); + } + + if (settings::verbosity >= 4) { + header("Results", 4); + fmt::print(" k-effective = {:.5f} +/- {:.5f}\n", + simulation::keff, simulation::keff_std); + } +} + +} // namespace openmc diff --git a/src/settings.cpp b/src/settings.cpp index 9f183a6c1..6790f2cc0 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -24,6 +24,7 @@ #include "openmc/output.h" #include "openmc/plot.h" #include "openmc/random_lcg.h" +#include "openmc/random_ray/random_ray.h" #include "openmc/simulation.h" #include "openmc/source.h" #include "openmc/string_utils.h" @@ -113,6 +114,7 @@ double res_scat_energy_min {0.01}; double res_scat_energy_max {1000.0}; vector res_scat_nuclides; RunMode run_mode {RunMode::UNSET}; +SolverType solver_type {SolverType::MONTE_CARLO}; std::unordered_set sourcepoint_batch; std::unordered_set statepoint_batch; std::unordered_set source_write_surf_id; @@ -233,6 +235,38 @@ void get_run_parameters(pugi::xml_node node_base) } } } + + // Random ray variables + if (solver_type == SolverType::RANDOM_RAY) { + xml_node random_ray_node = node_base.child("random_ray"); + if (check_for_node(random_ray_node, "distance_active")) { + RandomRay::distance_active_ = + std::stod(get_node_value(random_ray_node, "distance_active")); + if (RandomRay::distance_active_ <= 0.0) { + fatal_error("Random ray active distance must be greater than 0"); + } + } else { + fatal_error("Specify random ray active distance in settings XML"); + } + if (check_for_node(random_ray_node, "distance_inactive")) { + RandomRay::distance_inactive_ = + std::stod(get_node_value(random_ray_node, "distance_inactive")); + if (RandomRay::distance_inactive_ < 0) { + fatal_error( + "Random ray inactive distance must be greater than or equal to 0"); + } + } else { + fatal_error("Specify random ray inactive distance in settings XML"); + } + if (check_for_node(random_ray_node, "source")) { + xml_node source_node = random_ray_node.child("source"); + // Get point to list of elements and make sure there is at least + // one + RandomRay::ray_source_ = Source::create(source_node); + } else { + fatal_error("Specify random ray source in settings XML"); + } + } } void read_settings_xml() @@ -389,6 +423,14 @@ void read_settings_xml(pugi::xml_node root) } } + // Check solver type + if (check_for_node(root, "random_ray")) { + solver_type = SolverType::RANDOM_RAY; + if (run_CE) + fatal_error("multi-group energy mode must be specified in settings XML " + "when using the random ray solver."); + } + if (run_mode == RunMode::EIGENVALUE || run_mode == RunMode::FIXED_SOURCE) { // Read run parameters get_run_parameters(node_mode); diff --git a/src/simulation.cpp b/src/simulation.cpp index a7aea6942..1cf34a820 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -122,7 +122,8 @@ int openmc_simulation_init() write_message("Resuming simulation...", 6); } else { // Only initialize primary source bank for eigenvalue simulations - if (settings::run_mode == RunMode::EIGENVALUE) { + if (settings::run_mode == RunMode::EIGENVALUE && + settings::solver_type == SolverType::MONTE_CARLO) { initialize_source(); } } @@ -132,7 +133,11 @@ int openmc_simulation_init() if (settings::run_mode == RunMode::FIXED_SOURCE) { header("FIXED SOURCE TRANSPORT SIMULATION", 3); } else if (settings::run_mode == RunMode::EIGENVALUE) { - header("K EIGENVALUE SIMULATION", 3); + if (settings::solver_type == SolverType::MONTE_CARLO) { + header("K EIGENVALUE SIMULATION", 3); + } else if (settings::solver_type == SolverType::RANDOM_RAY) { + header("K EIGENVALUE SIMULATION (RANDOM RAY SOLVER)", 3); + } if (settings::verbosity >= 7) print_columns(); } @@ -196,10 +201,12 @@ int openmc_simulation_finalize() simulation::time_finalize.stop(); simulation::time_total.stop(); if (mpi::master) { - if (settings::verbosity >= 6) - print_runtime(); - if (settings::verbosity >= 4) - print_results(); + if (settings::solver_type != SolverType::RANDOM_RAY) { + if (settings::verbosity >= 6) + print_runtime(); + if (settings::verbosity >= 4) + print_results(); + } } if (settings::check_overlaps) print_overlap_check(); @@ -309,7 +316,8 @@ vector work_index; void allocate_banks() { - if (settings::run_mode == RunMode::EIGENVALUE) { + if (settings::run_mode == RunMode::EIGENVALUE && + settings::solver_type == SolverType::MONTE_CARLO) { // Allocate source bank simulation::source_bank.resize(simulation::work_per_rank); @@ -493,7 +501,8 @@ void finalize_generation() } global_tally_leakage = 0.0; - if (settings::run_mode == RunMode::EIGENVALUE) { + if (settings::run_mode == RunMode::EIGENVALUE && + settings::solver_type == SolverType::MONTE_CARLO) { // If using shared memory, stable sort the fission bank (by parent IDs) // so as to allow for reproducibility regardless of which order particles // are run in. @@ -501,6 +510,9 @@ void finalize_generation() // Distribute fission bank across processors evenly synchronize_bank(); + } + + if (settings::run_mode == RunMode::EIGENVALUE) { // Calculate shannon entropy if (settings::entropy_on) diff --git a/src/source.cpp b/src/source.cpp index 778962667..5ddac7f42 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -243,36 +243,40 @@ SourceSite IndependentSource::sample(uint64_t* seed) const // Sample angle site.u = angle_->sample(seed); - // Check for monoenergetic source above maximum particle energy - auto p = static_cast(particle_); - 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])) { - fatal_error("Source energy above range of energies of at least " - "one cross section table"); + // Sample energy and time for neutron and photon sources + if (settings::solver_type != SolverType::RANDOM_RAY) { + // Check for monoenergetic source above maximum particle energy + auto p = static_cast(particle_); + 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])) { + fatal_error("Source energy above range of energies of at least " + "one cross section table"); + } } - } - while (true) { - // Sample energy spectrum - site.E = energy_->sample(seed); + while (true) { + // Sample energy spectrum + site.E = energy_->sample(seed); - // Resample if energy falls above maximum particle energy - if (site.E < data::energy_max[p]) - break; + // Resample if energy falls above maximum particle energy + if (site.E < data::energy_max[p]) + break; - n_reject++; - if (n_reject >= EXTSRC_REJECT_THRESHOLD && - static_cast(n_accept) / n_reject <= EXTSRC_REJECT_FRACTION) { - fatal_error("More than 95% of external source sites sampled were " - "rejected. Please check your external source energy spectrum " - "definition."); + n_reject++; + if (n_reject >= EXTSRC_REJECT_THRESHOLD && + static_cast(n_accept) / n_reject <= EXTSRC_REJECT_FRACTION) { + fatal_error( + "More than 95% of external source sites sampled were " + "rejected. Please check your external source energy spectrum " + "definition."); + } } - } - // Sample particle creation time - site.time = time_->sample(seed); + // Sample particle creation time + site.time = time_->sample(seed); + } // Increment number of accepted samples ++n_accept; diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index df1ec5ea7..2e77bcad2 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -757,6 +757,10 @@ void Tally::accumulate() double norm = total_source / (settings::n_particles * settings::gen_per_batch); + if (settings::solver_type == SolverType::RANDOM_RAY) { + norm = 1.0; + } + // Accumulate each result #pragma omp parallel for for (int i = 0; i < results_.shape()[0]; ++i) { @@ -953,8 +957,9 @@ void accumulate_tallies() { #ifdef OPENMC_MPI // Combine tally results onto master process - if (mpi::n_procs > 1) + if (mpi::n_procs > 1 && settings::solver_type == SolverType::MONTE_CARLO) { reduce_tally_results(); + } #endif // Increase number of realizations (only used for global tallies) diff --git a/src/timer.cpp b/src/timer.cpp index 86436758a..6d692d4fb 100644 --- a/src/timer.cpp +++ b/src/timer.cpp @@ -26,6 +26,7 @@ Timer time_event_advance_particle; Timer time_event_surface_crossing; Timer time_event_collision; Timer time_event_death; +Timer time_update_src; } // namespace simulation @@ -85,6 +86,7 @@ void reset_timers() simulation::time_event_surface_crossing.reset(); simulation::time_event_collision.reset(); simulation::time_event_death.reset(); + simulation::time_update_src.reset(); } } // namespace openmc diff --git a/tests/regression_tests/random_ray_basic/__init__.py b/tests/regression_tests/random_ray_basic/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_basic/inputs_true.dat b/tests/regression_tests/random_ray_basic/inputs_true.dat new file mode 100644 index 000000000..97b7906f7 --- /dev/null +++ b/tests/regression_tests/random_ray_basic/inputs_true.dat @@ -0,0 +1,108 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.126 0.126 + 10 10 + -0.63 -0.63 + +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 + + + 1.26 1.26 + 2 2 + -1.26 -1.26 + +2 2 +2 5 + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + multi-group + + 100.0 + 20.0 + + + -1.26 -1.26 -1 1.26 1.26 1 + + + + + + + 2 2 + -1.26 -1.26 + 1.26 1.26 + + + 1 + + + 1e-05 0.0635 10.0 100.0 1000.0 500000.0 1000000.0 20000000.0 + + + 1 2 + flux fission nu-fission + analog + + + diff --git a/tests/regression_tests/random_ray_basic/results_true.dat b/tests/regression_tests/random_ray_basic/results_true.dat new file mode 100644 index 000000000..802e78a82 --- /dev/null +++ b/tests/regression_tests/random_ray_basic/results_true.dat @@ -0,0 +1,171 @@ +k-combined: +8.400322E-01 8.023349E-03 +tally 1: +1.260220E+00 +3.179889E-01 +1.484289E-01 +4.411066E-03 +3.612463E-01 +2.612843E-02 +7.086707E-01 +1.006119E-01 +3.342483E-02 +2.238499E-04 +8.134936E-02 +1.325949E-03 +4.194328E-01 +3.558669E-02 +4.287776E-03 +3.717447E-06 +1.043559E-02 +2.201986E-05 +5.878720E-01 +7.045887E-02 +6.147757E-03 +7.701173E-06 +1.496241E-02 +4.561699E-05 +1.768113E+00 +6.356917E-01 +6.513486E-03 +8.628535E-06 +1.585272E-02 +5.111136E-05 +5.063704E+00 +5.152401E+00 +2.440293E-03 +1.196869E-06 +6.038334E-03 +7.328193E-06 +3.253717E+00 +2.117655E+00 +1.389120E-02 +3.859385E-05 +3.863767E-02 +2.985798E-04 +1.876994E+00 +7.046366E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +8.390875E-01 +1.408791E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.513839E-01 +4.139640E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +6.682186E-01 +9.116003E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.849034E+00 +6.944337E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.523425E+00 +4.112118E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.821432E+00 +1.592568E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.159618E+00 +2.694138E-01 +1.354028E-01 +3.672094E-03 +3.295432E-01 +2.175122E-02 +6.880334E-01 +9.491215E-02 +3.234611E-02 +2.097782E-04 +7.872396E-02 +1.242596E-03 +4.184841E-01 +3.536436E-02 +4.274305E-03 +3.687209E-06 +1.040280E-02 +2.184074E-05 +5.810180E-01 +6.872944E-02 +6.060273E-03 +7.476500E-06 +1.474949E-02 +4.428617E-05 +1.782580E+00 +6.457892E-01 +6.552384E-03 +8.730345E-06 +1.594739E-02 +5.171444E-05 +5.278155E+00 +5.596601E+00 +2.546878E-03 +1.303010E-06 +6.302072E-03 +7.978072E-06 +3.420419E+00 +2.340454E+00 +1.465798E-02 +4.299061E-05 +4.077042E-02 +3.325951E-04 +1.279417E+00 +3.278133E-01 +1.509073E-01 +4.561836E-03 +3.672782E-01 +2.702150E-02 +7.212777E-01 +1.042487E-01 +3.411552E-02 +2.332877E-04 +8.303035E-02 +1.381852E-03 +4.269473E-01 +3.685202E-02 +4.378540E-03 +3.872997E-06 +1.065649E-02 +2.294124E-05 +5.973530E-01 +7.266946E-02 +6.260881E-03 +7.976490E-06 +1.523773E-02 +4.724780E-05 +1.795373E+00 +6.547440E-01 +6.635941E-03 +8.945067E-06 +1.615075E-02 +5.298634E-05 +5.161876E+00 +5.353441E+00 +2.505311E-03 +1.261399E-06 +6.199218E-03 +7.723296E-06 +3.344042E+00 +2.236603E+00 +1.443089E-02 +4.166228E-05 +4.013879E-02 +3.223186E-04 diff --git a/tests/regression_tests/random_ray_basic/test.py b/tests/regression_tests/random_ray_basic/test.py new file mode 100644 index 000000000..1727a6371 --- /dev/null +++ b/tests/regression_tests/random_ray_basic/test.py @@ -0,0 +1,232 @@ +import os + +import numpy as np +import openmc + +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 random_ray_model() -> openmc.Model: + ############################################################################### + # Create multigroup data + + # Instantiate the energy group data + group_edges = [1e-5, 0.0635, 10.0, 1.0e2, 1.0e3, 0.5e6, 1.0e6, 20.0e6] + groups = openmc.mgxs.EnergyGroups(group_edges) + + # Instantiate the 7-group (C5G7) cross section data + uo2_xsdata = openmc.XSdata('UO2', groups) + uo2_xsdata.order = 0 + uo2_xsdata.set_total( + [0.1779492, 0.3298048, 0.4803882, 0.5543674, 0.3118013, 0.3951678, + 0.5644058]) + uo2_xsdata.set_absorption([8.0248e-03, 3.7174e-03, 2.6769e-02, 9.6236e-02, + 3.0020e-02, 1.1126e-01, 2.8278e-01]) + scatter_matrix = np.array( + [[[0.1275370, 0.0423780, 0.0000094, 0.0000000, 0.0000000, 0.0000000, 0.0000000], + [0.0000000, 0.3244560, 0.0016314, 0.0000000, 0.0000000, 0.0000000, 0.0000000], + [0.0000000, 0.0000000, 0.4509400, 0.0026792, 0.0000000, 0.0000000, 0.0000000], + [0.0000000, 0.0000000, 0.0000000, 0.4525650, 0.0055664, 0.0000000, 0.0000000], + [0.0000000, 0.0000000, 0.0000000, 0.0001253, 0.2714010, 0.0102550, 0.0000000], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0012968, 0.2658020, 0.0168090], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0085458, 0.2730800]]]) + scatter_matrix = np.rollaxis(scatter_matrix, 0, 3) + uo2_xsdata.set_scatter_matrix(scatter_matrix) + uo2_xsdata.set_fission([7.21206e-03, 8.19301e-04, 6.45320e-03, + 1.85648e-02, 1.78084e-02, 8.30348e-02, + 2.16004e-01]) + uo2_xsdata.set_nu_fission([2.005998e-02, 2.027303e-03, 1.570599e-02, + 4.518301e-02, 4.334208e-02, 2.020901e-01, + 5.257105e-01]) + uo2_xsdata.set_chi([5.8791e-01, 4.1176e-01, 3.3906e-04, 1.1761e-07, 0.0000e+00, + 0.0000e+00, 0.0000e+00]) + + h2o_xsdata = openmc.XSdata('LWTR', groups) + h2o_xsdata.order = 0 + h2o_xsdata.set_total([0.15920605, 0.412969593, 0.59030986, 0.58435, + 0.718, 1.2544497, 2.650379]) + h2o_xsdata.set_absorption([6.0105e-04, 1.5793e-05, 3.3716e-04, + 1.9406e-03, 5.7416e-03, 1.5001e-02, + 3.7239e-02]) + scatter_matrix = np.array( + [[[0.0444777, 0.1134000, 0.0007235, 0.0000037, 0.0000001, 0.0000000, 0.0000000], + [0.0000000, 0.2823340, 0.1299400, 0.0006234, 0.0000480, 0.0000074, 0.0000010], + [0.0000000, 0.0000000, 0.3452560, 0.2245700, 0.0169990, 0.0026443, 0.0005034], + [0.0000000, 0.0000000, 0.0000000, 0.0910284, 0.4155100, 0.0637320, 0.0121390], + [0.0000000, 0.0000000, 0.0000000, 0.0000714, 0.1391380, 0.5118200, 0.0612290], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0022157, 0.6999130, 0.5373200], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.1324400, 2.4807000]]]) + scatter_matrix = np.rollaxis(scatter_matrix, 0, 3) + h2o_xsdata.set_scatter_matrix(scatter_matrix) + + mg_cross_sections = openmc.MGXSLibrary(groups) + mg_cross_sections.add_xsdatas([uo2_xsdata, h2o_xsdata]) + mg_cross_sections.export_to_hdf5() + + ############################################################################### + # Create materials for the problem + + # Instantiate some Materials and register the appropriate macroscopic data + uo2 = openmc.Material(name='UO2 fuel') + uo2.set_density('macro', 1.0) + uo2.add_macroscopic('UO2') + + water = openmc.Material(name='Water') + water.set_density('macro', 1.0) + water.add_macroscopic('LWTR') + + # Instantiate a Materials collection and export to XML + materials = openmc.Materials([uo2, water]) + materials.cross_sections = "mgxs.h5" + + ############################################################################### + # Define problem geometry + + ######################################## + # Define an unbounded pincell universe + + pitch = 1.26 + + # Create a surface for the fuel outer radius + fuel_or = openmc.ZCylinder(r=0.54, name='Fuel OR') + inner_ring_a = openmc.ZCylinder(r=0.33, name='inner ring a') + inner_ring_b = openmc.ZCylinder(r=0.45, name='inner ring b') + outer_ring_a = openmc.ZCylinder(r=0.60, name='outer ring a') + outer_ring_b = openmc.ZCylinder(r=0.69, name='outer ring b') + + # Instantiate Cells + fuel_a = openmc.Cell(fill=uo2, region=-inner_ring_a, name='fuel inner a') + fuel_b = openmc.Cell(fill=uo2, region=+inner_ring_a & -inner_ring_b, name='fuel inner b') + fuel_c = openmc.Cell(fill=uo2, region=+inner_ring_b & -fuel_or, name='fuel inner c') + moderator_a = openmc.Cell(fill=water, region=+fuel_or & -outer_ring_a, name='moderator inner a') + moderator_b = openmc.Cell(fill=water, region=+outer_ring_a & -outer_ring_b, name='moderator outer b') + moderator_c = openmc.Cell(fill=water, region=+outer_ring_b, name='moderator outer c') + + # Create pincell universe + pincell_base = openmc.Universe() + + # Register Cells with Universe + pincell_base.add_cells([fuel_a, fuel_b, fuel_c, moderator_a, moderator_b, moderator_c]) + + # Create planes for azimuthal sectors + azimuthal_planes = [] + for i in range(8): + angle = 2 * i * openmc.pi / 8 + normal_vector = (-openmc.sin(angle), openmc.cos(angle), 0) + azimuthal_planes.append(openmc.Plane(a=normal_vector[0], b=normal_vector[1], c=normal_vector[2], d=0)) + + # Create a cell for each azimuthal sector + azimuthal_cells = [] + for i in range(8): + azimuthal_cell = openmc.Cell(name=f'azimuthal_cell_{i}') + azimuthal_cell.fill = pincell_base + azimuthal_cell.region = +azimuthal_planes[i] & -azimuthal_planes[(i+1) % 8] + azimuthal_cells.append(azimuthal_cell) + + # Create a geometry with the azimuthal universes + pincell = openmc.Universe(cells=azimuthal_cells) + + ######################################## + # Define a moderator lattice universe + + moderator_infinite = openmc.Cell(fill=water, name='moderator infinite') + mu = openmc.Universe() + mu.add_cells([moderator_infinite]) + + lattice = openmc.RectLattice() + lattice.lower_left = [-pitch/2.0, -pitch/2.0] + lattice.pitch = [pitch/10.0, pitch/10.0] + lattice.universes = np.full((10, 10), mu) + + mod_lattice_cell = openmc.Cell(fill=lattice) + + mod_lattice_uni = openmc.Universe() + + mod_lattice_uni.add_cells([mod_lattice_cell]) + + ######################################## + # Define 2x2 outer lattice + lattice2x2 = openmc.RectLattice() + lattice2x2.lower_left = (-pitch, -pitch) + lattice2x2.pitch = (pitch, pitch) + lattice2x2.universes = [ + [pincell, pincell], + [pincell, mod_lattice_uni] + ] + + ######################################## + # Define cell containing lattice and other stuff + box = openmc.model.RectangularPrism(pitch*2, pitch*2, boundary_type='reflective') + + assembly = openmc.Cell(fill=lattice2x2, region=-box, name='assembly') + + # Create a geometry with the top-level cell + geometry = openmc.Geometry([assembly]) + + ############################################################################### + # Define problem settings + + # Instantiate a Settings object, set all runtime parameters, and export to XML + settings = openmc.Settings() + settings.energy_mode = "multi-group" + settings.batches = 10 + settings.inactive = 5 + settings.particles = 100 + + # Create an initial uniform spatial source distribution over fissionable zones + lower_left = (-pitch, -pitch, -1) + upper_right = (pitch, pitch, 1) + uniform_dist = openmc.stats.Box(lower_left, upper_right) + rr_source = openmc.IndependentSource(space=uniform_dist) + + settings.random_ray['distance_active'] = 100.0 + settings.random_ray['distance_inactive'] = 20.0 + settings.random_ray['ray_source'] = rr_source + + ############################################################################### + # Define tallies + + # Create a mesh that will be used for tallying + mesh = openmc.RegularMesh() + mesh.dimension = (2, 2) + mesh.lower_left = (-pitch, -pitch) + mesh.upper_right = (pitch, pitch) + + # Create a mesh filter that can be used in a tally + mesh_filter = openmc.MeshFilter(mesh) + + # Create an energy group filter as well + energy_filter = openmc.EnergyFilter(group_edges) + + # Now use the mesh filter in a tally and indicate what scores are desired + tally = openmc.Tally(name="Mesh tally") + tally.filters = [mesh_filter, energy_filter] + tally.scores = ['flux', 'fission', 'nu-fission'] + tally.estimator = 'analog' + + # Instantiate a Tallies collection and export to XML + tallies = openmc.Tallies([tally]) + + ############################################################################### + # Exporting to OpenMC model + ############################################################################### + + model = openmc.Model() + model.geometry = geometry + model.materials = materials + model.settings = settings + model.tallies = tallies + return model + + +def test_random_ray_basic(): + harness = MGXSTestHarness('statepoint.10.h5', random_ray_model()) + harness.main() diff --git a/tests/regression_tests/random_ray_vacuum/__init__.py b/tests/regression_tests/random_ray_vacuum/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_vacuum/inputs_true.dat b/tests/regression_tests/random_ray_vacuum/inputs_true.dat new file mode 100644 index 000000000..4ef109420 --- /dev/null +++ b/tests/regression_tests/random_ray_vacuum/inputs_true.dat @@ -0,0 +1,108 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.126 0.126 + 10 10 + -0.63 -0.63 + +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 + + + 1.26 1.26 + 2 2 + -1.26 -1.26 + +2 2 +2 5 + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + multi-group + + 100.0 + 20.0 + + + -1.26 -1.26 -1 1.26 1.26 1 + + + + + + + 2 2 + -1.26 -1.26 + 1.26 1.26 + + + 1 + + + 1e-05 0.0635 10.0 100.0 1000.0 500000.0 1000000.0 20000000.0 + + + 1 2 + flux fission nu-fission + analog + + + diff --git a/tests/regression_tests/random_ray_vacuum/results_true.dat b/tests/regression_tests/random_ray_vacuum/results_true.dat new file mode 100644 index 000000000..744ce6cef --- /dev/null +++ b/tests/regression_tests/random_ray_vacuum/results_true.dat @@ -0,0 +1,171 @@ +k-combined: +1.010455E-01 1.585558E-02 +tally 1: +1.849176E-01 +7.634332E-03 +2.181815E-02 +1.062861E-04 +5.310100E-02 +6.295730E-04 +4.048251E-02 +3.851890E-04 +1.893676E-03 +8.448769E-07 +4.608828E-03 +5.004529E-06 +4.063643E-03 +4.022442E-06 +4.112970E-05 +4.186661E-10 +1.001015E-04 +2.479919E-09 +7.467029E-03 +1.178864E-05 +7.688748E-05 +1.266903E-09 +1.871288E-04 +7.504350E-09 +3.870644E-02 +3.010745E-04 +1.375240E-04 +3.807356E-09 +3.347099E-04 +2.255298E-08 +4.524967E-01 +4.098857E-02 +2.437418E-04 +1.190325E-08 +6.031220E-04 +7.288126E-08 +4.989226E-01 +4.993728E-02 +2.374296E-03 +1.135824E-06 +6.603983E-03 +8.787258E-06 +3.899991E-01 +3.308783E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +7.108982E-02 +1.144390E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.295259E-03 +6.352159E-06 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +9.852001E-03 +1.984406E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.414391E-02 +3.905201E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.571668E-01 +1.323140E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.752932E-01 +1.517930E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.465446E-01 +4.901884E-03 +1.700791E-02 +6.587674E-05 +4.139385E-02 +3.902131E-04 +3.424032E-02 +2.841338E-04 +1.598985E-03 +6.216312E-07 +3.891610E-03 +3.682159E-06 +4.067582E-03 +4.209468E-06 +4.152829E-05 +4.494649E-10 +1.010715E-04 +2.662352E-09 +7.526712E-03 +1.225032E-05 +7.769969E-05 +1.328443E-09 +1.891055E-04 +7.868877E-09 +4.008649E-02 +3.246821E-04 +1.417944E-04 +4.070719E-09 +3.451035E-04 +2.411301E-08 +4.859902E-01 +4.747592E-02 +2.606214E-04 +1.369749E-08 +6.448895E-04 +8.386705E-08 +5.475198E-01 +6.061269E-02 +2.625477E-03 +1.405458E-06 +7.302631E-03 +1.087327E-05 +1.909660E-01 +8.147906E-03 +2.269063E-02 +1.149570E-04 +5.522446E-02 +6.809342E-04 +4.196583E-02 +4.141620E-04 +1.980406E-03 +9.227119E-07 +4.819913E-03 +5.465576E-06 +4.247004E-03 +4.420116E-06 +4.341806E-05 +4.691518E-10 +1.056709E-04 +2.778965E-09 +7.742814E-03 +1.272112E-05 +8.039606E-05 +1.389209E-09 +1.956679E-04 +8.228817E-09 +3.982370E-02 +3.190931E-04 +1.427171E-04 +4.103942E-09 +3.473492E-04 +2.430981E-08 +4.849535E-01 +4.707014E-02 +2.678327E-04 +1.438540E-08 +6.627333E-04 +8.807897E-08 +5.493457E-01 +6.069440E-02 +2.717400E-03 +1.501450E-06 +7.558312E-03 +1.161591E-05 diff --git a/tests/regression_tests/random_ray_vacuum/test.py b/tests/regression_tests/random_ray_vacuum/test.py new file mode 100644 index 000000000..e9ca22521 --- /dev/null +++ b/tests/regression_tests/random_ray_vacuum/test.py @@ -0,0 +1,235 @@ +import os + +import numpy as np +import openmc + +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 random_ray_model() -> openmc.Model: + ############################################################################### + # Create multigroup data + + # Instantiate the energy group data + group_edges = [1e-5, 0.0635, 10.0, 1.0e2, 1.0e3, 0.5e6, 1.0e6, 20.0e6] + groups = openmc.mgxs.EnergyGroups(group_edges) + + # Instantiate the 7-group (C5G7) cross section data + uo2_xsdata = openmc.XSdata('UO2', groups) + uo2_xsdata.order = 0 + uo2_xsdata.set_total( + [0.1779492, 0.3298048, 0.4803882, 0.5543674, 0.3118013, 0.3951678, + 0.5644058]) + uo2_xsdata.set_absorption([8.0248e-03, 3.7174e-03, 2.6769e-02, 9.6236e-02, + 3.0020e-02, 1.1126e-01, 2.8278e-01]) + scatter_matrix = np.array( + [[[0.1275370, 0.0423780, 0.0000094, 0.0000000, 0.0000000, 0.0000000, 0.0000000], + [0.0000000, 0.3244560, 0.0016314, 0.0000000, 0.0000000, 0.0000000, 0.0000000], + [0.0000000, 0.0000000, 0.4509400, 0.0026792, 0.0000000, 0.0000000, 0.0000000], + [0.0000000, 0.0000000, 0.0000000, 0.4525650, 0.0055664, 0.0000000, 0.0000000], + [0.0000000, 0.0000000, 0.0000000, 0.0001253, 0.2714010, 0.0102550, 0.0000000], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0012968, 0.2658020, 0.0168090], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0085458, 0.2730800]]]) + scatter_matrix = np.rollaxis(scatter_matrix, 0, 3) + uo2_xsdata.set_scatter_matrix(scatter_matrix) + uo2_xsdata.set_fission([7.21206e-03, 8.19301e-04, 6.45320e-03, + 1.85648e-02, 1.78084e-02, 8.30348e-02, + 2.16004e-01]) + uo2_xsdata.set_nu_fission([2.005998e-02, 2.027303e-03, 1.570599e-02, + 4.518301e-02, 4.334208e-02, 2.020901e-01, + 5.257105e-01]) + uo2_xsdata.set_chi([5.8791e-01, 4.1176e-01, 3.3906e-04, 1.1761e-07, 0.0000e+00, + 0.0000e+00, 0.0000e+00]) + + h2o_xsdata = openmc.XSdata('LWTR', groups) + h2o_xsdata.order = 0 + h2o_xsdata.set_total([0.15920605, 0.412969593, 0.59030986, 0.58435, + 0.718, 1.2544497, 2.650379]) + h2o_xsdata.set_absorption([6.0105e-04, 1.5793e-05, 3.3716e-04, + 1.9406e-03, 5.7416e-03, 1.5001e-02, + 3.7239e-02]) + scatter_matrix = np.array( + [[[0.0444777, 0.1134000, 0.0007235, 0.0000037, 0.0000001, 0.0000000, 0.0000000], + [0.0000000, 0.2823340, 0.1299400, 0.0006234, 0.0000480, 0.0000074, 0.0000010], + [0.0000000, 0.0000000, 0.3452560, 0.2245700, 0.0169990, 0.0026443, 0.0005034], + [0.0000000, 0.0000000, 0.0000000, 0.0910284, 0.4155100, 0.0637320, 0.0121390], + [0.0000000, 0.0000000, 0.0000000, 0.0000714, 0.1391380, 0.5118200, 0.0612290], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0022157, 0.6999130, 0.5373200], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.1324400, 2.4807000]]]) + scatter_matrix = np.rollaxis(scatter_matrix, 0, 3) + h2o_xsdata.set_scatter_matrix(scatter_matrix) + + mg_cross_sections = openmc.MGXSLibrary(groups) + mg_cross_sections.add_xsdatas([uo2_xsdata, h2o_xsdata]) + mg_cross_sections.export_to_hdf5() + + ############################################################################### + # Create materials for the problem + + # Instantiate some Materials and register the appropriate Macroscopic objects + uo2 = openmc.Material(name='UO2 fuel') + uo2.set_density('macro', 1.0) + uo2.add_macroscopic('UO2') + + water = openmc.Material(name='Water') + water.set_density('macro', 1.0) + water.add_macroscopic('LWTR') + + # Instantiate a Materials collection and export to XML + materials = openmc.Materials([uo2, water]) + materials.cross_sections = "mgxs.h5" + + ############################################################################### + # Define problem geometry + + ######################################## + # Define an unbounded pincell universe + + pitch = 1.26 + + # Create a surface for the fuel outer radius + fuel_or = openmc.ZCylinder(r=0.54, name='Fuel OR') + inner_ring_a = openmc.ZCylinder(r=0.33, name='inner ring a') + inner_ring_b = openmc.ZCylinder(r=0.45, name='inner ring b') + outer_ring_a = openmc.ZCylinder(r=0.60, name='outer ring a') + outer_ring_b = openmc.ZCylinder(r=0.69, name='outer ring b') + + # Instantiate Cells + fuel_a = openmc.Cell(fill=uo2, region=-inner_ring_a, name='fuel inner a') + fuel_b = openmc.Cell(fill=uo2, region=+inner_ring_a & -inner_ring_b, name='fuel inner b') + fuel_c = openmc.Cell(fill=uo2, region=+inner_ring_b & -fuel_or, name='fuel inner c') + moderator_a = openmc.Cell(fill=water, region=+fuel_or & -outer_ring_a, name='moderator inner a') + moderator_b = openmc.Cell(fill=water, region=+outer_ring_a & -outer_ring_b, name='moderator outer b') + moderator_c = openmc.Cell(fill=water, region=+outer_ring_b, name='moderator outer c') + + # Create pincell universe + pincell_base = openmc.Universe() + + # Register Cells with Universe + pincell_base.add_cells([fuel_a, fuel_b, fuel_c, moderator_a, moderator_b, moderator_c]) + + # Create planes for azimuthal sectors + azimuthal_planes = [] + for i in range(8): + angle = 2 * i * openmc.pi / 8 + normal_vector = (-openmc.sin(angle), openmc.cos(angle), 0) + azimuthal_planes.append(openmc.Plane(a=normal_vector[0], b=normal_vector[1], c=normal_vector[2], d=0)) + + # Create a cell for each azimuthal sector + azimuthal_cells = [] + for i in range(8): + azimuthal_cell = openmc.Cell(name=f'azimuthal_cell_{i}') + azimuthal_cell.fill = pincell_base + azimuthal_cell.region = +azimuthal_planes[i] & -azimuthal_planes[(i+1) % 8] + azimuthal_cells.append(azimuthal_cell) + + # Create a geometry with the azimuthal universes + pincell = openmc.Universe(cells=azimuthal_cells) + + ######################################## + # Define a moderator lattice universe + + moderator_infinite = openmc.Cell(fill=water, name='moderator infinite') + mu = openmc.Universe() + mu.add_cells([moderator_infinite]) + + lattice = openmc.RectLattice() + lattice.lower_left = [-pitch/2.0, -pitch/2.0] + lattice.pitch = [pitch/10.0, pitch/10.0] + lattice.universes = np.full((10, 10), mu) + + mod_lattice_cell = openmc.Cell(fill=lattice) + + mod_lattice_uni = openmc.Universe() + + mod_lattice_uni.add_cells([mod_lattice_cell]) + + ######################################## + # Define 2x2 outer lattice + lattice2x2 = openmc.RectLattice() + lattice2x2.lower_left = [-pitch, -pitch] + lattice2x2.pitch = [pitch, pitch] + lattice2x2.universes = [ + [pincell, pincell], + [pincell, mod_lattice_uni] + ] + + ######################################## + # Define cell containing lattice and other stuff + box = openmc.model.RectangularPrism(pitch*2, pitch*2, boundary_type='vacuum') + + assembly = openmc.Cell(fill=lattice2x2, region=-box, name='assembly') + + root = openmc.Universe(name='root universe') + root.add_cell(assembly) + + # Create a geometry with the two cells and export to XML + geometry = openmc.Geometry(root) + + ############################################################################### + # Define problem settings + + # Instantiate a Settings object, set all runtime parameters, and export to XML + settings = openmc.Settings() + settings.energy_mode = "multi-group" + settings.batches = 10 + settings.inactive = 5 + settings.particles = 100 + + # Create an initial uniform spatial source distribution over fissionable zones + lower_left = (-pitch, -pitch, -1) + upper_right = (pitch, pitch, 1) + uniform_dist = openmc.stats.Box(lower_left, upper_right) + rr_source = openmc.IndependentSource(space=uniform_dist) + + settings.random_ray['distance_active'] = 100.0 + settings.random_ray['distance_inactive'] = 20.0 + settings.random_ray['ray_source'] = rr_source + + ############################################################################### + # Define tallies + + # Create a mesh that will be used for tallying + mesh = openmc.RegularMesh() + mesh.dimension = (2, 2) + mesh.lower_left = (-pitch, -pitch) + mesh.upper_right = (pitch, pitch) + + # Create a mesh filter that can be used in a tally + mesh_filter = openmc.MeshFilter(mesh) + + # Create an energy group filter as well + energy_filter = openmc.EnergyFilter(group_edges) + + # Now use the mesh filter in a tally and indicate what scores are desired + tally = openmc.Tally(name="Mesh tally") + tally.filters = [mesh_filter, energy_filter] + tally.scores = ['flux', 'fission', 'nu-fission'] + tally.estimator = 'analog' + + # Instantiate a Tallies collection and export to XML + tallies = openmc.Tallies([tally]) + + ############################################################################### + # Exporting to OpenMC model + ############################################################################### + + model = openmc.Model() + model.geometry = geometry + model.materials = materials + model.settings = settings + model.tallies = tallies + return model + + +def test_random_ray_vacuum(): + harness = MGXSTestHarness('statepoint.10.h5', random_ray_model()) + harness.main() diff --git a/tests/testing_harness.py b/tests/testing_harness.py index 40c9d37d8..81527452b 100644 --- a/tests/testing_harness.py +++ b/tests/testing_harness.py @@ -384,6 +384,63 @@ class HashedPyAPITestHarness(PyAPITestHarness): return super()._get_results(True) +class TolerantPyAPITestHarness(PyAPITestHarness): + """Specialized harness for running tests that involve significant levels + of floating point non-associativity when using shared memory parallelism + due to single precision usage (e.g., as in the random ray solver). + + """ + def _are_files_equal(self, actual_path, expected_path, tolerance): + def isfloat(value): + try: + float(value) + return True + except ValueError: + return False + + def tokenize(line): + return line.strip().split() + + def compare_tokens(token1, token2): + if isfloat(token1) and isfloat(token2): + float1, float2 = float(token1), float(token2) + return abs(float1 - float2) <= tolerance * max(abs(float1), abs(float2)) + else: + return token1 == token2 + + expected = open(expected_path).readlines() + actual = open(actual_path).readlines() + + if len(expected) != len(actual): + return False + + for line1, line2 in zip(expected, actual): + tokens1 = tokenize(line1) + tokens2 = tokenize(line2) + + if len(tokens1) != len(tokens2): + return False + + for token1, token2 in zip(tokens1, tokens2): + if not compare_tokens(token1, token2): + return False + + return True + + def _compare_results(self): + """Make sure the current results agree with the reference.""" + compare = self._are_files_equal('results_test.dat', 'results_true.dat', 1e-6) + if not compare: + expected = open('results_true.dat').readlines() + actual = open('results_test.dat').readlines() + diff = unified_diff(expected, actual, 'results_true.dat', + 'results_test.dat') + print('Result differences:') + print(''.join(colorize(diff))) + os.rename('results_test.dat', 'results_error.dat') + assert compare, 'Results do not agree' + + class PlotTestHarness(TestHarness): """Specialized TestHarness for running OpenMC plotting tests.""" def __init__(self, plot_names, voxel_convert_checks=[]): diff --git a/tests/unit_tests/test_settings.py b/tests/unit_tests/test_settings.py index 30930ecdf..650bfd186 100644 --- a/tests/unit_tests/test_settings.py +++ b/tests/unit_tests/test_settings.py @@ -58,6 +58,13 @@ def test_export_to_xml(run_in_tmpdir): s.electron_treatment = 'led' s.write_initial_source = True s.weight_window_checkpoints = {'surface': True, 'collision': False} + s.random_ray = { + 'distance_inactive': 10.0, + 'distance_active': 100.0, + 'ray_source': openmc.IndependentSource( + space=openmc.stats.Box((-1., -1., -1.), (1., 1., 1.)) + ) + } s.max_particle_events = 100 @@ -131,3 +138,7 @@ def test_export_to_xml(run_in_tmpdir): assert vol.upper_right == (10., 10., 10.) assert s.weight_window_checkpoints == {'surface': True, 'collision': False} assert s.max_particle_events == 100 + assert s.random_ray['distance_inactive'] == 10.0 + assert s.random_ray['distance_active'] == 100.0 + assert s.random_ray['ray_source'].space.lower_left == [-1., -1., -1.] + assert s.random_ray['ray_source'].space.upper_right == [1., 1., 1.] From b53b601edc325a0458bb7348a078d4014146de32 Mon Sep 17 00:00:00 2001 From: Gavin Ridley Date: Sat, 20 Apr 2024 00:44:40 -0500 Subject: [PATCH 086/671] Tiny updates from experience building on Mac (#2894) Co-authored-by: Paul Romano --- CMakeLists.txt | 21 ++++--- cmake/OpenMCConfig.cmake.in | 4 ++ docs/source/quickinstall.rst | 63 +++++++++++++------ docs/source/usersguide/install.rst | 8 +-- examples/custom_source/CMakeLists.txt | 2 +- .../CMakeLists.txt | 2 +- openmc/data/multipole.py | 9 +-- openmc/mgxs/mdgxs.py | 2 +- src/nuclide.cpp | 27 +++++--- src/tallies/filter_energy.cpp | 2 +- tests/regression_tests/cpp_driver/test.py | 2 +- tests/regression_tests/dagmc/external/test.py | 2 +- tests/regression_tests/external_moab/test.py | 2 +- tests/regression_tests/source_dlopen/test.py | 2 +- .../source_parameterized_dlopen/test.py | 2 +- 15 files changed, 95 insertions(+), 55 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3f87cbd7e..49e987f22 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10 FATAL_ERROR) -project(openmc C CXX) +project(openmc CXX) # Set version numbers set(OPENMC_VERSION_MAJOR 0) @@ -80,6 +80,14 @@ if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Choose the type of build" FORCE) endif() +#=============================================================================== +# OpenMP for shared-memory parallelism (and GPU support some day!) +#=============================================================================== + +if(OPENMC_USE_OPENMP) + find_package(OpenMP REQUIRED) +endif() + #=============================================================================== # MPI for distributed-memory parallelism #=============================================================================== @@ -192,13 +200,6 @@ endif() # Skip for Visual Studio which has its own configurations through GUI if(NOT MSVC) -if(OPENMC_USE_OPENMP) - find_package(OpenMP REQUIRED) - # In CMake 3.9+, can use the OpenMP::OpenMP_CXX imported target - list(APPEND cxxflags ${OpenMP_CXX_FLAGS}) - list(APPEND ldflags ${OpenMP_CXX_FLAGS}) -endif() - set(CMAKE_POSITION_INDEPENDENT_CODE ON) if(OPENMC_ENABLE_PROFILE) @@ -522,6 +523,10 @@ if (PNG_FOUND) target_link_libraries(libopenmc PNG::PNG) endif() +if (OPENMC_USE_OPENMP) + target_link_libraries(libopenmc OpenMP::OpenMP_CXX) +endif() + if (OPENMC_USE_MPI) target_link_libraries(libopenmc MPI::MPI_CXX) endif() diff --git a/cmake/OpenMCConfig.cmake.in b/cmake/OpenMCConfig.cmake.in index 756fe26dc..1305ad3ed 100644 --- a/cmake/OpenMCConfig.cmake.in +++ b/cmake/OpenMCConfig.cmake.in @@ -31,6 +31,10 @@ if(@OPENMC_USE_MPI@) find_package(MPI REQUIRED) endif() +if(@OPENMC_USE_OPENMP@) + find_package(OpenMP REQUIRED) +endif() + if(@OPENMC_USE_MCPL@) find_package(MCPL REQUIRED) endif() diff --git a/docs/source/quickinstall.rst b/docs/source/quickinstall.rst index b25b02fb8..7f222f77c 100644 --- a/docs/source/quickinstall.rst +++ b/docs/source/quickinstall.rst @@ -107,31 +107,54 @@ can be used to access the installed packages. .. _Spack: https://spack.readthedocs.io/en/latest/ .. _setup guide: https://spack.readthedocs.io/en/latest/getting_started.html --------------------------------- -Installing from Source on Ubuntu --------------------------------- +------------------------------- +Manually Installing from Source +------------------------------- -To build OpenMC from source, several :ref:`prerequisites ` are -needed. If you are using Ubuntu or higher, all prerequisites can be installed -directly from the package manager: +Obtaining prerequisites on Ubuntu +--------------------------------- + +When building OpenMC from source, all :ref:`prerequisites ` can +be installed using the package manager: .. code-block:: sh sudo apt install g++ cmake libhdf5-dev libpng-dev -After the packages have been installed, follow the instructions below for -building and installing OpenMC from source. +After the packages have been installed, follow the instructions to build from +source below. -------------------------------------------- -Installing from Source on Linux or Mac OS X -------------------------------------------- +Obtaining prerequisites on macOS +-------------------------------- + +For an OpenMC build with multithreading enabled, a package manager like +`Homebrew `_ should first be installed. Then, the following +packages should be installed, for example in Homebrew via: + +.. code-block:: sh + + brew install llvm cmake xtensor 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 +by OpenMC. Consequently, the C++ compiler should explicitly be set before +proceeding: + +.. code-block:: sh + + export CXX=/opt/homebrew/opt/llvm/bin/clang++ + +After the packages have been installed, follow the instructions to build from +source below. + +Building Source on Linux or macOS +--------------------------------- All OpenMC source code is hosted on `GitHub `_. If you have `git -`_, the `gcc `_ compiler suite, -`CMake `_, and `HDF5 -`_ installed, you can download and -install OpenMC be entering the following commands in a terminal: +`_, a modern C++ compiler, `CMake `_, +and `HDF5 `_ installed, you can +download and install OpenMC by entering the following commands in a terminal: .. code-block:: sh @@ -151,14 +174,14 @@ should specify an installation directory where you have write access, e.g. cmake -DCMAKE_INSTALL_PREFIX=$HOME/.local .. The :mod:`openmc` Python package must be installed separately. The easiest way -to install it is using `pip `_, which is -included by default in Python 3.4+. From the root directory of the OpenMC -distribution/repository, run: +to install it is using `pip `_. +From the root directory of the OpenMC repository, run: .. code-block:: sh python -m pip install . -If you want to build a parallel version of OpenMC (using OpenMP or MPI), -directions can be found in the :ref:`detailed installation instructions +By default, OpenMC will be built with multithreading support. To build +distributed-memory parallel versions of OpenMC using MPI or to configure other +options, directions can be found in the :ref:`detailed installation instructions `. diff --git a/docs/source/usersguide/install.rst b/docs/source/usersguide/install.rst index 25329a77f..130e96c0a 100644 --- a/docs/source/usersguide/install.rst +++ b/docs/source/usersguide/install.rst @@ -464,11 +464,11 @@ can typically be set for a single command, i.e. .. _compile_linux: -Compiling on Linux and Mac OS X -------------------------------- +Compiling on Linux and macOS +---------------------------- -To compile OpenMC on Linux or Max OS X, run the following commands from within -the root directory of the source code: +To compile OpenMC on Linux or macOS, run the following commands from within the +root directory of the source code: .. code-block:: sh diff --git a/examples/custom_source/CMakeLists.txt b/examples/custom_source/CMakeLists.txt index 949817694..21463ed51 100644 --- a/examples/custom_source/CMakeLists.txt +++ b/examples/custom_source/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.3 FATAL_ERROR) +cmake_minimum_required(VERSION 3.10 FATAL_ERROR) project(openmc_sources CXX) add_library(source SHARED source_ring.cpp) find_package(OpenMC REQUIRED) diff --git a/examples/parameterized_custom_source/CMakeLists.txt b/examples/parameterized_custom_source/CMakeLists.txt index 3024e90cf..8232f3b54 100644 --- a/examples/parameterized_custom_source/CMakeLists.txt +++ b/examples/parameterized_custom_source/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.3 FATAL_ERROR) +cmake_minimum_required(VERSION 3.10 FATAL_ERROR) project(openmc_sources CXX) add_library(parameterized_source SHARED parameterized_source_ring.cpp) find_package(OpenMC REQUIRED) diff --git a/openmc/data/multipole.py b/openmc/data/multipole.py index d45d2beb6..5c9df39c2 100644 --- a/openmc/data/multipole.py +++ b/openmc/data/multipole.py @@ -1169,10 +1169,11 @@ class WindowedMultipole(EqualityMixin): sqrtE = sqrt(E) invE = 1.0 / E - # Locate us. The i_window calc omits a + 1 present in F90 because of - # the 1-based vs. 0-based indexing. Similarly startw needs to be - # decreased by 1. endw does not need to be decreased because - # range(startw, endw) does not include endw. + # Locate us. The i_window calc omits a + 1 present from the legacy + # Fortran version of OpenMC because of the 1-based vs. 0-based + # indexing. Similarly startw needs to be decreased by 1. endw does + # not need to be decreased because range(startw, endw) does not include + # endw. i_window = min(self.n_windows - 1, int(np.floor((sqrtE - sqrt(self.E_min)) / self.spacing))) startw = self.windows[i_window, 0] - 1 diff --git a/openmc/mgxs/mdgxs.py b/openmc/mgxs/mdgxs.py index 58f6a2d2f..45c559a22 100644 --- a/openmc/mgxs/mdgxs.py +++ b/openmc/mgxs/mdgxs.py @@ -20,7 +20,7 @@ MDGXS_TYPES = ( 'delayed-nu-fission matrix' ) -# Maximum number of delayed groups, from src/constants.F90 +# Maximum number of delayed groups, from include/openmc/constants.h MAX_DELAYED_GROUPS = 8 diff --git a/src/nuclide.cpp b/src/nuclide.cpp index 14d21bb09..91adc0777 100644 --- a/src/nuclide.cpp +++ b/src/nuclide.cpp @@ -634,16 +634,23 @@ void Nuclide::calculate_xs( } } - // Ensure these values are set - // Note, the only time either is used is in one of 4 places: - // 1. physics.cpp - scatter - For inelastic scatter. - // 2. physics.cpp - sample_fission - For partial fissions. - // 3. tally.F90 - score_general - For tallying on MTxxx reactions. - // 4. nuclide.cpp - calculate_urr_xs - For unresolved purposes. - // It is worth noting that none of these occur in the resolved - // resonance range, so the value here does not matter. index_temp is - // set to -1 to force a segfault in case a developer messes up and tries - // to use it with multipole. + /* + * index_temp, index_grid, and interp_factor are used only in the + * following places: + * 1. physics.cpp - scatter - For inelastic scatter. + * 2. physics.cpp - sample_fission - For partial fissions. + * 3. tallies/tally_scoring.cpp - score_general - + * For tallying on MTxxx reactions. + * 4. nuclide.cpp - calculate_urr_xs - For unresolved purposes. + * It is worth noting that none of these occur in the resolved resonance + * range, so the value here does not matter. index_temp is set to -1 to + * force a segfault in case a developer messes up and tries to use it with + * multipole. + * + * However, a segfault is not necessarily guaranteed with an out-of-bounds + * access, so this technique should be replaced by something more robust + * in the future. + */ micro.index_temp = -1; micro.index_grid = -1; micro.interp_factor = 0.0; diff --git a/src/tallies/filter_energy.cpp b/src/tallies/filter_energy.cpp index 825dd2ee5..4767dd175 100644 --- a/src/tallies/filter_energy.cpp +++ b/src/tallies/filter_energy.cpp @@ -3,7 +3,7 @@ #include #include "openmc/capi.h" -#include "openmc/constants.h" // For F90_NONE +#include "openmc/constants.h" // For C_NONE #include "openmc/mgxs_interface.h" #include "openmc/search.h" #include "openmc/settings.h" diff --git a/tests/regression_tests/cpp_driver/test.py b/tests/regression_tests/cpp_driver/test.py index 79c30967c..b80e82ee0 100644 --- a/tests/regression_tests/cpp_driver/test.py +++ b/tests/regression_tests/cpp_driver/test.py @@ -20,7 +20,7 @@ def cpp_driver(request): openmc_dir = Path(str(request.config.rootdir)) / 'build' with open('CMakeLists.txt', 'w') as f: f.write(textwrap.dedent(""" - cmake_minimum_required(VERSION 3.3 FATAL_ERROR) + cmake_minimum_required(VERSION 3.10 FATAL_ERROR) project(openmc_cpp_driver CXX) add_executable(cpp_driver driver.cpp) find_package(OpenMC REQUIRED HINTS {}) diff --git a/tests/regression_tests/dagmc/external/test.py b/tests/regression_tests/dagmc/external/test.py index 7993772e8..57bc9ea7f 100644 --- a/tests/regression_tests/dagmc/external/test.py +++ b/tests/regression_tests/dagmc/external/test.py @@ -25,7 +25,7 @@ def cpp_driver(request): openmc_dir = Path(str(request.config.rootdir)) / 'build' with open('CMakeLists.txt', 'w') as f: f.write(textwrap.dedent(""" - cmake_minimum_required(VERSION 3.3 FATAL_ERROR) + cmake_minimum_required(VERSION 3.10 FATAL_ERROR) project(openmc_cpp_driver CXX) add_executable(main main.cpp) find_package(OpenMC REQUIRED HINTS {}) diff --git a/tests/regression_tests/external_moab/test.py b/tests/regression_tests/external_moab/test.py index 90bff69ed..ce4e78a2c 100644 --- a/tests/regression_tests/external_moab/test.py +++ b/tests/regression_tests/external_moab/test.py @@ -32,7 +32,7 @@ def cpp_driver(request): openmc_dir = Path(str(request.config.rootdir)) / 'build' with open('CMakeLists.txt', 'w') as f: f.write(textwrap.dedent(""" - cmake_minimum_required(VERSION 3.3 FATAL_ERROR) + cmake_minimum_required(VERSION 3.10 FATAL_ERROR) project(openmc_cpp_driver CXX) add_executable(main main.cpp) find_package(OpenMC REQUIRED HINTS {}) diff --git a/tests/regression_tests/source_dlopen/test.py b/tests/regression_tests/source_dlopen/test.py index efe942933..88ff9dd85 100644 --- a/tests/regression_tests/source_dlopen/test.py +++ b/tests/regression_tests/source_dlopen/test.py @@ -18,7 +18,7 @@ def compile_source(request): openmc_dir = Path(str(request.config.rootdir)) / 'build' with open('CMakeLists.txt', 'w') as f: f.write(textwrap.dedent(""" - cmake_minimum_required(VERSION 3.3 FATAL_ERROR) + cmake_minimum_required(VERSION 3.10 FATAL_ERROR) project(openmc_sources CXX) add_library(source SHARED source_sampling.cpp) find_package(OpenMC REQUIRED HINTS {}) diff --git a/tests/regression_tests/source_parameterized_dlopen/test.py b/tests/regression_tests/source_parameterized_dlopen/test.py index c7c5d06b1..1cc253528 100644 --- a/tests/regression_tests/source_parameterized_dlopen/test.py +++ b/tests/regression_tests/source_parameterized_dlopen/test.py @@ -18,7 +18,7 @@ def compile_source(request): openmc_dir = Path(str(request.config.rootdir)) / 'build' with open('CMakeLists.txt', 'w') as f: f.write(textwrap.dedent(""" - cmake_minimum_required(VERSION 3.3 FATAL_ERROR) + cmake_minimum_required(VERSION 3.10 FATAL_ERROR) project(openmc_sources CXX) add_library(source SHARED parameterized_source_sampling.cpp) find_package(OpenMC REQUIRED HINTS {}) From cddb3be13944bad6a5e6762e9f2af3f689af14ad Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Sat, 20 Apr 2024 10:45:16 -0500 Subject: [PATCH 087/671] Add C to list of cmake project languages (#2969) --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 49e987f22..9b08914ac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10 FATAL_ERROR) -project(openmc CXX) +project(openmc C CXX) # Set version numbers set(OPENMC_VERSION_MAJOR 0) From b54b1e975c6696e14f274fd2fa5950d54d883882 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 24 Apr 2024 08:02:05 -0500 Subject: [PATCH 088/671] Update bounding_box docstrings (#2972) --- openmc/cell.py | 3 +-- openmc/region.py | 11 ++++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/openmc/cell.py b/openmc/cell.py index 6de8eadec..94fac8413 100644 --- a/openmc/cell.py +++ b/openmc/cell.py @@ -343,8 +343,7 @@ class Cell(IDManagerMixin): if self.region is not None: return self.region.bounding_box else: - return BoundingBox(np.array([-np.inf, -np.inf, -np.inf]), - np.array([np.inf, np.inf, np.inf])) + return BoundingBox.infinite() @property def num_instances(self): diff --git a/openmc/region.py b/openmc/region.py index d3c03b898..f679129c1 100644 --- a/openmc/region.py +++ b/openmc/region.py @@ -18,6 +18,11 @@ class Region(ABC): respective classes are typically not instantiated directly but rather are created through operators of the Surface and Region classes. + Attributes + ---------- + bounding_box : openmc.BoundingBox + Axis-aligned bounding box of the region + """ def __and__(self, other): return Intersection((self, other)) @@ -415,7 +420,7 @@ class Intersection(Region, MutableSequence): Attributes ---------- bounding_box : openmc.BoundingBox - Lower-left and upper-right coordinates of an axis-aligned bounding box + Axis-aligned bounding box of the region """ @@ -503,7 +508,7 @@ class Union(Region, MutableSequence): Attributes ---------- bounding_box : openmc.BoundingBox - Lower-left and upper-right coordinates of an axis-aligned bounding box + Axis-aligned bounding box of the region """ @@ -594,7 +599,7 @@ class Complement(Region): node : openmc.Region Regions to take the complement of bounding_box : openmc.BoundingBox - Lower-left and upper-right coordinates of an axis-aligned bounding box + Axis-aligned bounding box of the region """ From 6b08f75065840ab7e3ab14347c7f447451b8dc60 Mon Sep 17 00:00:00 2001 From: Luke Labrie-Cleary Date: Wed, 24 Apr 2024 12:05:11 -0400 Subject: [PATCH 089/671] make uwuw optional (#2965) --- CMakeLists.txt | 17 ++++-- cmake/OpenMCConfig.cmake.in | 4 ++ include/openmc/dagmc.h | 9 +++- openmc/lib/__init__.py | 3 ++ src/dagmc.cpp | 66 ++++++++++++++++------- src/output.cpp | 5 ++ tests/regression_tests/dagmc/refl/test.py | 4 +- tests/regression_tests/dagmc/uwuw/test.py | 4 +- 8 files changed, 86 insertions(+), 26 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9b08914ac..1841251ff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,6 +39,7 @@ option(OPENMC_USE_LIBMESH "Enable support for libMesh unstructured mesh tall option(OPENMC_USE_MPI "Enable MPI" OFF) option(OPENMC_USE_MCPL "Enable MCPL" OFF) option(OPENMC_USE_NCRYSTAL "Enable support for NCrystal scattering" OFF) +option(OPENMC_USE_UWUW "Enable UWUW" OFF) # Warnings for deprecated options foreach(OLD_OPT IN ITEMS "openmp" "profile" "coverage" "dagmc" "libmesh") @@ -125,10 +126,15 @@ endif() if(OPENMC_USE_DAGMC) find_package(DAGMC REQUIRED PATH_SUFFIXES lib/cmake) if (${DAGMC_VERSION} VERSION_LESS 3.2.0) - message(FATAL_ERROR "Discovered DAGMC Version: ${DAGMC_VERSION}. \ - Please update DAGMC to version 3.2.0 or greater.") + message(FATAL_ERROR "Discovered DAGMC Version: ${DAGMC_VERSION}." + "Please update DAGMC to version 3.2.0 or greater.") endif() message(STATUS "Found DAGMC: ${DAGMC_DIR} (version ${DAGMC_VERSION})") + + # Check if UWUW is needed and available + if(OPENMC_USE_UWUW AND NOT DAGMC_BUILD_UWUW) + message(FATAL_ERROR "UWUW is enabled but DAGMC was not configured with UWUW.") + endif() endif() #=============================================================================== @@ -510,7 +516,7 @@ endif() if(OPENMC_USE_DAGMC) target_compile_definitions(libopenmc PRIVATE DAGMC) - target_link_libraries(libopenmc dagmc-shared uwuw-shared) + target_link_libraries(libopenmc dagmc-shared) endif() if(OPENMC_USE_LIBMESH) @@ -547,6 +553,11 @@ if(OPENMC_USE_NCRYSTAL) target_link_libraries(libopenmc NCrystal::NCrystal) endif() +if (OPENMC_USE_UWUW) + target_compile_definitions(libopenmc PRIVATE UWUW) + target_link_libraries(libopenmc uwuw-shared) +endif() + #=============================================================================== # Log build info that this executable can report later #=============================================================================== diff --git a/cmake/OpenMCConfig.cmake.in b/cmake/OpenMCConfig.cmake.in index 1305ad3ed..44a5e0d5a 100644 --- a/cmake/OpenMCConfig.cmake.in +++ b/cmake/OpenMCConfig.cmake.in @@ -38,3 +38,7 @@ endif() if(@OPENMC_USE_MCPL@) find_package(MCPL REQUIRED) endif() + +if(@OPENMC_USE_UWUW@) + find_package(UWUW REQUIRED) +endif() diff --git a/include/openmc/dagmc.h b/include/openmc/dagmc.h index 0b23e567a..2facf4fc0 100644 --- a/include/openmc/dagmc.h +++ b/include/openmc/dagmc.h @@ -3,7 +3,8 @@ namespace openmc { extern "C" const bool DAGMC_ENABLED; -} +extern "C" const bool UWUW_ENABLED; +} // namespace openmc // always include the XML interface header #include "openmc/xml_interface.h" @@ -120,6 +121,12 @@ public: void write_uwuw_materials_xml( const std::string& outfile = "uwuw_materials.xml") const; + //! Assign a material to a cell from uwuw material library + //! \param[in] vol_handle The DAGMC material assignment string + //! \param[in] c The OpenMC cell to which the material is assigned + void uwuw_assign_material( + moab::EntityHandle vol_handle, std::unique_ptr& c) const; + //! Assign a material to a cell based //! \param[in] mat_string The DAGMC material assignment string //! \param[in] c The OpenMC cell to which the material is assigned diff --git a/openmc/lib/__init__.py b/openmc/lib/__init__.py index 0e5ad92fe..dc5bd7d9b 100644 --- a/openmc/lib/__init__.py +++ b/openmc/lib/__init__.py @@ -54,6 +54,9 @@ def _libmesh_enabled(): def _mcpl_enabled(): return c_bool.in_dll(_dll, "MCPL_ENABLED").value +def _uwuw_enabled(): + return c_bool.in_dll(_dll, "UWUW_ENABLED").value + from .error import * from .core import * diff --git a/src/dagmc.cpp b/src/dagmc.cpp index 6a4865c39..2f2502f6e 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -11,7 +11,7 @@ #include "openmc/settings.h" #include "openmc/string_utils.h" -#ifdef DAGMC +#ifdef UWUW #include "uwuw.hpp" #endif #include @@ -29,6 +29,12 @@ const bool DAGMC_ENABLED = true; const bool DAGMC_ENABLED = false; #endif +#ifdef UWUW +const bool UWUW_ENABLED = true; +#else +const bool UWUW_ENABLED = false; +#endif + } // namespace openmc #ifdef DAGMC @@ -85,7 +91,6 @@ DAGUniverse::DAGUniverse(std::shared_ptr dagmc_ptr, { set_id(); init_metadata(); - read_uwuw_materials(); init_geometry(); } @@ -111,8 +116,6 @@ void DAGUniverse::initialize() init_metadata(); - read_uwuw_materials(); - init_geometry(); } @@ -209,20 +212,7 @@ void DAGUniverse::init_geometry() c->material_.push_back(MATERIAL_VOID); } else { if (uses_uwuw()) { - // lookup material in uwuw if present - std::string uwuw_mat = - dmd_ptr->volume_material_property_data_eh[vol_handle]; - if (uwuw_->material_library.count(uwuw_mat) != 0) { - // Note: material numbers are set by UWUW - int mat_number = uwuw_->material_library.get_material(uwuw_mat) - .metadata["mat_number"] - .asInt(); - c->material_.push_back(mat_number); - } else { - fatal_error(fmt::format("Material with value '{}' not found in the " - "UWUW material library", - mat_str)); - } + uwuw_assign_material(vol_handle, c); } else { legacy_assign_material(mat_str, c); } @@ -439,11 +429,16 @@ void DAGUniverse::to_hdf5(hid_t universes_group) const bool DAGUniverse::uses_uwuw() const { +#ifdef UWUW return uwuw_ && !uwuw_->material_library.empty(); +#else + return false; +#endif // UWUW } std::string DAGUniverse::get_uwuw_materials_xml() const { +#ifdef UWUW if (!uses_uwuw()) { throw std::runtime_error("This DAGMC Universe does not use UWUW materials"); } @@ -461,10 +456,14 @@ std::string DAGUniverse::get_uwuw_materials_xml() const ss << ""; return ss.str(); +#else + fatal_error("DAGMC was not configured with UWUW."); +#endif // UWUW } void DAGUniverse::write_uwuw_materials_xml(const std::string& outfile) const { +#ifdef UWUW if (!uses_uwuw()) { throw std::runtime_error( "This DAGMC universe does not use UWUW materials."); @@ -475,6 +474,9 @@ void DAGUniverse::write_uwuw_materials_xml(const std::string& outfile) const std::ofstream mats_xml(outfile); mats_xml << xml_str; mats_xml.close(); +#else + fatal_error("DAGMC was not configured with UWUW."); +#endif } void DAGUniverse::legacy_assign_material( @@ -536,6 +538,7 @@ void DAGUniverse::legacy_assign_material( void DAGUniverse::read_uwuw_materials() { +#ifdef UWUW // If no filename was provided, don't read UWUW materials if (filename_ == "") return; @@ -573,8 +576,35 @@ void DAGUniverse::read_uwuw_materials() for (pugi::xml_node material_node : root.children("material")) { model::materials.push_back(std::make_unique(material_node)); } +#else + fatal_error("DAGMC was not configured with UWUW."); +#endif } +void DAGUniverse::uwuw_assign_material( + moab::EntityHandle vol_handle, std::unique_ptr& c) const +{ +#ifdef UWUW + // read materials from uwuw material file + read_uwuw_materials(); + + // lookup material in uwuw if present + std::string uwuw_mat = dmd_ptr->volume_material_property_data_eh[vol_handle]; + if (uwuw_->material_library.count(uwuw_mat) != 0) { + // Note: material numbers are set by UWUW + int mat_number = uwuw_->material_library.get_material(uwuw_mat) + .metadata["mat_number"] + .asInt(); + c->material_.push_back(mat_number); + } else { + fatal_error(fmt::format("Material with value '{}' not found in the " + "UWUW material library", + mat_str)); + } +#else + fatal_error("DAGMC was not configured with UWUW."); +#endif +} //============================================================================== // DAGMC Cell implementation //============================================================================== diff --git a/src/output.cpp b/src/output.cpp index b1b963f7d..5fdbea130 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -318,6 +318,7 @@ void print_build_info() std::string coverage(n); std::string mcpl(n); std::string ncrystal(n); + std::string uwuw(n); #ifdef PHDF5 phdf5 = y; @@ -346,6 +347,9 @@ void print_build_info() #ifdef COVERAGEBUILD coverage = y; #endif +#ifdef UWUW + uwuw = y; +#endif // Wraps macro variables in quotes #define STRINGIFY(x) STRINGIFY2(x) @@ -364,6 +368,7 @@ void print_build_info() fmt::print("NCrystal support: {}\n", ncrystal); fmt::print("Coverage testing: {}\n", coverage); fmt::print("Profiling flags: {}\n", profiling); + fmt::print("UWUW support: {}\n", uwuw); } } diff --git a/tests/regression_tests/dagmc/refl/test.py b/tests/regression_tests/dagmc/refl/test.py index 03c1c407b..a13acc025 100644 --- a/tests/regression_tests/dagmc/refl/test.py +++ b/tests/regression_tests/dagmc/refl/test.py @@ -6,8 +6,8 @@ import pytest from tests.testing_harness import PyAPITestHarness pytestmark = pytest.mark.skipif( - not openmc.lib._dagmc_enabled(), - reason="DAGMC CAD geometry is not enabled.") + not openmc.lib._uwuw_enabled(), + reason="UWUW is not enabled.") class UWUWTest(PyAPITestHarness): def __init__(self, *args, **kwargs): diff --git a/tests/regression_tests/dagmc/uwuw/test.py b/tests/regression_tests/dagmc/uwuw/test.py index 38c335a5e..bea464cfa 100644 --- a/tests/regression_tests/dagmc/uwuw/test.py +++ b/tests/regression_tests/dagmc/uwuw/test.py @@ -6,8 +6,8 @@ import pytest from tests.testing_harness import PyAPITestHarness pytestmark = pytest.mark.skipif( - not openmc.lib._dagmc_enabled(), - reason="DAGMC CAD geometry is not enabled.") + not openmc.lib._uwuw_enabled(), + reason="UWUW is not enabled.") class UWUWTest(PyAPITestHarness): def __init__(self, *args, **kwargs): From 1d56adbc3deb83145cab5539723f3c92bd30ce17 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Wed, 24 Apr 2024 19:07:36 +0100 Subject: [PATCH 090/671] added damage-energy as optional reaction for micro (#2903) --- openmc/deplete/microxs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index c1c4cb7ac..d80c52baa 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -24,6 +24,7 @@ import openmc.lib _valid_rxns = list(REACTIONS) _valid_rxns.append('fission') +_valid_rxns.append('damage-energy') def _resolve_chain_file_path(chain_file: str): From 3370ce1978ebea3dc70de24ffbbe47d41383c282 Mon Sep 17 00:00:00 2001 From: Catherine Yu <76599141+cxtherineyu@users.noreply.github.com> Date: Wed, 24 Apr 2024 17:05:10 -0400 Subject: [PATCH 091/671] Print warning if no natural isotopes when using add_element and wrote unit test (#2938) Co-authored-by: Catherine Yu Co-authored-by: Paul Romano --- openmc/element.py | 5 +++++ tests/unit_tests/test_element.py | 9 ++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/openmc/element.py b/openmc/element.py index f9cf102f7..082bee522 100644 --- a/openmc/element.py +++ b/openmc/element.py @@ -1,4 +1,5 @@ import re +import warnings import lxml.etree as ET @@ -123,6 +124,10 @@ class Element(str): # Get the nuclides present in nature natural_nuclides = {name for name, abundance in natural_isotopes(self)} + # Issue warning if no existing nuclides + if len(natural_nuclides) == 0: + warnings.warn(f"No naturally occurring isotopes found for {self}.") + # Create dict to store the expanded nuclides and abundances abundances = {} diff --git a/tests/unit_tests/test_element.py b/tests/unit_tests/test_element.py index bacb988b9..d3555701e 100644 --- a/tests/unit_tests/test_element.py +++ b/tests/unit_tests/test_element.py @@ -1,5 +1,5 @@ import openmc -from pytest import approx, raises +from pytest import approx, raises, warns from openmc.data import NATURAL_ABUNDANCE, atomic_mass @@ -37,6 +37,13 @@ def test_expand_enrichment(): assert isotope[1] == approx(ref[isotope[0]]) +def test_expand_no_isotopes(): + """Test that correct warning is raised for elements with no isotopes""" + with warns(UserWarning, match='No naturally occurring'): + element = openmc.Element('Tc') + element.expand(100.0, 'ao') + + def test_expand_exceptions(): """ Test that correct exceptions are raised for invalid input """ From f543c007a3662226c65a7dd71a6c5e93937c4468 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Wed, 24 Apr 2024 17:09:56 -0500 Subject: [PATCH 092/671] Statepoint file loading refactor and CAPI function (#2886) Co-authored-by: Paul Romano --- docs/source/pythonapi/capi.rst | 1 + docs/source/usersguide/settings.rst | 34 +++++++++++++++ include/openmc/capi.h | 1 + openmc/lib/core.py | 18 ++++++++ src/simulation.cpp | 8 +++- src/state_point.cpp | 42 ++++++++++++------- .../statepoint_restart/test.py | 24 +++++++---- 7 files changed, 104 insertions(+), 24 deletions(-) diff --git a/docs/source/pythonapi/capi.rst b/docs/source/pythonapi/capi.rst index 313543199..995ad97fa 100644 --- a/docs/source/pythonapi/capi.rst +++ b/docs/source/pythonapi/capi.rst @@ -43,6 +43,7 @@ Functions simulation_finalize simulation_init source_bank + statepoint_load statepoint_write Classes diff --git a/docs/source/usersguide/settings.rst b/docs/source/usersguide/settings.rst index 81fa78991..e966423f0 100644 --- a/docs/source/usersguide/settings.rst +++ b/docs/source/usersguide/settings.rst @@ -628,3 +628,37 @@ instance, whereas the :meth:`openmc.Track.filter` method returns a new .. code-block:: sh openmc-track-combine tracks_p*.h5 --out tracks.h5 + +----------------------- +Restarting a Simulation +----------------------- + +OpenMC can be run in a mode where it reads in a statepoint file and continues a +simulation from the ending point of the statepoint file. A restart simulation +can be performed by passing the path to the statepoint file to the OpenMC +executable: + +.. code-block:: sh + + openmc -r statepoint.100.h5 + +From the Python API, the `restart_file` argument provides the same behavior: + +.. code-block:: python + + openmc.run(restart_file='statepoint.100.h5') + +or if using the :class:`~openmc.Model` class: + +.. code-block:: python + + model.run(restart_file='statepoint.100.h5') + +The restart simulation will execute until the number of batches specified in the +:class:`~openmc.Settings` object on a model (or in the :ref:`settings XML file +`) is satisfied. Note that if the number of batches in the +statepoint file is the same as that specified in the settings object (i.e., if +the inputs were not modified before the restart run), no particles will be +transported and OpenMC will exit immediately. + +.. note:: A statepoint file must match the input model to be successfully used in a restart simulation. diff --git a/include/openmc/capi.h b/include/openmc/capi.h index 5f98152cd..9401156a6 100644 --- a/include/openmc/capi.h +++ b/include/openmc/capi.h @@ -150,6 +150,7 @@ int openmc_sphharm_filter_get_cosine(int32_t index, char cosine[]); int openmc_sphharm_filter_set_order(int32_t index, int order); int openmc_sphharm_filter_set_cosine(int32_t index, const char cosine[]); int openmc_statepoint_write(const char* filename, bool* write_source); +int openmc_statepoint_load(const char* filename); int openmc_tally_allocate(int32_t index, const char* type); int openmc_tally_get_active(int32_t index, bool* active); int openmc_tally_get_estimator(int32_t index, int* estimator); diff --git a/openmc/lib/core.py b/openmc/lib/core.py index 4ea1c86d0..d8e0bfdb5 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -95,6 +95,11 @@ _dll.openmc_simulation_finalize.errcheck = _error_handler _dll.openmc_statepoint_write.argtypes = [c_char_p, POINTER(c_bool)] _dll.openmc_statepoint_write.restype = c_int _dll.openmc_statepoint_write.errcheck = _error_handler +_dll.openmc_statepoint_load.argtypes = [c_char_p] +_dll.openmc_statepoint_load.restype = c_int +_dll.openmc_statepoint_load.errcheck = _error_handler +_dll.openmc_statepoint_write.restype = c_int +_dll.openmc_statepoint_write.errcheck = _error_handler _dll.openmc_global_bounding_box.argtypes = [POINTER(c_double), POINTER(c_double)] _dll.openmc_global_bounding_box.restype = c_int @@ -568,6 +573,19 @@ def statepoint_write(filename=None, write_source=True): _dll.openmc_statepoint_write(filename, c_bool(write_source)) +def statepoint_load(filename: PathLike): + """Load a statepoint file. + + Parameters + ---------- + filename : path-like + Path to the statepoint to load. + + """ + filename = c_char_p(str(filename).encode()) + _dll.openmc_statepoint_load(filename) + + @contextmanager def run_in_memory(**kwargs): """Provides context manager for calling OpenMC shared library functions. diff --git a/src/simulation.cpp b/src/simulation.cpp index 1cf34a820..43d920606 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -54,8 +54,14 @@ int openmc_run() openmc::simulation::time_total.start(); openmc_simulation_init(); - int err = 0; + // Ensure that a batch isn't executed in the case that the maximum number of + // batches has already been run in a restart statepoint file int status = 0; + if (openmc::simulation::current_batch >= openmc::settings::n_max_batches) { + status = openmc::STATUS_EXIT_MAX_BATCH; + } + + int err = 0; while (status == 0 && err == 0) { err = openmc_next_batch(&status); } diff --git a/src/state_point.cpp b/src/state_point.cpp index bff621320..c7b7d6ad8 100644 --- a/src/state_point.cpp +++ b/src/state_point.cpp @@ -364,11 +364,27 @@ void restart_set_keff() void load_state_point() { - // Write message - write_message("Loading state point " + settings::path_statepoint + "...", 5); + write_message( + fmt::format("Loading state point {}...", settings::path_statepoint_c), 5); + openmc_statepoint_load(settings::path_statepoint.c_str()); +} +void statepoint_version_check(hid_t file_id) +{ + // Read revision number for state point file and make sure it matches with + // current version + array version_array; + read_attribute(file_id, "version", version_array); + if (version_array != VERSION_STATEPOINT) { + fatal_error( + "State point version does not match current version in OpenMC."); + } +} + +extern "C" int openmc_statepoint_load(const char* filename) +{ // Open file for reading - hid_t file_id = file_open(settings::path_statepoint.c_str(), 'r', true); + hid_t file_id = file_open(filename, 'r', true); // Read filetype std::string word; @@ -377,14 +393,7 @@ void load_state_point() fatal_error("OpenMC tried to restart from a non-statepoint file."); } - // Read revision number for state point file and make sure it matches with - // current version - array array; - read_attribute(file_id, "version", array); - if (array != VERSION_STATEPOINT) { - fatal_error( - "State point version does not match current version in OpenMC."); - } + statepoint_version_check(file_id); // Read and overwrite random number seed int64_t seed; @@ -421,9 +430,10 @@ void load_state_point() read_dataset(file_id, "current_batch", simulation::restart_batch); if (simulation::restart_batch >= settings::n_max_batches) { - fatal_error(fmt::format( - "The number of batches specified for simulation ({}) is smaller" - " than the number of batches in the restart statepoint file ({})", + warning(fmt::format( + "The number of batches specified for simulation ({}) is smaller " + "than or equal to the number of batches in the restart statepoint file " + "({})", settings::n_max_batches, simulation::restart_batch)); } @@ -489,7 +499,6 @@ void load_state_point() if (internal) { tally->writable_ = false; } else { - auto& results = tally->results_; read_tally_results(tally_group, results.shape()[0], results.shape()[1], results.data()); @@ -497,7 +506,6 @@ void load_state_point() close_group(tally_group); } } - close_group(tallies_group); } } @@ -525,6 +533,8 @@ void load_state_point() // Close file file_close(file_id); + + return 0; } hid_t h5banktype() diff --git a/tests/regression_tests/statepoint_restart/test.py b/tests/regression_tests/statepoint_restart/test.py index 1e98bc480..82e514da8 100644 --- a/tests/regression_tests/statepoint_restart/test.py +++ b/tests/regression_tests/statepoint_restart/test.py @@ -1,7 +1,6 @@ from pathlib import Path import openmc -import pytest from tests.testing_harness import TestHarness from tests.regression_tests import config @@ -61,24 +60,35 @@ def test_statepoint_restart(): harness.main() -def test_batch_check(request): +def test_batch_check(request, capsys): xmls = list(request.path.parent.glob('*.xml')) with cdtemp(xmls): model = openmc.Model.from_xml() model.settings.particles = 100 + # run the model - sp_file = model.run() + sp_file = model.run(export_model_xml=False) + assert sp_file is not None # run a restart with the resulting statepoint # and the settings unchanged - with pytest.raises(RuntimeError, match='is smaller than the number of batches'): - model.run(restart_file=sp_file) + model.settings.batches = 6 + # ensure we capture output only from the next run + capsys.readouterr() + sp_file = model.run(export_model_xml=False, restart_file=sp_file) + # indicates that a new statepoint file was not created + assert sp_file is None - # update the number of batches and run again + output = capsys.readouterr().out + assert "WARNING" in output + assert "The number of batches specified for simulation" in output + + # update the number of batches and run again, + # this restart run should be successful model.settings.batches = 15 model.settings.statepoint = {} - sp_file = model.run(restart_file=sp_file) + sp_file = model.run(export_model_xml=False, restart_file=sp_file) sp = openmc.StatePoint(sp_file) assert sp.n_batches == 15 From 7936b8a59c578e4a2ea221f1da5dbea2ef7f95ee Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Wed, 24 Apr 2024 17:14:17 -0500 Subject: [PATCH 093/671] Support track file writing for particle restart runs. (#2957) Co-authored-by: Paul Romano --- src/particle_restart.cpp | 8 +++++++- tests/unit_tests/test_tracks.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/particle_restart.cpp b/src/particle_restart.cpp index 32d187dd8..6b7778211 100644 --- a/src/particle_restart.cpp +++ b/src/particle_restart.cpp @@ -87,8 +87,10 @@ void run_particle_restart() read_particle_restart(p, previous_run_mode); // write track if that was requested on command line - if (settings::write_all_tracks) + if (settings::write_all_tracks) { + open_track_file(); p.write_track() = true; + } // Set all tallies to 0 for now (just tracking errors) model::tallies.clear(); @@ -123,6 +125,10 @@ void run_particle_restart() // Write output if particle made it print_particle(p); + + if (settings::write_all_tracks) { + close_track_file(); + } } } // namespace openmc diff --git a/tests/unit_tests/test_tracks.py b/tests/unit_tests/test_tracks.py index 3a0170155..3951a72c6 100644 --- a/tests/unit_tests/test_tracks.py +++ b/tests/unit_tests/test_tracks.py @@ -1,5 +1,6 @@ from pathlib import Path +import h5py import numpy as np import openmc import pytest @@ -157,3 +158,35 @@ def test_write_to_vtk(sphere_model): assert isinstance(polydata, vtk.vtkPolyData) assert Path('tracks.vtp').is_file() + + +def test_restart_track(run_in_tmpdir, sphere_model): + # cut the sphere model in half with an improper boundary condition + plane = openmc.XPlane(x0=-1.0) + for cell in sphere_model.geometry.get_all_cells().values(): + cell.region &= +plane + + # generate lost particle files + with pytest.raises(RuntimeError, match='Maximum number of lost particles has been reached.'): + sphere_model.run(output=False) + + lost_particle_files = list(Path.cwd().glob('particle_*.h5')) + assert len(lost_particle_files) > 0 + particle_file = lost_particle_files[0] + # restart the lost particle with tracks enabled + sphere_model.run(tracks=True, restart_file=particle_file) + tracks_file = Path('tracks.h5') + assert tracks_file.is_file() + + # check that the last track of the file matches the lost particle file + tracks = openmc.Tracks(tracks_file) + initial_state = tracks[0].particle_tracks[0].states[0] + restart_r = np.array(initial_state['r']) + restart_u = np.array(initial_state['u']) + + with h5py.File(particle_file, 'r') as lost_particle_file: + lost_r = np.array(lost_particle_file['xyz'][()]) + lost_u = np.array(lost_particle_file['uvw'][()]) + + pytest.approx(restart_r, lost_r) + pytest.approx(restart_u, lost_u) From ff50afb19ffe9cffb5707118a7796b1ba534f280 Mon Sep 17 00:00:00 2001 From: Gavin Ridley Date: Wed, 24 Apr 2024 17:14:37 -0500 Subject: [PATCH 094/671] Allow pure decay IndependentOperator (#2966) Co-authored-by: Paul Romano --- openmc/deplete/abc.py | 2 +- openmc/deplete/independent_operator.py | 9 +++-- ...ecay_products.py => test_deplete_decay.py} | 36 +++++++++++++++++++ 3 files changed, 43 insertions(+), 4 deletions(-) rename tests/unit_tests/{test_deplete_decay_products.py => test_deplete_decay.py} (55%) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index a574d4855..fa07ad12a 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -848,7 +848,7 @@ class Integrator(ABC): print(f"[openmc.deplete] t={t} (final operator evaluation)") res_list = [self.operator(n, source_rate if final_step else 0.0)] StepResult.save(self.operator, [n], res_list, [t, t], - source_rate, self._i_res + len(self), proc_time) + source_rate, self._i_res + len(self), proc_time, path) self.operator.write_bos_data(len(self) + self._i_res) self.operator.finalize() diff --git a/openmc/deplete/independent_operator.py b/openmc/deplete/independent_operator.py index 5469b28c7..2328ca761 100644 --- a/openmc/deplete/independent_operator.py +++ b/openmc/deplete/independent_operator.py @@ -241,7 +241,6 @@ class IndependentOperator(OpenMCOperator): reduce_chain_level=reduce_chain_level, fission_yield_opts=fission_yield_opts) - @staticmethod def _consolidate_nuclides_to_material(nuclides, nuc_units, volume): """Puts nuclide list into an openmc.Materials object. @@ -270,7 +269,6 @@ class IndependentOperator(OpenMCOperator): self.prev_res[-1].transfer_volumes(model) self.materials = model.materials - # Store previous results in operator # Distribute reaction rates according to those tracked # on this process @@ -282,7 +280,6 @@ class IndependentOperator(OpenMCOperator): new_res = res_obj.distribute(self.local_mats, mat_indexes) self.prev_res.append(new_res) - def _get_nuclides_with_data(self, cross_sections: List[MicroXS]) -> Set[str]: """Finds nuclides with cross section data""" return set(cross_sections[0].nuclides) @@ -412,6 +409,12 @@ class IndependentOperator(OpenMCOperator): self._update_materials_and_nuclides(vec) + # If the source rate is zero, return zero reaction rates + if source_rate == 0.0: + rates = self.reaction_rates.copy() + rates.fill(0.0) + return OperatorResult(ufloat(0.0, 0.0), rates) + rates = self._calculate_reaction_rates(source_rate) keff = self._keff diff --git a/tests/unit_tests/test_deplete_decay_products.py b/tests/unit_tests/test_deplete_decay.py similarity index 55% rename from tests/unit_tests/test_deplete_decay_products.py rename to tests/unit_tests/test_deplete_decay.py index 751a96ca6..aca812560 100644 --- a/tests/unit_tests/test_deplete_decay_products.py +++ b/tests/unit_tests/test_deplete_decay.py @@ -1,3 +1,5 @@ +from pathlib import Path + import openmc.deplete import numpy as np import pytest @@ -45,3 +47,37 @@ def test_deplete_decay_products(run_in_tmpdir): # H1 and He4 assert h1[1] == pytest.approx(1e24) assert he4[1] == pytest.approx(1e24) + + +def test_deplete_decay_step_fissionable(run_in_tmpdir): + """Ensures that power is not computed in zero power cases with + fissionable material present. This tests decay calculations without + power, although this specific example does not exhibit any decay. + + Proves github issue #2963 is fixed + """ + + # Set up a pure decay operator + micro_xs = openmc.deplete.MicroXS(np.empty((0, 0)), [], []) + mat = openmc.Material() + mat.name = 'I do not decay.' + mat.add_nuclide('U238', 1.0, 'ao') + mat.volume = 10.0 + mat.set_density('g/cc', 1.0) + original_atoms = mat.get_nuclide_atoms()['U238'] + + mats = openmc.Materials([mat]) + op = openmc.deplete.IndependentOperator( + mats, [1.0], [micro_xs], Path(__file__).parents[1] / "chain_simple.xml") + + # Create time integrator and integrate + integrator = openmc.deplete.PredictorIntegrator( + op, [1.0], power=[0.0], timestep_units='s' + ) + integrator.integrate() + + # Get concentration of U238. It should be unchanged since this chain has no U238 decay. + results = openmc.deplete.Results('depletion_results.h5') + _, u238 = results.get_atoms("1", "U238") + + assert u238[1] == pytest.approx(original_atoms) From 95b15f9522aa11b8d1eca6f8270c519ef85575ca Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Thu, 25 Apr 2024 01:39:51 +0100 Subject: [PATCH 095/671] moved apt get to optional ci parts (#2970) Co-authored-by: Paul Romano --- .github/workflows/ci.yml | 14 ++++++++++++-- tools/ci/gha-install-vectfit.sh | 2 -- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5accc5e0..7aaaf1da4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -111,16 +111,26 @@ jobs: run: | sudo apt -y update sudo apt install -y libpng-dev \ - libmpich-dev \ libnetcdf-dev \ libpnetcdf-dev \ libhdf5-serial-dev \ - libhdf5-mpich-dev \ libeigen3-dev + + - name: Optional apt dependencies for MPI + shell: bash + if: ${{ matrix.mpi == 'y' }} + run: | + sudo apt install -y libhdf5-mpich-dev \ + libmpich-dev sudo update-alternatives --set mpi /usr/bin/mpicc.mpich sudo update-alternatives --set mpirun /usr/bin/mpirun.mpich sudo update-alternatives --set mpi-x86_64-linux-gnu /usr/include/x86_64-linux-gnu/mpich + - name: Optional apt dependencies for vectfit + shell: bash + if: ${{ matrix.vectfit == 'y' }} + run: sudo apt install -y libblas-dev liblapack-dev + - name: install shell: bash run: | diff --git a/tools/ci/gha-install-vectfit.sh b/tools/ci/gha-install-vectfit.sh index 8444c3036..bd38e1ea8 100755 --- a/tools/ci/gha-install-vectfit.sh +++ b/tools/ci/gha-install-vectfit.sh @@ -16,8 +16,6 @@ XTENSOR_PYTHON_REPO='https://github.com/xtensor-stack/xtensor-python' XTENSOR_BLAS_BRANCH='0.17.1' XTENSOR_BLAS_REPO='https://github.com/xtensor-stack/xtensor-blas' -sudo apt-get install -y libblas-dev liblapack-dev - cd $HOME git clone -b $PYBIND_BRANCH $PYBIND_REPO cd pybind11 && mkdir build && cd build && cmake .. && sudo make install From d1d37a5b99c90ea8cf5cb150c33cfaa6c84910c1 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 26 Apr 2024 05:57:02 -0500 Subject: [PATCH 096/671] Allow MOAB k-d tree to be configured (#2976) --- docs/source/io_formats/statepoint.rst | 2 ++ docs/source/io_formats/tallies.rst | 4 +++ include/openmc/mesh.h | 10 +++--- openmc/filter.py | 8 ++--- openmc/mesh.py | 36 +++++++++++++++++-- src/mesh.cpp | 36 ++++++++++++++----- .../unstructured_mesh/inputs_true10.dat | 2 +- .../unstructured_mesh/inputs_true11.dat | 2 +- .../unstructured_mesh/inputs_true12.dat | 2 +- .../unstructured_mesh/inputs_true13.dat | 2 +- .../unstructured_mesh/inputs_true14.dat | 2 +- .../unstructured_mesh/inputs_true15.dat | 2 +- .../unstructured_mesh/inputs_true8.dat | 2 +- .../unstructured_mesh/inputs_true9.dat | 2 +- .../unstructured_mesh/test.py | 2 ++ 15 files changed, 85 insertions(+), 29 deletions(-) diff --git a/docs/source/io_formats/statepoint.rst b/docs/source/io_formats/statepoint.rst index edff686b1..f61e967ca 100644 --- a/docs/source/io_formats/statepoint.rst +++ b/docs/source/io_formats/statepoint.rst @@ -96,6 +96,8 @@ The current version of the statepoint file format is 18.1. - **library** (*char[]*) -- Mesh library used to represent the mesh ("moab" or "libmesh"). - **length_multiplier** (*double*) Scaling factor applied to the mesh. + - **options** (*char[]*) -- Special options that control spatial + search data structures used. - **volumes** (*double[]*) -- Volume of each mesh cell. - **vertices** (*double[]*) -- x, y, z values of the mesh vertices. - **connectivity** (*int[]*) -- Connectivity array for the mesh diff --git a/docs/source/io_formats/tallies.rst b/docs/source/io_formats/tallies.rst index b88877b83..c28db988b 100644 --- a/docs/source/io_formats/tallies.rst +++ b/docs/source/io_formats/tallies.rst @@ -364,6 +364,10 @@ attributes/sub-elements: The mesh library used to represent an unstructured mesh. This can be either "moab" or "libmesh". (For unstructured mesh only.) + :options: + Special options that control spatial search data structures used. (For + unstructured mesh using MOAB only) + :filename: The name of the mesh file to be loaded at runtime. (For unstructured mesh only.) diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index 37b771832..3917e6368 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -659,11 +659,6 @@ protected: //! Set the length multiplier to apply to each point in the mesh void set_length_multiplier(const double length_multiplier); - // Data members - double length_multiplier_ { - 1.0}; //!< Constant multiplication factor to apply to mesh coordinates - bool specified_length_multiplier_ {false}; - //! Sample barycentric coordinates given a seed and the vertex positions and //! return the sampled position // @@ -672,6 +667,11 @@ protected: //! \return Sampled position within the tetrahedron Position sample_tet(std::array coords, uint64_t* seed) const; + // Data members + double length_multiplier_ { + -1.0}; //!< Multiplicative factor applied to mesh coordinates + std::string options_; //!< Options for search data structures + private: //! Setup method for the mesh. Builds data structures, //! sets up element mapping, creates bounding boxes, etc. diff --git a/openmc/filter.py b/openmc/filter.py index 66d832c69..e005140ee 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -1,3 +1,4 @@ +from __future__ import annotations from abc import ABCMeta from collections.abc import Iterable import hashlib @@ -804,8 +805,8 @@ class MeshFilter(Filter): id : int Unique identifier for the filter translation : Iterable of float - This array specifies a vector that is used to translate (shift) - the mesh for this filter + This array specifies a vector that is used to translate (shift) the mesh + for this filter bins : list of tuple A list of mesh indices for each filter bin, e.g. [(1, 1, 1), (2, 1, 1), ...] @@ -846,7 +847,6 @@ class MeshFilter(Filter): mesh_obj = kwargs['meshes'][mesh_id] filter_id = int(group.name.split('/')[-1].lstrip('filter ')) - out = cls(mesh_obj, filter_id=filter_id) translation = group.get('translation') @@ -972,7 +972,7 @@ class MeshFilter(Filter): return element @classmethod - def from_xml_element(cls, elem, **kwargs): + def from_xml_element(cls, elem: ET.Element, **kwargs) -> MeshFilter: mesh_id = int(get_text(elem, 'bins')) mesh_obj = kwargs['meshes'][mesh_id] filter_id = int(elem.get('id')) diff --git a/openmc/mesh.py b/openmc/mesh.py index 280f447fa..1f36524d2 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -1952,6 +1952,11 @@ class UnstructuredMesh(MeshBase): Name of the mesh length_multiplier: float Constant multiplier to apply to mesh coordinates + options : str, optional + Special options that control spatial search data structures used. This + is currently only used to set `parameters + `_ for MOAB's AdaptiveKDTree. If + None, OpenMC internally uses a default of "MAX_DEPTH=20;PLANE_SET=2;". Attributes ---------- @@ -1965,6 +1970,11 @@ class UnstructuredMesh(MeshBase): Multiplicative factor to apply to mesh coordinates library : {'moab', 'libmesh'} Mesh library used for the unstructured mesh tally + options : str + Special options that control spatial search data structures used. This + is currently only used to set `parameters + `_ for MOAB's AdaptiveKDTree. If + None, OpenMC internally uses a default of "MAX_DEPTH=20;PLANE_SET=2;". output : bool Indicates whether or not automatic tally output should be generated for this mesh @@ -1998,7 +2008,8 @@ class UnstructuredMesh(MeshBase): _LINEAR_HEX = 1 def __init__(self, filename: PathLike, library: str, mesh_id: Optional[int] = None, - name: str = '', length_multiplier: float = 1.0): + name: str = '', length_multiplier: float = 1.0, + options: Optional[str] = None): super().__init__(mesh_id, name) self.filename = filename self._volumes = None @@ -2008,6 +2019,7 @@ class UnstructuredMesh(MeshBase): self.library = library self._output = False self.length_multiplier = length_multiplier + self.options = options self._has_statepoint_data = False @property @@ -2028,6 +2040,15 @@ class UnstructuredMesh(MeshBase): cv.check_value('Unstructured mesh library', lib, ('moab', 'libmesh')) self._library = lib + @property + def options(self) -> Optional[str]: + return self._options + + @options.setter + def options(self, options: Optional[str]): + cv.check_type('options', options, (str, type(None))) + self._options = options + @property @require_statepoint_data def size(self): @@ -2139,6 +2160,8 @@ class UnstructuredMesh(MeshBase): if self.length_multiplier != 1.0: string += '{: <16}=\t{}\n'.format('\tLength multiplier', self.length_multiplier) + if self.options is not None: + string += '{: <16}=\t{}\n'.format('\tOptions', self.options) return string @property @@ -2294,8 +2317,12 @@ class UnstructuredMesh(MeshBase): mesh_id = int(group.name.split('/')[-1].lstrip('mesh ')) filename = group['filename'][()].decode() library = group['library'][()].decode() + if 'options' in group.attrs: + options = group.attrs['options'].decode() + else: + options = None - mesh = cls(filename=filename, library=library, mesh_id=mesh_id) + mesh = cls(filename=filename, library=library, mesh_id=mesh_id, options=options) mesh._has_statepoint_data = True vol_data = group['volumes'][()] mesh.volumes = np.reshape(vol_data, (vol_data.shape[0],)) @@ -2326,6 +2353,8 @@ class UnstructuredMesh(MeshBase): element.set("id", str(self._id)) element.set("type", "unstructured") element.set("library", self._library) + if self.options is not None: + element.set('options', self.options) subelement = ET.SubElement(element, "filename") subelement.text = str(self.filename) @@ -2352,8 +2381,9 @@ class UnstructuredMesh(MeshBase): filename = get_text(elem, 'filename') library = get_text(elem, 'library') length_multiplier = float(get_text(elem, 'length_multiplier', 1.0)) + options = elem.get('options') - return cls(filename, library, mesh_id, '', length_multiplier) + return cls(filename, library, mesh_id, '', length_multiplier, options) def _read_meshes(elem): diff --git a/src/mesh.cpp b/src/mesh.cpp index c6e421b42..c57dd5dcc 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -44,6 +44,10 @@ #include "libmesh/numeric_vector.h" #endif +#ifdef DAGMC +#include "moab/FileOptions.hpp" +#endif + namespace openmc { //============================================================================== @@ -291,7 +295,6 @@ UnstructuredMesh::UnstructuredMesh(pugi::xml_node node) : Mesh(node) // check if a length unit multiplier was specified if (check_for_node(node, "length_multiplier")) { length_multiplier_ = std::stod(get_node_value(node, "length_multiplier")); - specified_length_multiplier_ = true; } // get the filename of the unstructured mesh to load @@ -305,6 +308,10 @@ UnstructuredMesh::UnstructuredMesh(pugi::xml_node node) : Mesh(node) "No filename supplied for unstructured mesh with ID: {}", id_)); } + if (check_for_node(node, "options")) { + options_ = get_node_value(node, "options"); + } + // check if mesh tally data should be written with // statepoint files if (check_for_node(node, "output")) { @@ -367,8 +374,11 @@ void UnstructuredMesh::to_hdf5(hid_t group) const write_dataset(mesh_group, "type", mesh_type); write_dataset(mesh_group, "filename", filename_); write_dataset(mesh_group, "library", this->library()); + if (!options_.empty()) { + write_attribute(mesh_group, "options", options_); + } - if (specified_length_multiplier_) + if (length_multiplier_ > 0.0) write_dataset(mesh_group, "length_multiplier", length_multiplier_); // write vertex coordinates @@ -428,9 +438,6 @@ void UnstructuredMesh::to_hdf5(hid_t group) const void UnstructuredMesh::set_length_multiplier(double length_multiplier) { length_multiplier_ = length_multiplier; - - if (length_multiplier_ != 1.0) - specified_length_multiplier_ = true; } ElementType UnstructuredMesh::element_type(int bin) const @@ -2231,7 +2238,7 @@ void MOABMesh::initialize() fatal_error("Failed to add tetrahedra to an entity set."); } - if (specified_length_multiplier_) { + if (length_multiplier_ > 0.0) { // get the connectivity of all tets moab::Range adj; rval = mbi_->get_adjacencies(ehs_, 0, true, adj, moab::Interface::UNION); @@ -2291,6 +2298,7 @@ void MOABMesh::build_kdtree(const moab::Range& all_tets) { moab::Range all_tris; int adj_dim = 2; + write_message("Getting tet adjacencies...", 7); moab::ErrorCode rval = mbi_->get_adjacencies( all_tets, adj_dim, true, all_tris, moab::Interface::UNION); if (rval != moab::MB_SUCCESS) { @@ -2309,10 +2317,20 @@ void MOABMesh::build_kdtree(const moab::Range& all_tets) all_tets_and_tris.merge(all_tris); // create a kd-tree instance + write_message("Building adaptive k-d tree for tet mesh...", 7); kdtree_ = make_unique(mbi_.get()); - // build the tree - rval = kdtree_->build_tree(all_tets_and_tris, &kdtree_root_); + // Determine what options to use + std::ostringstream options_stream; + if (options_.empty()) { + options_stream << "MAX_DEPTH=20;PLANE_SET=2;"; + } else { + options_stream << options_; + } + moab::FileOptions file_opts(options_stream.str().c_str()); + + // Build the k-d tree + rval = kdtree_->build_tree(all_tets_and_tris, &kdtree_root_, &file_opts); if (rval != moab::MB_SUCCESS) { fatal_error("Failed to construct KDTree for the " "unstructured mesh file: " + @@ -2899,7 +2917,7 @@ void LibMesh::initialize() // assuming that unstructured meshes used in OpenMC are 3D n_dimension_ = 3; - if (specified_length_multiplier_) { + if (length_multiplier_ > 0.0) { libMesh::MeshTools::Modification::scale(*m_, length_multiplier_); } // if OpenMC is managing the libMesh::MeshBase instance, prepare the mesh. diff --git a/tests/regression_tests/unstructured_mesh/inputs_true10.dat b/tests/regression_tests/unstructured_mesh/inputs_true10.dat index ebc5548f5..81dfbdc52 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true10.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true10.dat @@ -67,7 +67,7 @@ -10.0 -10.0 -10.0 10.0 10.0 10.0 - + test_mesh_tets_w_holes.e diff --git a/tests/regression_tests/unstructured_mesh/inputs_true11.dat b/tests/regression_tests/unstructured_mesh/inputs_true11.dat index e30a5ccfc..b224a3ebc 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true11.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true11.dat @@ -67,7 +67,7 @@ -10.0 -10.0 -10.0 10.0 10.0 10.0 - + test_mesh_tets.e diff --git a/tests/regression_tests/unstructured_mesh/inputs_true12.dat b/tests/regression_tests/unstructured_mesh/inputs_true12.dat index a60b70eab..a18aee627 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true12.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true12.dat @@ -67,7 +67,7 @@ -10.0 -10.0 -10.0 10.0 10.0 10.0 - + test_mesh_tets_w_holes.e diff --git a/tests/regression_tests/unstructured_mesh/inputs_true13.dat b/tests/regression_tests/unstructured_mesh/inputs_true13.dat index 1b1aa19c3..0e9123c90 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true13.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true13.dat @@ -67,7 +67,7 @@ -10.0 -10.0 -10.0 10.0 10.0 10.0 - + test_mesh_tets.e diff --git a/tests/regression_tests/unstructured_mesh/inputs_true14.dat b/tests/regression_tests/unstructured_mesh/inputs_true14.dat index d07504b9d..9d4db4904 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true14.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true14.dat @@ -67,7 +67,7 @@ -10.0 -10.0 -10.0 10.0 10.0 10.0 - + test_mesh_tets_w_holes.e diff --git a/tests/regression_tests/unstructured_mesh/inputs_true15.dat b/tests/regression_tests/unstructured_mesh/inputs_true15.dat index 8cabbb38d..eab97b952 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true15.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true15.dat @@ -67,7 +67,7 @@ -10.0 -10.0 -10.0 10.0 10.0 10.0 - + test_mesh_tets.e diff --git a/tests/regression_tests/unstructured_mesh/inputs_true8.dat b/tests/regression_tests/unstructured_mesh/inputs_true8.dat index 5f14aa691..7994e1add 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true8.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true8.dat @@ -67,7 +67,7 @@ -10.0 -10.0 -10.0 10.0 10.0 10.0 - + test_mesh_tets_w_holes.e diff --git a/tests/regression_tests/unstructured_mesh/inputs_true9.dat b/tests/regression_tests/unstructured_mesh/inputs_true9.dat index 92354c75c..4fff123d7 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true9.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true9.dat @@ -67,7 +67,7 @@ -10.0 -10.0 -10.0 10.0 10.0 10.0 - + test_mesh_tets.e diff --git a/tests/regression_tests/unstructured_mesh/test.py b/tests/regression_tests/unstructured_mesh/test.py index f3ba46e37..0082198dd 100644 --- a/tests/regression_tests/unstructured_mesh/test.py +++ b/tests/regression_tests/unstructured_mesh/test.py @@ -277,6 +277,8 @@ def test_unstructured_mesh_tets(model, test_opts): # add analagous unstructured mesh tally uscd_mesh = openmc.UnstructuredMesh(mesh_filename, test_opts['library']) + if test_opts['library'] == 'moab': + uscd_mesh.options = 'MAX_DEPTH=15;PLANE_SET=2' uscd_filter = openmc.MeshFilter(mesh=uscd_mesh) # create tallies From e8ae7063afc20c5c33dbe49476819fb00b7e89d0 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 26 Apr 2024 17:54:38 -0500 Subject: [PATCH 097/671] Update CODEOWNERS file (#2974) --- CODEOWNERS | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 1b5130e0b..6d452e366 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -5,9 +5,9 @@ openmc/data/ @paulromano openmc/lib/ @paulromano # Depletion -openmc/deplete/ @drewejohnson -tests/regression_tests/deplete/ @drewejohnson -tests/unit_tests/test_deplete_*.py @drewejohnson +openmc/deplete/ @paulromano +tests/regression_tests/deplete/ @paulromano +tests/unit_tests/test_deplete_*.py @paulromano # MG-related functionality openmc/mgxs_library.py @nelsonag @@ -26,6 +26,12 @@ src/dagmc.cpp @pshriwise tests/regression_tests/dagmc/ @pshriwise tests/unit_tests/dagmc/ @pshriwise +# Weight windows +openmc/weight_windows.py @pshriwise +openmc/lib/weight_windows.py @pshriwise +src/weight_windows.py @pshriwise +tests/unit_tests/weightwindows/ @pshriwise + # Photon transport openmc/data/BREMX.DAT @amandalund openmc/data/compton_profiles.h5 @amandalund @@ -49,3 +55,12 @@ openmc/data/resonance_covariance.py @icmeyer # Docker Dockerfile @shimwell + +# Random ray +src/random_ray/ @jtramm + +# NCrystal interface +src/ncrystal_interface.cpp @marquezj + +# MCPL interface +src/mcpl_interface.cpp @ebknudsen From 5d2b352025e7dcfdb8772dc0415d6eef3000d335 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Mon, 29 Apr 2024 22:45:37 +0100 Subject: [PATCH 098/671] f strings instead of .format for string editing (#2987) --- openmc/arithmetic.py | 5 ++- openmc/cmfd.py | 17 +++++----- openmc/data/ace.py | 19 +++++------ openmc/data/angle_energy.py | 3 +- openmc/data/decay.py | 11 +++---- openmc/data/effective_dose/dose.py | 2 +- openmc/data/endf.py | 3 +- openmc/data/energy_distribution.py | 3 +- openmc/data/library.py | 3 +- openmc/data/multipole.py | 53 ++++++++++++++---------------- openmc/data/neutron.py | 16 ++++----- openmc/data/njoy.py | 36 ++++++++++---------- openmc/data/photon.py | 17 +++++----- openmc/data/product.py | 4 +-- openmc/data/reaction.py | 31 +++++++++-------- openmc/data/resonance.py | 4 +-- openmc/data/thermal.py | 8 ++--- openmc/deplete/abc.py | 13 +++----- openmc/deplete/chain.py | 20 +++++------ openmc/deplete/coupled_operator.py | 2 +- openmc/deplete/nuclide.py | 9 +++-- openmc/lattice.py | 4 +-- openmc/lib/__init__.py | 2 +- openmc/lib/cell.py | 2 +- openmc/lib/core.py | 4 +-- openmc/lib/error.py | 2 +- openmc/lib/plot.py | 31 +++++++++-------- openmc/lib/settings.py | 2 +- openmc/material.py | 25 ++++++-------- openmc/mgxs/library.py | 4 +-- openmc/mgxs/mdgxs.py | 10 +++--- openmc/mgxs/mgxs.py | 31 +++++++++-------- openmc/model/funcs.py | 3 +- openmc/model/surface_composite.py | 2 +- openmc/plots.py | 3 +- openmc/stats/univariate.py | 6 ++-- openmc/surface.py | 3 +- openmc/tallies.py | 13 ++++---- 38 files changed, 197 insertions(+), 229 deletions(-) diff --git a/openmc/arithmetic.py b/openmc/arithmetic.py index c66efe5b8..4520553da 100644 --- a/openmc/arithmetic.py +++ b/openmc/arithmetic.py @@ -54,8 +54,7 @@ class CrossScore: return str(other) == str(self) def __repr__(self): - return '({} {} {})'.format(self.left_score, self.binary_op, - self.right_score) + return f'({self.left_score} {self.binary_op} {self.right_score})' @property def left_score(self): @@ -271,7 +270,7 @@ class CrossFilter: def type(self): left_type = self.left_filter.type right_type = self.right_filter.type - return '({} {} {})'.format(left_type, self.binary_op, right_type) + return f'({left_type} {self.binary_op} {right_type})' @property def bins(self): diff --git a/openmc/cmfd.py b/openmc/cmfd.py index 9b0b4ca83..eff6a151f 100644 --- a/openmc/cmfd.py +++ b/openmc/cmfd.py @@ -135,7 +135,7 @@ class CMFDMesh: return outstr def _get_repr(self, list_var, label): - outstr = "\t{:<11} = ".format(label) + outstr = f"\t{label:<11} = " if list(list_var): outstr += ", ".join(str(i) for i in list_var) return outstr @@ -242,9 +242,9 @@ class CMFDMesh: check_length('CMFD mesh grid', grid, grid_length) for i in range(grid_length): - check_type('CMFD mesh {}-grid'.format(dims[i]), grid[i], Iterable, + check_type(f'CMFD mesh {dims[i]}-grid', grid[i], Iterable, Real) - check_greater_than('CMFD mesh {}-grid length'.format(dims[i]), + check_greater_than(f'CMFD mesh {dims[i]}-grid length', len(grid[i]), 1) self._grid = [np.array(g) for g in grid] self._display_mesh_warning('rectilinear', 'CMFD mesh grid') @@ -612,7 +612,7 @@ class CMFDRun: for key, value in display.items(): check_value('display key', key, ('balance', 'entropy', 'dominance', 'source')) - check_type("display['{}']".format(key), value, bool) + check_type(f"display['{key}']", value, bool) self._display[key] = value @downscatter.setter @@ -928,7 +928,7 @@ class CMFDRun: with h5py.File(filename, 'a') as f: if 'cmfd' not in f: if openmc.lib.settings.verbosity >= 5: - print(' Writing CMFD data to {}...'.format(filename)) + print(f' Writing CMFD data to {filename}...') sys.stdout.flush() cmfd_group = f.create_group("cmfd") cmfd_group.attrs['cmfd_on'] = self._cmfd_on @@ -1134,12 +1134,12 @@ class CMFDRun: with h5py.File(filename, 'r') as f: if 'cmfd' not in f: raise OpenMCError('Could not find CMFD parameters in ', - 'file {}'.format(filename)) + f'file {filename}') else: # Overwrite CMFD values from statepoint if (openmc.lib.master() and openmc.lib.settings.verbosity >= 5): - print(' Loading CMFD data from {}...'.format(filename)) + print(f' Loading CMFD data from {filename}...') sys.stdout.flush() cmfd_group = f['cmfd'] @@ -1409,8 +1409,7 @@ class CMFDRun: # Get all data entries for particular row in matrix data = matrix.data[matrix.indptr[row]:matrix.indptr[row+1]] for i in range(len(cols)): - fh.write('{:3d}, {:3d}, {:0.8f}\n'.format( - row, cols[i], data[i])) + fh.write(f'{row:3d}, {cols[i]:3d}, {data[i]:0.8f}\n') # Save matrix in scipy format sparse.save_npz(base_filename, matrix) diff --git a/openmc/data/ace.py b/openmc/data/ace.py index 7c348bd17..1247593a8 100644 --- a/openmc/data/ace.py +++ b/openmc/data/ace.py @@ -143,7 +143,7 @@ def ascii_to_binary(ascii_file, binary_file): # that XSS will start at the second record nxs = [int(x) for x in ' '.join(lines[idx + 6:idx + 8]).split()] jxs = [int(x) for x in ' '.join(lines[idx + 8:idx + 12]).split()] - binary_file.write(struct.pack(str('=16i32i{}x'.format(record_length - 500)), + binary_file.write(struct.pack(str(f'=16i32i{record_length - 500}x'), *(nxs + jxs))) # Read/write XSS array. Null bytes are added to form a complete record @@ -152,8 +152,7 @@ def ascii_to_binary(ascii_file, binary_file): start = idx + _ACE_HEADER_SIZE xss = np.fromstring(' '.join(lines[start:start + n_lines]), sep=' ') extra_bytes = record_length - ((len(xss)*8 - 1) % record_length + 1) - binary_file.write(struct.pack(str('={}d{}x'.format( - nxs[0], extra_bytes)), *xss)) + binary_file.write(struct.pack(str(f'={nxs[0]}d{extra_bytes}x'), *xss)) # Advance to next table in file idx += _ACE_HEADER_SIZE + n_lines @@ -184,8 +183,7 @@ def get_table(filename, name=None): if lib.tables: return lib.tables[0] else: - raise ValueError('Could not find ACE table with name: {}' - .format(name)) + raise ValueError(f'Could not find ACE table with name: {name}') # The beginning of an ASCII ACE file consists of 12 lines that include the name, @@ -295,14 +293,14 @@ class Library(EqualityMixin): if verbose: kelvin = round(temperature * EV_PER_MEV / K_BOLTZMANN) - print("Loading nuclide {} at {} K".format(name, kelvin)) + print(f"Loading nuclide {name} at {kelvin} K") # Read JXS jxs = list(struct.unpack(str('=32i'), ace_file.read(128))) # Read XSS ace_file.seek(start_position + recl_length) - xss = list(struct.unpack(str('={}d'.format(length)), + xss = list(struct.unpack(str(f'={length}d'), ace_file.read(length*8))) # Insert zeros at beginning of NXS, JXS, and XSS arrays so that the @@ -393,7 +391,7 @@ class Library(EqualityMixin): if verbose: kelvin = round(temperature * EV_PER_MEV / K_BOLTZMANN) - print("Loading nuclide {} at {} K".format(name, kelvin)) + print(f"Loading nuclide {name} at {kelvin} K") # Insert zeros at beginning of NXS, JXS, and XSS arrays so that the # indexing will be the same as Fortran. This makes it easier to @@ -455,8 +453,7 @@ class TableType(enum.Enum): for member in cls: if suffix.endswith(member.value): return member - raise ValueError("Suffix '{}' has no corresponding ACE table type." - .format(suffix)) + raise ValueError(f"Suffix '{suffix}' has no corresponding ACE table type.") class Table(EqualityMixin): @@ -507,7 +504,7 @@ class Table(EqualityMixin): return TableType.from_suffix(xs[-1]) def __repr__(self): - return "".format(self.name) + return f"" def get_libraries_from_xsdir(path): diff --git a/openmc/data/angle_energy.py b/openmc/data/angle_energy.py index b8fde5478..71ca47587 100644 --- a/openmc/data/angle_energy.py +++ b/openmc/data/angle_energy.py @@ -112,7 +112,6 @@ class AngleEnergy(EqualityMixin, ABC): distribution = openmc.data.NBodyPhaseSpace.from_ace( ace, idx, rx.q_value) else: - raise ValueError("Unsupported ACE secondary energy " - "distribution law {}".format(law)) + raise ValueError(f"Unsupported ACE secondary energy distribution law {law}") return distribution diff --git a/openmc/data/decay.py b/openmc/data/decay.py index 7b8993a28..be3dab77a 100644 --- a/openmc/data/decay.py +++ b/openmc/data/decay.py @@ -128,7 +128,7 @@ class FissionProductYields(EqualityMixin): isomeric_state = int(values[4*j + 1]) name = ATOMIC_SYMBOL[Z] + str(A) if isomeric_state > 0: - name += '_m{}'.format(isomeric_state) + name += f'_m{isomeric_state}' yield_j = ufloat(values[4*j + 2], values[4*j + 3]) yields[name] = yield_j @@ -257,9 +257,9 @@ class DecayMode(EqualityMixin): Z += delta_Z if self._daughter_state > 0: - return '{}{}_m{}'.format(ATOMIC_SYMBOL[Z], A, self._daughter_state) + return f'{ATOMIC_SYMBOL[Z]}{A}_m{self._daughter_state}' else: - return '{}{}'.format(ATOMIC_SYMBOL[Z], A) + return f'{ATOMIC_SYMBOL[Z]}{A}' @property def parent(self): @@ -350,10 +350,9 @@ class Decay(EqualityMixin): self.nuclide['mass_number'] = A self.nuclide['isomeric_state'] = metastable if metastable > 0: - self.nuclide['name'] = '{}{}_m{}'.format(ATOMIC_SYMBOL[Z], A, - metastable) + self.nuclide['name'] = f'{ATOMIC_SYMBOL[Z]}{A}_m{metastable}' else: - self.nuclide['name'] = '{}{}'.format(ATOMIC_SYMBOL[Z], A) + self.nuclide['name'] = f'{ATOMIC_SYMBOL[Z]}{A}' self.nuclide['mass'] = items[1] # AWR self.nuclide['excited_state'] = items[2] # State of the original nuclide self.nuclide['stable'] = (items[4] == 1) # Nucleus stability flag diff --git a/openmc/data/effective_dose/dose.py b/openmc/data/effective_dose/dose.py index eb8fe9f35..ae981ee7d 100644 --- a/openmc/data/effective_dose/dose.py +++ b/openmc/data/effective_dose/dose.py @@ -60,7 +60,7 @@ def dose_coefficients(particle, geometry='AP'): # Get all data for selected particle data = _DOSE_ICRP116.get(particle) if data is None: - raise ValueError("{} has no effective dose data".format(particle)) + raise ValueError(f"{particle} has no effective dose data") # Determine index for selected geometry if particle in ('neutron', 'photon', 'proton', 'photon kerma'): diff --git a/openmc/data/endf.py b/openmc/data/endf.py index d526bc53f..73299723a 100644 --- a/openmc/data/endf.py +++ b/openmc/data/endf.py @@ -449,8 +449,7 @@ class Evaluation: def __repr__(self): name = self.target['zsymam'].replace(' ', '') - return '<{} for {} {}>'.format(self.info['sublibrary'], name, - self.info['library']) + return f"<{self.info['sublibrary']} for {name} {self.info['library']}>" def _read_header(self): file_obj = io.StringIO(self.section[1, 451]) diff --git a/openmc/data/energy_distribution.py b/openmc/data/energy_distribution.py index b3566e999..069ab1b9b 100644 --- a/openmc/data/energy_distribution.py +++ b/openmc/data/energy_distribution.py @@ -53,8 +53,7 @@ class EnergyDistribution(EqualityMixin, ABC): elif energy_type == 'continuous': return ContinuousTabular.from_hdf5(group) else: - raise ValueError("Unknown energy distribution type: {}" - .format(energy_type)) + raise ValueError(f"Unknown energy distribution type: {energy_type}") @staticmethod def from_endf(file_obj, params): diff --git a/openmc/data/library.py b/openmc/data/library.py index de9de2744..a6ce1bbd3 100644 --- a/openmc/data/library.py +++ b/openmc/data/library.py @@ -93,8 +93,7 @@ class DataLibrary(list): materials = list(h5file) else: raise ValueError( - "File type {} not supported by {}" - .format(path.name, self.__class__.__name__)) + f"File type {path.name} not supported by {self.__class__.__name__}") library = {'path': str(path), 'type': filetype, 'materials': materials} self.append(library) diff --git a/openmc/data/multipole.py b/openmc/data/multipole.py index 5c9df39c2..dd14e0d19 100644 --- a/openmc/data/multipole.py +++ b/openmc/data/multipole.py @@ -194,9 +194,8 @@ def _vectfit_xs(energy, ce_xs, mts, rtol=1e-3, atol=1e-5, orders=None, test_xs_ref[i] = np.interp(test_energy, energy, ce_xs[i]) if log: - print(" energy: {:.3e} to {:.3e} eV ({} points)".format( - energy[0], energy[-1], ne)) - print(" error tolerance: rtol={}, atol={}".format(rtol, atol)) + print(f" energy: {energy[0]:.3e} to {energy[-1]:.3e} eV ({ne} points)") + print(f" error tolerance: rtol={rtol}, atol={atol}") # transform xs (sigma) and energy (E) to f (sigma*E) and s (sqrt(E)) to be # compatible with the multipole representation @@ -230,8 +229,8 @@ def _vectfit_xs(energy, ce_xs, mts, rtol=1e-3, atol=1e-5, orders=None, orders = list(range(lowest_order, highest_order + 1, 2)) if log: - print("Found {} peaks".format(n_peaks)) - print("Fitting orders from {} to {}".format(orders[0], orders[-1])) + print(f"Found {n_peaks} peaks") + print(f"Fitting orders from {orders[0]} to {orders[-1]}") # perform VF with increasing orders found_ideal = False @@ -239,7 +238,7 @@ def _vectfit_xs(energy, ce_xs, mts, rtol=1e-3, atol=1e-5, orders=None, best_quality = best_ratio = -np.inf for i, order in enumerate(orders): if log: - print("Order={}({}/{})".format(order, i, len(orders))) + print(f"Order={order}({i}/{len(orders)})") # initial guessed poles poles_r = np.linspace(s[0], s[-1], order//2) poles = poles_r + poles_r*0.01j @@ -249,7 +248,7 @@ def _vectfit_xs(energy, ce_xs, mts, rtol=1e-3, atol=1e-5, orders=None, # fitting iteration for i_vf in range(n_vf_iter): if log >= DETAILED_LOGGING: - print("VF iteration {}/{}".format(i_vf + 1, n_vf_iter)) + print(f"VF iteration {i_vf + 1}/{n_vf_iter}") # call vf poles, residues, cf, f_fit, rms = vf.vectfit(f, s, poles, weight) @@ -268,7 +267,7 @@ def _vectfit_xs(energy, ce_xs, mts, rtol=1e-3, atol=1e-5, orders=None, # re-calculate residues if poles changed if n_real_poles > 0: if log >= DETAILED_LOGGING: - print(" # real poles: {}".format(n_real_poles)) + print(f" # real poles: {n_real_poles}") new_poles, residues, cf, f_fit, rms = \ vf.vectfit(f, s, new_poles, weight, skip_pole=True) @@ -296,10 +295,10 @@ def _vectfit_xs(energy, ce_xs, mts, rtol=1e-3, atol=1e-5, orders=None, quality = -np.inf if log >= DETAILED_LOGGING: - print(" # poles: {}".format(new_poles.size)) - print(" Max relative error: {:.3f}%".format(maxre*100)) - print(" Satisfaction: {:.1f}%, {:.1f}%".format(ratio*100, ratio2*100)) - print(" Quality: {:.2f}".format(quality)) + print(f" # poles: {new_poles.size}") + print(f" Max relative error: {maxre * 100:.3f}%") + print(f" Satisfaction: {ratio * 100:.1f}%, {ratio2 * 100:.1f}%") + print(f" Quality: {quality:.2f}") if quality > best_quality: if log >= DETAILED_LOGGING: @@ -354,7 +353,7 @@ def _vectfit_xs(energy, ce_xs, mts, rtol=1e-3, atol=1e-5, orders=None, mp_residues = np.concatenate((best_residues[:, real_idx], best_residues[:, conj_idx]*2), axis=1)/1j if log: - print("Final number of poles: {}".format(mp_poles.size)) + print(f"Final number of poles: {mp_poles.size}") if path_out: if not os.path.exists(path_out): @@ -378,14 +377,14 @@ def _vectfit_xs(energy, ce_xs, mts, rtol=1e-3, atol=1e-5, orders=None, ax2.set_ylabel('relative error', color='r') ax2.tick_params('y', colors='r') - plt.title("MT {} vector fitted with {} poles".format(mt, mp_poles.size)) + plt.title(f"MT {mt} vector fitted with {mp_poles.size} poles") fig.tight_layout() fig_file = os.path.join(path_out, "{:.0f}-{:.0f}_MT{}.png".format( energy[0], energy[-1], mt)) plt.savefig(fig_file) plt.close() if log: - print("Saved figure: {}".format(fig_file)) + print(f"Saved figure: {fig_file}") return (mp_poles, mp_residues) @@ -423,7 +422,7 @@ def vectfit_nuclide(endf_file, njoy_error=5e-4, vf_pieces=None, # make 0K ACE data using njoy if log: - print("Running NJOY to get 0K point-wise data (error={})...".format(njoy_error)) + print(f"Running NJOY to get 0K point-wise data (error={njoy_error})...") nuc_ce = IncidentNeutron.from_njoy(endf_file, temperatures=[0.0], error=njoy_error, broadr=False, heatr=False, purr=False) @@ -477,9 +476,8 @@ def vectfit_nuclide(endf_file, njoy_error=5e-4, vf_pieces=None, mts = [2, 27] if log: - print(" MTs: {}".format(mts)) - print(" Energy range: {:.3e} to {:.3e} eV ({} points)".format( - E_min, E_max, n_points)) + print(f" MTs: {mts}") + print(f" Energy range: {E_min:.3e} to {E_max:.3e} eV ({n_points} points)") # ====================================================================== # PERFORM VECTOR FITTING @@ -500,7 +498,7 @@ def vectfit_nuclide(endf_file, njoy_error=5e-4, vf_pieces=None, # VF piece by piece for i_piece in range(vf_pieces): if log: - print("Vector fitting piece {}/{}...".format(i_piece + 1, vf_pieces)) + print(f"Vector fitting piece {i_piece + 1}/{vf_pieces}...") # start E of this piece e_bound = (sqrt(E_min) + piece_width*(i_piece-0.5))**2 if i_piece == 0 or sqrt(alpha*e_bound) < 4.0: @@ -534,12 +532,12 @@ def vectfit_nuclide(endf_file, njoy_error=5e-4, vf_pieces=None, if not os.path.exists(path_out): os.makedirs(path_out) if not mp_filename: - mp_filename = "{}_mp.pickle".format(nuc_ce.name) + mp_filename = f"{nuc_ce.name}_mp.pickle" mp_filename = os.path.join(path_out, mp_filename) with open(mp_filename, 'wb') as f: pickle.dump(mp_data, f) if log: - print("Dumped multipole data to file: {}".format(mp_filename)) + print(f"Dumped multipole data to file: {mp_filename}") return mp_data @@ -605,9 +603,8 @@ def _windowing(mp_data, n_cf, rtol=1e-3, atol=1e-5, n_win=None, spacing=None, if log: print("Windowing:") - print(" config: # windows={}, spacing={}, CF order={}".format( - n_win, spacing, n_cf)) - print(" error tolerance: rtol={}, atol={}".format(rtol, atol)) + print(f" config: # windows={n_win}, spacing={spacing}, CF order={n_cf}") + print(f" error tolerance: rtol={rtol}, atol={atol}") # sort poles (and residues) by the real component of the pole for ip in range(n_pieces): @@ -623,7 +620,7 @@ def _windowing(mp_data, n_cf, rtol=1e-3, atol=1e-5, n_win=None, spacing=None, win_data = [] for iw in range(n_win): if log >= DETAILED_LOGGING: - print("Processing window {}/{}...".format(iw + 1, n_win)) + print(f"Processing window {iw + 1}/{n_win}...") # inner window boundaries inbegin = sqrt(E_min) + spacing * iw @@ -658,7 +655,7 @@ def _windowing(mp_data, n_cf, rtol=1e-3, atol=1e-5, n_win=None, spacing=None, lp = rp = center_pole_ind while True: if log >= DETAILED_LOGGING: - print("Trying poles {} to {}".format(lp, rp)) + print(f"Trying poles {lp} to {rp}") # calculate the cross sections contributed by the windowed poles if rp > lp: @@ -1108,7 +1105,7 @@ class WindowedMultipole(EqualityMixin): for n_w in np.unique(np.linspace(n_win_min, n_win_max, 20, dtype=int)): for n_cf in range(10, 1, -1): if log: - print("Testing N_win={} N_cf={}".format(n_w, n_cf)) + print(f"Testing N_win={n_w} N_cf={n_cf}") # update arguments dictionary kwargs.update(n_win=n_w, n_cf=n_cf) diff --git a/openmc/data/neutron.py b/openmc/data/neutron.py index 94833d4b3..894be1871 100644 --- a/openmc/data/neutron.py +++ b/openmc/data/neutron.py @@ -122,10 +122,10 @@ class IncidentNeutron(EqualityMixin): if len(mts) > 0: return self._get_redundant_reaction(mt, mts) else: - raise KeyError('No reaction with MT={}.'.format(mt)) + raise KeyError(f'No reaction with MT={mt}.') def __repr__(self): - return "".format(self.name) + return f"" def __iter__(self): return iter(self.reactions.values()) @@ -231,7 +231,7 @@ class IncidentNeutron(EqualityMixin): @property def temperatures(self): - return ["{}K".format(int(round(kT / K_BOLTZMANN))) for kT in self.kTs] + return [f"{int(round(kT / K_BOLTZMANN))}K" for kT in self.kTs] @property def atomic_symbol(self): @@ -261,7 +261,7 @@ class IncidentNeutron(EqualityMixin): # Check if temprature already exists strT = data.temperatures[0] if strT in self.temperatures: - warn('Cross sections at T={} already exist.'.format(strT)) + warn(f'Cross sections at T={strT} already exist.') return # Check that name matches @@ -461,7 +461,7 @@ class IncidentNeutron(EqualityMixin): if not (photon_rx or rx.mt in keep_mts): continue - rx_group = rxs_group.create_group('reaction_{:03}'.format(rx.mt)) + rx_group = rxs_group.create_group(f'reaction_{rx.mt:03}') rx.to_hdf5(rx_group) # Write total nu data if available @@ -593,7 +593,7 @@ class IncidentNeutron(EqualityMixin): zaid, xs = ace.name.split('.') if not xs.endswith('c'): raise TypeError( - "{} is not a continuous-energy neutron ACE table.".format(ace)) + f"{ace} is not a continuous-energy neutron ACE table.") name, element, Z, mass_number, metastable = \ get_metadata(int(zaid), metastable_scheme) @@ -732,9 +732,9 @@ class IncidentNeutron(EqualityMixin): # Determine name element = ATOMIC_SYMBOL[atomic_number] if metastable > 0: - name = '{}{}_m{}'.format(element, mass_number, metastable) + name = f'{element}{mass_number}_m{metastable}' else: - name = '{}{}'.format(element, mass_number) + name = f'{element}{mass_number}' # Instantiate incident neutron data data = cls(name, atomic_number, mass_number, metastable, diff --git a/openmc/data/njoy.py b/openmc/data/njoy.py index 9c2055cc4..c1dcbab17 100644 --- a/openmc/data/njoy.py +++ b/openmc/data/njoy.py @@ -188,7 +188,7 @@ def run(commands, tapein, tapeout, input_filename=None, stdout=False, with tempfile.TemporaryDirectory() as tmpdir: # Copy evaluations to appropriates 'tapes' for tape_num, filename in tapein.items(): - tmpfilename = os.path.join(tmpdir, 'tape{}'.format(tape_num)) + tmpfilename = os.path.join(tmpdir, f'tape{tape_num}') shutil.copy(str(filename), tmpfilename) # Start up NJOY process @@ -216,7 +216,7 @@ def run(commands, tapein, tapeout, input_filename=None, stdout=False, # Copy output files back to original directory for tape_num, filename in tapeout.items(): - tmpfilename = os.path.join(tmpdir, 'tape{}'.format(tape_num)) + tmpfilename = os.path.join(tmpdir, f'tape{tape_num}') if os.path.isfile(tmpfilename): shutil.move(tmpfilename, str(filename)) @@ -317,7 +317,7 @@ def make_ace(filename, temperatures=None, acer=True, xsdir=None, else: output_dir = Path(output_dir) if not output_dir.is_dir(): - raise IOError("{} is not a directory".format(output_dir)) + raise IOError(f"{output_dir} is not a directory") ev = evaluation if evaluation is not None else endf.Evaluation(filename) mat = ev.material @@ -389,12 +389,12 @@ def make_ace(filename, temperatures=None, acer=True, xsdir=None, # Extend input with an ACER run for each temperature nace = nacer_in + 1 + 2*i ndir = nace + 1 - ext = '{:02}'.format(i + 1) + ext = f'{i + 1:02}' commands += _TEMPLATE_ACER.format(**locals()) # Indicate tapes to save for each ACER run - tapeout[nace] = output_dir / "ace_{:.1f}".format(temperature) - tapeout[ndir] = output_dir / "xsdir_{:.1f}".format(temperature) + tapeout[nace] = output_dir / f"ace_{temperature:.1f}" + tapeout[ndir] = output_dir / f"xsdir_{temperature:.1f}" commands += 'stop\n' run(commands, tapein, tapeout, **kwargs) @@ -404,7 +404,7 @@ def make_ace(filename, temperatures=None, acer=True, xsdir=None, with ace.open('w') as ace_file, xsdir.open('w') as xsdir_file: for temperature in temperatures: # Get contents of ACE file - text = (output_dir / "ace_{:.1f}".format(temperature)).read_text() + text = (output_dir / f"ace_{temperature:.1f}").read_text() # If the target is metastable, make sure that ZAID in the ACE # file reflects this by adding 400 @@ -417,13 +417,13 @@ def make_ace(filename, temperatures=None, acer=True, xsdir=None, ace_file.write(text) # Concatenate into destination xsdir file - xsdir_in = output_dir / "xsdir_{:.1f}".format(temperature) + xsdir_in = output_dir / f"xsdir_{temperature:.1f}" xsdir_file.write(xsdir_in.read_text()) # Remove ACE/xsdir files for each temperature for temperature in temperatures: - (output_dir / "ace_{:.1f}".format(temperature)).unlink() - (output_dir / "xsdir_{:.1f}".format(temperature)).unlink() + (output_dir / f"ace_{temperature:.1f}").unlink() + (output_dir / f"xsdir_{temperature:.1f}").unlink() def make_ace_thermal(filename, filename_thermal, temperatures=None, @@ -480,7 +480,7 @@ def make_ace_thermal(filename, filename_thermal, temperatures=None, else: output_dir = Path(output_dir) if not output_dir.is_dir(): - raise IOError("{} is not a directory".format(output_dir)) + raise IOError(f"{output_dir} is not a directory") ev = evaluation if evaluation is not None else endf.Evaluation(filename) mat = ev.material @@ -581,12 +581,12 @@ def make_ace_thermal(filename, filename_thermal, temperatures=None, # Extend input with an ACER run for each temperature nace = nthermal_acer_in + 1 + 2*i ndir = nace + 1 - ext = '{:02}'.format(i + 1) + ext = f'{i + 1:02}' commands += _THERMAL_TEMPLATE_ACER.format(**locals()) # Indicate tapes to save for each ACER run - tapeout[nace] = output_dir / "ace_{:.1f}".format(temperature) - tapeout[ndir] = output_dir / "xsdir_{:.1f}".format(temperature) + tapeout[nace] = output_dir / f"ace_{temperature:.1f}" + tapeout[ndir] = output_dir / f"xsdir_{temperature:.1f}" commands += 'stop\n' run(commands, tapein, tapeout, **kwargs) @@ -595,13 +595,13 @@ def make_ace_thermal(filename, filename_thermal, temperatures=None, with ace.open('w') as ace_file, xsdir.open('w') as xsdir_file: # Concatenate ACE and xsdir files together for temperature in temperatures: - ace_in = output_dir / "ace_{:.1f}".format(temperature) + ace_in = output_dir / f"ace_{temperature:.1f}" ace_file.write(ace_in.read_text()) - xsdir_in = output_dir / "xsdir_{:.1f}".format(temperature) + xsdir_in = output_dir / f"xsdir_{temperature:.1f}" xsdir_file.write(xsdir_in.read_text()) # Remove ACE/xsdir files for each temperature for temperature in temperatures: - (output_dir / "ace_{:.1f}".format(temperature)).unlink() - (output_dir / "xsdir_{:.1f}".format(temperature)).unlink() + (output_dir / f"ace_{temperature:.1f}").unlink() + (output_dir / f"xsdir_{temperature:.1f}").unlink() diff --git a/openmc/data/photon.py b/openmc/data/photon.py index ba4bf5c88..c9d51f062 100644 --- a/openmc/data/photon.py +++ b/openmc/data/photon.py @@ -451,10 +451,10 @@ class IncidentPhoton(EqualityMixin): if mt in self.reactions: return self.reactions[mt] else: - raise KeyError('No reaction with MT={}.'.format(mt)) + raise KeyError(f'No reaction with MT={mt}.') def __repr__(self): - return "".format(self.name) + return f"" def __iter__(self): return iter(self.reactions.values()) @@ -508,7 +508,7 @@ class IncidentPhoton(EqualityMixin): # Get atomic number based on name of ACE table zaid, xs = ace.name.split('.') if not xs.endswith('p'): - raise TypeError("{} is not a photoatomic transport ACE table.".format(ace)) + raise TypeError(f"{ace} is not a photoatomic transport ACE table.") Z = get_metadata(int(zaid))[2] # Read each reaction @@ -638,7 +638,7 @@ class IncidentPhoton(EqualityMixin): with h5py.File(filename, 'r') as f: _COMPTON_PROFILES['pz'] = f['pz'][()] for i in range(1, 101): - group = f['{:03}'.format(i)] + group = f[f'{i:03}'] num_electrons = group['num_electrons'][()] binding_energy = group['binding_energy'][()]*EV_PER_MEV J = group['J'][()] @@ -713,7 +713,7 @@ class IncidentPhoton(EqualityMixin): # Check for necessary reactions for mt in (502, 504, 522): - assert mt in data, "Reaction {} not found".format(mt) + assert mt in data, f"Reaction {mt} not found" # Read atomic relaxation data.atomic_relaxation = AtomicRelaxation.from_hdf5(group['subshells']) @@ -836,7 +836,7 @@ class IncidentPhoton(EqualityMixin): filename = os.path.join(os.path.dirname(__file__), 'density_effect.h5') with h5py.File(filename, 'r') as f: for i in range(1, 101): - group = f['{:03}'.format(i)] + group = f[f'{i:03}'] _BREMSSTRAHLUNG[i] = { 'I': group.attrs['I'], 'num_electrons': group['num_electrons'][()], @@ -924,10 +924,9 @@ class PhotonReaction(EqualityMixin): def __repr__(self): if self.mt in _REACTION_NAME: - return "".format( - self.mt, _REACTION_NAME[self.mt][0]) + return f"" else: - return "".format(self.mt) + return f"" @property def anomalous_real(self): diff --git a/openmc/data/product.py b/openmc/data/product.py index 4a9b07aee..88c83b81f 100644 --- a/openmc/data/product.py +++ b/openmc/data/product.py @@ -135,7 +135,7 @@ class Product(EqualityMixin): # Write applicability/distribution group.attrs['n_distribution'] = len(self.distribution) for i, d in enumerate(self.distribution): - dgroup = group.create_group('distribution_{}'.format(i)) + dgroup = group.create_group(f'distribution_{i}') if self.applicability: self.applicability[i].to_hdf5(dgroup, 'applicability') d.to_hdf5(dgroup) @@ -170,7 +170,7 @@ class Product(EqualityMixin): distribution = [] applicability = [] for i in range(n_distribution): - dgroup = group['distribution_{}'.format(i)] + dgroup = group[f'distribution_{i}'] if 'applicability' in dgroup: applicability.append(Tabulated1D.from_hdf5( dgroup['applicability'])) diff --git a/openmc/data/reaction.py b/openmc/data/reaction.py index 8ba478782..8f90d2de4 100644 --- a/openmc/data/reaction.py +++ b/openmc/data/reaction.py @@ -56,13 +56,13 @@ REACTION_NAME = {1: '(n,total)', 2: '(n,elastic)', 4: '(n,level)', 301: 'heating', 444: 'damage-energy', 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: '(n,n{})'.format(i - 50) for i in range(51, 91)}) -REACTION_NAME.update({i: '(n,p{})'.format(i - 600) for i in range(600, 649)}) -REACTION_NAME.update({i: '(n,d{})'.format(i - 650) for i in range(650, 699)}) -REACTION_NAME.update({i: '(n,t{})'.format(i - 700) for i in range(700, 749)}) -REACTION_NAME.update({i: '(n,3He{})'.format(i - 750) for i in range(750, 799)}) -REACTION_NAME.update({i: '(n,a{})'.format(i - 800) for i in range(800, 849)}) -REACTION_NAME.update({i: '(n,2n{})'.format(i - 875) for i in range(875, 891)}) +REACTION_NAME.update({i: f'(n,n{i - 50})' for i in range(51, 91)}) +REACTION_NAME.update({i: f'(n,p{i - 600})' for i in range(600, 649)}) +REACTION_NAME.update({i: f'(n,d{i - 650})' for i in range(650, 699)}) +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_MT = {name: mt for mt, name in REACTION_NAME.items()} REACTION_MT['fission'] = 18 @@ -119,7 +119,7 @@ def _get_products(ev, mt): p = Product('electron') else: Z, A = divmod(za, 1000) - p = Product('{}{}'.format(ATOMIC_SYMBOL[Z], A)) + p = Product(f'{ATOMIC_SYMBOL[Z]}{A}') p.yield_ = yield_ @@ -557,9 +557,9 @@ def _get_activation_products(ev, rx): # Get GNDS name for product symbol = ATOMIC_SYMBOL[Z] if excited_state > 0: - name = '{}{}_e{}'.format(symbol, A, excited_state) + name = f'{symbol}{A}_e{excited_state}' else: - name = '{}{}'.format(symbol, A) + name = f'{symbol}{A}' p = Product(name) if mf == 9: @@ -656,8 +656,7 @@ def _get_photon_products_ace(ace, rx): photon.yield_ = Tabulated1D(energy, yield_) else: - raise ValueError("MFTYPE must be 12, 13, 16. Got {0}".format( - mftype)) + raise ValueError(f"MFTYPE must be 12, 13, 16. Got {mftype}") # ================================================================== # Photon energy distribution @@ -846,9 +845,9 @@ class Reaction(EqualityMixin): def __repr__(self): if self.mt in REACTION_NAME: - return "".format(self.mt, REACTION_NAME[self.mt]) + return f"" else: - return "".format(self.mt) + return f"" @property def center_of_mass(self): @@ -933,7 +932,7 @@ class Reaction(EqualityMixin): threshold_idx = getattr(self.xs[T], '_threshold_idx', 0) dset.attrs['threshold_idx'] = threshold_idx for i, p in enumerate(self.products): - pgroup = group.create_group('product_{}'.format(i)) + pgroup = group.create_group(f'product_{i}') p.to_hdf5(pgroup) @classmethod @@ -985,7 +984,7 @@ class Reaction(EqualityMixin): # Read reaction products for i in range(n_product): - pgroup = group['product_{}'.format(i)] + pgroup = group[f'product_{i}'] rx.products.append(Product.from_hdf5(pgroup)) return rx diff --git a/openmc/data/resonance.py b/openmc/data/resonance.py index 340d73a18..31e230df5 100644 --- a/openmc/data/resonance.py +++ b/openmc/data/resonance.py @@ -830,7 +830,7 @@ class RMatrixLimited(ResonanceRange): elif mt == 102: columns.append('captureWidth') else: - columns.append('width (MT={})'.format(mt)) + columns.append(f'width (MT={mt})') # Create Pandas dataframe with resonance parameters parameters = pd.DataFrame.from_records(records, columns=columns) @@ -896,7 +896,7 @@ class SpinGroup: self.parameters = parameters def __repr__(self): - return ''.format(self.spin, self.parity) + return f'' class Unresolved(ResonanceRange): diff --git a/openmc/data/thermal.py b/openmc/data/thermal.py index f8dd43ed9..f5841d9c9 100644 --- a/openmc/data/thermal.py +++ b/openmc/data/thermal.py @@ -90,7 +90,7 @@ _THERMAL_NAMES = { def _temperature_str(T): # round() normally returns an int when called with a single argument, but # numpy floats overload rounding to return another float - return "{}K".format(int(round(T))) + return f"{int(round(T))}K" def get_thermal_name(name): @@ -439,7 +439,7 @@ class ThermalScattering(EqualityMixin): def __repr__(self): if hasattr(self, 'name'): - return "".format(self.name) + return f"" else: return "" @@ -506,7 +506,7 @@ class ThermalScattering(EqualityMixin): # Check if temprature already exists strT = data.temperatures[0] if strT in self.temperatures: - warn('S(a,b) data at T={} already exists.'.format(strT)) + warn(f'S(a,b) data at T={strT} already exists.') return # Check that name matches @@ -614,7 +614,7 @@ class ThermalScattering(EqualityMixin): # Get new name that is GND-consistent ace_name, xs = ace.name.split('.') if not xs.endswith('t'): - raise TypeError("{} is not a thermal scattering ACE table.".format(ace)) + raise TypeError(f"{ace} is not a thermal scattering ACE table.") if name is None: name = get_thermal_name(ace_name) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index fa07ad12a..49bb3e6c7 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -647,7 +647,7 @@ class Integrator(ABC): days = watt_days_per_kg * kilograms / rate seconds.append(days*_SECONDS_PER_DAY) else: - raise ValueError("Invalid timestep unit '{}'".format(unit)) + raise ValueError(f"Invalid timestep unit '{unit}'") self.timesteps = np.asarray(seconds) self.source_rates = np.asarray(source_rates) @@ -664,8 +664,7 @@ class Integrator(ABC): self._solver = CRAM16 else: raise ValueError( - "Solver {} not understood. Expected 'cram48' or " - "'cram16'".format(solver)) + f"Solver {solver} not understood. Expected 'cram48' or 'cram16'") else: self.solver = solver @@ -677,14 +676,13 @@ class Integrator(ABC): def solver(self, func): if not isinstance(func, Callable): raise TypeError( - "Solver must be callable, not {}".format(type(func))) + f"Solver must be callable, not {type(func)}") try: sig = signature(func) except ValueError: # Guard against callables that aren't introspectable, e.g. # fortran functions wrapped by F2PY - warn("Could not determine arguments to {}. Proceeding " - "anyways".format(func)) + warn(f"Could not determine arguments to {func}. Proceeding anyways") self._solver = func return @@ -696,8 +694,7 @@ class Integrator(ABC): for ix, param in enumerate(sig.parameters.values()): if param.kind in {param.KEYWORD_ONLY, param.VAR_KEYWORD}: raise ValueError( - "Keyword arguments like {} at position {} are not " - "allowed".format(ix, param)) + f"Keyword arguments like {ix} at position {param} are not allowed") self._solver = func diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index e15b127d0..1d3834980 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -141,7 +141,7 @@ def replace_missing(product, decay_data): # First check if ground state is available if state: - product = '{}{}'.format(symbol, A) + product = f'{symbol}{A}' # Find isotope with longest half-life half_life = 0.0 @@ -172,7 +172,7 @@ def replace_missing(product, decay_data): Z += 1 else: Z -= 1 - product = '{}{}'.format(openmc.data.ATOMIC_SYMBOL[Z], A) + product = f'{openmc.data.ATOMIC_SYMBOL[Z]}{A}' return product @@ -417,7 +417,7 @@ class Chain: if mts & reactions_available: A = data.nuclide['mass_number'] + delta_A Z = data.nuclide['atomic_number'] + delta_Z - daughter = '{}{}'.format(openmc.data.ATOMIC_SYMBOL[Z], A) + daughter = f'{openmc.data.ATOMIC_SYMBOL[Z]}{A}' if daughter not in decay_data: daughter = replace_missing(daughter, decay_data) @@ -483,7 +483,7 @@ class Chain: if missing_daughter: print('The following decay modes have daughters with no decay data:') for mode in missing_daughter: - print(' {}'.format(mode)) + print(f' {mode}') print('') if missing_rx_product: @@ -495,7 +495,7 @@ class Chain: if missing_fpy: print('The following fissionable nuclides have no fission product yields:') for parent, replacement in missing_fpy: - print(' {}, replaced with {}'.format(parent, replacement)) + print(f' {parent}, replaced with {replacement}') print('') if missing_fp: @@ -873,8 +873,7 @@ class Chain: if len(indexes) == 0: if strict: raise AttributeError( - "Nuclide {} does not have {} reactions".format( - parent, reaction)) + f"Nuclide {parent} does not have {reaction} reactions") missing_reaction.add(parent) continue @@ -896,8 +895,7 @@ class Chain: if len(rxn_ix_map) == 0: raise IndexError( - "No {} reactions found in this {}".format( - reaction, self.__class__.__name__)) + f"No {reaction} reactions found in this {self.__class__.__name__}") if len(missing_parents) > 0: warn("The following nuclides were not found in {}: {}".format( @@ -908,14 +906,14 @@ class Chain: "{}".format(reaction, ", ".join(sorted(missing_reaction)))) if len(missing_products) > 0: - tail = ("{} -> {}".format(k, v) + tail = (f"{k} -> {v}" for k, v in sorted(missing_products.items())) warn("The following products were not found in the {} and " "parents were unmodified: \n{}".format( self.__class__.__name__, ", ".join(tail))) if len(bad_sums) > 0: - tail = ("{}: {:5.3f}".format(k, s) + tail = (f"{k}: {s:5.3f}" for k, s in sorted(bad_sums.items())) warn("The following parent nuclides were given {} branch ratios " "with a sum outside tolerance of 1 +/- {:5.3e}:\n{}".format( diff --git a/openmc/deplete/coupled_operator.py b/openmc/deplete/coupled_operator.py index 8d43e2b2c..acdcf467c 100644 --- a/openmc/deplete/coupled_operator.py +++ b/openmc/deplete/coupled_operator.py @@ -518,7 +518,7 @@ class CoupledOperator(OpenMCOperator): """ openmc.lib.statepoint_write( - "openmc_simulation_n{}.h5".format(step), + f"openmc_simulation_n{step}.h5", write_source=False) def finalize(self): diff --git a/openmc/deplete/nuclide.py b/openmc/deplete/nuclide.py index d055c2e37..60e3e5317 100644 --- a/openmc/deplete/nuclide.py +++ b/openmc/deplete/nuclide.py @@ -275,7 +275,7 @@ class Nuclide: if parent is not None: assert root is not None fpy_elem = root.find( - './/nuclide[@name="{}"]/neutron_fission_yields'.format(parent) + f'.//nuclide[@name="{parent}"]/neutron_fission_yields' ) if fpy_elem is None: raise ValueError( @@ -413,7 +413,7 @@ class Nuclide: continue msg = msg_func( name=self.name, actual=sum_br, expected=1.0, tol=tolerance, - prop="{} reaction branch ratios".format(rxn_type)) + prop=f"{rxn_type} reaction branch ratios") if strict: raise ValueError(msg) elif quiet: @@ -430,7 +430,7 @@ class Nuclide: msg = msg_func( name=self.name, actual=sum_yield, expected=2.0, tol=tolerance, - prop="fission yields (E = {:7.4e} eV)".format(energy)) + prop=f"fission yields (E = {energy:7.4e} eV)") if strict: raise ValueError(msg) elif quiet: @@ -695,8 +695,7 @@ class FissionYield(Mapping): return self * scalar def __repr__(self): - return "<{} containing {} products and yields>".format( - self.__class__.__name__, len(self)) + return f"<{self.__class__.__name__} containing {len(self)} products and yields>" def __deepcopy__(self, memo): result = FissionYield(self.products, self.yields.copy()) diff --git a/openmc/lattice.py b/openmc/lattice.py index d281f6e76..40b97f6cf 100644 --- a/openmc/lattice.py +++ b/openmc/lattice.py @@ -1851,7 +1851,7 @@ class HexLattice(Lattice): largest_index = 6*(num_rings - 1) n_digits_index = len(str(largest_index)) n_digits_ring = len(str(num_rings - 1)) - str_form = '({{:{}}},{{:{}}})'.format(n_digits_ring, n_digits_index) + str_form = f'({{:{n_digits_ring}}},{{:{n_digits_index}}})' pad = ' '*(n_digits_index + n_digits_ring + 3) # Initialize the list for each row. @@ -1956,7 +1956,7 @@ class HexLattice(Lattice): largest_index = 6*(num_rings - 1) n_digits_index = len(str(largest_index)) n_digits_ring = len(str(num_rings - 1)) - str_form = '({{:{}}},{{:{}}})'.format(n_digits_ring, n_digits_index) + str_form = f'({{:{n_digits_ring}}},{{:{n_digits_index}}})' pad = ' '*(n_digits_index + n_digits_ring + 3) # Initialize the list for each row. diff --git a/openmc/lib/__init__.py b/openmc/lib/__init__.py index dc5bd7d9b..42d779441 100644 --- a/openmc/lib/__init__.py +++ b/openmc/lib/__init__.py @@ -28,7 +28,7 @@ else: if os.environ.get('READTHEDOCS', None) != 'True': # Open shared library _filename = pkg_resources.resource_filename( - __name__, 'libopenmc.{}'.format(_suffix)) + __name__, f'libopenmc.{_suffix}') _dll = CDLL(_filename) else: # For documentation builds, we don't actually have the shared library diff --git a/openmc/lib/cell.py b/openmc/lib/cell.py index 807fe73c7..971a24cba 100644 --- a/openmc/lib/cell.py +++ b/openmc/lib/cell.py @@ -268,7 +268,7 @@ class Cell(_FortranObjectWithID): return rotation_data[9:] else: raise ValueError( - 'Invalid size of rotation matrix: {}'.format(rot_size)) + f'Invalid size of rotation matrix: {rot_size}') @rotation.setter def rotation(self, rotation_data): diff --git a/openmc/lib/core.py b/openmc/lib/core.py index d8e0bfdb5..a9a549fa0 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -629,7 +629,7 @@ class _DLLGlobal: class _FortranObject: def __repr__(self): - return "<{}(index={})>".format(type(self).__name__, self._index) + return f"<{type(self).__name__}(index={self._index})>" class _FortranObjectWithID(_FortranObject): @@ -641,7 +641,7 @@ class _FortranObjectWithID(_FortranObject): self.id def __repr__(self): - return "<{}(id={})>".format(type(self).__name__, self.id) + return f"<{type(self).__name__}(id={self.id})>" @contextmanager diff --git a/openmc/lib/error.py b/openmc/lib/error.py index 89e7b6e39..dbe08e1ef 100644 --- a/openmc/lib/error.py +++ b/openmc/lib/error.py @@ -37,5 +37,5 @@ def _error_handler(err, func, args): warn(msg) elif err < 0: if not msg: - msg = "Unknown error encountered (code {}).".format(err) + msg = f"Unknown error encountered (code {err})." raise exc.OpenMCError(msg) diff --git a/openmc/lib/plot.py b/openmc/lib/plot.py index 9f294a42c..d98636676 100644 --- a/openmc/lib/plot.py +++ b/openmc/lib/plot.py @@ -31,7 +31,7 @@ class _Position(Structure): elif idx == 2: return self.z else: - raise IndexError("{} index is invalid for _Position".format(idx)) + raise IndexError(f"{idx} index is invalid for _Position") def __setitem__(self, idx, val): if idx == 0: @@ -41,10 +41,10 @@ class _Position(Structure): elif idx == 2: self.z = val else: - raise IndexError("{} index is invalid for _Position".format(idx)) + raise IndexError(f"{idx} index is invalid for _Position") def __repr__(self): - return "({}, {}, {})".format(self.x, self.y, self.z) + return f"({self.x}, {self.y}, {self.z})" class _PlotBase(Structure): @@ -127,7 +127,7 @@ class _PlotBase(Structure): elif self.basis_ == 3: return 'yz' - raise ValueError("Plot basis {} is invalid".format(self.basis_)) + raise ValueError(f"Plot basis {self.basis_} is invalid") @basis.setter def basis(self, basis): @@ -135,7 +135,7 @@ class _PlotBase(Structure): valid_bases = ('xy', 'xz', 'yz') basis = basis.lower() if basis not in valid_bases: - raise ValueError("{} is not a valid plot basis.".format(basis)) + raise ValueError(f"{basis} is not a valid plot basis.") if basis == 'xy': self.basis_ = 1 @@ -148,12 +148,11 @@ class _PlotBase(Structure): if isinstance(basis, int): valid_bases = (1, 2, 3) if basis not in valid_bases: - raise ValueError("{} is not a valid plot basis.".format(basis)) + raise ValueError(f"{basis} is not a valid plot basis.") self.basis_ = basis return - raise ValueError("{} of type {} is an" - " invalid plot basis".format(basis, type(basis))) + raise ValueError(f"{basis} of type {type(basis)} is an invalid plot basis") @property def h_res(self): @@ -199,14 +198,14 @@ class _PlotBase(Structure): out_str = ["-----", "Plot:", "-----", - "Origin: {}".format(self.origin), - "Width: {}".format(self.width), - "Height: {}".format(self.height), - "Basis: {}".format(self.basis), - "HRes: {}".format(self.h_res), - "VRes: {}".format(self.v_res), - "Color Overlaps: {}".format(self.color_overlaps), - "Level: {}".format(self.level)] + f"Origin: {self.origin}", + f"Width: {self.width}", + f"Height: {self.height}", + f"Basis: {self.basis}", + f"HRes: {self.h_res}", + f"VRes: {self.v_res}", + f"Color Overlaps: {self.color_overlaps}", + f"Level: {self.level}"] return '\n'.join(out_str) diff --git a/openmc/lib/settings.py b/openmc/lib/settings.py index 6a7e5aa12..062670ef8 100644 --- a/openmc/lib/settings.py +++ b/openmc/lib/settings.py @@ -53,7 +53,7 @@ class _Settings: current_idx.value = idx break else: - raise ValueError('Invalid run mode: {}'.format(mode)) + raise ValueError(f'Invalid run mode: {mode}') @property def path_statepoint(self): diff --git a/openmc/material.py b/openmc/material.py index c31862a18..6edc37216 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -152,7 +152,7 @@ class Material(IDManagerMixin): for nuclide, percent, percent_type in self._nuclides: string += '{: <16}'.format('\t{}'.format(nuclide)) - string += '=\t{: <12} [{}]\n'.format(percent, percent_type) + string += f'=\t{percent: <12} [{percent_type}]\n' if self._macroscopic is not None: string += '{: <16}\n'.format('\tMacroscopic Data') @@ -469,8 +469,7 @@ class Material(IDManagerMixin): raise ValueError('No volume information found for material ID={}.' .format(self.id)) else: - raise ValueError('No volume information found for material ID={}.' - .format(self.id)) + raise ValueError(f'No volume information found for material ID={self.id}.') def set_density(self, units: str, density: Optional[float] = None): """Set the density of the material @@ -500,7 +499,7 @@ class Material(IDManagerMixin): '"sum" unit'.format(self.id) raise ValueError(msg) - cv.check_type('the density for Material ID="{}"'.format(self.id), + cv.check_type(f'the density for Material ID="{self.id}"', density, Real) self._density = density @@ -743,20 +742,18 @@ class Material(IDManagerMixin): el = element.lower() element = openmc.data.ELEMENT_SYMBOL.get(el) if element is None: - msg = 'Element name "{}" not recognised'.format(el) + msg = f'Element name "{el}" not recognised' raise ValueError(msg) else: if element[0].islower(): - msg = 'Element name "{}" should start with an uppercase ' \ - 'letter'.format(element) + msg = f'Element name "{element}" should start with an uppercase letter' raise ValueError(msg) if len(element) == 2 and element[1].isupper(): - msg = 'Element name "{}" should end with a lowercase ' \ - 'letter'.format(element) + msg = f'Element name "{element}" should end with a lowercase letter' raise ValueError(msg) # skips the first entry of ATOMIC_SYMBOL which is n for neutron if element not in list(openmc.data.ATOMIC_SYMBOL.values())[1:]: - msg = 'Element name "{}" not recognised'.format(element) + msg = f'Element name "{element}" not recognised' raise ValueError(msg) if self._macroscopic is not None: @@ -847,8 +844,7 @@ class Material(IDManagerMixin): for token in row: if token.isalpha(): if token == "n" or token not in openmc.data.ATOMIC_NUMBER: - msg = 'Formula entry {} not an element symbol.' \ - .format(token) + msg = f'Formula entry {token} not an element symbol.' raise ValueError(msg) elif token not in ['(', ')', ''] and not token.isdigit(): msg = 'Formula must be made from a sequence of ' \ @@ -1373,8 +1369,7 @@ class Material(IDManagerMixin): subelement.set("value", str(self._density)) subelement.set("units", self._density_units) else: - raise ValueError('Density has not been set for material {}!' - .format(self.id)) + raise ValueError(f'Density has not been set for material {self.id}!') if self._macroscopic is None: # Create nuclide XML subelements @@ -1480,7 +1475,7 @@ class Material(IDManagerMixin): # Create the new material with the desired name if name is None: - name = '-'.join(['{}({})'.format(m.name, f) for m, f in + name = '-'.join([f'{m.name}({f})' for m, f in zip(materials, fracs)]) new_mat = openmc.Material(name=name) diff --git a/openmc/mgxs/library.py b/openmc/mgxs/library.py index 2fa2e0f05..12a4630bd 100644 --- a/openmc/mgxs/library.py +++ b/openmc/mgxs/library.py @@ -685,7 +685,7 @@ class Library: # Check that requested domain is included in library if mgxs_type not in self.mgxs_types: - msg = 'Unable to find MGXS type "{0}"'.format(mgxs_type) + msg = f'Unable to find MGXS type "{mgxs_type}"' raise ValueError(msg) return self.all_mgxs[domain_id][mgxs_type] @@ -901,7 +901,7 @@ class Library: if not os.path.exists(directory): os.makedirs(directory) - full_filename = os.path.join(directory, '{}.pkl'.format(filename)) + full_filename = os.path.join(directory, f'{filename}.pkl') full_filename = full_filename.replace(' ', '-') # Load and return pickled Library object diff --git a/openmc/mgxs/mdgxs.py b/openmc/mgxs/mdgxs.py index 45c559a22..b95a4fbc0 100644 --- a/openmc/mgxs/mdgxs.py +++ b/openmc/mgxs/mdgxs.py @@ -612,7 +612,7 @@ class MDGXS(MGXS): string += '{0: <16}=\t{1}\n'.format('\tDomain ID', self.domain.id) # Generate the header for an individual XS - xs_header = '\tCross Sections [{0}]:'.format(self.get_units(xs_type)) + xs_header = f'\tCross Sections [{self.get_units(xs_type)}]:' # If cross section data has not been computed, only print string header if self.tallies is None: @@ -641,7 +641,7 @@ class MDGXS(MGXS): string += '{0: <16}=\t{1}\n'.format('\tNuclide', nuclide) # Add the cross section header - string += '{0: <16}\n'.format(xs_header) + string += f'{xs_header: <16}\n' for delayed_group in self.delayed_groups: @@ -875,7 +875,7 @@ class MDGXS(MGXS): # Sort the dataframe by domain type id (e.g., distribcell id) and # energy groups such that data is from fast to thermal if self.domain_type == 'mesh': - mesh_str = 'mesh {0}'.format(self.domain.id) + mesh_str = f'mesh {self.domain.id}' df.sort_values(by=[(mesh_str, 'x'), (mesh_str, 'y'), (mesh_str, 'z')] + columns, inplace=True) else: @@ -2496,7 +2496,7 @@ class MatrixMDGXS(MDGXS): string += '{0: <16}=\t{1}\n'.format('\tDomain ID', self.domain.id) # Generate the header for an individual XS - xs_header = '\tCross Sections [{0}]:'.format(self.get_units(xs_type)) + xs_header = f'\tCross Sections [{self.get_units(xs_type)}]:' # If cross section data has not been computed, only print string header if self.tallies is None: @@ -2532,7 +2532,7 @@ class MatrixMDGXS(MDGXS): string += '{: <16}=\t{}\n'.format('\tNuclide', nuclide) # Build header for cross section type - string += '{: <16}\n'.format(xs_header) + string += f'{xs_header: <16}\n' if self.delayed_groups is not None: diff --git a/openmc/mgxs/mgxs.py b/openmc/mgxs/mgxs.py index 2eb7f2756..588b6f64e 100644 --- a/openmc/mgxs/mgxs.py +++ b/openmc/mgxs/mgxs.py @@ -1427,7 +1427,7 @@ class MGXS: filter_bins=subdomains) avg_xs.tallies[tally_type] = tally_avg - avg_xs._domain_type = 'sum({0})'.format(self.domain_type) + avg_xs._domain_type = f'sum({self.domain_type})' avg_xs.sparse = self.sparse return avg_xs @@ -1478,7 +1478,7 @@ class MGXS: # Clone this MGXS to initialize the homogenized version homogenized_mgxs = copy.deepcopy(self) homogenized_mgxs._derived = True - name = 'hom({}, '.format(self.domain.name) + name = f'hom({self.domain.name}, ' # Get the domain filter filter_type = _DOMAIN_TO_FILTER[self.domain_type] @@ -1505,7 +1505,7 @@ class MGXS: denom_tally += other_denom_tally # Update the name for the homogenzied MGXS - name += '{}, '.format(mgxs.domain.name) + name += f'{mgxs.domain.name}, ' # Set the properties of the homogenized MGXS homogenized_mgxs._rxn_rate_tally = rxn_rate_tally @@ -1745,7 +1745,7 @@ class MGXS: string += '{0: <16}=\t{1}\n'.format('\tDomain ID', self.domain.id) # Generate the header for an individual XS - xs_header = '\tCross Sections [{0}]:'.format(self.get_units(xs_type)) + xs_header = f'\tCross Sections [{self.get_units(xs_type)}]:' # If cross section data has not been computed, only print string header if self.tallies is None: @@ -1773,7 +1773,7 @@ class MGXS: string += '{0: <16}=\t{1}\n'.format('\tNuclide', nuclide) # Build header for cross section type - string += '{0: <16}\n'.format(xs_header) + string += f'{xs_header: <16}\n' template = '{0: <12}Group {1} [{2: <10} - {3: <10}eV]:\t' average_xs = self.get_xs(nuclides=[nuclide], @@ -2131,7 +2131,7 @@ class MGXS: # Sort the dataframe by domain type id (e.g., distribcell id) and # energy groups such that data is from fast to thermal if self.domain_type == 'mesh': - mesh_str = 'mesh {0}'.format(self.domain.id) + mesh_str = f'mesh {self.domain.id}' df.sort_values(by=[(mesh_str, 'x'), (mesh_str, 'y'), (mesh_str, 'z')] + columns, inplace=True) else: @@ -2472,7 +2472,7 @@ class MatrixMGXS(MGXS): string += '{0: <16}=\t{1}\n'.format('\tDomain ID', self.domain.id) # Generate the header for an individual XS - xs_header = '\tCross Sections [{0}]:'.format(self.get_units(xs_type)) + xs_header = f'\tCross Sections [{self.get_units(xs_type)}]:' # If cross section data has not been computed, only print string header if self.tallies is None: @@ -2508,7 +2508,7 @@ class MatrixMGXS(MGXS): string += '{0: <16}=\t{1}\n'.format('\tNuclide', nuclide) # Build header for cross section type - string += '{0: <16}\n'.format(xs_header) + string += f'{xs_header: <16}\n' template = '{0: <12}Group {1} -> Group {2}:\t\t' average_xs = self.get_xs(nuclides=[nuclide], @@ -4476,7 +4476,7 @@ class ScatterMatrixXS(MatrixMGXS): slice_xs.legendre_order = legendre_order # Slice the scattering tally - filter_bins = [tuple(['P{}'.format(i) + filter_bins = [tuple([f'P{i}' for i in range(self.legendre_order + 1)])] slice_xs.tallies[self.rxn_type] = \ slice_xs.tallies[self.rxn_type].get_slice( @@ -4613,7 +4613,7 @@ class ScatterMatrixXS(MatrixMGXS): cv.check_less_than( 'moment', moment, self.legendre_order, equality=True) filters.append(openmc.LegendreFilter) - filter_bins.append(('P{}'.format(moment),)) + filter_bins.append((f'P{moment}',)) num_angle_bins = 1 else: num_angle_bins = self.legendre_order + 1 @@ -4804,7 +4804,7 @@ class ScatterMatrixXS(MatrixMGXS): cv.check_value('xs_type', xs_type, ['macro', 'micro']) if self.correction != 'P0' and self.scatter_format == SCATTER_LEGENDRE: - rxn_type = '{0} (P{1})'.format(self.mgxs_type, moment) + rxn_type = f'{self.mgxs_type} (P{moment})' else: rxn_type = self.mgxs_type @@ -4815,7 +4815,7 @@ class ScatterMatrixXS(MatrixMGXS): string += '{0: <16}=\t{1}\n'.format('\tDomain ID', self.domain.id) # Generate the header for an individual XS - xs_header = '\tCross Sections [{0}]:'.format(self.get_units(xs_type)) + xs_header = f'\tCross Sections [{self.get_units(xs_type)}]:' # If cross section data has not been computed, only print string header if self.tallies is None: @@ -4851,7 +4851,7 @@ class ScatterMatrixXS(MatrixMGXS): string += '{0: <16}=\t{1}\n'.format('\tNuclide', nuclide) # Build header for cross section type - string += '{0: <16}\n'.format(xs_header) + string += f'{xs_header: <16}\n' average_xs = self.get_xs(nuclides=[nuclide], subdomains=[subdomain], @@ -4903,8 +4903,7 @@ class ScatterMatrixXS(MatrixMGXS): for azi in range(len(azi_bins) - 1): azi_low, azi_high = azi_bins[azi: azi + 2] string += \ - '\t\tPolar Angle: [{0:5f} - {1:5f}]'.format( - pol_low, pol_high) + \ + f'\t\tPolar Angle: [{pol_low:5f} - {pol_high:5f}]' + \ '\tAzimuthal Angle: [{0:5f} - {1:5f}]'.format( azi_low, azi_high) + '\n' string += print_groups_and_histogram( @@ -6226,7 +6225,7 @@ class MeshSurfaceMGXS(MGXS): if 'group out' in df: df = df[df['group out'].isin(groups)] - mesh_str = 'mesh {0}'.format(self.domain.id) + mesh_str = f'mesh {self.domain.id}' col_key = (mesh_str, 'surf') surfaces = df.pop(col_key) df.insert(len(self.domain.dimension), col_key, surfaces) diff --git a/openmc/model/funcs.py b/openmc/model/funcs.py index 815d03829..41aa920ea 100644 --- a/openmc/model/funcs.py +++ b/openmc/model/funcs.py @@ -221,8 +221,7 @@ def pin(surfaces, items, subdivisions=None, divide_vols=True, center_getter = attrgetter("z0", "y0") else: raise TypeError( - "Not configured to interpret {} surfaces".format( - surf_type.__name__)) + f"Not configured to interpret {surf_type.__name__} surfaces") centers = set() prev_rad = 0 diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index 5543a88e8..6f9123311 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -46,7 +46,7 @@ class CompositeSurface(ABC): getattr(self, name).boundary_type = boundary_type def __repr__(self): - return "<{} at 0x{:x}>".format(type(self).__name__, id(self)) + return f"<{type(self).__name__} at 0x{id(self):x}>" @property @abstractmethod diff --git a/openmc/plots.py b/openmc/plots.py index 5552b18a8..df4a76633 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -634,8 +634,7 @@ class Plot(PlotBase): raise ValueError(msg) elif meshlines['type'] not in ['tally', 'entropy', 'ufs', 'cmfd']: - msg = 'Unable to set the meshlines with ' \ - 'type "{}"'.format(meshlines['type']) + msg = f"Unable to set the meshlines with type \"{meshlines['type']}\"" raise ValueError(msg) if 'id' in meshlines: diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index 3d5f647ac..a0e86c3f8 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -383,7 +383,7 @@ class Uniform(Univariate): """ element = ET.Element(element_name) element.set("type", "uniform") - element.set("parameters", '{} {}'.format(self.a, self.b)) + element.set("parameters", f'{self.a} {self.b}') return element @classmethod @@ -672,7 +672,7 @@ class Watt(Univariate): """ element = ET.Element(element_name) element.set("type", "watt") - element.set("parameters", '{} {}'.format(self.a, self.b)) + element.set("parameters", f'{self.a} {self.b}') return element @classmethod @@ -762,7 +762,7 @@ class Normal(Univariate): """ element = ET.Element(element_name) element.set("type", "normal") - element.set("parameters", '{} {}'.format(self.mean_value, self.std_dev)) + element.set("parameters", f'{self.mean_value} {self.std_dev}') return element @classmethod diff --git a/openmc/surface.py b/openmc/surface.py index bc10f7e8a..806331024 100644 --- a/openmc/surface.py +++ b/openmc/surface.py @@ -187,8 +187,7 @@ class Surface(IDManagerMixin, ABC): coefficients = '{0: <20}'.format('\tCoefficients') + '\n' for coeff in self._coefficients: - coefficients += '{0: <20}{1}{2}\n'.format( - coeff, '=\t', self._coefficients[coeff]) + coefficients += f'{coeff: <20}=\t{self._coefficients[coeff]}\n' string += coefficients diff --git a/openmc/tallies.py b/openmc/tallies.py index ddc5d68ee..04ae00b6a 100644 --- a/openmc/tallies.py +++ b/openmc/tallies.py @@ -2149,7 +2149,7 @@ class Tally(IDManagerMixin): new_tally.sparse = self.sparse else: - msg = 'Unable to add "{}" to Tally ID="{}"'.format(other, self.id) + msg = f'Unable to add "{other}" to Tally ID="{self.id}"' raise ValueError(msg) return new_tally @@ -2220,7 +2220,7 @@ class Tally(IDManagerMixin): new_tally.sparse = self.sparse else: - msg = 'Unable to subtract "{}" from Tally ID="{}"'.format(other, self.id) + msg = f'Unable to subtract "{other}" from Tally ID="{self.id}"' raise ValueError(msg) return new_tally @@ -2291,7 +2291,7 @@ class Tally(IDManagerMixin): new_tally.sparse = self.sparse else: - msg = 'Unable to multiply Tally ID="{}" by "{}"'.format(self.id, other) + msg = f'Unable to multiply Tally ID="{self.id}" by "{other}"' raise ValueError(msg) return new_tally @@ -2362,7 +2362,7 @@ class Tally(IDManagerMixin): new_tally.sparse = self.sparse else: - msg = 'Unable to divide Tally ID="{}" by "{}"'.format(self.id, other) + msg = f'Unable to divide Tally ID="{self.id}" by "{other}"' raise ValueError(msg) return new_tally @@ -2437,7 +2437,7 @@ class Tally(IDManagerMixin): new_tally.sparse = self.sparse else: - msg = 'Unable to raise Tally ID="{}" to power "{}"'.format(self.id, power) + msg = f'Unable to raise Tally ID="{self.id}" to power "{power}"' raise ValueError(msg) return new_tally @@ -3105,8 +3105,7 @@ class Tallies(cv.CheckedList): """ if not isinstance(tally, Tally): - msg = 'Unable to add a non-Tally "{}" to the ' \ - 'Tallies instance'.format(tally) + msg = f'Unable to add a non-Tally "{tally}" to the Tallies instance' raise TypeError(msg) if merge: From d1366c05450a6bb65ec3566012f2b7a221d94b55 Mon Sep 17 00:00:00 2001 From: Gavin Ridley Date: Thu, 2 May 2024 06:52:39 -0500 Subject: [PATCH 099/671] Update random_dist.h comment to be less specific (#2991) --- include/openmc/random_dist.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/openmc/random_dist.h b/include/openmc/random_dist.h index 4c9ab3c67..0fb186edc 100644 --- a/include/openmc/random_dist.h +++ b/include/openmc/random_dist.h @@ -48,9 +48,9 @@ extern "C" double maxwell_spectrum(double T, uint64_t* seed); extern "C" double watt_spectrum(double a, double b, uint64_t* seed); //============================================================================== -//! Samples an energy from the Gaussian energy-dependent fission distribution. +//! Samples an energy from the Gaussian distribution. //! -//! Samples from a Normal distribution with a given mean and standard deviation +//! Samples from a normal distribution with a given mean and standard deviation //! The PDF is defined as s(x) = (1/2*sigma*sqrt(2) * e-((mu-x)/2*sigma)^2 //! Its sampled according to //! http://www-pdg.lbl.gov/2009/reviews/rpp2009-rev-monte-carlo-techniques.pdf From c976653abfba28ce4ac3c464d3e1d22b685d93d1 Mon Sep 17 00:00:00 2001 From: Travis L Date: Thu, 2 May 2024 10:00:22 -0400 Subject: [PATCH 100/671] Allow zero bins in tally triggers (#2928) Co-authored-by: Paul Romano --- docs/source/io_formats/tallies.rst | 12 +++++++++ include/openmc/tallies/trigger.h | 1 + openmc/trigger.py | 28 +++++++++++++++++++-- src/tallies/tally.cpp | 10 ++++++-- src/tallies/trigger.cpp | 6 ++--- tests/unit_tests/test_triggers.py | 40 ++++++++++++++++++++++++++++++ 6 files changed, 90 insertions(+), 7 deletions(-) diff --git a/docs/source/io_formats/tallies.rst b/docs/source/io_formats/tallies.rst index c28db988b..78101ab66 100644 --- a/docs/source/io_formats/tallies.rst +++ b/docs/source/io_formats/tallies.rst @@ -100,6 +100,18 @@ The ```` element accepts the following sub-elements: *Default*: None + :ignore_zeros: + Whether to allow zero tally bins to be ignored when assessing the + convergece of the precision trigger. If True, only nonzero tally scores + will be compared to the trigger's threshold. + + .. note:: The ``ignore_zeros`` option can cause the tally trigger to fire + prematurely if there are no hits in any bins at the first + evalulation. It is the user's responsibility to specify enough + particles per batch to get a nonzero score in at least one bin. + + *Default*: False + :scores: The score(s) in this tally to which the trigger should be applied. diff --git a/include/openmc/tallies/trigger.h b/include/openmc/tallies/trigger.h index 9fe159b9d..7feed5e8a 100644 --- a/include/openmc/tallies/trigger.h +++ b/include/openmc/tallies/trigger.h @@ -23,6 +23,7 @@ enum class TriggerMetric { struct Trigger { TriggerMetric metric; //!< The type of uncertainty (e.g. std dev) measured double threshold; //!< Uncertainty value below which trigger is satisfied + bool ignore_zeros; //!< Whether to allow zero tally bins to be ignored int score_index; //!< Index of the relevant score in the tally's arrays }; diff --git a/openmc/trigger.py b/openmc/trigger.py index c7d9e9240..79f5ea5af 100644 --- a/openmc/trigger.py +++ b/openmc/trigger.py @@ -17,6 +17,12 @@ class Trigger(EqualityMixin): relative error of scores. threshold : float The threshold for the trigger type. + ignore_zeros : bool + Whether to allow zero tally bins to be ignored. Note that this option + can cause the trigger to fire prematurely if there are zero scores in + any bin at the first evaluation. + + .. versionadded:: 0.14.1 Attributes ---------- @@ -27,18 +33,22 @@ class Trigger(EqualityMixin): The threshold for the trigger type. scores : list of str Scores which should be checked against the trigger + ignore_zeros : bool + Whether to allow zero tally bins to be ignored. """ - def __init__(self, trigger_type: str, threshold: float): + def __init__(self, trigger_type: str, threshold: float, ignore_zeros: bool = False): self.trigger_type = trigger_type self.threshold = threshold + self.ignore_zeros = ignore_zeros self._scores = [] def __repr__(self): string = 'Trigger\n' string += '{: <16}=\t{}\n'.format('\tType', self._trigger_type) string += '{: <16}=\t{}\n'.format('\tThreshold', self._threshold) + string += '{: <16}=\t{}\n'.format('\tIgnore Zeros', self._ignore_zeros) string += '{: <16}=\t{}\n'.format('\tScores', self._scores) return string @@ -61,6 +71,15 @@ class Trigger(EqualityMixin): cv.check_type('tally trigger threshold', threshold, Real) self._threshold = threshold + @property + def ignore_zeros(self): + return self._ignore_zeros + + @ignore_zeros.setter + def ignore_zeros(self, ignore_zeros): + cv.check_type('tally trigger ignores zeros', ignore_zeros, bool) + self._ignore_zeros = ignore_zeros + @property def scores(self): return self._scores @@ -88,6 +107,8 @@ class Trigger(EqualityMixin): element = ET.Element("trigger") element.set("type", self._trigger_type) element.set("threshold", str(self._threshold)) + if self._ignore_zeros: + element.set("ignore_zeros", "true") if len(self._scores) != 0: element.set("scores", ' '.join(self._scores)) return element @@ -110,7 +131,10 @@ class Trigger(EqualityMixin): # Generate trigger object trigger_type = elem.get("type") threshold = float(elem.get("threshold")) - trigger = cls(trigger_type, threshold) + ignore_zeros = str(elem.get("ignore_zeros", "false")).lower() + # Try to convert to bool. Let Trigger error out on instantiation. + ignore_zeros = ignore_zeros in ('true', '1') + trigger = cls(trigger_type, threshold, ignore_zeros) # Add scores if present scores = elem.get("scores") diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index 2e77bcad2..674987b8f 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -690,6 +690,12 @@ void Tally::init_triggers(pugi::xml_node node) "Must specify trigger threshold for tally {} in tally XML file", id_)); } + // Read whether to allow zero-tally bins to be ignored. + bool ignore_zeros = false; + if (check_for_node(trigger_node, "ignore_zeros")) { + ignore_zeros = get_node_value_bool(trigger_node, "ignore_zeros"); + } + // Read the trigger scores. vector trigger_scores; if (check_for_node(trigger_node, "scores")) { @@ -703,7 +709,7 @@ void Tally::init_triggers(pugi::xml_node node) if (score_str == "all") { triggers_.reserve(triggers_.size() + this->scores_.size()); for (auto i_score = 0; i_score < this->scores_.size(); ++i_score) { - triggers_.push_back({metric, threshold, i_score}); + triggers_.push_back({metric, threshold, ignore_zeros, i_score}); } } else { int i_score = 0; @@ -717,7 +723,7 @@ void Tally::init_triggers(pugi::xml_node node) "{} but it was listed in a trigger on that tally", score_str, id_)); } - triggers_.push_back({metric, threshold, i_score}); + triggers_.push_back({metric, threshold, ignore_zeros, i_score}); } } } diff --git a/src/tallies/trigger.cpp b/src/tallies/trigger.cpp index 87f298baa..f1f83e298 100644 --- a/src/tallies/trigger.cpp +++ b/src/tallies/trigger.cpp @@ -77,9 +77,9 @@ void check_tally_triggers(double& ratio, int& tally_id, int& score) auto uncert_pair = get_tally_uncertainty(i_tally, trigger.score_index, filter_index); - // if there is a score without contributions, set ratio to inf and - // exit early - if (uncert_pair.first == -1) { + // If there is a score without contributions, set ratio to inf and + // exit early, unless zero scores are ignored for this trigger. + if (uncert_pair.first == -1 && !trigger.ignore_zeros) { ratio = INFINITY; score = t.scores_[trigger.score_index]; tally_id = t.id_; diff --git a/tests/unit_tests/test_triggers.py b/tests/unit_tests/test_triggers.py index 4fe6e044a..6b9e54eeb 100644 --- a/tests/unit_tests/test_triggers.py +++ b/tests/unit_tests/test_triggers.py @@ -73,3 +73,43 @@ def test_tally_trigger_null_score(run_in_tmpdir): total_batches = sp.n_realizations + sp.n_inactive assert total_batches == pincell.settings.trigger_max_batches + +def test_tally_trigger_zero_ignored(run_in_tmpdir): + pincell = openmc.examples.pwr_pin_cell() + + # create an energy filter below and around the O-16(n,p) threshold (1.02e7 eV) + e_filter = openmc.EnergyFilter([0.0, 1e7, 2e7]) + + # create a tally with triggers applied + tally = openmc.Tally() + tally.filters = [e_filter] + tally.scores = ['(n,p)'] + tally.nuclides = ["O16"] + + # 100% relative error: should be immediately satisfied in nonzero bin + trigger = openmc.Trigger('rel_err', 1.0) + trigger.scores = ['(n,p)'] + trigger.ignore_zeros = True + + tally.triggers = [trigger] + + pincell.tallies = [tally] + + pincell.settings.particles = 1000 # we need a few more particles for this + pincell.settings.trigger_active = True + pincell.settings.trigger_max_batches = 50 + pincell.settings.trigger_batch_interval = 20 + + sp_file = pincell.run() + + with openmc.StatePoint(sp_file) as sp: + # verify that the first bin is zero and the second is nonzero + tally_out = sp.get_tally(id=tally.id) + below, above = tally_out.mean.squeeze() + assert below == 0.0, "Tally events observed below expected threshold" + assert above > 0, "No tally events observed. Test with more particles." + + # we expect that the trigger fires before max batches are hit + total_batches = sp.n_realizations + sp.n_inactive + assert total_batches < pincell.settings.trigger_max_batches + From 6e57f1dc7265ad7761886115408e2ec53afc165e Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 2 May 2024 12:47:10 -0500 Subject: [PATCH 101/671] Compute homogenized materials over mesh elements (#2971) --- openmc/deplete/abc.py | 22 +----- openmc/deplete/independent_operator.py | 1 - openmc/deplete/microxs.py | 59 ++++++++--------- openmc/mesh.py | 92 +++++++++++++++++++++++++- openmc/model/model.py | 22 ++---- openmc/utility_funcs.py | 38 +++++++++++ tests/unit_tests/test_mesh.py | 48 ++++++++++++++ 7 files changed, 212 insertions(+), 70 deletions(-) create mode 100644 openmc/utility_funcs.py diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 49bb3e6c7..5d9b8a240 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -10,8 +10,6 @@ from collections.abc import Iterable, Callable from copy import deepcopy from inspect import signature from numbers import Real, Integral -from contextlib import contextmanager -import os from pathlib import Path import time from typing import Optional, Union, Sequence @@ -22,6 +20,7 @@ from uncertainties import ufloat from openmc.checkvalue import check_type, check_greater_than, PathLike from openmc.mpi import comm +from openmc.utility_funcs import change_directory from openmc import Material from .stepresult import StepResult from .chain import Chain @@ -61,25 +60,6 @@ except AttributeError: # Can't set __doc__ on properties on Python 3.4 pass -@contextmanager -def change_directory(output_dir): - """ - Helper function for managing the current directory. - - Parameters - ---------- - output_dir : pathlib.Path - Directory to switch to. - """ - orig_dir = os.getcwd() - output_dir = Path(output_dir) - output_dir.mkdir(parents=True, exist_ok=True) - try: - os.chdir(output_dir) - yield - finally: - os.chdir(orig_dir) - class TransportOperator(ABC): """Abstract class defining a transport operator diff --git a/openmc/deplete/independent_operator.py b/openmc/deplete/independent_operator.py index 2328ca761..250b94deb 100644 --- a/openmc/deplete/independent_operator.py +++ b/openmc/deplete/independent_operator.py @@ -130,7 +130,6 @@ class IndependentOperator(OpenMCOperator): # Validate micro-xs parameters check_type('materials', materials, openmc.Materials) check_type('micros', micros, Iterable, MicroXS) - check_type('fluxes', fluxes, Iterable, float) if not (len(fluxes) == len(micros) == len(materials)): msg = (f'The length of fluxes ({len(fluxes)}) should be equal to ' diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index d80c52baa..497a5284a 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -13,11 +13,11 @@ import numpy as np from openmc.checkvalue import check_type, check_value, check_iterable_type, PathLike from openmc.exceptions import DataError +from openmc.utility_funcs import change_directory from openmc import StatePoint from openmc.mgxs import GROUP_STRUCTURES from openmc.data import REACTION_MT import openmc -from .abc import change_directory from .chain import Chain, REACTIONS from .coupled_operator import _find_cross_sections, _get_nuclides_with_data import openmc.lib @@ -284,38 +284,37 @@ class MicroXS: # Create 2D array for microscopic cross sections microxs_arr = np.zeros((len(nuclides), len(mts))) - with TemporaryDirectory() as tmpdir: - # Create a material with all nuclides - mat_all_nucs = openmc.Material() - for nuc in nuclides: - if nuc in nuclides_with_data: - mat_all_nucs.add_nuclide(nuc, 1.0) - mat_all_nucs.set_density("atom/b-cm", 1.0) + # Create a material with all nuclides + mat_all_nucs = openmc.Material() + for nuc in nuclides: + if nuc in nuclides_with_data: + mat_all_nucs.add_nuclide(nuc, 1.0) + mat_all_nucs.set_density("atom/b-cm", 1.0) - # Create simple model containing the above material - surf1 = openmc.Sphere(boundary_type="vacuum") - surf1_cell = openmc.Cell(fill=mat_all_nucs, region=-surf1) - model = openmc.Model() - model.geometry = openmc.Geometry([surf1_cell]) - model.settings = openmc.Settings( - particles=1, batches=1, output={'summary': False}) + # Create simple model containing the above material + surf1 = openmc.Sphere(boundary_type="vacuum") + surf1_cell = openmc.Cell(fill=mat_all_nucs, region=-surf1) + model = openmc.Model() + model.geometry = openmc.Geometry([surf1_cell]) + model.settings = openmc.Settings( + particles=1, batches=1, output={'summary': False}) - with change_directory(tmpdir): - # Export model within temporary directory - model.export_to_model_xml() + with change_directory(tmpdir=True): + # Export model within temporary directory + model.export_to_model_xml() - with openmc.lib.run_in_memory(**init_kwargs): - # For each nuclide and reaction, compute the flux-averaged - # cross section - for nuc_index, nuc in enumerate(nuclides): - if nuc not in nuclides_with_data: - continue - lib_nuc = openmc.lib.nuclides[nuc] - for mt_index, mt in enumerate(mts): - xs = lib_nuc.collapse_rate( - mt, temperature, energies, multigroup_flux - ) - microxs_arr[nuc_index, mt_index] = xs + with openmc.lib.run_in_memory(**init_kwargs): + # For each nuclide and reaction, compute the flux-averaged + # cross section + for nuc_index, nuc in enumerate(nuclides): + if nuc not in nuclides_with_data: + continue + lib_nuc = openmc.lib.nuclides[nuc] + for mt_index, mt in enumerate(mts): + xs = lib_nuc.collapse_rate( + mt, temperature, energies, multigroup_flux + ) + microxs_arr[nuc_index, mt_index] = xs return cls(microxs_arr, nuclides, reactions) diff --git a/openmc/mesh.py b/openmc/mesh.py index 1f36524d2..8777da325 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -7,7 +7,8 @@ from functools import wraps from math import pi, sqrt, atan2 from numbers import Integral, Real from pathlib import Path -from typing import Optional, Sequence, Tuple +import tempfile +from typing import Optional, Sequence, Tuple, List import h5py import lxml.etree as ET @@ -16,6 +17,7 @@ import numpy as np import openmc import openmc.checkvalue as cv from openmc.checkvalue import PathLike +from openmc.utility_funcs import change_directory from ._xml import get_text from .mixin import IDManagerMixin from .surface import _BOUNDARY_TYPES @@ -145,6 +147,94 @@ class MeshBase(IDManagerMixin, ABC): else: raise ValueError(f'Unrecognized mesh type "{mesh_type}" found.') + def get_homogenized_materials( + self, + model: openmc.Model, + n_samples: int = 10_000, + prn_seed: Optional[int] = None, + **kwargs + ) -> List[openmc.Material]: + """Generate homogenized materials over each element in a mesh. + + .. versionadded:: 0.14.1 + + Parameters + ---------- + model : openmc.Model + Model containing materials to be homogenized and the associated + geometry. + n_samples : int + Number of samples in each mesh element. + prn_seed : int, optional + Pseudorandom number generator (PRNG) seed; if None, one will be + generated randomly. + **kwargs + Keyword-arguments passed to :func:`openmc.lib.init`. + + Returns + ------- + list of openmc.Material + Homogenized material in each mesh element + + """ + import openmc.lib + + with change_directory(tmpdir=True): + # In order to get mesh into model, we temporarily replace the + # tallies with a single mesh tally using the current mesh + original_tallies = model.tallies + new_tally = openmc.Tally() + new_tally.filters = [openmc.MeshFilter(self)] + new_tally.scores = ['flux'] + model.tallies = [new_tally] + + # Export model to XML + model.export_to_model_xml() + + # Get material volume fractions + openmc.lib.init(**kwargs) + mesh = openmc.lib.tallies[new_tally.id].filters[0].mesh + mat_volume_by_element = [ + [ + (mat.id if mat is not None else None, volume) + for mat, volume in mat_volume_list + ] + for mat_volume_list in mesh.material_volumes(n_samples, prn_seed) + ] + openmc.lib.finalize() + + # Restore original tallies + model.tallies = original_tallies + + # Create homogenized material for each element + materials = model.geometry.get_all_materials() + homogenized_materials = [] + for mat_volume_list in mat_volume_by_element: + material_ids, volumes = [list(x) for x in zip(*mat_volume_list)] + total_volume = sum(volumes) + + # Check for void material and remove + try: + index_void = material_ids.index(None) + except ValueError: + pass + else: + material_ids.pop(index_void) + volumes.pop(index_void) + + # Compute volume fractions + volume_fracs = np.array(volumes) / total_volume + + # Get list of materials and mix 'em up! + mats = [materials[uid] for uid in material_ids] + homogenized_mat = openmc.Material.mix_materials( + mats, volume_fracs, 'vo' + ) + homogenized_mat.volume = total_volume + homogenized_materials.append(homogenized_mat) + + return homogenized_materials + class StructuredMesh(MeshBase): """A base class for structured mesh functionality diff --git a/openmc/model/model.py b/openmc/model/model.py index 2be6fcefa..2160c97e6 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1,6 +1,5 @@ from __future__ import annotations from collections.abc import Iterable -from contextlib import contextmanager from functools import lru_cache import os from pathlib import Path @@ -18,18 +17,7 @@ from openmc.dummy_comm import DummyCommunicator from openmc.executor import _process_CLI_arguments from openmc.checkvalue import check_type, check_value, PathLike from openmc.exceptions import InvalidIDError - - -@contextmanager -def _change_directory(working_dir): - """A context manager for executing in a provided working directory""" - start_dir = Path.cwd() - Path.mkdir(working_dir, parents=True, exist_ok=True) - os.chdir(working_dir) - try: - yield - finally: - os.chdir(start_dir) +from openmc.utility_funcs import change_directory class Model: @@ -395,7 +383,7 @@ class Model: # Store whether or not the library was initialized when we started started_initialized = self.is_initialized - with _change_directory(Path(directory)): + with change_directory(directory): with openmc.lib.quiet_dll(output): # TODO: Support use of IndependentOperator too depletion_operator = dep.CoupledOperator(self, **op_kwargs) @@ -677,7 +665,7 @@ class Model: last_statepoint = None # Operate in the provided working directory - with _change_directory(Path(cwd)): + with change_directory(cwd): if self.is_initialized: # Handle the run options as applicable # First dont allow ones that must be set via init @@ -767,7 +755,7 @@ class Model: raise ValueError("The Settings.volume_calculations attribute must" " be specified before executing this method!") - with _change_directory(Path(cwd)): + with change_directory(cwd): if self.is_initialized: if threads is not None: msg = "Threads must be set via Model.is_initialized(...)" @@ -829,7 +817,7 @@ class Model: raise ValueError("The Model.plots attribute must be specified " "before executing this method!") - with _change_directory(Path(cwd)): + with change_directory(cwd): if self.is_initialized: # Compute the volumes openmc.lib.plot_geometry(output) diff --git a/openmc/utility_funcs.py b/openmc/utility_funcs.py new file mode 100644 index 000000000..4eb307c93 --- /dev/null +++ b/openmc/utility_funcs.py @@ -0,0 +1,38 @@ +from contextlib import contextmanager +import os +from pathlib import Path +from tempfile import TemporaryDirectory +from typing import Optional + +from .checkvalue import PathLike + +@contextmanager +def change_directory(working_dir: Optional[PathLike] = None, *, tmpdir: bool = False): + """Context manager for executing in a provided working directory + + Parameters + ---------- + working_dir : path-like + Directory to switch to. + tmpdir : bool + Whether to use a temporary directory instead of a specific working directory + + """ + orig_dir = Path.cwd() + + # Set up temporary directory if requested + if tmpdir: + tmp = TemporaryDirectory() + working_dir = tmp.name + elif working_dir is None: + raise ValueError('Must pass working_dir argument or specify tmpdir=True.') + + working_dir = Path(working_dir) + working_dir.mkdir(parents=True, exist_ok=True) + os.chdir(working_dir) + try: + yield + finally: + os.chdir(orig_dir) + if tmpdir: + tmp.cleanup() diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index d50d2968e..c49930650 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -299,6 +299,7 @@ def test_CylindricalMesh_get_indices_at_coords(): assert mesh.get_indices_at_coords([98, 199.9, 299]) == (0, 2, 0) # third angle quadrant assert mesh.get_indices_at_coords([102, 199.1, 299]) == (0, 3, 0) # forth angle quadrant + def test_umesh_roundtrip(run_in_tmpdir, request): umesh = openmc.UnstructuredMesh(request.path.parent / 'test_mesh_tets.e', 'moab') umesh.output = True @@ -317,3 +318,50 @@ def test_umesh_roundtrip(run_in_tmpdir, request): xml_mesh = xml_tally.filters[0].mesh assert umesh.id == xml_mesh.id + + +def test_mesh_get_homogenized_materials(): + """Test the get_homogenized_materials method""" + # Simple model with 1 cm of Fe56 next to 1 cm of H1 + fe = openmc.Material() + fe.add_nuclide('Fe56', 1.0) + fe.set_density('g/cm3', 5.0) + h = openmc.Material() + h.add_nuclide('H1', 1.0) + h.set_density('g/cm3', 1.0) + + x0 = openmc.XPlane(-1.0, boundary_type='vacuum') + x1 = openmc.XPlane(0.0) + x2 = openmc.XPlane(1.0) + x3 = openmc.XPlane(2.0, boundary_type='vacuum') + cell1 = openmc.Cell(fill=fe, region=+x0 & -x1) + cell2 = openmc.Cell(fill=h, region=+x1 & -x2) + cell_empty = openmc.Cell(region=+x2 & -x3) + model = openmc.Model(geometry=openmc.Geometry([cell1, cell2, cell_empty])) + model.settings.particles = 1000 + model.settings.batches = 10 + + mesh = openmc.RegularMesh() + mesh.lower_left = (-1., -1., -1.) + mesh.upper_right = (1., 1., 1.) + mesh.dimension = (3, 1, 1) + m1, m2, m3 = mesh.get_homogenized_materials(model, n_samples=1_000_000) + + # Left mesh element should be only Fe56 + assert m1.get_mass_density('Fe56') == pytest.approx(5.0) + + # Middle mesh element should be 50% Fe56 and 50% H1 + assert m2.get_mass_density('Fe56') == pytest.approx(2.5, rel=1e-2) + assert m2.get_mass_density('H1') == pytest.approx(0.5, rel=1e-2) + + # Right mesh element should be only H1 + assert m3.get_mass_density('H1') == pytest.approx(1.0) + + mesh_void = openmc.RegularMesh() + mesh_void.lower_left = (0.5, 0.5, -1.) + mesh_void.upper_right = (1.5, 1.5, 1.) + mesh_void.dimension = (1, 1, 1) + m4, = mesh_void.get_homogenized_materials(model, n_samples=1_000_000) + + # Mesh element that overlaps void should have half density + assert m4.get_mass_density('H1') == pytest.approx(0.5, rel=1e-2) From cfe210da22367ce5cf928482e7b74e48c909a7ee Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 3 May 2024 13:30:49 -0500 Subject: [PATCH 102/671] Allow MeshSource to take a 1D array of sources (#2980) --- openmc/source.py | 51 ++++++++++++++-------------- tests/unit_tests/test_source_mesh.py | 9 ++--- 2 files changed, 28 insertions(+), 32 deletions(-) diff --git a/openmc/source.py b/openmc/source.py index a77ca8b04..5deb6b05f 100644 --- a/openmc/source.py +++ b/openmc/source.py @@ -399,20 +399,19 @@ class MeshSource(SourceBase): ---------- mesh : openmc.MeshBase The mesh over which source sites will be generated. - sources : iterable of openmc.SourceBase - Sources for each element in the mesh. If spatial distributions are set - on any of the source objects, they will be ignored during source site - sampling. + sources : sequence of openmc.SourceBase + Sources for each element in the mesh. Sources must be specified as + either a 1-D array in the order of the mesh indices or a + multidimensional array whose shape matches the mesh shape. If spatial + distributions are set on any of the source objects, they will be ignored + during source site sampling. Attributes ---------- mesh : openmc.MeshBase The mesh over which source sites will be generated. - sources : numpy.ndarray or iterable of openmc.SourceBase - The set of sources to apply to each element. The shape of this array - must match the shape of the mesh with and exception in the case of - unstructured mesh, which allows for application of 1-D array or - iterable. + sources : numpy.ndarray of openmc.SourceBase + Sources to apply to each element strength : float Strength of the source type : str @@ -433,7 +432,7 @@ class MeshSource(SourceBase): @property def strength(self) -> float: - return sum(s.strength for s in self.sources.flat) + return sum(s.strength for s in self.sources) @property def sources(self) -> np.ndarray: @@ -450,16 +449,23 @@ class MeshSource(SourceBase): s = np.asarray(s) - if isinstance(self.mesh, StructuredMesh) and s.shape != self.mesh.dimension: - raise ValueError('The shape of the source array' - f'({s.shape}) does not match the ' - f'dimensions of the structured mesh ({self.mesh.dimension})') + if isinstance(self.mesh, StructuredMesh): + if s.size != self.mesh.num_mesh_cells: + raise ValueError( + f'The length of the source array ({s.size}) does not match ' + f'the number of mesh elements ({self.mesh.num_mesh_cells}).') + + # If user gave a multidimensional array, flatten in the order + # of the mesh indices + if s.ndim > 1: + s = s.ravel(order='F') + elif isinstance(self.mesh, UnstructuredMesh): - if len(s.shape) > 1: + if s.ndim > 1: raise ValueError('Sources must be a 1-D array for unstructured mesh') self._sources = s - for src in self._sources.flat: + for src in self._sources: if isinstance(src, IndependentSource) and src.space is not None: warnings.warn('Some sources on the mesh have spatial ' 'distributions that will be ignored at runtime.') @@ -481,7 +487,7 @@ class MeshSource(SourceBase): """ current_strength = self.strength if self.strength != 0.0 else 1.0 - for s in self.sources.flat: + for s in self.sources: s.strength *= strength / current_strength def normalize_source_strengths(self): @@ -500,13 +506,8 @@ class MeshSource(SourceBase): elem.set("mesh", str(self.mesh.id)) # write in the order of mesh indices - if isinstance(self.mesh, openmc.UnstructuredMesh): - for s in self.sources: - elem.append(s.to_xml_element()) - else: - for idx in self.mesh.indices: - idx = tuple(i - 1 for i in idx) - elem.append(self.sources[idx].to_xml_element()) + for s in self.sources: + elem.append(s.to_xml_element()) @classmethod def from_xml_element(cls, elem: ET.Element, meshes) -> openmc.MeshSource: @@ -527,11 +528,9 @@ class MeshSource(SourceBase): MeshSource generated from the XML element """ mesh_id = int(get_text(elem, 'mesh')) - mesh = meshes[mesh_id] sources = [SourceBase.from_xml_element(e) for e in elem.iterchildren('source')] - sources = np.asarray(sources).reshape(mesh.dimension, order='F') return cls(mesh, sources) diff --git a/tests/unit_tests/test_source_mesh.py b/tests/unit_tests/test_source_mesh.py index f0813d2f9..43bb1678c 100644 --- a/tests/unit_tests/test_source_mesh.py +++ b/tests/unit_tests/test_source_mesh.py @@ -276,12 +276,12 @@ def test_mesh_source_independent(run_in_tmpdir, void_model, mesh_type): # for each element, set a single-non zero source with particles # traveling out of the mesh (and geometry) w/o crossing any other # mesh elements - for i, j, k in mesh.indices: + for flat_index, (i, j, k) in enumerate(mesh.indices): ijk = (i-1, j-1, k-1) # zero-out all source strengths and set the strength # on the element of interest mesh_source.strength = 0.0 - mesh_source.sources[ijk].strength = 1.0 + mesh_source.sources[flat_index].strength = 1.0 sp_file = model.run() @@ -375,10 +375,7 @@ def test_mesh_source_file(run_in_tmpdir): mesh.upper_right = (2, 3, 4) mesh.dimension = (1, 1, 1) - mesh_source_arr = np.asarray([file_source]).reshape(mesh.dimension) - source = openmc.MeshSource(mesh, mesh_source_arr) - - model.settings.source = source + model.settings.source = openmc.MeshSource(mesh, [file_source]) model.export_to_model_xml() From a7d6939c11a2d905d0aab3c57a97cd82e7018b3a Mon Sep 17 00:00:00 2001 From: April Novak Date: Tue, 14 May 2024 01:47:11 -0500 Subject: [PATCH 103/671] Fixes reordering in random ray. Refs #2997 (#2998) Co-authored-by: Paul Romano --- include/openmc/random_ray/random_ray.h | 6 +++--- src/random_ray/random_ray.cpp | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/include/openmc/random_ray/random_ray.h b/include/openmc/random_ray/random_ray.h index c114007c6..5ee64574e 100644 --- a/include/openmc/random_ray/random_ray.h +++ b/include/openmc/random_ray/random_ray.h @@ -38,14 +38,14 @@ public: // Public data members vector angular_flux_; +private: //---------------------------------------------------------------------------- // Private data members -private: + vector delta_psi_; + int negroups_; FlatSourceDomain* domain_ {nullptr}; // pointer to domain that has flat source // data needed for ray transport - vector delta_psi_; double distance_travelled_ {0}; - int negroups_; bool is_active_ {false}; bool is_alive_ {true}; }; // class RandomRay diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp index f471c7551..63d728cce 100644 --- a/src/random_ray/random_ray.cpp +++ b/src/random_ray/random_ray.cpp @@ -70,9 +70,9 @@ double RandomRay::distance_active_; unique_ptr RandomRay::ray_source_; RandomRay::RandomRay() - : negroups_(data::mg.num_energy_groups_), - angular_flux_(data::mg.num_energy_groups_), - delta_psi_(data::mg.num_energy_groups_) + : angular_flux_(data::mg.num_energy_groups_), + delta_psi_(data::mg.num_energy_groups_), + negroups_(data::mg.num_energy_groups_) {} RandomRay::RandomRay(uint64_t ray_id, FlatSourceDomain* domain) : RandomRay() From 1702b4554bfb1a7f19cc2b9f1318edeb0f7ad0d2 Mon Sep 17 00:00:00 2001 From: Erik B Knudsen Date: Wed, 15 May 2024 22:49:04 +0200 Subject: [PATCH 104/671] Restricted file source (#2916) Co-authored-by: church89 Co-authored-by: Paul Romano --- docs/source/io_formats/settings.rst | 38 +- docs/source/usersguide/settings.rst | 48 +++ examples/assembly/assembly.py | 9 +- .../pincell_depletion/restart_depletion.py | 5 +- examples/pincell_depletion/run_depletion.py | 5 +- include/openmc/distribution_spatial.h | 5 + include/openmc/source.h | 90 ++++- openmc/examples.py | 12 +- openmc/source.py | 328 +++++++++++++----- openmc/stats/multivariate.py | 11 + src/distribution_spatial.cpp | 7 +- src/simulation.cpp | 4 +- src/source.cpp | 304 +++++++++++----- .../asymmetric_lattice/inputs_true.dat | 5 +- .../asymmetric_lattice/test.py | 6 +- .../mg_temperature_multi/inputs_true.dat | 5 +- .../mg_temperature_multi/test.py | 5 +- .../mgxs_library_ce_to_mg/inputs_true.dat | 5 +- .../inputs_true.dat | 5 +- .../mgxs_library_condense/inputs_true.dat | 5 +- .../mgxs_library_correction/inputs_true.dat | 5 +- .../mgxs_library_distribcell/inputs_true.dat | 5 +- .../mgxs_library_hdf5/inputs_true.dat | 5 +- .../mgxs_library_histogram/inputs_true.dat | 5 +- .../mgxs_library_no_nuclides/inputs_true.dat | 5 +- .../mgxs_library_nuclides/inputs_true.dat | 5 +- .../inputs_true.dat | 5 +- .../surface_tally/inputs_true.dat | 5 +- tests/regression_tests/surface_tally/test.py | 6 +- tests/unit_tests/test_lib.py | 6 +- tests/unit_tests/test_model.py | 6 +- tests/unit_tests/test_source.py | 103 +++++- 32 files changed, 823 insertions(+), 240 deletions(-) diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 6318a9916..ae3266dab 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -534,8 +534,9 @@ attributes/sub-elements: *Default*: 1.0 :type: - Indicator of source type. One of ``independent``, ``file``, ``compiled``, or ``mesh``. - The type of the source will be determined by this attribute if it is present. + Indicator of source type. One of ``independent``, ``file``, ``compiled``, or + ``mesh``. The type of the source will be determined by this attribute if it + is present. :particle: The source particle type, either ``neutron`` or ``photon``. @@ -716,6 +717,39 @@ attributes/sub-elements: mesh element and follows the format for :ref:`source_element`. The number of ```` sub-elements should correspond to the number of mesh elements. + :constraints: + This sub-element indicates the presence of constraints on sampled source + sites (see :ref:`usersguide_source_constraints` for details). It may have + the following sub-elements: + + :domain_ids: + The unique IDs of domains for which source sites must be within. + + *Default*: None + + :domain_type: + The type of each domain for source rejection ("cell", "material", or + "universe"). + + *Default*: None + + :fissionable: + A boolean indicating whether source sites must be sampled within a + material that is fissionable in order to be accepted. + + :time_bounds: + A pair of times in [s] indicating the lower and upper bound for a time + interval that source particles must be within. + + :energy_bounds: + A pair of energies in [eV] indicating the lower and upper bound for an + energy interval that source particles must be within. + + :rejection_strategy: + Either "resample", indicating that source sites should be resampled when + one is rejected, or "kill", indicating that a rejected source site is + assigned zero weight. + .. _univariate: Univariate Probability Distributions diff --git a/docs/source/usersguide/settings.rst b/docs/source/usersguide/settings.rst index e966423f0..eb02f654c 100644 --- a/docs/source/usersguide/settings.rst +++ b/docs/source/usersguide/settings.rst @@ -420,6 +420,54 @@ string, which gets passed down to the ``openmc_create_source()`` function:: settings.source = openmc.CompiledSource('libsource.so', '3.5e6') +.. _usersguide_source_constraints: + +Source Constraints +------------------ + +All source classes in OpenMC have the ability to apply a set of "constraints" +that limit which sampled source sites are actually used for transport. The most +common use case is to sample source sites over some simple spatial distribution +(e.g., uniform over a box) and then only accept those that appear in a given +cell or material. This can be done with a domain constraint, which can be +specified as follows:: + + source_cell = openmc.Cell(...) + ... + + spatial_dist = openmc.stats.Box((-10., -10., -10.), (10., 10., 10.)) + source = openmc.IndependentSource( + space=spatial_dist, + constraints={'domains': [source_cell]} + ) + +For k-eigenvalue problems, a convenient constraint is available that limits +source sites to those sampled in a fissionable material:: + + source = openmc.IndependentSource( + space=spatial_dist, constraints={'fissionable': True} + ) + +Constraints can also be placed on a range of energies or times:: + + # Only use source sites between 500 keV and 1 MeV and with times under 1 sec + source = openmc.FileSource( + 'source.h5', + constraints={'energy_bounds': [500.0e3, 1.0e6], 'time_bounds': [0.0, 1.0]} + ) + +Normally, when a source site is rejected, a new one will be resampled until one +is found that meets the constraints. However, the rejection strategy can be +changed so that a rejected site will just not be simulated by specifying:: + + source = openmc.IndependentSource( + space=spatial_dist, + constraints={'domains': [cell], 'rejection_strategy': 'kill'} + ) + +In this case, the actual number of particles simulated may be less than what you +specified in :attr:`Settings.particles`. + .. _usersguide_entropy: --------------- diff --git a/examples/assembly/assembly.py b/examples/assembly/assembly.py index a370a3611..d94cb69bf 100644 --- a/examples/assembly/assembly.py +++ b/examples/assembly/assembly.py @@ -112,11 +112,10 @@ def assembly_model(): model.settings.batches = 150 model.settings.inactive = 50 model.settings.particles = 1000 - model.settings.source = openmc.IndependentSource(space=openmc.stats.Box( - (-pitch/2, -pitch/2, -1), - (pitch/2, pitch/2, 1), - only_fissionable=True - )) + model.settings.source = openmc.IndependentSource( + space=openmc.stats.Box((-pitch/2, -pitch/2, -1), (pitch/2, pitch/2, 1)), + constraints={'fissionable': True} + ) # NOTE: We never actually created a Materials object. When you export/run # using the Model object, if no materials were assigned it will look through diff --git a/examples/pincell_depletion/restart_depletion.py b/examples/pincell_depletion/restart_depletion.py index ac065d340..de9fc16cb 100644 --- a/examples/pincell_depletion/restart_depletion.py +++ b/examples/pincell_depletion/restart_depletion.py @@ -26,8 +26,9 @@ settings.particles = 10000 # Create an initial uniform spatial source distribution over fissionable zones bounds = [-0.62992, -0.62992, -1, 0.62992, 0.62992, 1] -uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) -settings.source = openmc.IndependentSource(space=uniform_dist) +uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:]) +settings.source = openmc.IndependentSource( + space=uniform_dist, constraints={'fissionable': True}) entropy_mesh = openmc.RegularMesh() entropy_mesh.lower_left = [-0.39218, -0.39218, -1.e50] diff --git a/examples/pincell_depletion/run_depletion.py b/examples/pincell_depletion/run_depletion.py index e1959938f..013c83c86 100644 --- a/examples/pincell_depletion/run_depletion.py +++ b/examples/pincell_depletion/run_depletion.py @@ -71,8 +71,9 @@ settings.particles = 1000 # Create an initial uniform spatial source distribution over fissionable zones bounds = [-0.62992, -0.62992, -1, 0.62992, 0.62992, 1] -uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) -settings.source = openmc.IndependentSource(space=uniform_dist) +uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:]) +settings.source = openmc.IndependentSource( + space=uniform_dist, constraints={'fissionable': True}) entropy_mesh = openmc.RegularMesh() entropy_mesh.lower_left = [-0.39218, -0.39218, -1.e50] diff --git a/include/openmc/distribution_spatial.h b/include/openmc/distribution_spatial.h index bd10a2996..28568c890 100644 --- a/include/openmc/distribution_spatial.h +++ b/include/openmc/distribution_spatial.h @@ -116,6 +116,11 @@ public: //! \return Sampled element index and position within that element std::pair sample_mesh(uint64_t* seed) const; + //! Sample a mesh element + //! \param seed Pseudorandom number seed pointer + //! \return Sampled element index + int32_t sample_element_index(uint64_t* seed) const; + //! For unstructured meshes, ensure that elements are all linear tetrahedra void check_element_types() const; diff --git a/include/openmc/source.h b/include/openmc/source.h index d5ea9bd1d..9ef8b9251 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 "pugixml.hpp" @@ -39,19 +40,72 @@ extern vector> external_sources; //============================================================================== //! Abstract source interface +// +//! The Source class provides the interface that must be implemented by derived +//! classes, namely the sample() method that returns a sampled source site. From +//! this base class, source rejection is handled within the +//! sample_with_constraints() method. However, note that some classes directly +//! check for constraints for efficiency reasons (like IndependentSource), in +//! which case the constraints_applied() method indicates that constraints +//! should not be checked a second time from the base class. //============================================================================== class Source { public: + // Constructors, destructors + Source() = default; + explicit Source(pugi::xml_node node); virtual ~Source() = default; - // Methods that must be implemented + // Methods that can be overridden + virtual double strength() const { return strength_; } + + //! Sample a source site and apply constraints + // + //! \param[inout] seed Pseudorandom seed pointer + //! \return Sampled site + SourceSite sample_with_constraints(uint64_t* seed) const; + + //! Sample a source site (without applying constraints) + // + //! Sample from the external source distribution + //! \param[inout] seed Pseudorandom seed pointer + //! \return Sampled site virtual SourceSite sample(uint64_t* seed) const = 0; - // Methods that can be overridden - virtual double strength() const { return 1.0; } - static unique_ptr create(pugi::xml_node node); + +protected: + // Domain types + enum class DomainType { UNIVERSE, MATERIAL, CELL }; + + // Strategy used for rejecting sites when constraints are applied. KILL means + // that sites are always accepted but if they don't satisfy constraints, they + // are given weight 0. RESAMPLE means that a new source site will be sampled + // until constraints are met. + enum class RejectionStrategy { KILL, RESAMPLE }; + + // Indicates whether derived class already handles constraints + virtual bool constraints_applied() const { return false; } + + // Methods for constraints + void read_constraints(pugi::xml_node node); + bool satisfies_spatial_constraints(Position r) const; + bool satisfies_energy_constraints(double E) const; + bool satisfies_time_constraints(double time) const; + + // Data members + double strength_ {1.0}; //!< Source strength + std::unordered_set domain_ids_; //!< Domains to reject from + DomainType domain_type_; //!< Domain type for rejection + std::pair time_bounds_ {-std::numeric_limits::max(), + std::numeric_limits::max()}; //!< time limits + std::pair energy_bounds_ { + 0, std::numeric_limits::max()}; //!< energy limits + bool only_fissionable_ { + false}; //!< Whether site must be in fissionable material + RejectionStrategy rejection_strategy_ { + RejectionStrategy::RESAMPLE}; //!< Procedure for rejecting }; //============================================================================== @@ -73,7 +127,6 @@ public: // Properties ParticleType particle_type() const { return particle_; } - double strength() const override { return strength_; } // Make observing pointers available SpatialDistribution* space() const { return space_.get(); } @@ -81,19 +134,17 @@ public: Distribution* energy() const { return energy_.get(); } Distribution* time() const { return time_.get(); } -private: - // Domain types - enum class DomainType { UNIVERSE, MATERIAL, CELL }; +protected: + // Indicates whether derived class already handles constraints + bool constraints_applied() const override { return true; } +private: // Data members ParticleType particle_ {ParticleType::neutron}; //!< Type of particle emitted - double strength_ {1.0}; //!< Source strength UPtrSpace space_; //!< Spatial distribution UPtrAngle angle_; //!< Angular distribution UPtrDist energy_; //!< Energy distribution UPtrDist time_; //!< Time distribution - DomainType domain_type_; //!< Domain type for rejection - std::unordered_set domain_ids_; //!< Domains to reject from }; //============================================================================== @@ -107,9 +158,12 @@ public: explicit FileSource(const std::string& path); // Methods - SourceSite sample(uint64_t* seed) const override; void load_sites_from_file( const std::string& path); //!< Load source sites from file + +protected: + SourceSite sample(uint64_t* seed) const override; + private: vector sites_; //!< Source sites from a file }; @@ -124,16 +178,17 @@ public: CompiledSourceWrapper(pugi::xml_node node); ~CompiledSourceWrapper(); + double strength() const override { return compiled_source_->strength(); } + + void setup(const std::string& path, const std::string& parameters); + +protected: // Defer implementation to custom source library SourceSite sample(uint64_t* seed) const override { return compiled_source_->sample(seed); } - double strength() const override { return compiled_source_->strength(); } - - void setup(const std::string& path, const std::string& parameters); - private: void* shared_library_; //!< library from dlopen unique_ptr compiled_source_; @@ -164,6 +219,9 @@ public: return sources_.size() == 1 ? sources_[0] : sources_[i]; } +protected: + bool constraints_applied() const override { return true; } + private: // Data members unique_ptr space_; //!< Mesh spatial diff --git a/openmc/examples.py b/openmc/examples.py index cef86a47a..fc94b8b52 100644 --- a/openmc/examples.py +++ b/openmc/examples.py @@ -76,8 +76,10 @@ def pwr_pin_cell(): model.settings.batches = 10 model.settings.inactive = 5 model.settings.particles = 100 - model.settings.source = openmc.IndependentSource(space=openmc.stats.Box( - [-pitch/2, -pitch/2, -1], [pitch/2, pitch/2, 1], only_fissionable=True)) + model.settings.source = openmc.IndependentSource( + space=openmc.stats.Box([-pitch/2, -pitch/2, -1], [pitch/2, pitch/2, 1]), + constraints={'fissionable': True} + ) plot = openmc.Plot.from_geometry(model.geometry) plot.pixels = (300, 300) @@ -527,8 +529,10 @@ def pwr_assembly(): model.settings.batches = 10 model.settings.inactive = 5 model.settings.particles = 100 - model.settings.source = openmc.IndependentSource(space=openmc.stats.Box( - [-pitch/2, -pitch/2, -1], [pitch/2, pitch/2, 1], only_fissionable=True)) + model.settings.source = openmc.IndependentSource( + space=openmc.stats.Box([-pitch/2, -pitch/2, -1], [pitch/2, pitch/2, 1]), + constraints={'fissionable': True} + ) plot = openmc.Plot() plot.origin = (0.0, 0.0, 0) diff --git a/openmc/source.py b/openmc/source.py index 5deb6b05f..91318f8e9 100644 --- a/openmc/source.py +++ b/openmc/source.py @@ -6,7 +6,7 @@ from numbers import Real import warnings import typing # imported separately as py3.8 requires typing.Iterable # also required to prevent typing.Union namespace overwriting Union -from typing import Optional, Sequence +from typing import Optional, Sequence, Dict, Any import lxml.etree as ET import numpy as np @@ -28,6 +28,19 @@ class SourceBase(ABC): ---------- strength : float Strength of the source + constraints : dict + Constraints on sampled source particles. Valid keys include 'domains', + 'time_bounds', 'energy_bounds', 'fissionable', and 'rejection_strategy'. + For 'domains', the corresponding value is an iterable of + :class:`openmc.Cell`, :class:`openmc.Material`, or + :class:`openmc.Universe` for which sampled sites must be within. For + 'time_bounds' and 'energy_bounds', the corresponding value is a sequence + of floats giving the lower and upper bounds on time in [s] or energy in + [eV] that the sampled particle must be within. For 'fissionable', the + value is a bool indicating that only sites in fissionable material + should be accepted. The 'rejection_strategy' indicates what should + happen when a source particle is rejected: either 'resample' (pick a new + particle) or 'kill' (accept and terminate). Attributes ---------- @@ -35,11 +48,20 @@ class SourceBase(ABC): Indicator of source type. strength : float Strength of the source + constraints : dict + Constraints on sampled source particles. Valid keys include + 'domain_type', 'domain_ids', 'time_bounds', 'energy_bounds', + 'fissionable', and 'rejection_strategy'. """ - def __init__(self, strength=1.0): + def __init__( + self, + strength: Optional[float] = 1.0, + constraints: Optional[Dict[str, Any]] = None + ): self.strength = strength + self.constraints = constraints @property def strength(self): @@ -47,10 +69,47 @@ class SourceBase(ABC): @strength.setter def strength(self, strength): - cv.check_type('source strength', strength, Real) - cv.check_greater_than('source strength', strength, 0.0, True) + cv.check_type('source strength', strength, Real, none_ok=True) + if strength is not None: + cv.check_greater_than('source strength', strength, 0.0, True) self._strength = strength + @property + def constraints(self) -> Dict[str, Any]: + return self._constraints + + @constraints.setter + def constraints(self, constraints: Optional[Dict[str, Any]]): + self._constraints = {} + if constraints is None: + return + + for key, value in constraints.items(): + if key == 'domains': + cv.check_type('domains', value, Iterable, + (openmc.Cell, openmc.Material, openmc.Universe)) + if isinstance(value[0], openmc.Cell): + self._constraints['domain_type'] = 'cell' + elif isinstance(value[0], openmc.Material): + self._constraints['domain_type'] = 'material' + elif isinstance(value[0], openmc.Universe): + self._constraints['domain_type'] = 'universe' + self._constraints['domain_ids'] = [d.id for d in value] + elif key == 'time_bounds': + cv.check_type('time bounds', value, Iterable, Real) + self._constraints['time_bounds'] = tuple(value) + elif key == 'energy_bounds': + cv.check_type('energy bounds', value, Iterable, Real) + self._constraints['energy_bounds'] = tuple(value) + elif key == 'fissionable': + cv.check_type('fissionable', value, bool) + self._constraints['fissionable'] = value + elif key == 'rejection_strategy': + cv.check_value('rejection strategy', value, ('resample', 'kill')) + self._constraints['rejection_strategy'] = value + else: + raise ValueError('Unknown key in constraints dictionary: {key}') + @abstractmethod def populate_xml_element(self, element): """Add necessary source information to an XML element @@ -73,8 +132,30 @@ class SourceBase(ABC): """ element = ET.Element("source") element.set("type", self.type) - element.set("strength", str(self.strength)) + if self.strength is not None: + element.set("strength", str(self.strength)) self.populate_xml_element(element) + constraints = self.constraints + if constraints: + constraints_elem = ET.SubElement(element, "constraints") + if "domain_ids" in constraints: + dt_elem = ET.SubElement(constraints_elem, "domain_type") + dt_elem.text = constraints["domain_type"] + id_elem = ET.SubElement(constraints_elem, "domain_ids") + id_elem.text = ' '.join(str(uid) for uid in constraints["domain_ids"]) + if "time_bounds" in constraints: + dt_elem = ET.SubElement(constraints_elem, "time_bounds") + dt_elem.text = ' '.join(str(t) for t in constraints["time_bounds"]) + if "energy_bounds" in constraints: + dt_elem = ET.SubElement(constraints_elem, "energy_bounds") + dt_elem.text = ' '.join(str(E) for E in constraints["energy_bounds"]) + if "fissionable" in constraints: + dt_elem = ET.SubElement(constraints_elem, "fissionable") + dt_elem.text = str(constraints["fissionable"]).lower() + if "rejection_strategy" in constraints: + dt_elem = ET.SubElement(constraints_elem, "rejection_strategy") + dt_elem.text = constraints["rejection_strategy"] + return element @classmethod @@ -118,6 +199,47 @@ class SourceBase(ABC): else: raise ValueError(f'Source type {source_type} is not recognized') + @staticmethod + def _get_constraints(elem: ET.Element) -> Dict[str, Any]: + # Find element containing constraints + constraints_elem = elem.find("constraints") + elem = constraints_elem if constraints_elem is not None else elem + + constraints = {} + domain_type = get_text(elem, "domain_type") + if domain_type is not None: + domain_ids = [int(x) for x in get_text(elem, "domain_ids").split()] + + # Instantiate some throw-away domains that are used by the + # constructor to assign IDs + with warnings.catch_warnings(): + warnings.simplefilter('ignore', openmc.IDWarning) + if domain_type == 'cell': + domains = [openmc.Cell(uid) for uid in domain_ids] + elif domain_type == 'material': + domains = [openmc.Material(uid) for uid in domain_ids] + elif domain_type == 'universe': + domains = [openmc.Universe(uid) for uid in domain_ids] + constraints['domains'] = domains + + time_bounds = get_text(elem, "time_bounds") + if time_bounds is not None: + constraints['time_bounds'] = [float(x) for x in time_bounds.split()] + + energy_bounds = get_text(elem, "energy_bounds") + if energy_bounds is not None: + constraints['energy_bounds'] = [float(x) for x in energy_bounds.split()] + + fissionable = get_text(elem, "fissionable") + if fissionable is not None: + constraints['fissionable'] = fissionable in ('true', '1') + + rejection_strategy = get_text(elem, "rejection_strategy") + if rejection_strategy is not None: + constraints['rejection_strategy'] = rejection_strategy + + return constraints + class IndependentSource(SourceBase): """Distribution of phase space coordinates for source sites. @@ -142,6 +264,22 @@ class IndependentSource(SourceBase): Domains to reject based on, i.e., if a sampled spatial location is not within one of these domains, it will be rejected. + .. deprecated:: 0.14.1 + Use the `constraints` argument instead. + constraints : dict + Constraints on sampled source particles. Valid keys include 'domains', + 'time_bounds', 'energy_bounds', 'fissionable', and 'rejection_strategy'. + For 'domains', the corresponding value is an iterable of + :class:`openmc.Cell`, :class:`openmc.Material`, or + :class:`openmc.Universe` for which sampled sites must be within. For + 'time_bounds' and 'energy_bounds', the corresponding value is a sequence + of floats giving the lower and upper bounds on time in [s] or energy in + [eV] that the sampled particle must be within. For 'fissionable', the + value is a bool indicating that only sites in fissionable material + should be accepted. The 'rejection_strategy' indicates what should + happen when a source particle is rejected: either 'resample' (pick a new + particle) or 'kill' (accept and terminate). + Attributes ---------- space : openmc.stats.Spatial or None @@ -161,10 +299,10 @@ class IndependentSource(SourceBase): particle : {'neutron', 'photon'} Source particle type - ids : Iterable of int - IDs of domains to use for rejection - domain_type : {'cell', 'material', 'universe'} - Type of domain to use for rejection + constraints : dict + Constraints on sampled source particles. Valid keys include + 'domain_type', 'domain_ids', 'time_bounds', 'energy_bounds', + 'fissionable', and 'rejection_strategy'. """ @@ -176,9 +314,15 @@ class IndependentSource(SourceBase): time: Optional[openmc.stats.Univariate] = None, strength: float = 1.0, particle: str = 'neutron', - domains: Optional[Sequence[typing.Union[openmc.Cell, openmc.Material, openmc.Universe]]] = None + domains: Optional[Sequence[typing.Union[openmc.Cell, openmc.Material, openmc.Universe]]] = None, + constraints: Optional[Dict[str, Any]] = None ): - super().__init__(strength) + if domains is not None: + warnings.warn("The 'domains' arguments has been replaced by the " + "'constraints' argument.", FutureWarning) + constraints = {'domains': domains} + + super().__init__(strength=strength, constraints=constraints) self._space = None self._angle = None @@ -193,20 +337,8 @@ class IndependentSource(SourceBase): self.energy = energy if time is not None: self.time = time - self.strength = strength self.particle = particle - self._domain_ids = [] - self._domain_type = None - if domains is not None: - if isinstance(domains[0], openmc.Cell): - self.domain_type = 'cell' - elif isinstance(domains[0], openmc.Material): - self.domain_type = 'material' - elif isinstance(domains[0], openmc.Universe): - self.domain_type = 'universe' - self.domain_ids = [d.id for d in domains] - @property def type(self) -> str: return 'independent' @@ -273,24 +405,6 @@ class IndependentSource(SourceBase): cv.check_value('source particle', particle, ['neutron', 'photon']) self._particle = particle - @property - def domain_ids(self): - return self._domain_ids - - @domain_ids.setter - def domain_ids(self, ids): - cv.check_type('domain IDs', ids, Iterable, Real) - self._domain_ids = ids - - @property - def domain_type(self): - return self._domain_type - - @domain_type.setter - def domain_type(self, domain_type): - cv.check_value('domain type', domain_type, ('cell', 'material', 'universe')) - self._domain_type = domain_type - def populate_xml_element(self, element): """Add necessary source information to an XML element @@ -309,11 +423,6 @@ class IndependentSource(SourceBase): element.append(self.energy.to_xml_element('energy')) if self.time is not None: element.append(self.time.to_xml_element('time')) - if self.domain_ids: - dt_elem = ET.SubElement(element, "domain_type") - dt_elem.text = self.domain_type - id_elem = ET.SubElement(element, "domain_ids") - id_elem.text = ' '.join(str(uid) for uid in self.domain_ids) @classmethod def from_xml_element(cls, elem: ET.Element, meshes=None) -> SourceBase: @@ -333,24 +442,8 @@ class IndependentSource(SourceBase): Source generated from XML element """ - domain_type = get_text(elem, "domain_type") - if domain_type is not None: - domain_ids = [int(x) for x in get_text(elem, "domain_ids").split()] - - # Instantiate some throw-away domains that are used by the - # constructor to assign IDs - with warnings.catch_warnings(): - warnings.simplefilter('ignore', openmc.IDWarning) - if domain_type == 'cell': - domains = [openmc.Cell(uid) for uid in domain_ids] - elif domain_type == 'material': - domains = [openmc.Material(uid) for uid in domain_ids] - elif domain_type == 'universe': - domains = [openmc.Universe(uid) for uid in domain_ids] - else: - domains = None - - source = cls(domains=domains) + constraints = cls._get_constraints(elem) + source = cls(constraints=constraints) strength = get_text(elem, 'strength') if strength is not None: @@ -360,10 +453,6 @@ class IndependentSource(SourceBase): if particle is not None: source.particle = particle - filename = get_text(elem, 'file') - if filename is not None: - source.file = filename - space = elem.find('space') if space is not None: source.space = Spatial.from_xml_element(space, meshes) @@ -405,6 +494,19 @@ class MeshSource(SourceBase): multidimensional array whose shape matches the mesh shape. If spatial distributions are set on any of the source objects, they will be ignored during source site sampling. + constraints : dict + Constraints on sampled source particles. Valid keys include 'domains', + 'time_bounds', 'energy_bounds', 'fissionable', and 'rejection_strategy'. + For 'domains', the corresponding value is an iterable of + :class:`openmc.Cell`, :class:`openmc.Material`, or + :class:`openmc.Universe` for which sampled sites must be within. For + 'time_bounds' and 'energy_bounds', the corresponding value is a sequence + of floats giving the lower and upper bounds on time in [s] or energy in + [eV] that the sampled particle must be within. For 'fissionable', the + value is a bool indicating that only sites in fissionable material + should be accepted. The 'rejection_strategy' indicates what should + happen when a source particle is rejected: either 'resample' (pick a new + particle) or 'kill' (accept and terminate). Attributes ---------- @@ -416,9 +518,19 @@ class MeshSource(SourceBase): Strength of the source type : str Indicator of source type: 'mesh' + constraints : dict + Constraints on sampled source particles. Valid keys include + 'domain_type', 'domain_ids', 'time_bounds', 'energy_bounds', + 'fissionable', and 'rejection_strategy'. """ - def __init__(self, mesh: MeshBase, sources: Sequence[SourceBase]): + def __init__( + self, + mesh: MeshBase, + sources: Sequence[SourceBase], + constraints: Optional[Dict[str, Any]] = None, + ): + super().__init__(strength=None, constraints=constraints) self.mesh = mesh self.sources = sources @@ -473,8 +585,9 @@ class MeshSource(SourceBase): @strength.setter def strength(self, val): - cv.check_type('mesh source strength', val, Real) - self.set_total_strength(val) + if val is not None: + cv.check_type('mesh source strength', val, Real) + self.set_total_strength(val) def set_total_strength(self, strength: float): """Scales the element source strengths based on a desired total strength. @@ -531,7 +644,8 @@ class MeshSource(SourceBase): mesh = meshes[mesh_id] sources = [SourceBase.from_xml_element(e) for e in elem.iterchildren('source')] - return cls(mesh, sources) + constraints = cls._get_constraints(elem) + return cls(mesh, sources, constraints=constraints) def Source(*args, **kwargs): @@ -556,6 +670,19 @@ class CompiledSource(SourceBase): Parameters to be provided to the compiled shared library function strength : float Strength of the source + constraints : dict + Constraints on sampled source particles. Valid keys include 'domains', + 'time_bounds', 'energy_bounds', 'fissionable', and 'rejection_strategy'. + For 'domains', the corresponding value is an iterable of + :class:`openmc.Cell`, :class:`openmc.Material`, or + :class:`openmc.Universe` for which sampled sites must be within. For + 'time_bounds' and 'energy_bounds', the corresponding value is a sequence + of floats giving the lower and upper bounds on time in [s] or energy in + [eV] that the sampled particle must be within. For 'fissionable', the + value is a bool indicating that only sites in fissionable material + should be accepted. The 'rejection_strategy' indicates what should + happen when a source particle is rejected: either 'resample' (pick a new + particle) or 'kill' (accept and terminate). Attributes ---------- @@ -567,10 +694,20 @@ class CompiledSource(SourceBase): Strength of the source type : str Indicator of source type: 'compiled' + constraints : dict + Constraints on sampled source particles. Valid keys include + 'domain_type', 'domain_ids', 'time_bounds', 'energy_bounds', + 'fissionable', and 'rejection_strategy'. """ - def __init__(self, library: Optional[str] = None, parameters: Optional[str] = None, strength=1.0) -> None: - super().__init__(strength=strength) + def __init__( + self, + library: Optional[str] = None, + parameters: Optional[str] = None, + strength: float = 1.0, + constraints: Optional[Dict[str, Any]] = None + ) -> None: + super().__init__(strength=strength, constraints=constraints) self._library = None if library is not None: @@ -634,9 +771,10 @@ class CompiledSource(SourceBase): Source generated from XML element """ - library = get_text(elem, 'library') + kwargs = {'constraints': cls._get_constraints(elem)} + kwargs['library'] = get_text(elem, 'library') - source = cls(library) + source = cls(**kwargs) strength = get_text(elem, 'strength') if strength is not None: @@ -660,6 +798,19 @@ class FileSource(SourceBase): Path to the source file from which sites should be sampled strength : float Strength of the source (default is 1.0) + constraints : dict + Constraints on sampled source particles. Valid keys include 'domains', + 'time_bounds', 'energy_bounds', 'fissionable', and 'rejection_strategy'. + For 'domains', the corresponding value is an iterable of + :class:`openmc.Cell`, :class:`openmc.Material`, or + :class:`openmc.Universe` for which sampled sites must be within. For + 'time_bounds' and 'energy_bounds', the corresponding value is a sequence + of floats giving the lower and upper bounds on time in [s] or energy in + [eV] that the sampled particle must be within. For 'fissionable', the + value is a bool indicating that only sites in fissionable material + should be accepted. The 'rejection_strategy' indicates what should + happen when a source particle is rejected: either 'resample' (pick a new + particle) or 'kill' (accept and terminate). Attributes ---------- @@ -669,13 +820,21 @@ class FileSource(SourceBase): Strength of the source type : str Indicator of source type: 'file' + constraints : dict + Constraints on sampled source particles. Valid keys include + 'domain_type', 'domain_ids', 'time_bounds', 'energy_bounds', + 'fissionable', and 'rejection_strategy'. """ - def __init__(self, path: Optional[PathLike] = None, strength=1.0) -> None: - super().__init__(strength=strength) + def __init__( + self, + path: Optional[PathLike] = None, + strength: float = 1.0, + constraints: Optional[Dict[str, Any]] = None + ): + super().__init__(strength=strength, constraints=constraints) self._path = None - if path is not None: self.path = path @@ -722,16 +881,13 @@ class FileSource(SourceBase): Source generated from XML element """ - - filename = get_text(elem, 'file') - - source = cls(filename) - + kwargs = {'constraints': cls._get_constraints(elem)} + kwargs['path'] = get_text(elem, 'file') strength = get_text(elem, 'strength') if strength is not None: - source.strength = float(strength) + kwargs['strength'] = float(strength) - return source + return cls(**kwargs) class ParticleType(IntEnum): diff --git a/openmc/stats/multivariate.py b/openmc/stats/multivariate.py index 3922d601a..212083c3c 100644 --- a/openmc/stats/multivariate.py +++ b/openmc/stats/multivariate.py @@ -4,6 +4,7 @@ from abc import ABC, abstractmethod from collections.abc import Iterable from math import cos, pi from numbers import Real +from warnings import warn import lxml.etree as ET import numpy as np @@ -768,6 +769,9 @@ class Box(Spatial): Whether spatial sites should only be accepted if they occur in fissionable materials + .. deprecated:: 0.14.1 + Use the `constraints` argument when defining a source object instead. + Attributes ---------- lower_left : Iterable of float @@ -778,6 +782,9 @@ class Box(Spatial): Whether spatial sites should only be accepted if they occur in fissionable materials + .. deprecated:: 0.14.1 + Use the `constraints` argument when defining a source object instead. + """ def __init__( @@ -818,6 +825,10 @@ class Box(Spatial): def only_fissionable(self, only_fissionable): cv.check_type('only fissionable', only_fissionable, bool) self._only_fissionable = only_fissionable + if only_fissionable: + warn("The 'only_fissionable' has been deprecated. Use the " + "'constraints' argument when defining a source instead.", + FutureWarning) def to_xml_element(self): """Return XML representation of the box distribution diff --git a/src/distribution_spatial.cpp b/src/distribution_spatial.cpp index 8f34ea6b9..8dbd7d887 100644 --- a/src/distribution_spatial.cpp +++ b/src/distribution_spatial.cpp @@ -281,10 +281,15 @@ void MeshSpatial::check_element_types() const } } +int32_t MeshSpatial::sample_element_index(uint64_t* seed) const +{ + return elem_idx_dist_.sample(seed); +} + std::pair MeshSpatial::sample_mesh(uint64_t* seed) const { // Sample the CDF defined in initialization above - int32_t elem_idx = elem_idx_dist_.sample(seed); + int32_t elem_idx = this->sample_element_index(seed); return {elem_idx, mesh()->sample_element(elem_idx, seed)}; } diff --git a/src/simulation.cpp b/src/simulation.cpp index 43d920606..527d98db4 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -750,7 +750,7 @@ void free_memory_simulation() void transport_history_based_single_particle(Particle& p) { - while (true) { + while (p.alive()) { p.event_calculate_xs(); if (!p.alive()) break; @@ -761,8 +761,6 @@ void transport_history_based_single_particle(Particle& p) p.event_collide(); } p.event_revive_from_secondary(); - if (!p.alive()) - break; } p.event_death(); } diff --git a/src/source.cpp b/src/source.cpp index 5ddac7f42..405de998c 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -47,9 +47,23 @@ vector> external_sources; } //============================================================================== -// Source create implementation +// Source implementation //============================================================================== +Source::Source(pugi::xml_node node) +{ + // Check for source strength + if (check_for_node(node, "strength")) { + strength_ = std::stod(get_node_value(node, "strength")); + if (strength_ < 0.0) { + fatal_error("Source strength is negative."); + } + } + + // Check for additional defined constraints + read_constraints(node); +} + unique_ptr Source::create(pugi::xml_node node) { // if the source type is present, use it to determine the type @@ -79,6 +93,163 @@ unique_ptr Source::create(pugi::xml_node node) } } +void Source::read_constraints(pugi::xml_node node) +{ + // Check for constraints node. For backwards compatibility, if no constraints + // node is given, still try searching for domain constraints from top-level + // node. + pugi::xml_node constraints_node = node.child("constraints"); + if (constraints_node) { + node = constraints_node; + } + + // Check for domains to reject from + if (check_for_node(node, "domain_type")) { + std::string domain_type = get_node_value(node, "domain_type"); + if (domain_type == "cell") { + domain_type_ = DomainType::CELL; + } else if (domain_type == "material") { + domain_type_ = DomainType::MATERIAL; + } else if (domain_type == "universe") { + domain_type_ = DomainType::UNIVERSE; + } else { + fatal_error( + std::string("Unrecognized domain type for constraint: " + domain_type)); + } + + auto ids = get_node_array(node, "domain_ids"); + domain_ids_.insert(ids.begin(), ids.end()); + } + + if (check_for_node(node, "time_bounds")) { + auto ids = get_node_array(node, "time_bounds"); + if (ids.size() != 2) { + fatal_error("Time bounds must be represented by two numbers."); + } + time_bounds_ = std::make_pair(ids[0], ids[1]); + } + if (check_for_node(node, "energy_bounds")) { + auto ids = get_node_array(node, "energy_bounds"); + if (ids.size() != 2) { + fatal_error("Energy bounds must be represented by two numbers."); + } + energy_bounds_ = std::make_pair(ids[0], ids[1]); + } + + if (check_for_node(node, "fissionable")) { + only_fissionable_ = get_node_value_bool(node, "fissionable"); + } + + // Check for how to handle rejected particles + if (check_for_node(node, "rejection_strategy")) { + std::string rejection_strategy = get_node_value(node, "rejection_strategy"); + if (rejection_strategy == "kill") { + rejection_strategy_ = RejectionStrategy::KILL; + } else if (rejection_strategy == "resample") { + rejection_strategy_ = RejectionStrategy::RESAMPLE; + } else { + fatal_error(std::string( + "Unrecognized strategy source rejection: " + rejection_strategy)); + } + } +} + +SourceSite Source::sample_with_constraints(uint64_t* seed) const +{ + bool accepted = false; + static int n_reject = 0; + static int n_accept = 0; + SourceSite site; + + while (!accepted) { + // Sample a source site without considering constraints yet + site = this->sample(seed); + + if (constraints_applied()) { + accepted = true; + } else { + // Check whether sampled site satisfies constraints + accepted = satisfies_spatial_constraints(site.r) && + satisfies_energy_constraints(site.E) && + satisfies_time_constraints(site.time); + if (!accepted) { + ++n_reject; + if (n_reject >= EXTSRC_REJECT_THRESHOLD && + static_cast(n_accept) / n_reject <= + EXTSRC_REJECT_FRACTION) { + fatal_error("More than 95% of external source sites sampled were " + "rejected. Please check your source definition."); + } + + // For the "kill" strategy, accept particle but set weight to 0 so that + // it is terminated immediately + if (rejection_strategy_ == RejectionStrategy::KILL) { + accepted = true; + site.wgt = 0.0; + } + } + } + } + + // Increment number of accepted samples + ++n_accept; + + return site; +} + +bool Source::satisfies_energy_constraints(double E) const +{ + return E > energy_bounds_.first && E < energy_bounds_.second; +} + +bool Source::satisfies_time_constraints(double time) const +{ + return time > time_bounds_.first && time < time_bounds_.second; +} + +bool Source::satisfies_spatial_constraints(Position r) const +{ + GeometryState geom_state; + geom_state.r() = r; + + // Reject particle if it's not in the geometry at all + bool found = exhaustive_find_cell(geom_state); + if (!found) + return false; + + // Check the geometry state against specified domains + bool accepted = true; + if (!domain_ids_.empty()) { + if (domain_type_ == DomainType::MATERIAL) { + auto mat_index = geom_state.material(); + if (mat_index != MATERIAL_VOID) { + accepted = contains(domain_ids_, model::materials[mat_index]->id()); + } + } else { + for (int i = 0; i < geom_state.n_coord(); i++) { + auto id = (domain_type_ == DomainType::CELL) + ? model::cells[geom_state.coord(i).cell]->id_ + : model::universes[geom_state.coord(i).universe]->id_; + if ((accepted = contains(domain_ids_, id))) + break; + } + } + } + + // Check if spatial site is in fissionable material + if (accepted && only_fissionable_) { + // Determine material + auto mat_index = geom_state.material(); + if (mat_index == MATERIAL_VOID) { + accepted = false; + } else { + accepted = model::materials[mat_index]->fissionable(); + } + } + + return accepted; +} + //============================================================================== // IndependentSource implementation //============================================================================== @@ -89,7 +260,7 @@ IndependentSource::IndependentSource( energy_ {std::move(energy)}, time_ {std::move(time)} {} -IndependentSource::IndependentSource(pugi::xml_node node) +IndependentSource::IndependentSource(pugi::xml_node node) : Source(node) { // Check for particle type if (check_for_node(node, "particle")) { @@ -104,11 +275,6 @@ IndependentSource::IndependentSource(pugi::xml_node node) } } - // Check for source strength - if (check_for_node(node, "strength")) { - strength_ = std::stod(get_node_value(node, "strength")); - } - // Check for external source file if (check_for_node(node, "file")) { @@ -122,6 +288,15 @@ IndependentSource::IndependentSource(pugi::xml_node node) space_ = UPtrSpace {new SpatialPoint()}; } + // For backwards compatibility, check for only fissionable setting on box + // source + auto space_box = dynamic_cast(space_.get()); + if (space_box) { + if (!only_fissionable_) { + only_fissionable_ = space_box->only_fissionable(); + } + } + // Determine external source angular distribution if (check_for_node(node, "angle")) { angle_ = UnitSphereDistribution::create(node.child("angle")); @@ -148,24 +323,6 @@ IndependentSource::IndependentSource(pugi::xml_node node) double p[] {1.0}; time_ = UPtrDist {new Discrete {T, p, 1}}; } - - // Check for domains to reject from - if (check_for_node(node, "domain_type")) { - std::string domain_type = get_node_value(node, "domain_type"); - if (domain_type == "cell") { - domain_type_ = DomainType::CELL; - } else if (domain_type == "material") { - domain_type_ = DomainType::MATERIAL; - } else if (domain_type == "universe") { - domain_type_ = DomainType::UNIVERSE; - } else { - fatal_error(std::string( - "Unrecognized domain type for source rejection: " + domain_type)); - } - - auto ids = get_node_array(node, "domain_ids"); - domain_ids_.insert(ids.begin(), ids.end()); - } } } @@ -174,60 +331,21 @@ SourceSite IndependentSource::sample(uint64_t* seed) const SourceSite site; site.particle = particle_; - // Repeat sampling source location until a good site has been found - bool found = false; - int n_reject = 0; + // Repeat sampling source location until a good site has been accepted + bool accepted = false; + static int n_reject = 0; static int n_accept = 0; - while (!found) { - // Set particle type - Particle p; - p.type() = particle_; - p.u() = {0.0, 0.0, 1.0}; + while (!accepted) { // Sample spatial distribution - p.r() = space_->sample(seed); + site.r = space_->sample(seed); - // Now search to see if location exists in geometry - found = exhaustive_find_cell(p); - - // Check if spatial site is in fissionable material - if (found) { - auto space_box = dynamic_cast(space_.get()); - if (space_box) { - if (space_box->only_fissionable()) { - // Determine material - auto mat_index = p.material(); - if (mat_index == MATERIAL_VOID) { - found = false; - } else { - found = model::materials[mat_index]->fissionable(); - } - } - } - - // Rejection based on cells/materials/universes - if (!domain_ids_.empty()) { - found = false; - if (domain_type_ == DomainType::MATERIAL) { - auto mat_index = p.material(); - if (mat_index != MATERIAL_VOID) { - found = contains(domain_ids_, model::materials[mat_index]->id()); - } - } else { - for (int i = 0; i < p.n_coord(); i++) { - auto id = (domain_type_ == DomainType::CELL) - ? model::cells[p.coord(i).cell]->id_ - : model::universes[p.coord(i).universe]->id_; - if ((found = contains(domain_ids_, id))) - break; - } - } - } - } + // Check if sampled position satisfies spatial constraints + accepted = satisfies_spatial_constraints(site.r); // Check for rejection - if (!found) { + if (!accepted) { ++n_reject; if (n_reject >= EXTSRC_REJECT_THRESHOLD && static_cast(n_accept) / n_reject <= EXTSRC_REJECT_FRACTION) { @@ -236,8 +354,6 @@ SourceSite IndependentSource::sample(uint64_t* seed) const "definition."); } } - - site.r = p.r(); } // Sample angle @@ -261,7 +377,8 @@ SourceSite IndependentSource::sample(uint64_t* seed) const site.E = energy_->sample(seed); // Resample if energy falls above maximum particle energy - if (site.E < data::energy_max[p]) + if (site.E < data::energy_max[p] and + (satisfies_energy_constraints(site.E))) break; n_reject++; @@ -287,7 +404,8 @@ SourceSite IndependentSource::sample(uint64_t* seed) const //============================================================================== // FileSource implementation //============================================================================== -FileSource::FileSource(pugi::xml_node node) + +FileSource::FileSource(pugi::xml_node node) : Source(node) { auto path = get_node_value(node, "file", false, true); if (ends_with(path, ".mcpl") || ends_with(path, ".mcpl.gz")) { @@ -332,6 +450,7 @@ void FileSource::load_sites_from_file(const std::string& path) SourceSite FileSource::sample(uint64_t* seed) const { + // Sample a particle randomly from list size_t i_site = sites_.size() * prn(seed); return sites_[i_site]; } @@ -339,7 +458,8 @@ SourceSite FileSource::sample(uint64_t* seed) const //============================================================================== // CompiledSourceWrapper implementation //============================================================================== -CompiledSourceWrapper::CompiledSourceWrapper(pugi::xml_node node) + +CompiledSourceWrapper::CompiledSourceWrapper(pugi::xml_node node) : Source(node) { // Get shared library path and parameters auto path = get_node_value(node, "library", false, true); @@ -403,7 +523,7 @@ CompiledSourceWrapper::~CompiledSourceWrapper() // MeshSource implementation //============================================================================== -MeshSource::MeshSource(pugi::xml_node node) +MeshSource::MeshSource(pugi::xml_node node) : Source(node) { int32_t mesh_id = stoi(get_node_value(node, "mesh")); int32_t mesh_idx = model::mesh_map.at(mesh_id); @@ -430,15 +550,27 @@ MeshSource::MeshSource(pugi::xml_node node) SourceSite MeshSource::sample(uint64_t* seed) const { - // sample location and element from mesh - auto mesh_location = space_->sample_mesh(seed); + // Sample the CDF defined in initialization above + int32_t element = space_->sample_element_index(seed); - // Sample source for the chosen element - int32_t element = mesh_location.first; - SourceSite site = source(element)->sample(seed); + // Sample position and apply rejection on spatial domains + Position r; + do { + r = space_->mesh()->sample_element(element, seed); + } while (!this->satisfies_spatial_constraints(r)); - // Replace spatial position with the one already sampled - site.r = mesh_location.second; + SourceSite site; + while (true) { + // Sample source for the chosen element and replace the position + site = source(element)->sample_with_constraints(seed); + site.r = r; + + // Apply other rejections + if (satisfies_energy_constraints(site.E) && + satisfies_time_constraints(site.time)) { + break; + } + } return site; } @@ -493,7 +625,7 @@ SourceSite sample_external_source(uint64_t* seed) } // Sample source site from i-th source distribution - SourceSite site {model::external_sources[i]->sample(seed)}; + SourceSite site {model::external_sources[i]->sample_with_constraints(seed)}; // If running in MG, convert site.E to group if (!settings::run_CE) { diff --git a/tests/regression_tests/asymmetric_lattice/inputs_true.dat b/tests/regression_tests/asymmetric_lattice/inputs_true.dat index 3977220da..302ef24c2 100644 --- a/tests/regression_tests/asymmetric_lattice/inputs_true.dat +++ b/tests/regression_tests/asymmetric_lattice/inputs_true.dat @@ -209,9 +209,12 @@ 10 5 - + -32 -32 0 32 32 32 + + true + diff --git a/tests/regression_tests/asymmetric_lattice/test.py b/tests/regression_tests/asymmetric_lattice/test.py index 0d17a6c98..fadf272ff 100644 --- a/tests/regression_tests/asymmetric_lattice/test.py +++ b/tests/regression_tests/asymmetric_lattice/test.py @@ -54,8 +54,10 @@ class AsymmetricLatticeTestHarness(PyAPITestHarness): self._model.tallies.append(tally) # Specify summary output and correct source sampling box - self._model.settings.source = openmc.IndependentSource(space=openmc.stats.Box( - [-32, -32, 0], [32, 32, 32], only_fissionable = True)) + self._model.settings.source = openmc.IndependentSource( + space=openmc.stats.Box([-32, -32, 0], [32, 32, 32]), + constraints={'fissionable': True} + ) def _get_results(self, hash_output=True): """Digest info in statepoint and summary and return as a string.""" diff --git a/tests/regression_tests/mg_temperature_multi/inputs_true.dat b/tests/regression_tests/mg_temperature_multi/inputs_true.dat index b2ad5063c..0c452cd52 100644 --- a/tests/regression_tests/mg_temperature_multi/inputs_true.dat +++ b/tests/regression_tests/mg_temperature_multi/inputs_true.dat @@ -28,9 +28,12 @@ 10 5 - + -0.63 -0.63 -1 0.63 0.63 1 + + true + multi-group diff --git a/tests/regression_tests/mg_temperature_multi/test.py b/tests/regression_tests/mg_temperature_multi/test.py index 404c8c0cd..3117e29ba 100755 --- a/tests/regression_tests/mg_temperature_multi/test.py +++ b/tests/regression_tests/mg_temperature_multi/test.py @@ -133,8 +133,9 @@ def test_mg_temperature_multi(): # Create an initial uniform spatial source distribution over fissionable zones lower_left = (-pitch/2, -pitch/2, -1) upper_right = (pitch/2, pitch/2, 1) - uniform_dist = openmc.stats.Box(lower_left, upper_right, only_fissionable=True) - settings.source = openmc.IndependentSource(space=uniform_dist) + uniform_dist = openmc.stats.Box(lower_left, upper_right) + settings.source = openmc.IndependentSource( + space=uniform_dist, constraints={'fissionable': True}) ############################################################################### # Define tallies diff --git a/tests/regression_tests/mgxs_library_ce_to_mg/inputs_true.dat b/tests/regression_tests/mgxs_library_ce_to_mg/inputs_true.dat index d9a3c52c8..6c071263f 100644 --- a/tests/regression_tests/mgxs_library_ce_to_mg/inputs_true.dat +++ b/tests/regression_tests/mgxs_library_ce_to_mg/inputs_true.dat @@ -42,9 +42,12 @@ 10 5 - + -0.63 -0.63 -1 0.63 0.63 1 + + true + diff --git a/tests/regression_tests/mgxs_library_ce_to_mg_nuclides/inputs_true.dat b/tests/regression_tests/mgxs_library_ce_to_mg_nuclides/inputs_true.dat index f84adc329..ad3d50974 100644 --- a/tests/regression_tests/mgxs_library_ce_to_mg_nuclides/inputs_true.dat +++ b/tests/regression_tests/mgxs_library_ce_to_mg_nuclides/inputs_true.dat @@ -42,9 +42,12 @@ 10 5 - + -0.63 -0.63 -1 0.63 0.63 1 + + true + diff --git a/tests/regression_tests/mgxs_library_condense/inputs_true.dat b/tests/regression_tests/mgxs_library_condense/inputs_true.dat index 93519cd2a..8606e9fdd 100644 --- a/tests/regression_tests/mgxs_library_condense/inputs_true.dat +++ b/tests/regression_tests/mgxs_library_condense/inputs_true.dat @@ -42,9 +42,12 @@ 10 5 - + -0.63 -0.63 -1 0.63 0.63 1 + + true + diff --git a/tests/regression_tests/mgxs_library_correction/inputs_true.dat b/tests/regression_tests/mgxs_library_correction/inputs_true.dat index 80cbc4bb5..ea2d1b714 100644 --- a/tests/regression_tests/mgxs_library_correction/inputs_true.dat +++ b/tests/regression_tests/mgxs_library_correction/inputs_true.dat @@ -42,9 +42,12 @@ 10 5 - + -0.63 -0.63 -1 0.63 0.63 1 + + true + diff --git a/tests/regression_tests/mgxs_library_distribcell/inputs_true.dat b/tests/regression_tests/mgxs_library_distribcell/inputs_true.dat index fc2c06d1c..f1a605334 100644 --- a/tests/regression_tests/mgxs_library_distribcell/inputs_true.dat +++ b/tests/regression_tests/mgxs_library_distribcell/inputs_true.dat @@ -69,9 +69,12 @@ 10 5 - + -10.71 -10.71 -1 10.71 10.71 1 + + true + diff --git a/tests/regression_tests/mgxs_library_hdf5/inputs_true.dat b/tests/regression_tests/mgxs_library_hdf5/inputs_true.dat index 93519cd2a..8606e9fdd 100644 --- a/tests/regression_tests/mgxs_library_hdf5/inputs_true.dat +++ b/tests/regression_tests/mgxs_library_hdf5/inputs_true.dat @@ -42,9 +42,12 @@ 10 5 - + -0.63 -0.63 -1 0.63 0.63 1 + + true + diff --git a/tests/regression_tests/mgxs_library_histogram/inputs_true.dat b/tests/regression_tests/mgxs_library_histogram/inputs_true.dat index 178a44464..013fcd0fa 100644 --- a/tests/regression_tests/mgxs_library_histogram/inputs_true.dat +++ b/tests/regression_tests/mgxs_library_histogram/inputs_true.dat @@ -42,9 +42,12 @@ 10 5 - + -0.63 -0.63 -1 0.63 0.63 1 + + true + diff --git a/tests/regression_tests/mgxs_library_no_nuclides/inputs_true.dat b/tests/regression_tests/mgxs_library_no_nuclides/inputs_true.dat index 0003413ac..5cad36b7e 100644 --- a/tests/regression_tests/mgxs_library_no_nuclides/inputs_true.dat +++ b/tests/regression_tests/mgxs_library_no_nuclides/inputs_true.dat @@ -42,9 +42,12 @@ 10 5 - + -0.63 -0.63 -1 0.63 0.63 1 + + true + diff --git a/tests/regression_tests/mgxs_library_nuclides/inputs_true.dat b/tests/regression_tests/mgxs_library_nuclides/inputs_true.dat index 97e227177..9b3a22510 100644 --- a/tests/regression_tests/mgxs_library_nuclides/inputs_true.dat +++ b/tests/regression_tests/mgxs_library_nuclides/inputs_true.dat @@ -42,9 +42,12 @@ 10 5 - + -0.63 -0.63 -1 0.63 0.63 1 + + true + diff --git a/tests/regression_tests/mgxs_library_specific_nuclides/inputs_true.dat b/tests/regression_tests/mgxs_library_specific_nuclides/inputs_true.dat index b73ed179e..1372741c9 100644 --- a/tests/regression_tests/mgxs_library_specific_nuclides/inputs_true.dat +++ b/tests/regression_tests/mgxs_library_specific_nuclides/inputs_true.dat @@ -42,9 +42,12 @@ 10 5 - + -0.63 -0.63 -1 0.63 0.63 1 + + true + diff --git a/tests/regression_tests/surface_tally/inputs_true.dat b/tests/regression_tests/surface_tally/inputs_true.dat index abd01266d..192c2e89f 100644 --- a/tests/regression_tests/surface_tally/inputs_true.dat +++ b/tests/regression_tests/surface_tally/inputs_true.dat @@ -30,9 +30,12 @@ 10 0 - + -0.62992 -0.62992 -1 0.62992 0.62992 1 + + true + diff --git a/tests/regression_tests/surface_tally/test.py b/tests/regression_tests/surface_tally/test.py index 8968db9b6..e496ac0f6 100644 --- a/tests/regression_tests/surface_tally/test.py +++ b/tests/regression_tests/surface_tally/test.py @@ -72,9 +72,9 @@ class SurfaceTallyTestHarness(PyAPITestHarness): # Create an initial uniform spatial source distribution bounds = [-0.62992, -0.62992, -1, 0.62992, 0.62992, 1] - uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:],\ - only_fissionable=True) - settings_file.source = openmc.IndependentSource(space=uniform_dist) + uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:],) + settings_file.source = openmc.IndependentSource( + space=uniform_dist, constraints={'fissionable': True}) self._model.settings = settings_file # Tallies file diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index 1310a6a7f..009a90968 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -67,8 +67,10 @@ def uo2_trigger_model(): model.settings.batches = 10 model.settings.inactive = 5 model.settings.particles = 100 - model.settings.source = openmc.IndependentSource(space=openmc.stats.Box( - [-0.5, -0.5, -1], [0.5, 0.5, 1], only_fissionable=True)) + model.settings.source = openmc.IndependentSource( + space=openmc.stats.Box([-0.5, -0.5, -1], [0.5, 0.5, 1]), + constraints={'fissionable': True}, + ) model.settings.verbosity = 1 model.settings.keff_trigger = {'type': 'std_dev', 'threshold': 0.001} model.settings.trigger_active = True diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 16fa18d45..404235bef 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -57,9 +57,9 @@ def pin_model_attributes(): # Create a uniform spatial source distribution over fissionable zones bounds = [-0.62992, -0.62992, -1, 0.62992, 0.62992, 1] - uniform_dist = openmc.stats.Box( - bounds[:3], bounds[3:], only_fissionable=True) - settings.source = openmc.IndependentSource(space=uniform_dist) + uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:]) + settings.source = openmc.IndependentSource( + space=uniform_dist, constraints={'fissionable': True}) entropy_mesh = openmc.RegularMesh() entropy_mesh.lower_left = [-0.39218, -0.39218, -1.e50] diff --git a/tests/unit_tests/test_source.py b/tests/unit_tests/test_source.py index 4783ff8f1..9a19f6f24 100644 --- a/tests/unit_tests/test_source.py +++ b/tests/unit_tests/test_source.py @@ -7,6 +7,8 @@ import numpy as np import pytest from pytest import approx +from tests.regression_tests import config + def test_source(): space = openmc.stats.Point() @@ -100,7 +102,8 @@ def test_source_xml_roundtrip(): assert new_src.strength == approx(src.strength) -def test_rejection(run_in_tmpdir): +@pytest.fixture +def sphere_box_model(): # Model with two spheres inside a box mat = openmc.Material() mat.add_nuclide('H1', 1.0) @@ -119,24 +122,108 @@ def test_rejection(run_in_tmpdir): model.settings.batches = 10 model.settings.run_mode = 'fixed source' + return model, cell1, cell2, cell3 + + +def test_constraints_independent(sphere_box_model, run_in_tmpdir): + model, cell1, cell2, cell3 = sphere_box_model + # Set up a box source with rejection on the spherical cell - space = openmc.stats.Box(*cell3.bounding_box) - model.settings.source = openmc.IndependentSource(space=space, domains=[cell1, cell2]) + space = openmc.stats.Box((-4., -1., -1.), (4., 1., 1.)) + model.settings.source = openmc.IndependentSource( + space=space, constraints={'domains': [cell1, cell2]} + ) # Load up model via openmc.lib and sample source - model.export_to_xml() + model.export_to_model_xml() openmc.lib.init() particles = openmc.lib.sample_external_source(1000) # Make sure that all sampled sources are within one of the spheres - joint_region = cell1.region | cell2.region for p in particles: - assert p.r in joint_region - assert p.r not in non_source_region + assert p.r in (cell1.region | cell2.region) + assert p.r not in cell3.region openmc.lib.finalize() +def test_constraints_mesh(sphere_box_model, run_in_tmpdir): + model, cell1, cell2, cell3 = sphere_box_model + + bbox = cell3.bounding_box + mesh = openmc.RegularMesh() + mesh.lower_left = bbox.lower_left + mesh.upper_right = bbox.upper_right + mesh.dimension = (2, 1, 1) + + left_source = openmc.IndependentSource() + right_source = openmc.IndependentSource() + model.settings.source = openmc.MeshSource( + mesh, [left_source, right_source], constraints={'domains': [cell1, cell2]} + ) + + # Load up model via openmc.lib and sample source + model.export_to_model_xml() + openmc.lib.init() + particles = openmc.lib.sample_external_source(1000) + + # Make sure that all sampled sources are within one of the spheres + for p in particles: + assert p.r in (cell1.region | cell2.region) + assert p.r not in cell3.region + + openmc.lib.finalize() + + +def test_constraints_file(sphere_box_model, run_in_tmpdir): + model = sphere_box_model[0] + + # Create source file with randomly sampled source sites + rng = np.random.default_rng() + energy = rng.uniform(0., 1e6, 10_000) + time = rng.uniform(0., 1., 10_000) + particles = [openmc.SourceParticle(E=e, time=t) for e, t in zip(energy, time)] + openmc.write_source_file(particles, 'uniform_source.h5') + + # Use source file + model.settings.source = openmc.FileSource( + 'uniform_source.h5', + constraints={ + 'time_bounds': [0.25, 0.75], + 'energy_bounds': [500.e3, 1.0e6], + } + ) + + # Load up model via openmc.lib and sample source + model.export_to_model_xml() + openmc.lib.init() + particles = openmc.lib.sample_external_source(1000) + + # Make sure that all sampled sources are within energy/time bounds + for p in particles: + assert 0.25 <= p.time <= 0.75 + assert 500.e3 <= p.E <= 1.0e6 + + openmc.lib.finalize() + + +@pytest.mark.skipif(config['mpi'], reason='Not compatible with MPI') +def test_rejection_limit(sphere_box_model, run_in_tmpdir): + model, cell1 = sphere_box_model[:2] + + # Define a point source that will get rejected 100% of the time + model.settings.source = openmc.IndependentSource( + space=openmc.stats.Point((-3., 0., 0.)), + constraints={'domains': [cell1]} + ) + + # Confirm that OpenMC doesn't run in an infinite loop. Note that this may + # work when running with MPI since it won't necessarily capture the error + # message correctly + with pytest.raises(RuntimeError, match="rejected"): + model.run(openmc_exec=config['exe']) + + def test_exceptions(): with pytest.raises(AttributeError, match=r'Please use the FileSource class'): @@ -156,4 +243,4 @@ def test_exceptions(): with pytest.raises(AttributeError, match=r'has no attribute \'frisbee\''): s = openmc.IndependentSource() - s.frisbee \ No newline at end of file + s.frisbee From 25e47dea9bc1857272bb7a69642b868f4c27fe08 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Fri, 24 May 2024 18:56:20 -0500 Subject: [PATCH 105/671] Apply memoization in `get_all_universes` (#2995) Co-authored-by: Paul Romano --- openmc/cell.py | 33 ++++++++++++++++++--------------- openmc/geometry.py | 6 +++--- openmc/lattice.py | 36 +++++++++++++++++++++++------------- openmc/tallies.py | 1 + openmc/universe.py | 42 +++++++++++++++++++++++++++--------------- 5 files changed, 72 insertions(+), 46 deletions(-) diff --git a/openmc/cell.py b/openmc/cell.py index 94fac8413..fe3939bbe 100644 --- a/openmc/cell.py +++ b/openmc/cell.py @@ -426,15 +426,13 @@ class Cell(IDManagerMixin): instances """ + if memo is None: + memo = set() + elif self in memo: + return {} + memo.add(self) cells = {} - - if memo and self in memo: - return cells - - if memo is not None: - memo.add(self) - if self.fill_type in ('universe', 'lattice'): cells.update(self.fill.get_all_cells(memo)) @@ -465,7 +463,7 @@ class Cell(IDManagerMixin): return materials - def get_all_universes(self): + def get_all_universes(self, memo=None): """Return all universes that are contained within this one if any of its cells are filled with a universe or lattice. @@ -476,18 +474,22 @@ class Cell(IDManagerMixin): :class:`Universe` instances """ + if memo is None: + memo = set() + if self in memo: + return {} + memo.add(self) universes = {} - if self.fill_type == 'universe': universes[self.fill.id] = self.fill - universes.update(self.fill.get_all_universes()) + universes.update(self.fill.get_all_universes(memo)) elif self.fill_type == 'lattice': - universes.update(self.fill.get_all_universes()) + universes.update(self.fill.get_all_universes(memo)) return universes - def clone(self, clone_materials=True, clone_regions=True, memo=None): + def clone(self, clone_materials=True, clone_regions=True, memo=None): """Create a copy of this cell with a new unique ID, and clones the cell's region and fill. @@ -678,10 +680,11 @@ class Cell(IDManagerMixin): # thus far. def create_surface_elements(node, element, memo=None): if isinstance(node, Halfspace): - if memo and node.surface in memo: + if memo is None: + memo = set() + elif node.surface in memo: return - if memo is not None: - memo.add(node.surface) + memo.add(node.surface) xml_element.append(node.surface.to_xml_element()) elif isinstance(node, Complement): diff --git a/openmc/geometry.py b/openmc/geometry.py index 175cef2bf..db927c46e 100644 --- a/openmc/geometry.py +++ b/openmc/geometry.py @@ -134,7 +134,7 @@ class Geometry: # Create XML representation element = ET.Element("geometry") - self.root_universe.create_xml_subelement(element, memo=set()) + self.root_universe.create_xml_subelement(element) # Sort the elements in the file element[:] = sorted(element, key=lambda x: ( @@ -373,7 +373,7 @@ class Geometry: """ if self.root_universe is not None: - return self.root_universe.get_all_cells(memo=set()) + return self.root_universe.get_all_cells() else: return {} @@ -417,7 +417,7 @@ class Geometry: """ if self.root_universe is not None: - return self.root_universe.get_all_materials(memo=set()) + return self.root_universe.get_all_materials() else: return {} diff --git a/openmc/lattice.py b/openmc/lattice.py index 40b97f6cf..6282cf21c 100644 --- a/openmc/lattice.py +++ b/openmc/lattice.py @@ -170,11 +170,11 @@ class Lattice(IDManagerMixin, ABC): """ cells = {} - if memo and self in memo: + if memo is None: + memo = set() + elif self in memo: return cells - - if memo is not None: - memo.add(self) + memo.add(self) unique_universes = self.get_unique_universes() @@ -194,6 +194,9 @@ class Lattice(IDManagerMixin, ABC): """ + if memo is None: + memo = set() + materials = {} # Append all Cells in each Cell in the Universe to the dictionary @@ -203,7 +206,7 @@ class Lattice(IDManagerMixin, ABC): return materials - def get_all_universes(self): + def get_all_universes(self, memo=None): """Return all universes that are contained within the lattice Returns @@ -213,11 +216,16 @@ class Lattice(IDManagerMixin, ABC): :class:`Universe` instances """ - # Initialize a dictionary of all Universes contained by the Lattice # in each nested Universe level all_universes = {} + if memo is None: + memo = set() + elif self in memo: + return all_universes + memo.add(self) + # Get all unique Universes contained in each of the lattice cells unique_universes = self.get_unique_universes() @@ -226,7 +234,7 @@ class Lattice(IDManagerMixin, ABC): # Append all Universes containing each cell to the dictionary for universe in unique_universes.values(): - all_universes.update(universe.get_all_universes()) + all_universes.update(universe.get_all_universes(memo)) return all_universes @@ -846,10 +854,11 @@ class RectLattice(Lattice): """ # If the element already contains the Lattice subelement, then return - if memo and self in memo: + if memo is None: + memo = set() + elif self in memo: return - if memo is not None: - memo.add(self) + memo.add(self) # Make sure universes have been assigned if self.universes is None: @@ -1417,10 +1426,11 @@ class HexLattice(Lattice): def create_xml_subelement(self, xml_element, memo=None): # If this subelement has already been written, return - if memo and self in memo: + if memo is None: + memo = set() + elif self in memo: return - if memo is not None: - memo.add(self) + memo.add(self) lattice_subelement = ET.Element("hex_lattice") lattice_subelement.set("id", str(self._id)) diff --git a/openmc/tallies.py b/openmc/tallies.py index 04ae00b6a..f39ffa078 100644 --- a/openmc/tallies.py +++ b/openmc/tallies.py @@ -3209,6 +3209,7 @@ class Tallies(cv.CheckedList): def to_xml_element(self, memo=None): """Creates a 'tallies' element to be written to an XML file. """ + memo = memo if memo is not None else set() element = ET.Element("tallies") self._create_mesh_subelements(element, memo) self._create_filter_subelements(element) diff --git a/openmc/universe.py b/openmc/universe.py index 2e86ab05a..9fab9ae51 100644 --- a/openmc/universe.py +++ b/openmc/universe.py @@ -90,7 +90,7 @@ class UniverseBase(ABC, IDManagerMixin): else: raise ValueError('No volume information found for this universe.') - def get_all_universes(self): + def get_all_universes(self, memo=None): """Return all universes that are contained within this one. Returns @@ -100,10 +100,16 @@ class UniverseBase(ABC, IDManagerMixin): :class:`Universe` instances """ + if memo is None: + memo = set() + elif self in memo: + return {} + memo.add(self) + # Append all Universes within each Cell to the dictionary universes = {} for cell in self.get_all_cells().values(): - universes.update(cell.get_all_universes()) + universes.update(cell.get_all_universes(memo)) return universes @@ -639,15 +645,14 @@ class Universe(UniverseBase): """ - cells = {} - - if memo and self in memo: - return cells - - if memo is not None: - memo.add(self) + if memo is None: + memo = set() + elif self in memo: + return {} + memo.add(self) # Add this Universe's cells to the dictionary + cells = {} cells.update(self._cells) # Append all Cells in each Cell in the Universe to the dictionary @@ -667,6 +672,9 @@ class Universe(UniverseBase): """ + if memo is None: + memo = set() + materials = {} # Append all Cells in each Cell in the Universe to the dictionary @@ -677,15 +685,17 @@ class Universe(UniverseBase): return materials def create_xml_subelement(self, xml_element, memo=None): + if memo is None: + memo = set() + # Iterate over all Cells for cell in self._cells.values(): # If the cell was already written, move on - if memo and cell in memo: + if cell in memo: continue - if memo is not None: - memo.add(cell) + memo.add(cell) # Create XML subelement for this Cell cell_element = cell.create_xml_subelement(xml_element, memo) @@ -928,11 +938,13 @@ class DAGMCUniverse(UniverseBase): return self._n_geom_elements('surface') def create_xml_subelement(self, xml_element, memo=None): - if memo and self in memo: + if memo is None: + memo = set() + + if self in memo: return - if memo is not None: - memo.add(self) + memo.add(self) # Set xml element values dagmc_element = ET.Element('dagmc_universe') From 12ecc17997a65e925f9ee378c4e83e7462931796 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Fri, 24 May 2024 19:50:59 -0500 Subject: [PATCH 106/671] Hexagonal Lattice Roundtrip (#3003) Co-authored-by: Paul Romano --- openmc/lattice.py | 2 ++ tests/unit_tests/test_lattice.py | 43 ++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/openmc/lattice.py b/openmc/lattice.py index 6282cf21c..f7f9da8ea 100644 --- a/openmc/lattice.py +++ b/openmc/lattice.py @@ -1574,6 +1574,7 @@ class HexLattice(Lattice): alpha -= 1 if not lat.is_valid_index((x, alpha, z)): # Reached the bottom + j += 1 break j += 1 else: @@ -1593,6 +1594,7 @@ class HexLattice(Lattice): # Check if we've reached the bottom if y == -n_rings: + j += 1 break while not lat.is_valid_index((alpha, y, z)): diff --git a/tests/unit_tests/test_lattice.py b/tests/unit_tests/test_lattice.py index 6fa32760e..d72d9c5c3 100644 --- a/tests/unit_tests/test_lattice.py +++ b/tests/unit_tests/test_lattice.py @@ -377,3 +377,46 @@ def test_unset_universes(): hex_lattice.pitch = (1.,) with pytest.raises(ValueError): hex_lattice.create_xml_subelement(elem) + + +@pytest.mark.parametrize("orientation", ['x', 'y']) +def test_hex_lattice_roundtrip(orientation): + openmc.reset_auto_ids() + + # ensure that the lattice universes are the same on all axial levels + def check_lattice_universes(og_lattice, xml_lattice): + for axial_og, axial_rt in zip(og_lattice.universes, xml_lattice.universes): + for ring_og, ring_rt in zip(axial_og, axial_rt): + assert [u.id for u in ring_og] == [u.id for u in ring_rt] + + latt = openmc.HexLattice() + latt.pitch = (1.0, 1.0) + latt.center = (0.0, 0.0, 0.0) + latt.orientation = orientation + + # fill the lattice with universes in increasing order and repeat for + # the second actial level + lvl_one_univs = [openmc.Universe(cells=[openmc.Cell()]) for _ in range(19)] + lvl_one_univs = [lvl_one_univs[-12:], lvl_one_univs[1:7], lvl_one_univs[:1]] + latt.universes = [lvl_one_univs, lvl_one_univs] + + geom = openmc.Geometry([openmc.Cell(fill=latt)]) + geom.export_to_xml() + + xml_geom = openmc.Geometry.from_xml(materials=openmc.Materials()) + + xml_latt = xml_geom.get_all_lattices()[latt.id] + + check_lattice_universes(latt, xml_latt) + + # same test but with unique universes for each axial level + lvl_two_univs = [openmc.Universe(cells=[openmc.Cell()]) for _ in range(19)] + lvl_two_univs = [lvl_two_univs[-12:], lvl_two_univs[1:7], lvl_two_univs[:1]] + latt.universes = [lvl_one_univs, lvl_two_univs] + + geom.export_to_xml() + + xml_geom = openmc.Geometry.from_xml(materials=openmc.Materials()) + xml_latt = xml_geom.get_all_lattices()[latt.id] + + check_lattice_universes(latt, xml_latt) From 18cd81a6aad6a6aabc633571f0fd7030dbfcf89d Mon Sep 17 00:00:00 2001 From: Chris Wagner <72230352+chrwagne@users.noreply.github.com> Date: Fri, 24 May 2024 23:27:37 -0400 Subject: [PATCH 107/671] added extra error checking on spherical mesh creation (#2973) Co-authored-by: Jonathan Shimwell Co-authored-by: Paul Romano --- openmc/checkvalue.py | 23 +++++++++++++++++++++++ openmc/mesh.py | 6 ++++++ tests/unit_tests/test_mesh.py | 21 +++++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/openmc/checkvalue.py b/openmc/checkvalue.py index 840306486..42df3e5ef 100644 --- a/openmc/checkvalue.py +++ b/openmc/checkvalue.py @@ -171,6 +171,29 @@ def check_length(name, value, length_min, length_max=None): raise ValueError(msg) +def check_increasing(name: str, value, equality: bool = False): + """Ensure that a list's elements are strictly or loosely increasing. + + Parameters + ---------- + name : str + Description of value being checked + value : iterable + Object to check if increasing + equality : bool, optional + Whether equality is allowed. Defaults to False. + + """ + if equality: + if not np.all(np.diff(value) >= 0.0): + raise ValueError(f'Unable to set "{name}" to "{value}" since its ' + 'elements must be increasing.') + elif not equality: + if not np.all(np.diff(value) > 0.0): + raise ValueError(f'Unable to set "{name}" to "{value}" since its ' + 'elements must be strictly increasing.') + + def check_value(name, value, accepted_values): """Ensure that an object's value is contained in a set of acceptable values. diff --git a/openmc/mesh.py b/openmc/mesh.py index 8777da325..48263c205 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -1820,6 +1820,8 @@ class SphericalMesh(StructuredMesh): @r_grid.setter def r_grid(self, grid): cv.check_type('mesh r_grid', grid, Iterable, Real) + cv.check_length('mesh r_grid', grid, 2) + cv.check_increasing('mesh r_grid', grid) self._r_grid = np.asarray(grid, dtype=float) @property @@ -1829,6 +1831,8 @@ class SphericalMesh(StructuredMesh): @theta_grid.setter def theta_grid(self, grid): cv.check_type('mesh theta_grid', grid, Iterable, Real) + cv.check_length('mesh theta_grid', grid, 2) + cv.check_increasing('mesh theta_grid', grid) self._theta_grid = np.asarray(grid, dtype=float) @property @@ -1838,6 +1842,8 @@ class SphericalMesh(StructuredMesh): @phi_grid.setter def phi_grid(self, grid): cv.check_type('mesh phi_grid', grid, Iterable, Real) + cv.check_length('mesh phi_grid', grid, 2) + cv.check_increasing('mesh phi_grid', grid) self._phi_grid = np.asarray(grid, dtype=float) @property diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index c49930650..5bacd5fc6 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -132,6 +132,27 @@ def test_SphericalMesh_initiation(): mesh.r_grid = (0, 11) assert (mesh.r_grid == np.array([0., 11.])).all() + # test invalid r_grid values + with pytest.raises(ValueError): + openmc.SphericalMesh(r_grid=[1, 1]) + + with pytest.raises(ValueError): + openmc.SphericalMesh(r_grid=[0]) + + # test invalid theta_grid values + with pytest.raises(ValueError): + openmc.SphericalMesh(r_grid=[1, 2], theta_grid=[1, 1]) + + with pytest.raises(ValueError): + openmc.SphericalMesh(r_grid=[1, 2], theta_grid=[0]) + + # test invalid phi_grid values + with pytest.raises(ValueError): + openmc.SphericalMesh(r_grid=[1, 2], phi_grid=[1, 1]) + + with pytest.raises(ValueError): + openmc.SphericalMesh(r_grid=[1, 2], phi_grid=[0]) + # waffles and pancakes are unfortunately not valid radii with pytest.raises(TypeError): openmc.SphericalMesh(('🧇', '🥞')) From 0b686e365edffc9034802a1cfe1e433e18b6ecb2 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Sat, 25 May 2024 14:11:21 -0500 Subject: [PATCH 108/671] Correction for histogram interpolation of Tabular distributions (#2981) Co-authored-by: Paul Romano --- openmc/data/thermal.py | 4 +-- openmc/stats/univariate.py | 60 ++++++++++++++++++---------------- tests/unit_tests/test_stats.py | 23 +++++++++++-- 3 files changed, 54 insertions(+), 33 deletions(-) diff --git a/openmc/data/thermal.py b/openmc/data/thermal.py index f5841d9c9..6df171d3a 100644 --- a/openmc/data/thermal.py +++ b/openmc/data/thermal.py @@ -698,8 +698,8 @@ class ThermalScattering(EqualityMixin): # add an outgoing energy 0 eV that has a PDF of 0 (and of # course, a CDF of 0 as well). if eout_i.c[0] > 0.: - eout_i.x = np.insert(eout_i.x, 0, 0.) - eout_i.p = np.insert(eout_i.p, 0, 0.) + eout_i._x = np.insert(eout_i.x, 0, 0.) + eout_i._p = np.insert(eout_i.p, 0, 0.) eout_i.c = np.insert(eout_i.c, 0, 0.) # For this added outgoing energy (of 0 eV) we add a set of diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index a0e86c3f8..f44bce67c 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -837,10 +837,11 @@ class Tabular(Univariate): x : Iterable of float Tabulated values of the random variable p : Iterable of float - Tabulated probabilities + Tabulated probabilities. For histogram interpolation, if the length of + `p` is the same as `x`, the last value is ignored. interpolation : {'histogram', 'linear-linear', 'linear-log', 'log-linear', 'log-log'}, optional - Indicate whether the density function is constant between tabulated - points or linearly-interpolated. Defaults to 'linear-linear'. + Indicates how the density function is interpolated between tabulated + points. Defaults to 'linear-linear'. ignore_negative : bool Ignore negative probabilities @@ -850,9 +851,9 @@ class Tabular(Univariate): Tabulated values of the random variable p : numpy.ndarray Tabulated probabilities - interpolation : {'histogram', 'linear-linear', 'linear-log', 'log-linear', 'log-log'}, optional - Indicate whether the density function is constant between tabulated - points or linearly-interpolated. + interpolation : {'histogram', 'linear-linear', 'linear-log', 'log-linear', 'log-log'} + Indicates how the density function is interpolated between tabulated + points. Defaults to 'linear-linear'. """ @@ -863,35 +864,38 @@ class Tabular(Univariate): interpolation: str = 'linear-linear', ignore_negative: bool = False ): - self._ignore_negative = ignore_negative - self.x = x - self.p = p self.interpolation = interpolation + cv.check_type('tabulated values', x, Iterable, Real) + cv.check_type('tabulated probabilities', p, Iterable, Real) + + x = np.array(x, dtype=float) + p = np.array(p, dtype=float) + + if p.size > x.size: + raise ValueError('Number of probabilities exceeds number of table values.') + if self.interpolation != 'histogram' and x.size != p.size: + raise ValueError(f'Tabulated values ({x.size}) and probabilities ' + f'({p.size}) should have the same length') + + if not ignore_negative: + for pk in p: + cv.check_greater_than('tabulated probability', pk, 0.0, True) + + self._x = x + self._p = p + def __len__(self): - return len(self.x) + return self.p.size @property def x(self): return self._x - @x.setter - def x(self, x): - cv.check_type('tabulated values', x, Iterable, Real) - self._x = np.array(x, dtype=float) - @property def p(self): return self._p - @p.setter - def p(self, p): - cv.check_type('tabulated probabilities', p, Iterable, Real) - if not self._ignore_negative: - for pk in p: - cv.check_greater_than('tabulated probability', pk, 0.0, True) - self._p = np.array(p, dtype=float) - @property def interpolation(self): return self._interpolation @@ -907,7 +911,7 @@ class Tabular(Univariate): p = self.p if self.interpolation == 'histogram': - c[1:] = p[:-1] * np.diff(x) + c[1:] = p[:x.size-1] * np.diff(x) elif self.interpolation == 'linear-linear': c[1:] = 0.5 * (p[:-1] + p[1:]) * np.diff(x) else: @@ -938,7 +942,7 @@ class Tabular(Univariate): elif self.interpolation == 'histogram': x_l = self.x[:-1] x_r = self.x[1:] - p_l = self.p[:-1] + p_l = self.p[:self.x.size-1] mean = (0.5 * (x_l + x_r) * (x_r - x_l) * p_l).sum() else: raise NotImplementedError('Can only compute mean for tabular ' @@ -952,7 +956,7 @@ class Tabular(Univariate): def normalize(self): """Normalize the probabilities stored on the distribution""" - self.p /= self.cdf().max() + self._p /= self.cdf().max() def sample(self, n_samples: int = 1, seed: typing.Optional[int] = None): rng = np.random.RandomState(seed) @@ -1070,7 +1074,7 @@ class Tabular(Univariate): Integral of tabular distrbution """ if self.interpolation == 'histogram': - return np.sum(np.diff(self.x) * self.p[:-1]) + return np.sum(np.diff(self.x) * self.p[:self.x.size-1]) elif self.interpolation == 'linear-linear': return trapezoid(self.p, self.x) else: @@ -1346,7 +1350,7 @@ def combine_distributions( # Apply probabilites to continuous distributions for i in cont_index: dist = dist_list[i] - dist.p *= probs[i] + dist._p *= probs[i] if discrete_index: # Create combined discrete distribution diff --git a/tests/unit_tests/test_stats.py b/tests/unit_tests/test_stats.py index 761f26ab3..f5b467d03 100644 --- a/tests/unit_tests/test_stats.py +++ b/tests/unit_tests/test_stats.py @@ -92,7 +92,7 @@ def test_clip_discrete(): with pytest.raises(ValueError): d.clip(-1.) - + with pytest.raises(ValueError): d.clip(5) @@ -188,8 +188,8 @@ def test_watt(): def test_tabular(): - x = np.array([0.0, 5.0, 7.0]) - p = np.array([10.0, 20.0, 5.0]) + x = np.array([0.0, 5.0, 7.0, 10.0]) + p = np.array([10.0, 20.0, 5.0, 6.0]) d = openmc.stats.Tabular(x, p, 'linear-linear') elem = d.to_xml_element('distribution') @@ -217,6 +217,23 @@ def test_tabular(): d.normalize() assert d.integral() == pytest.approx(1.0) + # ensure that passing a set of probabilities shorter than x works + # for histogram interpolation + d = openmc.stats.Tabular(x, p[:-1], interpolation='histogram') + d.cdf() + d.mean() + assert_sample_mean(d.sample(n_samples), d.mean()) + + # passing a shorter probability set should raise an error for linear-linear + with pytest.raises(ValueError): + d = openmc.stats.Tabular(x, p[:-1], interpolation='linear-linear') + d.cdf() + + # Use probabilities of correct length for linear-linear interpolation and + # call the CDF method + d = openmc.stats.Tabular(x, p, interpolation='linear-linear') + d.cdf() + def test_legendre(): # Pu239 elastic scattering at 100 keV From 342019971839c7195101af9ac1b169de3726742a Mon Sep 17 00:00:00 2001 From: Olek <45364492+yardasol@users.noreply.github.com> Date: Tue, 28 May 2024 12:14:38 -0400 Subject: [PATCH 109/671] Fix `CylinderSector` and `IsogonalOctagon` translations (#3018) Co-authored-by: Paul Romano --- openmc/model/surface_composite.py | 56 ++++++++++++++-------- tests/unit_tests/test_surface_composite.py | 37 ++++++++------ 2 files changed, 59 insertions(+), 34 deletions(-) diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index 6f9123311..1fa4234fb 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -90,7 +90,7 @@ class CylinderSector(CompositeSurface): counterclockwise direction with respect to the first basis axis (+y, +z, or +x). Must be greater than :attr:`theta1`. center : iterable of float - Coordinate for central axes of cylinders in the (y, z), (z, x), or (x, y) + Coordinate for central axes of cylinders in the (y, z), (x, z), or (x, y) basis. Defaults to (0,0). axis : {'x', 'y', 'z'} Central axis of the cylinders defining the inner and outer surfaces of @@ -133,13 +133,13 @@ class CylinderSector(CompositeSurface): phi2 = pi / 180 * theta2 # Coords for axis-perpendicular planes - p1 = np.array([0., 0., 1.]) + p1 = np.array([center[0], center[1], 1.]) - p2_plane1 = np.array([r1 * cos(phi1), r1 * sin(phi1), 0.]) - p3_plane1 = np.array([r2 * cos(phi1), r2 * sin(phi1), 0.]) + p2_plane1 = np.array([r1 * cos(phi1) + center[0], r1 * sin(phi1) + center[1], 0.]) + p3_plane1 = np.array([r2 * cos(phi1) + center[0], r2 * sin(phi1) + center[1], 0.]) - p2_plane2 = np.array([r1 * cos(phi2), r1 * sin(phi2), 0.]) - p3_plane2 = np.array([r2 * cos(phi2), r2 * sin(phi2), 0.]) + p2_plane2 = np.array([r1 * cos(phi2) + center[0], r1 * sin(phi2)+ center[1], 0.]) + p3_plane2 = np.array([r2 * cos(phi2) + center[0], r2 * sin(phi2)+ center[1], 0.]) points = [p1, p2_plane1, p3_plane1, p2_plane2, p3_plane2] if axis == 'z': @@ -147,7 +147,7 @@ class CylinderSector(CompositeSurface): self.inner_cyl = openmc.ZCylinder(*center, r1, **kwargs) self.outer_cyl = openmc.ZCylinder(*center, r2, **kwargs) elif axis == 'y': - coord_map = [1, 2, 0] + coord_map = [0, 2, 1] self.inner_cyl = openmc.YCylinder(*center, r1, **kwargs) self.outer_cyl = openmc.YCylinder(*center, r2, **kwargs) elif axis == 'x': @@ -155,6 +155,7 @@ class CylinderSector(CompositeSurface): self.inner_cyl = openmc.XCylinder(*center, r1, **kwargs) self.outer_cyl = openmc.XCylinder(*center, r2, **kwargs) + # Reorder the points to correspond to the correct central axis for p in points: p[:] = p[coord_map] @@ -192,8 +193,8 @@ class CylinderSector(CompositeSurface): with respect to the first basis axis (+y, +z, or +x). Note that negative values translate to an offset in the clockwise direction. center : iterable of float - Coordinate for central axes of cylinders in the (y, z), (z, x), or (x, y) - basis. Defaults to (0,0). + Coordinate for central axes of cylinders in the (y, z), (x, z), or + (x, y) basis. Defaults to (0,0). axis : {'x', 'y', 'z'} Central axis of the cylinders defining the inner and outer surfaces of the sector. Defaults to 'z'. @@ -216,10 +217,16 @@ class CylinderSector(CompositeSurface): return cls(r1, r2, theta1, theta2, center=center, axis=axis, **kwargs) def __neg__(self): - return -self.outer_cyl & +self.inner_cyl & -self.plane1 & +self.plane2 + if isinstance(self.inner_cyl, openmc.YCylinder): + return -self.outer_cyl & +self.inner_cyl & +self.plane1 & -self.plane2 + else: + return -self.outer_cyl & +self.inner_cyl & -self.plane1 & +self.plane2 def __pos__(self): - return +self.outer_cyl | -self.inner_cyl | +self.plane1 | -self.plane2 + if isinstance(self.inner_cyl, openmc.YCylinder): + return +self.outer_cyl | -self.inner_cyl | -self.plane1 | +self.plane2 + else: + return +self.outer_cyl | -self.inner_cyl | +self.plane1 | -self.plane2 class IsogonalOctagon(CompositeSurface): @@ -284,11 +291,11 @@ class IsogonalOctagon(CompositeSurface): c1, c2 = center # Coords for axis-perpendicular planes - ctop = c1 + r1 - cbottom = c1 - r1 + cright = c1 + r1 + cleft = c1 - r1 - cright = c2 + r1 - cleft = c2 - r1 + ctop = c2 + r1 + cbottom = c2 - r1 # Side lengths if r2 > r1 * sqrt(2): @@ -309,7 +316,16 @@ class IsogonalOctagon(CompositeSurface): p2_lr = np.array([L_basis_ax, -r1, 0.]) p3_lr = np.array([L_basis_ax, -r1, 1.]) - points = [p1_ur, p2_ur, p3_ur, p1_lr, p2_lr, p3_lr] + p1_ll = -p1_ur + p2_ll = -p2_ur + p3_ll = -p3_ur + + p1_ul = -p1_lr + p2_ul = -p2_lr + p3_ul = -p3_lr + + points = [p1_ur, p2_ur, p3_ur, p1_lr, p2_lr, p3_lr, + p1_ll, p2_ll, p3_ll, p1_ul, p2_ul, p3_ul] # Orientation specific variables if axis == 'z': @@ -331,17 +347,19 @@ class IsogonalOctagon(CompositeSurface): self.right = openmc.YPlane(cright, **kwargs) self.left = openmc.YPlane(cleft, **kwargs) - # Put our coordinates in (x,y,z) order + # Put our coordinates in (x,y,z) order and add the offset for p in points: + p[0] += c1 + p[1] += c2 p[:] = p[coord_map] self.upper_right = openmc.Plane.from_points(p1_ur, p2_ur, p3_ur, **kwargs) self.lower_right = openmc.Plane.from_points(p1_lr, p2_lr, p3_lr, **kwargs) - self.lower_left = openmc.Plane.from_points(-p1_ur, -p2_ur, -p3_ur, + self.lower_left = openmc.Plane.from_points(p1_ll, p2_ll, p3_ll, **kwargs) - self.upper_left = openmc.Plane.from_points(-p1_lr, -p2_lr, -p3_lr, + self.upper_left = openmc.Plane.from_points(p1_ul, p2_ul, p3_ul, **kwargs) def __neg__(self): diff --git a/tests/unit_tests/test_surface_composite.py b/tests/unit_tests/test_surface_composite.py index 19221212e..62ec18b32 100644 --- a/tests/unit_tests/test_surface_composite.py +++ b/tests/unit_tests/test_surface_composite.py @@ -158,18 +158,23 @@ def test_cone_one_sided(axis, point_pos, point_neg, ll_true): @pytest.mark.parametrize( - "axis, indices", [ - ("X", [0, 1, 2]), - ("Y", [1, 2, 0]), - ("Z", [2, 0, 1]), + "axis, indices, center", [ + ("X", [2, 0, 1], (0., 0.)), + ("Y", [0, 2, 1], (0., 0.)), + ("Z", [0, 1, 2], (0., 0.)), + ("X", [2, 0, 1], (10., 5.)), + ("Y", [0, 2, 1], (10., 5.)), + ("Z", [0, 1, 2], (10., 5.)), + ] ) -def test_cylinder_sector(axis, indices): +def test_cylinder_sector(axis, indices, center): + c1, c2 = center r1, r2 = 0.5, 1.5 d = (r2 - r1) / 2 phi1 = -60. phi2 = 60 - s = openmc.model.CylinderSector(r1, r2, phi1, phi2, + s = openmc.model.CylinderSector(r1, r2, phi1, phi2, center=center, axis=axis.lower()) assert isinstance(s.outer_cyl, getattr(openmc, axis + "Cylinder")) assert isinstance(s.inner_cyl, getattr(openmc, axis + "Cylinder")) @@ -189,16 +194,18 @@ def test_cylinder_sector(axis, indices): assert np.all(np.isinf(ll)) assert np.all(np.isinf(ur)) ll, ur = (-s).bounding_box - assert ll == pytest.approx(np.roll([-np.inf, -r2, -r2], indices[0])) - assert ur == pytest.approx(np.roll([np.inf, r2, r2], indices[0])) + test_point_ll = np.array([-r2 + c1, -r2 + c2, -np.inf]) + assert ll == pytest.approx(test_point_ll[indices]) + test_point_ur = np.array([r2 + c1, r2 + c2, np.inf]) + assert ur == pytest.approx(test_point_ur[indices]) # __contains__ on associated half-spaces - point_pos = np.roll([0, r2 + 1, 0], indices[0]) - assert point_pos in +s - assert point_pos not in -s - point_neg = np.roll([0, r1 + d, r1 + d], indices[0]) - assert point_neg in -s - assert point_neg not in +s + point_pos = np.array([0 + c1, r2 + 1 + c2, 0]) + assert point_pos[indices] in +s + assert point_pos[indices] not in -s + point_neg = np.array([r1 + d + c1, r1 + d + c2, 0]) + assert point_neg[indices] in -s + assert point_neg[indices] not in +s # translate method t = uniform(-5.0, 5.0) @@ -399,7 +406,7 @@ def test_polygon(): with pytest.raises(ValueError): openmc.model.Polygon(rz_points) - # Test "M" shaped polygon + # Test "M" shaped polygon points = np.array([[8.5151581, -17.988337], [10.381711000000001, -17.988337], [12.744357, -24.288728000000003], From 2a53aba1c171773e11305a841e8b714d81c5e186 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 3 Jun 2024 11:34:37 -0500 Subject: [PATCH 110/671] Make sure direction is set when checking source spatial constraints (#3022) --- src/source.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/source.cpp b/src/source.cpp index 405de998c..15fe8433b 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -211,6 +211,7 @@ bool Source::satisfies_spatial_constraints(Position r) const { GeometryState geom_state; geom_state.r() = r; + geom_state.u() = {0.0, 0.0, 1.0}; // Reject particle if it's not in the geometry at all bool found = exhaustive_find_cell(geom_state); From e6c5f56ee284530f447e4578fc97f8f899298cac Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Mon, 10 Jun 2024 18:18:04 +0100 Subject: [PATCH 111/671] Fixing plot xs for when plotting element string reaction (#3029) --- openmc/plotter.py | 13 +++++++++---- tests/unit_tests/test_plotter.py | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/openmc/plotter.py b/openmc/plotter.py index 97ca5f929..aa3825f5e 100644 --- a/openmc/plotter.py +++ b/openmc/plotter.py @@ -59,10 +59,15 @@ def _get_legend_label(this, type): """Gets a label for the element or nuclide or material and reaction plotted""" if isinstance(this, str): if type in openmc.data.DADZ: - z, a, m = openmc.data.zam(this) - da, dz = openmc.data.DADZ[type] - gnds_name = openmc.data.gnds_name(z + dz, a + da, m) - return f'{this} {type} {gnds_name}' + if this in ELEMENT_NAMES: + return f'{this} {type}' + else: # this is a nuclide so the legend can contain more information + z, a, m = openmc.data.zam(this) + da, dz = openmc.data.DADZ[type] + gnds_name = openmc.data.gnds_name(z + dz, a + da, m) + # makes a string with nuclide reaction and new nuclide + # For example "Be9 (n,2n) Be8" + return f'{this} {type} {gnds_name}' return f'{this} {type}' elif this.name == '': return f'Material {this.id} {type}' diff --git a/tests/unit_tests/test_plotter.py b/tests/unit_tests/test_plotter.py index 2c195c5e1..0220cec3e 100644 --- a/tests/unit_tests/test_plotter.py +++ b/tests/unit_tests/test_plotter.py @@ -75,7 +75,7 @@ def test_calculate_cexs_with_materials(test_mat): @pytest.mark.parametrize("this", ["Be", "Be9"]) def test_plot_xs(this): from matplotlib.figure import Figure - assert isinstance(openmc.plot_xs({this: ['total', 'elastic']}), Figure) + assert isinstance(openmc.plot_xs({this: ['total', 'elastic', 16, '(n,2n)']}), Figure) def test_plot_xs_mat(test_mat): From 88f3e4d21f3c86ba76858cca05cb4a36d453d0ca Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 10 Jun 2024 12:19:46 -0500 Subject: [PATCH 112/671] Allow mesh material homogenization to include or exclude voids (#3000) --- openmc/mesh.py | 7 +++++++ tests/unit_tests/test_mesh.py | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/openmc/mesh.py b/openmc/mesh.py index 48263c205..f0c0d7414 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -152,6 +152,7 @@ class MeshBase(IDManagerMixin, ABC): model: openmc.Model, n_samples: int = 10_000, prn_seed: Optional[int] = None, + include_void: bool = True, **kwargs ) -> List[openmc.Material]: """Generate homogenized materials over each element in a mesh. @@ -168,6 +169,8 @@ class MeshBase(IDManagerMixin, ABC): prn_seed : int, optional Pseudorandom number generator (PRNG) seed; if None, one will be generated randomly. + include_void : bool, optional + Whether homogenization should include voids. **kwargs Keyword-arguments passed to :func:`openmc.lib.init`. @@ -222,6 +225,10 @@ class MeshBase(IDManagerMixin, ABC): material_ids.pop(index_void) volumes.pop(index_void) + # If void should be excluded, adjust total volume + if not include_void: + total_volume = sum(volumes) + # Compute volume fractions volume_fracs = np.array(volumes) / total_volume diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index 5bacd5fc6..0f7f71cea 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -386,3 +386,9 @@ def test_mesh_get_homogenized_materials(): # Mesh element that overlaps void should have half density assert m4.get_mass_density('H1') == pytest.approx(0.5, rel=1e-2) + + # If not including void, density of homogenized material should be same as + # original material + m5, = mesh_void.get_homogenized_materials( + model, n_samples=1000, include_void=False) + assert m5.get_mass_density('H1') == pytest.approx(1.0) From 12a278b1acc6c3ba85c6ec07ff6657ca476c4cb6 Mon Sep 17 00:00:00 2001 From: kmeag <143649668+kmeag@users.noreply.github.com> Date: Mon, 10 Jun 2024 15:54:59 -0400 Subject: [PATCH 113/671] Enforce lower_left in lattice geometry (#2982) Co-authored-by: Paul Romano --- openmc/lattice.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openmc/lattice.py b/openmc/lattice.py index f7f9da8ea..518068560 100644 --- a/openmc/lattice.py +++ b/openmc/lattice.py @@ -884,6 +884,10 @@ class RectLattice(Lattice): dimension = ET.SubElement(lattice_subelement, "dimension") dimension.text = ' '.join(map(str, self.shape)) + # Make sure lower_left has been specified + if self.lower_left is None: + raise ValueError(f"Lattice {self.id} does not have lower_left specified.") + # Export Lattice lower left lower_left = ET.SubElement(lattice_subelement, "lower_left") lower_left.text = ' '.join(map(str, self._lower_left)) From e971bd121313b1e9c7113385751ffd9c793e4a95 Mon Sep 17 00:00:00 2001 From: hsameer481 <165707775+hsameer481@users.noreply.github.com> Date: Mon, 10 Jun 2024 17:31:00 -0400 Subject: [PATCH 114/671] added error checking on cylindrical mesh (#2977) Co-authored-by: Paul Romano --- openmc/mesh.py | 21 +++++++++-- tests/unit_tests/mesh_to_vtk/test_vtk_dims.py | 2 +- tests/unit_tests/test_mesh.py | 36 +++++++++++++++++++ 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/openmc/mesh.py b/openmc/mesh.py index f0c0d7414..215c91923 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -1388,6 +1388,8 @@ class CylindricalMesh(StructuredMesh): @r_grid.setter def r_grid(self, grid): cv.check_type('mesh r_grid', grid, Iterable, Real) + cv.check_length('mesh r_grid', grid, 2) + cv.check_increasing('mesh r_grid', grid) self._r_grid = np.asarray(grid, dtype=float) @property @@ -1397,7 +1399,12 @@ class CylindricalMesh(StructuredMesh): @phi_grid.setter def phi_grid(self, grid): cv.check_type('mesh phi_grid', grid, Iterable, Real) - self._phi_grid = np.asarray(grid, dtype=float) + cv.check_length('mesh phi_grid', grid, 2) + cv.check_increasing('mesh phi_grid', grid) + grid = np.asarray(grid, dtype=float) + if np.any((grid < 0.0) | (grid > 2*pi)): + raise ValueError("phi_grid values must be in [0, 2π].") + self._phi_grid = grid @property def z_grid(self): @@ -1406,6 +1413,8 @@ class CylindricalMesh(StructuredMesh): @z_grid.setter def z_grid(self, grid): cv.check_type('mesh z_grid', grid, Iterable, Real) + cv.check_length('mesh z_grid', grid, 2) + cv.check_increasing('mesh z_grid', grid) self._z_grid = np.asarray(grid, dtype=float) @property @@ -1840,7 +1849,10 @@ class SphericalMesh(StructuredMesh): cv.check_type('mesh theta_grid', grid, Iterable, Real) cv.check_length('mesh theta_grid', grid, 2) cv.check_increasing('mesh theta_grid', grid) - self._theta_grid = np.asarray(grid, dtype=float) + grid = np.asarray(grid, dtype=float) + if np.any((grid < 0.0) | (grid > pi)): + raise ValueError("theta_grid values must be in [0, π].") + self._theta_grid = grid @property def phi_grid(self): @@ -1851,7 +1863,10 @@ class SphericalMesh(StructuredMesh): cv.check_type('mesh phi_grid', grid, Iterable, Real) cv.check_length('mesh phi_grid', grid, 2) cv.check_increasing('mesh phi_grid', grid) - self._phi_grid = np.asarray(grid, dtype=float) + grid = np.asarray(grid, dtype=float) + if np.any((grid < 0.0) | (grid > 2*pi)): + raise ValueError("phi_grid values must be in [0, 2π].") + self._phi_grid = grid @property def _grids(self): diff --git a/tests/unit_tests/mesh_to_vtk/test_vtk_dims.py b/tests/unit_tests/mesh_to_vtk/test_vtk_dims.py index baa544984..0b6acf5aa 100644 --- a/tests/unit_tests/mesh_to_vtk/test_vtk_dims.py +++ b/tests/unit_tests/mesh_to_vtk/test_vtk_dims.py @@ -153,7 +153,7 @@ def test_write_data_to_vtk_round_trip(run_in_tmpdir): smesh = openmc.SphericalMesh( r_grid=(0.0, 1.0, 2.0), - theta_grid=(0.0, 2.0, 4.0, 5.0), + theta_grid=(0.0, 0.5, 1.0, 2.0), phi_grid=(0.0, 3.0, 6.0), ) rmesh = openmc.RegularMesh() diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index 0f7f71cea..ed08be816 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -189,6 +189,42 @@ def test_CylindricalMesh_initiation(): openmc.SphericalMesh(('🧇', '🥞')) +def test_invalid_cylindrical_mesh_errors(): + # Test invalid r_grid values + with pytest.raises(ValueError): + openmc.CylindricalMesh(r_grid=[5, 1], phi_grid=[0, pi], z_grid=[0, 10]) + + with pytest.raises(ValueError): + openmc.CylindricalMesh(r_grid=[1, 2, 4, 3], phi_grid=[0, pi], z_grid=[0, 10]) + + with pytest.raises(ValueError): + openmc.CylindricalMesh(r_grid=[1], phi_grid=[0, pi], z_grid=[0, 10]) + + # Test invalid phi_grid values + with pytest.raises(ValueError): + openmc.CylindricalMesh(r_grid=[0, 1, 2], phi_grid=[-1, 3], z_grid=[0, 10]) + + with pytest.raises(ValueError): + openmc.CylindricalMesh( + r_grid=[0, 1, 2], + phi_grid=[0, 2*pi + 0.1], + z_grid=[0, 10] + ) + + with pytest.raises(ValueError): + openmc.CylindricalMesh(r_grid=[0, 1, 2], phi_grid=[pi], z_grid=[0, 10]) + + # Test invalid z_grid values + with pytest.raises(ValueError): + openmc.CylindricalMesh(r_grid=[0, 1, 2], phi_grid=[0, pi], z_grid=[5]) + + with pytest.raises(ValueError): + openmc.CylindricalMesh(r_grid=[0, 1, 2], phi_grid=[0, pi], z_grid=[5, 1]) + + with pytest.raises(ValueError): + openmc.CylindricalMesh(r_grid=[1, 2, 4, 3], phi_grid=[0, pi], z_grid=[0, 10, 5]) + + def test_centroids(): # regular mesh mesh = openmc.RegularMesh() From 8b33615ac2d016ffde636f2a7f918c883094a3f0 Mon Sep 17 00:00:00 2001 From: Isaac Meyer Date: Tue, 11 Jun 2024 22:37:28 -0400 Subject: [PATCH 115/671] fix shannon entropy broken link (#3034) --- docs/source/methods/eigenvalue.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/methods/eigenvalue.rst b/docs/source/methods/eigenvalue.rst index 41bf86549..b63723c02 100644 --- a/docs/source/methods/eigenvalue.rst +++ b/docs/source/methods/eigenvalue.rst @@ -142,7 +142,7 @@ than unity. By ensuring that the expected number of fission sites in each mesh cell is constant, the collision density across all cells, and hence the variance of tallies, is more uniform than it would be otherwise. -.. _Shannon entropy: https://laws.lanl.gov/vhosts/mcnp.lanl.gov/pdf_files/la-ur-06-3737.pdf +.. _Shannon entropy: https://mcnp.lanl.gov/pdf_files/TechReport_2006_LANL_LA-UR-06-3737_Brown.pdf .. [Lieberoth] J. Lieberoth, "A Monte Carlo Technique to Solve the Static Eigenvalue Problem of the Boltzmann Transport Equation," *Nukleonik*, **11**, From 1f3280461fa41e22dbc563324c98fcf53179d61b Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 11 Jun 2024 21:49:01 -0500 Subject: [PATCH 116/671] Make sure skewed dataset is cast to bool properly (#3001) --- openmc/data/njoy.py | 2 +- openmc/data/thermal_angle_energy.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openmc/data/njoy.py b/openmc/data/njoy.py index c1dcbab17..ac1b5e345 100644 --- a/openmc/data/njoy.py +++ b/openmc/data/njoy.py @@ -453,7 +453,7 @@ def make_ace_thermal(filename, filename_thermal, temperatures=None, error : float, optional Fractional error tolerance for NJOY processing iwt : int - `iwt` parameter used in NJOR/ACER card 9 + `iwt` parameter used in NJOY/ACER card 9 evaluation : openmc.data.endf.Evaluation, optional If the ENDF neutron sublibrary file contains multiple material evaluations, this argument indicates which evaluation to use. diff --git a/openmc/data/thermal_angle_energy.py b/openmc/data/thermal_angle_energy.py index 0dd2a0b7a..4789ebcc6 100644 --- a/openmc/data/thermal_angle_energy.py +++ b/openmc/data/thermal_angle_energy.py @@ -225,7 +225,7 @@ class IncoherentInelasticAEDiscrete(AngleEnergy): """ energy_out = group['energy_out'][()] mu_out = group['mu_out'][()] - skewed = bool(group['skewed']) + skewed = bool(group['skewed'][()]) return cls(energy_out, mu_out, skewed) From dcb80338c5794490a99db58bbcdc7c25a9dd0439 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 11 Jun 2024 21:56:31 -0500 Subject: [PATCH 117/671] Eliminate deprecation warnings from scipy and pandas (#2951) --- openmc/data/photon.py | 12 +++++++----- openmc/mgxs_library.py | 18 ++++++++++++------ tests/unit_tests/test_data_photon.py | 7 ++++--- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/openmc/data/photon.py b/openmc/data/photon.py index c9d51f062..0d0419f07 100644 --- a/openmc/data/photon.py +++ b/openmc/data/photon.py @@ -124,7 +124,7 @@ class AtomicRelaxation(EqualityMixin): Dictionary indicating the number of electrons in a subshell when neutral (values) for given subshells (keys). The subshells should be given as strings, e.g., 'K', 'L1', 'L2', etc. - transitions : pandas.DataFrame + transitions : dict of str to pandas.DataFrame Dictionary indicating allowed transitions and their probabilities (values) for given subshells (keys). The subshells should be given as strings, e.g., 'K', 'L1', 'L2', etc. The transitions are represented as @@ -363,8 +363,9 @@ class AtomicRelaxation(EqualityMixin): df = pd.DataFrame(sub_group['transitions'][()], columns=columns) # Replace float indexes back to subshell strings - df[columns[:2]] = df[columns[:2]].replace( - np.arange(float(len(_SUBSHELLS))), _SUBSHELLS) + with pd.option_context('future.no_silent_downcasting', True): + df[columns[:2]] = df[columns[:2]].replace( + np.arange(float(len(_SUBSHELLS))), _SUBSHELLS) transitions[shell] = df return cls(binding_energy, num_electrons, transitions) @@ -387,8 +388,9 @@ class AtomicRelaxation(EqualityMixin): # Write transition data with replacements if shell in self.transitions: - df = self.transitions[shell].replace( - _SUBSHELLS, range(len(_SUBSHELLS))) + with pd.option_context('future.no_silent_downcasting', True): + df = self.transitions[shell].replace( + _SUBSHELLS, range(len(_SUBSHELLS))) group.create_dataset('transitions', data=df.values.astype(float)) diff --git a/openmc/mgxs_library.py b/openmc/mgxs_library.py index 642da83d1..d1b962890 100644 --- a/openmc/mgxs_library.py +++ b/openmc/mgxs_library.py @@ -3,7 +3,7 @@ from numbers import Real, Integral import h5py import numpy as np -from scipy.integrate import simps +import scipy.integrate from scipy.interpolate import interp1d from scipy.special import eval_legendre @@ -1823,6 +1823,12 @@ class XSdata: # Reset and re-generate XSdata.xs_shapes with the new scattering format xsdata._xs_shapes = None + # scipy 1.11+ prefers 'simpson', whereas older versions use 'simps' + if hasattr(scipy.integrate, 'simpson'): + integrate = scipy.integrate.simpson + else: + integrate = scipy.integrate.simps + for i, temp in enumerate(xsdata.temperatures): orig_data = self._scatter_matrix[i] new_shape = orig_data.shape[:-1] + (xsdata.num_orders,) @@ -1860,7 +1866,7 @@ class XSdata: table_fine[..., imu] += ((l + 0.5) * eval_legendre(l, mu_fine[imu]) * orig_data[..., l]) - new_data[..., h_bin] = simps(table_fine, mu_fine) + new_data[..., h_bin] = integrate(table_fine, mu_fine) elif self.scatter_format == SCATTER_TABULAR: # Calculate the mu points of the current data @@ -1874,7 +1880,7 @@ class XSdata: for l in range(xsdata.num_orders): y = (interp1d(mu_self, orig_data)(mu_fine) * eval_legendre(l, mu_fine)) - new_data[..., l] = simps(y, mu_fine) + new_data[..., l] = integrate(y, mu_fine) elif target_format == SCATTER_TABULAR: # Simply use an interpolating function to get the new data @@ -1893,7 +1899,7 @@ class XSdata: interp = interp1d(mu_self, orig_data) for h_bin in range(xsdata.num_orders): mu_fine = np.linspace(mu[h_bin], mu[h_bin + 1], _NMU) - new_data[..., h_bin] = simps(interp(mu_fine), mu_fine) + new_data[..., h_bin] = integrate(interp(mu_fine), mu_fine) elif self.scatter_format == SCATTER_HISTOGRAM: # The histogram format does not have enough information to @@ -1919,7 +1925,7 @@ class XSdata: mu_fine = np.linspace(-1, 1, _NMU) for l in range(xsdata.num_orders): y = interp(mu_fine) * norm * eval_legendre(l, mu_fine) - new_data[..., l] = simps(y, mu_fine) + new_data[..., l] = integrate(y, mu_fine) elif target_format == SCATTER_TABULAR: # Simply use an interpolating function to get the new data @@ -1938,7 +1944,7 @@ class XSdata: for h_bin in range(xsdata.num_orders): mu_fine = np.linspace(mu[h_bin], mu[h_bin + 1], _NMU) new_data[..., h_bin] = \ - norm * simps(interp(mu_fine), mu_fine) + norm * integrate(interp(mu_fine), mu_fine) # Remove small values resulting from numerical precision issues new_data[..., np.abs(new_data) < 1.E-10] = 0. diff --git a/tests/unit_tests/test_data_photon.py b/tests/unit_tests/test_data_photon.py index f7274e9c1..171dd52f3 100644 --- a/tests/unit_tests/test_data_photon.py +++ b/tests/unit_tests/test_data_photon.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python - from collections.abc import Mapping, Callable import os from pathlib import Path @@ -123,6 +121,8 @@ def test_reactions(element, reaction): reactions[18] +# TODO: Remove skip when support is Python 3.9+ +@pytest.mark.skipif(not hasattr(pd.options, 'future'), reason='pandas version too old') @pytest.mark.parametrize('element', ['Pu'], indirect=True) def test_export_to_hdf5(tmpdir, element): filename = str(tmpdir.join('tmp.h5')) @@ -146,8 +146,9 @@ def test_export_to_hdf5(tmpdir, element): # Export to hdf5 again element2.export_to_hdf5(filename, 'w') + def test_photodat_only(run_in_tmpdir): endf_dir = Path(os.environ['OPENMC_ENDF_DATA']) photoatomic_file = endf_dir / 'photoat' / 'photoat-001_H_000.endf' data = openmc.data.IncidentPhoton.from_endf(photoatomic_file) - data.export_to_hdf5('tmp.h5', 'w') \ No newline at end of file + data.export_to_hdf5('tmp.h5', 'w') From 3e16c038fee8ee8699c290f9e89c338ec4c5cd9c Mon Sep 17 00:00:00 2001 From: Gavin Ridley Date: Wed, 12 Jun 2024 17:33:55 -0500 Subject: [PATCH 118/671] only add png or h5 extension if not present in plots.py (#3036) Co-authored-by: Paul Romano --- openmc/plots.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/openmc/plots.py b/openmc/plots.py index df4a76633..02a44449b 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -171,8 +171,14 @@ def _get_plot_image(plot, cwd): from IPython.display import Image # Make sure .png file was created - stem = plot.filename if plot.filename is not None else f'plot_{plot.id}' - png_file = Path(cwd) / f'{stem}.png' + png_filename = plot.filename if plot.filename is not None else f'plot_{plot.id}' + + # Add file extension if not already present. The C++ code added it + # automatically if it wasn't present. + if Path(png_filename).suffix != ".png": + png_filename += ".png" + + png_file = Path(cwd) / png_filename if not png_file.exists(): raise FileNotFoundError( f"Could not find .png image for plot {plot.id}. Your version of " @@ -630,7 +636,7 @@ class Plot(PlotBase): cv.check_type('plot meshlines', meshlines, dict) if 'type' not in meshlines: msg = f'Unable to set the meshlines to "{meshlines}" which ' \ - 'does not have a "type" key' + 'does not have a "type" key' raise ValueError(msg) elif meshlines['type'] not in ['tally', 'entropy', 'ufs', 'cmfd']: @@ -968,8 +974,13 @@ class Plot(PlotBase): # Run OpenMC in geometry plotting mode and produces a h5 file openmc.plot_geometry(False, openmc_exec, cwd) - stem = self.filename if self.filename is not None else f'plot_{self.id}' - h5_voxel_file = Path(cwd) / f'{stem}.h5' + h5_voxel_filename = self.filename if self.filename is not None else f'plot_{self.id}' + + # Add file extension if not already present + if Path(h5_voxel_filename).suffix != ".h5": + h5_voxel_filename += ".h5" + + h5_voxel_file = Path(cwd) / h5_voxel_filename if output is None: output = h5_voxel_file.with_suffix('.vti') From b1b8a4c32834f82b0687600efbffa0e3181ef4c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigfrid=20Stj=C3=A4rnholm?= Date: Thu, 13 Jun 2024 05:46:18 +0200 Subject: [PATCH 119/671] Set OpenMCOperator materials to the model materials when diff_burnable_mats = True (#2877) --- openmc/deplete/openmc_operator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py index ad7a3924b..75f0925c4 100644 --- a/openmc/deplete/openmc_operator.py +++ b/openmc/deplete/openmc_operator.py @@ -142,6 +142,7 @@ class OpenMCOperator(TransportOperator): if diff_burnable_mats: self._differentiate_burnable_mats() + self.materials = self.model.materials # Determine which nuclides have cross section data # This nuclides variables contains every nuclides From 5222b343a4e7f21fa64d0d6be92292878e1f6ae3 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Mon, 17 Jun 2024 11:02:20 -0500 Subject: [PATCH 120/671] Fixed Source Random Ray (#2988) Co-authored-by: Gavin Ridley Co-authored-by: Paul Romano --- docs/source/methods/random_ray.rst | 48 ++- docs/source/usersguide/random_ray.rst | 162 ++++++++- include/openmc/mgxs_interface.h | 3 + .../openmc/random_ray/flat_source_domain.h | 131 +++++-- .../openmc/random_ray/random_ray_simulation.h | 3 +- include/openmc/source.h | 9 +- src/mgxs_interface.cpp | 10 + src/random_ray/flat_source_domain.cpp | 296 +++++++++++++-- src/random_ray/random_ray_simulation.cpp | 95 +++-- src/settings.cpp | 8 +- src/simulation.cpp | 14 +- .../random_ray_basic/results_true.dat | 282 +++++++-------- .../random_ray_fixed_source/__init__.py | 0 .../cell/inputs_true.dat | 204 +++++++++++ .../cell/results_true.dat | 47 +++ .../material/inputs_true.dat | 204 +++++++++++ .../material/results_true.dat | 47 +++ .../random_ray_fixed_source/test.py | 339 ++++++++++++++++++ .../universe/inputs_true.dat | 204 +++++++++++ .../universe/results_true.dat | 47 +++ .../random_ray_vacuum/results_true.dat | 280 +++++++-------- 21 files changed, 2064 insertions(+), 369 deletions(-) create mode 100644 tests/regression_tests/random_ray_fixed_source/__init__.py create mode 100644 tests/regression_tests/random_ray_fixed_source/cell/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source/cell/results_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source/material/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source/material/results_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source/test.py create mode 100644 tests/regression_tests/random_ray_fixed_source/universe/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source/universe/results_true.dat diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index aa4367847..e0fff792e 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -94,8 +94,8 @@ Method of Characteristics The Boltzmann neutron transport equation is a partial differential equation (PDE) that describes the angular flux within a system. It is a balance equation, with the streaming and absorption terms typically appearing on the left hand -side, which are balanced by the scattering source and fission source terms on -the right hand side. +side, which are balanced by the scattering source, fission, and fixed source +terms on the right hand side. .. math:: :label: transport @@ -522,8 +522,8 @@ make their traversals, and summing these contributions up as in Equation improve the estimate of the source and scalar flux over many iterations, given that our initial starting source will just be a guess? -The source :math:`Q^{n}` for iteration :math:`n` can be inferred -from the scalar flux from the previous iteration :math:`n-1` as: +In an eigenvalue simulation, the source :math:`Q^{n}` for iteration :math:`n` +can be inferred from the scalar flux from the previous iteration :math:`n-1` as: .. math:: :label: source_update @@ -535,7 +535,7 @@ where :math:`Q^{n}(i, g)` is the total source (fission + scattering) in region :math:`g` must be computed by summing over the contributions from all groups :math:`g' \in G`. -In a similar manner, the eigenvalue for iteration :math:`n` can be computed as: +The eigenvalue for iteration :math:`n` can be computed as: .. math:: :label: eigenvalue_update @@ -576,6 +576,18 @@ and a similar substitution can be made to update Equation estimate is used, such that the total fission source from the previous iteration (:math:`n-1`) is also recomputed each iteration. +In a fixed source simulation, the fission source is replaced by a user specified +fixed source term :math:`Q_\text{fixed}(i,E)`, which is defined for each FSR and +energy group. This additional source term is applied at this stage for +generating the next iteration's source estimate as: + +.. math:: + :label: fixed_source_update + + Q^{n}(i, g) = Q_\text{fixed}(i,g) + \sum\limits^{G}_{g'} \Sigma_{s}(i,g,g') \phi^{n-1}(g') + +and no eigenvalue is computed. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Ray Starting Conditions and Inactive Length ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -742,6 +754,32 @@ behavior if a single simulation cell is able to score to multiple filter mesh cells. In the future, the capability to fully support mesh tallies may be added to OpenMC, but for now this restriction needs to be respected. +.. _usersguide_fixed_source_methods: + +------------ +Fixed Source +------------ + +The random ray solver in OpenMC can be used for both eigenvalue and fixed source +problems. There are a few key differences between fixed source transport with +random ray and Monte Carlo, however. + +- **Source definition:** In Monte Carlo, it is relatively easy to define various + source distributions, including point sources, surface sources, volume + sources, and even custom user sources -- all with varying angular and spatial + statistical distributions. In random ray, the natural way to include a fixed + source term is by adding a fixed (flat) contribution to specific flat source + regions. Thus, in the OpenMC implementation of random ray, particle sources + are restricted to being volumetric and isotropic, although different energy + spectrums are supported. Fixed sources can be applied to specific materials, + cells, or universes. + +- **Inactive batches:** In Monte Carlo, use of a fixed source implies that all + batches are active batches, as there is no longer a need to develop a fission + source distribution. However, in random ray mode, there is still a need to + develop the scattering source by way of inactive batches before beginning + active batches. + --------------------------- Fundamental Sources of Bias --------------------------- diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index dad86c826..61f074650 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -40,13 +40,15 @@ Carlo, **inactive batches are required for both eigenvalue and fixed source solves in random ray mode** due to this additional need to converge the scattering source. +.. warning:: + Unlike Monte Carlo, the random ray solver still requires usage of inactive + batches when in fixed source mode so as to develop the scattering source. + The additional burden of converging the scattering source generally results in a higher requirement for the number of inactive batches---often by an order of magnitude or more. For instance, it may be reasonable to only use 50 inactive batches for a light water reactor simulation with Monte Carlo, but random ray -might require 500 or more inactive batches. Similar to Monte Carlo, -:ref:`Shannon entropy ` can be used to gauge whether the -combined scattering and fission source has fully developed. +might require 500 or more inactive batches. Similar to Monte Carlo, active batches are used in the random ray solver mode to accumulate and converge statistics on unknown quantities (i.e., the random ray @@ -248,6 +250,8 @@ a larger value until the "low ray density" messages go away. ray lengths are sufficiently long to allow for transport to occur between source and target regions of interest. +.. _usersguide_ray_source: + ---------- Ray Source ---------- @@ -261,7 +265,7 @@ that the source must not be limited to only fissionable regions. Additionally, the source box must cover the entire simulation domain. In the case of a simulation domain that is not box shaped, a box source should still be used to bound the domain but with the source limited to rejection sampling the actual -simulation universe (which can be specified via the ``domains`` field of the +simulation universe (which can be specified via the ``domains`` constraint of the :class:`openmc.IndependentSource` Python class). Similar to Monte Carlo sources, for two-dimensional problems (e.g., a 2D pincell) it is desirable to make the source bounded near the origin of the infinite dimension. An example of an @@ -411,11 +415,78 @@ in the `OpenMC Jupyter notebook collection separate materials can be defined each with a separate multigroup dataset corresponding to a given temperature. +--------------------------------- +Fixed Source and Eigenvalue Modes +--------------------------------- + +Both fixed source and eigenvalue modes are supported with the random ray solver +in OpenMC. Modes can be selected as described in the :ref:`run modes section +`. In both modes, a ray source must be provided to let +OpenMC know where to sample ray starting locations from, as discussed in the +:ref:`ray source section `. In fixed source mode, at +least one regular source must be provided as well that represents the physical +particle fixed source. As discussed in the :ref:`fixed source methodology +section `, the types of fixed sources supported +in the random ray solver mode are limited compared to what is possible with the +Monte Carlo solver. + +Currently, all of the following conditions must be met for the particle source +to be valid in random ray mode: + +- One or more domain ids must be specified that indicate which cells, universes, + or materials the source applies to. This implicitly limits the source type to + being volumetric. This is specified via the ``domains`` constraint placed on the + :class:`openmc.IndependentSource` Python class. +- The source must be isotropic (default for a source) +- The source must use a discrete (i.e., multigroup) energy distribution. The + discrete energy distribution is input by defining a + :class:`openmc.stats.Discrete` Python class, and passed as the ``energy`` + field of the :class:`openmc.IndependentSource` Python class. + +Any other spatial distribution information contained in a particle source will +be ignored. Only the specified cell, material, or universe domains will be used +to define the spatial location of the source, as the source will be applied +during a pre-processing stage of OpenMC to all source regions that are contained +within the specified domains for the source. + +When defining a :class:`openmc.stats.Discrete` object, note that the ``x`` field +will correspond to the discrete energy points, and the ``p`` field will +correspond to the discrete probabilities. It is recommended to select energy +points that fall within energy groups rather than on boundaries between the +groups. That is, if the problem contains two energy groups (with bin edges of +1.0e-5, 1.0e-1, 1.0e7), then a good selection for the ``x`` field might be +points of 1.0e-2 and 1.0e1. + +:: + + # Define geometry, etc. + ... + source_cell = openmc.Cell(fill=source_mat, name='cell where fixed source will be') + ... + # Define physical neutron fixed source + energy_points = [1.0e-2, 1.0e1] + strengths = [0.25, 0.75] + energy_distribution = openmc.stats.Discrete(x=energy_points, p=strengths) + neutron_source = openmc.IndependentSource( + energy=energy_distribution, + constraints={'domains': [source_cell]} + ) + + # Add fixed source and ray sampling source to settings file + settings.source = [neutron_source] + --------------------------------------- Putting it All Together: Example Inputs --------------------------------------- -An example of a settings definition for random ray is given below:: +~~~~~~~~~~~~~~~~~~ +Eigenvalue Example +~~~~~~~~~~~~~~~~~~ + +An example of a settings definition for an eigenvalue random ray simulation is +given below: + +:: # Geometry and MGXS material definition of 2x2 lattice (not shown) pitch = 1.26 @@ -478,3 +549,84 @@ Monte Carlo run (see the :ref:`geometry ` and There is also a complete example of a pincell available in the ``openmc/examples/pincell_random_ray`` folder. + +~~~~~~~~~~~~~~~~~~~~ +Fixed Source Example +~~~~~~~~~~~~~~~~~~~~ + +An example of a settings definition for a fixed source random ray simulation is +given below: + +:: + + # Geometry and MGXS material definition of 2x2 lattice (not shown) + pitch = 1.26 + source_cell = openmc.Cell(fill=source_mat, name='cell where fixed source will be') + ebins = [1e-5, 1e-1, 20.0e6] + ... + + # Instantiate a settings object for a random ray solve + settings = openmc.Settings() + settings.energy_mode = "multi-group" + settings.batches = 1200 + settings.inactive = 600 + settings.particles = 2000 + settings.run_mode = 'fixed source' + settings.random_ray['distance_inactive'] = 40.0 + settings.random_ray['distance_active'] = 400.0 + + # Create an initial uniform spatial source distribution for sampling rays + lower_left = (-pitch, -pitch, -pitch) + upper_right = ( pitch, pitch, pitch) + uniform_dist = openmc.stats.Box(lower_left, upper_right) + settings.random_ray['ray_source'] = openmc.IndependentSource(space=uniform_dist) + + # Define physical neutron fixed source + energy_points = [1.0e-2, 1.0e1] + strengths = [0.25, 0.75] + energy_distribution = openmc.stats.Discrete(x=energy_points, p=strengths) + neutron_source = openmc.IndependentSource( + energy=energy_distribution, + constraints={'domains': [source_cell]} + ) + + # Add fixed source and ray sampling source to settings file + settings.source = [neutron_source] + + settings.export_to_xml() + + # Define tallies + + # Create a mesh filter + mesh = openmc.RegularMesh() + mesh.dimension = (2, 2) + mesh.lower_left = (-pitch/2, -pitch/2) + mesh.upper_right = (pitch/2, pitch/2) + mesh_filter = openmc.MeshFilter(mesh) + + # Create a multigroup energy filter + energy_filter = openmc.EnergyFilter(ebins) + + # Create tally using our two filters and add scores + tally = openmc.Tally() + tally.filters = [mesh_filter, energy_filter] + tally.scores = ['flux'] + + # Instantiate a Tallies collection and export to XML + tallies = openmc.Tallies([tally]) + tallies.export_to_xml() + + # Create voxel plot + plot = openmc.Plot() + plot.origin = [0, 0, 0] + plot.width = [2*pitch, 2*pitch, 1] + plot.pixels = [1000, 1000, 1] + plot.type = 'voxel' + + # Instantiate a Plots collection and export to XML + plots = openmc.Plots([plot]) + plots.export_to_xml() + +All other inputs (e.g., geometry, material) will be unchanged from a typical +Monte Carlo run (see the :ref:`geometry ` and +:ref:`multigroup materials ` user guides for more information). diff --git a/include/openmc/mgxs_interface.h b/include/openmc/mgxs_interface.h index 8bcdf6dc6..da074f825 100644 --- a/include/openmc/mgxs_interface.h +++ b/include/openmc/mgxs_interface.h @@ -46,6 +46,9 @@ public: // Get the kT values which are used in the OpenMC model vector> get_mat_kTs(); + // Get the group index corresponding to a continuous energy + int get_group_index(double E); + int num_energy_groups_; int num_delayed_groups_; vector xs_names_; // available names in HDF5 file diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index 5f64914ed..badbb25fc 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -3,9 +3,83 @@ #include "openmc/openmp_interface.h" #include "openmc/position.h" +#include "openmc/source.h" namespace openmc { +//---------------------------------------------------------------------------- +// Helper Functions + +// The hash_combine function is the standard hash combine function from boost +// that is typically used for combining multiple hash values into a single hash +// as is needed for larger objects being stored in a hash map. The function is +// taken from: +// https://www.boost.org/doc/libs/1_55_0/doc/html/hash/reference.html#boost.hash_combine +// which carries the following license: +// +// Boost Software License - Version 1.0 - August 17th, 2003 +// Permission is hereby granted, free of charge, to any person or organization +// obtaining a copy of the software and accompanying documentation covered by +// this license (the "Software") to use, reproduce, display, distribute, +// execute, and transmit the Software, and to prepare derivative works of the +// Software, and to permit third-parties to whom the Software is furnished to +// do so, all subject to the following: +// The copyright notices in the Software and this entire statement, including +// the above license grant, this restriction and the following disclaimer, +// must be included in all copies of the Software, in whole or in part, and +// all derivative works of the Software, unless such copies or derivative +// works are solely in the form of machine-executable object code generated by +// a source language processor. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +// SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +// FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +inline void hash_combine(size_t& seed, const size_t v) +{ + seed ^= (v + 0x9e3779b9 + (seed << 6) + (seed >> 2)); +} + +//---------------------------------------------------------------------------- +// Helper Structs + +// A mapping object that is used to map between a specific random ray +// source region and an OpenMC native tally bin that it should score to +// every iteration. +struct TallyTask { + int tally_idx; + int filter_idx; + int score_idx; + int score_type; + TallyTask(int tally_idx, int filter_idx, int score_idx, int score_type) + : tally_idx(tally_idx), filter_idx(filter_idx), score_idx(score_idx), + score_type(score_type) + {} + TallyTask() = default; + + // Comparison and Hash operators are defined to allow usage of the + // TallyTask struct as a key in an unordered_set + bool operator==(const TallyTask& other) const + { + return tally_idx == other.tally_idx && filter_idx == other.filter_idx && + score_idx == other.score_idx && score_type == other.score_type; + } + + struct HashFunctor { + size_t operator()(const TallyTask& task) const + { + size_t seed = 0; + hash_combine(seed, task.tally_idx); + hash_combine(seed, task.filter_idx); + hash_combine(seed, task.score_idx); + hash_combine(seed, task.score_type); + return seed; + } + }; +}; + /* * The FlatSourceDomain class encompasses data and methods for storing * scalar flux and source region for all flat source regions in a @@ -14,23 +88,6 @@ namespace openmc { class FlatSourceDomain { public: - //---------------------------------------------------------------------------- - // Helper Structs - - // A mapping object that is used to map between a specific random ray - // source region and an OpenMC native tally bin that it should score to - // every iteration. - struct TallyTask { - int tally_idx; - int filter_idx; - int score_idx; - int score_type; - TallyTask(int tally_idx, int filter_idx, int score_idx, int score_type) - : tally_idx(tally_idx), filter_idx(filter_idx), score_idx(score_idx), - score_type(score_type) - {} - }; - //---------------------------------------------------------------------------- // Constructors FlatSourceDomain(); @@ -44,10 +101,13 @@ public: int64_t add_source_to_scalar_flux(); void batch_reset(); void convert_source_regions_to_tallies(); - void random_ray_tally() const; + void reset_tally_volumes(); + void random_ray_tally(); void accumulate_iteration_flux(); void output_to_vtk() const; void all_reduce_replicated_source_regions(); + void convert_external_sources(); + void count_external_source_regions(); //---------------------------------------------------------------------------- // Public Data members @@ -55,6 +115,8 @@ public: bool mapped_all_tallies_ {false}; // If all source regions have been visited int64_t n_source_regions_ {0}; // Total number of source regions in the model + int64_t n_external_source_regions_ {0}; // Total number of source regions with + // non-zero external source terms // 1D array representing source region starting offset for each OpenMC Cell // in model::cells @@ -72,18 +134,39 @@ public: vector scalar_flux_old_; vector scalar_flux_new_; vector source_; + vector external_source_; + +private: + //---------------------------------------------------------------------------- + // Methods + void apply_external_source_to_source_region( + Discrete* discrete, double strength_factor, int64_t source_region); + void apply_external_source_to_cell_instances(int32_t i_cell, + Discrete* discrete, double strength_factor, int target_material_id, + const vector& instances); + void apply_external_source_to_cell_and_children(int32_t i_cell, + Discrete* discrete, double strength_factor, int32_t target_material_id); //---------------------------------------------------------------------------- // Private data members -private: int negroups_; // Number of energy groups in simulation int64_t n_source_elements_ {0}; // Total number of source regions in the model // times the number of energy groups - // 2D array representing values for all source regions x energy groups x tally + double + simulation_volume_; // Total physical volume of the simulation domain, as + // defined by the 3D box of the random ray source + + // 2D array representing values for all source elements x tally // tasks vector> tally_task_; + // 1D array representing values for all source regions, with each region + // containing a set of volume tally tasks. This more complicated data + // structure is convenient for ensuring that volumes are only tallied once per + // source region, regardless of how many energy groups are used for tallying. + vector> volume_task_; + // 1D arrays representing values for all source regions vector material_; vector volume_t_; @@ -92,6 +175,14 @@ private: // groups vector scalar_flux_final_; + // 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 + // 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_; + }; // class FlatSourceDomain //============================================================================ diff --git a/include/openmc/random_ray/random_ray_simulation.h b/include/openmc/random_ray/random_ray_simulation.h index cde0d27df..b439574bf 100644 --- a/include/openmc/random_ray/random_ray_simulation.h +++ b/include/openmc/random_ray/random_ray_simulation.h @@ -24,7 +24,8 @@ public: void instability_check( int64_t n_hits, double k_eff, double& avg_miss_rate) const; void print_results_random_ray(uint64_t total_geometric_intersections, - double avg_miss_rate, int negroups, int64_t n_source_regions) const; + double avg_miss_rate, int negroups, int64_t n_source_regions, + int64_t n_external_source_regions) const; //---------------------------------------------------------------------------- // Data members diff --git a/include/openmc/source.h b/include/openmc/source.h index 9ef8b9251..5cc0356e3 100644 --- a/include/openmc/source.h +++ b/include/openmc/source.h @@ -52,6 +52,8 @@ extern vector> external_sources; class Source { public: + // Domain types + enum class DomainType { UNIVERSE, MATERIAL, CELL }; // Constructors, destructors Source() = default; explicit Source(pugi::xml_node node); @@ -76,9 +78,6 @@ public: static unique_ptr create(pugi::xml_node node); protected: - // Domain types - enum class DomainType { UNIVERSE, MATERIAL, CELL }; - // Strategy used for rejecting sites when constraints are applied. KILL means // that sites are always accepted but if they don't satisfy constraints, they // are given weight 0. RESAMPLE means that a new source site will be sampled @@ -134,6 +133,10 @@ public: Distribution* energy() const { return energy_.get(); } Distribution* time() const { return time_.get(); } + // Make domain type and ids available + DomainType domain_type() const { return domain_type_; } + const std::unordered_set& domain_ids() const { return domain_ids_; } + protected: // Indicates whether derived class already handles constraints bool constraints_applied() const override { return true; } diff --git a/src/mgxs_interface.cpp b/src/mgxs_interface.cpp index d54211eca..0febc612b 100644 --- a/src/mgxs_interface.cpp +++ b/src/mgxs_interface.cpp @@ -15,6 +15,7 @@ #include "openmc/material.h" #include "openmc/math_functions.h" #include "openmc/nuclide.h" +#include "openmc/search.h" #include "openmc/settings.h" namespace openmc { @@ -183,6 +184,15 @@ vector> MgxsInterface::get_mat_kTs() //============================================================================== +int MgxsInterface::get_group_index(double E) +{ + int g = + lower_bound_index(rev_energy_bins_.begin(), rev_energy_bins_.end(), E); + return num_energy_groups_ - g - 1.; +} + +//============================================================================== + void MgxsInterface::read_header(const std::string& path_cross_sections) { // Save name of HDF5 file to be read to struct data diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 5e1194aa9..7f5d76943 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -2,6 +2,7 @@ #include "openmc/cell.h" #include "openmc/geometry.h" +#include "openmc/material.h" #include "openmc/message_passing.h" #include "openmc/mgxs_interface.h" #include "openmc/output.h" @@ -48,10 +49,19 @@ FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) // Initialize element-wise arrays scalar_flux_new_.assign(n_source_elements_, 0.0); - scalar_flux_old_.assign(n_source_elements_, 1.0); scalar_flux_final_.assign(n_source_elements_, 0.0); source_.resize(n_source_elements_); + external_source_.assign(n_source_elements_, 0.0); tally_task_.resize(n_source_elements_); + volume_task_.resize(n_source_regions_); + + if (settings::run_mode == RunMode::EIGENVALUE) { + // If in eigenvalue mode, set starting flux to guess of unity + scalar_flux_old_.assign(n_source_elements_, 1.0); + } else { + // If in fixed source mode, set starting flux to guess of zero + scalar_flux_old_.assign(n_source_elements_, 0.0); + } // Initialize material array int64_t source_region_id = 0; @@ -68,6 +78,25 @@ FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) if (source_region_id != n_source_regions_) { fatal_error("Unexpected number of source regions"); } + + // Initialize tally volumes + tally_volumes_.resize(model::tallies.size()); + for (int i = 0; i < model::tallies.size(); i++) { + // Get the shape of the 3D result tensor + auto shape = model::tallies[i]->results().shape(); + + // 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]}); + } + + // Compute simulation domain volume based on ray source + auto* is = dynamic_cast(RandomRay::ray_source_.get()); + SpatialDistribution* space_dist = is->space(); + SpatialBox* sb = dynamic_cast(space_dist); + Position dims = sb->upper_right() - sb->lower_left(); + simulation_volume_ = dims.x * dims.y * dims.z; } void FlatSourceDomain::batch_reset() @@ -97,11 +126,11 @@ void FlatSourceDomain::update_neutron_source(double k_eff) // Temperature and angle indices, if using multiple temperature // data sets and/or anisotropic data sets. - // TODO: Currently assumes we are only using single temp/single - // angle data. + // TODO: Currently assumes we are only using single temp/single angle data. const int t = 0; const int a = 0; + // Add scattering source #pragma omp parallel for for (int sr = 0; sr < n_source_regions_; sr++) { int material = material_[sr]; @@ -110,23 +139,47 @@ void FlatSourceDomain::update_neutron_source(double k_eff) float sigma_t = data::mg.macro_xs_[material].get_xs( MgxsType::TOTAL, e_out, nullptr, nullptr, nullptr, t, a); float scatter_source = 0.0f; - float fission_source = 0.0f; for (int e_in = 0; e_in < negroups_; e_in++) { float scalar_flux = scalar_flux_old_[sr * negroups_ + e_in]; + float sigma_s = data::mg.macro_xs_[material].get_xs( MgxsType::NU_SCATTER, e_in, &e_out, nullptr, nullptr, t, a); - float nu_sigma_f = data::mg.macro_xs_[material].get_xs( - MgxsType::NU_FISSION, e_in, nullptr, nullptr, nullptr, t, a); - float chi = data::mg.macro_xs_[material].get_xs( - MgxsType::CHI_PROMPT, e_in, &e_out, nullptr, nullptr, t, a); scatter_source += sigma_s * scalar_flux; - fission_source += nu_sigma_f * scalar_flux * chi; } - fission_source *= inverse_k_eff; - float new_isotropic_source = (scatter_source + fission_source) / sigma_t; - source_[sr * negroups_ + e_out] = new_isotropic_source; + source_[sr * negroups_ + e_out] = scatter_source / sigma_t; + } + } + + if (settings::run_mode == RunMode::EIGENVALUE) { + // Add fission source if in eigenvalue mode +#pragma omp parallel for + for (int sr = 0; sr < n_source_regions_; sr++) { + int material = material_[sr]; + + for (int e_out = 0; e_out < negroups_; e_out++) { + float sigma_t = data::mg.macro_xs_[material].get_xs( + MgxsType::TOTAL, e_out, nullptr, nullptr, nullptr, t, a); + float fission_source = 0.0f; + + for (int e_in = 0; e_in < negroups_; e_in++) { + float scalar_flux = scalar_flux_old_[sr * negroups_ + e_in]; + float nu_sigma_f = data::mg.macro_xs_[material].get_xs( + MgxsType::NU_FISSION, e_in, nullptr, nullptr, nullptr, t, a); + float chi = data::mg.macro_xs_[material].get_xs( + MgxsType::CHI_PROMPT, e_in, &e_out, nullptr, nullptr, t, a); + fission_source += nu_sigma_f * scalar_flux * chi; + } + source_[sr * negroups_ + e_out] += + fission_source * inverse_k_eff / sigma_t; + } + } + } else { +// Add external source if in fixed source mode +#pragma omp parallel for + for (int se = 0; se < n_source_elements_; se++) { + source_[se] += external_source_[se]; } } @@ -355,10 +408,14 @@ void FlatSourceDomain::convert_source_regions_to_tallies() for (auto score_index = 0; score_index < tally.scores_.size(); score_index++) { auto score_bin = tally.scores_[score_index]; - // If a valid tally, filter, and score cobination has been found, + // If a valid tally, filter, and score combination has been found, // then add it to the list of tally tasks for this source element. - tally_task_[source_element].emplace_back( - i_tally, filter_index, score_index, score_bin); + TallyTask task(i_tally, filter_index, score_index, score_bin); + tally_task_[source_element].push_back(task); + + // Also add this task to the list of volume tasks for this source + // region. + volume_task_[sr].insert(task); } } } @@ -372,6 +429,16 @@ void FlatSourceDomain::convert_source_regions_to_tallies() mapped_all_tallies_ = all_source_regions_mapped; } +// Set the volume accumulators to zero for all tallies +void FlatSourceDomain::reset_tally_volumes() +{ +#pragma omp parallel for + for (int i = 0; i < tally_volumes_.size(); i++) { + auto& tensor = tally_volumes_[i]; + tensor.fill(0.0); // Set all elements of the tensor to 0.0 + } +} + // Tallying in random ray is not done directly during transport, rather, // it is done only once after each power iteration. This is made possible // by way of a mapping data structure that relates spatial source regions @@ -381,10 +448,13 @@ void FlatSourceDomain::convert_source_regions_to_tallies() // tally function simply traverses the mapping data structure and executes // the scoring operations to OpenMC's native tally result arrays. -void FlatSourceDomain::random_ray_tally() const +void FlatSourceDomain::random_ray_tally() { openmc::simulation::time_tallies.start(); + // Reset our tally volumes to zero + reset_tally_volumes(); + // Temperature and angle indices, if using multiple temperature // data sets and/or anisotropic data sets. // TODO: Currently assumes we are only using single temp/single @@ -397,32 +467,46 @@ void FlatSourceDomain::random_ray_tally() const // them. #pragma omp parallel for for (int sr = 0; sr < n_source_regions_; sr++) { - double volume = volume_[sr]; + // The fsr.volume_ is the unitless fractional simulation averaged volume + // (i.e., it is the FSR's fraction of the overall simulation volume). The + // simulation_volume_ is the total 3D physical volume in cm^3 of the entire + // global simulation domain (as defined by the ray source box). Thus, the + // FSR's true 3D spatial volume in cm^3 is found by multiplying its fraction + // of the total volume by the total volume. Not important in eigenvalue + // solves, but useful in fixed source solves for returning the flux shape + // with a magnitude that makes sense relative to the fixed source strength. + double volume = volume_[sr] * simulation_volume_; + double material = material_[sr]; for (int g = 0; g < negroups_; g++) { int idx = sr * negroups_ + g; - double flux = scalar_flux_new_[idx] * volume; + double flux = scalar_flux_new_[idx]; + + // Determine numerical score value for (auto& task : tally_task_[idx]) { double score; switch (task.score_type) { case SCORE_FLUX: - score = flux; + score = flux * volume; break; case SCORE_TOTAL: - score = flux * data::mg.macro_xs_[material].get_xs( - MgxsType::TOTAL, g, NULL, NULL, NULL, t, a); + score = flux * volume * + data::mg.macro_xs_[material].get_xs( + MgxsType::TOTAL, g, NULL, NULL, NULL, t, a); break; case SCORE_FISSION: - score = flux * data::mg.macro_xs_[material].get_xs( - MgxsType::FISSION, g, NULL, NULL, NULL, t, a); + score = flux * volume * + data::mg.macro_xs_[material].get_xs( + MgxsType::FISSION, g, NULL, NULL, NULL, t, a); break; case SCORE_NU_FISSION: - score = flux * data::mg.macro_xs_[material].get_xs( - MgxsType::NU_FISSION, g, NULL, NULL, NULL, t, a); + score = flux * volume * + data::mg.macro_xs_[material].get_xs( + MgxsType::NU_FISSION, g, NULL, NULL, NULL, t, a); break; case SCORE_EVENTS: @@ -435,13 +519,49 @@ void FlatSourceDomain::random_ray_tally() const "random ray mode."); break; } + + // Apply score to the appropriate tally bin Tally& tally {*model::tallies[task.tally_idx]}; #pragma omp atomic tally.results_(task.filter_idx, task.score_idx, TallyResult::VALUE) += score; + } // end tally task loop + } // end energy group loop + + // For flux tallies, the total volume of the spatial region is needed + // for normalizing the flux. We store this volume in a separate tensor. + // We only contribute to each volume tally bin once per FSR. + for (const auto& task : volume_task_[sr]) { + if (task.score_type == SCORE_FLUX) { +#pragma omp atomic + tally_volumes_[task.tally_idx](task.filter_idx, task.score_idx) += + volume; + } + } + } // end FSR loop + + // Normalize any flux scores by the total volume of the FSRs scoring to that + // bin. To do this, we loop over all tallies, and then all filter bins, + // and then scores. For each score, we check the tally data structure to + // see what index that score corresponds to. If that score is a flux score, + // then we divide it by volume. + for (int i = 0; i < model::tallies.size(); i++) { + Tally& tally {*model::tallies[i]}; +#pragma omp parallel for + for (int bin = 0; bin < tally.n_filter_bins(); bin++) { + for (int score_idx = 0; score_idx < tally.n_scores(); score_idx++) { + auto score_type = tally.scores_[score_idx]; + if (score_type == SCORE_FLUX) { + double vol = tally_volumes_[i](bin, score_idx); + if (vol > 0.0) { + tally.results_(bin, score_idx, TallyResult::VALUE) /= vol; + } + } } } } + + openmc::simulation::time_tallies.stop(); } void FlatSourceDomain::all_reduce_replicated_source_regions() @@ -683,4 +803,130 @@ void FlatSourceDomain::output_to_vtk() const } } +void FlatSourceDomain::apply_external_source_to_source_region( + Discrete* discrete, double strength_factor, int64_t source_region) +{ + const auto& discrete_energies = discrete->x(); + const auto& discrete_probs = discrete->prob(); + + for (int e = 0; e < discrete_energies.size(); e++) { + int g = data::mg.get_group_index(discrete_energies[e]); + external_source_[source_region * negroups_ + g] += + discrete_probs[e] * strength_factor; + } +} + +void FlatSourceDomain::apply_external_source_to_cell_instances(int32_t i_cell, + Discrete* discrete, double strength_factor, int target_material_id, + const vector& instances) +{ + Cell& cell = *model::cells[i_cell]; + + if (cell.type_ != Fill::MATERIAL) + return; + + for (int j : instances) { + int cell_material_idx = cell.material(j); + int cell_material_id = model::materials[cell_material_idx]->id(); + if (target_material_id == C_NONE || + cell_material_id == target_material_id) { + int64_t source_region = source_region_offsets_[i_cell] + j; + apply_external_source_to_source_region( + discrete, strength_factor, source_region); + } + } +} + +void FlatSourceDomain::apply_external_source_to_cell_and_children( + int32_t i_cell, Discrete* discrete, double strength_factor, + int32_t target_material_id) +{ + Cell& cell = *model::cells[i_cell]; + + if (cell.type_ == Fill::MATERIAL) { + vector instances(cell.n_instances_); + std::iota(instances.begin(), instances.end(), 0); + apply_external_source_to_cell_instances( + i_cell, discrete, strength_factor, target_material_id, instances); + } else if (target_material_id == C_NONE) { + std::unordered_map> cell_instance_list = + cell.get_contained_cells(0, nullptr); + for (const auto& pair : cell_instance_list) { + int32_t i_child_cell = pair.first; + apply_external_source_to_cell_instances(i_child_cell, discrete, + strength_factor, target_material_id, pair.second); + } + } +} + +void FlatSourceDomain::count_external_source_regions() +{ +#pragma omp parallel for reduction(+ : n_external_source_regions_) + for (int sr = 0; sr < n_source_regions_; sr++) { + float total = 0.f; + for (int e = 0; e < negroups_; e++) { + int64_t se = sr * negroups_ + e; + total += external_source_[se]; + } + if (total != 0.f) { + n_external_source_regions_++; + } + } +} + +void FlatSourceDomain::convert_external_sources() +{ + // Loop over external sources + for (int es = 0; es < model::external_sources.size(); es++) { + Source* s = model::external_sources[es].get(); + IndependentSource* is = dynamic_cast(s); + Discrete* energy = dynamic_cast(is->energy()); + const std::unordered_set& domain_ids = is->domain_ids(); + + double strength_factor = is->strength(); + + if (is->domain_type() == Source::DomainType::MATERIAL) { + for (int32_t material_id : domain_ids) { + for (int i_cell = 0; i_cell < model::cells.size(); i_cell++) { + apply_external_source_to_cell_and_children( + i_cell, energy, strength_factor, material_id); + } + } + } else if (is->domain_type() == Source::DomainType::CELL) { + for (int32_t cell_id : domain_ids) { + int32_t i_cell = model::cell_map[cell_id]; + apply_external_source_to_cell_and_children( + i_cell, energy, strength_factor, C_NONE); + } + } else if (is->domain_type() == Source::DomainType::UNIVERSE) { + for (int32_t universe_id : domain_ids) { + int32_t i_universe = model::universe_map[universe_id]; + Universe& universe = *model::universes[i_universe]; + for (int32_t i_cell : universe.cells_) { + apply_external_source_to_cell_and_children( + i_cell, energy, strength_factor, C_NONE); + } + } + } + } // End loop over external sources + + // Temperature and angle indices, if using multiple temperature + // data sets and/or anisotropic data sets. + // TODO: Currently assumes we are only using single temp/single angle data. + const int t = 0; + const int a = 0; + +// Divide the fixed source term by sigma t (to save time when applying each +// iteration) +#pragma omp parallel for + for (int sr = 0; sr < n_source_regions_; sr++) { + int material = material_[sr]; + for (int e = 0; e < negroups_; e++) { + float sigma_t = data::mg.macro_xs_[material].get_xs( + MgxsType::TOTAL, e, nullptr, nullptr, nullptr, t, a); + external_source_[sr * negroups_ + e] /= sigma_t; + } + } +} + } // namespace openmc diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index b9fd93a48..17866c302 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -111,13 +111,6 @@ void validate_random_ray_inputs() } } - // Validate solver mode - /////////////////////////////////////////////////////////////////// - if (settings::run_mode == RunMode::FIXED_SOURCE) { - fatal_error( - "Invalid run mode. Fixed source not yet supported in random ray mode."); - } - // Validate ray source /////////////////////////////////////////////////////////////////// @@ -125,8 +118,8 @@ void validate_random_ray_inputs() IndependentSource* is = dynamic_cast(RandomRay::ray_source_.get()); if (!is) { - fatal_error( - "Invalid ray source definition. Ray source must be IndependentSource."); + fatal_error("Invalid ray source definition. Ray source must provided and " + "be of type IndependentSource."); } // Check for box source @@ -134,24 +127,68 @@ void validate_random_ray_inputs() SpatialBox* sb = dynamic_cast(space_dist); if (!sb) { fatal_error( - "Invalid source definition -- only box sources are allowed in random " - "ray " - "mode. If no source is specified, OpenMC default is an isotropic point " - "source at the origin, which is invalid in random ray mode."); + "Invalid ray source definition -- only box sources are allowed."); } // Check that box source is not restricted to fissionable areas if (sb->only_fissionable()) { - fatal_error("Invalid source definition -- fissionable spatial distribution " - "not allowed for random ray source."); + fatal_error( + "Invalid ray source definition -- fissionable spatial distribution " + "not allowed."); } // Check for isotropic source UnitSphereDistribution* angle_dist = is->angle(); Isotropic* id = dynamic_cast(angle_dist); if (!id) { - fatal_error("Invalid source definition -- only isotropic sources are " - "allowed for random ray source."); + fatal_error("Invalid ray source definition -- only isotropic sources are " + "allowed."); + } + + // Validate external sources + /////////////////////////////////////////////////////////////////// + if (settings::run_mode == RunMode::FIXED_SOURCE) { + if (model::external_sources.size() < 1) { + fatal_error("Must provide a particle source (in addition to ray source) " + "in fixed source random ray mode."); + } + + for (int i = 0; i < model::external_sources.size(); i++) { + Source* s = model::external_sources[i].get(); + + // Check for independent source + IndependentSource* is = dynamic_cast(s); + + if (!is) { + fatal_error( + "Only IndependentSource external source types are allowed in " + "random ray mode"); + } + + // Check for isotropic source + UnitSphereDistribution* angle_dist = is->angle(); + Isotropic* id = dynamic_cast(angle_dist); + if (!id) { + fatal_error( + "Invalid source definition -- only isotropic external sources are " + "allowed in random ray mode."); + } + + // Validate that a domain ID was specified + if (is->domain_ids().size() == 0) { + fatal_error("Fixed sources must be specified by domain " + "id (cell, material, or universe) in random ray mode."); + } + + // Check that a discrete energy distribution was used + Distribution* d = is->energy(); + Discrete* dd = dynamic_cast(d); + if (!dd) { + fatal_error( + "Only discrete (multigroup) energy distributions are allowed for " + "external sources in random ray mode."); + } + } } // Validate plotting files @@ -208,6 +245,12 @@ RandomRaySimulation::RandomRaySimulation() void RandomRaySimulation::simulate() { + if (settings::run_mode == RunMode::FIXED_SOURCE) { + // Transfer external source user inputs onto random ray source regions + domain_.convert_external_sources(); + domain_.count_external_source_regions(); + } + // Random ray power iteration loop while (simulation::current_batch < settings::n_batches) { @@ -249,11 +292,13 @@ void RandomRaySimulation::simulate() // Add source to scalar flux, compute number of FSR hits int64_t n_hits = domain_.add_source_to_scalar_flux(); - // Compute random ray k-eff - k_eff_ = domain_.compute_k_eff(k_eff_); + if (settings::run_mode == RunMode::EIGENVALUE) { + // Compute random ray k-eff + k_eff_ = domain_.compute_k_eff(k_eff_); - // Store random ray k-eff into OpenMC's native k-eff variable - global_tally_tracklength = k_eff_; + // Store random ray k-eff into OpenMC's native k-eff variable + global_tally_tracklength = k_eff_; + } // Execute all tallying tasks, if this is an active batch if (simulation::current_batch > settings::n_inactive && mpi::master) { @@ -302,7 +347,7 @@ void RandomRaySimulation::output_simulation_results() const if (mpi::master) { print_results_random_ray(total_geometric_intersections_, avg_miss_rate_ / settings::n_batches, negroups_, - domain_.n_source_regions_); + domain_.n_source_regions_, domain_.n_external_source_regions_); if (model::plots.size() > 0) { domain_.output_to_vtk(); } @@ -339,7 +384,7 @@ void RandomRaySimulation::instability_check( // Print random ray simulation results void RandomRaySimulation::print_results_random_ray( uint64_t total_geometric_intersections, double avg_miss_rate, int negroups, - int64_t n_source_regions) const + int64_t n_source_regions, int64_t n_external_source_regions) const { using namespace simulation; @@ -355,6 +400,8 @@ void RandomRaySimulation::print_results_random_ray( fmt::print( " Total Iterations = {}\n", settings::n_batches); fmt::print(" Flat Source Regions (FSRs) = {}\n", n_source_regions); + fmt::print( + " FSRs Containing External Sources = {}\n", n_external_source_regions); fmt::print(" Total Geometric Intersections = {:.4e}\n", static_cast(total_geometric_intersections)); fmt::print(" Avg per Iteration = {:.4e}\n", @@ -387,7 +434,7 @@ void RandomRaySimulation::print_results_random_ray( show_time("Time per integration", time_per_integration); } - if (settings::verbosity >= 4) { + if (settings::verbosity >= 4 && settings::run_mode == RunMode::EIGENVALUE) { header("Results", 4); fmt::print(" k-effective = {:.5f} +/- {:.5f}\n", simulation::keff, simulation::keff_std); diff --git a/src/settings.cpp b/src/settings.cpp index 6790f2cc0..136020d4a 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -191,7 +191,8 @@ void get_run_parameters(pugi::xml_node node_base) } // Get number of inactive batches - if (run_mode == RunMode::EIGENVALUE) { + if (run_mode == RunMode::EIGENVALUE || + solver_type == SolverType::RANDOM_RAY) { if (check_for_node(node_base, "inactive")) { n_inactive = std::stoi(get_node_value(node_base, "inactive")); } @@ -524,8 +525,9 @@ void read_settings_xml(pugi::xml_node root) } // If no source specified, default to isotropic point source at origin with - // Watt spectrum - if (model::external_sources.empty()) { + // Watt spectrum. No default source is needed in random ray mode. + if (model::external_sources.empty() && + settings::solver_type != SolverType::RANDOM_RAY) { double T[] {0.0}; double p[] {1.0}; model::external_sources.push_back(make_unique( diff --git a/src/simulation.cpp b/src/simulation.cpp index 527d98db4..e218cdb29 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -137,7 +137,11 @@ int openmc_simulation_init() // Display header if (mpi::master) { if (settings::run_mode == RunMode::FIXED_SOURCE) { - header("FIXED SOURCE TRANSPORT SIMULATION", 3); + if (settings::solver_type == SolverType::MONTE_CARLO) { + header("FIXED SOURCE TRANSPORT SIMULATION", 3); + } else if (settings::solver_type == SolverType::RANDOM_RAY) { + header("FIXED SOURCE TRANSPORT SIMULATION (RANDOM RAY SOLVER)", 3); + } } else if (settings::run_mode == RunMode::EIGENVALUE) { if (settings::solver_type == SolverType::MONTE_CARLO) { header("K EIGENVALUE SIMULATION", 3); @@ -343,7 +347,13 @@ void initialize_batch() ++simulation::current_batch; if (settings::run_mode == RunMode::FIXED_SOURCE) { - write_message(6, "Simulating batch {}", simulation::current_batch); + if (settings::solver_type == SolverType::RANDOM_RAY && + simulation::current_batch < settings::n_inactive + 1) { + write_message( + 6, "Simulating batch {:<4} (inactive)", simulation::current_batch); + } else { + write_message(6, "Simulating batch {}", simulation::current_batch); + } } // Reset total starting particle weight used for normalizing tallies diff --git a/tests/regression_tests/random_ray_basic/results_true.dat b/tests/regression_tests/random_ray_basic/results_true.dat index 802e78a82..ee2e4000d 100644 --- a/tests/regression_tests/random_ray_basic/results_true.dat +++ b/tests/regression_tests/random_ray_basic/results_true.dat @@ -1,171 +1,171 @@ k-combined: -8.400322E-01 8.023349E-03 +8.400322E-01 8.023350E-03 tally 1: -1.260220E+00 -3.179889E-01 -1.484289E-01 -4.411066E-03 -3.612463E-01 -2.612843E-02 -7.086707E-01 -1.006119E-01 -3.342483E-02 -2.238499E-04 -8.134936E-02 -1.325949E-03 -4.194328E-01 -3.558669E-02 -4.287776E-03 -3.717447E-06 -1.043559E-02 -2.201986E-05 -5.878720E-01 -7.045887E-02 -6.147757E-03 -7.701173E-06 -1.496241E-02 -4.561699E-05 -1.768113E+00 -6.356917E-01 -6.513486E-03 -8.628535E-06 -1.585272E-02 -5.111136E-05 -5.063704E+00 -5.152401E+00 -2.440293E-03 -1.196869E-06 -6.038334E-03 -7.328193E-06 -3.253717E+00 -2.117655E+00 -1.389120E-02 -3.859385E-05 -3.863767E-02 -2.985798E-04 -1.876994E+00 -7.046366E-01 +5.086560E+00 +5.180937E+00 +1.885166E+00 +7.115505E-01 +4.588117E+00 +4.214785E+00 +2.860401E+00 +1.639329E+00 +4.245221E-01 +3.610930E-02 +1.033202E+00 +2.138892E-01 +1.692631E+00 +5.793967E-01 +5.445818E-02 +5.996625E-04 +1.325403E-01 +3.552030E-03 +2.372249E+00 +1.146944E+00 +7.808143E-02 +1.242279E-03 +1.900346E-01 +7.358492E-03 +7.134949E+00 +1.034824E+01 +8.272648E-02 +1.391872E-03 +2.013422E-01 +8.244790E-03 +2.043539E+01 +8.389902E+01 +3.099367E-02 +1.930673E-04 +7.669167E-02 +1.182113E-03 +1.313212E+01 +3.449537E+01 +1.764293E-01 +6.225587E-03 +4.907293E-01 +4.816401E-02 +7.567717E+00 +1.145439E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -8.390875E-01 -1.408791E-01 +3.383194E+00 +2.290469E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -4.513839E-01 -4.139640E-02 +1.819673E+00 +6.726159E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -6.682186E-01 -9.116003E-02 +2.693683E+00 +1.480961E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.849034E+00 -6.944337E-01 +7.453759E+00 +1.128171E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -4.523425E+00 -4.112118E+00 +1.823561E+01 +6.681652E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.821432E+00 -1.592568E+00 +1.137517E+01 +2.588512E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.159618E+00 -2.694138E-01 -1.354028E-01 -3.672094E-03 -3.295432E-01 -2.175122E-02 -6.880334E-01 -9.491215E-02 -3.234611E-02 -2.097782E-04 -7.872396E-02 -1.242596E-03 -4.184841E-01 -3.536436E-02 -4.274305E-03 -3.687209E-06 -1.040280E-02 -2.184074E-05 -5.810180E-01 -6.872944E-02 -6.060273E-03 -7.476500E-06 -1.474949E-02 -4.428617E-05 -1.782580E+00 -6.457892E-01 -6.552384E-03 -8.730345E-06 -1.594739E-02 -5.171444E-05 -5.278155E+00 -5.596601E+00 -2.546878E-03 -1.303010E-06 -6.302072E-03 -7.978072E-06 -3.420419E+00 -2.340454E+00 -1.465798E-02 -4.299061E-05 -4.077042E-02 -3.325951E-04 -1.279417E+00 -3.278133E-01 -1.509073E-01 -4.561836E-03 -3.672782E-01 -2.702150E-02 -7.212777E-01 -1.042487E-01 -3.411552E-02 -2.332877E-04 -8.303035E-02 -1.381852E-03 -4.269473E-01 -3.685202E-02 -4.378540E-03 -3.872997E-06 -1.065649E-02 -2.294124E-05 -5.973530E-01 -7.266946E-02 -6.260881E-03 -7.976490E-06 -1.523773E-02 -4.724780E-05 -1.795373E+00 -6.547440E-01 -6.635941E-03 -8.945067E-06 -1.615075E-02 -5.298634E-05 -5.161876E+00 -5.353441E+00 -2.505311E-03 -1.261399E-06 -6.199218E-03 -7.723296E-06 -3.344042E+00 -2.236603E+00 -1.443089E-02 -4.166228E-05 -4.013879E-02 -3.223186E-04 +4.601917E+00 +4.242626E+00 +1.719723E+00 +5.923467E-01 +4.185463E+00 +3.508696E+00 +2.730305E+00 +1.494324E+00 +4.108214E-01 +3.383938E-02 +9.998572E-01 +2.004436E-01 +1.660852E+00 +5.570433E-01 +5.428709E-02 +5.947848E-04 +1.321239E-01 +3.523137E-03 +2.306069E+00 +1.082856E+00 +7.697032E-02 +1.206037E-03 +1.873304E-01 +7.143816E-03 +7.075194E+00 +1.017519E+01 +8.322052E-02 +1.408295E-03 +2.025446E-01 +8.342072E-03 +2.094832E+01 +8.816716E+01 +3.234739E-02 +2.101889E-04 +8.004135E-02 +1.286945E-03 +1.357413E+01 +3.685984E+01 +1.861680E-01 +6.934828E-03 +5.178169E-01 +5.365103E-02 +5.072150E+00 +5.151431E+00 +1.916644E+00 +7.358713E-01 +4.664727E+00 +4.358847E+00 +2.859465E+00 +1.638250E+00 +4.332944E-01 +3.763171E-02 +1.054552E+00 +2.229070E-01 +1.693008E+00 +5.796672E-01 +5.561096E-02 +6.247543E-04 +1.353459E-01 +3.700658E-03 +2.368860E+00 +1.143296E+00 +7.951820E-02 +1.286690E-03 +1.935314E-01 +7.621558E-03 +7.119587E+00 +1.030023E+01 +8.428176E-02 +1.442932E-03 +2.051275E-01 +8.547244E-03 +2.046758E+01 +8.418768E+01 +3.181946E-02 +2.034766E-04 +7.873502E-02 +1.245847E-03 +1.325834E+01 +3.515919E+01 +1.832838E-01 +6.720556E-03 +5.097947E-01 +5.199331E-02 diff --git a/tests/regression_tests/random_ray_fixed_source/__init__.py b/tests/regression_tests/random_ray_fixed_source/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_fixed_source/cell/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source/cell/inputs_true.dat new file mode 100644 index 000000000..99e67745a --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source/cell/inputs_true.dat @@ -0,0 +1,204 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + 5.0 5.0 5.0 + 2 2 2 + -5.0 -5.0 -5.0 + +1 1 +1 1 + +1 1 +1 1 + + + 5.0 5.0 5.0 + 2 2 2 + -5.0 -5.0 -5.0 + +2 2 +2 2 + +2 2 +2 2 + + + 5.0 5.0 5.0 + 2 2 2 + -5.0 -5.0 -5.0 + +3 3 +3 3 + +3 3 +3 3 + + + 10.0 10.0 10.0 + 6 10 6 + 0.0 0.0 0.0 + +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +8 8 8 8 9 9 +8 9 9 9 9 9 +8 9 9 9 9 9 +8 9 9 9 9 9 +8 9 9 9 9 9 +7 9 9 9 9 9 + +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 8 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 + +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 8 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 + +9 9 9 8 9 9 +9 9 9 8 9 9 +9 9 9 8 9 9 +9 9 9 8 9 9 +9 9 9 8 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 + +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 + +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 + + + + + + + + + + fixed source + 1000 + 10 + 5 + + + 100.0 1.0 + + + cell + 4 + + + multi-group + + 400.0 + 100.0 + + + 0.0 0.0 0.0 60.0 100.0 60.0 + + + + + + + 1 10 1 + 0.0 0.0 0.0 + 10.0 100.0 10.0 + + + 6 1 1 + 0.0 50.0 0.0 + 60.0 60.0 10.0 + + + 6 1 1 + 0.0 90.0 30.0 + 60.0 100.0 40.0 + + + 1 + + + 2 + + + 3 + + + 1 + flux + analog + + + 2 + flux + analog + + + 3 + flux + analog + + + diff --git a/tests/regression_tests/random_ray_fixed_source/cell/results_true.dat b/tests/regression_tests/random_ray_fixed_source/cell/results_true.dat new file mode 100644 index 000000000..df923ab23 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source/cell/results_true.dat @@ -0,0 +1,47 @@ +tally 1: +3.751048E+01 +2.841797E+02 +9.930788E+00 +1.988180E+01 +3.781121E+00 +3.005165E+00 +2.383139E+00 +1.232560E+00 +1.561884E+00 +6.440577E-01 +1.089787E+00 +3.724896E-01 +6.608456E-01 +1.285592E-01 +2.372611E-01 +1.601299E-02 +7.814803E-02 +1.765829E-03 +2.862108E-02 +2.460129E-04 +tally 2: +1.089787E+00 +3.724896E-01 +3.767926E-01 +3.724399E-02 +8.614121E-02 +1.526889E-03 +3.610725E-02 +2.629885E-04 +1.466261E-02 +4.536997E-05 +4.653106E-03 +4.381672E-06 +tally 3: +1.617918E-03 +6.317049E-07 +1.161473E-03 +2.789553E-07 +1.198879E-03 +3.189531E-07 +1.031737E-03 +2.207381E-07 +5.466329E-04 +6.166808E-08 +2.146062E-04 +9.937520E-09 diff --git a/tests/regression_tests/random_ray_fixed_source/material/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source/material/inputs_true.dat new file mode 100644 index 000000000..7fde0c419 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source/material/inputs_true.dat @@ -0,0 +1,204 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + 5.0 5.0 5.0 + 2 2 2 + -5.0 -5.0 -5.0 + +1 1 +1 1 + +1 1 +1 1 + + + 5.0 5.0 5.0 + 2 2 2 + -5.0 -5.0 -5.0 + +2 2 +2 2 + +2 2 +2 2 + + + 5.0 5.0 5.0 + 2 2 2 + -5.0 -5.0 -5.0 + +3 3 +3 3 + +3 3 +3 3 + + + 10.0 10.0 10.0 + 6 10 6 + 0.0 0.0 0.0 + +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +8 8 8 8 9 9 +8 9 9 9 9 9 +8 9 9 9 9 9 +8 9 9 9 9 9 +8 9 9 9 9 9 +7 9 9 9 9 9 + +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 8 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 + +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 8 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 + +9 9 9 8 9 9 +9 9 9 8 9 9 +9 9 9 8 9 9 +9 9 9 8 9 9 +9 9 9 8 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 + +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 + +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 + + + + + + + + + + fixed source + 1000 + 10 + 5 + + + 100.0 1.0 + + + material + 1 + + + multi-group + + 400.0 + 100.0 + + + 0.0 0.0 0.0 60.0 100.0 60.0 + + + + + + + 1 10 1 + 0.0 0.0 0.0 + 10.0 100.0 10.0 + + + 6 1 1 + 0.0 50.0 0.0 + 60.0 60.0 10.0 + + + 6 1 1 + 0.0 90.0 30.0 + 60.0 100.0 40.0 + + + 1 + + + 2 + + + 3 + + + 1 + flux + analog + + + 2 + flux + analog + + + 3 + flux + analog + + + diff --git a/tests/regression_tests/random_ray_fixed_source/material/results_true.dat b/tests/regression_tests/random_ray_fixed_source/material/results_true.dat new file mode 100644 index 000000000..e3d528909 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source/material/results_true.dat @@ -0,0 +1,47 @@ +tally 1: +3.751047E+01 +2.841797E+02 +9.930788E+00 +1.988181E+01 +3.781121E+00 +3.005165E+00 +2.383139E+00 +1.232560E+00 +1.561884E+00 +6.440577E-01 +1.089787E+00 +3.724896E-01 +6.608456E-01 +1.285592E-01 +2.372611E-01 +1.601299E-02 +7.814803E-02 +1.765829E-03 +2.862108E-02 +2.460129E-04 +tally 2: +1.089787E+00 +3.724896E-01 +3.767925E-01 +3.724398E-02 +8.614120E-02 +1.526889E-03 +3.610725E-02 +2.629885E-04 +1.466261E-02 +4.536997E-05 +4.653106E-03 +4.381672E-06 +tally 3: +1.617918E-03 +6.317049E-07 +1.161473E-03 +2.789553E-07 +1.198879E-03 +3.189531E-07 +1.031737E-03 +2.207381E-07 +5.466329E-04 +6.166809E-08 +2.146062E-04 +9.937520E-09 diff --git a/tests/regression_tests/random_ray_fixed_source/test.py b/tests/regression_tests/random_ray_fixed_source/test.py new file mode 100644 index 000000000..41a6c35bb --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source/test.py @@ -0,0 +1,339 @@ +import os + +import numpy as np +import openmc +from openmc.utility_funcs import change_directory +import pytest + +from tests.testing_harness import TolerantPyAPITestHarness + +def fill_3d_list(n, val): + """ + Generates a 3D list of dimensions nxnxn filled with copies of val. + + Parameters: + n (int): The dimension of the 3D list. + val (any): The value to fill the 3D list with. + + Returns: + list: A 3D list of dimensions nxnxn filled with val. + """ + return [[[val for _ in range(n)] for _ in range(n)] for _ in range(n)] + + +class MGXSTestHarness(TolerantPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + +def create_random_ray_model(domain_type): + openmc.reset_auto_ids() + ############################################################################### + # Create multigroup data + + # Instantiate the energy group data + ebins = [1e-5, 20.0e6] + groups = openmc.mgxs.EnergyGroups(group_edges=ebins) + + # High scattering ratio means system is all scattering + # Low means fully absorbing + scattering_ratio = 0.5 + + source_total_xs = 0.1 + source_mat_data = openmc.XSdata('source', groups) + source_mat_data.order = 0 + source_mat_data.set_total([source_total_xs]) + source_mat_data.set_absorption([source_total_xs * (1.0 - scattering_ratio)]) + source_mat_data.set_scatter_matrix(np.rollaxis(np.array([[[source_total_xs * scattering_ratio]]]),0,3)) + + void_total_xs = 1.0e-4 + void_mat_data = openmc.XSdata('void', groups) + void_mat_data.order = 0 + void_mat_data.set_total([void_total_xs]) + void_mat_data.set_absorption([void_total_xs * (1.0 - scattering_ratio)]) + void_mat_data.set_scatter_matrix(np.rollaxis(np.array([[[void_total_xs * scattering_ratio]]]),0,3)) + + shield_total_xs = 0.1 + shield_mat_data = openmc.XSdata('shield', groups) + shield_mat_data.order = 0 + shield_mat_data.set_total([shield_total_xs]) + shield_mat_data.set_absorption([shield_total_xs * (1.0 - scattering_ratio)]) + shield_mat_data.set_scatter_matrix(np.rollaxis(np.array([[[shield_total_xs * scattering_ratio]]]),0,3)) + + mg_cross_sections_file = openmc.MGXSLibrary(groups) + mg_cross_sections_file.add_xsdatas([source_mat_data, void_mat_data, shield_mat_data]) + mg_cross_sections_file.export_to_hdf5() + + ############################################################################### + # Create materials for the problem + + # Instantiate some Macroscopic Data + source_data = openmc.Macroscopic('source') + void_data = openmc.Macroscopic('void') + shield_data = openmc.Macroscopic('shield') + + # Instantiate some Materials and register the appropriate Macroscopic objects + source_mat = openmc.Material(name='source') + source_mat.set_density('macro', 1.0) + source_mat.add_macroscopic(source_data) + + void_mat = openmc.Material(name='void') + void_mat.set_density('macro', 1.0) + void_mat.add_macroscopic(void_data) + + shield_mat = openmc.Material(name='shield') + shield_mat.set_density('macro', 1.0) + shield_mat.add_macroscopic(shield_data) + + # Instantiate a Materials collection and export to XML + materials_file = openmc.Materials([source_mat, void_mat, shield_mat]) + materials_file.cross_sections = "mgxs.h5" + + ############################################################################### + # Define problem geometry + + source_cell = openmc.Cell(fill=source_mat, name='infinite source region') + void_cell = openmc.Cell(fill=void_mat, name='infinite void region') + shield_cell = openmc.Cell(fill=shield_mat, name='infinite shield region') + + sub = openmc.Universe() + sub.add_cells([source_cell]) + + vub = openmc.Universe() + vub.add_cells([void_cell]) + + aub = openmc.Universe() + aub.add_cells([shield_cell]) + + # n controls the dimension of subdivision within each outer lattice element + # E.g., n = 10 results in 1cm cubic FSRs + n = 2 + delta = 10.0 / n + ll = [-5.0, -5.0, -5.0] + pitch = [delta, delta, delta] + + source_lattice = openmc.RectLattice() + source_lattice.lower_left = ll + source_lattice.pitch = pitch + source_lattice.universes = fill_3d_list(n, sub) + + void_lattice = openmc.RectLattice() + void_lattice.lower_left = ll + void_lattice.pitch = pitch + void_lattice.universes = fill_3d_list(n, vub) + + shield_lattice = openmc.RectLattice() + shield_lattice.lower_left = ll + shield_lattice.pitch = pitch + shield_lattice.universes = fill_3d_list(n, aub) + + source_lattice_cell = openmc.Cell(fill=source_lattice, name='source lattice cell') + su = openmc.Universe() + su.add_cells([source_lattice_cell]) + + void_lattice_cell = openmc.Cell(fill=void_lattice, name='void lattice cell') + vu = openmc.Universe() + vu.add_cells([void_lattice_cell]) + + shield_lattice_cell = openmc.Cell(fill=shield_lattice, name='shield lattice cell') + au = openmc.Universe() + au.add_cells([shield_lattice_cell]) + + z_base = [ + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [vu, vu, vu, vu, au, au], + [vu, au, au, au, au, au], + [vu, au, au, au, au, au], + [vu, au, au, au, au, au], + [vu, au, au, au, au, au], + [su, au, au, au, au, au] + ] + + z_col = [ + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, vu, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au] + ] + + z_high = [ + [au, au, au, vu, au, au], + [au, au, au, vu, au, au], + [au, au, au, vu, au, au], + [au, au, au, vu, au, au], + [au, au, au, vu, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au] + ] + + z_cap = [ + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au] + ] + + dogleg_pattern = [ + z_base, + z_col, + z_col, + z_high, + z_cap, + z_cap + ] + + x = 60.0 + x_dim = 6 + + y = 100.0 + y_dim = 10 + + z = 60.0 + z_dim = 6 + + lattice = openmc.RectLattice() + lattice.lower_left = [0.0, 0.0, 0.0] + lattice.pitch = [x/x_dim, y/y_dim, z/z_dim] + lattice.universes = dogleg_pattern + + lattice_cell = openmc.Cell(fill=lattice, name='dogleg lattice cell') + + lattice_uni = openmc.Universe() + lattice_uni.add_cells([lattice_cell]) + + x_low = openmc.XPlane(x0=0.0,boundary_type='reflective') + x_high = openmc.XPlane(x0=x,boundary_type='vacuum') + + y_low = openmc.YPlane(y0=0.0,boundary_type='reflective') + y_high = openmc.YPlane(y0=y,boundary_type='vacuum') + + z_low = openmc.ZPlane(z0=0.0,boundary_type='reflective') + z_high = openmc.ZPlane(z0=z,boundary_type='vacuum') + + full_domain = openmc.Cell(fill=lattice_uni, region=+x_low & -x_high & +y_low & -y_high & +z_low & -z_high, name='full domain') + + root = openmc.Universe(name='root universe') + root.add_cell(full_domain) + + # Create a geometry with the two cells and export to XML + geometry = openmc.Geometry(root) + + ############################################################################### + # Define problem settings + + # Instantiate a Settings object, set all runtime parameters, and export to XML + settings = openmc.Settings() + settings.energy_mode = "multi-group" + settings.batches = 10 + settings.inactive = 5 + settings.particles = 1000 + settings.run_mode = 'fixed source' + + settings.random_ray['distance_active'] = 400.0 + settings.random_ray['distance_inactive'] = 100.0 + + # Create an initial uniform spatial source for ray integration + lower_left = (0.0, 0.0, 0.0) + upper_right = (x, y, z) + uniform_dist = openmc.stats.Box(lower_left, upper_right) + settings.random_ray['ray_source']= openmc.IndependentSource(space=uniform_dist) + + # Create the neutron source in the bottom right of the moderator + strengths = [1.0] + midpoints = [100.0] + energy_distribution = openmc.stats.Discrete(x=midpoints,p=strengths) + if domain_type == 'cell': + domain = source_lattice_cell + elif domain_type == 'material': + domain = source_mat + elif domain_type == 'universe': + domain = sub + source = openmc.IndependentSource( + energy=energy_distribution, + constraints={'domains': [domain]} + ) + settings.source = [source] + + ############################################################################### + # Define tallies + + estimator = 'analog' + + # Case 3A + mesh_3A = openmc.RegularMesh() + mesh_3A.dimension = (1, y_dim, 1) + mesh_3A.lower_left = (0.0, 0.0, 0.0) + mesh_3A.upper_right = (10.0, y, 10.0) + mesh_filter_3A = openmc.MeshFilter(mesh_3A) + + tally_3A = openmc.Tally(name="Case 3A") + tally_3A.filters = [mesh_filter_3A] + tally_3A.scores = ['flux'] + tally_3A.estimator = estimator + + # Case 3B + mesh_3B = openmc.RegularMesh() + mesh_3B.dimension = (x_dim, 1, 1) + mesh_3B.lower_left = (0.0, 50.0, 0.0) + mesh_3B.upper_right = (x, 60.0, 10.0) + mesh_filter_3B = openmc.MeshFilter(mesh_3B) + + tally_3B = openmc.Tally(name="Case 3B") + tally_3B.filters = [mesh_filter_3B] + tally_3B.scores = ['flux'] + tally_3B.estimator = estimator + + # Case 3C + mesh_3C = openmc.RegularMesh() + mesh_3C.dimension = (x_dim, 1, 1) + mesh_3C.lower_left = (0.0, 90.0, 30.0) + mesh_3C.upper_right = (x, 100.0, 40.0) + mesh_filter_3C = openmc.MeshFilter(mesh_3C) + + tally_3C = openmc.Tally(name="Case 3C") + tally_3C.filters = [mesh_filter_3C] + tally_3C.scores = ['flux'] + tally_3C.estimator = estimator + + # Instantiate a Tallies collection and export to XML + tallies = openmc.Tallies([tally_3A, tally_3B, tally_3C]) + + ############################################################################### + # Assmble Model + + model = openmc.model.Model() + model.geometry = geometry + model.materials = materials_file + model.settings = settings + model.xs_data = mg_cross_sections_file + model.tallies = tallies + + return model + +@pytest.mark.parametrize("domain_type", ["cell", "material", "universe"]) +def test_random_ray_fixed_source(domain_type): + with change_directory(domain_type): + model = create_random_ray_model(domain_type) + + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_fixed_source/universe/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source/universe/inputs_true.dat new file mode 100644 index 000000000..349917fee --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source/universe/inputs_true.dat @@ -0,0 +1,204 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + 5.0 5.0 5.0 + 2 2 2 + -5.0 -5.0 -5.0 + +1 1 +1 1 + +1 1 +1 1 + + + 5.0 5.0 5.0 + 2 2 2 + -5.0 -5.0 -5.0 + +2 2 +2 2 + +2 2 +2 2 + + + 5.0 5.0 5.0 + 2 2 2 + -5.0 -5.0 -5.0 + +3 3 +3 3 + +3 3 +3 3 + + + 10.0 10.0 10.0 + 6 10 6 + 0.0 0.0 0.0 + +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +8 8 8 8 9 9 +8 9 9 9 9 9 +8 9 9 9 9 9 +8 9 9 9 9 9 +8 9 9 9 9 9 +7 9 9 9 9 9 + +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 8 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 + +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 8 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 + +9 9 9 8 9 9 +9 9 9 8 9 9 +9 9 9 8 9 9 +9 9 9 8 9 9 +9 9 9 8 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 + +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 + +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 + + + + + + + + + + fixed source + 1000 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 400.0 + 100.0 + + + 0.0 0.0 0.0 60.0 100.0 60.0 + + + + + + + 1 10 1 + 0.0 0.0 0.0 + 10.0 100.0 10.0 + + + 6 1 1 + 0.0 50.0 0.0 + 60.0 60.0 10.0 + + + 6 1 1 + 0.0 90.0 30.0 + 60.0 100.0 40.0 + + + 1 + + + 2 + + + 3 + + + 1 + flux + analog + + + 2 + flux + analog + + + 3 + flux + analog + + + diff --git a/tests/regression_tests/random_ray_fixed_source/universe/results_true.dat b/tests/regression_tests/random_ray_fixed_source/universe/results_true.dat new file mode 100644 index 000000000..36d24e939 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source/universe/results_true.dat @@ -0,0 +1,47 @@ +tally 1: +3.751047E+01 +2.841797E+02 +9.930788E+00 +1.988180E+01 +3.781121E+00 +3.005165E+00 +2.383139E+00 +1.232560E+00 +1.561884E+00 +6.440577E-01 +1.089787E+00 +3.724896E-01 +6.608456E-01 +1.285592E-01 +2.372611E-01 +1.601299E-02 +7.814803E-02 +1.765829E-03 +2.862107E-02 +2.460129E-04 +tally 2: +1.089787E+00 +3.724896E-01 +3.767926E-01 +3.724399E-02 +8.614120E-02 +1.526889E-03 +3.610725E-02 +2.629885E-04 +1.466261E-02 +4.536997E-05 +4.653106E-03 +4.381672E-06 +tally 3: +1.617918E-03 +6.317049E-07 +1.161473E-03 +2.789553E-07 +1.198879E-03 +3.189531E-07 +1.031737E-03 +2.207381E-07 +5.466329E-04 +6.166808E-08 +2.146062E-04 +9.937520E-09 diff --git a/tests/regression_tests/random_ray_vacuum/results_true.dat b/tests/regression_tests/random_ray_vacuum/results_true.dat index 744ce6cef..c25999aad 100644 --- a/tests/regression_tests/random_ray_vacuum/results_true.dat +++ b/tests/regression_tests/random_ray_vacuum/results_true.dat @@ -1,171 +1,171 @@ k-combined: 1.010455E-01 1.585558E-02 tally 1: -1.849176E-01 -7.634332E-03 -2.181815E-02 -1.062861E-04 -5.310100E-02 -6.295730E-04 -4.048251E-02 -3.851890E-04 -1.893676E-03 -8.448769E-07 -4.608828E-03 -5.004529E-06 -4.063643E-03 -4.022442E-06 -4.112970E-05 -4.186661E-10 -1.001015E-04 -2.479919E-09 -7.467029E-03 -1.178864E-05 -7.688748E-05 -1.266903E-09 -1.871288E-04 -7.504350E-09 -3.870644E-02 -3.010745E-04 -1.375240E-04 -3.807356E-09 -3.347099E-04 -2.255298E-08 -4.524967E-01 -4.098857E-02 -2.437418E-04 -1.190325E-08 -6.031220E-04 -7.288126E-08 -4.989226E-01 -4.993728E-02 -2.374296E-03 -1.135824E-06 -6.603983E-03 -8.787258E-06 -3.899991E-01 -3.308783E-02 +7.466984E-01 +1.245920E-01 +2.771079E-01 +1.714504E-02 +6.744252E-01 +1.015566E-01 +1.634870E-01 +6.288701E-03 +2.405120E-02 +1.362874E-04 +5.853580E-02 +8.072823E-04 +1.641162E-02 +6.568765E-05 +5.223801E-04 +6.753516E-08 +1.271369E-03 +4.000365E-07 +3.014736E-02 +1.922973E-04 +9.765325E-04 +2.043645E-07 +2.376685E-03 +1.210529E-06 +1.562379E-01 +4.906526E-03 +1.746664E-03 +6.141658E-07 +4.251084E-03 +3.638028E-06 +1.826385E+00 +6.678141E-01 +3.095716E-03 +1.920117E-06 +7.660132E-03 +1.175650E-05 +2.013809E+00 +8.136726E-01 +3.015545E-02 +1.832201E-04 +8.387586E-02 +1.417475E-03 +1.573069E+00 +5.388374E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -7.108982E-02 -1.144390E-03 +2.867849E-01 +1.864798E-02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.295259E-03 -6.352159E-06 +2.136372E-02 +1.035499E-04 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -9.852001E-03 -1.984406E-05 +3.973323E-02 +3.229766E-04 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -4.414391E-02 -3.905201E-04 +1.779974E-01 +6.350613E-03 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.571668E-01 -1.323140E-02 +1.036886E+00 +2.151147E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.752932E-01 -1.517930E-02 +1.109991E+00 +2.468005E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.465446E-01 -4.901884E-03 -1.700791E-02 -6.587674E-05 -4.139385E-02 -3.902131E-04 -3.424032E-02 -2.841338E-04 -1.598985E-03 -6.216312E-07 -3.891610E-03 -3.682159E-06 -4.067582E-03 -4.209468E-06 -4.152829E-05 -4.494649E-10 -1.010715E-04 -2.662352E-09 -7.526712E-03 -1.225032E-05 -7.769969E-05 -1.328443E-09 -1.891055E-04 -7.868877E-09 -4.008649E-02 -3.246821E-04 -1.417944E-04 -4.070719E-09 -3.451035E-04 -2.411301E-08 -4.859902E-01 -4.747592E-02 -2.606214E-04 -1.369749E-08 -6.448895E-04 -8.386705E-08 -5.475198E-01 -6.061269E-02 -2.625477E-03 -1.405458E-06 -7.302631E-03 -1.087327E-05 -1.909660E-01 -8.147906E-03 -2.269063E-02 -1.149570E-04 -5.522446E-02 -6.809342E-04 -4.196583E-02 -4.141620E-04 -1.980406E-03 -9.227119E-07 -4.819913E-03 -5.465576E-06 -4.247004E-03 -4.420116E-06 -4.341806E-05 -4.691518E-10 -1.056709E-04 -2.778965E-09 -7.742814E-03 -1.272112E-05 -8.039606E-05 -1.389209E-09 -1.956679E-04 -8.228817E-09 -3.982370E-02 -3.190931E-04 -1.427171E-04 -4.103942E-09 -3.473492E-04 -2.430981E-08 -4.849535E-01 -4.707014E-02 -2.678327E-04 -1.438540E-08 -6.627333E-04 -8.807897E-08 -5.493457E-01 -6.069440E-02 -2.717400E-03 -1.501450E-06 -7.558312E-03 -1.161591E-05 +5.813217E-01 +7.704274E-02 +2.160141E-01 +1.062660E-02 +5.257350E-01 +6.294540E-02 +1.358032E-01 +4.462239E-03 +2.030839E-02 +1.002755E-04 +4.942655E-02 +5.939703E-04 +1.612888E-02 +6.604109E-05 +5.274425E-04 +7.250333E-08 +1.283689E-03 +4.294649E-07 +2.985723E-02 +1.925418E-04 +9.868482E-04 +2.142916E-07 +2.401791E-03 +1.269331E-06 +1.590678E-01 +5.110940E-03 +1.800903E-03 +6.566490E-07 +4.383091E-03 +3.889678E-06 +1.928611E+00 +7.475889E-01 +3.310101E-03 +2.209547E-06 +8.190613E-03 +1.352862E-05 +2.172777E+00 +9.544513E-01 +3.334566E-02 +2.267149E-04 +9.274926E-02 +1.753971E-03 +7.566911E-01 +1.278105E-01 +2.881892E-01 +1.854375E-02 +7.013949E-01 +1.098417E-01 +1.662732E-01 +6.495387E-03 +2.515274E-02 +1.488430E-04 +6.121675E-02 +8.816538E-04 +1.682747E-02 +6.933017E-05 +5.514441E-04 +7.567901E-08 +1.342104E-03 +4.482757E-07 +3.068773E-02 +1.997062E-04 +1.021094E-03 +2.240938E-07 +2.485139E-03 +1.327393E-06 +1.578722E-01 +5.013656E-03 +1.812622E-03 +6.620083E-07 +4.411613E-03 +3.921424E-06 +1.922798E+00 +7.400474E-01 +3.401690E-03 +2.320513E-06 +8.417243E-03 +1.420805E-05 +2.178318E+00 +9.546106E-01 +3.451316E-02 +2.421994E-04 +9.599661E-02 +1.873766E-03 From 8be35cd7b5c9e761cacfb49c258157cfaa850211 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 17 Jun 2024 14:25:05 -0500 Subject: [PATCH 121/671] Fix bug with invalidated iterators when enforcing precedence in region expressions (#2950) --- include/openmc/cell.h | 3 +-- src/cell.cpp | 47 +++++++++++++++++++++---------------------- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/include/openmc/cell.h b/include/openmc/cell.h index e68443a32..d78614f05 100644 --- a/include/openmc/cell.h +++ b/include/openmc/cell.h @@ -128,8 +128,7 @@ private: void add_precedence(); //! Add parenthesis to enforce precedence - std::vector::iterator add_parentheses( - std::vector::iterator start); + gsl::index add_parentheses(gsl::index start); //! Remove complement operators from the expression void remove_complement_ops(); diff --git a/src/cell.cpp b/src/cell.cpp index a46f05687..888766787 100644 --- a/src/cell.cpp +++ b/src/cell.cpp @@ -578,17 +578,14 @@ void Region::apply_demorgan( //! precedence than unions using parentheses. //============================================================================== -std::vector::iterator Region::add_parentheses( - std::vector::iterator start) +gsl::index Region::add_parentheses(gsl::index start) { - int32_t start_token = *start; - // Add left parenthesis - if (start_token == OP_INTERSECTION) { - start = expression_.insert(start - 1, OP_LEFT_PAREN); - } else { - start = expression_.insert(start + 1, OP_LEFT_PAREN); + int32_t start_token = expression_[start]; + // Add left parenthesis and set new position to be after parenthesis + if (start_token == OP_UNION) { + start += 2; } - start++; + expression_.insert(expression_.begin() + start - 1, OP_LEFT_PAREN); // Keep track of return iterator distance. If we don't encounter a left // parenthesis, we return an iterator corresponding to wherever the right @@ -600,23 +597,23 @@ std::vector::iterator Region::add_parentheses( // Add right parenthesis // While the start iterator is within the bounds of infix - while (start < expression_.end()) { + while (start + 1 < expression_.size()) { start++; // If the current token is an operator and is different than the start token - if (*start >= OP_UNION && *start != start_token) { + if (expression_[start] >= OP_UNION && expression_[start] != start_token) { // Skip wrapped regions but save iterator position to check precedence and // add right parenthesis, right parenthesis position depends on the // operator, when the operator is a union then do not include the operator // in the region, when the operator is an intersection then include the // operator and next surface - if (*start == OP_LEFT_PAREN) { - return_it_dist = std::distance(expression_.begin(), start); + if (expression_[start] == OP_LEFT_PAREN) { + return_it_dist = start; int depth = 1; do { start++; - if (*start > OP_COMPLEMENT) { - if (*start == OP_RIGHT_PAREN) { + if (expression_[start] > OP_COMPLEMENT) { + if (expression_[start] == OP_RIGHT_PAREN) { depth--; } else { depth++; @@ -624,10 +621,12 @@ std::vector::iterator Region::add_parentheses( } } while (depth > 0); } else { - start = expression_.insert( - start_token == OP_UNION ? start - 1 : start, OP_RIGHT_PAREN); + if (start_token == OP_UNION) { + --start; + } + expression_.insert(expression_.begin() + start, OP_RIGHT_PAREN); if (return_it_dist > 0) { - return expression_.begin() + return_it_dist; + return return_it_dist; } else { return start - 1; } @@ -638,7 +637,7 @@ std::vector::iterator Region::add_parentheses( // return iterator expression_.push_back(OP_RIGHT_PAREN); if (return_it_dist > 0) { - return expression_.begin() + return_it_dist; + return return_it_dist; } else { return start - 1; } @@ -651,21 +650,21 @@ void Region::add_precedence() int32_t current_op = 0; std::size_t current_dist = 0; - for (auto it = expression_.begin(); it != expression_.end(); it++) { - int32_t token = *it; + for (gsl::index i = 0; i < expression_.size(); i++) { + int32_t token = expression_[i]; if (token == OP_UNION || token == OP_INTERSECTION) { if (current_op == 0) { // Set the current operator if is hasn't been set current_op = token; - current_dist = std::distance(expression_.begin(), it); + current_dist = i; } else if (token != current_op) { // If the current operator doesn't match the token, add parenthesis to // assert precedence if (current_op == OP_INTERSECTION) { - it = add_parentheses(expression_.begin() + current_dist); + i = add_parentheses(current_dist); } else { - it = add_parentheses(it); + i = add_parentheses(i); } current_op = 0; current_dist = 0; From 89d4dafa5ab95503a423b6f18a58f1cea23e68f3 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 17 Jun 2024 16:00:45 -0500 Subject: [PATCH 122/671] Implement policy for Python, C++, and CMake versions (#3035) --- .github/workflows/ci.yml | 6 ---- CMakeLists.txt | 18 +++------- docs/source/devguide/index.rst | 1 + docs/source/devguide/policies.rst | 35 +++++++++++++++++++ docs/source/devguide/styleguide.rst | 2 +- examples/custom_source/CMakeLists.txt | 2 +- .../CMakeLists.txt | 2 +- setup.py | 4 +-- .../deplete_with_transfer_rates/test.py | 1 - tests/unit_tests/test_data_photon.py | 2 -- 10 files changed, 44 insertions(+), 29 deletions(-) create mode 100644 docs/source/devguide/policies.rst diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7aaaf1da4..9293e319b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,12 +35,6 @@ jobs: vectfit: [n] include: - - python-version: "3.8" - omp: n - mpi: n - - python-version: "3.9" - omp: n - mpi: n - python-version: "3.11" omp: n mpi: n diff --git a/CMakeLists.txt b/CMakeLists.txt index 1841251ff..98473e492 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.10 FATAL_ERROR) +cmake_minimum_required(VERSION 3.16 FATAL_ERROR) project(openmc C CXX) # Set version numbers @@ -16,11 +16,6 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) # Set module path set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/Modules) -# Allow user to specify _ROOT variables -if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.12) - cmake_policy(SET CMP0074 NEW) -endif() - # Enable correct usage of CXX_EXTENSIONS if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.22) cmake_policy(SET CMP0128 NEW) @@ -272,11 +267,6 @@ endif() # xtensor header-only library #=============================================================================== -# CMake 3.13+ will complain about policy CMP0079 unless it is set explicitly -if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.13) - cmake_policy(SET CMP0079 NEW) -endif() - find_package_write_status(xtensor) if (NOT xtensor_FOUND) add_subdirectory(vendor/xtl) @@ -580,9 +570,9 @@ target_compile_options(openmc PRIVATE ${cxxflags}) target_include_directories(openmc PRIVATE ${CMAKE_BINARY_DIR}/include) target_link_libraries(openmc libopenmc) -# Ensure C++14 standard is used and turn off GNU extensions -target_compile_features(openmc PUBLIC cxx_std_14) -target_compile_features(libopenmc PUBLIC cxx_std_14) +# Ensure C++17 standard is used and turn off GNU extensions +target_compile_features(openmc PUBLIC cxx_std_17) +target_compile_features(libopenmc PUBLIC cxx_std_17) set_target_properties(openmc libopenmc PROPERTIES CXX_EXTENSIONS OFF) #=============================================================================== diff --git a/docs/source/devguide/index.rst b/docs/source/devguide/index.rst index d100fdcdf..2e131e094 100644 --- a/docs/source/devguide/index.rst +++ b/docs/source/devguide/index.rst @@ -15,6 +15,7 @@ other related topics. contributing workflow styleguide + policies tests user-input docbuild diff --git a/docs/source/devguide/policies.rst b/docs/source/devguide/policies.rst new file mode 100644 index 000000000..2cf319987 --- /dev/null +++ b/docs/source/devguide/policies.rst @@ -0,0 +1,35 @@ +.. _devguide_policies: + +======== +Policies +======== + +--------------------- +Python Version Policy +--------------------- + +OpenMC follows the Scientific Python Ecosystem Coordination guidelines `SPEC 0 +`_ on minimum supported +versions, which recommends that support for Python versions be dropped 3 years +after their initial release. + +------------------- +C++ Standard Policy +------------------- + +C++ code in OpenMC must conform to the most recent C++ standard that is fully +supported in the `version of the gcc compiler +`_ that is distributed with the +oldest version of Ubuntu that is still within its `standard support period +`_. Ubuntu 20.04 LTS will be supported +through April 2025 and is distributed with gcc 9.3.0, which fully supports the +C++17 standard. + +-------------------- +CMake Version Policy +-------------------- + +Similar to the C++ standard policy, the minimum supported version of CMake +corresponds to whatever version is distributed with the oldest version of Ubuntu +still within its standard support period. Ubuntu 20.04 LTS is distributed with +CMake 3.16. diff --git a/docs/source/devguide/styleguide.rst b/docs/source/devguide/styleguide.rst index 3c71d14ad..b266f5f02 100644 --- a/docs/source/devguide/styleguide.rst +++ b/docs/source/devguide/styleguide.rst @@ -40,7 +40,7 @@ Follow the `C++ Core Guidelines`_ except when they conflict with another guideline listed here. For convenience, many important guidelines from that list are repeated here. -Conform to the C++14 standard. +Conform to the C++17 standard. Always use C++-style comments (``//``) as opposed to C-style (``/**/``). (It is more difficult to comment out a large section of code that uses C-style diff --git a/examples/custom_source/CMakeLists.txt b/examples/custom_source/CMakeLists.txt index 21463ed51..ba5ae94ad 100644 --- a/examples/custom_source/CMakeLists.txt +++ b/examples/custom_source/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.10 FATAL_ERROR) +cmake_minimum_required(VERSION 3.16 FATAL_ERROR) project(openmc_sources CXX) add_library(source SHARED source_ring.cpp) find_package(OpenMC REQUIRED) diff --git a/examples/parameterized_custom_source/CMakeLists.txt b/examples/parameterized_custom_source/CMakeLists.txt index 8232f3b54..20dac4d8f 100644 --- a/examples/parameterized_custom_source/CMakeLists.txt +++ b/examples/parameterized_custom_source/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.10 FATAL_ERROR) +cmake_minimum_required(VERSION 3.16 FATAL_ERROR) project(openmc_sources CXX) add_library(parameterized_source SHARED parameterized_source_ring.cpp) find_package(OpenMC REQUIRED) diff --git a/setup.py b/setup.py index a33037ad3..de87e1490 100755 --- a/setup.py +++ b/setup.py @@ -53,15 +53,13 @@ kwargs = { 'Topic :: Scientific/Engineering' 'Programming Language :: C++', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', ], # Dependencies - 'python_requires': '>=3.8', + 'python_requires': '>=3.10', 'install_requires': [ 'numpy>=1.9', 'h5py', 'scipy', 'ipython', 'matplotlib', 'pandas', 'lxml', 'uncertainties', 'setuptools' diff --git a/tests/regression_tests/deplete_with_transfer_rates/test.py b/tests/regression_tests/deplete_with_transfer_rates/test.py index 669fd4bea..9bd5a052b 100644 --- a/tests/regression_tests/deplete_with_transfer_rates/test.py +++ b/tests/regression_tests/deplete_with_transfer_rates/test.py @@ -46,7 +46,6 @@ def model(): return openmc.Model(geometry, materials, settings) -@pytest.mark.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9+") @pytest.mark.parametrize("rate, dest_mat, power, ref_result", [ (1e-5, None, 0.0, 'no_depletion_only_removal'), (-1e-5, None, 0.0, 'no_depletion_only_feed'), diff --git a/tests/unit_tests/test_data_photon.py b/tests/unit_tests/test_data_photon.py index 171dd52f3..010ac1f35 100644 --- a/tests/unit_tests/test_data_photon.py +++ b/tests/unit_tests/test_data_photon.py @@ -121,8 +121,6 @@ def test_reactions(element, reaction): reactions[18] -# TODO: Remove skip when support is Python 3.9+ -@pytest.mark.skipif(not hasattr(pd.options, 'future'), reason='pandas version too old') @pytest.mark.parametrize('element', ['Pu'], indirect=True) def test_export_to_hdf5(tmpdir, element): filename = str(tmpdir.join('tmp.h5')) From ce4176deb5b8f64a45c46ad87063c76140ce77dd Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Mon, 17 Jun 2024 16:14:12 -0500 Subject: [PATCH 123/671] Hexagonal lattice iterators (#2921) Co-authored-by: Paul Romano --- include/openmc/lattice.h | 35 +++--- src/geometry_aux.cpp | 3 +- src/lattice.cpp | 46 +++++-- .../cell_instances/test_hex_multilattice.py | 119 ++++++++++++++++++ .../test_rect_multilattice.py} | 22 ++-- 5 files changed, 192 insertions(+), 33 deletions(-) create mode 100644 tests/unit_tests/cell_instances/test_hex_multilattice.py rename tests/unit_tests/{test_cell_instance.py => cell_instances/test_rect_multilattice.py} (84%) diff --git a/include/openmc/lattice.h b/include/openmc/lattice.h index 11dda4a6b..429d81ddd 100644 --- a/include/openmc/lattice.h +++ b/include/openmc/lattice.h @@ -56,13 +56,14 @@ public: virtual ~Lattice() {} - virtual int32_t const& operator[](array const& i_xyz) = 0; + virtual const int32_t& operator[](const array& i_xyz) = 0; virtual LatticeIter begin(); - LatticeIter end(); + virtual LatticeIter end(); + virtual int32_t& back(); virtual ReverseLatticeIter rbegin(); - ReverseLatticeIter rend(); + virtual ReverseLatticeIter rend(); //! Convert internal universe values from IDs to indices using universe_map. void adjust_indices(); @@ -81,7 +82,7 @@ public: //! \param i_xyz[3] The indices for a lattice tile. //! \return true if the given indices fit within the lattice bounds. False //! otherwise. - virtual bool are_valid_indices(array const& i_xyz) const = 0; + virtual bool are_valid_indices(const array& i_xyz) const = 0; //! \brief Find the next lattice surface crossing //! \param r A 3D Cartesian coordinate. @@ -125,7 +126,7 @@ public: //! \param i_xyz[3] The indices for a lattice tile. //! \return Distribcell offset i.e. the largest instance number for the target //! cell found in the geometry tree under this lattice tile. - virtual int32_t& offset(int map, array const& i_xyz) = 0; + virtual int32_t& offset(int map, const array& i_xyz) = 0; //! \brief Get the distribcell offset for a lattice tile. //! \param The map index for the target cell. @@ -167,12 +168,12 @@ public: LatticeIter& operator++() { - while (indx_ < lat_.universes_.size()) { + while (indx_ < lat_.end().indx_) { ++indx_; if (lat_.is_valid_index(indx_)) return *this; } - indx_ = lat_.universes_.size(); + indx_ = lat_.end().indx_; return *this; } @@ -190,7 +191,7 @@ public: ReverseLatticeIter& operator++() { - while (indx_ > -1) { + while (indx_ > lat_.begin().indx_ - 1) { --indx_; if (lat_.is_valid_index(indx_)) return *this; @@ -206,9 +207,9 @@ class RectLattice : public Lattice { public: explicit RectLattice(pugi::xml_node lat_node); - int32_t const& operator[](array const& i_xyz) override; + const int32_t& operator[](const array& i_xyz) override; - bool are_valid_indices(array const& i_xyz) const override; + bool are_valid_indices(const array& i_xyz) const override; std::pair> distance( Position r, Direction u, const array& i_xyz) const override; @@ -221,7 +222,7 @@ public: Position get_local_position( Position r, const array& i_xyz) const override; - int32_t& offset(int map, array const& i_xyz) override; + int32_t& offset(int map, const array& i_xyz) override; int32_t offset(int map, int indx) const override; @@ -241,13 +242,19 @@ class HexLattice : public Lattice { public: explicit HexLattice(pugi::xml_node lat_node); - int32_t const& operator[](array const& i_xyz) override; + const int32_t& operator[](const array& i_xyz) override; LatticeIter begin() override; ReverseLatticeIter rbegin() override; - bool are_valid_indices(array const& i_xyz) const override; + LatticeIter end() override; + + int32_t& back() override; + + ReverseLatticeIter rend() override; + + bool are_valid_indices(const array& i_xyz) const override; std::pair> distance( Position r, Direction u, const array& i_xyz) const override; @@ -262,7 +269,7 @@ public: bool is_valid_index(int indx) const override; - int32_t& offset(int map, array const& i_xyz) override; + int32_t& offset(int map, const array& i_xyz) override; int32_t offset(int map, int indx) const override; diff --git a/src/geometry_aux.cpp b/src/geometry_aux.cpp index 10b239be8..050d4db96 100644 --- a/src/geometry_aux.cpp +++ b/src/geometry_aux.cpp @@ -530,7 +530,8 @@ std::string distribcell_path_inner(int32_t target_cell, int32_t map, if (c.type_ != Fill::MATERIAL) { int32_t temp_offset; if (c.type_ == Fill::UNIVERSE) { - temp_offset = offset + c.offset_[map]; + temp_offset = + offset + c.offset_[map]; // TODO: should also apply to lattice fills? } else { Lattice& lat = *model::lattices[c.fill_]; int32_t indx = lat.universes_.size() * map + lat.begin().indx_; diff --git a/src/lattice.cpp b/src/lattice.cpp index fa2e2828e..73005ad09 100644 --- a/src/lattice.cpp +++ b/src/lattice.cpp @@ -58,6 +58,11 @@ LatticeIter Lattice::end() return LatticeIter(*this, universes_.size()); } +int32_t& Lattice::back() +{ + return universes_.back(); +} + ReverseLatticeIter Lattice::rbegin() { return ReverseLatticeIter(*this, universes_.size() - 1); @@ -106,9 +111,10 @@ int32_t Lattice::fill_offset_table(int32_t offset, int32_t target_univ_id, // offsets_ array doesn't actually include the offset accounting for the last // universe, so we get the before-last offset for the given map and then // explicitly add the count for the last universe. - if (offsets_[map * universes_.size()] != C_NONE) { - int last_offset = offsets_[(map + 1) * universes_.size() - 1]; - int last_univ = universes_.back(); + if (offsets_[map * universes_.size() + this->begin().indx_] != C_NONE) { + int last_offset = + offsets_[(map + 1) * universes_.size() - this->begin().indx_ - 1]; + int last_univ = this->back(); return last_offset + count_universe_instances(last_univ, target_univ_id, univ_count_memo); } @@ -117,6 +123,7 @@ int32_t Lattice::fill_offset_table(int32_t offset, int32_t target_univ_id, offsets_[map * universes_.size() + it.indx_] = offset; offset += count_universe_instances(*it, target_univ_id, univ_count_memo); } + return offset; } @@ -225,14 +232,14 @@ RectLattice::RectLattice(pugi::xml_node lat_node) : Lattice {lat_node} //============================================================================== -int32_t const& RectLattice::operator[](array const& i_xyz) +const int32_t& RectLattice::operator[](const array& i_xyz) { return universes_[get_flat_index(i_xyz)]; } //============================================================================== -bool RectLattice::are_valid_indices(array const& i_xyz) const +bool RectLattice::are_valid_indices(const array& i_xyz) const { return ((i_xyz[0] >= 0) && (i_xyz[0] < n_cells_[0]) && (i_xyz[1] >= 0) && (i_xyz[1] < n_cells_[1]) && (i_xyz[2] >= 0) && @@ -354,7 +361,7 @@ Position RectLattice::get_local_position( //============================================================================== -int32_t& RectLattice::offset(int map, array const& i_xyz) +int32_t& RectLattice::offset(int map, const array& i_xyz) { return offsets_[n_cells_[0] * n_cells_[1] * n_cells_[2] * map + n_cells_[0] * n_cells_[1] * i_xyz[2] + @@ -676,13 +683,19 @@ void HexLattice::fill_lattice_y(const vector& univ_words) //============================================================================== -int32_t const& HexLattice::operator[](array const& i_xyz) +const int32_t& HexLattice::operator[](const array& i_xyz) { return universes_[get_flat_index(i_xyz)]; } //============================================================================== +// The HexLattice iterators need their own versions b/c the universes array is +// "square", meaning that it is allocated with entries that are intentionally +// left empty. As such, the iterator indices need to skip the empty entries to +// get cell instances and geometry paths correct. See the image in the Theory +// and Methodology section on "Hexagonal Lattice Indexing" for a visual of where +// the empty positions are. LatticeIter HexLattice::begin() { return LatticeIter(*this, n_rings_ - 1); @@ -693,9 +706,24 @@ ReverseLatticeIter HexLattice::rbegin() return ReverseLatticeIter(*this, universes_.size() - n_rings_); } +int32_t& HexLattice::back() +{ + return universes_[universes_.size() - n_rings_]; +} + +LatticeIter HexLattice::end() +{ + return LatticeIter(*this, universes_.size() - n_rings_ + 1); +} + +ReverseLatticeIter HexLattice::rend() +{ + return ReverseLatticeIter(*this, n_rings_ - 2); +} + //============================================================================== -bool HexLattice::are_valid_indices(array const& i_xyz) const +bool HexLattice::are_valid_indices(const array& i_xyz) const { // Check if (x, alpha, z) indices are valid, accounting for number of rings return ((i_xyz[0] >= 0) && (i_xyz[1] >= 0) && (i_xyz[2] >= 0) && @@ -992,7 +1020,7 @@ bool HexLattice::is_valid_index(int indx) const //============================================================================== -int32_t& HexLattice::offset(int map, array const& i_xyz) +int32_t& HexLattice::offset(int map, const array& i_xyz) { int nx {2 * n_rings_ - 1}; int ny {2 * n_rings_ - 1}; diff --git a/tests/unit_tests/cell_instances/test_hex_multilattice.py b/tests/unit_tests/cell_instances/test_hex_multilattice.py new file mode 100644 index 000000000..3f503fe9b --- /dev/null +++ b/tests/unit_tests/cell_instances/test_hex_multilattice.py @@ -0,0 +1,119 @@ +from math import sqrt + +import pytest +import numpy as np +import openmc +import openmc.lib + +from tests import cdtemp + + +@pytest.fixture(scope='module', autouse=True) +def double_hex_lattice_model(): + openmc.reset_auto_ids() + radius = 0.9 + pin_lattice_pitch = 2.0 + # make the hex prism a little larger to make sure test + # locations are definitively in the model + hex_prism_edge = 1.2 * pin_lattice_pitch + + model = openmc.Model() + + # materials + nat_u = openmc.Material() + nat_u.set_density('g/cm3', 12.0) + nat_u.add_element('U', 1.0) + + graphite = openmc.Material() + graphite.set_density('g/cm3', 1.1995) + graphite.add_element('C', 1.0) + + # zplanes to define lower and upper region + z_low = openmc.ZPlane(-10, boundary_type='vacuum') + z_mid = openmc.ZPlane(0) + z_high = openmc.ZPlane(10, boundary_type='vacuum') + hex_prism = openmc.model.HexagonalPrism( + edge_length=hex_prism_edge, boundary_type='reflective') + + # geometry + cyl = openmc.ZCylinder(r=radius) + univ = openmc.model.pin([cyl], [nat_u, graphite]) + + # create a hexagonal lattice of compacts + hex_lattice = openmc.HexLattice() + hex_lattice.orientation = 'y' + hex_lattice.pitch = (pin_lattice_pitch,) + hex_lattice.center = (0., 0.) + center = [univ] + ring = [univ, univ, univ, univ, univ, univ] + hex_lattice.universes = [ring, center] + lower_hex_cell = openmc.Cell(fill=hex_lattice, region=-hex_prism & +z_low & -z_mid) + upper_hex_cell = openmc.Cell(fill=hex_lattice, region=-hex_prism & +z_mid & -z_high) + hex_cells = [lower_hex_cell, upper_hex_cell] + model.geometry = openmc.Geometry(hex_cells) + + # moderator + cell = next(iter(univ.get_all_cells().values())) + tally = openmc.Tally(tally_id=1) + filter = openmc.DistribcellFilter(cell) + tally.filters = [filter] + tally.scores = ['flux'] + model.tallies = [tally] + + # settings + # source definition. fission source given bounding box of graphite active region + system_LL = (-pin_lattice_pitch*sqrt(3)/2, -pin_lattice_pitch, -5) + system_UR = (pin_lattice_pitch*sqrt(3)/2, pin_lattice_pitch, 5) + source_dist = openmc.stats.Box(system_LL, system_UR) + model.settings.source = openmc.IndependentSource(space=source_dist) + model.settings.particles = 100 + model.settings.inactive = 2 + model.settings.batches = 10 + + with cdtemp(): + model.export_to_xml() + openmc.lib.init() + yield + openmc.lib.finalize() + + +# Lower cell instances +# 6 +# 5 4 +# 3 +# 2 1 +# 0 +# Upper cell instances +# 13 +# 12 11 +# 10 +# 9 8 +# 7 +hex_expected_results = [ + ((0.0, -2.0, -5.0), 0), + ((1.732, -1.0, -5.0), 1), + ((-1.732, -1.0, -5.0), 2), + ((0.0, 0.0, -0.1), 3), + ((1.732, 1.0, -5.0), 4), + ((-1.732, 1.0, -5.0), 5), + ((0.0, 2.0, -0.1), 6), + ((0.0, -2.0, 5.0), 7), + ((1.732, -1.0, 5.0), 8), + ((-1.732, -1.0, 5.0), 9), + ((0.0, 0.0, 5.0), 10), + ((1.732, 1.0, 5.0), 11), + ((-1.732, 1.0, 5.0), 12), + ((0.0, 2.0, 5.0), 13), +] + + +@pytest.mark.parametrize("r,expected_cell_instance", hex_expected_results, ids=str) +def test_cell_instance_hex_multilattice(r, expected_cell_instance): + _, cell_instance = openmc.lib.find_cell(r) + assert cell_instance == expected_cell_instance + + +def test_cell_instance_multilattice_results(): + openmc.lib.run() + tally_results = openmc.lib.tallies[1].mean + assert (tally_results != 0.0).all() diff --git a/tests/unit_tests/test_cell_instance.py b/tests/unit_tests/cell_instances/test_rect_multilattice.py similarity index 84% rename from tests/unit_tests/test_cell_instance.py rename to tests/unit_tests/cell_instances/test_rect_multilattice.py index 25c20cfef..aaecb3bda 100644 --- a/tests/unit_tests/test_cell_instance.py +++ b/tests/unit_tests/cell_instances/test_rect_multilattice.py @@ -1,5 +1,5 @@ -import numpy as np import pytest +import numpy as np import openmc import openmc.lib @@ -8,7 +8,7 @@ from tests import cdtemp @pytest.fixture(scope='module', autouse=True) -def double_lattice_model(): +def double_rect_lattice_model(): openmc.reset_auto_ids() model = openmc.Model() @@ -40,8 +40,9 @@ def double_lattice_model(): cell_with_lattice2.translation = (2., 0., 0.) model.geometry = openmc.Geometry([cell_with_lattice1, cell_with_lattice2]) - tally = openmc.Tally() - tally.filters = [openmc.DistribcellFilter(c)] + tally = openmc.Tally(tally_id=1) + dcell_filter = openmc.DistribcellFilter(c) + tally.filters = [dcell_filter] tally.scores = ['flux'] model.tallies = [tally] @@ -50,7 +51,8 @@ def double_lattice_model(): bbox[0][2] = -0.5 bbox[1][2] = 0.5 space = openmc.stats.Box(*bbox) - model.settings.source = openmc.IndependentSource(space=space) + source = openmc.IndependentSource(space=space) + model.settings.source = source # Add necessary settings and export model.settings.batches = 10 @@ -63,14 +65,13 @@ def double_lattice_model(): yield openmc.lib.finalize() - # This shows the expected cell instance numbers for each lattice position: # ┌─┬─┬─┬─┐ # │2│3│6│7│ # ├─┼─┼─┼─┤ # │0│1│4│5│ # └─┴─┴─┴─┘ -expected_results = [ +rect_expected_results = [ ((0.5, 0.5, 0.0), 0), ((1.5, 0.5, 0.0), 1), ((0.5, 1.5, 0.0), 2), @@ -80,13 +81,16 @@ expected_results = [ ((2.5, 1.5, 0.0), 6), ((3.5, 1.5, 0.0), 7), ] -@pytest.mark.parametrize("r,expected_cell_instance", expected_results) -def test_cell_instance_multilattice(r, expected_cell_instance): + + +@pytest.mark.parametrize("r,expected_cell_instance", rect_expected_results, ids=lambda p : f'{p}') +def test_cell_instance_rect_multilattice(r, expected_cell_instance): _, cell_instance = openmc.lib.find_cell(r) assert cell_instance == expected_cell_instance def test_cell_instance_multilattice_results(): + openmc.run() openmc.lib.run() tally_results = openmc.lib.tallies[1].mean assert (tally_results != 0.0).all() From 97537d5e9ae9284262bc1d54cc3e51227627f397 Mon Sep 17 00:00:00 2001 From: vanessalulla Date: Mon, 17 Jun 2024 23:31:34 -0400 Subject: [PATCH 124/671] Rename max_splits to max_history_splits, set default value to 1.0e7 (#2954) Co-authored-by: Patrick Shriwise Co-authored-by: Paul Romano --- docs/source/io_formats/settings.rst | 4 +-- include/openmc/settings.h | 3 +- openmc/settings.py | 36 ++++++++++--------- src/finalize.cpp | 4 +-- src/settings.cpp | 7 ++-- src/weight_windows.cpp | 2 +- .../weightwindows/generators/test.py | 2 +- .../weightwindows/inputs_true.dat | 2 +- tests/regression_tests/weightwindows/test.py | 2 +- tests/unit_tests/weightwindows/test.py | 2 +- tests/unit_tests/weightwindows/test_ww_gen.py | 2 +- 11 files changed, 36 insertions(+), 30 deletions(-) diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index ae3266dab..6e9a7f2ab 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -285,10 +285,10 @@ then, OpenMC will only use up to the :math:`P_1` data. :ref:`energy_mode`. ------------------------ -```` Element +```` Element ------------------------ -The ```` element indicates the number of times a particle can split during a history. +The ```` element indicates the number of times a particle can split during a history. *Default*: 1000 diff --git a/include/openmc/settings.h b/include/openmc/settings.h index b71ec3649..df4a54a6f 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -121,7 +121,8 @@ extern std::unordered_set statepoint_batch; //!< Batches when state should be written extern std::unordered_set source_write_surf_id; //!< Surface ids where sources will be written -extern int max_splits; //!< maximum number of particle splits for weight windows +extern int + max_history_splits; //!< maximum number of particle splits for weight windows extern int64_t max_surface_particles; //!< maximum number of particles to be //!< banked on surfaces per process extern TemperatureMethod diff --git a/openmc/settings.py b/openmc/settings.py index 631246029..9565c2b78 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -116,7 +116,7 @@ class Settings: .. versionadded:: 0.14.1 max_order : None or int Maximum scattering order to apply globally when in multi-group mode. - max_splits : int + max_history_splits : int Maximum number of times a particle can split during a history .. versionadded:: 0.13 @@ -358,7 +358,7 @@ class Settings: self._weight_windows_on = None self._weight_windows_file = None self._weight_window_checkpoints = {} - self._max_splits = None + self._max_history_splits = None self._max_tracks = None self._random_ray = {} @@ -1007,14 +1007,18 @@ class Settings: self._weight_window_checkpoints = weight_window_checkpoints @property - def max_splits(self) -> int: - return self._max_splits + def max_splits(self): + raise AttributeError('max_splits has been deprecated. Please use max_history_splits instead') - @max_splits.setter - def max_splits(self, value: int): + @property + def max_history_splits(self) -> int: + return self._max_history_splits + + @max_history_splits.setter + def max_history_splits(self, value: int): cv.check_type('maximum particle splits', value, Integral) cv.check_greater_than('max particle splits', value, 0) - self._max_splits = value + self._max_history_splits = value @property def max_tracks(self) -> int: @@ -1472,10 +1476,10 @@ class Settings: subelement = ET.SubElement(element, "surface") subelement.text = str(self._weight_window_checkpoints['surface']).lower() - def _create_max_splits_subelement(self, root): - if self._max_splits is not None: - elem = ET.SubElement(root, "max_splits") - elem.text = str(self._max_splits) + def _create_max_history_splits_subelement(self, root): + if self._max_history_splits is not None: + elem = ET.SubElement(root, "max_history_splits") + elem.text = str(self._max_history_splits) def _create_max_tracks_subelement(self, root): if self._max_tracks is not None: @@ -1834,10 +1838,10 @@ class Settings: value = value in ('true', '1') self.weight_window_checkpoints[key] = value - def _max_splits_from_xml_element(self, root): - text = get_text(root, 'max_splits') + def _max_history_splits_from_xml_element(self, root): + text = get_text(root, 'max_history_splits') if text is not None: - self.max_splits = int(text) + self.max_history_splits = int(text) def _max_tracks_from_xml_element(self, root): text = get_text(root, 'max_tracks') @@ -1915,7 +1919,7 @@ class Settings: self._create_weight_window_generators_subelement(element, mesh_memo) self._create_weight_windows_file_element(element) self._create_weight_window_checkpoints_subelement(element) - self._create_max_splits_subelement(element) + self._create_max_history_splits_subelement(element) self._create_max_tracks_subelement(element) self._create_random_ray_subelement(element) @@ -2019,7 +2023,7 @@ class Settings: settings._weight_windows_from_xml_element(elem, meshes) settings._weight_window_generators_from_xml_element(elem, meshes) settings._weight_window_checkpoints_from_xml_element(elem) - settings._max_splits_from_xml_element(elem) + settings._max_history_splits_from_xml_element(elem) settings._max_tracks_from_xml_element(elem) settings._random_ray_from_xml_element(elem) diff --git a/src/finalize.cpp b/src/finalize.cpp index 26efc9723..2bde0e338 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -91,8 +91,8 @@ int openmc_finalize() settings::max_lost_particles = 10; settings::max_order = 0; settings::max_particles_in_flight = 100000; - settings::max_particle_events = 1000000; - settings::max_splits = 1000; + settings::max_particle_events = 1'000'000; + settings::max_history_splits = 10'000'000; settings::max_tracks = 1000; settings::max_write_lost_particles = -1; settings::n_log_bins = 8000; diff --git a/src/settings.cpp b/src/settings.cpp index 136020d4a..f5cd1b7e9 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -107,7 +107,7 @@ int max_order {0}; int n_log_bins {8000}; int n_batches; int n_max_batches; -int max_splits {1000}; +int max_history_splits {10'000'000}; int max_tracks {1000}; ResScatMethod res_scat_method {ResScatMethod::rvs}; double res_scat_energy_min {0.01}; @@ -981,8 +981,9 @@ void read_settings_xml(pugi::xml_node root) weight_windows_on = get_node_value_bool(root, "weight_windows_on"); } - if (check_for_node(root, "max_splits")) { - settings::max_splits = std::stoi(get_node_value(root, "max_splits")); + if (check_for_node(root, "max_history_splits")) { + settings::max_history_splits = + std::stoi(get_node_value(root, "max_history_splits")); } if (check_for_node(root, "max_tracks")) { diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index 1ca83a2bd..2eb930965 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -102,7 +102,7 @@ void apply_weight_windows(Particle& p) // the window if (weight > weight_window.upper_weight) { // do not further split the particle if above the limit - if (p.n_split() >= settings::max_splits) + if (p.n_split() >= settings::max_history_splits) return; double n_split = std::ceil(weight / weight_window.upper_weight); diff --git a/tests/regression_tests/weightwindows/generators/test.py b/tests/regression_tests/weightwindows/generators/test.py index 1d516db8d..d6a40b44f 100644 --- a/tests/regression_tests/weightwindows/generators/test.py +++ b/tests/regression_tests/weightwindows/generators/test.py @@ -22,7 +22,7 @@ def test_ww_generator(run_in_tmpdir): model.settings.particles = 500 model.settings.batches = 5 model.settings.run_mode = 'fixed source' - model.settings.max_splits = 100 + model.settings.max_history_splits = 100 mesh = openmc.RegularMesh.from_domain(model.geometry.root_universe) energy_bounds = np.linspace(0.0, 1e6, 70) diff --git a/tests/regression_tests/weightwindows/inputs_true.dat b/tests/regression_tests/weightwindows/inputs_true.dat index c40f4b761..dcc41ae84 100644 --- a/tests/regression_tests/weightwindows/inputs_true.dat +++ b/tests/regression_tests/weightwindows/inputs_true.dat @@ -70,7 +70,7 @@ 10 1e-38 - 200 + 200 diff --git a/tests/regression_tests/weightwindows/test.py b/tests/regression_tests/weightwindows/test.py index 9ed855976..864f4f062 100644 --- a/tests/regression_tests/weightwindows/test.py +++ b/tests/regression_tests/weightwindows/test.py @@ -48,7 +48,7 @@ def model(): settings.run_mode = 'fixed source' settings.particles = 200 settings.batches = 2 - settings.max_splits = 200 + settings.max_history_splits = 200 settings.photon_transport = True space = Point((0.001, 0.001, 0.001)) energy = Discrete([14E6], [1.0]) diff --git a/tests/unit_tests/weightwindows/test.py b/tests/unit_tests/weightwindows/test.py index 8af843cbd..d3da0cd11 100644 --- a/tests/unit_tests/weightwindows/test.py +++ b/tests/unit_tests/weightwindows/test.py @@ -94,7 +94,7 @@ def model(): settings.run_mode = 'fixed source' settings.particles = 500 settings.batches = 2 - settings.max_splits = 100 + settings.max_history_splits = 100 settings.photon_transport = True space = Point((0.001, 0.001, 0.001)) energy = Discrete([14E6], [1.0]) diff --git a/tests/unit_tests/weightwindows/test_ww_gen.py b/tests/unit_tests/weightwindows/test_ww_gen.py index 072e332cb..6fc3747b1 100644 --- a/tests/unit_tests/weightwindows/test_ww_gen.py +++ b/tests/unit_tests/weightwindows/test_ww_gen.py @@ -55,7 +55,7 @@ def model(): run_mode='fixed source', particles=100, batches=10, - max_splits=10, + max_history_splits=10, survival_biasing=False ) From b0732cb6b3754a2718b7c6c028f151e317a5174f Mon Sep 17 00:00:00 2001 From: Lorenzo Chierici Date: Tue, 18 Jun 2024 13:31:07 +0200 Subject: [PATCH 125/671] add dagmc fill materials to homogenized materials method (#3026) Co-authored-by: Paul Romano --- openmc/mesh.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/openmc/mesh.py b/openmc/mesh.py index 215c91923..5456abd1a 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -211,6 +211,18 @@ class MeshBase(IDManagerMixin, ABC): # Create homogenized material for each element materials = model.geometry.get_all_materials() + + # Account for materials in DAGMC universes + # TODO: This should really get incorporated in lower-level calls to + # get_all_materials, but right now it requires information from the + # Model object + for cell in model.geometry.get_all_cells().values(): + if isinstance(cell.fill, openmc.DAGMCUniverse): + names = cell.fill.material_names + materials.update({ + mat.id: mat for mat in model.materials if mat.name in names + }) + homogenized_materials = [] for mat_volume_list in mat_volume_by_element: material_ids, volumes = [list(x) for x in zip(*mat_volume_list)] From e33e66aa888453efaf43afcc94e7d6b2c74b1e7c Mon Sep 17 00:00:00 2001 From: pitkajuh Date: Tue, 18 Jun 2024 11:56:51 +0000 Subject: [PATCH 126/671] Fix #2994, non-existent path causes segmentation fault when saving plot (#3038) Co-authored-by: Patrick Shriwise Co-authored-by: Paul Romano --- src/plot.cpp | 5 +++++ tests/unit_tests/test_plots.py | 25 ++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/plot.cpp b/src/plot.cpp index bf733cff4..f29653f80 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -315,6 +315,11 @@ void Plot::set_output_path(pugi::xml_node plot_node) } else { filename = fmt::format("plot_{}", id()); } + const std::string dir_if_present = + filename.substr(0, filename.find_last_of("/") + 1); + if (dir_if_present.size() > 0 && !dir_exists(dir_if_present)) { + fatal_error(fmt::format("Directory '{}' does not exist!", dir_if_present)); + } // add appropriate file extension to name switch (type_) { case PlotType::slice: diff --git a/tests/unit_tests/test_plots.py b/tests/unit_tests/test_plots.py index 922e4d9dc..a1e15016a 100644 --- a/tests/unit_tests/test_plots.py +++ b/tests/unit_tests/test_plots.py @@ -11,7 +11,7 @@ def myplot(): plot.width = (100., 100.) plot.origin = (2., 3., -10.) plot.pixels = (500, 500) - plot.filename = 'myplot' + plot.filename = './not-a-dir/myplot' plot.type = 'slice' plot.basis = 'yz' plot.background = 'black' @@ -221,3 +221,26 @@ def test_voxel_plot_roundtrip(): assert new_plot.origin == plot.origin assert new_plot.width == plot.width assert new_plot.color_by == plot.color_by + + +def test_plot_directory(run_in_tmpdir): + pwr_pin = openmc.examples.pwr_pin_cell() + + # create a standard plot, expected to work + plot = openmc.Plot() + plot.filename = 'plot_1' + plot.type = 'slice' + plot.pixels = (10, 10) + plot.color_by = 'material' + plot.width = (100., 100.) + pwr_pin.plots = [plot] + pwr_pin.plot_geometry() + + # use current directory, also expected to work + plot.filename = './plot_1' + pwr_pin.plot_geometry() + + # use a non-existent directory, should raise an error + plot.filename = './not-a-dir/plot_1' + with pytest.raises(RuntimeError, match='does not exist'): + pwr_pin.plot_geometry() From f6d3ee7a26b1c3eeb616425890259c5645afa718 Mon Sep 17 00:00:00 2001 From: ybadr16 <104090877+ybadr16@users.noreply.github.com> Date: Wed, 19 Jun 2024 01:56:08 +0300 Subject: [PATCH 127/671] Update IsogonalOctagon to use xz basis and update tests (#3045) --- openmc/model/surface_composite.py | 46 ++++++++++++++-------- tests/unit_tests/test_surface_composite.py | 2 +- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index 1fa4234fb..7c134e843 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -233,7 +233,7 @@ class IsogonalOctagon(CompositeSurface): r"""Infinite isogonal octagon composite surface An isogonal octagon is composed of eight planar surfaces. The prism is - parallel to the x, y, or z axis. The remaining two axes (y and z, z and x, + parallel to the x, y, or z axis. The remaining two axes (y and z, x and z, or x and y) serve as a basis for constructing the surfaces. Two surfaces are parallel to the first basis axis, two surfaces are parallel to the second basis axis, and the remaining four surfaces intersect both @@ -241,7 +241,7 @@ class IsogonalOctagon(CompositeSurface): This class acts as a proper surface, meaning that unary `+` and `-` operators applied to it will produce a half-space. The negative side is - defined to be the region inside of the octogonal prism. + defined to be the region inside of the octagonal prism. .. versionadded:: 0.13.1 @@ -249,7 +249,7 @@ class IsogonalOctagon(CompositeSurface): ---------- center : iterable of float Coordinate for the central axis of the octagon in the - (y, z), (z, x), or (x, y) basis. + (y, z), (x, z), or (x, y) basis depending on the axis parameter. r1 : float Half-width of octagon across its basis axis-parallel sides in units of cm. Must be less than :math:`r_2\sqrt{2}`. @@ -290,7 +290,7 @@ class IsogonalOctagon(CompositeSurface): def __init__(self, center, r1, r2, axis='z', **kwargs): c1, c2 = center - # Coords for axis-perpendicular planes + # Coordinates for axis-perpendicular planes cright = c1 + r1 cleft = c1 - r1 @@ -307,7 +307,7 @@ class IsogonalOctagon(CompositeSurface): L_basis_ax = (r2 * sqrt(2) - r1) - # Coords for quadrant planes + # Coordinates for quadrant planes p1_ur = np.array([L_basis_ax, r1, 0.]) p2_ur = np.array([r1, L_basis_ax, 0.]) p3_ur = np.array([r1, L_basis_ax, 1.]) @@ -335,17 +335,18 @@ class IsogonalOctagon(CompositeSurface): self.right = openmc.XPlane(cright, **kwargs) self.left = openmc.XPlane(cleft, **kwargs) elif axis == 'y': - coord_map = [1, 2, 0] - self.top = openmc.XPlane(ctop, **kwargs) - self.bottom = openmc.XPlane(cbottom, **kwargs) - self.right = openmc.ZPlane(cright, **kwargs) - self.left = openmc.ZPlane(cleft, **kwargs) + coord_map = [0, 2, 1] + self.top = openmc.ZPlane(ctop, **kwargs) + self.bottom = openmc.ZPlane(cbottom, **kwargs) + self.right = openmc.XPlane(cright, **kwargs) + self.left = openmc.XPlane(cleft, **kwargs) elif axis == 'x': coord_map = [2, 0, 1] self.top = openmc.ZPlane(ctop, **kwargs) self.bottom = openmc.ZPlane(cbottom, **kwargs) self.right = openmc.YPlane(cright, **kwargs) self.left = openmc.YPlane(cleft, **kwargs) + self.axis = axis # Put our coordinates in (x,y,z) order and add the offset for p in points: @@ -363,14 +364,27 @@ class IsogonalOctagon(CompositeSurface): **kwargs) def __neg__(self): - return -self.top & +self.bottom & -self.right & +self.left & \ - +self.upper_right & +self.lower_right & -self.lower_left & \ - -self.upper_left + if self.axis == 'y': + region = -self.top & +self.bottom & -self.right & +self.left & \ + -self.upper_right & -self.lower_right & +self.lower_left & \ + +self.upper_left + else: + region = -self.top & +self.bottom & -self.right & +self.left & \ + +self.upper_right & +self.lower_right & -self.lower_left & \ + -self.upper_left + + return region def __pos__(self): - return +self.top | -self.bottom | +self.right | -self.left | \ - -self.upper_right | -self.lower_right | +self.lower_left | \ - +self.upper_left + if self.axis == 'y': + region = +self.top | -self.bottom | +self.right | -self.left | \ + +self.upper_right | +self.lower_right | -self.lower_left | \ + -self.upper_left + else: + region = +self.top | -self.bottom | +self.right | -self.left | \ + -self.upper_right | -self.lower_right | +self.lower_left | \ + +self.upper_left + return region class RightCircularCylinder(CompositeSurface): diff --git a/tests/unit_tests/test_surface_composite.py b/tests/unit_tests/test_surface_composite.py index 62ec18b32..01c827223 100644 --- a/tests/unit_tests/test_surface_composite.py +++ b/tests/unit_tests/test_surface_composite.py @@ -255,7 +255,7 @@ def test_cylinder_sector_from_theta_alpha(): @pytest.mark.parametrize( "axis, plane_tb, plane_lr, axis_idx", [ ("x", "Z", "Y", 0), - ("y", "X", "Z", 1), + ("y", "Z", "X", 1), ("z", "Y", "X", 2), ] ) From 84f561c1ed561a85cbfb036bf791dafc72744a5c Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Wed, 19 Jun 2024 15:48:34 +0100 Subject: [PATCH 128/671] sets used instead of lists when membership testing (#3021) --- openmc/arithmetic.py | 4 ++-- openmc/data/photon.py | 4 ++-- openmc/mgxs_library.py | 12 ++++-------- openmc/plots.py | 2 +- openmc/plotter.py | 8 ++++---- openmc/search.py | 2 +- openmc/settings.py | 2 +- openmc/stats/univariate.py | 4 ++-- openmc/surface.py | 4 ++-- 9 files changed, 19 insertions(+), 23 deletions(-) diff --git a/openmc/arithmetic.py b/openmc/arithmetic.py index 4520553da..821014e36 100644 --- a/openmc/arithmetic.py +++ b/openmc/arithmetic.py @@ -10,10 +10,10 @@ from .filter import _FILTER_TYPES # Acceptable tally arithmetic binary operations -_TALLY_ARITHMETIC_OPS = ['+', '-', '*', '/', '^'] +_TALLY_ARITHMETIC_OPS = {'+', '-', '*', '/', '^'} # Acceptable tally aggregation operations -_TALLY_AGGREGATE_OPS = ['sum', 'avg'] +_TALLY_AGGREGATE_OPS = {'sum', 'avg'} class CrossScore: diff --git a/openmc/data/photon.py b/openmc/data/photon.py index 0d0419f07..25ded24cb 100644 --- a/openmc/data/photon.py +++ b/openmc/data/photon.py @@ -28,10 +28,10 @@ CM_PER_ANGSTROM = 1.0e-8 R0 = CM_PER_ANGSTROM * PLANCK_C / (2.0 * pi * FINE_STRUCTURE * MASS_ELECTRON_EV) # Electron subshell labels -_SUBSHELLS = [None, 'K', 'L1', 'L2', 'L3', 'M1', 'M2', 'M3', 'M4', 'M5', +_SUBSHELLS = (None, 'K', 'L1', 'L2', 'L3', 'M1', 'M2', 'M3', 'M4', 'M5', 'N1', 'N2', 'N3', 'N4', 'N5', 'N6', 'N7', 'O1', 'O2', 'O3', 'O4', 'O5', 'O6', 'O7', 'O8', 'O9', 'P1', 'P2', 'P3', 'P4', - 'P5', 'P6', 'P7', 'P8', 'P9', 'P10', 'P11', 'Q1', 'Q2', 'Q3'] + 'P5', 'P6', 'P7', 'P8', 'P9', 'P10', 'P11', 'Q1', 'Q2', 'Q3') _REACTION_NAME = { 501: ('Total photon interaction', 'total'), diff --git a/openmc/mgxs_library.py b/openmc/mgxs_library.py index d1b962890..3381c3abb 100644 --- a/openmc/mgxs_library.py +++ b/openmc/mgxs_library.py @@ -18,21 +18,17 @@ ROOM_TEMPERATURE_KELVIN = 294.0 # Supported incoming particle MGXS angular treatment representations REPRESENTATION_ISOTROPIC = 'isotropic' REPRESENTATION_ANGLE = 'angle' -_REPRESENTATIONS = [ +_REPRESENTATIONS = { REPRESENTATION_ISOTROPIC, REPRESENTATION_ANGLE -] +} # Supported scattering angular distribution representations -_SCATTER_TYPES = [ +_SCATTER_TYPES = { SCATTER_TABULAR, SCATTER_LEGENDRE, SCATTER_HISTOGRAM -] - -# List of MGXS indexing schemes -_XS_SHAPES = ["[G][G'][Order]", "[G]", "[G']", "[G][G']", "[DG]", "[DG][G]", - "[DG][G']", "[DG][G][G']"] +} # Number of mu points for conversion between scattering formats _NMU = 257 diff --git a/openmc/plots.py b/openmc/plots.py index 02a44449b..0d9dca30e 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -14,7 +14,7 @@ from openmc.checkvalue import PathLike from ._xml import clean_indentation, get_elem_tuple, reorder_attributes, get_text from .mixin import IDManagerMixin -_BASES = ['xy', 'xz', 'yz'] +_BASES = {'xy', 'xz', 'yz'} _SVG_COLORS = { 'aliceblue': (240, 248, 255), diff --git a/openmc/plotter.py b/openmc/plotter.py index aa3825f5e..8a9a331d8 100644 --- a/openmc/plotter.py +++ b/openmc/plotter.py @@ -7,15 +7,15 @@ import openmc.checkvalue as cv import openmc.data # Supported keywords for continuous-energy cross section plotting -PLOT_TYPES = ['total', 'scatter', 'elastic', 'inelastic', 'fission', +PLOT_TYPES = {'total', 'scatter', 'elastic', 'inelastic', 'fission', 'absorption', 'capture', 'nu-fission', 'nu-scatter', 'unity', - 'slowing-down power', 'damage'] + 'slowing-down power', 'damage'} # Supported keywords for multi-group cross section plotting -PLOT_TYPES_MGXS = ['total', 'absorption', 'scatter', 'fission', +PLOT_TYPES_MGXS = {'total', 'absorption', 'scatter', 'fission', 'kappa-fission', 'nu-fission', 'prompt-nu-fission', 'deleyed-nu-fission', 'chi', 'chi-prompt', 'chi-delayed', - 'inverse-velocity', 'beta', 'decay-rate', 'unity'] + 'inverse-velocity', 'beta', 'decay-rate', 'unity'} # Create a dictionary which can be used to convert PLOT_TYPES_MGXS to the # openmc.XSdata attribute name needed to access the data _PLOT_MGXS_ATTR = {line: line.replace(' ', '_').replace('-', '_') diff --git a/openmc/search.py b/openmc/search.py index 7f6b3b5b9..70ce011b6 100644 --- a/openmc/search.py +++ b/openmc/search.py @@ -8,7 +8,7 @@ import openmc.model import openmc.checkvalue as cv -_SCALAR_BRACKETED_METHODS = ['brentq', 'brenth', 'ridder', 'bisect'] +_SCALAR_BRACKETED_METHODS = {'brentq', 'brenth', 'ridder', 'bisect'} def _search_keff(guess, target, model_builder, model_args, print_iterations, diff --git a/openmc/settings.py b/openmc/settings.py index 9565c2b78..43f763a19 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -26,7 +26,7 @@ class RunMode(Enum): PARTICLE_RESTART = 'particle restart' -_RES_SCAT_METHODS = ['dbrc', 'rvs'] +_RES_SCAT_METHODS = {'dbrc', 'rvs'} class Settings: diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index f44bce67c..4069c5a9e 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -16,13 +16,13 @@ import openmc.checkvalue as cv from .._xml import get_text from ..mixin import EqualityMixin -_INTERPOLATION_SCHEMES = [ +_INTERPOLATION_SCHEMES = { 'histogram', 'linear-linear', 'linear-log', 'log-linear', 'log-log' -] +} class Univariate(EqualityMixin, ABC): diff --git a/openmc/surface.py b/openmc/surface.py index 806331024..0fa2ae439 100644 --- a/openmc/surface.py +++ b/openmc/surface.py @@ -14,8 +14,8 @@ from .region import Region, Intersection, Union from .bounding_box import BoundingBox -_BOUNDARY_TYPES = ['transmission', 'vacuum', 'reflective', 'periodic', 'white'] -_ALBEDO_BOUNDARIES = ['reflective', 'periodic', 'white'] +_BOUNDARY_TYPES = {'transmission', 'vacuum', 'reflective', 'periodic', 'white'} +_ALBEDO_BOUNDARIES = {'reflective', 'periodic', 'white'} _WARNING_UPPER = """\ "{}(...) accepts an argument named '{}', not '{}'. Future versions of OpenMC \ From ddc9526966103a4dc28780f9cbc38dc942932445 Mon Sep 17 00:00:00 2001 From: Joffrey Dorville <54550047+JoffreyDorville@users.noreply.github.com> Date: Wed, 19 Jun 2024 10:11:58 -0500 Subject: [PATCH 129/671] Storing surface source points using a cell ID (#2888) Co-authored-by: Paul Romano --- docs/source/io_formats/settings.rst | 35 +- docs/source/usersguide/settings.rst | 49 +- include/openmc/particle.h | 4 +- include/openmc/particle_data.h | 8 +- include/openmc/settings.h | 12 + openmc/settings.py | 93 +- src/dagmc.cpp | 4 +- src/particle.cpp | 163 ++- src/settings.cpp | 32 +- src/surface.cpp | 3 +- .../filter_cellfrom/__init__.py | 0 .../filter_cellfrom/inputs_true.dat | 151 +++ .../filter_cellfrom/results_true.dat | 53 + .../regression_tests/filter_cellfrom/test.py | 315 +++++ tests/regression_tests/surface_source/test.py | 6 +- .../surface_source_write/__init__.py | 0 .../surface_source_write/_visualize.py | 66 + .../case-01/inputs_true.dat | 54 + .../case-01/results_true.dat | 2 + .../case-01/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-02/inputs_true.dat | 55 + .../case-02/results_true.dat | 2 + .../case-02/surface_source_true.h5 | Bin 0 -> 12752 bytes .../case-03/inputs_true.dat | 55 + .../case-03/results_true.dat | 2 + .../case-03/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-04/inputs_true.dat | 56 + .../case-04/results_true.dat | 2 + .../case-04/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-05/inputs_true.dat | 56 + .../case-05/results_true.dat | 2 + .../case-05/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-06/inputs_true.dat | 55 + .../case-06/results_true.dat | 2 + .../case-06/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-07/inputs_true.dat | 55 + .../case-07/results_true.dat | 2 + .../case-07/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-08/inputs_true.dat | 55 + .../case-08/results_true.dat | 2 + .../case-08/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-09/inputs_true.dat | 55 + .../case-09/results_true.dat | 2 + .../case-09/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-10/inputs_true.dat | 55 + .../case-10/results_true.dat | 2 + .../case-10/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-11/inputs_true.dat | 55 + .../case-11/results_true.dat | 2 + .../case-11/surface_source_true.h5 | Bin 0 -> 6408 bytes .../case-12/inputs_true.dat | 48 + .../case-12/results_true.dat | 2 + .../case-12/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-13/inputs_true.dat | 49 + .../case-13/results_true.dat | 2 + .../case-13/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-14/inputs_true.dat | 49 + .../case-14/results_true.dat | 2 + .../case-14/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-15/inputs_true.dat | 49 + .../case-15/results_true.dat | 2 + .../case-15/surface_source_true.h5 | Bin 0 -> 2144 bytes .../case-16/inputs_true.dat | 48 + .../case-16/results_true.dat | 2 + .../case-16/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-17/inputs_true.dat | 49 + .../case-17/results_true.dat | 2 + .../case-17/surface_source_true.h5 | Bin 0 -> 2144 bytes .../case-18/inputs_true.dat | 49 + .../case-18/results_true.dat | 2 + .../case-18/surface_source_true.h5 | Bin 0 -> 2144 bytes .../case-19/inputs_true.dat | 49 + .../case-19/results_true.dat | 2 + .../case-19/surface_source_true.h5 | Bin 0 -> 2144 bytes .../case-20/inputs_true.dat | 48 + .../case-20/results_true.dat | 2 + .../case-20/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-21/inputs_true.dat | 49 + .../case-21/results_true.dat | 2 + .../case-21/surface_source_true.h5 | Bin 0 -> 2144 bytes .../case-a01/inputs_true.dat | 56 + .../case-a01/results_true.dat | 2 + .../case-a01/surface_source_true.h5 | Bin 0 -> 6616 bytes .../case-d01/inputs_true.dat | 33 + .../case-d01/results_true.dat | 2 + .../case-d01/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-d02/inputs_true.dat | 34 + .../case-d02/results_true.dat | 2 + .../case-d02/surface_source_true.h5 | Bin 0 -> 31472 bytes .../case-d03/inputs_true.dat | 34 + .../case-d03/results_true.dat | 2 + .../case-d03/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-d04/inputs_true.dat | 35 + .../case-d04/results_true.dat | 2 + .../case-d04/surface_source_true.h5 | Bin 0 -> 31472 bytes .../case-d05/inputs_true.dat | 34 + .../case-d05/results_true.dat | 2 + .../case-d05/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-d06/inputs_true.dat | 34 + .../case-d06/results_true.dat | 2 + .../case-d06/surface_source_true.h5 | Bin 0 -> 29496 bytes .../case-d07/inputs_true.dat | 49 + .../case-d07/results_true.dat | 2 + .../case-d07/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-d08/inputs_true.dat | 49 + .../case-d08/results_true.dat | 2 + .../case-d08/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-e01/inputs_true.dat | 56 + .../case-e01/results_true.dat | 2 + .../case-e01/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-e02/inputs_true.dat | 55 + .../case-e02/results_true.dat | 2 + .../case-e02/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-e03/inputs_true.dat | 49 + .../case-e03/results_true.dat | 2 + .../case-e03/surface_source_true.h5 | Bin 0 -> 33344 bytes .../surface_source_write/test.py | 1126 +++++++++++++++++ tests/unit_tests/test_surface_source_write.py | 248 ++++ .../unit_tests/test_tally_multiply_density.py | 2 +- tests/unit_tests/weightwindows/test_ww_gen.py | 2 +- 120 files changed, 3963 insertions(+), 86 deletions(-) create mode 100644 tests/regression_tests/filter_cellfrom/__init__.py create mode 100644 tests/regression_tests/filter_cellfrom/inputs_true.dat create mode 100644 tests/regression_tests/filter_cellfrom/results_true.dat create mode 100644 tests/regression_tests/filter_cellfrom/test.py create mode 100644 tests/regression_tests/surface_source_write/__init__.py create mode 100644 tests/regression_tests/surface_source_write/_visualize.py create mode 100644 tests/regression_tests/surface_source_write/case-01/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-01/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-01/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-02/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-02/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-02/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-03/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-03/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-03/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-04/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-04/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-04/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-05/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-05/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-05/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-06/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-06/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-06/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-07/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-07/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-07/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-08/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-08/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-08/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-09/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-09/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-09/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-10/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-10/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-10/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-11/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-11/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-11/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-12/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-12/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-12/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-13/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-13/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-13/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-14/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-14/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-14/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-15/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-15/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-15/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-16/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-16/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-16/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-17/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-17/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-17/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-18/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-18/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-18/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-19/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-19/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-19/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-20/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-20/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-20/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-21/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-21/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-21/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-a01/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-a01/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-a01/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-d01/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d01/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d01/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-d02/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d02/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d02/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-d03/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d03/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d03/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-d04/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d04/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d04/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-d05/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d05/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d05/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-d06/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d06/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d06/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-d07/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d07/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d07/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-d08/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d08/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d08/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-e01/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-e01/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-e01/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-e02/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-e02/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-e02/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-e03/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-e03/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-e03/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/test.py create mode 100644 tests/unit_tests/test_surface_source_write.py diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 6e9a7f2ab..915f698b6 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -902,7 +902,12 @@ attributes/sub-elements: The ```` element triggers OpenMC to bank particles crossing certain surfaces and write out the source bank in a separate file called -``surface_source.h5``. This element has the following attributes/sub-elements: +``surface_source.h5``. One or multiple surface IDs and one cell ID can be used +to select the surfaces of interest. If no surface IDs are declared, every surface +of the model is eligible to bank particles. In that case, a cell ID (using +either the ``cell``, ``cellfrom`` or ``cellto`` attributes) can be used to select +every surface of a specific cell. This element has the following +attributes/sub-elements: :surface_ids: A list of integers separated by spaces indicating the unique IDs of surfaces @@ -928,6 +933,34 @@ certain surfaces and write out the source bank in a separate file called .. _MCPL: https://mctools.github.io/mcpl/mcpl.pdf + :cell: + An integer representing the cell ID used to determine if particles crossing + identified surfaces are to be banked. Particles coming from or going to this + declared cell will be banked if they cross the identified surfaces. + + *Default*: None + + :cellfrom: + An integer representing the cell ID used to determine if particles crossing + identified surfaces are to be banked. Particles coming from this declared + cell will be banked if they cross the identified surfaces. + + *Default*: None + + :cellto: + An integer representing the cell ID used to determine if particles crossing + identified surfaces are to be banked. Particles going to this declared cell + will be banked if they cross the identified surfaces. + + *Default*: None + +.. note:: The ``cell``, ``cellfrom`` and ``cellto`` attributes cannot be + used simultaneously. + +.. note:: Surfaces with boundary conditions that are not "transmission" or "vacuum" + are not eligible to store any particles when using ``cell``, ``cellfrom`` + or ``cellto`` attributes. It is recommended to use surface IDs instead. + ------------------------------ ```` Element ------------------------------ diff --git a/docs/source/usersguide/settings.rst b/docs/source/usersguide/settings.rst index eb02f654c..d73d90cbb 100644 --- a/docs/source/usersguide/settings.rst +++ b/docs/source/usersguide/settings.rst @@ -277,6 +277,9 @@ source file can be manually generated with the :func:`openmc.write_source_file` function. This is particularly useful for coupling OpenMC with another program that generates a source to be used in OpenMC. +Surface Sources ++++++++++++++++ + A source file based on particles that cross one or more surfaces can be generated during a simulation using the :attr:`Settings.surf_source_write` attribute:: @@ -287,7 +290,51 @@ attribute:: } In this example, at most 10,000 source particles are stored when particles cross -surfaces with IDs of 1, 2, or 3. +surfaces with IDs of 1, 2, or 3. If no surface IDs are declared, particles +crossing any surface of the model will be banked:: + + settings.surf_source_write = {'max_particles': 10000} + +A cell ID can also be used to bank particles that are crossing any surface of +a cell that particles are either coming from or going to:: + + settings.surf_source_write = {'cell': 1, 'max_particles': 10000} + +In this example, particles that are crossing any surface that bounds cell 1 will +be banked excluding any surface that does not use a 'transmission' or 'vacuum' +boundary condition. + +.. note:: Surfaces with boundary conditions that are not "transmission" or "vacuum" + are not eligible to store any particles when using ``cell``, ``cellfrom`` + or ``cellto`` attributes. It is recommended to use surface IDs instead. + +Surface IDs can be used in combination with a cell ID:: + + settings.surf_source_write = { + 'cell': 1, + 'surfaces_ids': [1, 2, 3], + 'max_particles': 10000 + } + +In that case, only particles that are crossing the declared surfaces coming from +cell 1 or going to cell 1 will be banked. To account specifically for particles +leaving or entering a given cell, ``cellfrom`` and ``cellto`` are also available +to respectively account for particles coming from a cell:: + + settings.surf_source_write = { + 'cellfrom': 1, + 'max_particles': 10000 + } + +or particles going to a cell:: + + settings.surf_source_write = { + 'cellto': 1, + 'max_particles': 10000 + } + +.. note:: The ``cell``, ``cellfrom`` and ``cellto`` attributes cannot be + used simultaneously. .. _compiled_source: diff --git a/include/openmc/particle.h b/include/openmc/particle.h index e423880f8..6a2e67049 100644 --- a/include/openmc/particle.h +++ b/include/openmc/particle.h @@ -74,7 +74,7 @@ public: void pht_secondary_particles(); //! Cross a surface and handle boundary conditions - void cross_surface(); + void cross_surface(const Surface& surf); //! Cross a vacuum boundary condition. // @@ -127,6 +127,8 @@ std::string particle_type_to_str(ParticleType type); ParticleType str_to_particle_type(std::string str); +void add_surf_source_to_bank(Particle& p, const Surface& surf); + } // namespace openmc #endif // OPENMC_PARTICLE_H diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index 5c765e2e6..164148cce 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -211,9 +211,15 @@ public: // resets all coordinate levels for the particle void clear() { - for (auto& level : coord_) + for (auto& level : coord_) { level.reset(); + } n_coord_ = 1; + + for (auto& cell : cell_last_) { + cell = C_NONE; + } + n_coord_last_ = 1; } // Initialize all internal state from position and direction diff --git a/include/openmc/settings.h b/include/openmc/settings.h index df4a54a6f..a95f1ced9 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -16,6 +16,14 @@ namespace openmc { +// Type of surface source write +enum class SSWCellType { + None, + Both, + From, + To, +}; + //============================================================================== // Global variable declarations //============================================================================== @@ -125,6 +133,10 @@ extern int max_history_splits; //!< maximum number of particle splits for weight windows extern int64_t max_surface_particles; //!< maximum number of particles to be //!< banked on surfaces per process +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 TemperatureMethod temperature_method; //!< method for choosing temperatures extern double diff --git a/openmc/settings.py b/openmc/settings.py index 43f763a19..80d766ab1 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -201,6 +201,15 @@ class Settings: :max_particles: Maximum number of particles to be banked on surfaces per process (int) :mcpl: Output in the form of an MCPL-file (bool) + :cell: Cell ID used to determine if particles crossing identified + surfaces are to be banked. Particles coming from or going to this + declared cell will be banked (int) + :cellfrom: Cell ID used to determine if particles crossing identified + surfaces are to be banked. Particles coming from this + declared cell will be banked (int) + :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) survival_biasing : bool Indicate whether survival biasing is to be used tabular_legendre : dict @@ -693,23 +702,30 @@ class Settings: @surf_source_write.setter def surf_source_write(self, surf_source_write: dict): - cv.check_type('surface source writing options', surf_source_write, Mapping) + cv.check_type("surface source writing options", surf_source_write, Mapping) for key, value in surf_source_write.items(): - cv.check_value('surface source writing key', key, - ('surface_ids', 'max_particles', 'mcpl')) - if key == 'surface_ids': - cv.check_type('surface ids for source banking', value, - Iterable, Integral) + cv.check_value( + "surface source writing key", + key, + ("surface_ids", "max_particles", "mcpl", "cell", "cellfrom", "cellto"), + ) + if key == "surface_ids": + cv.check_type( + "surface ids for source banking", value, Iterable, Integral + ) for surf_id in value: - cv.check_greater_than('surface id for source banking', - surf_id, 0) - elif key == 'max_particles': - cv.check_type('maximum particle banks on surfaces per process', - value, Integral) - cv.check_greater_than('maximum particle banks on surfaces per process', - value, 0) - elif key == 'mcpl': - cv.check_type('write to an MCPL-format file', value, bool) + cv.check_greater_than("surface id for source banking", surf_id, 0) + elif key == "mcpl": + cv.check_type("write to an MCPL-format file", value, bool) + elif key in ("max_particles", "cell", "cellfrom", "cellto"): + name = { + "max_particles": "maximum particle banks on surfaces per process", + "cell": "Cell ID for source banking (from or to)", + "cellfrom": "Cell ID for source banking (from only)", + "cellto": "Cell ID for source banking (to only)", + }[key] + cv.check_type(name, value, Integral) + cv.check_greater_than(name, value, 0) self._surf_source_write = surf_source_write @@ -1207,16 +1223,18 @@ class Settings: def _create_surf_source_write_subelement(self, root): if self._surf_source_write: element = ET.SubElement(root, "surf_source_write") - if 'surface_ids' in self._surf_source_write: + if "surface_ids" in self._surf_source_write: subelement = ET.SubElement(element, "surface_ids") - subelement.text = ' '.join( - str(x) for x in self._surf_source_write['surface_ids']) - if 'max_particles' in self._surf_source_write: - subelement = ET.SubElement(element, "max_particles") - subelement.text = str(self._surf_source_write['max_particles']) - if 'mcpl' in self._surf_source_write: + subelement.text = " ".join( + str(x) for x in self._surf_source_write["surface_ids"] + ) + if "mcpl" in self._surf_source_write: subelement = ET.SubElement(element, "mcpl") - subelement.text = str(self._surf_source_write['mcpl']).lower() + subelement.text = str(self._surf_source_write["mcpl"]).lower() + for key in ("max_particles", "cell", "cellfrom", "cellto"): + if key in self._surf_source_write: + subelement = ET.SubElement(element, key) + subelement.text = str(self._surf_source_write[key]) def _create_confidence_intervals(self, root): if self._confidence_intervals is not None: @@ -1381,9 +1399,9 @@ class Settings: elem.text = str(self._create_fission_neutrons).lower() def _create_create_delayed_neutrons_subelement(self, root): - if self._create_delayed_neutrons is not None: - elem = ET.SubElement(root, "create_delayed_neutrons") - elem.text = str(self._create_delayed_neutrons).lower() + if self._create_delayed_neutrons is not None: + elem = ET.SubElement(root, "create_delayed_neutrons") + elem.text = str(self._create_delayed_neutrons).lower() def _create_delayed_photon_scaling_subelement(self, root): if self._delayed_photon_scaling is not None: @@ -1610,17 +1628,18 @@ class Settings: def _surf_source_write_from_xml_element(self, root): elem = root.find('surf_source_write') - if elem is not None: - for key in ('surface_ids', 'max_particles','mcpl'): - value = get_text(elem, key) - if value is not None: - if key == 'surface_ids': - value = [int(x) for x in value.split()] - elif key in ('max_particles'): - value = int(value) - elif key == 'mcpl': - value = value in ('true', '1') - self.surf_source_write[key] = value + if elem is None: + return + for key in ('surface_ids', 'max_particles', 'mcpl', 'cell', 'cellto', 'cellfrom'): + value = get_text(elem, key) + if value is not None: + if key == 'surface_ids': + value = [int(x) for x in value.split()] + elif key == 'mcpl': + value = value in ('true', '1') + elif key in ('max_particles', 'cell', 'cellfrom', 'cellto'): + value = int(value) + self.surf_source_write[key] = value def _confidence_intervals_from_xml_element(self, root): text = get_text(root, 'confidence_intervals') diff --git a/src/dagmc.cpp b/src/dagmc.cpp index 2f2502f6e..a29a2589f 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -271,8 +271,10 @@ void DAGUniverse::init_geometry() : dagmc_instance_->id_by_index(2, i + 1); // set surface source attribute if needed - if (contains(settings::source_write_surf_id, s->id_)) + if (contains(settings::source_write_surf_id, s->id_) || + settings::source_write_surf_id.empty()) { s->surf_source_ = true; + } // set BCs std::string bc_value = diff --git a/src/particle.cpp b/src/particle.cpp index a91113c61..0c6a33b78 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -168,6 +168,12 @@ void Particle::event_calculate_xs() // Set birth cell attribute if (cell_born() == C_NONE) cell_born() = lowest_coord().cell; + + // Initialize last cells from current cell + for (int j = 0; j < n_coord(); ++j) { + cell_last(j) = coord(j).cell; + } + n_coord_last() = n_coord(); } // Write particle track. @@ -264,16 +270,16 @@ void Particle::event_advance() void Particle::event_cross_surface() { - // Set surface that particle is on and adjust coordinate levels - surface() = boundary().surface_index; - n_coord() = boundary().coord_level; - // Saving previous cell data for (int j = 0; j < n_coord(); ++j) { cell_last(j) = coord(j).cell; } n_coord_last() = n_coord(); + // Set surface that particle is on and adjust coordinate levels + surface() = boundary().surface_index; + n_coord() = boundary().coord_level; + if (boundary().lattice_translation[0] != 0 || boundary().lattice_translation[1] != 0 || boundary().lattice_translation[2] != 0) { @@ -284,7 +290,17 @@ void Particle::event_cross_surface() event() = TallyEvent::LATTICE; } else { // Particle crosses surface - cross_surface(); + // TODO: off-by-one + const auto& surf {model::surfaces[std::abs(surface()) - 1].get()}; + // If BC, add particle to surface source before crossing surface + if (surf->surf_source_ && surf->bc_) { + add_surf_source_to_bank(*this, *surf); + } + 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 (settings::weight_window_checkpoint_surface) { apply_weight_windows(*this); } @@ -418,6 +434,12 @@ void Particle::event_revive_from_secondary() // Set birth cell attribute if (cell_born() == C_NONE) cell_born() = lowest_coord().cell; + + // Initialize last cells from current cell + for (int j = 0; j < n_coord(); ++j) { + cell_last(j) = coord(j).cell; + } + n_coord_last() = n_coord(); } pht_secondary_particles(); } @@ -502,40 +524,22 @@ void Particle::pht_secondary_particles() } } -void Particle::cross_surface() +void Particle::cross_surface(const Surface& surf) { - int i_surface = std::abs(surface()); - // TODO: off-by-one - const auto& surf {model::surfaces[i_surface - 1].get()}; - if (settings::verbosity >= 10 || trace()) { - write_message(1, " Crossing surface {}", surf->id_); - } - if (surf->surf_source_ && simulation::current_batch > settings::n_inactive && - !simulation::surf_source_bank.full()) { - SourceSite site; - site.r = r(); - site.u = u(); - site.E = E(); - site.time = time(); - site.wgt = wgt(); - site.delayed_group = delayed_group(); - site.surf_id = surf->id_; - site.particle = type(); - site.parent_id = id(); - site.progeny_id = n_progeny(); - int64_t idx = simulation::surf_source_bank.thread_safe_append(site); + if (settings::verbosity >= 10 || trace()) { + write_message(1, " Crossing surface {}", surf.id_); } // if we're crossing a CSG surface, make sure the DAG history is reset #ifdef DAGMC - if (surf->geom_type_ == GeometryType::CSG) + if (surf.geom_type_ == GeometryType::CSG) history().reset(); #endif // Handle any applicable boundary conditions. - if (surf->bc_ && settings::run_mode != RunMode::PLOTTING) { - surf->bc_->handle_particle(*this, *surf); + if (surf.bc_ && settings::run_mode != RunMode::PLOTTING) { + surf.bc_->handle_particle(*this, surf); return; } @@ -544,10 +548,10 @@ void Particle::cross_surface() #ifdef DAGMC // in DAGMC, we know what the next cell should be - if (surf->geom_type_ == GeometryType::DAG) { - int32_t i_cell = - next_cell(i_surface, cell_last(n_coord() - 1), lowest_coord().universe) - - 1; + if (surf.geom_type_ == GeometryType::DAG) { + int32_t i_cell = next_cell(std::abs(surface()), cell_last(n_coord() - 1), + lowest_coord().universe) - + 1; // save material and temp material_last() = material(); sqrtkT_last() = sqrtkT(); @@ -561,8 +565,9 @@ void Particle::cross_surface() #endif bool verbose = settings::verbosity >= 10 || trace(); - if (neighbor_list_find_cell(*this, verbose)) + if (neighbor_list_find_cell(*this, verbose)) { return; + } // ========================================================================== // COULDN'T FIND PARTICLE IN NEIGHBORING CELLS, SEARCH ALL CELLS @@ -586,7 +591,7 @@ void Particle::cross_surface() if (!exhaustive_find_cell(*this, verbose)) { mark_as_lost("After particle " + std::to_string(id()) + - " crossed surface " + std::to_string(surf->id_) + + " crossed surface " + std::to_string(surf.id_) + " it could not be located in any cell and it did not leak."); return; } @@ -650,7 +655,7 @@ void Particle::cross_reflective_bc(const Surface& surf, Direction new_u) u() = new_u; // Reassign particle's cell and surface - coord(0).cell = cell_last(n_coord_last() - 1); + coord(0).cell = cell_last(0); surface() = -surface(); // If a reflective surface is coincident with a lattice or universe @@ -873,4 +878,90 @@ ParticleType str_to_particle_type(std::string str) } } +void add_surf_source_to_bank(Particle& p, const Surface& surf) +{ + if (simulation::current_batch <= settings::n_inactive || + simulation::surf_source_bank.full()) { + return; + } + + // If a cell/cellfrom/cellto parameter is defined + if (settings::ssw_cell_id != C_NONE) { + + // Retrieve cell index and storage type + int cell_idx = model::cell_map[settings::ssw_cell_id]; + + if (surf.bc_) { + // Leave if cellto with vacuum boundary condition + if (surf.bc_->type() == "vacuum" && + settings::ssw_cell_type == SSWCellType::To) { + return; + } + + // Leave if other boundary condition than vacuum + if (surf.bc_->type() != "vacuum") { + return; + } + } + + // Check if the cell of interest has been exited + bool exited = false; + for (int i = 0; i < p.n_coord_last(); ++i) { + if (p.cell_last(i) == cell_idx) { + exited = true; + } + } + + // Check if the cell of interest has been entered + bool entered = false; + for (int i = 0; i < p.n_coord(); ++i) { + if (p.coord(i).cell == cell_idx) { + entered = true; + } + } + + // Vacuum boundary conditions: return if cell is not exited + if (surf.bc_) { + if (surf.bc_->type() == "vacuum" && !exited) { + return; + } + } else { + + // If we both enter and exit the cell of interest + if (entered && exited) { + return; + } + + // If we did not enter nor exit the cell of interest + if (!entered && !exited) { + return; + } + + // If cellfrom and the cell before crossing is not the cell of + // interest + if (settings::ssw_cell_type == SSWCellType::From && !exited) { + return; + } + + // If cellto and the cell after crossing is not the cell of interest + if (settings::ssw_cell_type == SSWCellType::To && !entered) { + return; + } + } + } + + SourceSite site; + site.r = p.r(); + site.u = p.u(); + site.E = p.E(); + site.time = p.time(); + site.wgt = p.wgt(); + site.delayed_group = p.delayed_group(); + site.surf_id = surf.id_; + site.particle = p.type(); + site.parent_id = p.id(); + site.progeny_id = p.n_progeny(); + int64_t idx = simulation::surf_source_bank.thread_safe_append(site); +} + } // namespace openmc diff --git a/src/settings.cpp b/src/settings.cpp index f5cd1b7e9..6c3226b04 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -119,6 +119,8 @@ std::unordered_set sourcepoint_batch; std::unordered_set statepoint_batch; std::unordered_set source_write_surf_id; int64_t max_surface_particles; +int64_t ssw_cell_id {C_NONE}; +SSWCellType ssw_cell_type {SSWCellType::None}; TemperatureMethod temperature_method {TemperatureMethod::NEAREST}; double temperature_tolerance {10.0}; double temperature_default {293.6}; @@ -747,7 +749,9 @@ void read_settings_xml(pugi::xml_node root) // Get surface source write node xml_node node_ssw = root.child("surf_source_write"); - // Determine surface ids at which crossing particles are to be banked + // Determine surface ids at which crossing particles are to be banked. + // If no surfaces are specified, all surfaces in the model will be used + // to bank source points. if (check_for_node(node_ssw, "surface_ids")) { auto temp = get_node_array(node_ssw, "surface_ids"); for (const auto& b : temp) { @@ -759,7 +763,12 @@ void read_settings_xml(pugi::xml_node root) if (check_for_node(node_ssw, "max_particles")) { max_surface_particles = std::stoll(get_node_value(node_ssw, "max_particles")); + } else { + fatal_error("A maximum number of particles needs to be specified " + "using the 'max_particles' parameter to store surface " + "source points."); } + if (check_for_node(node_ssw, "mcpl")) { surf_mcpl_write = get_node_value_bool(node_ssw, "mcpl"); @@ -769,6 +778,27 @@ void read_settings_xml(pugi::xml_node root) "surface source files."); } } + // Get cell information + if (check_for_node(node_ssw, "cell")) { + ssw_cell_id = std::stoll(get_node_value(node_ssw, "cell")); + ssw_cell_type = SSWCellType::Both; + } + if (check_for_node(node_ssw, "cellfrom")) { + if (ssw_cell_id != C_NONE) { + fatal_error( + "'cell', 'cellfrom' and 'cellto' cannot be used at the same time."); + } + ssw_cell_id = std::stoll(get_node_value(node_ssw, "cellfrom")); + ssw_cell_type = SSWCellType::From; + } + if (check_for_node(node_ssw, "cellto")) { + if (ssw_cell_id != C_NONE) { + fatal_error( + "'cell', 'cellfrom' and 'cellto' cannot be used at the same time."); + } + ssw_cell_id = std::stoll(get_node_value(node_ssw, "cellto")); + ssw_cell_type = SSWCellType::To; + } } // If source is not separate and is to be written out in the statepoint file, diff --git a/src/surface.cpp b/src/surface.cpp index 12fef070e..50ef2a128 100644 --- a/src/surface.cpp +++ b/src/surface.cpp @@ -63,7 +63,8 @@ Surface::Surface(pugi::xml_node surf_node) { if (check_for_node(surf_node, "id")) { id_ = std::stoi(get_node_value(surf_node, "id")); - if (contains(settings::source_write_surf_id, id_)) { + if (contains(settings::source_write_surf_id, id_) || + settings::source_write_surf_id.empty()) { surf_source_ = true; } } else { diff --git a/tests/regression_tests/filter_cellfrom/__init__.py b/tests/regression_tests/filter_cellfrom/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/filter_cellfrom/inputs_true.dat b/tests/regression_tests/filter_cellfrom/inputs_true.dat new file mode 100644 index 000000000..8a37e8286 --- /dev/null +++ b/tests/regression_tests/filter_cellfrom/inputs_true.dat @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 2000 + 15 + 5 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + 1 + + + + 1 + + + 1 + + + 2 + + + 3 + + + 4 + + + 2 + + + 3 + + + 4 + + + 5 1 + total + + + 5 2 + total + + + 5 3 + total + + + 5 4 + total + + + 6 1 + total + + + 6 2 + total + + + 6 3 + total + + + 6 4 + total + + + 7 1 + total + + + 7 2 + total + + + 7 3 + total + + + 7 4 + total + + + 8 1 + total + + + 8 2 + total + + + 8 3 + total + + + 8 4 + total + + + total + + + diff --git a/tests/regression_tests/filter_cellfrom/results_true.dat b/tests/regression_tests/filter_cellfrom/results_true.dat new file mode 100644 index 000000000..5dd43e1f3 --- /dev/null +++ b/tests/regression_tests/filter_cellfrom/results_true.dat @@ -0,0 +1,53 @@ +k-combined: +9.640806E-02 2.655206E-03 +tally 1: +6.025999E+00 +3.635317E+00 +tally 2: +4.523606E-04 +2.051522E-08 +tally 3: +6.026452E+00 +3.635862E+00 +tally 4: +0.000000E+00 +0.000000E+00 +tally 5: +1.530903E+00 +2.357048E-01 +tally 6: +4.992646E-05 +2.600391E-10 +tally 7: +1.530953E+00 +2.357201E-01 +tally 8: +1.889115E+01 +3.574727E+01 +tally 9: +7.556902E+00 +5.717680E+00 +tally 10: +5.022871E-04 +2.531352E-08 +tally 11: +7.557405E+00 +5.718440E+00 +tally 12: +1.889115E+01 +3.574727E+01 +tally 13: +0.000000E+00 +0.000000E+00 +tally 14: +2.663407E-04 +7.161725E-09 +tally 15: +2.663407E-04 +7.161725E-09 +tally 16: +8.025710E+01 +6.459305E+02 +tally 17: +1.067059E+02 +1.140939E+03 diff --git a/tests/regression_tests/filter_cellfrom/test.py b/tests/regression_tests/filter_cellfrom/test.py new file mode 100644 index 000000000..8fb7509cb --- /dev/null +++ b/tests/regression_tests/filter_cellfrom/test.py @@ -0,0 +1,315 @@ +"""This test ensures that the CellFromFilter works correctly even if the level of +coordinates (number of encapsulated universes) is different in the cell from +where the particle originates compared to the cell where the particle is going. + +A matrix of reaction rates based on where the particle is coming from and +where it goes to is calculated and compared to the total reaction rate of the problem. +The components of this matrix are also compared to other components using symmetric +properties. + +TODO: + +- Test with a lattice, +- Test with mesh, +- Test with reflective boundary conditions, +- Test with periodic boundary conditions. + +""" + +from numpy.testing import assert_allclose, assert_equal +import numpy as np +import openmc +import pytest + +from tests.testing_harness import PyAPITestHarness + + +RTOL = 1.0e-7 +ATOL = 0.0 + + +@pytest.fixture +def model(): + """Cylindrical core contained in a first box which is contained in a larger box. + A lower universe is used to describe the interior of the first box which + contains the core and its surrounding space.""" + openmc.reset_auto_ids() + model = openmc.Model() + + # ============================================================================= + # Materials + # ============================================================================= + + fuel = openmc.Material() + fuel.add_nuclide("U234", 0.0004524) + fuel.add_nuclide("U235", 0.0506068) + fuel.add_nuclide("U238", 0.9487090) + fuel.add_nuclide("U236", 0.0002318) + fuel.add_nuclide("O16", 2.0) + fuel.set_density("g/cm3", 10.97) + + water = openmc.Material() + water.add_nuclide("H1", 2.0) + water.add_nuclide("O16", 1.0) + water.set_density("g/cm3", 1.0) + + air = openmc.Material() + air.add_element("O", 0.2) + air.add_element("N", 0.8) + air.set_density("g/cm3", 0.001225) + + # ============================================================================= + # Geometry + # ============================================================================= + + # ----------------------------------------------------------------------------- + # Cylindrical core + # ----------------------------------------------------------------------------- + + # Parameters + core_radius = 2.0 + core_height = 4.0 + + # Surfaces + core_cylinder = openmc.ZCylinder(r=core_radius) + core_lower_plane = openmc.ZPlane(z0=-core_height / 2.0) + core_upper_plane = openmc.ZPlane(z0=core_height / 2.0) + + # Region + core_region = -core_cylinder & +core_lower_plane & -core_upper_plane + + # Cells + core = openmc.Cell(fill=fuel, region=core_region) + outside_core_region = +core_cylinder | -core_lower_plane | +core_upper_plane + outside_core = openmc.Cell(fill=air, region=outside_core_region) + + # Universe + inside_box1_universe = openmc.Universe(cells=[core, outside_core]) + + # ----------------------------------------------------------------------------- + # Box 1 + # ----------------------------------------------------------------------------- + + # Parameters + box1_size = 4.1 + + # Surfaces + box1_rpp = openmc.model.RectangularParallelepiped( + -box1_size / 2.0, box1_size / 2.0, + -box1_size / 2.0, box1_size / 2.0, + -box1_size / 2.0, box1_size / 2.0, + ) + + # Cell + box1 = openmc.Cell(fill=inside_box1_universe, region=-box1_rpp) + + # ----------------------------------------------------------------------------- + # Box 2 + # ----------------------------------------------------------------------------- + + # Parameters + box2_size = 12 + + # Surfaces + box2_rpp = openmc.model.RectangularParallelepiped( + -box2_size / 2.0, box2_size / 2.0, + -box2_size / 2.0, box2_size / 2.0, + -box2_size / 2.0, box2_size / 2.0, + boundary_type="vacuum" + ) + + # Cell + box2 = openmc.Cell(fill=water, region=-box2_rpp & +box1_rpp) + + # Register geometry + model.geometry = openmc.Geometry([box1, box2]) + + # ============================================================================= + # Settings + # ============================================================================= + + model.settings = openmc.Settings() + model.settings.particles = 2000 + model.settings.batches = 15 + model.settings.inactive = 5 + model.settings.seed = 1 + + bounds = [ + -core_radius, + -core_radius, + -core_height / 2.0, + core_radius, + core_radius, + core_height / 2.0, + ] + distribution = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) + model.settings.source = openmc.IndependentSource(space=distribution) + + # ============================================================================= + # Tallies + # ============================================================================= + + in_core_filter = openmc.CellFilter([core]) + in_outside_core_filter = openmc.CellFilter([outside_core]) + in_box1_filter = openmc.CellFilter([box1]) + in_box2_filter = openmc.CellFilter([box2]) + + from_core_filter = openmc.CellFromFilter([core]) + from_outside_core_filter = openmc.CellFromFilter([outside_core]) + from_box1_filter = openmc.CellFromFilter([box1]) + from_box2_filter = openmc.CellFromFilter([box2]) + + t1_1 = openmc.Tally(name="total from 1 in 1") + t1_1.filters = [from_core_filter, in_core_filter] + t1_1.scores = ["total"] + + t1_2 = openmc.Tally(name="total from 1 in 2") + t1_2.filters = [from_core_filter, in_outside_core_filter] + t1_2.scores = ["total"] + + t1_3 = openmc.Tally(name="total from 1 in 3") + t1_3.filters = [from_core_filter, in_box1_filter] + t1_3.scores = ["total"] + + t1_4 = openmc.Tally(name="total from 1 in 4") + t1_4.filters = [from_core_filter, in_box2_filter] + t1_4.scores = ["total"] + + t2_1 = openmc.Tally(name="total from 2 in 1") + t2_1.filters = [from_outside_core_filter, in_core_filter] + t2_1.scores = ["total"] + + t2_2 = openmc.Tally(name="total from 2 in 2") + t2_2.filters = [from_outside_core_filter, in_outside_core_filter] + t2_2.scores = ["total"] + + t2_3 = openmc.Tally(name="total from 2 in 3") + t2_3.filters = [from_outside_core_filter, in_box1_filter] + t2_3.scores = ["total"] + + t2_4 = openmc.Tally(name="total from 2 in 4") + t2_4.filters = [from_outside_core_filter, in_box2_filter] + t2_4.scores = ["total"] + + t3_1 = openmc.Tally(name="total from 3 in 1") + t3_1.filters = [from_box1_filter, in_core_filter] + t3_1.scores = ["total"] + + t3_2 = openmc.Tally(name="total from 3 in 2") + t3_2.filters = [from_box1_filter, in_outside_core_filter] + t3_2.scores = ["total"] + + t3_3 = openmc.Tally(name="total from 3 in 3") + t3_3.filters = [from_box1_filter, in_box1_filter] + t3_3.scores = ["total"] + + t3_4 = openmc.Tally(name="total from 3 in 4") + t3_4.filters = [from_box1_filter, in_box2_filter] + t3_4.scores = ["total"] + + t4_1 = openmc.Tally(name="total from 4 in 1") + t4_1.filters = [from_box2_filter, in_core_filter] + t4_1.scores = ["total"] + + t4_2 = openmc.Tally(name="total from 4 in 2") + t4_2.filters = [from_box2_filter, in_outside_core_filter] + t4_2.scores = ["total"] + + t4_3 = openmc.Tally(name="total from 4 in 3") + t4_3.filters = [from_box2_filter, in_box1_filter] + t4_3.scores = ["total"] + + t4_4 = openmc.Tally(name="total from 4 in 4") + t4_4.filters = [from_box2_filter, in_box2_filter] + t4_4.scores = ["total"] + + tglobal = openmc.Tally(name="total") + tglobal.scores = ["total"] + + model.tallies += [ + t1_1, + t1_2, + t1_3, + t1_4, + t2_1, + t2_2, + t2_3, + t2_4, + t3_1, + t3_2, + t3_3, + t3_4, + t4_1, + t4_2, + t4_3, + t4_4, + tglobal, + ] + return model + + +class CellFromFilterTest(PyAPITestHarness): + + def _compare_results(self): + """Additional unit tests on the tally results to check + consistency of CellFromFilter.""" + with openmc.StatePoint(self.statepoint_name) as sp: + + t1_1 = sp.get_tally(name="total from 1 in 1").mean + t1_2 = sp.get_tally(name="total from 1 in 2").mean + t1_3 = sp.get_tally(name="total from 1 in 3").mean + t1_4 = sp.get_tally(name="total from 1 in 4").mean + + t2_1 = sp.get_tally(name="total from 2 in 1").mean + t2_2 = sp.get_tally(name="total from 2 in 2").mean + t2_3 = sp.get_tally(name="total from 2 in 3").mean + t2_4 = sp.get_tally(name="total from 2 in 4").mean + + t3_1 = sp.get_tally(name="total from 3 in 1").mean + t3_2 = sp.get_tally(name="total from 3 in 2").mean + t3_3 = sp.get_tally(name="total from 3 in 3").mean + t3_4 = sp.get_tally(name="total from 3 in 4").mean + + t4_1 = sp.get_tally(name="total from 4 in 1").mean + t4_2 = sp.get_tally(name="total from 4 in 2").mean + t4_3 = sp.get_tally(name="total from 4 in 3").mean + t4_4 = sp.get_tally(name="total from 4 in 4").mean + + tglobal = sp.get_tally(name="total").mean + + # From 1 and 2 is equivalent to from 3 + assert_allclose(t1_1 + t2_1, t3_1, rtol=RTOL, atol=ATOL) + assert_allclose(t1_2 + t2_2, t3_2, rtol=RTOL, atol=ATOL) + assert_allclose(t1_3 + t2_3, t3_3, rtol=RTOL, atol=ATOL) + assert_allclose(t1_4 + t2_4, t3_4, rtol=RTOL, atol=ATOL) + + # In 1 and 2 equivalent to in 3 + assert_allclose(t1_1 + t1_2, t1_3, rtol=RTOL, atol=ATOL) + assert_allclose(t2_1 + t2_2, t2_3, rtol=RTOL, atol=ATOL) + assert_allclose(t3_1 + t3_2, t3_3, rtol=RTOL, atol=ATOL) + assert_allclose(t4_1 + t4_2, t4_3, rtol=RTOL, atol=ATOL) + + # Comparison to global from 3 + assert_allclose(t3_3 + t3_4 + t4_3 + t4_4, tglobal, rtol=RTOL, atol=ATOL) + + # Comparison to global from 1 and 2 + t_from_1_wo_3 = t1_1 + t1_2 + t1_4 + t_from_2_wo_3 = t2_1 + t2_2 + t2_4 + t_from_4_wo_3 = t4_1 + t4_2 + t4_4 + assert_allclose( + t_from_1_wo_3 + t_from_2_wo_3 + t_from_4_wo_3, + tglobal, + rtol=RTOL, + atol=ATOL, + ) + + # 1 cannot contribute to 4 and 4 cannot contribute to 1 by symmetry + assert_equal(t1_4, np.zeros_like(t1_4)) + assert_equal(t4_1, np.zeros_like(t4_1)) + + return super()._compare_results() + + +def test_filter_cellfrom(model): + harness = CellFromFilterTest("statepoint.15.h5", model) + harness.main() diff --git a/tests/regression_tests/surface_source/test.py b/tests/regression_tests/surface_source/test.py index a25b40064..81bba0c94 100644 --- a/tests/regression_tests/surface_source/test.py +++ b/tests/regression_tests/surface_source/test.py @@ -118,15 +118,13 @@ class SurfaceSourceTestHarness(PyAPITestHarness): @pytest.mark.surf_source_op('write') -def test_surface_source_write(model): +def test_surface_source_write(model, monkeypatch): # Test result is based on 1 MPI process - np = config['mpi_np'] - config['mpi_np'] = '1' + monkeypatch.setitem(config, "mpi_np", "1") harness = SurfaceSourceTestHarness('statepoint.10.h5', model, 'inputs_true_write.dat') harness.main() - config['mpi_np'] = np @pytest.mark.surf_source_op('read') diff --git a/tests/regression_tests/surface_source_write/__init__.py b/tests/regression_tests/surface_source_write/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/surface_source_write/_visualize.py b/tests/regression_tests/surface_source_write/_visualize.py new file mode 100644 index 000000000..73340cae0 --- /dev/null +++ b/tests/regression_tests/surface_source_write/_visualize.py @@ -0,0 +1,66 @@ +"""Helper script to visualize the surface_source.h5 files created with this test. +""" + +import h5py +import matplotlib.pyplot as plt + + +if __name__ == "__main__": + + # Select an option + # "show": 3D visualization using matplotlib + # "savefig": 2D representation using matplotlib and storing the fig under plot_2d.png + option = "show" + # option = "savefig" + + # Select the case from its folder name + folder = "case-20" + + # Reading the surface source file + with h5py.File(f"{folder}/surface_source_true.h5", "r") as fp: + source_bank = fp["source_bank"][()] + r_xs = source_bank['r']['x'] + r_ys = source_bank['r']['y'] + r_zs = source_bank['r']['z'] + + print("Size of the source bank: ", len(source_bank)) + + # Select data range to visualize + idx_1 = 0 + idx_2 = -1 + + # Show 3D representation + if option == "show": + + fig = plt.figure(figsize=(10, 10)) + ax1 = fig.add_subplot(projection="3d", proj_type="ortho") + ax1.scatter(r_xs[idx_1:idx_2], r_ys[idx_1:idx_2], r_zs[idx_1:idx_2], marker=".") + ax1.view_init(0, 0) + ax1.xaxis.set_ticklabels([]) + ax1.set_ylabel("y-axis [cm]") + ax1.set_zlabel("z-axis [cm]") + ax1.set_aspect("equal", "box") + + plt.show() + + # Save 2D representations + elif option == "savefig": + + fig = plt.figure(figsize=(14, 5)) + ax1 = fig.add_subplot(121, projection="3d", proj_type="ortho") + ax1.scatter(r_xs[idx_1:idx_2], r_ys[idx_1:idx_2], r_zs[idx_1:idx_2], marker=".") + ax1.view_init(0, 0) + ax1.xaxis.set_ticklabels([]) + ax1.set_ylabel("y-axis [cm]") + ax1.set_zlabel("z-axis [cm]") + ax1.set_aspect("equal", "box") + + ax2 = fig.add_subplot(122, projection="3d", proj_type="ortho") + ax2.scatter(r_xs[idx_1:idx_2], r_ys[idx_1:idx_2], r_zs[idx_1:idx_2], marker=".") + ax2.view_init(90, -90) + ax2.zaxis.set_ticklabels([]) + ax2.set_xlabel("x-axis [cm]") + ax2.set_ylabel("y-axis [cm]") + ax2.set_aspect("equal", "box") + + plt.savefig("plot_2d.png") diff --git a/tests/regression_tests/surface_source_write/case-01/inputs_true.dat b/tests/regression_tests/surface_source_write/case-01/inputs_true.dat new file mode 100644 index 000000000..ecca3a9f8 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-01/inputs_true.dat @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 300 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-01/results_true.dat b/tests/regression_tests/surface_source_write/case-01/results_true.dat new file mode 100644 index 000000000..8979eb554 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-01/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.169123E-02 9.144814E-03 diff --git a/tests/regression_tests/surface_source_write/case-01/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-01/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..82dca4d032eec7395fe8697247c7dc4fb01f51e1 GIT binary patch literal 33344 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 8 + 300 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-02/results_true.dat b/tests/regression_tests/surface_source_write/case-02/results_true.dat new file mode 100644 index 000000000..8979eb554 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-02/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.169123E-02 9.144814E-03 diff --git a/tests/regression_tests/surface_source_write/case-02/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-02/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..872efa070701d0a052aa84f2f1980145fb59519b GIT binary patch literal 12752 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 fIbtUfR=hm1DWH4{y87gfrX|1|kOEHDC< literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-03/inputs_true.dat b/tests/regression_tests/surface_source_write/case-03/inputs_true.dat new file mode 100644 index 000000000..38faa2a78 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-03/inputs_true.dat @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 4 5 6 7 8 9 + 300 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-03/results_true.dat b/tests/regression_tests/surface_source_write/case-03/results_true.dat new file mode 100644 index 000000000..8979eb554 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-03/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.169123E-02 9.144814E-03 diff --git a/tests/regression_tests/surface_source_write/case-03/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-03/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..04a6ac9a387d5bd34e4d46fc533e90171d779238 GIT binary patch literal 33344 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 4 5 6 7 8 9 + 300 + 2 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-04/results_true.dat b/tests/regression_tests/surface_source_write/case-04/results_true.dat new file mode 100644 index 000000000..8979eb554 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-04/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.169123E-02 9.144814E-03 diff --git a/tests/regression_tests/surface_source_write/case-04/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-04/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..75c309eb4f43740be530493ea10863478d06a525 GIT binary patch literal 33344 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 GIsXqD9JFl! literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-05/inputs_true.dat b/tests/regression_tests/surface_source_write/case-05/inputs_true.dat new file mode 100644 index 000000000..49eedb7cf --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-05/inputs_true.dat @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 4 5 6 7 8 9 + 300 + 3 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-05/results_true.dat b/tests/regression_tests/surface_source_write/case-05/results_true.dat new file mode 100644 index 000000000..8979eb554 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-05/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.169123E-02 9.144814E-03 diff --git a/tests/regression_tests/surface_source_write/case-05/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-05/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..1a2f54d765811bd10a2758e20a11b7ea1f242ae5 GIT binary patch literal 33344 zcmeIb2UHYE)WfzZoaTXv9P_lunnP{-=QE-5q^Efv2dJup?p57`2)Ka zj_)JuPX2KS_e%sn;s3a0S@2hKzg|v|S}31^KtK22QR`Dy7aeS@@Zwj`?a(_glKaM?F@PCShmO()=f98gTFW_4$f)v%SLu_-7e|&Cxe(vAjd9lrv|I0@9f9K@6 zb6EJmoQhvBgdn?&AUiwveTP5Z&xM7B?NXaR`>x}{$)wTbf0poP-?d#h4X@+>`K|dQ z|8Lef7wrCc&3n$o6#wLW;zNJ-UAto!|2PF9KJicX|LnV#R;La- zSXmxAcK)ob&D?jk{mJ);4>8X<4)q@g_y`wl&mTQxZAs80KH*RHiN9lW`20m{i&Kkj zYS*8fPke~@yEJoN`>WkhQd0iWqUaae85M>3f;-pa?)^s*s}{Wc*U1J?jvw8#Q-3SP zTnV`E+bpzKV&4D1|G!(Hm+mZYy!RO3yL+MLXyzoG{{}r4lVo^H^K21lc`Vq=#@GUA z*1rEbz}yOJb7s8S^CeNmbGxiGrA5`|&+iAnws>7kUHKhX=RYS9zFg{!v{0f%wx)DH z3zr&S$ogV1648K1_+~g%L`#HVV9L-W z;mo%U0^!f}7&dF}kYOJBL-(9=EJV}3Bi@i2J6};+zh!wPINsoL*|4ty-cSnMDfvkzkl^3k4)=Qt6}`~fgudapu}-09X+5|N=M@qN=DT8EtV3qs8l$Nl;8xl5>pLD*!c0TfuG=fCV8eBx-k0ALQR=ftUZ0L%nnQyF z7PkME-SE1?ajPWXG=vt9F6z1NFy1_sItK4Y<;#AUYyoS9n3xMMHNZIMR!8m6wXi8u zooZLv7PS24JIfW!yi5AQLU$U+{A+%Iw^DHi}Z7+6KzF1GwTmv$07oX@) z>;zL5vGy*niomCG?M-4wsn7(26&y(6mcPwGOuqA|Upax7kKFFDP(7DMz}RhCXq3n6 zq1Lq{1!2PNz~vQn)KGjmjO*Z>y;|IW)TRgR=h(7?goC%4Khtv#`TR5DZYORpBTt&CdRB#ZmaKt_gv^yfO`o{Gl)HV!Y%{W#I{GUxduqapP-G8Ob7Rr=-ebiyI`8>bcd5+2#`$M@l!0XorLpO z_0zU0sqx0xFm5+2#ZBdzUmyXaPJc}CRjd`HBQ|z=!VR#FxkYbJS1sK9(OU39>lW1L zknT4+2R;%G9@Bo=Z3UShEch29AoHG9;Zi4jNo#F*o`wn;<#Mv&GoJIafTMBY4-=u= zSzRs7%0lq+E-#J6bp^Df(0%Q;FcA_C9;1J8R+4eDq!Z0s-!5K%kWvj+`E>{d!k^LY zA;g;vRo~U5R&qB1-SD{Hsql7iAddGl=T#1LIkkt*tHbgn9AZBpAKz>W_D_B+^bM~U zME6Qg%IPs01QFP+>$;^Grc~8C*{HTd)0?9-EqN&*liNn@q#h%h;^TG7L0j!_bF2>J z=+*=bK#}w3FS;;InS(iRd8knCHB2?4^!jG_C?NK zi(saUYr5bD*-i3imGfX&m@*clR}04}BX00Uk02YiNv4WsZU5Vx1wX)^QoWpVyx#!c zr3KJ8exOGFH;(m2DwmT7^+`B*pWx5*;QppMoV2Q@e-JVr^Tis{X%LPOH>J%^ZJ>Hq z(6G?C5^4^HZ(Fb00E8gx78&`?C{{%GPU(gi35VFN5xDJI7ieruua}W&7l! z-DJmCNKXH(gK2VbWU!^9dm3Fm^9jE~6A+oEz$6SttLtH((nwU+|PSBwj3*6jl_xgIAvDtv^Uz2m@<^A)1z`2Co`ZLtO3GSrCCJ$C8MZ@0TSpTA{wX8@KCOE3MTw-H65EYy3C=%#om=}Kak!V@ zNL6=*g$j>C*%8k`Nl_}S*L3D(lTaJT$u#mAJ=_9CU-o^yxjPMJ7N$j=^E5y$UX3Id zY#m(EZtIrd2qdN)T*ue|J0D!UZr(@?4Vc0U(}Dl5*(ZUO%JVqQew3|)=vU%Pr}_I z6Y2^V8o)XWwV3B2ozUw3olY+p3qpoCt`~V2p!{Zrj*Wl85nSNlebh_x8N6ecs1eal z$9*$-jd1>BdhB@LtBoo2Er6avjrDv`F_iu^b$eY_3pDc)epj+{8;bDmAI>4;5cjWz zmeBK0`S)oze$jB1Q=&mNHWag*K6##NllVd85O2p z`YtPKrUq8Bo(|Z=-U+FCWy3drF9Fw`^y9y5U5_d+Q}^J|B#qa2Tlv+$UQfn}q>xjt z$c+J!1fiCQ?g5;K+_@z8RYs``K1?!=zrQ^cMkMGpxW(54-;ZBMw>LARZizphHIi|N z`v)7yIMt)F^_2>BfcGVbY2%d^RqvHzT zL}P+JE0p{NXCoP>(R$5>Ly=9Oy31#x?Dpb(EYj=$a7sz+8+6n&yAmJ|CIoK0k$i6NaYY=Iq%I=$P!}-R#P7W z_hsL2{Or{TrdA!OH5{t|E*Y1f^FD6{24^-OwRt0oRu(Xxyhg?$?(aw~mOa0dPgj$d z!&n6aw{BG{-Q9!h!c6vA0S4|4upYem&?1Pz9d~tl1y41>?RzYp4JtPxR@Cm#^Sem> zjo7cH$vE?G=@IUS`a%K-HF6wdJX|49iDj}+t_JzQmx9IE2 z+H}y?B9^y-sTZ925d*$Nh@#BfvPKihIK=HomW;!s5d|@o#>ILGNnJL5d+igzwRy;M zIPeoZcAg&|QSO6VeB9gKif6)CP~K0)-(#ePN!EzGbXeR?exSg*0(MRKF@vtgirc7OkwZ+aQAGcw`1&CeuUgZwh3b zATPeUOy^-h9p~^(J%tv*xfgGIt`X`e0QI7^bP8DwV1wZsOYvnLz^LWtgwJVF)KY@l zbA*gTY_~tZ$4ZX`o$|WVL_>o}B~n@5{gDOLiZ~N7*%q+(%eKw1yB3)0JM3W7Z-flu zwNjj%0_ZEwBZ^XFb|coKM5gE8iaW|=oCP~!&XqS<*$oGPAPh zV?>ade^KFV4q~hp*{o3z-5!cjJ<^uZ-bw0l6dWJgGjG z2x?^~L=`-LfQAq1_v+X6fuADnrYhYcDB;+|{%8Txc`@Slv+#rGvR)QmD|U_w4t4>p z`qd0-Zz!<&qx4Ain$pK#dkaCx0k0FMsLJ8&?)#5Va(9Cdw$pMMUzVZD?Hy~X3=k4M z#2j>?_(jei@BjJwQD5NTn~Rzwy>;0mKtA+Q+gDB+Z2mYsqI5;L*nPSZo?&26a|v#S z9p<;yo6MVmEMXw{^+-PQW&6!c-!Ri9^=K>rNbj4_WA6G-AD5p<0YSsB7>l(E0adcV zWy7up=)Q~1D^Rx?Do~l(POWGFcOPZyX;iI7 z{2+WXX>yx0q#0Bg`oFZSXaWIo-aNHd?T}uXw%Exh9ftLpGh>ff&^C?iH(y#*{?D8| zcG*%jH)_nKXvNU=q;yygJRMJ8>VPHk=P6p^OF%=&kph?H^>A2XRyTw$3~^xWDcZrZ z?{9O$y*3}1IlXwDLk8_HPw(_fgM_T6(n0wu&{*{7Tv>BB7)?m!mhO#!dq=XqHAN_( z<61l)N5Cc$4spLqbHQHZ{$`-A8D%&V!kx_%`W6x#*I zbMSV<58j=(2NTXCyn<*VeV`Euhq!*UNa_WD^UwRWHeU3?cD;(AlFWf*IRBO&Iqv9wDO9lYy5jevcqa&K~E zsMK;2yWwU28kgvh>A`jU)12-95l8nw;{17j7Co|CKdPeO>v!;uwoklq(=;r|)vVb{ z-3z+A#~f`XO2K{|Qz0!SPpE5Vd0S6yANpPTSjq-orX}lV$ATU_hW}gjv-3Z)oBk4< zd#kBU!JV(rG~?#I9q2IJ!NiplGuRC}D*g9wi_Zjcin(j}(#yb%Mib{%#(ij*!36*M z>r311&vW4D|MB*-dkH=Nbo(**k2rgl;8Y|=n=>Ru0{27lrS@jiFs&@JSU<4^_|29_ zKBcV$0!Fth5+aA=_CmbhhJ8rs zOdR#hg|F2=JV6|CAY)a}Rfgl8knZ{YLtC!a1209k4dM^?qRMO+dkinLlIS5G&l!<% zJoFRP_KWm_mv6(BLwaeD`H$(b{@hF#4XG@!+aYgVMpqkfr5W&hF4zo5QqS#k<&8D|rl_6e@4N$|wScJ1WZ#rw2a2Y2RHk%=Z?&XC&vPN^J7 z<##g))waOV^|y14+%k}%G)K>{bg3ov>?h-dJJnJ#y!t(!|8yt3`0Tw|=wJ5Wot!`= zFdDV$Phjr>rv}c!d*jhi2s?C?(@qy1XK+pWJi$)FA-!pJnV!9n}*0+nj&P4@}AQ z{9Bv@WSpDy?3??p$1L`DcHJ+myY5SgDJgDOS9{k1n`>!V-bL1e^o_!IZ{?PP7D#i0 zF=QM19GMcg3|hCO-42p*UKkG7d+b;oUtzuW{k_JjBk;{7OexJHA3XNwK;O0Y!gkl@ z)!sGFL0LfPE!$WvRPk$!#j*ziBpl*)YDUKSx9oQ4KjN5^an^(iop9~wUG(e9XY0C4 z*{HF+K|k{_;T%8{uFn)~+znFLLk;|zE8wb??E4RK=%K8KEg6G4mu^3Y7dW^rPx?x` z8>+Pd-{rD->?k!d|1mwbW#j5ML(#>MW4{4IXh0=MvzxeUEZqgM&0SKWV+iVK4u(Ea zEFIsDkm<4UjC+%xFrU}_E8l@{pSYxQ9xZm*L6lxNw-}tHz4v4-S1-sbSlv2O^#wlR zVT&qs*G13rhWgWbagz7}aXhdfX7V(uie!RVZ40Kve@98`n z3CHcH4h9DG0a@+07Hh{bxF!5;M3Lpr1RDU0wbSoe01pm-cGU9 zEe&#N9u2@*Dce?xs|~JT674)w+6WamW3TILl|dPLEol`e8FasQxyM}+a)0@k&_4XyS*!z!X?s{`|!G?M)_3_O@c=r9v=~kaV2_E>y418t$oj-`gRsXbB2m=*XLEGLYe}~TxmQ)a) z_JR28joxvE0;rgVE7CDenjeq%rGM$c$B?8rdZhD1!CGODI+z{Ry@LJS2>htPZD+vR z2&Da-f|FVr0Z`ws=Vj9fgH}?HaHmWlBh4rDHS9>|k%-4x_&(05_;A@o;zc_sA-ICm zy%gB|ae8czK)+hTm3W|2E6eruL^V9mzaxPfdjEiJ)>_v5cN_#*f((yKN{{XKm zQeE^2S4%l->h)TfeXm1WI+Y69Fm?TO*S8*UDRL{)sg(w3c=)(|$R2Y|R9AjO-e29H!?oddi+#J+ta$TWf8eB$eq&nZBB%ClB0ALjvXi~^zfU4xDI_a6Mut;v+ki#Q6Dd`LS}5q^vFSiW9k{tl^+no{44OqF^{ShUgWKrOI1A+?)Mc}u zFYk#3YI~wr+!&jHrbiF?NN=AT*E6T@Y&aDQnNEFOk;Iz;jNCp82C|5v7jG)L5Xd;h z^@IC3uIK(xsO`}=6W|m~ms4e>MQ~j)>MloAUgv_$bw&ODw>vA~~7N#hSaa@sb`eD?V`kZId&;v-GKzJEE_FB01e9wm&tG}9~q zhESc^@ zq(HXtPggTo+hNVMf=Ug$W)P&P`)>Gx5b{mD#8haM^u7%4%fIxHb6mRjY+8A84EF=X zAb?KNC6oe*ZfU50K3M@hKb*X>#lHb?RHTP@7`6eM&Um^XJf$F!|HbGEt%2?wwHKhs0WJ9E^`ZxX(YlYHtr^80?U-uLeZCpFukNl*&ha`H-`fHj z<)u%`TDXCoAA*{vov0S-Ps>VtP(OB*<~n(rn|YJzW17q_gE+lg}JUD=fW7kMb8^T`AP57nTi zDknAaq}nCw`?KHUpGFbB_RjRV@lX6pf`)7vr10T!y1cs;pv&6VtO}$>QBS)E%4B{` z+F5E}G8WpIzU^z1Q&4qLFqobyw=Jdza;TN683*~HBGHYb~;W=$TGggrj$Y*0H z)Wd?HA{ciXIfuA@$n)0i>Dd(ul^Pb?PtW}cCMA0cWRK+aXRA)t!2?cNCw84~2l4`H z<|k?^!SmRT)|+BNXlUiT)_yWQ#O;SXZ(T~)eq#wO1%~UQxUfP*?d%Ysn%3rTe%J(k z+}rO5Xm-GF*Ur8jDh~pmF5g%yN3$8dcN$c2lldEQye7}{UML@7eBIpe4eJH055H^{ zXq^E=Kl?H_&fUNNd~{Ut;m3D?ewl~8`h^N`UD4Ytu}}jwxBmR>ui_hdUi8!EjHIeS zN(ASvs7W!XS04o5`5&`cu{DC-S(Xo~j5@)cuy{jU_FZcXXFN-;D(^wYWGzpxvg z7d>At{+Av>`+73`1g0RicJ~Dz&mQp2{`_+(l?I^jZe{Dei5&28--=44wF4%s98^bw z9wIsMdf#$(k;YNtcuk%ceeJ@e6 z|3@O*9;lYWwm@_TrEND5Q!PDl_0kZ+w>mO>*I#hR^Og7BwP@3KAYcpigPq-Rb%xiL zfb6G!b-lEz5gK5t-|~02LyFL#ob=0C;QfZ#!y&nP=$&-$!0%-BL+o!$pXi93w;ioq(mQ^y7AlL%=<3Yt(}h zI+Qt|l7}vsbUg;KA1skajJXax_2R8*2YjO(>|3!+D0{``QvTUKIIw5^=2h&e@OjkB zV{1HOfT#x{fu#UJ&w98vt0$56M~U_Pk38b#DfT~8u*LdkjxS#2ccu*lu8<9+h;M)b z-&=RwcIgCef%~xj^)WEE!LKJUi5k`JKmAr$L6_uuN*s5V$RoybbM995dKbYn)w@jl zgRMRsIyxUdkG zUQbKTp<4DsIrY%!V!V!A*RYarcNIAFZQsit{d{n@by~x!umJ=Li}bwK?SMx!8xnzU z77+J0ydriZV@bP_b973%v~ST5FLDU=$sQe;GJNz(ak>Hez-qiV?sKDp9!a7Eh4gIYHnQW zNO?2s<(qEkR(ZE%?c6%oYmx8tSlgGLw)%!_-te$t_yq8SE!NR z-2`LaJ$l&hQwXw-BpGmg}_A`jrkJul|&D5|A2?2eG&*aj|kWAyZXC- zC?9>#!PIRWuyC!Euj%Rlo*ANCEF3l9Z1Z@zbaWZ856yx?Cb8a|5}Q9vk6mnLjbK>Y10FR9uZ6Xhu-o4=NGqltZiKBLfqO0B6rJ_HR6UGr z)*c`vyaOb9h~pa{8An(8^j3fQY202&@G-ViUXPoh%B^ety)4bp&{F$(zCb?6J0bPD z&!-J~I4W6aoajeRSi6qsjVhCHi1qN3ac~_x?Ke2-2}5v)wZV@X=N=GF;JF!T(F(<6 zxe9&4n_-rhwfAG^B4Ec>EGk~3jXoPT7mzfQAmI?V()07tx@zWG+?p)x-9GW`ZLg8=>q6X_bU8p)kSf zzHKJ;Z2>3lqK|3HL{lcl4(*I9-Z8-KKES=#sC>q0G8KWla7 zD1bB7td;dSO(0vOjd4?88%#{=OaEL_iR@Gj+HLK&jYJP|I~674%)gzR&tbM!9u@Be z6+=h%XznQhDl6&LcJAze`zG($K22?eA_?Z(51FLFq~d!kl<&}>+)u+Ft16TFHL<^m zk#RPteLng5%-pyzX_+lwdjDcOdb{GDzf1HTczIV%9Ccd-yy{KYE~DN9RCFsHe3|so zg0=20SLjIBArsq8oQ&f#T%8wtw*vNEL*C2OFUB9tEH7mG_(lkZboWWCy>9_-;VIJj zN^Q_ePhonw8j9}ObmqRa;L>rFJpX#vd8e2-rAZiDmF~x7witg9T3!RkG_D`8)~LPj z1$#4mUr4u4kgFY3Ez5dWif%<;T+ET!tU$^k*0Y{W&y3Z2mIK*Ci}o60JJBL)Mu}i- z(^n?G$HB%oPbs6t+d$c+Hp$eo7VzwjNak`sbrh>Rag{H1={QQBfBkQ9Hj?SNNoXpR zD;t3PJQY|6SR4lAM|B z{F>Nqc>XmW!$0=YC<-noV7nEgzM9n+gZZC9kL~QU-7I(~2MV@J1vp*#0a*rqW_LLB z!lDr&aieA8C^wzJX-^u`d1d1EgXdq9%A&_stw6U5GE{(lDV*-f5){a4!2oG*lWK@r z1(rE-LTGY{HSOZ5Zt&{dE#K81vgrL2j$OqDvLt$l+YfpE^}_E*wx3PhE~i3+5bhi7 zn%NVaeL?TYPxbzurHyqXITpPFdfA;YblMERjCZceOr268+rb9^@RpVJ68)NeLh z-?RobDy=_i-K9=qH)0NX{xxnl4OhFMyKz$>F=Efg*ZY&fhPBCV<|ms#QoPd5TV9-9MfnF9!S1EDI>fwY|xj~B@EuW|qKm)2^`cg_Y|O?nj=X*=QetFs9S zb?q>Woja#>Pb089uJGl3NItm05gHIgy%XiI*(AzOxAb@c&%ees;(AWbNN^K={sc1j zwQ1a)XW{yz6QVkGT_7o+z*dnDLURvl;lPq>w6E+fmBj%9jdGmC7<%ls(0B+X+ z|H##4c-%n-dFi}1oBn{&E#EbZlQZBWIwz$t*b8p-iErV^-H5(U9$-AJN4gG~IKGkR zd9#@@?ThIAJuk}OXy-BRGz(M&PQeh78pu<7oa>BfGkDZxe0Qjk;+F-K(qKwdvN+;59HzRl&68)Sb6yQq+cidv^-W4)lq z`qRP3RH;y{;|=Xm_Y4qcWGu;O&W($j=M6+%yyP_Q|!3-0oy z^W)R31gAw#P6pnVMAy_OS6Q7Q%_GKb^h*!2K*0O)`ep;43d{yU_x=>8@y6f&fNt0G zXDn?2tT#%$?1Cy`!zUG$6s0C05-jV79ydkKYFu>B=DbPbZ^V9$E-IPd(dOHC&Z1!y zN>FyMq4t`q46A!5s+{)NVpcH2s(=fgpsLU^1KnLkXA=>||s=3b$~+M9KnheDqL ziWi*Sthvo_m-409eR}QCV85K&sPPtb`@ua<60)T8F2sJINyb?yAG!GMrJ`tU0lb#} zWcPbXI;1{~DrVJ_8gR?hd(B8~EHJr1sl8L78~C_aRIsvWqMGY?C}W&R^$^EVEiz8l z1#`vOuY(KjPLF99mZUR9C4&#=Or#}d6JZ8JL(Tf6KIojXOG4V&4-}M?fvj^|P!YKd z<<%La9AZ7%3mn|9gWLouti19UIhJm^g{>$R^7&ZS(Vxi>DlT75#e(%gOYeenP~sN2 zEmS+9`;`$Lt70oNh$4-n#GGwpdT_g~`nb1yIIJCBWxO~d)BO{qaYcv(akqig!&eg9 z4?wt6XVY2jj22LN?L*~VW(|~e%tLzQDQSO)*ls#xoXryl{3it#`)!Dyr`7T8$>VVS znn(LfBeOv|n|IV{pAIlv^>Wkqx#uCA(8;H0J<5lA2;bD+kh}Erk;wC+c|@vsvLFE*;Q5lK`1zIc7d?XaskgbkS0dPO$xz+Jo|j?}#F+md2h3+9ZBW z9N%=w^emK*JydEqeu;wyS*4hv^5GT%xgzi&YbKx`%uWS~r)W2Tu|m-y$xSVQ@W6{N zEMXel5KRx3O#4W}A&%GNob3StKE|cgi+c78NK;{u3NgF-GWmf?E5JIuo+hlE^;Jd@Zzik}*_hP;f~-c%C<2O)7Q6h(*98 z^rsn8-=gz79}=?F25WCO-$_pB0y~0i3#f%O(c4%1BeY(TUbl&_ z3rppB2Mo9cww+Fbs~h~vhT%k&A6b($lN;35PwRR zALR)^RQl%UotEP0iT3)70*y=0SCQv=_om9u7^i0}awfj7DpZuB!3=M|T2Ax00!aAC z#cm5K0N?UiS-y>TL9ojF&d-|+Xd11UHqQ^z@f@)qkmq@SSVP71DwGykXh(>Wfp$!S1x(%wH#{uktKUaJ*i(4 zw^Q;wZ{#_PnepXwi*`%ANu|l<(gbMZR(0xG_CPIP3hw0{YP-4z-~}upSMIS9MAOa^>(d;ITzlK zov*D2-DsL;?~M~k{15P@v*^O#<}B0;<{h`{M)0`-@YG<%t!BHq=YOpU(ECt3_k4{T zN}<$*F34S*wsmiA984=!>`X5^faE>a-=2E;9tnroZsdA~MWVK}PyCKM%S3z*xX=th z#ChlG+Wg~ir&s#162U%DmmQ=lHJ4v5{a~=#d_My!@IIqx>=fyEj@S?IymkC~itEWe zyzYDz$2j;NbMFyDWk1ZlP4LRT-33P?x__)P`vHcx9^TkITLaJDRsPI2%!k_RC(}@i zkoHH3^^oVSCzPBxsm?qOYwsrNBt~Vp@Cz9u_ziCyt>3Kd0?EWn9QepLCKM{NG{9vOh4T+n&j7 zKd)L3FM23Zgc{dEw{if`>8pM4NSsOP;pil|;e>)g=bm_Y)R=kwF`wt)?cL~>*E!S+ z-|PQl`&W9Dm6NYQgIGKsVElF0AFJIUVEQ_3uKY};&|PR}!Ks)wAaRg6=geIGj-{(E zQ-SS0AnzJ;IeHE4-{y#}tg(3SvltJsmzF_~1Usm(yFDuB(1S-~tcWU;BgFP3{@?8l2`<(BDuJ!p*zsFb9X4@I}ZlJ=LWG<=?-pqs4 zk}{os$-VG9_Y`-fH3V^SEEW&D~=l2q~AAH>h<-CItPIi{X zh##n5_+9ka2i@$3m1iihY(?H>7IX6y7o4s?9GF{oIxSyrX%|ofL)_$!$bB3~N}oh9 z{&Xe1k3sBj#oyb5spu1lV*A$a#KY@e%U?9f9rO4-}gM2usrqE>w^GYzqaR^&GMz^T@EbN zKfdt^`F=wzA1mtFl&T zzD9YC^iewWiDGp6lr(9-ir5d#a3LgHddxcN@`P4E8_;W`4ZLoL!41KW9h6-2VT;_e zWZ|3j@VpG~CU3q@P_a37SI_|&G`Q}liu|B3i60Qx&mofX350|3FDAVX=7Sxy-AbJ1 zi{nQurh`*kxGfc437a{ae=;A`|FFpqv+0GQY(gedoeHQXN4rM+o~74Gn3Hk(eLYz& z)b+qa;UEO@?p>@OcJV`=&%V|{Ujsiz*1K)c!C;%Og;NcDdhe3w09cLc7C&j^xI@}M zAhz3KG7fOi4%fe54OI54!aiXdWd37%%r4Oh6FpiFC>Se!9Cvj9tVTl?GcSQ=$JKdk z=I#d@bkl+3S9nS6M$9=v##tyA*|maJQ#^eZI7wh9XSjx7v$-QhLRSxvu}7bzJt+d= zf=<~fjcK5Re_P_jb}2ObV{ z2EMDKv2WXwq4+a5ZMljbC~cJi>CQAEhA}5iUbv9n&m!hnl5t)p8NOTVOoQ-Dj#w3x z{LUvHwQd#52&(|wHnY_Qjr#&7&ZvgdJ9>ca)*o!C;!5ZaU+ErsvgaofkLO79t_g(D zqFaZ?tEmv&ek-nh^m3Odh1BY4xG}Zw5OhX4&<9J zF+mKGpwpgdm%0Ki`e3yDZXaCwx`_3Qzwq2W(M@`PoA~;Jw^>{dZm+G920KbiCxN4` z@a6$R3vjFz{Cupq3iuktGo51Z06wwQ{QSn%@LP)at)v1)R6J31mG%8&6pR0$!bDXzhns>-;lO#p+9wK^PsQLrSs7o82I$_O?6mCF907ZJ-MzdLpc~P zCq?d5Bxygyet>JkIk=vJl9aN<8Ui*XFa+0%QzI7M$FPa97BIF}!{gP57RX)haN}%u zK6tIDdpMXv9zFc|bfPT{soii5zvg>!pCIQ}!WI?TnG;+9M#kwtXWu_xu{0 zQ?55OsGdVT?ybSBnpLpT_Epi_TL?PU)jZijPkQ|!=HRh`oU<$X{Jp-kWT=StDyPlG z)2rMueeVuc0NI}B`e9j}V48CU#>Z9;1{uXxFMHCBv~T%lX!f4;`O3r`+=it7K#$aY zTScc6lL?rTj_5l@OoG%;8$Y+}*TXX`tOZ5c&491ODTSJ$5xiAU4M{ol1}Rq-Pi|}4 zMN&V1#WA>0)-F3ScRhVN5S(W8`*~@|y4|z7eC1FS^QF0WvL1-}J?t&~&<@1E8wr1@ z3P6rPuUC0aM@cxihF|rA@8f#Doc383K1PY*{=k&*pzFAIB~Ur6S~@)12se9qF%<;2 zfxO$wj}F++t^dhziDkRK4gI;UVilobkc5NR{Vxu_?gQtjVnxS~?_0cIf|y<}vD&3W zg}J&OOA_QvhLw~~PU%kN;CTV7+O>^6aCuj#g!PS`sF(h~xvCpRu3|v%Okn8`#?bER<<8(70-txZ@0il~yavK%3UKo)bXBLF1JGGCjDD z{nA6euE6~h%{5BiOn}>s(#YJTlD-#&H6LmnpPN5yezS4neLy?7I>tP5@KHVp^c!)c z;FL#wmN%}e{|i0j`TjJ@VX~^vXfd1gR5}1p!hY(# z4;rCXwvuw`xnhJM6}U{XX=%HW=i&35z4B4PzI0I!_F0ZDN@@TIi+z|?N-P7;b=p5=x7FhJtZIQJ+qcLhYcVukwGv2?*^Sr_$n)?QbYN7q$@-3u3*o~cN>At0Q$U%6 zOW%a69cCUq<9g^?35eYJdes;9Mo>}5(eapj8p%IQ$(m2b!F}sj{Vb7(kEv!PaUBzD z0Eg9te9ft;kQ{9(3ZvL2u>NuUvvG%35a$t~c_*(F4tLJj^;5n@n%R6*Tv|!(_IEf| z6w@05`Z~Z#d*B_VHUsX1X6hA+HLze-yya9c1V!wO{5DeEkbU)Y3r9&^#L(qhY4za$ zo#WK+AIs*@4m7oEhZ`m*f#vatJBbhSfZOBJpp`L6AZ76Q^P3-gq4aID(x>LKi1ESY zz6#@ipA(s_Mb(+V2WFZ28cIVq;X(WCvW%`KnQk zUJ~)&^vv~0;SfnYAZ|ZP)DKeb`eqr`g)U%kc=o9~b2qpeQ~CUo@!a}Ald$iccOzig zyK_5zo@787u3f9e>>1I#dsbqX8%gyL$5HZi5_r4GJ=kwfy@h~uR|qR^W*CE=K7AH+ z@{KTkgt{hACJh*8J>e3~tEz-*5}II^%olStv0kX%SO)~HO2Ad|{YLkT-GM>U8{Z&)R&-TrQ0*47_Cw4e zU-uC((oMlD^E+N!hB`bmp~p&q)g zaARrtU&J@^bsq~lu;dFVd|`vlP{S3j2wP5p98>UypQ4)Jw2J9rQ|cD@pfNhvpuP(9 zYjQa6zNUumJ?~h?OQwgooszHnz&QkwVfUd`1Mp4p9mA9-lt{g?x8BNu9AI>}{asc< zH{_V^ztC7#224XnchuexK@T>FpI7=na2^D7csW0##_n`od6<27abEF%<;?6Pa z;MeM3@r_*1cFvMpSzxh!BB$Tk!?#>bAkEzN=~g(m-n6rBf|92TtT8;vqoAAtTVj_- z^N6fKrF;o3J!E=_{b0?!p%&LYGj9>#mM$I#-Rgvkf`W^Ax7Zk8ewHg}%1g`gxShuZi`Lua_|j z9`kP0DuTG*g(n}`YVd;+v!g#Cc=%-@)X05tR_as@e4Br-#k-;t9&>n_IhNXotj*Uc zJJw7Z5AeSGFF#mny$r|l3T0Hi4RD+E8eg+22aMrW+m7(`L2q%bIIp!ofS4dX)ADr~ zG@W!yd$Cps?JTt|=OnWmar^ln>t+5BLXVW}6MXh{Y8u|#5L-F(;Wc;_Gf2lckOGoD zdzS@$?19dT{7=-+mcaM*PA<+GhY*Qoop(RkNUsaT@tSrfzDep~ukZJQ)yAR@jyda58za6M@_q=hACRvvNjUdN z)wXT`3fc%sM`kbj8^&M26PEua7hWsP9$9Tt41}^ASX1`*z_bZ@`_w=LHMgBHT|;I! zVm}~X_p$K%F{YLeEiKf=;Ka}PTWsz_uvYk<^82&B&}C*=zU)g8Jmxq)xm?xZuz=smngb=ye&?eB2*mXFsA7E}UNl+%LBmkP`5KHxR+x(U{1 zN51RnYlh9&1ns5ts$oM^QQ(6Y0MWC$8gXMKsUQ3u&K*0iCOWAB_^O%tehVEP7HrBn zkT|!_?6yFPY`sJ=)R>XHy&yL%HBqI&y4mmZxuf+^NV<51HtpPcCgZKHvkr65J!Kb09foI@?C+4*PowI# zNnidP&|eqvcGh_D`i(iCW4q~pqZdA|*K=FH_BFV>@@HKOYZ2HizQz3@-(zs*&9y8U z1`06$yXNo95c?ZBN5PbjU1?SBV%*vKYGSx|oDz$Yo1*(pT?nr-=uAtFw?gD4!^ha64^#TeW%r-2XS=vbdl<-{ojHwDSnfP(c^m zVG&#q8Ceca843F+Bo{%m>T>$hCKdGM1+9ahe3o7>!#iK_;@=}I#sVJgGmCnB1P;bs zIMD#4+HPNdMDY=(YbWbd)X(J!ZCao4EIkZV2Gp&gdbt^u4$shvla(Qx{J9NInBRP4q6FK16 z$W(RdFbRh^zVQK@{^{1E9WX5|YwE5~H5?P!*`aH-6;*dNOUm~rwHq-QR&#A0uPy#DIoZfW6p9W3ff6w#!3+erBV!H_~ zl#R1z`Xdk14-J6eS$1{GOd4eV-w>^m@jHXPgC-wtdU$DVAt(gcLOv>SgO z(LjTRoD1TQY$mZA@pVCXQN#Ssr>Z!O_qPbxLit#hP0MD3U6Ke-k0&;N??YyYaPDcoKB+o*(k4p}?j# zgk^jfX@eccRrN}5>cLqQN!Yxz60QpT`AXra4mvYaF!IEKG=B%T(Xal2=n|YTOTKe< zDXsAM%X4c6V9CCzxvz8b%HWI^Lj&dAcEDn|;l*@xJv@gvo?9cv zgUb2yizl5Wowp?JkBXCVToXbhmGZnml#bFmqqmFe&alV(wS*sSsD`SWLPP2+cLNi~ zF^Q}3-OxxPy@1h61GTzWqQ*`j)k7Rd@pVR23%tFx^sIFezwa+``pc|s3mk>ZNFn`mAM4tW(7J#ErJIZy3Apb|`)_e5Mj z`+r4)pvw)RqkI#*XCipC5ogv-9Y7qEn{QAbQZorZ~#prb9G30mN{PBxD zA8K}Z`G+rCN!QB|w^Q^{5AL_DWIgX=nigoDbfowVp-}vhQPd8nz+vYhpd*9G*RKR<+}c;{?Qm zBR5Z@wFNv5yLCXdG6v@H^6vFG*9RVcw+^%^OM#L;EW6xF_aVcytpfM?9+7Z}{hFNf F{{Rq!v~K_a literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-06/inputs_true.dat b/tests/regression_tests/surface_source_write/case-06/inputs_true.dat new file mode 100644 index 000000000..cdb9726d6 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-06/inputs_true.dat @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 300 + 2 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-06/results_true.dat b/tests/regression_tests/surface_source_write/case-06/results_true.dat new file mode 100644 index 000000000..8979eb554 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-06/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.169123E-02 9.144814E-03 diff --git a/tests/regression_tests/surface_source_write/case-06/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-06/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..bef135e5ace081b17fa1400cddb095fda62df652 GIT binary patch literal 33344 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 YCj~A4wCW)cI{o0XzX)Z2bf$d!KQh5CSO5S3 literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-07/inputs_true.dat b/tests/regression_tests/surface_source_write/case-07/inputs_true.dat new file mode 100644 index 000000000..ff73358ca --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-07/inputs_true.dat @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 300 + 3 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-07/results_true.dat b/tests/regression_tests/surface_source_write/case-07/results_true.dat new file mode 100644 index 000000000..8979eb554 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-07/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.169123E-02 9.144814E-03 diff --git a/tests/regression_tests/surface_source_write/case-07/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-07/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..f84a5c4c5b6d8cb4ad3f720e40580f6309998d57 GIT binary patch literal 33344 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 300 + 2 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-08/results_true.dat b/tests/regression_tests/surface_source_write/case-08/results_true.dat new file mode 100644 index 000000000..8979eb554 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-08/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.169123E-02 9.144814E-03 diff --git a/tests/regression_tests/surface_source_write/case-08/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-08/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..1fd302a7cd429beaee0a203ec8e5f1da09cb09d9 GIT binary patch literal 33344 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 ud%f^f literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-09/inputs_true.dat b/tests/regression_tests/surface_source_write/case-09/inputs_true.dat new file mode 100644 index 000000000..4e893fffa --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-09/inputs_true.dat @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 300 + 2 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-09/results_true.dat b/tests/regression_tests/surface_source_write/case-09/results_true.dat new file mode 100644 index 000000000..8979eb554 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-09/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.169123E-02 9.144814E-03 diff --git a/tests/regression_tests/surface_source_write/case-09/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-09/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..af832f060cbfbfd6f25fd6cd2049a5bfef55e307 GIT binary patch literal 33344 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 literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-10/inputs_true.dat b/tests/regression_tests/surface_source_write/case-10/inputs_true.dat new file mode 100644 index 000000000..2d8f1ce64 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-10/inputs_true.dat @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 300 + 3 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-10/results_true.dat b/tests/regression_tests/surface_source_write/case-10/results_true.dat new file mode 100644 index 000000000..8979eb554 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-10/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.169123E-02 9.144814E-03 diff --git a/tests/regression_tests/surface_source_write/case-10/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-10/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..1512d869298f2a8a0c86d1218646be2abe1ac77b GIT binary patch literal 33344 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 300 + 3 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-11/results_true.dat b/tests/regression_tests/surface_source_write/case-11/results_true.dat new file mode 100644 index 000000000..8979eb554 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-11/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.169123E-02 9.144814E-03 diff --git a/tests/regression_tests/surface_source_write/case-11/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-11/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..0edb06c88e9a4ba368a72bb0a16b698553dee244 GIT binary patch literal 6408 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 literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-12/inputs_true.dat b/tests/regression_tests/surface_source_write/case-12/inputs_true.dat new file mode 100644 index 000000000..087024695 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-12/inputs_true.dat @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 4 5 6 7 8 9 + 300 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-12/results_true.dat b/tests/regression_tests/surface_source_write/case-12/results_true.dat new file mode 100644 index 000000000..d793a7e42 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-12/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +4.752377E-02 6.773395E-03 diff --git a/tests/regression_tests/surface_source_write/case-12/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-12/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..ea81a8250956ba2f3da07bde3332e13963cd26d7 GIT binary patch literal 33344 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 S)+dnMzTq}Zl@F~h?f(H)kZg?r literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-13/inputs_true.dat b/tests/regression_tests/surface_source_write/case-13/inputs_true.dat new file mode 100644 index 000000000..a3421298b --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-13/inputs_true.dat @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 4 5 6 7 8 9 + 300 + 3 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-13/results_true.dat b/tests/regression_tests/surface_source_write/case-13/results_true.dat new file mode 100644 index 000000000..d793a7e42 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-13/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +4.752377E-02 6.773395E-03 diff --git a/tests/regression_tests/surface_source_write/case-13/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-13/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..08bd809852152200e5449745616feef691f46778 GIT binary patch literal 33344 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 Q0?F+gZo^dh(CX6uAE<_K5&!@I literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-14/inputs_true.dat b/tests/regression_tests/surface_source_write/case-14/inputs_true.dat new file mode 100644 index 000000000..b4c141b5d --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-14/inputs_true.dat @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 4 5 6 7 8 9 + 300 + 3 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-14/results_true.dat b/tests/regression_tests/surface_source_write/case-14/results_true.dat new file mode 100644 index 000000000..d793a7e42 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-14/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +4.752377E-02 6.773395E-03 diff --git a/tests/regression_tests/surface_source_write/case-14/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-14/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..50cddcc70cd27df73f37a288d86a906d7e7694fe GIT binary patch literal 33344 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 TdaX|&xqZWJm?|GyUE2QxR|Ib8 literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-15/inputs_true.dat b/tests/regression_tests/surface_source_write/case-15/inputs_true.dat new file mode 100644 index 000000000..9244d839d --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-15/inputs_true.dat @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 4 5 6 7 8 9 + 300 + 3 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-15/results_true.dat b/tests/regression_tests/surface_source_write/case-15/results_true.dat new file mode 100644 index 000000000..d793a7e42 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-15/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +4.752377E-02 6.773395E-03 diff --git a/tests/regression_tests/surface_source_write/case-15/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-15/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..3e15017a16626a9a66d903daf1721a97dc3c4058 GIT binary patch literal 2144 zcmeHI!A`kb>e1ZM}pWvtXS2(*n zV-{FqJQ`U7GyCSvv^#xM-kbG{!`w*@nD9$mAib{Y`T#0ae-AwT^utk*OrwCyAEUE_U%tM+FS*YuY)<+YG#NrL zlPhTJLdBJlA_{+qHeL>I+!e9D?9uB+#&V!gKEqSURfFn2R2P^(C5pyrV==be8S$3i zChN(z+>~hijMuX?@|VwuFFUogifCgow*DmHlhWC#g>OQ(+?PS3C^eI4@ol-iVAPog zJ%137(+G2#mOK;6qi`|`D8nR;`+nGiOOs&n&77zcCt-K=JyR_U--OKEEUH<~jpI0K nusoVGSFHZC=n6-qeo6M{!~X5nbzNWu)_z3uYF=;a|J;Euu@!3E literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-16/inputs_true.dat b/tests/regression_tests/surface_source_write/case-16/inputs_true.dat new file mode 100644 index 000000000..b3139928a --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-16/inputs_true.dat @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 4 5 6 7 8 9 + 300 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-16/results_true.dat b/tests/regression_tests/surface_source_write/case-16/results_true.dat new file mode 100644 index 000000000..a97be70ca --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-16/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +1.496403E+00 3.891283E-02 diff --git a/tests/regression_tests/surface_source_write/case-16/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-16/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..49f67e8205608e848f0130ed65b472ff98eee046 GIT binary patch literal 33344 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 NWw4>&FGk7_{y#M + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 4 5 6 7 8 9 + 300 + 3 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-17/results_true.dat b/tests/regression_tests/surface_source_write/case-17/results_true.dat new file mode 100644 index 000000000..a97be70ca --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-17/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +1.496403E+00 3.891283E-02 diff --git a/tests/regression_tests/surface_source_write/case-17/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-17/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..35878eecae7d7fcf08cbd53e5db8b2d63e3177d3 GIT binary patch literal 2144 zcmeHI%}&BV5T0#8D{2Co5aNkP;K0F~n1JcQC{YjI_$|FTVm3ulb>*LW9kI~t|FJE8Zm)vI+HYfcHnhYVB z$rZG9q2kI&5rscO8!v}9?uuAn_UL6JV>wVLpW!LwszG%hste4Y5k=#)u^3zKEAf`! zChN(z+>~hijMuX?@|VwuFFUogifCgow*DmHqte-_g>OQ(+~;AUC^eI4@ol;NVBDDn z{njv^q!H%$mOK;6qi`|`D8n?32d%IVmnOmDn>kS@PQu>!d#0Kez6qJRSyZ!}8^>|f oV0koWu2}tN(G`wJ{gT|Bj|R6_*L8svSo;ypt9iYx|8obv0JQmP-v9sr literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-18/inputs_true.dat b/tests/regression_tests/surface_source_write/case-18/inputs_true.dat new file mode 100644 index 000000000..80d6253a1 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-18/inputs_true.dat @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 4 5 6 7 8 9 + 300 + 3 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-18/results_true.dat b/tests/regression_tests/surface_source_write/case-18/results_true.dat new file mode 100644 index 000000000..a97be70ca --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-18/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +1.496403E+00 3.891283E-02 diff --git a/tests/regression_tests/surface_source_write/case-18/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-18/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..36da9e7a7f7307e5af5221f380b58750742fcc41 GIT binary patch literal 2144 zcmeHI!A`kb>e1ZM}pWvtXS2(*n zV-{FqJQ`U7GyCSvv^#xM-kbG{!`w*@4&2rnMMJZKSpN + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 4 5 6 7 8 9 + 300 + 3 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-19/results_true.dat b/tests/regression_tests/surface_source_write/case-19/results_true.dat new file mode 100644 index 000000000..a97be70ca --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-19/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +1.496403E+00 3.891283E-02 diff --git a/tests/regression_tests/surface_source_write/case-19/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-19/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..f4261451dd4832b3ebf2e4f014543c850f8f35de GIT binary patch literal 2144 zcmeHI!A`kb>e1ZM}pWvtXS2(*n zV-{FqJQ`U7GyCSvv^#xM-kbG{!`w*@I+!e9D?9uB+#&V!gKEqSURfFn2R2P^(C5pyrV==be8S$3i zChN(z+>~hijMuX?@|VwuFFUogifCgow*DmHlhWC#g>OQ(+?PS3C^eI4@ol+%f7G4& z{q`UprxE5fEqNxCN8w}?P=-kw2ko#AmnOmDn>kT8PQu>kd!||zz6qJRSyZ!}8^>|f nV0koWu2}tN(G`wJ{gNEahr#XDbzNWu)_z3uYF=;a|J;Eux+!Yo literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-20/inputs_true.dat b/tests/regression_tests/surface_source_write/case-20/inputs_true.dat new file mode 100644 index 000000000..44f0d72c7 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-20/inputs_true.dat @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 4 + 300 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-20/results_true.dat b/tests/regression_tests/surface_source_write/case-20/results_true.dat new file mode 100644 index 000000000..7ccce7cc3 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-20/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +1.149925E+00 2.542255E-01 diff --git a/tests/regression_tests/surface_source_write/case-20/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-20/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..fa8a4e628728deb4c4d75b03cd31f0aefe6d132c GIT binary patch literal 33344 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

`BTVz zMB)c?1qpIG95ua!pEsVu;}p{9 z#_cYhfCb4xjcWC{`%mhpT$(D17i8z$d9tvt z8ho#&DRBQf2v1mhowJjS1A2ZLM4YaK$B*K3!aP^_q71u$(dVFjXKR|VesD>?+PAf% z31+Vb-Lp{`fxm*C`D?4&;Jz=rJ1-C=BPy>w)FW>35X>JfJiGa6O1{sx0+X7B2&*Z&Y!Xj1B^Cvgz~u>-!zsX$VxDOPUCSdE-qe@ zbpH-nsY^$`kKo#)9C9Xh_XNI!jva&i?O!Xw!|870mM0UiM&^)mm&65Gd zqpH*%i4*Pz<8x9vn=ZvX?}EJz!6oTzIL<`|p-1jP-@xMdYFPE*Y(OWje%1W?Bovbu zd}tF$gl3Yu^~oxJgrFW>JUy~(k@1Z#*%gGzd!&naQ z!J)vTqe(zW;iH)MXcPEz-u|rIt8qABlQnD2n+&+Y@b7yLKHAbQwpWHAB#U@ z!o?|w((+m+*Sew2@2LhckqlTo=UH!PFaj4$?VbchBj}OH??P+-!36W?44&Qio6guC zmHY-A{oV8r99)1Kn<+7}m0yi-8a2a%6_&Yz`Q@ZX<#u%&w5_A;z< zs7n9bL+uzF2cgBs&ZNJ}1W}K8 zTs#LG;MChF{*Eqyro9$n&lAf=(67(qaRjxgkN%(~au=#UOP41)ld?O%H zepJX+(hc8xlxs?&hTy^(i97J*1>~`9WU&AH?Q=*R4v71kEkNcntGRD#xcliPgR{dv>#X^Z{Wo?q!m}HRSP$u4 zK!RE2_Btw1hKp0sJ)hUO(>(yVK(dTjm}$y{=-3GRh#8kE#L>+M4)k?G6tF89Fdl5(FvmjjSg?^nDedmR0KxKc8tgDj3 z=5WWwyDzmE0l8-fw(m1zw6o8JgrbY#v96$++ossY$LK%6)5i@Mf$Z zRtrj>9uw+;^YsQ_=8yLwdx7iAzA<@%9P=%}X8QIj#q+DGBXCvp@VTddaDE+fO&k4j zp%}!$y1=NS=Wx<%I(W!_655}?ImmFX1OY33(}CmL=cISa7>`&kg6Jw&rj96Fzw!8! z#~jgOeQ;-@jzV-MGM0p{8lbn$IYOK?5L}r8 zPoC*fSGl}C-tMmtCFan1W9?DjX1Mp+vbfuWPT=k(^sX2+3=Oq9B`=(0MssEaAL@-f zOpvn`H`vOi1fsT&7ktK{O(MId&o$gQI}gV{^sniAf%5ycqW+{BsP<}#)@bh#+-;Gi zwWpyT@kCQRXgW){9^LZ2EzXJVrtY-aDUcjdvuJ68Yll2{a6M(3+YfGxaYzYTm&2KN zONQ0v15kWCb5t|_IZ(eYB^KGpPEZd%hxR#Z#`za-q2STsfk>%2xMep#sw!)-6A@7F zt4Gf1@8#fX&+}&iB9m}r%;`z|t3&ANW7CCUf`sEpWLrWu)7`vfjQek|&zEj@E+1)_ z#^%ql#;OZvgbIMF;=X&r5~DEZtBA8T=z@=8BH83`WB@38&C=C>`y7{_wl&Cb3-JAD z#$D(^j@iIdV#IIdc*G7(K#kSI!WpWGaHWJaSpQfx^evz@+c&_B2poUgU8g~qL%ykF zD+#F2DcyO004}h3)E`bG$84-$pYQBO%{!%$1rJaj&stUa32Pfh1n;cR7pJ>iGd|!G z380S{Gl9sJ*N?x%QNjN5Y7cYb7%&KznMRW&smO9;ufkP!PrLzTTcZ} zVEsDA%rP@lu^Ce6$ZNgoD}!9O#*Vp_jKPX8t9kks=^*2VUH5)p!sC1KIl02M;hcOZ z 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 I=Y?4Z0NP<`Z~y=R literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-21/inputs_true.dat b/tests/regression_tests/surface_source_write/case-21/inputs_true.dat new file mode 100644 index 000000000..893b8b5b9 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-21/inputs_true.dat @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 4 + 300 + 3 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-21/results_true.dat b/tests/regression_tests/surface_source_write/case-21/results_true.dat new file mode 100644 index 000000000..7ccce7cc3 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-21/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +1.149925E+00 2.542255E-01 diff --git a/tests/regression_tests/surface_source_write/case-21/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-21/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..ed87ce849f788a346f99843aa263565fd326040c GIT binary patch literal 2144 zcmeHI!A`KklY} zzdej5Nr*X3OP&ekQ8*a|lwq1ggLcq|OOs&n&780s#X)cUJyR_U--OKEEUH<~jpI0K nusoVGSFHZC=n6-qen}4IqrvUfbzNWu)_zFyYF=;a|J;Euz71;T literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-a01/inputs_true.dat b/tests/regression_tests/surface_source_write/case-a01/inputs_true.dat new file mode 100644 index 000000000..a65c9f770 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-a01/inputs_true.dat @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 1 2 3 + 200 + 2 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-a01/results_true.dat b/tests/regression_tests/surface_source_write/case-a01/results_true.dat new file mode 100644 index 000000000..8979eb554 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-a01/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.169123E-02 9.144814E-03 diff --git a/tests/regression_tests/surface_source_write/case-a01/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-a01/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..4a86235679085a07458904653a5b63f162059dea GIT binary patch literal 6616 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 LC5*DhS910bE;gYLeTFMBG>ducP#FN z=pYOE7`c2{G2H!|uFoDUCF$kQq)p5?jO)XSGp})T98XL?c!rhn86EsKg(L1=yM3JY zeP_GMnMfU%4B*zk$kp4L4+Xa?ReVw<$cCMt6MYPrbt7=-=fQHE5B_fjlJoV;q_h`# zpqW*-<4XY*ZHix-FR^+6jc%#SlxizMD!NZ)QjgTY++TO5iQa65`8&L0uMN*3vzc7< z8slQ-Luf?TjrIGSNj-1ue)CeKY0MM#FqDdf#geWmZTSX4Ek<2YFI&MSEZCSku@<72 g7E`WJ^I(JNZ + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -4 -4 -20 4 4 20 + + + + 300 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-d01/results_true.dat b/tests/regression_tests/surface_source_write/case-d01/results_true.dat new file mode 100644 index 000000000..7fb415cb1 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-d01/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +9.263843E-01 3.193920E-02 diff --git a/tests/regression_tests/surface_source_write/case-d01/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-d01/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..8f8cd604dc7d685af08c32e69922749b6ad01654 GIT binary patch literal 33344 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 literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-d02/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d02/inputs_true.dat new file mode 100644 index 000000000..6877634f1 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-d02/inputs_true.dat @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -4 -4 -20 4 4 20 + + + + 1 + 300 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-d02/results_true.dat b/tests/regression_tests/surface_source_write/case-d02/results_true.dat new file mode 100644 index 000000000..7fb415cb1 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-d02/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +9.263843E-01 3.193920E-02 diff --git a/tests/regression_tests/surface_source_write/case-d02/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-d02/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..3abc471e043152b09e736a5f09012f5f0a95191b GIT binary patch literal 31472 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 + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -4 -4 -20 4 4 20 + + + + 300 + 2 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-d03/results_true.dat b/tests/regression_tests/surface_source_write/case-d03/results_true.dat new file mode 100644 index 000000000..7fb415cb1 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-d03/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +9.263843E-01 3.193920E-02 diff --git a/tests/regression_tests/surface_source_write/case-d03/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-d03/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..8435e8f17f4284ea1916ca71e048265aea650f19 GIT binary patch literal 33344 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 TcuCG7%T29+iWS#a5jp + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -4 -4 -20 4 4 20 + + + + 1 + 300 + 2 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-d04/results_true.dat b/tests/regression_tests/surface_source_write/case-d04/results_true.dat new file mode 100644 index 000000000..7fb415cb1 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-d04/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +9.263843E-01 3.193920E-02 diff --git a/tests/regression_tests/surface_source_write/case-d04/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-d04/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..6c45d92e6117a936f05040f5ce367471f98b32fa GIT binary patch literal 31472 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 literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-d05/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d05/inputs_true.dat new file mode 100644 index 000000000..865633fbb --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-d05/inputs_true.dat @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -4 -4 -20 4 4 20 + + + + 300 + 2 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-d05/results_true.dat b/tests/regression_tests/surface_source_write/case-d05/results_true.dat new file mode 100644 index 000000000..7fb415cb1 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-d05/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +9.263843E-01 3.193920E-02 diff --git a/tests/regression_tests/surface_source_write/case-d05/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-d05/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..24b04a862e42c6038d6b0e65184b43715c8145d3 GIT binary patch literal 33344 zcmeFZc|28H{Qr+KW}at`d7fvo_7*}!$ZPOCeVtJ}!c?L(@+cPJ^=QT%z2cl9{y>h(p}{e8e@ z_4tnMCMFasXX4k#x2tbc@Y4RhM~5HyzuW&`dBDsB{Yxs2t5)NBs#w(-?u?^H0{suT z?sswa@%rl!u1f?z;s529ZB?(Nx@M$MS-n0z1>MSjhdjJp15Wt5;+N-G*`ZnaALsx5 zz5e`7{nxRCRsB=0_6$nOzt2=y{Q$mQN1?Lr&mry=;k&PF{a60~KY4MlT>p=cg8olV zUO9(N6s%bB=Y>#cY@pEiz4G}JfBn5OEUa$HZT{`ECs$7<9!>uDEBxDM16NPO+xUO~ z)Za(`zii{mVE5PSZxyZn@h_Wy^G|@s;lFgI^DpPGJ@jv%J?<9p*D1)_6aQuZ-#&ZL z)!X@m>%skQ{zn3RS3YC*FP~d`h;>Cc>;F0+N^vyM|ImJqgA|r)PxzPpwV&~I_7Ctl z;QddZviXVVR8)WUD7w{twvN(iRnA?wy#Klc`>M+SoE)9te#myK zwK3(&6>#nQuJ+fpy#KrZ-yYE4C9Ttcf)(-n&2N^R#*H!Z9Tdt^lftTZIGnj0`W!s? zb-eJEZ!a*qr#&3tFbqzzyWfp#WJ2>i>i-<$LdhI_PyxDPf+`jEKah1Y|V^i+!*1POTNZQx{RkAd;$>u<7Xu($? z&AvnNh1Li-mvbifrECl6ok}P*YS@If)cj~(e&$S;^LIb_Up5YzarV5j=RootvgWMz z3S%@(Dm>qc8L^w3Y3z#@Wq@k*q4=|^3F!QS@qE&$kFbWqWYE!26y5zqt*W5Tjx6WT zob=~~+w;IxeSAX^0b@M!Tirix9txC1&bU;`V^UHLoL+XLu+b6O{miBvUNOHb`z-Ve z&kLEFn6T{9%H6^B8 z=l~-Ujh9O620)Lo-$`AcE?Av8F-YkmiZYsBWISPEO2omx`=9o~`zL;^uQq7*nEnDt z31JJ&$|qnp8lF>*d)4%Y&z3>)DQ-jbGOx z9*eJMuTf9J$nBck?rFuK>_Nw#j;v2`cwIe3Pn;%tq+aD@+kQi`oWBG9|D^+mJ#*@%6ad|!N?4OG>+99?5oFMMYtv>bp#=*Y zp58P!C(GIJk0pQalp8UpNjw~euXAR(AC40+T^-ZwY4#hinpA64exeEFMm)~K zj9EWBT;gvdiY00B&p0@*!`{C>%=y0I1xyL+mAq0q2IEVp?lC=U1<^FIea0IRG~&cN zr9rnXWH~G(9I3bTzb^+0V^+>TIDAz&uwTa5$nq{mtmX6dHyKMk@WqcByOCRcu(sMt z)^el+D)E$lop_3%IY9+N&I!_FIi&Gf<5$KJH)a9Mk(Dv?Y5gzAdpMlYdi?@4zV7x+ z@jqKFtmC78aO88O!FJL`W+ z0h9RsCG*AcASw|HwvkyB7)Y<1 zZTm=%-D1uQDTo>Xw}bY+?T+aM{Qg7c3W7~AU`O%G5N~7DGT;5n6_ZdRd)E4m4|mw= z*5Uk}@kj0iq#GC?Y_O!ls?F(7B;@pfBn|}=9*rJw`-kA06pk#=vENuDf8z)wET8|d zL}P^Rs(9tSZzO~)ho6LFzjyju|lGW;V-tH&tw$g|Ssk`ucLVOFPhtD&-w=&WH8m)vv6(q)`T)5-T~gy+rol*ZiXc!Bu9U+YSY&-A;?emLgN#nh@o=sqZw8Kq%?&lA4dt0)qb^X2Ns;5xt= z;eOGadmLPu(-t(yp%~`7+tM!o(pdO&M0fY(haV?F1eAHG6JtJ?$0sqW<$S($zPuR96**MN@CA#;ep(P8O+$n zdIOMTE{b%&9(JLUP(uQ(58bJX`2;8egzs}N41$#H7js{3=mTTFiynH0@uRo5A6}nr zW=WPqAh9QJi@(iDdk*Z!Xk1M<;6zHE2xi@nRY9&IN)5_rH_;<1TSakq>JqO?Esy(J=x^xxan z^+|gKC>`;9MR=Ms#A+J!{rg0h#2Ox7S0uVXglAW`q1;@Z?*1Djgp$N>h>K@T1R)$ey+R zRE)%Lg>4^}^>Y{zg}Zjig$%sNFS|a0rw9v6uuqz8cp@ zoh}8eQ?4c{G;An1CmObm(wnS3%Bvjw{xMSf-WK|Id{~I;=LjbWK19>`jo<}V6=cF; z$_ynA30*MP8zU|eZ91fm$6NNwv?%s-B*wR7oDIplc$u9`vm8#xbzW_FI0iEg9sTa} zy9K`BaDK+ZtBjg|e!6S3G5PyOH4+{CnqQWBc})mmejl}MZv-dS_SD)@KTrzu^b443 zN$3G3a|Zh)2D+e9rrR!-R^y^m6oCXTY0xF-z>5#Pt&pF~+mlWaVUX0{NzV3QF zCcT3f>zD8<5!)z)SzgI5Tz)(W<{cNsoax@cAceT9s_|rSLWc8W$xjrm(8?>FS0bN3 z&?T{lCXdaB?>H+aeIx$U&igWNhkErDJ)7ixYEQsX^ z`&RpT*etb8>hn<=Bx*yl4vRq#%uhO9Xyo_>sJ>O#py3~e1M&B?)D9b=#l;Oywhzeb zK%c~K+hvdTHtS9TF+UZanmz(XOV1)`eT^5WY^J5HNCCXz?9PRg&OAp$e!hkNo>?cn@tk(=>m!KJ8Q?CjU@I&1%Ei~6*33b4J1?R zyjh8Rws_vTI>V0ixdcW!B$vR)F&DDsi-tk}$Dd7_cYEOTWYJrJEE=dSA#OuO?M9;Z zS<6{{;~$z!{iYn_aRJ2d!;hVg3SGdv=KjEVqdG#hLGcr}X*KNYzTYa8JOU4V@KJr4 z*a;e0Ds|3m7DqJ~d-ne>H74TVKKi2rK;pNF9k;pU73h(^a$)x0RD8&QV2kTRCRXhA z{$upV{m0-a*O3bsW-DQ!6U);dDlOo_w~o9vK29`U)M{RMf_R==%R&FKXe-&gc~Xeyn1o`5~xTG`Wls}q!}x3!$E z9ff7v9?ieJ8VeqtkMukyB93OqF&w9vB%cp4CGlHJJw0XUZUXl9n23D_>pG;pSo{5n zoju^(aI6x6Z4zSN*f24cTVPKk)edl>3U0RRo1;%gQ2Pw~Wt(bUvi58y@!O7XZ=)TV zIIsZCx%jwfVN7D4bMMDDN?7CxLbhXg22iWtzj3^H0!nXD?K&P^1KVq&m$oPvp<~KQ z`~9?u?@!nI&5VRY?PRK_Im?HnaIfDG_Kg?gyD1Pao<~3wM6)Ax^H%cXW$B$-Sp(3g zwm|FNd;@r;yf4$_gd{5P^x!%tXY&2WoP?8Yd#lNG4<}-{utn>t4hN>tUGW47XT)w_ zOP{vBTLYJQTT6qpzk(5Egjw&^C}_;uw(r~zV>Hrq(feK`ab9_?-nNi%Zi&zEL1qr5 z=ha5Byo)Pw!j0g4b{vw(lGPX23+|KP_$aGz(cTKE8my^H85IY6jdtJpt;vMiDqCbH z#FEdG;~advo?gl^$#>>J2z^@47WTrJc6)BZfiY>scK-*(D4Be4DExEl3@%J+ z+{=Mi7E;nB#JUhqnA;x-e97v7lrw*aH`?a7AQo{e$@mpLHxk<*Ena9SgH&8PsV*zi z1R3_7c;Wn}0VKM4KAkNa1;au$vU~0*p?h4^C|u>XlI7s*VYn|1;-1ZZE+b&-du@lj zQ)Z!-?6!+1Lue6OPS2sKu`wv{B3D^Lumy;}<)jz<^d8V#FT>lfW^O_61LYKlI%dAGxD#6PmvC`l6cfcCz;DAhE|m$Sp0(iVquq zdoOEXkrrG3$ZtA0OabdXwkWD1-3RQ{P8;b4-Unw>vL)$jhXAbiUfA2Oh+b!B&CHDd4W6|GV7lAoNo1@rs0@n3v=VI0!4orWS zljGz?3}lBz@Mb9Y!hsnqP|I`}++7Tm5IZP@nsO!4pT`c6bb>u)0 z26qHzP?=t}MEMN;u;FkX*jUElYU|${nE%4c9|*O z{st)4=r1JqoXK(F;p~J)kejB zQxvOfdn8j>q>62~-FKu=up30qfqn1&MxbzN4$q#rGQbzUPBYYC)}HSq5)aEtI=E zhECm6L{lzr`^DEooDadT{bzh5T|d2lP`1uylml5m`Mc-(Qw}7qg5`S0jebya^;pJ^ z(3SqVWH~M4)(@R-4=)FSR`@KP|F`8n4s_QCjrO$N#Pb~9_G>wKI~hs|92r_7U{<;( zxu0&Y0VeiAoZ-<5*yDBOk#gH7;ASBmn%}4Dz|)?bOP2@p!GXeU3VU{$p+Y4u3p#$1 zZy$VJ6Zf03J*S2iBRx{p+_6rsOBAzsZllTEt%N02ee#@?t^- zUg$z|I(e4S9IbtJZ)0dQ`T2)*T{F0oCCK4}C^GVLm)-AQ^jNxX=~=^{>yb^IGv?MG z(m_>`sfJ@S5-*g; zzDggKMYk9(v&c4)k9!$c8wMZWq}O}gK6QfzGxPFMJ<3DCu7(buHKXa?w0N4JbY zEnAt0+}H-VcuR3qJi8v)wLQpAlu}0-PhC|huqU4n!Phl$4Z7T5Mj|#aVr|ATxt0dJ z*lqUv-*qKuv3sTzn?CJWS&tbzQpp}s0R|YtBKP@ag9fRv;~Qx;(XwppVkD~`k>BuZ z{?Rq*y5_A{1y8ukmmm$D(iYcb0*0DZt!sNJj2!U+@@|^_FvNRPccelqoMFfmnJDUo z^#dJut2eWwV;V1WLYsFJan|-zoP+x9#v_vNN7a#V=^9xa}(z+|IHqpqa{2br^U^RHGVyl zvz=cQ+iEf+d*Xs5W+5+57kQ@g8{(hRg zJ*4ZjQMNw|0{vOBC*gLR1`hFJf6GJzbKO}&=i%@I@@``aDLVBEY-%2i+gdRRKbeHS zh`2S6R2geI+655zAAC&v(>|n}8G5JmYp40KjXQOY*@;tOG+sC5+a}gywfdPYROyX? zrX!2_n_~^=W4)yiykbxJdcUYrN;R}ZCDF7sfjHh->o?N%({9edcJr0-%|O=eR`1g~ zc;@^&BV>pZ<36LX(K%!oTD&f_Yu(umZg$7%P_xxQZ5ya>5-Nm-#4$vqgpprw#Me*p z@hx54^5e$|0+zaYq&%o?2*~N{=165rV?O&7sZ%a>K^sDBP~zSeFz!ky*=;%m<7{UU z2}4cv0ROk~Q}y;_^~Qw*R=4%ful~NQJOHGtA1a!!+y`&<>0Kwl&xKv|oUu({tcP=y zl;6X{M`7-bd4GqGEl_Zs@Zr%4J@h&zM|dGXemvk_y*}{C`` zwq;NwoaXnlJKOy9eUI5ahb+m~2%S`E5@^EIJf zM8+Jw`tq4-^o>xmej{DieDU1R_HCpvGDt1(V}NQFy)C(Xco$iY0ErG_$`@byL<*KfJar!IgEQ7vNU@aW@0wsu5cU>e~@rc$5yUh(@1Z8_faz84yY zMn5`*hxyY0t;J33(PSs0Lal9aMB0tW9^6NN#sgB0<8&vJmp3)C5c>L}i82Aw35jC3!5q{kJrJx!?%zft>Zu~Wbjz_tqM?bpiIog zVFbA`Z~Rnd=JbDZa2pSvrLlbd{3}#*G4(Q>7C<69qnjKTnK35L*k;z^G3c3+q?brh z3y;`QY7M0|fc<<{2_XTx=m`ty>p~yNuRDv7*i(uMD)>DV#<+DA?yC2l(phK(6`gmG>@dg-kD#AM}Kw$DZ!(& zMceyU^1Z?FAv0a@JbTiV3Zo{Pv#I+?Pd-a1bvDR~*A3uOgNNRd%*>8mR{_u46H2LvOl*Dg9 z>-aeB8&2%!5q)=Y4nE}Xw>6ONu!$`VVb{Qgg7IA&ynA4)%h|9TnU(d&aG%T#>erFb zQKt>*Uyc�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 + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -4 -4 -20 4 4 20 + + + + 300 + 2 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-d06/results_true.dat b/tests/regression_tests/surface_source_write/case-d06/results_true.dat new file mode 100644 index 000000000..7fb415cb1 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-d06/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +9.263843E-01 3.193920E-02 diff --git a/tests/regression_tests/surface_source_write/case-d06/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-d06/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..3eb95bef96a07b77a50c9ef9df25e0a849075c08 GIT binary patch literal 29496 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 literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-d07/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d07/inputs_true.dat new file mode 100644 index 000000000..77498b64d --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-d07/inputs_true.dat @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -4 -4 -20 4 4 20 + + + + 101 102 103 104 105 106 + 300 + 7 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-d07/results_true.dat b/tests/regression_tests/surface_source_write/case-d07/results_true.dat new file mode 100644 index 000000000..63ea1a64d --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-d07/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +9.756086E-01 4.639209E-02 diff --git a/tests/regression_tests/surface_source_write/case-d07/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-d07/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..0332f2dcfb100123fe3a4ee3a47f690be2af1bab GIT binary patch literal 33344 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 literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-d08/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d08/inputs_true.dat new file mode 100644 index 000000000..561a38337 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-d08/inputs_true.dat @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -4 -4 -20 4 4 20 + + + + 101 102 103 104 105 106 + 300 + 8 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-d08/results_true.dat b/tests/regression_tests/surface_source_write/case-d08/results_true.dat new file mode 100644 index 000000000..63ea1a64d --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-d08/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +9.756086E-01 4.639209E-02 diff --git a/tests/regression_tests/surface_source_write/case-d08/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-d08/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..2525592045ca7912138b0559d72d9d4eaeda6118 GIT binary patch literal 33344 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 literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-e01/inputs_true.dat b/tests/regression_tests/surface_source_write/case-e01/inputs_true.dat new file mode 100644 index 000000000..99a5a2899 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-e01/inputs_true.dat @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 4 5 6 7 8 9 + 300 + 2 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-e01/results_true.dat b/tests/regression_tests/surface_source_write/case-e01/results_true.dat new file mode 100644 index 000000000..8979eb554 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-e01/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.169123E-02 9.144814E-03 diff --git a/tests/regression_tests/surface_source_write/case-e01/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-e01/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..3047519e8c52b140a4756266192c9d81c7d204fb GIT binary patch literal 33344 zcmeI42{={V+y9UGq=b@?%yVU)leM>5k|Go(LZviQC{ig6Xwslj6qTYR61BFB5t-+C zo`+;ehCfed>zw^Qp7-~^uJ`G>{@4HYTu*zSea=~*^S$qT4STJ7?F&YF25hW+tfZMg zGMU6k;`r+n|7VKwhn)U(1%HjdZ%;X)qa4puj!)@-ozRgONHg#8P_A=O$`@kzbzuYL z`X+OIebV$N@$&H_Dr)Bo*sadmd{Jm`!Uzj*qDdHR2x|Lb?n zyk`3A+HFew(@|;$9o?@_uB3bcKQfTy8D=hVPY3?)>Eq$)|NoOe?&^$~>ngMV}Wci(k#c6IP{ zcG|w{@B#OO)88@ro9|Iy;+PH`#=kBIk&d_@-nreyiL{RT1AlW){hfmjhuvHpU1#gm zhQIkd^(E@>GEGPAOueC}r~j))kty|zfzF;1&h>bB|5d~yN|euh`1$SKJ1y3m>CsJ> zfXDtpN`0l~{qOmIZ-GXaB`deTY68_=!DD5Y2EngykdfZd>*E?yAK*QiF)NGvnZUD< zofUTwUo4~m|m{G6Xve+7+M=mse{W4#gM$fOh zK_boQS@VZDlpjE#{m<*sArSs8yBYpH2hlT4x-#@LAI65|i1A5`!_iog%+Fu@K(X$_ zOZ)}z0cOJ)NjAI(Oa)iG83LP8&EC{kQSu9D{KG^kd&WP;e~7d84{=QX5a;jPd-DId zf7bsYJ!XH1v+)mc%>NL_;tz2w2^`C2eN#n_*?tFms_)T}WXXsft|+Zvy`U28Ztyu_ z*;fJ2X@qYQQ|yG-4P8_R4x;F%_HDi2)@my=|`dTl2k zG>Nr=+-$4hVTTqd`JnIp`HdMcyD%g6&_#39@!?QvfgbJs4|V-n{)ae%e~2UWhd9E2 zh$H@oI1+z|Bl(9oQh$geP2l*OC2Mby=mig+L~B0nWkPmTB5|3e)6 zKg4k&a2CluZip%#2EQ(ov9q3|&Ex4K@M>(n@{6A>V3`;jN8!;1nCRW=W%#xhHf8HD zY$#iemY;v-w2(uPCa&>&gqgUeEXU2#UeT#&R^-KlS0u5PnC=0bhXfo|d9SF^1+S;r zCS6?{1!I!eHJnbW2RC28A70zcfu8>O`EKJs%=4|BB~YJ`ku~2_7S;5BgRHx5Vhx#0 z2+z~g8cN68K=qWUWubQ^)c+Q3v|>#I5QAK+6;+i`tcc~A#yKe(J=FavCw1|&=hNdi zw`&}nMO{!WQy2B^r9<-76Q4Gl^Z~^@|2-WQpP}4#A+PHp&w)eExmVs}DyY^$`tNIF z<{tNQ&z4PnGR^szwrx3U1MIx!cGkX)4m((@Z}y_B4{*mAK21w~0W~<5?lNqRgH17) zZk}f_M;8eD$A12Y@qqw!@w4Y-WT^hzNv^UVP}VZ%P1F)5Y_piPahy{*Pzmw5ma(@T zvZnIEzSw%WZj1Kj+b7ncId=%nRweq;OxDr;Ag4b z25v4;{<;)pL?}FL^{T~>2jhw%&ldA}QDK!JG4XMJ@~`>lnen{PW@$OiugsW(rzBZCuNdrQzH)mRUoZGtu()-o>K(i-z#UuY zXM!FOj0$BA0`6!;u7jvk?*sdw9{I5L&8ZI zW9BWo_foAO6FGQnop=MR<7ioD(NznTUb~21Yh8_6Z8!PA;weO9H|ln0Ar7D%kE&(~ zjvQq~Bs;w}PkwELzb=!p-F**NrIA|znNFMQa6~awcr$)!c}@$o3l@J?vfc{^t#F zs<(lSV0_WmTFa3N;FERYp5VP!V7^~z=fTI4Xk`KG-ZO;unwszQQawCE50TVvnB z_@JTvX;};`ycD_CB&-+IpG;gYwXP5>?b;cCrsxUO6&k;dgo&XKtl}g?{(+-Jup66h zEX1@LXXEW@`uvGY%WeZcrNN8e!{5MNhlSw|%|5s~*stx0bT&-O^C*++6Ga2u`dMWR zXz#Od8_n2Fg}}k>rsA_x$oxSk5HRo#kshi7=jXZmbO!YTXYX{$LZ(C@X;u|3)mH#I zi`c7{j(!a{DU5~zvp1ymFKvYS`GlG51jdzcXmQmp8L5g)O+DKwkro+wnhTy ztdvco<;8H3bm09=D}eIbe!FO;G50#tY6=JU8AT7~eQ;sCU|%=5)+oZe zIMxf}nb{wn{Qd|=D>!|$bSr{c3cR6Tra7?}?2}iZb91x^oE2K48j4#}@p^;o4*J}n zZ`}{s&(-oDVn~Lfh~vGm;V$sIbB+(y&3(auq$n) zARA4)gWG5(4ooQ=+IdAXa%C~2EeLoIZL`i_F-5;aQ#QWb_;1~yqcU`hQBpQYRL@%? zlvxHQb(?rkv2I3RnvV&uI6F6oxW;{mYh%>UrG>L`{UWXN#-wne!V1Per9)WmG4Od5#JxO78?`-hE$OgmR$|dZv%H=xJffqW3Y1{U)TUH z-^gC4TeTQX3Q&_(*{4Xu!TZpeIMDlh4pQ+k&VF9XE8w?1sno-60%nwD7n^--0U=Z6 zukJ8c0uifA6(27)!PC3CJTeruQBNmzL+*JTG#u)9+eF~>vK$ah3K{`Imygu!%>D^+ zeeo%l7xWJlftFjMz1*xVfN9zD_X8ZQur_xxusvT6RX?=BSzkethC|)|{FNL*#S&qi zS|~SfYSusZY-&=DmoZ>ErO$F=Cu?9O*S;{>rJazWS2&PWM%*8*EJPg8EQMmSHpR-Ttv1bxW6LtUQG|4{4En1jQA-Aqgb zVMKOgtPT~b^jP-t)q6KHHv^tk9|fBI+JS`jJiEg&pWq$4kGt2t5kphZlX>e2dZ;;y z1kRX@6njeFBsemPalB*t-7Y+pxL4ldQ3(75i{7)>W`edBsjn;9dcpqB@!(yIB+6lw zGyIXjp|+bcfioJtq?`NcI1sy>m}+=(wp~C1#Jzv&jC=&O%43ph7e9lB7dlt!m-m4m z679BH-4ZBi*VvXg5!!x%nuAa{xW*+>Vtaf$dT0G#d3SktDK{hb^;?MjOYvO56m7;9 zY26LdmPVO}G*`ey>`S+7=UInxIXJOKbk5zrIZ!xwtmi0vv~PVfJB~+6b-7fRBXo#x zR=2;H;1{^&SxtH+e-ki?PV60zZUf?X;z1WM`WruVP^F1$>UNZaV7Ff{$%uVq z_+ZD8>)?w@m`7&{3!-l&^25x(1)dEvPOdrA3S_T7?>9Ee1yV|V7u)ic(5BBGxiyCs zXgIiyX5xB|d4Ft|x|!E^)o<|jPn%1;Pn$u7W#|K^iY5@27$i{Z+z!c_%*DrpGvUiV zdk*XdC)%d_{W4p<68}8w&`U!3ayD$5B;O8u-gFjnO zVyp?+&r5HArcn;$^Sjx^YFps&ic5J`r?Zg346lnXGv(*xY$5n3VDamlqvs~^@{#QZ zy<^tq)AvE{hU%v^ir|@}pYBFak4trelaV!QFCnY*Qq8TmbkMt5>(hq~#ArCw^@lhP z85=Zy#~t1=%XxQ&;qk(SBk=KtWhY`9r}NdWJULVA-2xciv`DZQR={U9alZBRyr^R9 zzRXHO`$o-?qv)CS)57(`qTjlJLH*)++K=h5UvHC<>LsPO-uD)Qr&|N}xH6Q(sf|}} z?d9(VFWe_ovfj-{HQPIuRGA|*dZ^$b#mYA{SWfZHG@4F>upR&WENLHFVTK;PnuYrQuNP(I;?D-SzYi*wq5DHSb5$ z*3QNOqCC1#ZsJEB%us{Vzu)Or?CIC#E; zY-DKaEwK*}x0lJu+_ZfoOb8;f(ZqLkGfb9qinC>;CW82DxIHD*Ca=n>u4qXT2t+O_GBI?J{Fn8BiIdJ z1a)5emV6iy6h%Lh!>wpI)a{!AMGt<62o$Qz_WlkS6FooZq%kA7F1K@UYs5MVK)qxw zi&{r&-X?bD2yPtn4`vniRd99;C;ceDeRs2-+kNh$#iPj?jf zEU1UyWu{D?vb;n*S$m31IXBaAsO_eQ8;o)+x8G2(e<3~gsnUY2ac}#qo=uLCY;H@s z;7Vm#)dQMe;menrSp2$LI7%OLPB3l=S!pDfE}65IhV!53xx?^a-0Nxsc$N`HZhXOr z{C6CeRSZ6Rx0%s!sPz!-hS+=(&^fthR>!3t$2EsCNZ8VYElirD_0ZtVj)Irs?ZD?D zW9(p3IZW)}ojO(AfYfG2Y~fjLO2eU!YhpiVL>H+uGM^E-9=@()X9g1%h>VbcT<>-#3sm9GHSSMd(NI46!;rNSd@S^6{_YP-$0 zpOiQ_guML`bGZ}#dPzoN_ex2~^H0J>EJp5vLagtZ6v^WH&ecq29VLLqIj^M%#N{CJg6koD?tio2Z2Q=grghiEM@)_Lp>; z?3;lyX&~~^P(Jc*?fLARFKuZ!)bXZE(SiHv`j^7~9=lPvnJ+lec;0D#V%5?_Pcv?YtN@M*#;= zj_y8Nw^$0z&MPA~6?gA$?;io3&Jz}$2VTKZkMV8c5q&_}@QLHHQ4Fq*e$w%ZSpt>2 zyG$hU7Hz*k?H^(sB#1bjuFwAouHB3uc+dJf9+1T9A1-PyL4|XK(Ua6q@ZPqP3eufE zkaV^&D6vokmD2S^I!0;lgQ@+4#~v>0ApLGR^LYh;&rGB&wyAH{ZZ}mj<`wK|hUwmt z&pTx5LHbomYyR7f&|$a7Qww{2)I@dV*MFMt#chalaH!(|A0PZG7yp-x`3~F(6s&0nLc=^u^{{NHeDdJY`~!V(z+#2c zqNVBZUhIQiOZ?-3q(3Q{vj9O4`1>~Nq|n|s;=Z5JL!3XL=)m%FFIV;k6~X=08*KX{ za-hXa-W7J++u&yb$@aZzEpXF%|1YeMV*vfl)h0G8*w90-o*qhwp^a;54lz&Y{0{N@ z&8Jup3J+0@yW(kkdK7T-v8&c}b%2Xml6;&zHQ+$=Xt_dM8Sse8fuA=TqBqZ(XijL& zJ$@6!O+|AgV*>5xc*&$eXzF7AxyHK(M3V&0zjADaQp$XV!O_hyC(tG6mUj_2#$7Ba zU1Nye{cbNJXD36`?ojI?t^+bI{qdNr*#vMsyVG*E8uv^>c8LIv2kNQdHuk*F(6JS` z?fqbQ)20n;aRO(h_S?O0_xC{l zl=<#Lnf(yg^+k-jgt1v&?sD}60DA=d7==QmQe7Ge(Ds^*7 zFHH3{bxCI|09h+EFSjBOp!nf=JoH9u(KE+lu19mzw%640M)?7=){DvAT>7M`Z`LpO zy2d(7-qGA$}=x2FLUUk zTqFJpLw9uMw3{)3uwiKu)2*83Sv}1UzIb(-w?QY-B|f&21(28hx8!Yp)Wq#UT z0PaD_NaX{Qxu%`BXvD+do*~81oh&6!j?*Jzj*h39lU8C!pe2I4SJdz z-E=%&1MgfpdT{_OMoo%uH}V9~u47Q^aUgIg{y{b@WY(9?oC3#Xu)ULfgRt4&i!QmV z2Pk@=w=-@RfoRd=U(y;gK!>o=$Fa5Y=$F^u-&NR%)7Xu=J#Zv&OceI%g{n^AI*`a+ z+^&JQnxWQ(GyT1s&Ct@x@Ls-1KKQyv{!w3W8}#?maMaz?kL+>r9a=Z6NyDLzYheQC z#S#X#hf&N3u1iDByhi)sH*n1CTJJ`!PEb<)ePn(}1(?@5b?umXC1|<8>@yrHkH(A^ zwHKz%-5!_|I6{NN%PW!wKnZhIuNQ0QY`lePv_OrWtw73A-=qA(Enrmkv(&k)71}S+ zT%xd(1--2vmpQIL%c1s<9f70Dwml?CYaDo8F0Yqz9Gd0WO!)?}hkpf!U0(#%wX^}( z#X{#{N)vptuefEA%6gRV>q*&6LS6thhj_o>L0+m>sL?R%pPs8@Y#JVPh=ttQyNg`w z;MU_gdp7KA2dX04_Iqk8!M%iz*7H(gXjJ91*8YDOzY+8OH(hpYGxH>26#rlcOixV; zx|5JCj1SM2Ry9I%Z1EG}?siBQ6_J~HA_qKQIpy#)Zyg$t85I7Bz@Z-Z66ax;>N+g5 zikpJwobK=E=lq^OaJ;K|B<5!$RDPkLmHaLWCOco%?0?e%9OEOqjnp@x+RiJY9>mhl zCsOy@5(NMFe6RkRaJd5Zok5-}*3XXPFuSi$Cw8xbV9;c{SanBbFU1(&#>02z3=|gX8632 zWwR(>JE)qU^Q;urL+`ug$|$MPa;WtX*XJgkS8#6qGB_ItBiwshB<<)CjC|a(&rwJ67%`~EzT-}fABb4v=G%3omT-i zr}6rw%FrQ;MZ*+=Y^ovV9A4(d3!%+Xt_(NVZt(EXg`120mC>tvyt<0bm1)`?>i$Qb zz%dlse8{n37|PIhFJTP)y^gUp?CDo2rX-koK-A5*y#=0g8rmh{`zz|pGvjyQ(>grQ7>I;)L;uLEKpQ7U$a>Yztq zv*@F`w_w?&=77}XE?^qrUce}(k6t>}A7k*4_CAX`-sae^i7{QekE*fN!(cPkW6RI7 zU@=~Ircx$*A#z?zMs@xbD16~kci*S4FnK}x!$;cyx?)++nS%@F9w!p>h=Zee7O$bB zLvG&)G<|2>g2y+ua!w2z&rfTZO6zTAb?Fy@arfea@#XGt>I$>06Tex217dmXIJU# zwRGEo-%GvNYb7iwM?Sp(OC;?)IdvTH&cW$?QLs$hzYczh?OwR_*${lK#(&J5s}U%K z9FI(CX#_xL%eugWjWB|pafm-{3>j+PW2SqIc0Q4sgP-F#wNsU)W(i}^FJAK}d-d=8 z$0`xjHkzdfhTf1cy0)eiwuPgn^zPk2YE9{$Q%468p~bJFHxTASsN-#}yjxx0V-c4V z$HB)Ki&c-dq=J>pQcv6OZ30a>Dq2mQ^&k&BX;3}U2t)-AZspD%K=>O-RUbWR>nW~b z=01zKzK#25@1zVr>BkSCc-4@}&wC24*f}O?T-OCs@_C-`E-wXclRZ%QP6<3)B$fQ& zQMsEce!dU>7YV#{wnJTaZTO6;paHcCqbC5Vdd}p zZ04&4NBnYGki+MCz>iJMP^jr~cd0@>aM6w``1!06MB6aTFHBKG$$X&U@&(%a1?q7k zaX&inG>kU8S`DG=>WvbPhZ`jqV3-RyfwbK%k#Ctg=~_Y`%4?48gIf` zGlMdyNH$Q=I#i@1g7M#iA+&h5bp zYp0)oz>&GW!8HN0xxQbRBA5lNPQMim=afX<&TIIP2>zjtH@q(47}Iek2L`|I5NSu1 zg1bXz<6uP0SoAGvAFM?rR*#>rhFkE; zVIK3iZ+e`lgr%>~W(Tnj0F&~v6)P8ZK_g)pbNX98WHdSUCE;ZaDCBmrmM=F%T}_zh zq58D4LIu5RyZ|1?GU>w_mQkT^|)4cTys#(fG!RF-3 zp809u#Y8OMl#>ElR;4wV_+jpRB;q{zcZt~5?PI^=dcH*PRv)GThb^vn$WQ0@DO~$jZNFt6D)Kz5XvCFvJ(xPK@qO>O9)ZrlTq)Ku!0#It`f70* z?jPivAWNW%_JVVL(yMv$R-unl2Uz#5qusxN`+g>_HEHaH zzaS&~++W&H-5UjpZAv!53MA~)yF>jF3BBM(^3Vf2{Q_WF#ro}7@Ee#K=hYUgF^)V< zTlD0g=9P*2oqv(Yi1*S>&x(*qc-(EV@bjT!aC31S_Q5C>O5Z(gs8Z1b z6`Zpm%l;-J(vZDi{`L&FQ^#YVWDqP z0JPZ2+UwVMz|B7c-0!3}LWyMiwcBknU`p|og_;3ODF2=4TWd6F?}Mr1TI%mP$YWzC zsnF6Yh}z^$XQ)}&6r5WoHY6U8@BXX&-ed-gQ|A$F%Cx|;y?WM*~>2%^om zq_!Kr?u@frPLAI)9h<#hz#PTiirq{c0F2QqjI>k=;YF<|pp@7JH(X>15z?;&`y}@M z48J6YE~!tga^6onPeI+jArw7$T;~Uh3ao!#3wy)Ydp*$kj<=@>TbwrP70?Y|9@SXa zIGF?))e{e>vQ>l4MP&@KVTR}^a$k9zXYTcFT>|IVOEMBRa5}tgUkY5~K9(gHSO?xP zOHFi6uLqTvvalQrkA-KxjD)XW{upw6aG!TkTpqnYeB*l-2W|UCZ8tLthxS0m@*I{Q zuHqR5pW?6Fm{-{k^DdDBzg+5qLowZ-7ukIV-}M|;HBZ&R1D7@5a(@>>J7$s=! zhWlcso)YtWD_)$ik-6UvN=Uw7UoRa-(T7=x^lK-dOajKW%6#wlRKvr10%v0wNB7>M;p{;KP~>lx)%5)k>7;wuLI<)gj!El9rSr&B+Vy(e_cQ*{r*QCt0K+%^HN4aJA=N&y zpYG1)31N*ALhYTI)A?da>?B>~GDsIJaQwu^R)EfLU$Q8i8AUH1yQWEq18P0Q`6^sT zbKi+efx8>v<>5D|v%?5{r}H^D^jbGyS9fF5KTr)En4~l#-^2nWL*<%rNIGJg!{ftA zqSb@@Vnz>feJ+!D0qZtPeT`6&!*x7r-U4 z+A)*&tmsG;cbR#t@tor~Bivw=$PXRi-T z=hIB6mOC8_tAS5XtL#vDJ&Kgxj$!@bOWQ9{`-hlkcMOyj`iG1|+@(B zbAAD~!lQ&4d;WU=wT%QWW;pPTJ;e)ozt{!JHu*Pp=e+YU1K zg#ja(=YjX1l!0%V=_*F?J&rCDSJO&Wr1X{?q^lG)6inn zYI1cwM(jF<@nisT_1YD(N}s%r$Gidd4#^< zYhf{nEm)WJB}Nr(&=);uCqo+t)crQD4fomA!6^5gkH>&3%v4#!#f(t6SiFwU4y{Ld zAbWXHf9RzSkS-;Wc|WEBj-Eaq?Q=;1-K`F}oCx)Y+CTX7gz&tVGe>^PeTSnU{lbk% zwuQ5Cfb8t^+p5%G4J5Hki5uw5K%sQ1?T3rq;9bjy4C}{AsAAKmZpMF@4N;b=Fge&Wn>{c`_L9I&bce-@Z zu4_{3A@0+|{gcc8q?qT!^nNq*FQ32$2Be{)_W1mfUeM$6X4@@>bg13&n0cpP7D%+R zmSeT&M@Qee$n1%p+dpf575vvTcV|v})0HQ!GqZ3V$k?Yvh3fK5nB}F13z%+I0GVKw z1fz%o@FAaz^TTKt1dHqgew?3&W-v<`3Vf!GH{AC#^=Bi6gCCH4oOaeH4$X2to@dbK z^JxOiiHkbdIrTt;n{@mOI?BP6ERWl|y4S!>SzlEsC;*|C*|2}3;K4{`qww;kK& zn9kq(SLS&g^B+Av1++w5;ZunkC{VkbZ~yeV=8Z0^kf@#}@c6JvL6HcCxLVv3X`7(! zf2j4~`vGx1N@H6?e~QemXCfgNop-NI9fd2F+}Kk3>I=x^4vO6u+ySPl9>{*0-lwz2 zIG?U{rx5Bde%^3p-rV_#YjFV0k&K{yJy{_l;}Bc6@ksE+9`M2A@I8602B7wgz4gji zE_l6pVI|Vq0h8Ik=^zo;k=&$pA96R)=A}{BQ{wZUa*Rzjc`Gtwqc>#dXMFmdXXmu| zZmTy>0i3Mns;tj#0$(KBSY^Z8;Kz)<%(o?#$oe%A8(mHt(fEftt|e)7kVp|{Ouec$ z&Cc&2@?L>DybOKd*f-tHK{BmyH2Y?6kZ&E#J@S0yaBV&4Ml&wYeFpD3i;mE6 zsQp8XgGsx|$2lUE@SaP0k9q<<_UmVmF)znXo6dC&ponhFbLHEI;9a&7uW2d;ZW-#jt@u+(mZ~oVSrLT!jV+GC}e*d2w$iiPM6dGMLxBfUh?S# zr^7d6{VU>OLPJPTcnTwG*uU?IiJA!whg#1vg5B`ne{U#U&fBscs+W;Y=iW~QY~)*? zAM$s?L!(TuE0`i7*UD`V1r9X=Kfi4g3QMKYP@}sK4G8TUb-zG-j#+Zap1nF8qd+B0 ztW|RLY@P^aU^Bk`14A0T+(&A-HRDPZE|8FuBG+;z!|K7*c!YGCVFMT`lpK_k zZ2_cffkH2nC%`$$%t*P6*K=}+{kECiz?%ZQZ=ici+VRoG-}gc2+I69JD%N|xnkxm>j5_! z#FxR^O4uEGF~T6e9j=0{uYq4J;FX*TI$Hf5Q8FALB|if+9K4U7X|II{oV8(L!PcdW z*ssfEWQ&Lb0|proyHgKRui3N$tRwJFGW)dM$}FCAU#tc9_=3|p?8X3ZYpB=dt^^tm zbzBqec9H9<(+g{6jLr3w^?_G~c>jRi?km*I>~4bb&u(1r4=#j8&KFD{A#Ko#ue-|f zX(8}YK;z%Vzo+3)$D05_&*N6L^A``NfQ@fO9;hvzoe#n8d0$&4(b5LnlE)%=#j9Y= z&fN9d&h_9#F-PIyAx^}j(r)*Tb9d+D5aXJmZ=Qt0_Rq8J!8()SLvL4igSD;evW9rl zAb0eeQ(0W?(|KYAmAc28L4>-=v+qa5kPp%&wqnDy?KQRCi1U0t-4?R!dq;44A?9H$ zaz0UXNL))p{k@+R@ZyWTCs&6y0G^7>=nl&^aIiCp<+DI3_$YjTc%kMHjh=tv;QpWA z`a|3H#XGoWB5AboVF%PPEfbrXp6}H<#d&b`(Rk2nZ{SASQvs@tT*n1v*-;aNJ)&0v zY3E_7`yVxmfADK6VO^H{Rhcjf4;y&IbeENT2)5l-S$lX*J#_Qepo_Aug{PO_Z(YjV z23ELEH62T+2f?m!nVaeIX!KCWHPJukNlk?+Wdl%HpaSavL$iKjRZV|?)cPUVckeNW zh)*lLklR-3`=J$tggx6^Q0azzwCX9$+~`Tep{}R06g@aQ!s@B!>Y4#)`zCcs+!zZ& z(SJCh*U;LzV+jO2}0?#Kqy9!tsAQ83iNC+tBOpNI|JuxST7zgf2bXcq( z14bzytE}Ux_e^Mm*5|*5H7{rcQRjTb>0h;i(82C49o$vm#OVV41?X)sF?urr+PBbf z@ZUbu9uVzDFF3i=F61YCkSKS@cj)){YTc(#+kWb|L7Rx`SFToJ@Q~M8EUC8zG^#4> zRdzfL*1w2oo;c1xvGcFz!%*9esE1+xXU+8Ov*T*y)$#^*p^a5w`-jaBdd%{{<<<#Z z=fVaME-umY$fN`A%x?GyZsq{#P|K52=dx(@P{%cKp2E14&+r0ywjDr7^{M_Hm?pe& zUHuE)L_G+-_2#PT+5#At6`Hp&t^-~<9aW;`^BU0muqg;i6#mDYZa3rklOg4JoM9GP zqK! zsb`>h0>tA$xbEyN?Qahob!p)wIVX3SRaM2k*mij^7)mGxUP9>*ol)mAy?Rxfi@_-rhPoy`J6tc-7eRuy$~2 zgkxyijeHOuGUP?atBM9MXk1=jKX-phoZsPCEyAx*JPNvXNN%E{v-xk>h){l_Mr%EI zsJYAk7DpZo66@Q`wY(D^wLfzv#!?sk5MaAdyn&X3Ynah9M?M8+^rT8@%k*=KPDqP- z2%U(7gX?!N4Hk4kQ^&}HSFg%}tCe`LT51urt1c&(Hff;`ju>ov6Fm360zuqhl;aLg zYZ3qUNl5V%7A&$Y@yMPAAm4WB#0|RFFw-#AjIMrq9#(cm*4@mPpfap(3Bv;=R3SRc zAW>P7rhTJscZ3KWy!;pOpXS{V9s+wj3hH>a{e<81Lf9F$Ho)rbf!W&@7J?VfzS1J* z9nhs|qPNph8#NZnJ+MVvfrdjpju)AOlN8ySr2LctGg_3j)kbUx?p#A+9~3Kx9A85- zU4}bgMn=x~<=|>KBC)>1#90s3@wH3I52e*Z%@Li0^U_J^(6O{uxckAOCEwy-0VI=U zWWQQ3d{el!GbT9$zD_S$@;tc!WM8iK?Jbo;#ksBSE*OxTQ;*c&bC9;hL+O_#=rJU_ zr(=w73huEs)sx9C11-+)qoUf{pbxzf=aTRSn98_S(D>v=RPh3K?F0MV>y65DaFq1y z5~B|HKz(M8lzlnn03M6-La=5~9;@5Kyrmub{!k6bIdB`kUfqV?iZ((+&&@YX=H{o- zLtRht`CeKa8A&(v?1NR>CNr)Ktf*pJG`mZf2f=A;91yo8QI2AI& ze&D$*`tJIpuiLt4*JY?V#C7z;Z}O(}HqGYOVH~_lSJ=tJK>6t|jY#2V@QwbSETz32 z@Z{%?4te_~u*B7{KsQwrjkYv=d((t=U6VSl^=XArUXig{ffc)%oTi^6wxpFwUU?8M zoos**-P?L8s+JU7CAgkV-U$s^Rb=as~Fz82{ zzeBCZn82Zwk7RXRs@_w^gniKVZL{F20-fxkCAHtmL9`*+s@|>#iuL{2qOAA^Y~R{= zv^rA}71CQHbaPGGoN++RPn6o_{O+p!XJFv_^TU|p@8?@8Gy3are5-{zZX+RVZN*?^ zw!3KUcn8pk+SC79O&mSBjQe#vpyg1H6X#eDMmF5t#2i*U2@ck|`{~CvL)($|?dnF4d(y1*x%2bSg{p!LjwtD;Ikhpkxbf@hh6NdZ^=?I8Wg+Amy3K z^xIz9u^9#{c27c9@#_oq`m&*wU(gmK*a3Ro%l9^} z#C*%xE#I;`x@O}IS+l50tR}e$<|w|i*OuyqhK+SV)VTzllHOu_;k~T;TT=p4AiIUkCEXSbj z&(j(Amx-aBrS9dt1P*omA?`ceR7>Wni|GV-{oxH&T-Fvo4CkvZHQC+BqzE@lKC6|C&19 zhTSTv#pvea8^6c%7{j*sSA|U=d4wFSQC2$3kxM%sr5^7P^Sp7tNJl3cPPz=hi!A*CeS+UX3e#CrKlWajze1AZOg#pccYk4J zM~gv!_k?8ozfy1WmZ`Oz49)Z9S!qYvMYF0P{80pfU4zn?`EUa;W2N$E*_S6Ef2E zyinm@89m0a@Ks!LWG_6U`QpiG<`#g!z-8F&>E{=1zBWO-00plS6H?Y z^iaovBX#k!=VXLi*oXb|>#s2W?$wKA{k3n?P$%Gc>f>ay#8w#=Ie5%@XeTu}0R-fGyTRVwRc1u#l*ia|gQf!6S7Ohe$eA)Zy*EkM2yg`%kIuhQ|cXS%Ver-o1G? zj|;IqTjIRIm;v+k-IXHBn+hxGj~~xGUJmXRaA}`e)dLrFMaj6FTaN~sO{nz#!+i;H zJ|t|Yn@&*iH^(W;^RA^J15zLVc8{M!Hq08R;j7VZ0B-i-am6B?z)|3RjsNv^=;p#z zrRD#?A+G=Y(nZEnkE98`{MHP0ec{5F3+RwtYC-T#Y!jT&vURX!Y=PGr>d#J~AK_E~22xJu;k84`0W>H;)d> z1@=~foh%xiz&FAyUawjbHRG?XGbC`R<6tR)b0_PbTrAg5h#pr=;EANeIhd@V!HVXsldJlg)M7eecxmfdoMh zUOvN_bMH&Ve}XQ-!-M`cblCXHmsu}{+F*xuRlUaJdT;cI-4|CgHgzHk++NA@ zz-m<{Tp66`@^X4Va^peeD||O}(S2G%LXimV^U|o}fS8x2D0}P8UI{wn?8=%@0(n&M)3d8Og#L%R{t)*S7)_QOle1$$_!4d7oK7$!zkUW8 zsg+!jH=dLNdh`wt+ZT0?su?U2bz3#Q{X#$^YgS4|}o{!&kL+H|S19Yput85#g-=^*t zi0i=%g%34+Rh!-?G1f!B>*4R`g97d6_RGCK!gKA5RXsCLfE#NY&@H}GNO{=-tk3A5gM^6hqavfntIdAQ$R{HFW77cDJ} z#bD2mqzl}BgRpk`F6#M#Ug$IVUA6385!~f9`g4JDFj!%>OUgQ-4^gLE&mlnQ7pU(S z1ZV-2S7byBGZ@-Aj}Fn^8he5K><7r{85t|@DFh=U-J0Fs+MwyxN^L{C2_+}{R}lB;1w z*3Lmsmj>`XWYZy;HC*UEjgsP&DcX8U%`u<_kluyNXUaWWgts@Ck7m`i%PYPEJ&6E5 z1Lx^=VetpM?{HT@AJyb7rDvYOhoN%I4%bXShlwRv-&LM=eU7^SA?Bg@nzhT=oN9rm zoRm;pc5c`~>Xp zeoyG`ZwKzIBez$dYyx6|hK)aV=%SHg-UUfJX!EbB?Iup(j5~%N4xRc8%1PVmbPh9M zzuqPzrk2AG&3Nmf*5iyDHNw@7Y)G2#Q~OSx9O33>!5Y)1G&KJa{IHK^>^Kn + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 300 + 3 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-e02/results_true.dat b/tests/regression_tests/surface_source_write/case-e02/results_true.dat new file mode 100644 index 000000000..8979eb554 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-e02/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.169123E-02 9.144814E-03 diff --git a/tests/regression_tests/surface_source_write/case-e02/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-e02/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..80615285332359f6d38365cce0cffbbfba1ba116 GIT binary patch literal 33344 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 literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-e03/inputs_true.dat b/tests/regression_tests/surface_source_write/case-e03/inputs_true.dat new file mode 100644 index 000000000..a3421298b --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-e03/inputs_true.dat @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 4 5 6 7 8 9 + 300 + 3 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-e03/results_true.dat b/tests/regression_tests/surface_source_write/case-e03/results_true.dat new file mode 100644 index 000000000..d793a7e42 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-e03/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +4.752377E-02 6.773395E-03 diff --git a/tests/regression_tests/surface_source_write/case-e03/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-e03/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..becc9254a3f1c326d78c58a23e0be68087b3b5d2 GIT binary patch literal 33344 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

l11eivG9^A2FF@o(kK8lbM?n}(nd+t7C#e>`?H zC6C)gdGK=|xNVf6k)K$De&^fTD6DwFa;qE2&u7ybxseT<>-PJ~OtzUXh^T>XbIBxI74S-f2E^v>Q+dKd( z-#@rNdTB^j%8i$t=S}3$pUa_fFekOBW2V6=uy}!eJX0wPR=U(7 z-GrqAvaX#KlQAn0?px1zPh6=DK8ntz%zLklo>gGUN;pEc1HA7~%7f?O-XZe%u0Ozo*!suD$6BDRzI&-Os)jl?)epW= zAajWQhgQ*4brXM0B%Lphy-S|1Um@@I;ti|1A@|)`%!c%ldU<||3O|?Dx6d>(8*WqOWqHTV=3xy;CZ86jVurT+b7$HKF`5_`Nw|s zCnLDt5RY-ogE`f+u!SD1bE?i(0mBEu@)5VH!NR)jch+;&L*ETsZ=K&Ijb366otJAp zb3et;mrOSht=${GHkjXWR}q0>(fFnYN|~IQ2FaS-E9c_x}14*rw?z z_1rEB$ff2O)h%oXo<3{uJkAtEAAkAf(63CM{}BBFeofv_r(&0SeRb0`8L_nrx=$i| zi{LBIGqWl-cfcFT`Jh@L8Jz8i@!WGa8zu~T9*`UCN028cn+1oINp?V#ho7u4+8q@u zi7rN4EcyV0Yowc!Tou%aKSXt(h}%aSBCI7>rGYt>#|0UmPt5=8Uwkt9)`+BzTOMvS zA?J@0IrzU%f$Ob9HMHmZi3T90J;=TA(nS8|KQAgGAE_vFtM3g+IWAM#ZJ7#&+HWVu z?d$}WT8|d08uTK_je-4dT1`mu;JTW$H~f4FSu83N+s@z9HnINcka^L@?#mc>*_ib@ z+^G{BU1{I+=*bs&@`H#?^>`dG`D`|nsW=-ov*9{p`C#V!H$3kNkAXg9AK=U0HogCY z%<1VCe%Cn);+gElTQ}vwRK3W`+wyH7*3UF4+PVPTDhl8%PZmS>MO>C*yGV}Th~v+0 zvLF;@CU5F&CPqwkyPK#R7b7zDHWlfIF&8u@^8D|+RPs}BorJmj|~wGUAq`g5W9?{BFuj6gZNL9d#G^VNu9$kVCl znUv551WJRnm@Wl?TgJBTL#$;$&y;OVjVw?lj4xaTEwQ7Pa-G};$tU1xtWt+FkYLL%o5&bC6 z#O<1|`jN3jei4lPaNm*7@b@}}&%47F(Zc0$SZZC|ouLxom1ckZ7;iHaI(nINEWsaP z1)N^|ZYN24BXa1^N#J^06Y|wtazO(~q(q;UIm3cgItU!fJU4NUp!(y^c$2rl$vyL8 z0bdfB_h8)m!2A~omUr^Uu=P8|47r?AMd+3C+7vGp5-?N7n!YiZCu5S8N01^ z+PSU`AmV1ej`d}*)3x|03)e(`-0}?^xtuM?Xg@{MD2F`$5IMp$4qiCizU8~mb2@&* zGPjn#mv}!4mWfFK_7kxncbA*=tNv_Q&A_^X9}xQw-u}2ec)f#x0}F=@`r+ly^RdB;s^O*TOII%1wS%Z#2lMwb zyn+L_zDu7dErLud9+YH7uR)U^-@i5cEqR@fs5g8)jE;jjGOoN3S;>Ow$aZcOIMNMI z^hc*3C}@MthxXQnX6HkPeH8*#!r8F)e)oY+e}FD)LNa(Zk>}fZn@{Qu*B8!NaOwJm zScB>F=-5|?2DNql1krQ#mKR@3g=pAuYpPl&%*ruecKOr?sM+yqVPmcw>Ma%akohoq z9hTTW!~b=-c=JgcN z!aP`ZTCW_J2gCZp`^%LaL03V$$jjm{V8y9JjBdf5P;p}8r2m2(TDrw5?4<~KzCh%N z;{aNDCZaV0p;&S}6o$Tpo@z5LHUfGpNxulVi;;Mmynf?c2iyzKWiRID;V zZ&Me~MeG57{;! edE5?HrP?A#ixAW#=z`r^N%DD3qCE8bS^o!D&vB6e literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/test.py b/tests/regression_tests/surface_source_write/test.py new file mode 100644 index 000000000..f949d8085 --- /dev/null +++ b/tests/regression_tests/surface_source_write/test.py @@ -0,0 +1,1126 @@ +"""Test the 'surface_source_write' setting. + +Results +------- + +All results are generated using only 1 MPI process. + +All results are generated using 1 thread except for "test_consistency_low_realization_number". +This specific test verifies that when the number of realization (i.e., point being candidate +to be stored) is lower than the capacity, results are reproducible even with multiple +threads (i.e., there is no potential thread competition that would produce different +results in that case). + +All results are generated using the history-based mode except for cases e01 to e03. + +All results are visually verified using the '_visualize.py' script in the regression test folder. + +OpenMC models +------------- + +Four OpenMC models with CSG-only geometries are used to cover the transmission, vacuum, +reflective and periodic Boundary Conditions (BC): + +- model_1: cylindrical core in 2 boxes (vacuum and transmission BC), +- model_2: cylindrical core in 1 box (vacuum BC), +- model_3: cylindrical core in 1 box (reflective BC), +- model_4: cylindrical core in 1 box (periodic BC). + +Two models including DAGMC geometries are also used, based on the mesh file 'dagmc.h5m' +available from tests/regression_tests/dagmc/legacy: + +- model_dagmc_1: model adapted from tests/regression_tests/dagmc/legacy, +- model_dagmc_2: model_dagmc_1 contained in two CSG boxes to introduce multiple level of coordinates. + +Test cases +---------- + +Test cases using CSG-only geometries: + +======== ======= ========= ========================= ===== =================================== +Folder Model Surface Cell BC* Expected particles +======== ======= ========= ========================= ===== =================================== +case-01 model_1 No No T+V Particles crossing any surface in + the model +case-02 model_1 1 No T Particles crossing this surface + only +case-03 model_1 Multiple No T Particles crossing the declared + surfaces +case-04 model_1 Multiple cell (lower universe) T Particles crossing the declared + surfaces that come from or are + coming to the cell +case-05 model_1 Multiple cell (root universe) T Particles crossing the declared + surfaces that come from or are + coming to the cell +case-06 model_1 No cell (lower universe) T Particles crossing any surface that + come from or are coming to the cell +case-07 model_1 No cell (root universe) T Particles crossing any surface that + come from or are coming to the cell +case-08 model_1 No cellfrom (lower universe) T Particles crossing any surface that + come from the cell +case-09 model_1 No cellto (lower universe) T Particles crossing any surface that + are coming to the cell +case-10 model_1 No cellfrom (root universe) T Particles crossing any surface that + come from the cell +case-11 model_1 No cellto (root universe) T Particles crossing any surface that + are coming to the cell +case-12 model_2 Multiple No V Particles crossing the declared + surfaces +case-13 model_2 Multiple cell (root universe) V Particles crossing any surface that + come from or are coming to the cell +case-14 model_2 Multiple cellfrom (root universe) V Particles crossing any surface that + are coming to the cell +case-15 model_2 Multiple cellto (root universe) V None +case-16 model_3 Multiple No R Particles crossing the declared + surfaces +case-17 model_3 Multiple cell (root universe) R None +case-18 model_3 Multiple cellfrom (root universe) R None +case-19 model_3 Multiple cellto (root universe) R None +case-20 model_4 1 No P+R Particles crossing the declared + periodic surface +case-21 model_4 1 cell (root universe) P+R None +======== ======= ========= ========================= ===== =================================== + +*: BC stands for Boundary Conditions, T for Transmission, R for Reflective, and V for Vacuum. + +An additional case, called 'case-a01', is used to check that the results are comparable when +the number of threads is set to 2 if the number of realization is lower than the capacity. + +Cases e01 to e03 are the event-based cases corresponding to the history-based cases 04, 07 and 13, +respectively. + +Test cases using DAGMC geometries: + +======== ============= ========= ===================== ===== =================================== +Folder Model Surface Cell BC* Expected particles +======== ============= ========= ===================== ===== =================================== +case-d01 model_dagmc_1 No No T+V Particles crossing any surface in + the model +case-d02 model_dagmc_1 1 No T Particles crossing this surface + only +case-d03 model_dagmc_1 No cell T Particles crossing any surface that + come from or are coming to the cell +case-d04 model_dagmc_1 1 cell T Particles crossing the declared + surface that come from or are + coming to the cell +case-d05 model_dagmc_1 No cellfrom T Particles crossing any surface that + come from the cell +case-d06 model_dagmc_1 No cellto T Particles crossing any surface that + are coming to the cell +case-d07 model_dagmc_2 Multiple cell (lower universe) T Particles crossing the declared + surfaces that come from or are + coming to the cell +case-d08 model_dagmc_2 Multiple cell (root universe) T Particles crossing the declared + surfaces that come from or are + coming to the cell +======== ============= ========= ===================== ===== =================================== + +*: BC stands for Boundary Conditions, T for Transmission, and V for Vacuum. + +Notes: + +- The test cases list is non-exhaustive compared to the number of possible combinations. + Test cases have been selected based on use and internal code logic. +- Cases 08 to 11 are testing that the feature still works even if the level of coordinates + before and after crossing a surface is different, +- Tests on boundary conditions are not performed on DAGMC models as the logic is shared + with CSG-only models, +- Cases that should return an error are tested in the 'test_exceptions' unit test + from 'unit_tests/surface_source_write/test.py'. + +TODO: + +- Test with a lattice. + +""" + +import os +import shutil +from pathlib import Path + +import h5py +import numpy as np +import openmc +import openmc.lib +import pytest + +from tests.testing_harness import PyAPITestHarness +from tests.regression_tests import config + + +@pytest.fixture(scope="function") +def single_thread(monkeypatch): + """Set the number of OMP threads to 1 for the test.""" + monkeypatch.setenv("OMP_NUM_THREADS", "1") + + +@pytest.fixture(scope="function") +def two_threads(monkeypatch): + """Set the number of OMP threads to 2 for the test.""" + monkeypatch.setenv("OMP_NUM_THREADS", "2") + + +@pytest.fixture(scope="function") +def single_process(monkeypatch): + """Set the number of MPI process to 1 for the test.""" + monkeypatch.setitem(config, "mpi_np", "1") + + +@pytest.fixture(scope="module") +def model_1(): + """Cylindrical core contained in a first box which is contained in a larger box. + A lower universe is used to describe the interior of the first box which + contains the core and its surrounding space. + + """ + openmc.reset_auto_ids() + model = openmc.Model() + + # ============================================================================= + # Materials + # ============================================================================= + + fuel = openmc.Material() + fuel.add_nuclide("U234", 0.0004524) + fuel.add_nuclide("U235", 0.0506068) + fuel.add_nuclide("U238", 0.9487090) + fuel.add_nuclide("U236", 0.0002318) + fuel.add_nuclide("O16", 2.0) + fuel.set_density("g/cm3", 11.0) + + water = openmc.Material() + water.add_nuclide("H1", 2.0) + water.add_nuclide("O16", 1.0) + water.set_density("g/cm3", 1.0) + + # ============================================================================= + # Geometry + # ============================================================================= + + # ----------------------------------------------------------------------------- + # Cylindrical core + # ----------------------------------------------------------------------------- + + # Parameters + core_radius = 2.0 + core_height = 4.0 + + # Surfaces + core_cylinder = openmc.ZCylinder(r=core_radius) + core_lower_plane = openmc.ZPlane(-core_height / 2.0) + core_upper_plane = openmc.ZPlane(core_height / 2.0) + + # Region + core_region = -core_cylinder & +core_lower_plane & -core_upper_plane + + # Cells + core = openmc.Cell(fill=fuel, region=core_region) + outside_core_region = +core_cylinder | -core_lower_plane | +core_upper_plane + outside_core = openmc.Cell(fill=water, region=outside_core_region) + + # Universe + inside_box1_universe = openmc.Universe(cells=[core, outside_core]) + + # ----------------------------------------------------------------------------- + # Box 1 + # ----------------------------------------------------------------------------- + + # Parameters + box1_size = 6.0 + + # Surfaces + box1_rpp = openmc.model.RectangularParallelepiped( + -box1_size / 2.0, box1_size / 2.0, + -box1_size / 2.0, box1_size / 2.0, + -box1_size / 2.0, box1_size / 2.0, + ) + + # Cell + box1 = openmc.Cell(fill=inside_box1_universe, region=-box1_rpp) + + # ----------------------------------------------------------------------------- + # Box 2 + # ----------------------------------------------------------------------------- + + # Parameters + box2_size = 8 + + # Surfaces + box2_rpp = openmc.model.RectangularParallelepiped( + -box2_size / 2.0, box2_size / 2.0, + -box2_size / 2.0, box2_size / 2.0, + -box2_size / 2.0, box2_size / 2.0, + boundary_type="vacuum" + ) + + # Cell + box2 = openmc.Cell(fill=water, region=-box2_rpp & +box1_rpp) + + # Register geometry + model.geometry = openmc.Geometry([box1, box2]) + + # ============================================================================= + # Settings + # ============================================================================= + + model.settings = openmc.Settings() + model.settings.particles = 100 + model.settings.batches = 5 + model.settings.inactive = 1 + model.settings.seed = 1 + + bounds = [ + -core_radius, + -core_radius, + -core_height / 2.0, + core_radius, + core_radius, + core_height / 2.0, + ] + distribution = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) + model.settings.source = openmc.IndependentSource(space=distribution) + + return model + + +@pytest.fixture +def model_2(): + """Cylindrical core contained in a box. + A lower universe is used to describe the interior of the box which + contains the core and its surrounding space. + + The box is defined with vacuum boundary conditions. + + """ + openmc.reset_auto_ids() + model = openmc.Model() + + # ============================================================================= + # Materials + # ============================================================================= + + fuel = openmc.Material() + fuel.add_nuclide("U234", 0.0004524) + fuel.add_nuclide("U235", 0.0506068) + fuel.add_nuclide("U238", 0.9487090) + fuel.add_nuclide("U236", 0.0002318) + fuel.add_nuclide("O16", 2.0) + fuel.set_density("g/cm3", 11.0) + + water = openmc.Material() + water.add_nuclide("H1", 2.0) + water.add_nuclide("O16", 1.0) + water.set_density("g/cm3", 1.0) + + # ============================================================================= + # Geometry + # ============================================================================= + + # ----------------------------------------------------------------------------- + # Cylindrical core + # ----------------------------------------------------------------------------- + + # Parameters + core_radius = 2.0 + core_height = 4.0 + + # Surfaces + core_cylinder = openmc.ZCylinder(r=core_radius) + core_lower_plane = openmc.ZPlane(-core_height / 2.0) + core_upper_plane = openmc.ZPlane(core_height / 2.0) + + # Region + core_region = -core_cylinder & +core_lower_plane & -core_upper_plane + + # Cells + core = openmc.Cell(fill=fuel, region=core_region) + outside_core_region = +core_cylinder | -core_lower_plane | +core_upper_plane + outside_core = openmc.Cell(fill=water, region=outside_core_region) + + # Universe + inside_box1_universe = openmc.Universe(cells=[core, outside_core]) + + # ----------------------------------------------------------------------------- + # Box 1 + # ----------------------------------------------------------------------------- + + # Parameters + box1_size = 6.0 + + # Surfaces + box1_rpp = openmc.model.RectangularParallelepiped( + -box1_size / 2.0, box1_size / 2.0, + -box1_size / 2.0, box1_size / 2.0, + -box1_size / 2.0, box1_size / 2.0, + boundary_type="vacuum" + ) + + # Cell + box1 = openmc.Cell(fill=inside_box1_universe, region=-box1_rpp) + + # Register geometry + model.geometry = openmc.Geometry([box1]) + + # ============================================================================= + # Settings + # ============================================================================= + + model.settings = openmc.Settings() + model.settings.particles = 100 + model.settings.batches = 5 + model.settings.inactive = 1 + model.settings.seed = 1 + + bounds = [ + -core_radius, + -core_radius, + -core_height / 2.0, + core_radius, + core_radius, + core_height / 2.0, + ] + distribution = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) + model.settings.source = openmc.IndependentSource(space=distribution) + + return model + + +@pytest.fixture +def model_3(): + """Cylindrical core contained in a box. + A lower universe is used to describe the interior of the box which + contains the core and its surrounding space. + + The box is defined with reflective boundary conditions. + + """ + openmc.reset_auto_ids() + model = openmc.Model() + + # ============================================================================= + # Materials + # ============================================================================= + + fuel = openmc.Material() + fuel.add_nuclide("U234", 0.0004524) + fuel.add_nuclide("U235", 0.0506068) + fuel.add_nuclide("U238", 0.9487090) + fuel.add_nuclide("U236", 0.0002318) + fuel.add_nuclide("O16", 2.0) + fuel.set_density("g/cm3", 11.0) + + water = openmc.Material() + water.add_nuclide("H1", 2.0) + water.add_nuclide("O16", 1.0) + water.set_density("g/cm3", 1.0) + + # ============================================================================= + # Geometry + # ============================================================================= + + # ----------------------------------------------------------------------------- + # Cylindrical core + # ----------------------------------------------------------------------------- + + # Parameters + core_radius = 2.0 + core_height = 4.0 + + # Surfaces + core_cylinder = openmc.ZCylinder(r=core_radius) + core_lower_plane = openmc.ZPlane(-core_height / 2.0) + core_upper_plane = openmc.ZPlane(core_height / 2.0) + + # Region + core_region = -core_cylinder & +core_lower_plane & -core_upper_plane + + # Cells + core = openmc.Cell(fill=fuel, region=core_region) + outside_core_region = +core_cylinder | -core_lower_plane | +core_upper_plane + outside_core = openmc.Cell(fill=water, region=outside_core_region) + + # Universe + inside_box1_universe = openmc.Universe(cells=[core, outside_core]) + + # ----------------------------------------------------------------------------- + # Box 1 + # ----------------------------------------------------------------------------- + + # Parameters + box1_size = 6.0 + + # Surfaces + box1_rpp = openmc.model.RectangularParallelepiped( + -box1_size / 2.0, box1_size / 2.0, + -box1_size / 2.0, box1_size / 2.0, + -box1_size / 2.0, box1_size / 2.0, + boundary_type="reflective" + ) + + # Cell + box1 = openmc.Cell(fill=inside_box1_universe, region=-box1_rpp) + + # Register geometry + model.geometry = openmc.Geometry([box1]) + + # ============================================================================= + # Settings + # ============================================================================= + + model.settings = openmc.Settings() + model.settings.particles = 100 + model.settings.batches = 5 + model.settings.inactive = 1 + model.settings.seed = 1 + + bounds = [ + -core_radius, + -core_radius, + -core_height / 2.0, + core_radius, + core_radius, + core_height / 2.0, + ] + distribution = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) + model.settings.source = openmc.IndependentSource(space=distribution) + + return model + + +@pytest.fixture +def model_4(): + """Cylindrical core contained in a box. + A lower universe is used to describe the interior of the box which + contains the core and its surrounding space. + + The box is defined with a pair of periodic boundary with reflective + boundaries. + + """ + openmc.reset_auto_ids() + model = openmc.Model() + + # ============================================================================= + # Materials + # ============================================================================= + + fuel = openmc.Material() + fuel.add_nuclide("U234", 0.0004524) + fuel.add_nuclide("U235", 0.0506068) + fuel.add_nuclide("U238", 0.9487090) + fuel.add_nuclide("U236", 0.0002318) + fuel.add_nuclide("O16", 2.0) + fuel.set_density("g/cm3", 11.0) + + water = openmc.Material() + water.add_nuclide("H1", 2.0) + water.add_nuclide("O16", 1.0) + water.set_density("g/cm3", 1.0) + + # ============================================================================= + # Geometry + # ============================================================================= + + # ----------------------------------------------------------------------------- + # Cylindrical core + # ----------------------------------------------------------------------------- + + # Parameters + core_radius = 2.0 + core_height = 4.0 + + # Surfaces + core_cylinder = openmc.ZCylinder(r=core_radius) + core_lower_plane = openmc.ZPlane(-core_height / 2.0) + core_upper_plane = openmc.ZPlane(core_height / 2.0) + + # Region + core_region = -core_cylinder & +core_lower_plane & -core_upper_plane + + # Cells + core = openmc.Cell(fill=fuel, region=core_region) + outside_core_region = +core_cylinder | -core_lower_plane | +core_upper_plane + outside_core = openmc.Cell(fill=water, region=outside_core_region) + + # Universe + inside_box1_universe = openmc.Universe(cells=[core, outside_core]) + + # ----------------------------------------------------------------------------- + # Box 1 + # ----------------------------------------------------------------------------- + + # Parameters + box1_size = 6.0 + + # Surfaces + box1_lower_plane = openmc.ZPlane(-box1_size / 2.0, boundary_type="periodic") + box1_upper_plane = openmc.ZPlane(box1_size / 2.0, boundary_type="periodic") + box1_left_plane = openmc.XPlane(-box1_size / 2.0, boundary_type="reflective") + box1_right_plane = openmc.XPlane(box1_size / 2.0, boundary_type="reflective") + box1_rear_plane = openmc.YPlane(-box1_size / 2.0, boundary_type="reflective") + box1_front_plane = openmc.YPlane(box1_size / 2.0, boundary_type="reflective") + + # Region + box1_region = ( + +box1_lower_plane + & -box1_upper_plane + & +box1_left_plane + & -box1_right_plane + & +box1_rear_plane + & -box1_front_plane + ) + + # Cell + box1 = openmc.Cell(fill=inside_box1_universe, region=box1_region) + + # Register geometry + model.geometry = openmc.Geometry([box1]) + + # ============================================================================= + # Settings + # ============================================================================= + + model.settings = openmc.Settings() + model.settings.particles = 100 + model.settings.batches = 5 + model.settings.inactive = 1 + model.settings.seed = 1 + + bounds = [ + -core_radius, + -core_radius, + -core_height / 2.0, + core_radius, + core_radius, + core_height / 2.0, + ] + distribution = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) + model.settings.source = openmc.IndependentSource(space=distribution) + + return model + + +def return_surface_source_data(filepath): + """Read a surface source file and return a sorted array composed + of flatten arrays of source data for each surface source point. + + TODO: + + - use read_source_file from source.py instead. Or a dedicated function + to produce sorted list of source points for a given file. + + Parameters + ---------- + filepath : str + Path to the surface source file + + Returns + ------- + data : np.array + Sorted array composed of flatten arrays of source data for + each surface source point + + """ + data = [] + keys = [] + + # Read source file + with h5py.File(filepath, "r") as f: + for point in f["source_bank"]: + r = point["r"] + u = point["u"] + e = point["E"] + time = point["time"] + wgt = point["wgt"] + delayed_group = point["delayed_group"] + surf_id = point["surf_id"] + particle = point["particle"] + + key = ( + f"{r[0]:.10e} {r[1]:.10e} {r[2]:.10e} {u[0]:.10e} {u[1]:.10e} {u[2]:.10e}" + f"{e:.10e} {time:.10e} {wgt:.10e} {delayed_group} {surf_id} {particle}" + ) + + keys.append(key) + + values = [*r, *u, e, time, wgt, delayed_group, surf_id, particle] + assert len(values) == 12 + data.append(values) + + data = np.array(data) + keys = np.array(keys) + sorted_idx = np.argsort(keys) + + return data[sorted_idx] + + +class SurfaceSourceWriteTestHarness(PyAPITestHarness): + def __init__(self, statepoint_name, model=None, inputs_true=None, workdir=None): + super().__init__(statepoint_name, model, inputs_true) + self.workdir = workdir + + def _test_output_created(self): + """Make sure surface_source.h5 has also been created.""" + super()._test_output_created() + if self._model.settings.surf_source_write: + assert os.path.exists( + "surface_source.h5" + ), "Surface source file has not been created." + + def _compare_output(self): + """Compare surface_source.h5 files.""" + if self._model.settings.surf_source_write: + source_true = return_surface_source_data("surface_source_true.h5") + source_test = return_surface_source_data("surface_source.h5") + np.testing.assert_allclose(source_true, source_test, rtol=1e-07) + + def main(self): + """Accept commandline arguments and either run or update tests.""" + if config["build_inputs"]: + self.build_inputs() + elif config["update"]: + self.update_results() + else: + self.execute_test() + + def build_inputs(self): + """Build inputs.""" + base_dir = os.getcwd() + try: + os.chdir(self.workdir) + self._build_inputs() + finally: + os.chdir(base_dir) + + def execute_test(self): + """Build inputs, run OpenMC, and verify correct results.""" + base_dir = os.getcwd() + try: + os.chdir(self.workdir) + self._build_inputs() + inputs = self._get_inputs() + self._write_inputs(inputs) + self._compare_inputs() + self._run_openmc() + self._test_output_created() + self._compare_output() + results = self._get_results() + self._write_results(results) + self._compare_results() + finally: + self._cleanup() + os.chdir(base_dir) + + def update_results(self): + """Update results_true.dat and inputs_true.dat""" + base_dir = os.getcwd() + try: + os.chdir(self.workdir) + self._build_inputs() + inputs = self._get_inputs() + self._write_inputs(inputs) + self._overwrite_inputs() + self._run_openmc() + self._test_output_created() + results = self._get_results() + self._write_results(results) + self._overwrite_results() + finally: + self._cleanup() + os.chdir(base_dir) + + def _overwrite_results(self): + """Also add the 'surface_source.h5' file during overwriting.""" + super()._overwrite_results() + if os.path.exists("surface_source.h5"): + shutil.copyfile("surface_source.h5", "surface_source_true.h5") + + def _cleanup(self): + """Also remove the 'surface_source.h5' file while cleaning.""" + super()._cleanup() + fs = "surface_source.h5" + if os.path.exists(fs): + os.remove(fs) + + +@pytest.mark.skipif(config["event"] is True, reason="Results from history-based mode.") +@pytest.mark.parametrize( + "folder, model_name, parameter", + [ + ("case-01", "model_1", {"max_particles": 300}), + ("case-02", "model_1", {"max_particles": 300, "surface_ids": [8]}), + ( + "case-03", + "model_1", + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9]}, + ), + ( + "case-04", + "model_1", + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": 2}, + ), + ( + "case-05", + "model_1", + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": 3}, + ), + ("case-06", "model_1", {"max_particles": 300, "cell": 2}), + ("case-07", "model_1", {"max_particles": 300, "cell": 3}), + ("case-08", "model_1", {"max_particles": 300, "cellfrom": 2}), + ("case-09", "model_1", {"max_particles": 300, "cellto": 2}), + ("case-10", "model_1", {"max_particles": 300, "cellfrom": 3}), + ("case-11", "model_1", {"max_particles": 300, "cellto": 3}), + ( + "case-12", + "model_2", + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9]}, + ), + ( + "case-13", + "model_2", + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": 3}, + ), + ( + "case-14", + "model_2", + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cellfrom": 3}, + ), + ( + "case-15", + "model_2", + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cellto": 3}, + ), + ( + "case-16", + "model_3", + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9]}, + ), + ( + "case-17", + "model_3", + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": 3}, + ), + ( + "case-18", + "model_3", + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cellfrom": 3}, + ), + ( + "case-19", + "model_3", + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cellto": 3}, + ), + ( + "case-20", + "model_4", + {"max_particles": 300, "surface_ids": [4]}, + ), + ( + "case-21", + "model_4", + {"max_particles": 300, "surface_ids": [4], "cell": 3}, + ), + ], +) +def test_surface_source_cell_history_based( + folder, model_name, parameter, single_thread, single_process, request +): + """Test on history-based results for CSG-only geometries.""" + assert os.environ["OMP_NUM_THREADS"] == "1" + assert config["mpi_np"] == "1" + model = request.getfixturevalue(model_name) + model.settings.surf_source_write = parameter + harness = SurfaceSourceWriteTestHarness( + "statepoint.5.h5", model=model, workdir=folder + ) + harness.main() + + +@pytest.mark.skipif(config["event"] is True, reason="Results from history-based mode.") +def test_consistency_low_realization_number(model_1, two_threads, single_process): + """The objective is to test that the results produced, in a case where + the number of potential realization (particle storage) is low + compared to the capacity of storage, are still consistent. + + This configuration ensures that the competition between threads does not + occur and that the content of the source file created can be compared. + + """ + assert os.environ["OMP_NUM_THREADS"] == "2" + assert config["mpi_np"] == "1" + model_1.settings.surf_source_write = { + "max_particles": 200, + "surface_ids": [1, 2, 3], + "cellfrom": 2, + } + harness = SurfaceSourceWriteTestHarness( + "statepoint.5.h5", model=model_1, workdir="case-a01" + ) + harness.main() + + +@pytest.mark.skipif(config["event"] is False, reason="Results from event-based mode.") +@pytest.mark.parametrize( + "folder, model_name, parameter", + [ + ( + "case-e01", + "model_1", + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": 2}, + ), + ("case-e02", "model_1", {"max_particles": 300, "cell": 3}), + ( + "case-e03", + "model_2", + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": 3}, + ), + ], +) +def test_surface_source_cell_event_based( + folder, model_name, parameter, single_thread, single_process, request +): + """Test on event-based results for CSG-only geometries.""" + assert os.environ["OMP_NUM_THREADS"] == "1" + assert config["mpi_np"] == "1" + model = request.getfixturevalue(model_name) + model.settings.surf_source_write = parameter + harness = SurfaceSourceWriteTestHarness( + "statepoint.5.h5", model=model, workdir=folder + ) + harness.main() + + +@pytest.fixture(scope="module") +def model_dagmc_1(): + """Model based on the mesh file 'dagmc.h5m' available from + tests/regression_tests/dagmc/legacy. + + """ + openmc.reset_auto_ids() + model = openmc.Model() + + # ============================================================================= + # Materials + # ============================================================================= + + u235 = openmc.Material(name="no-void fuel") + u235.add_nuclide("U235", 1.0, "ao") + u235.set_density("g/cc", 11) + u235.id = 40 + + water = openmc.Material(name="water") + water.add_nuclide("H1", 2.0, "ao") + water.add_nuclide("O16", 1.0, "ao") + water.set_density("g/cc", 1.0) + water.add_s_alpha_beta("c_H_in_H2O") + water.id = 41 + + materials = openmc.Materials([u235, water]) + model.materials = materials + + # ============================================================================= + # Geometry + # ============================================================================= + + dagmc_univ = openmc.DAGMCUniverse(Path("../../dagmc/legacy/dagmc.h5m")) + model.geometry = openmc.Geometry(dagmc_univ) + + # ============================================================================= + # Settings + # ============================================================================= + + model.settings = openmc.Settings() + model.settings.particles = 100 + model.settings.batches = 5 + model.settings.inactive = 1 + model.settings.seed = 1 + + source_box = openmc.stats.Box([-4, -4, -20], [4, 4, 20], only_fissionable=True) + model.settings.source = openmc.IndependentSource(space=source_box) + + return model + + +@pytest.fixture(scope="module") +def model_dagmc_2(): + """Model based on the mesh file 'dagmc.h5m' available from + tests/regression_tests/dagmc/legacy. + + This model corresponds to the model_dagmc_1 contained in two boxes to introduce + multiple level of coordinates from CSG geometry. + + """ + openmc.reset_auto_ids() + model = openmc.Model() + + # ============================================================================= + # Materials + # ============================================================================= + + u235 = openmc.Material(name="no-void fuel") + u235.add_nuclide("U235", 1.0, "ao") + u235.set_density("g/cc", 11) + u235.id = 40 + + water = openmc.Material(name="water") + water.add_nuclide("H1", 2.0, "ao") + water.add_nuclide("O16", 1.0, "ao") + water.set_density("g/cc", 1.0) + water.add_s_alpha_beta("c_H_in_H2O") + water.id = 41 + + materials = openmc.Materials([u235, water]) + model.materials = materials + + # ============================================================================= + # Geometry + # ============================================================================= + + dagmc_univ = openmc.DAGMCUniverse(Path("../../dagmc/legacy/dagmc.h5m")) + + # ----------------------------------------------------------------------------- + # Box 1 + # ----------------------------------------------------------------------------- + + # Parameters + box1_size = 44 + + # Surfaces + box1_lower_plane = openmc.ZPlane(-box1_size / 2.0, surface_id=101) + box1_upper_plane = openmc.ZPlane(box1_size / 2.0, surface_id=102) + box1_left_plane = openmc.XPlane(-box1_size / 2.0, surface_id=103) + box1_right_plane = openmc.XPlane(box1_size / 2.0, surface_id=104) + box1_rear_plane = openmc.YPlane(-box1_size / 2.0, surface_id=105) + box1_front_plane = openmc.YPlane(box1_size / 2.0, surface_id=106) + + # Region + box1_region = ( + +box1_lower_plane + & -box1_upper_plane + & +box1_left_plane + & -box1_right_plane + & +box1_rear_plane + & -box1_front_plane + ) + + # Cell + box1 = openmc.Cell(fill=dagmc_univ, region=box1_region, cell_id=8) + + # ----------------------------------------------------------------------------- + # Box 2 + # ----------------------------------------------------------------------------- + + # Parameters + box2_size = 48 + + # Surfaces + box2_lower_plane = openmc.ZPlane( + -box2_size / 2.0, boundary_type="vacuum", surface_id=107 + ) + box2_upper_plane = openmc.ZPlane( + box2_size / 2.0, boundary_type="vacuum", surface_id=108 + ) + box2_left_plane = openmc.XPlane( + -box2_size / 2.0, boundary_type="vacuum", surface_id=109 + ) + box2_right_plane = openmc.XPlane( + box2_size / 2.0, boundary_type="vacuum", surface_id=110 + ) + box2_rear_plane = openmc.YPlane( + -box2_size / 2.0, boundary_type="vacuum", surface_id=111 + ) + box2_front_plane = openmc.YPlane( + box2_size / 2.0, boundary_type="vacuum", surface_id=112 + ) + + # Region + inside_box2 = ( + +box2_lower_plane + & -box2_upper_plane + & +box2_left_plane + & -box2_right_plane + & +box2_rear_plane + & -box2_front_plane + ) + outside_box1 = ( + -box1_lower_plane + | +box1_upper_plane + | -box1_left_plane + | +box1_right_plane + | -box1_rear_plane + | +box1_front_plane + ) + + box2_region = inside_box2 & outside_box1 + + # Cell + box2 = openmc.Cell(fill=water, region=box2_region, cell_id=9) + + # Register geometry + model.geometry = openmc.Geometry([box1, box2]) + + # ============================================================================= + # Settings + # ============================================================================= + + model.settings = openmc.Settings() + model.settings.particles = 100 + model.settings.batches = 5 + model.settings.inactive = 1 + model.settings.seed = 1 + + source_box = openmc.stats.Box([-4, -4, -20], [4, 4, 20], only_fissionable=True) + model.settings.source = openmc.IndependentSource(space=source_box) + + return model + + +@pytest.mark.skipif( + not openmc.lib._dagmc_enabled(), reason="DAGMC CAD geometry is not enabled." +) +@pytest.mark.skipif(config["event"] is True, reason="Results from history-based mode.") +@pytest.mark.parametrize( + "folder, model_name, parameter", + [ + ("case-d01", "model_dagmc_1", {"max_particles": 300}), + ("case-d02", "model_dagmc_1", {"max_particles": 300, "surface_ids": [1]}), + ("case-d03", "model_dagmc_1", {"max_particles": 300, "cell": 2}), + ( + "case-d04", + "model_dagmc_1", + {"max_particles": 300, "surface_ids": [1], "cell": 2}, + ), + ("case-d05", "model_dagmc_1", {"max_particles": 300, "cellfrom": 2}), + ("case-d06", "model_dagmc_1", {"max_particles": 300, "cellto": 2}), + ( + "case-d07", + "model_dagmc_2", + { + "max_particles": 300, + "surface_ids": [101, 102, 103, 104, 105, 106], + "cell": 7, + }, + ), + ( + "case-d08", + "model_dagmc_2", + { + "max_particles": 300, + "surface_ids": [101, 102, 103, 104, 105, 106], + "cell": 8, + }, + ), + ], +) +def test_surface_source_cell_dagmc( + folder, model_name, parameter, single_thread, single_process, request +): + """Test on models with DAGMC geometries.""" + assert os.environ["OMP_NUM_THREADS"] == "1" + assert config["mpi_np"] == "1" + model = request.getfixturevalue(model_name) + model.settings.surf_source_write = parameter + harness = SurfaceSourceWriteTestHarness( + "statepoint.5.h5", model=model, workdir=folder + ) + harness.main() diff --git a/tests/unit_tests/test_surface_source_write.py b/tests/unit_tests/test_surface_source_write.py new file mode 100644 index 000000000..472363073 --- /dev/null +++ b/tests/unit_tests/test_surface_source_write.py @@ -0,0 +1,248 @@ +"""Test the 'surf_source_write' setting used to store particles that cross +surfaces in a file for a given simulation.""" + +from pathlib import Path + +import openmc +import openmc.lib +import pytest +import h5py +import numpy as np + + +@pytest.fixture(scope="module") +def geometry(): + """Simple hydrogen sphere geometry""" + openmc.reset_auto_ids() + material = openmc.Material(name="H1") + material.add_element("H", 1.0) + sphere = openmc.Sphere(r=1.0, boundary_type="vacuum") + cell = openmc.Cell(region=-sphere, fill=material) + return openmc.Geometry([cell]) + + +@pytest.mark.parametrize( + "parameter", + [ + {"max_particles": 200}, + {"max_particles": 200, "cell": 1}, + {"max_particles": 200, "cellto": 1}, + {"max_particles": 200, "cellfrom": 1}, + {"max_particles": 200, "surface_ids": [2]}, + {"max_particles": 200, "surface_ids": [2], "cell": 1}, + {"max_particles": 200, "surface_ids": [2], "cellto": 1}, + {"max_particles": 200, "surface_ids": [2], "cellfrom": 1}, + ], +) +def test_xml_serialization(parameter, run_in_tmpdir): + """Check that the different use cases can be written and read in XML.""" + settings = openmc.Settings() + settings.surf_source_write = parameter + settings.export_to_xml() + + read_settings = openmc.Settings.from_xml() + assert read_settings.surf_source_write == parameter + + +ERROR_MSG_1 = ( + "A maximum number of particles needs to be specified " + "using the 'max_particles' parameter to store surface " + "source points." +) +ERROR_MSG_2 = "'cell', 'cellfrom' and 'cellto' cannot be used at the same time." + + +@pytest.mark.parametrize( + "parameter, error", + [ + ({"cell": 1}, ERROR_MSG_1), + ({"max_particles": 200, "cell": 1, "cellto": 1}, ERROR_MSG_2), + ({"max_particles": 200, "cell": 1, "cellfrom": 1}, ERROR_MSG_2), + ({"max_particles": 200, "cellto": 1, "cellfrom": 1}, ERROR_MSG_2), + ({"max_particles": 200, "cell": 1, "cellto": 1, "cellfrom": 1}, ERROR_MSG_2), + ], +) +def test_exceptions(parameter, error, run_in_tmpdir, geometry): + """Test parameters configuration that should return an error.""" + settings = openmc.Settings(run_mode="fixed source", batches=5, particles=100) + settings.surf_source_write = parameter + model = openmc.Model(geometry=geometry, settings=settings) + with pytest.raises(RuntimeError, match=error): + model.run() + + +@pytest.fixture(scope="module") +def model(): + """Simple hydrogen sphere divided in two hemispheres + by a z-plane to form 2 cells.""" + openmc.reset_auto_ids() + model = openmc.Model() + + # Material + material = openmc.Material(name="H1") + material.add_element("H", 1.0) + + # Geometry + radius = 1.0 + sphere = openmc.Sphere(r=radius, boundary_type="reflective") + plane = openmc.ZPlane(0.0) + cell_1 = openmc.Cell(region=-sphere & -plane, fill=material) + cell_2 = openmc.Cell(region=-sphere & +plane, fill=material) + root = openmc.Universe(cells=[cell_1, cell_2]) + model.geometry = openmc.Geometry(root) + + # Settings + model.settings = openmc.Settings() + model.settings.run_mode = "fixed source" + model.settings.particles = 100 + model.settings.batches = 3 + model.settings.seed = 1 + + bounds = [-radius, -radius, -radius, radius, radius, radius] + distribution = openmc.stats.Box(bounds[:3], bounds[3:]) + model.settings.source = openmc.IndependentSource(space=distribution) + + return model + + +@pytest.mark.parametrize( + "parameter", + [ + {"max_particles": 200, "cellto": 2, "surface_ids": [2]}, + {"max_particles": 200, "cellfrom": 2, "surface_ids": [2]}, + ], +) +def test_particle_direction(parameter, run_in_tmpdir, model): + """Test the direction of particles with the 'cellfrom' and 'cellto' parameters + on a simple model with only one surface of interest. + + Cell 2 is the upper hemisphere and surface 2 is the plane dividing the sphere + into two hemispheres. + + """ + model.settings.surf_source_write = parameter + model.run() + with h5py.File("surface_source.h5", "r") as f: + source = f["source_bank"] + + assert len(source) == 200 + + # We want to verify that the dot product of the surface's normal vector + # and the direction of the particle is either positive or negative + # depending on cellfrom or cellto. In this case, it is equivalent + # to just compare the z component of the direction of the particle. + for point in source: + if "cellto" in parameter.keys(): + assert point["u"]["z"] > 0.0 + elif "cellfrom" in parameter.keys(): + assert point["u"]["z"] < 0.0 + else: + assert False + + +@pytest.fixture +def model_dagmc(request): + """Model based on the mesh file 'dagmc.h5m' available from + tests/regression_tests/dagmc/legacy. + + """ + openmc.reset_auto_ids() + model = openmc.Model() + + # ============================================================================= + # Materials + # ============================================================================= + + u235 = openmc.Material(name="no-void fuel") + u235.add_nuclide("U235", 1.0, "ao") + u235.set_density("g/cc", 11) + u235.id = 40 + + water = openmc.Material(name="water") + water.add_nuclide("H1", 2.0, "ao") + water.add_nuclide("O16", 1.0, "ao") + water.set_density("g/cc", 1.0) + water.add_s_alpha_beta("c_H_in_H2O") + water.id = 41 + + model.materials = openmc.Materials([u235, water]) + + # ============================================================================= + # Geometry + # ============================================================================= + dagmc_path = Path(request.fspath).parent / "../regression_tests/dagmc/legacy/dagmc.h5m" + dagmc_univ = openmc.DAGMCUniverse(dagmc_path) + model.geometry = openmc.Geometry(dagmc_univ) + + # ============================================================================= + # Settings + # ============================================================================= + + model.settings = openmc.Settings() + model.settings.particles = 300 + model.settings.batches = 5 + model.settings.inactive = 1 + model.settings.seed = 1 + + source_box = openmc.stats.Box([-4, -4, -20], [4, 4, 20]) + model.settings.source = openmc.IndependentSource(space=source_box) + + return model + + +@pytest.mark.skipif( + not openmc.lib._dagmc_enabled(), reason="DAGMC CAD geometry is not enabled." +) +@pytest.mark.parametrize( + "parameter", + [ + {"max_particles": 200, "cellto": 1}, + {"max_particles": 200, "cellfrom": 1}, + ], +) +def test_particle_direction_dagmc(parameter, run_in_tmpdir, model_dagmc): + """Test the direction of particles with the 'cellfrom' and 'cellto' parameters + on a DAGMC model.""" + model_dagmc.settings.surf_source_write = parameter + model_dagmc.run() + + r = 7.0 + h = 20.0 + + with h5py.File("surface_source.h5", "r") as f: + source = f["source_bank"] + + assert len(source) == 200 + + for point in source: + + x, y, z = point["r"] + ux, uy, uz = point["u"] + + # If the point is on the upper or lower circle + if np.allclose(abs(z), h): + # If the point is also on the cylindrical surface + if np.allclose(np.sqrt(x**2 + y**2), r): + if "cellfrom" in parameter.keys(): + assert (uz * z > 0) or (ux * x + uy * y > 0) + elif "cellto" in parameter.keys(): + assert (uz * z < 0) or (ux * x + uy * y < 0) + else: + assert False + # If the point is not on the cylindrical surface + else: + if "cellfrom" in parameter.keys(): + assert uz * z > 0 + elif "cellto" in parameter.keys(): + assert uz * z < 0 + else: + assert False + # If the point is not on the upper or lower circle, + # meaning it is on the cylindrical surface + else: + if "cellfrom" in parameter.keys(): + assert ux * x + uy * y > 0 + elif "cellto" in parameter.keys(): + assert ux * x + uy * y < 0 + else: + assert False diff --git a/tests/unit_tests/test_tally_multiply_density.py b/tests/unit_tests/test_tally_multiply_density.py index 66ef41e0d..552d76bd5 100644 --- a/tests/unit_tests/test_tally_multiply_density.py +++ b/tests/unit_tests/test_tally_multiply_density.py @@ -3,7 +3,7 @@ import openmc import pytest -def test_micro_macro_compare(): +def test_micro_macro_compare(run_in_tmpdir): # Create simple sphere model with H1 and H2 mat = openmc.Material() mat.add_components({'H1': 1.0, 'H2': 1.0}) diff --git a/tests/unit_tests/weightwindows/test_ww_gen.py b/tests/unit_tests/weightwindows/test_ww_gen.py index 6fc3747b1..555421461 100644 --- a/tests/unit_tests/weightwindows/test_ww_gen.py +++ b/tests/unit_tests/weightwindows/test_ww_gen.py @@ -101,7 +101,7 @@ def labels(params): @pytest.mark.parametrize("filters", test_cases, ids=labels) -def test_ww_gen(filters, model): +def test_ww_gen(filters, run_in_tmpdir, model): tally = openmc.Tally() tally.filters = list(filters) From 3bedd043d0026490412018685901c9409635713b Mon Sep 17 00:00:00 2001 From: lhchg Date: Wed, 19 Jun 2024 23:56:02 +0800 Subject: [PATCH 130/671] update math function unit test with catch2 (#2955) Co-authored-by: Paul Romano --- openmc/lib/math.py | 266 +-------------------- tests/cpp_unit_tests/CMakeLists.txt | 1 + tests/cpp_unit_tests/test_math.cpp | 357 ++++++++++++++++++++++++++++ tests/unit_tests/test_math.py | 247 ------------------- 4 files changed, 359 insertions(+), 512 deletions(-) create mode 100644 tests/cpp_unit_tests/test_math.cpp delete mode 100644 tests/unit_tests/test_math.py diff --git a/openmc/lib/math.py b/openmc/lib/math.py index 029f1c4c9..8c62f2416 100644 --- a/openmc/lib/math.py +++ b/openmc/lib/math.py @@ -1,5 +1,4 @@ -from ctypes import c_int, c_double, POINTER, c_uint64 -from random import getrandbits +from ctypes import c_int, c_double import numpy as np from numpy.ctypeslib import ndpointer @@ -7,137 +6,18 @@ from numpy.ctypeslib import ndpointer from . import _dll -_dll.t_percentile.restype = c_double -_dll.t_percentile.argtypes = [c_double, c_int] - -_dll.calc_pn_c.restype = None -_dll.calc_pn_c.argtypes = [c_int, c_double, ndpointer(c_double)] - -_dll.evaluate_legendre.restype = c_double -_dll.evaluate_legendre.argtypes = [c_int, POINTER(c_double), c_double] - -_dll.calc_rn_c.restype = None -_dll.calc_rn_c.argtypes = [c_int, ndpointer(c_double), ndpointer(c_double)] - _dll.calc_zn.restype = None _dll.calc_zn.argtypes = [c_int, c_double, c_double, ndpointer(c_double)] _dll.calc_zn_rad.restype = None _dll.calc_zn_rad.argtypes = [c_int, c_double, ndpointer(c_double)] -_dll.rotate_angle_c.restype = None -_dll.rotate_angle_c.argtypes = [ndpointer(c_double), c_double, - POINTER(c_double), POINTER(c_uint64)] -_dll.maxwell_spectrum.restype = c_double -_dll.maxwell_spectrum.argtypes = [c_double, POINTER(c_uint64)] - -_dll.watt_spectrum.restype = c_double -_dll.watt_spectrum.argtypes = [c_double, c_double, POINTER(c_uint64)] - -_dll.broaden_wmp_polynomials.restype = None -_dll.broaden_wmp_polynomials.argtypes = [c_double, c_double, c_int, - ndpointer(c_double)] - -_dll.normal_variate.restype = c_double -_dll.normal_variate.argtypes = [c_double, c_double, POINTER(c_uint64)] - -def t_percentile(p, df): - """ Calculate the percentile of the Student's t distribution with a - specified probability level and number of degrees of freedom - - Parameters - ---------- - p : float - Probability level - df : int - Degrees of freedom - - Returns - ------- - float - Corresponding t-value - - """ - - return _dll.t_percentile(p, df) - - -def calc_pn(n, x): - """ Calculate the n-th order Legendre polynomial at the value of x. - - Parameters - ---------- - n : int - Legendre order - x : float - Independent variable to evaluate the Legendre at - - Returns - ------- - float - Corresponding Legendre polynomial result - - """ - - pnx = np.empty(n + 1, dtype=np.float64) - _dll.calc_pn_c(n, x, pnx) - return pnx - - -def evaluate_legendre(data, x): - """ Finds the value of f(x) given a set of Legendre coefficients - and the value of x. - - Parameters - ---------- - data : iterable of float - Legendre coefficients - x : float - Independent variable to evaluate the Legendre at - - Returns - ------- - float - Corresponding Legendre expansion result - - """ - - data_arr = np.array(data, dtype=np.float64) - return _dll.evaluate_legendre(len(data)-1, - data_arr.ctypes.data_as(POINTER(c_double)), x) - - -def calc_rn(n, uvw): - """ Calculate the n-th order real Spherical Harmonics for a given angle; - all Rn,m values are provided for all n (where -n <= m <= n). - - Parameters - ---------- - n : int - Harmonics order - uvw : iterable of float - Independent variable to evaluate the Legendre at - - Returns - ------- - numpy.ndarray - Corresponding real harmonics value - - """ - - num_nm = (n + 1) * (n + 1) - rn = np.empty(num_nm, dtype=np.float64) - uvw_arr = np.array(uvw, dtype=np.float64) - _dll.calc_rn_c(n, uvw_arr, rn) - return rn - def calc_zn(n, rho, phi): """ Calculate the n-th order modified Zernike polynomial moment for a given angle (rho, theta) location in the unit disk. The normalization of the polynomials is such that the integral of Z_pq*Z_pq over the unit disk is exactly pi - Parameters ---------- n : int @@ -146,12 +26,10 @@ def calc_zn(n, rho, phi): Radial location in the unit disk phi : float Theta (radians) location in the unit disk - Returns ------- numpy.ndarray Corresponding resulting list of coefficients - """ num_bins = ((n + 1) * (n + 2)) // 2 @@ -165,161 +43,19 @@ def calc_zn_rad(n, rho): moment with no azimuthal dependency (m=0) for a given radial location in the unit disk. The normalization of the polynomials is such that the integral of Z_pq*Z_pq over the unit disk is exactly pi. - Parameters ---------- n : int Maximum order rho : float Radial location in the unit disk - Returns ------- numpy.ndarray Corresponding resulting list of coefficients - """ num_bins = n // 2 + 1 zn_rad = np.zeros(num_bins, dtype=np.float64) _dll.calc_zn_rad(n, rho, zn_rad) return zn_rad - - -def rotate_angle(uvw0, mu, phi, prn_seed=None): - """ Rotates direction cosines through a polar angle whose cosine is - mu and through an azimuthal angle sampled uniformly. - - Parameters - ---------- - uvw0 : iterable of float - Original direction cosine - mu : float - Polar angle cosine to rotate - phi : float - Azimuthal angle; if None, one will be sampled uniformly - prn_seed : int - Pseudorandom number generator (PRNG) seed; if None, one will be - generated randomly. - - Returns - ------- - numpy.ndarray - Rotated direction cosine - - """ - - if prn_seed is None: - prn_seed = getrandbits(63) - - uvw0_arr = np.array(uvw0, dtype=np.float64) - if phi is None: - _dll.rotate_angle_c(uvw0_arr, mu, None, c_uint64(prn_seed)) - else: - _dll.rotate_angle_c(uvw0_arr, mu, c_double(phi), c_uint64(prn_seed)) - - uvw = uvw0_arr - - return uvw - - -def maxwell_spectrum(T, prn_seed=None): - """ Samples an energy from the Maxwell fission distribution based - on a direct sampling scheme. - - Parameters - ---------- - T : float - Spectrum parameter - prn_seed : int - Pseudorandom number generator (PRNG) seed; if None, one will be - generated randomly. - - Returns - ------- - float - Sampled outgoing energy - - """ - - if prn_seed is None: - prn_seed = getrandbits(63) - - return _dll.maxwell_spectrum(T, c_uint64(prn_seed)) - - -def watt_spectrum(a, b, prn_seed=None): - """ Samples an energy from the Watt energy-dependent fission spectrum. - - Parameters - ---------- - a : float - Spectrum parameter a - b : float - Spectrum parameter b - prn_seed : int - Pseudorandom number generator (PRNG) seed; if None, one will be - generated randomly. - - Returns - ------- - float - Sampled outgoing energy - - """ - - if prn_seed is None: - prn_seed = getrandbits(63) - - return _dll.watt_spectrum(a, b, c_uint64(prn_seed)) - - -def normal_variate(mean_value, std_dev, prn_seed=None): - """ Samples an energy from the Normal distribution. - - Parameters - ---------- - mean_value : float - Mean of the Normal distribution - std_dev : float - Standard deviation of the normal distribution - prn_seed : int - Pseudorandom number generator (PRNG) seed; if None, one will be - generated randomly. - - Returns - ------- - float - Sampled outgoing normally distributed value - - """ - - if prn_seed is None: - prn_seed = getrandbits(63) - - return _dll.normal_variate(mean_value, std_dev, c_uint64(prn_seed)) - - -def broaden_wmp_polynomials(E, dopp, n): - """ Doppler broadens the windowed multipole curvefit. The curvefit is a - polynomial of the form a/E + b/sqrt(E) + c + d sqrt(E) ... - - Parameters - ---------- - E : float - Energy to evaluate at - dopp : float - sqrt(atomic weight ratio / kT), with kT given in eV - n : int - Number of components to the polynomial - - Returns - ------- - numpy.ndarray - Resultant leading coefficients - - """ - - factors = np.zeros(n, dtype=np.float64) - _dll.broaden_wmp_polynomials(E, dopp, n, factors) - return factors diff --git a/tests/cpp_unit_tests/CMakeLists.txt b/tests/cpp_unit_tests/CMakeLists.txt index 24ec1b7a9..f0f5f2853 100644 --- a/tests/cpp_unit_tests/CMakeLists.txt +++ b/tests/cpp_unit_tests/CMakeLists.txt @@ -3,6 +3,7 @@ set(TEST_NAMES test_file_utils test_tally test_interpolate + test_math # Add additional unit test files here ) diff --git a/tests/cpp_unit_tests/test_math.cpp b/tests/cpp_unit_tests/test_math.cpp new file mode 100644 index 000000000..1ad7c4b70 --- /dev/null +++ b/tests/cpp_unit_tests/test_math.cpp @@ -0,0 +1,357 @@ +#include +#include + +#include +#include +#include + +#include "openmc/math_functions.h" +#include "openmc/random_dist.h" +#include "openmc/random_lcg.h" +#include "openmc/wmp.h" + +TEST_CASE("Test t_percentile") +{ + // The reference solutions come from scipy.stats.t.ppf + std::vector> ref_ts { + {-15.894544844102773, -0.32491969623407446, 0.000000000000000, + 0.32491969623407446, 15.894544844102759}, + {-4.848732214442601, -0.2886751346880066, 0.000000000000000, + 0.2886751346880066, 4.848732214442598}, + {-2.756508521909475, -0.2671808657039658, 0.000000000000000, + 0.2671808657039658, 2.7565085219094745}}; + + // Permutations include 1 DoF, 2 DoF, and > 2 DoF + // We will test 5 p-values at 3-DoF values + std::vector test_ps {0.02, 0.4, 0.5, 0.6, 0.98}; + std::vector test_dfs {1, 2, 5}; + + for (int i = 0; i < test_dfs.size(); i++) { + int df = test_dfs[i]; + + std::vector test_ts; + + for (double p : test_ps) { + double test_t = openmc::t_percentile(p, df); + test_ts.push_back(test_t); + } + + // The 5 DoF approximation in openmc.lib.math.t_percentile is off by up to + // 8e-3 from the scipy solution, so test that one separately with looser + // tolerance + double tolerance = (df > 2) ? 1e-2 : 1e-6; + + REQUIRE_THAT( + ref_ts[i], Catch::Matchers::Approx(test_ts).epsilon(tolerance)); + } +} + +TEST_CASE("Test calc_pn") +{ + // The reference solutions come from scipy.special.eval_legendre + std::vector> ref_vals { + {1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1}, + {1, -0.5, -0.125, 0.4375, -0.289062, -0.0898438, 0.323242, -0.223145, + -0.0736389, 0.267899, -0.188229}, + {1, 0, -0.5, -0, 0.375, 0, -0.3125, -0, 0.273438, 0, -0.246094}, + {1, 0.5, -0.125, -0.4375, -0.289062, 0.0898438, 0.323242, 0.223145, + -0.0736389, -0.267899, -0.188229}, + {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}}; + + int max_order = 10; + std::vector test_xs = {-1.0, -0.5, 0.0, 0.5, 1.0}; + + std::vector> test_vals; + for (double x : test_xs) { + std::vector test_val(max_order + 1); + openmc::calc_pn_c(max_order, x, test_val.data()); + test_vals.push_back(test_val); + } + + for (int i = 0; i < ref_vals.size(); i++) { + REQUIRE_THAT(ref_vals[i], Catch::Matchers::Approx(test_vals[i])); + } +} + +TEST_CASE("Test evaluate_legendre") +{ + // The reference solutions come from numpy.polynomial.legendre.legval + std::vector ref_vals { + 5.5, -0.45597649, -1.35351562, -2.7730999, 60.5}; + + int max_order = 10; + std::vector test_xs = {-1.0, -0.5, 0.0, 0.5, 1.0}; + + // Set the coefficients back to 1s for the test values since + // evaluate legendre incorporates the (2l+1)/2 term on its own + std::vector test_coeffs(max_order + 1, 1.0); + + std::vector test_vals; + for (double x : test_xs) { + test_vals.push_back( + openmc::evaluate_legendre(test_coeffs.size() - 1, test_coeffs.data(), x)); + } + + REQUIRE_THAT(ref_vals, Catch::Matchers::Approx(test_vals)); +} + +TEST_CASE("Test calc_rn") +{ + std::vector ref_vals {1.000000000000000, -0.019833838076210, + 0.980066577841242, -0.197676811654084, 0.006790834062088, + -0.033668438114859, 0.940795745502164, -0.335561350977312, + 0.033500236162691, -0.001831975566765, 0.014882082223994, + -0.046185860057145, 0.883359726009014, -0.460318044571973, + 0.073415616482180, -0.005922278973373, 0.000448625292461, + -0.004750335422039, 0.025089695062177, -0.057224052171859, + 0.809468042300133, -0.570331780454957, 0.123771351522967, + -0.015356543011155, 0.001061098599927, -0.000104097571795, + 0.001319047965347, -0.009263463267120, 0.037043163155191, + -0.066518621473934, 0.721310852552881, -0.662967447756079, + 0.182739660926192, -0.029946258412359, 0.003119841820746, + -0.000190549327031, 0.000023320052630, -0.000338370521658, + 0.002878809439524, -0.015562587450914, 0.050271226423217, + -0.073829294593737, 0.621486505922182, -0.735830327235834, + 0.247995745731425, -0.050309614442385, 0.006809024629381, + -0.000619383085285, 0.000034086826414, -0.000005093626712, + 0.000082405610567, -0.000809532012556, 0.005358034016708, + -0.023740240859138, 0.064242405926477, -0.078969918083157, + 0.512915839160049, -0.787065093668736, 0.316917738015632, + -0.076745744765114, 0.012672942183651, -0.001481838409317, + 0.000120451946983, -0.000006047366709, 0.000001091052697, + -0.000019334294214, 0.000213051604838, -0.001640234119608, + 0.008982263900105, -0.033788039035668, 0.078388909900756, + -0.081820779415058, 0.398746190829636, -0.815478614863816, + 0.386704633068855, -0.109227544713261, 0.021245051959237, + -0.003002428416676, 0.000311416667310, -0.000022954482885, + 0.000001059646310, -0.000000230023931, 0.000004408854505, + -0.000053457925526, 0.000464152759861, -0.002976305522860, + 0.013958017448970, -0.045594791382625, 0.092128969315914, + -0.082334538374971, 0.282248459574595, -0.820599067736528, + 0.454486474163594, -0.147395565311743, 0.033013815809602, + -0.005448090715661, 0.000678450207914, -0.000063467485444, + 0.000004281943868, -0.000000182535754, 0.000000047847775, + -0.000000982664801, 0.000012933320414, -0.000124076425457, + 0.000901739739837, -0.004982323311961, 0.020457776068931, + -0.058948376674391, 0.104888993733747, -0.080538298991650, + 0.166710763818175, -0.802696588503912, 0.517433650833039, + -0.190564076304612, 0.048387190622376, -0.009120081648146, + 0.001318069323039, -0.000147308722683, 0.000012561029621, + -0.000000779794781, 0.000000030722703}; + + int max_order = 10; + + double azi = 0.1; // Longitude + double pol = 0.2; // Latitude + double mu = std::cos(pol); + + std::vector test_uvw {std::sin(pol) * std::cos(azi), + std::sin(pol) * std::sin(azi), std::cos(pol)}; + + std::vector test_vals((max_order + 1) * (max_order + 1), 0); + openmc::calc_rn_c(max_order, test_uvw.data(), test_vals.data()); + + REQUIRE_THAT(ref_vals, Catch::Matchers::Approx(test_vals)); +} + +TEST_CASE("Test calc_zn") +{ + std::vector ref_vals {1.00000000e+00, 2.39712769e-01, 4.38791281e-01, + 2.10367746e-01, -5.00000000e-01, 1.35075576e-01, 1.24686873e-01, + -2.99640962e-01, -5.48489101e-01, 8.84215021e-03, 5.68310892e-02, + -4.20735492e-01, -1.25000000e-01, -2.70151153e-01, -2.60091773e-02, + 1.87022545e-02, -3.42888902e-01, 1.49820481e-01, 2.74244551e-01, + -2.43159131e-02, -2.50357380e-02, 2.20500013e-03, -1.98908812e-01, + 4.07587508e-01, 4.37500000e-01, 2.61708929e-01, 9.10321205e-02, + -1.54686328e-02, -2.74049397e-03, -7.94845816e-02, 4.75368705e-01, + 7.11647284e-02, 1.30266162e-01, 3.37106977e-02, 1.06401886e-01, + -7.31606787e-03, -2.95625975e-03, -1.10250006e-02, 3.55194307e-01, + -1.44627826e-01, -2.89062500e-01, -9.28644588e-02, -1.62557358e-01, + 7.73431638e-02, -2.55329539e-03, -1.90923851e-03, 1.57578403e-02, + 1.72995854e-01, -3.66267690e-01, -1.81657333e-01, -3.32521518e-01, + -2.59738162e-02, -2.31580576e-01, 4.20673902e-02, -4.11710546e-04, + -9.36449487e-04, 1.92156884e-02, 2.82515641e-02, -3.90713738e-01, + -1.69280296e-01, -8.98437500e-02, -1.08693628e-01, 1.78813094e-01, + -1.98191857e-01, 1.65964201e-02, 2.77013853e-04}; + + int n = 10; + double rho = 0.5; + double phi = 0.5; + + int nums = ((n + 1) * (n + 2)) / 2; + + std::vector test_vals(nums, 0); + openmc::calc_zn(n, rho, phi, test_vals.data()); + + REQUIRE_THAT(ref_vals, Catch::Matchers::Approx(test_vals)); +} + +TEST_CASE("Test calc_zn_rad") +{ + std::vector ref_vals {1.00000000e+00, -5.00000000e-01, + -1.25000000e-01, 4.37500000e-01, -2.89062500e-01, -8.98437500e-02}; + + int n = 10; + double rho = 0.5; + + int nums = n / 2 + 1; + std::vector test_vals(nums, 0); + openmc::calc_zn_rad(n, rho, test_vals.data()); + + REQUIRE_THAT(ref_vals, Catch::Matchers::Approx(test_vals)); +} + +TEST_CASE("Test rotate_angle") +{ + std::vector uvw0 {1.0, 0.0, 0.0}; + double phi = 0.0; + + uint64_t prn_seed = 1; + openmc::prn(&prn_seed); + + SECTION("Test rotate_angle mu is 0") + { + std::vector ref_uvw {0.0, 0.0, -1.0}; + + double mu = 0.0; + + std::vector test_uvw(uvw0); + openmc::rotate_angle_c(test_uvw.data(), mu, &phi, &prn_seed); + + REQUIRE_THAT(ref_uvw, Catch::Matchers::Approx(test_uvw)); + } + + SECTION("Test rotate_angle mu is 1") + { + std::vector ref_uvw = {1.0, 0.0, 0.0}; + + double mu = 1.0; + + std::vector test_uvw(uvw0); + openmc::rotate_angle_c(test_uvw.data(), mu, &phi, &prn_seed); + + REQUIRE_THAT(ref_uvw, Catch::Matchers::Approx(test_uvw)); + } + + // Now to test phi is None + SECTION("Test rotate_angle no phi") + { + // When seed = 1, phi will be sampled as 1.9116495709698769 + // The resultant reference is from hand-calculations given the above + std::vector ref_uvw = { + 0.9, -0.422746750548505, 0.10623175090659095}; + + double mu = 0.9; + prn_seed = 1; + + std::vector test_uvw(uvw0); + openmc::rotate_angle_c(test_uvw.data(), mu, NULL, &prn_seed); + + REQUIRE_THAT(ref_uvw, Catch::Matchers::Approx(test_uvw)); + } +} + +TEST_CASE("Test maxwell_spectrum") +{ + double ref_val = 0.27767406743161277; + + double T = 0.5; + uint64_t prn_seed = 1; + + double test_val = openmc::maxwell_spectrum(T, &prn_seed); + + REQUIRE(ref_val == test_val); +} + +TEST_CASE("Test watt_spectrum") +{ + double ref_val = 0.30957476387766697; + + double a = 0.5; + double b = 0.75; + uint64_t prn_seed = 1; + + double test_val = openmc::watt_spectrum(a, b, &prn_seed); + + REQUIRE(ref_val == test_val); +} + +TEST_CASE("Test normal_variate") +{ + + // Generate a series of normally distributed random numbers and test + // whether their mean and standard deviation are close to the expected value + SECTION("Test with non-zero standard deviation") + { + uint64_t seed = 1; + + double mean = 0.0; + double standard_deviation = 1.0; + + int num_samples = 10000; + double sum = 0.0; + double sum_squared_difference = 0.0; + + for (int i = 0; i < num_samples; ++i) { + double sample = openmc::normal_variate(mean, standard_deviation, &seed); + sum += sample; + sum_squared_difference += (sample - mean) * (sample - mean); + } + + double actual_mean = sum / num_samples; + double actual_standard_deviation = + std::sqrt(sum_squared_difference / num_samples); + + REQUIRE_THAT(mean, Catch::Matchers::WithinAbs(actual_mean, 0.1)); + REQUIRE_THAT(standard_deviation, + Catch::Matchers::WithinAbs(actual_standard_deviation, 0.1)); + } + + // When the standard deviation is zero + // the generated random number should always be equal to the mean + SECTION("Test with zero standard deviation") + { + uint64_t seed = 1; + double mean = 5.0; + double standard_deviation = 0.0; + + for (int i = 0; i < 10; ++i) { + double sample = openmc::normal_variate(mean, standard_deviation, &seed); + REQUIRE(sample == mean); + } + } +} + +TEST_CASE("Test broaden_wmp_polynomials") +{ + double test_E = 0.5; + int n = 6; + + // Two branches of the code to worry about, beta > 6 and otherwise + // beta = sqrtE * dopp + SECTION("Test broaden_wmp_polynomials beta > 6") + { + std::vector ref_val { + 2., 1.41421356, 1.0001, 0.70731891, 0.50030001, 0.353907}; + + double test_dopp = 100.0; // approximately U235 at room temperature + + std::vector test_val(n, 0); + openmc::broaden_wmp_polynomials(test_E, test_dopp, n, test_val.data()); + + REQUIRE_THAT(ref_val, Catch::Matchers::Approx(test_val)); + } + + SECTION("Test broaden_wmp_polynomials beta < 6") + { + std::vector ref_val = { + 1.99999885, 1.41421356, 1.04, 0.79195959, 0.6224, 0.50346003}; + + double test_dopp = 5.0; + + std::vector test_val(n, 0); + openmc::broaden_wmp_polynomials(test_E, test_dopp, n, test_val.data()); + + REQUIRE_THAT(ref_val, Catch::Matchers::Approx(test_val)); + } +} diff --git a/tests/unit_tests/test_math.py b/tests/unit_tests/test_math.py deleted file mode 100644 index c50057fcd..000000000 --- a/tests/unit_tests/test_math.py +++ /dev/null @@ -1,247 +0,0 @@ -import numpy as np -import pytest -import scipy as sp -from scipy.stats import shapiro - -import openmc -import openmc.lib - - -def test_t_percentile(): - # Permutations include 1 DoF, 2 DoF, and > 2 DoF - # We will test 5 p-values at 3-DoF values - test_ps = [0.02, 0.4, 0.5, 0.6, 0.98] - test_dfs = [1, 2, 5] - - # The reference solutions come from Scipy - ref_ts = [[sp.stats.t.ppf(p, df) for p in test_ps] for df in test_dfs] - - test_ts = [[openmc.lib.math.t_percentile(p, df) for p in test_ps] - for df in test_dfs] - - # The 5 DoF approximation in openmc.lib.math.t_percentile is off by up to - # 8e-3 from the scipy solution, so test that one separately with looser - # tolerance - assert np.allclose(ref_ts[:-1], test_ts[:-1]) - assert np.allclose(ref_ts[-1], test_ts[-1], atol=1e-2) - - -def test_calc_pn(): - max_order = 10 - test_xs = np.linspace(-1., 1., num=5, endpoint=True) - - # Reference solutions from scipy - ref_vals = np.array([sp.special.eval_legendre(n, test_xs) - for n in range(0, max_order + 1)]) - - test_vals = [] - for x in test_xs: - test_vals.append(openmc.lib.math.calc_pn(max_order, x).tolist()) - - test_vals = np.swapaxes(np.array(test_vals), 0, 1) - - assert np.allclose(ref_vals, test_vals) - - -def test_evaluate_legendre(): - max_order = 10 - # Coefficients are set to 1, but will incorporate the (2l+1)/2 norm factor - # for the reference solution - test_coeffs = [0.5 * (2. * l + 1.) for l in range(max_order + 1)] - test_xs = np.linspace(-1., 1., num=5, endpoint=True) - - ref_vals = np.polynomial.legendre.legval(test_xs, test_coeffs) - - # Set the coefficients back to 1s for the test values since - # evaluate legendre incorporates the (2l+1)/2 term on its own - test_coeffs = [1. for l in range(max_order + 1)] - - test_vals = np.array([openmc.lib.math.evaluate_legendre(test_coeffs, x) - for x in test_xs]) - - assert np.allclose(ref_vals, test_vals) - - -def test_calc_rn(): - max_order = 10 - test_ns = np.array([i for i in range(0, max_order + 1)]) - azi = 0.1 # Longitude - pol = 0.2 # Latitude - test_uvw = np.array([np.sin(pol) * np.cos(azi), - np.sin(pol) * np.sin(azi), - np.cos(pol)]) - - # Reference solutions from the equations - ref_vals = [] - - def coeff(n, m): - return np.sqrt((2. * n + 1) * sp.special.factorial(n - m) / - (sp.special.factorial(n + m))) - - def pnm_bar(n, m, mu): - val = coeff(n, m) - if m != 0: - val *= np.sqrt(2.) - val *= sp.special.lpmv([m], [n], [mu]) - return val[0] - - ref_vals = [] - for n in test_ns: - for m in range(-n, n + 1): - if m < 0: - ylm = pnm_bar(n, np.abs(m), np.cos(pol)) * \ - np.sin(np.abs(m) * azi) - else: - ylm = pnm_bar(n, m, np.cos(pol)) * np.cos(m * azi) - - # Un-normalize for comparison - ylm /= np.sqrt(2. * n + 1.) - ref_vals.append(ylm) - - test_vals = [] - test_vals = openmc.lib.math.calc_rn(max_order, test_uvw) - - assert np.allclose(ref_vals, test_vals) - - -def test_calc_zn(): - n = 10 - rho = 0.5 - phi = 0.5 - - # Reference solution from running the C++ implementation - ref_vals = np.array([ - 1.00000000e+00, 2.39712769e-01, 4.38791281e-01, - 2.10367746e-01, -5.00000000e-01, 1.35075576e-01, - 1.24686873e-01, -2.99640962e-01, -5.48489101e-01, - 8.84215021e-03, 5.68310892e-02, -4.20735492e-01, - -1.25000000e-01, -2.70151153e-01, -2.60091773e-02, - 1.87022545e-02, -3.42888902e-01, 1.49820481e-01, - 2.74244551e-01, -2.43159131e-02, -2.50357380e-02, - 2.20500013e-03, -1.98908812e-01, 4.07587508e-01, - 4.37500000e-01, 2.61708929e-01, 9.10321205e-02, - -1.54686328e-02, -2.74049397e-03, -7.94845816e-02, - 4.75368705e-01, 7.11647284e-02, 1.30266162e-01, - 3.37106977e-02, 1.06401886e-01, -7.31606787e-03, - -2.95625975e-03, -1.10250006e-02, 3.55194307e-01, - -1.44627826e-01, -2.89062500e-01, -9.28644588e-02, - -1.62557358e-01, 7.73431638e-02, -2.55329539e-03, - -1.90923851e-03, 1.57578403e-02, 1.72995854e-01, - -3.66267690e-01, -1.81657333e-01, -3.32521518e-01, - -2.59738162e-02, -2.31580576e-01, 4.20673902e-02, - -4.11710546e-04, -9.36449487e-04, 1.92156884e-02, - 2.82515641e-02, -3.90713738e-01, -1.69280296e-01, - -8.98437500e-02, -1.08693628e-01, 1.78813094e-01, - -1.98191857e-01, 1.65964201e-02, 2.77013853e-04]) - - test_vals = openmc.lib.math.calc_zn(n, rho, phi) - - assert np.allclose(ref_vals, test_vals) - - -def test_calc_zn_rad(): - n = 10 - rho = 0.5 - - # Reference solution from running the C++ implementation - ref_vals = np.array([ - 1.00000000e+00, -5.00000000e-01, -1.25000000e-01, - 4.37500000e-01, -2.89062500e-01,-8.98437500e-02]) - - test_vals = openmc.lib.math.calc_zn_rad(n, rho) - - assert np.allclose(ref_vals, test_vals) - - -def test_rotate_angle(): - uvw0 = np.array([1., 0., 0.]) - phi = 0. - mu = 0. - - # reference: mu of 0 pulls the vector the bottom, so: - ref_uvw = np.array([0., 0., -1.]) - - test_uvw = openmc.lib.math.rotate_angle(uvw0, mu, phi) - - assert np.array_equal(ref_uvw, test_uvw) - - # Repeat for mu = 1 (no change) - mu = 1. - ref_uvw = np.array([1., 0., 0.]) - - test_uvw = openmc.lib.math.rotate_angle(uvw0, mu, phi) - - assert np.array_equal(ref_uvw, test_uvw) - - # Now to test phi is None - mu = 0.9 - phi = None - prn_seed = 1 - - # When seed = 1, phi will be sampled as 1.9116495709698769 - # The resultant reference is from hand-calculations given the above - ref_uvw = [0.9, -0.422746750548505, 0.10623175090659095] - test_uvw = openmc.lib.math.rotate_angle(uvw0, mu, phi, prn_seed) - - assert np.allclose(ref_uvw, test_uvw) - - -def test_maxwell_spectrum(): - prn_seed = 1 - T = 0.5 - ref_val = 0.27767406743161277 - test_val = openmc.lib.math.maxwell_spectrum(T, prn_seed) - - assert ref_val == test_val - - -def test_watt_spectrum(): - prn_seed = 1 - a = 0.5 - b = 0.75 - ref_val = 0.30957476387766697 - test_val = openmc.lib.math.watt_spectrum(a, b, prn_seed) - - assert ref_val == test_val - - -def test_normal_dist(): - # When standard deviation is zero, sampled value should be mean - prn_seed = 1 - mean = 14.08 - stdev = 0.0 - ref_val = 14.08 - test_val = openmc.lib.math.normal_variate(mean, stdev, prn_seed) - assert ref_val == pytest.approx(test_val) - - # Use Shapiro-Wilk test to ensure normality of sampled vairates - stdev = 1.0 - samples = [] - num_samples = 10000 - for _ in range(num_samples): - # sample the normal distribution from openmc - samples.append(openmc.lib.math.normal_variate(mean, stdev, prn_seed)) - prn_seed += 1 - stat, p = shapiro(samples) - assert p > 0.05 - - -def test_broaden_wmp_polynomials(): - # Two branches of the code to worry about, beta > 6 and otherwise - # beta = sqrtE * dopp - # First lets do beta > 6 - test_E = 0.5 - test_dopp = 100. # approximately U235 at room temperature - n = 6 - - ref_val = [2., 1.41421356, 1.0001, 0.70731891, 0.50030001, 0.353907] - test_val = openmc.lib.math.broaden_wmp_polynomials(test_E, test_dopp, n) - - assert np.allclose(ref_val, test_val) - - # now beta < 6 - test_dopp = 5. - ref_val = [1.99999885, 1.41421356, 1.04, 0.79195959, 0.6224, 0.50346003] - test_val = openmc.lib.math.broaden_wmp_polynomials(test_E, test_dopp, n) - - assert np.allclose(ref_val, test_val) From 3dff03f8249223751a34f701cd89e7959ba57fe8 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 19 Jun 2024 15:47:08 -0500 Subject: [PATCH 131/671] Resolve warnings related to numpy 2.0 (#3044) --- openmc/mgxs_library.py | 10 +++++----- openmc/statepoint.py | 2 +- tests/regression_tests/mg_temperature/build_2g.py | 5 +++-- tests/unit_tests/test_stats.py | 9 --------- 4 files changed, 9 insertions(+), 17 deletions(-) diff --git a/openmc/mgxs_library.py b/openmc/mgxs_library.py index 3381c3abb..b840563cf 100644 --- a/openmc/mgxs_library.py +++ b/openmc/mgxs_library.py @@ -1862,7 +1862,7 @@ class XSdata: table_fine[..., imu] += ((l + 0.5) * eval_legendre(l, mu_fine[imu]) * orig_data[..., l]) - new_data[..., h_bin] = integrate(table_fine, mu_fine) + new_data[..., h_bin] = integrate(table_fine, x=mu_fine) elif self.scatter_format == SCATTER_TABULAR: # Calculate the mu points of the current data @@ -1876,7 +1876,7 @@ class XSdata: for l in range(xsdata.num_orders): y = (interp1d(mu_self, orig_data)(mu_fine) * eval_legendre(l, mu_fine)) - new_data[..., l] = integrate(y, mu_fine) + new_data[..., l] = integrate(y, x=mu_fine) elif target_format == SCATTER_TABULAR: # Simply use an interpolating function to get the new data @@ -1895,7 +1895,7 @@ class XSdata: interp = interp1d(mu_self, orig_data) for h_bin in range(xsdata.num_orders): mu_fine = np.linspace(mu[h_bin], mu[h_bin + 1], _NMU) - new_data[..., h_bin] = integrate(interp(mu_fine), mu_fine) + new_data[..., h_bin] = integrate(interp(mu_fine), x=mu_fine) elif self.scatter_format == SCATTER_HISTOGRAM: # The histogram format does not have enough information to @@ -1921,7 +1921,7 @@ class XSdata: mu_fine = np.linspace(-1, 1, _NMU) for l in range(xsdata.num_orders): y = interp(mu_fine) * norm * eval_legendre(l, mu_fine) - new_data[..., l] = integrate(y, mu_fine) + new_data[..., l] = integrate(y, x=mu_fine) elif target_format == SCATTER_TABULAR: # Simply use an interpolating function to get the new data @@ -1940,7 +1940,7 @@ class XSdata: for h_bin in range(xsdata.num_orders): mu_fine = np.linspace(mu[h_bin], mu[h_bin + 1], _NMU) new_data[..., h_bin] = \ - norm * integrate(interp(mu_fine), mu_fine) + norm * integrate(interp(mu_fine), x=mu_fine) # Remove small values resulting from numerical precision issues new_data[..., np.abs(new_data) < 1.E-10] = 0. diff --git a/openmc/statepoint.py b/openmc/statepoint.py index f6df01bd4..13aac0caf 100644 --- a/openmc/statepoint.py +++ b/openmc/statepoint.py @@ -235,7 +235,7 @@ class StatePoint: if self._global_tallies is None: data = self._f['global_tallies'][()] gt = np.zeros(data.shape[0], dtype=[ - ('name', 'a14'), ('sum', 'f8'), ('sum_sq', 'f8'), + ('name', 'S14'), ('sum', 'f8'), ('sum_sq', 'f8'), ('mean', 'f8'), ('std_dev', 'f8')]) gt['name'] = ['k-collision', 'k-absorption', 'k-tracklength', 'leakage'] diff --git a/tests/regression_tests/mg_temperature/build_2g.py b/tests/regression_tests/mg_temperature/build_2g.py index f236031a7..1fb723449 100644 --- a/tests/regression_tests/mg_temperature/build_2g.py +++ b/tests/regression_tests/mg_temperature/build_2g.py @@ -289,8 +289,9 @@ def build_inf_model(xsnames, xslibname, temperature, tempmethod='nearest'): settings_file.output = {'summary': False} # Create an initial uniform spatial source distribution over fissionable zones bounds = [-INF, -INF, -INF, INF, INF, INF] - uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) + uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:]) settings_file.temperature = {'method': tempmethod} - settings_file.source = openmc.IndependentSource(space=uniform_dist) + settings_file.source = openmc.IndependentSource( + space=uniform_dist, constraints={'fissionable': True}) model.settings = settings_file model.export_to_model_xml() diff --git a/tests/unit_tests/test_stats.py b/tests/unit_tests/test_stats.py index f5b467d03..5451c0a2c 100644 --- a/tests/unit_tests/test_stats.py +++ b/tests/unit_tests/test_stats.py @@ -376,15 +376,6 @@ def test_box(): d = openmc.stats.Box.from_xml_element(elem) assert d.lower_left == pytest.approx(lower_left) assert d.upper_right == pytest.approx(upper_right) - assert not d.only_fissionable - - # only fissionable parameter - d2 = openmc.stats.Box(lower_left, upper_right, True) - assert d2.only_fissionable - elem = d2.to_xml_element() - assert elem.attrib['type'] == 'fission' - d = openmc.stats.Spatial.from_xml_element(elem) - assert isinstance(d, openmc.stats.Box) def test_point(): From 390005e8411f689f8529c73e35709ac581b42e44 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 20 Jun 2024 09:26:53 -0500 Subject: [PATCH 132/671] Make sure time boundary doesn't prevent secondary particle creation (#3043) --- src/simulation.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/simulation.cpp b/src/simulation.cpp index e218cdb29..2d5d75aa4 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -762,13 +762,15 @@ void transport_history_based_single_particle(Particle& p) { while (p.alive()) { p.event_calculate_xs(); - if (!p.alive()) - break; - p.event_advance(); - if (p.collision_distance() > p.boundary().distance) { - p.event_cross_surface(); - } else { - p.event_collide(); + if (p.alive()) { + p.event_advance(); + } + if (p.alive()) { + if (p.collision_distance() > p.boundary().distance) { + p.event_cross_surface(); + } else if (p.alive()) { + p.event_collide(); + } } p.event_revive_from_secondary(); } From 78ee851990bd63608324b630d960ef007a5c2a3f Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 20 Jun 2024 09:27:49 -0500 Subject: [PATCH 133/671] Determine whether nuclides are fissionable in volume calc mode (#3047) Co-authored-by: Patrick Shriwise --- src/nuclide.cpp | 13 +++++++++++++ tests/unit_tests/test_lib.py | 8 +++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/nuclide.cpp b/src/nuclide.cpp index 91adc0777..04ec11073 100644 --- a/src/nuclide.cpp +++ b/src/nuclide.cpp @@ -63,6 +63,19 @@ Nuclide::Nuclide(hid_t group, const vector& temperature) read_attribute(group, "atomic_weight_ratio", awr_); if (settings::run_mode == RunMode::VOLUME) { + // Determine whether nuclide is fissionable and then exit + int mt; + hid_t rxs_group = open_group(group, "reactions"); + for (auto name : group_names(rxs_group)) { + if (starts_with(name, "reaction_")) { + hid_t rx_group = open_group(rxs_group, name.c_str()); + read_attribute(rx_group, "mt", mt); + if (is_fission(mt)) { + fissionable_ = true; + break; + } + } + } return; } diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index 009a90968..e3c7ce3b7 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -963,7 +963,8 @@ def test_sample_external_source(run_in_tmpdir, mpi_intracomm): model.settings.source = openmc.IndependentSource( space=openmc.stats.Box([-5., -5., -5.], [5., 5., 5.]), angle=openmc.stats.Monodirectional((0., 0., 1.)), - energy=openmc.stats.Discrete([1.0e5], [1.0]) + energy=openmc.stats.Discrete([1.0e5], [1.0]), + constraints={'fissionable': True} ) model.settings.particles = 1000 model.settings.batches = 10 @@ -993,3 +994,8 @@ def test_sample_external_source(run_in_tmpdir, mpi_intracomm): assert p1.wgt == p2.wgt openmc.lib.finalize() + + # Make sure sampling works in volume calculation mode + openmc.lib.init(["-c"]) + openmc.lib.sample_external_source(100) + openmc.lib.finalize() From 4bd0b09e608ed4b27e7bf5e158e44d4147457f2e Mon Sep 17 00:00:00 2001 From: Ethan Peterson Date: Thu, 20 Jun 2024 11:14:55 -0700 Subject: [PATCH 134/671] Avoiding more numpy 2.0 deprecation warnings (#3049) --- openmc/model/surface_composite.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index 7c134e843..2945f73b7 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -972,19 +972,19 @@ class Polygon(CompositeSurface): # Check if polygon is self-intersecting by comparing edges pairwise n = len(points) for i in range(n): - p0 = points[i, :] - p1 = points[(i + 1) % n, :] + p0 = np.append(points[i, :], 0) + p1 = np.append(points[(i + 1) % n, :], 0) for j in range(i + 1, n): - p2 = points[j, :] - p3 = points[(j + 1) % n, :] + p2 = np.append(points[j, :], 0) + p3 = np.append(points[(j + 1) % n, :], 0) # Compute orientation of p0 wrt p2->p3 line segment - cp0 = np.cross(p3-p0, p2-p0) + cp0 = np.cross(p3-p0, p2-p0)[-1] # Compute orientation of p1 wrt p2->p3 line segment - cp1 = np.cross(p3-p1, p2-p1) + cp1 = np.cross(p3-p1, p2-p1)[-1] # Compute orientation of p2 wrt p0->p1 line segment - cp2 = np.cross(p1-p2, p0-p2) + cp2 = np.cross(p1-p2, p0-p2)[-1] # Compute orientation of p3 wrt p0->p1 line segment - cp3 = np.cross(p1-p3, p0-p3) + cp3 = np.cross(p1-p3, p0-p3)[-1] # Group cross products in an array and find out how many are 0 cross_products = np.array([[cp0, cp1], [cp2, cp3]]) From 00faa7d698251be80970ce50460c860dbfdc34cb Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Fri, 21 Jun 2024 18:21:13 -0500 Subject: [PATCH 135/671] Set DAGMC cell instances on surface crossing (#3052) --- src/particle.cpp | 9 +++++++-- .../dagmc/universes/inputs_true.dat | 13 +++++++++++-- .../dagmc/universes/results_true.dat | 13 ++++++++++++- tests/regression_tests/dagmc/universes/test.py | 11 +++++++++-- 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/particle.cpp b/src/particle.cpp index 0c6a33b78..7d30e26bd 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -557,9 +557,14 @@ void Particle::cross_surface(const Surface& surf) sqrtkT_last() = sqrtkT(); // set new cell value lowest_coord().cell = i_cell; + auto& cell = model::cells[i_cell]; + cell_instance() = 0; - material() = model::cells[i_cell]->material_[0]; - sqrtkT() = model::cells[i_cell]->sqrtkT_[0]; + if (cell->distribcell_index_ >= 0) + cell_instance() = cell_instance_at_level(*this, n_coord() - 1); + + material() = cell->material(cell_instance()); + sqrtkT() = cell->sqrtkT(cell_instance()); return; } #endif diff --git a/tests/regression_tests/dagmc/universes/inputs_true.dat b/tests/regression_tests/dagmc/universes/inputs_true.dat index babf43ff1..4b5be3612 100644 --- a/tests/regression_tests/dagmc/universes/inputs_true.dat +++ b/tests/regression_tests/dagmc/universes/inputs_true.dat @@ -47,9 +47,18 @@ eigenvalue 100 10 - 2 + 5

false - + + + 4 0 4 1 4 2 4 3 4 4 + + + 1 + scatter + + + diff --git a/tests/regression_tests/dagmc/universes/results_true.dat b/tests/regression_tests/dagmc/universes/results_true.dat index f7bd016c1..76cbc9db0 100644 --- a/tests/regression_tests/dagmc/universes/results_true.dat +++ b/tests/regression_tests/dagmc/universes/results_true.dat @@ -1,2 +1,13 @@ k-combined: -9.156561E-01 4.398617E-02 +9.887663E-01 1.510336E-02 +tally 1: +4.340758E+00 +4.265459E+00 +4.712319E+00 +4.654778E+00 +4.151897E+00 +3.588090E+00 +2.965925E+00 +1.852746E+00 +0.000000E+00 +0.000000E+00 diff --git a/tests/regression_tests/dagmc/universes/test.py b/tests/regression_tests/dagmc/universes/test.py index a318275cd..057a7b0d4 100644 --- a/tests/regression_tests/dagmc/universes/test.py +++ b/tests/regression_tests/dagmc/universes/test.py @@ -72,13 +72,20 @@ class DAGMCUniverseTest(PyAPITestHarness): self._model.geometry = openmc.Geometry([bounding_cell]) + # add a cell instance tally + tally = openmc.Tally(name='cell instance tally') + # using scattering + cell_instance_filter = openmc.CellInstanceFilter(((4, 0), (4, 1), (4, 2), (4, 3), (4, 4))) + tally.filters = [cell_instance_filter] + tally.scores = ['scatter'] + self._model.tallies = [tally] + # settings self._model.settings.particles = 100 self._model.settings.batches = 10 - self._model.settings.inactive = 2 + self._model.settings.inactive = 5 self._model.settings.output = {'summary' : False} - def test_univ(): harness = DAGMCUniverseTest('statepoint.10.h5', model=openmc.Model()) harness.main() From 55b52b7ef3c9415ce045712132bf31c2a013d8c8 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 21 Jun 2024 20:28:56 -0500 Subject: [PATCH 136/671] Release of version 0.15.0 (#3050) --- CMakeLists.txt | 4 +- docs/source/conf.py | 4 +- docs/source/io_formats/settings.rst | 4 +- docs/source/methods/random_ray.rst | 4 +- docs/source/releasenotes/0.15.0.rst | 262 ++++++++++++++++++ docs/source/releasenotes/index.rst | 1 + examples/lattice/hexagonal/build_xml.py | 5 +- examples/lattice/nested/build_xml.py | 5 +- examples/lattice/simple/build_xml.py | 5 +- examples/pincell/build_xml.py | 5 +- examples/pincell_multigroup/build_xml.py | 5 +- include/openmc/version.h.in | 2 +- openmc/__init__.py | 2 +- openmc/deplete/abc.py | 4 +- openmc/deplete/microxs.py | 2 +- openmc/lib/mesh.py | 4 +- openmc/mesh.py | 4 +- openmc/plotter.py | 2 +- openmc/region.py | 2 +- openmc/settings.py | 4 +- openmc/source.py | 6 +- openmc/stats/multivariate.py | 4 +- openmc/trigger.py | 2 +- .../filter_cellfrom/inputs_true.dat | 5 +- .../regression_tests/filter_cellfrom/test.py | 5 +- .../case-01/inputs_true.dat | 5 +- .../case-02/inputs_true.dat | 5 +- .../case-03/inputs_true.dat | 5 +- .../case-04/inputs_true.dat | 5 +- .../case-05/inputs_true.dat | 5 +- .../case-06/inputs_true.dat | 5 +- .../case-07/inputs_true.dat | 5 +- .../case-08/inputs_true.dat | 5 +- .../case-09/inputs_true.dat | 5 +- .../case-10/inputs_true.dat | 5 +- .../case-11/inputs_true.dat | 5 +- .../case-12/inputs_true.dat | 5 +- .../case-13/inputs_true.dat | 5 +- .../case-14/inputs_true.dat | 5 +- .../case-15/inputs_true.dat | 5 +- .../case-16/inputs_true.dat | 5 +- .../case-17/inputs_true.dat | 5 +- .../case-18/inputs_true.dat | 5 +- .../case-19/inputs_true.dat | 5 +- .../case-20/inputs_true.dat | 5 +- .../case-21/inputs_true.dat | 5 +- .../case-a01/inputs_true.dat | 5 +- .../case-d01/inputs_true.dat | 5 +- .../case-d02/inputs_true.dat | 5 +- .../case-d03/inputs_true.dat | 5 +- .../case-d04/inputs_true.dat | 5 +- .../case-d05/inputs_true.dat | 5 +- .../case-d06/inputs_true.dat | 5 +- .../case-d07/inputs_true.dat | 5 +- .../case-d08/inputs_true.dat | 5 +- .../case-e01/inputs_true.dat | 5 +- .../case-e02/inputs_true.dat | 5 +- .../case-e03/inputs_true.dat | 5 +- .../surface_source_write/test.py | 30 +- tests/unit_tests/test_tracks.py | 2 +- 60 files changed, 464 insertions(+), 85 deletions(-) create mode 100644 docs/source/releasenotes/0.15.0.rst diff --git a/CMakeLists.txt b/CMakeLists.txt index 98473e492..8a000c9b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,8 +3,8 @@ project(openmc C CXX) # Set version numbers set(OPENMC_VERSION_MAJOR 0) -set(OPENMC_VERSION_MINOR 14) -set(OPENMC_VERSION_RELEASE 1) +set(OPENMC_VERSION_MINOR 15) +set(OPENMC_VERSION_RELEASE 0) set(OPENMC_VERSION ${OPENMC_VERSION_MAJOR}.${OPENMC_VERSION_MINOR}.${OPENMC_VERSION_RELEASE}) configure_file(include/openmc/version.h.in "${CMAKE_BINARY_DIR}/include/openmc/version.h" @ONLY) diff --git a/docs/source/conf.py b/docs/source/conf.py index 53d529f3d..19c04d98a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -69,9 +69,9 @@ copyright = '2011-2024, Massachusetts Institute of Technology, UChicago Argonne # built documents. # # The short X.Y version. -version = "0.14" +version = "0.15" # The full version, including alpha/beta/rc tags. -release = "0.14.1-dev" +release = "0.15.0" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 915f698b6..e395ada68 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -284,9 +284,9 @@ then, OpenMC will only use up to the :math:`P_1` data. .. note:: This element is not used in the continuous-energy :ref:`energy_mode`. ------------------------- +-------------------------------- ```` Element ------------------------- +-------------------------------- The ```` element indicates the number of times a particle can split during a history. diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index e0fff792e..427ba1148 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -100,11 +100,11 @@ terms on the right hand side. .. math:: :label: transport - \begin{align*} + \begin{aligned} \mathbf{\Omega} \cdot \mathbf{\nabla} \psi(\mathbf{r},\mathbf{\Omega},E) & + \Sigma_t(\mathbf{r},E) \psi(\mathbf{r},\mathbf{\Omega},E) = \\ & \int_0^\infty d E^\prime \int_{4\pi} d \Omega^{\prime} \Sigma_s(\mathbf{r},\mathbf{\Omega}^\prime \rightarrow \mathbf{\Omega}, E^\prime \rightarrow E) \psi(\mathbf{r},\mathbf{\Omega}^\prime, E^\prime) \\ & + \frac{\chi(\mathbf{r}, E)}{4\pi k_{eff}} \int_0^\infty dE^\prime \nu \Sigma_f(\mathbf{r},E^\prime) \int_{4\pi}d \Omega^\prime \psi(\mathbf{r},\mathbf{\Omega}^\prime,E^\prime) - \end{align*} + \end{aligned} In Equation :eq:`transport`, :math:`\psi` is the angular neutron flux. This parameter represents the total distance traveled by all neutrons in a particular diff --git a/docs/source/releasenotes/0.15.0.rst b/docs/source/releasenotes/0.15.0.rst new file mode 100644 index 000000000..069bfd783 --- /dev/null +++ b/docs/source/releasenotes/0.15.0.rst @@ -0,0 +1,262 @@ +==================== +What's New in 0.15.0 +==================== + +.. currentmodule:: openmc + +------- +Summary +------- + +This release of OpenMC includes many bug fixes, performance improvements, and +several notable new features. The major highlight of this release is the +introduction of a new transport solver based on the random ray method, which is +fully described in the :ref:`user's guide `. Other notable additions +include a mesh-based source class (:class:`openmc.MeshSource`), a generalization +of source domain rejection through the notion of "constraints", and new methods +on mesh-based classes for computing material volume fractions and homogenized +materials. + +------------------------------------ +Compatibility Notes and Deprecations +------------------------------------ + +Previously, specifying domain rejection for a source was only possible on the +:class:`~openmc.IndependentSoure` class and worked by specifying a `domains` +argument. This capability has been generalized to all source classes and +expanded as well; specifying a domain to reject on should now be done with the +`constraints` argument as follows:: + + source = openmc.IndependentSource(..., constraints={'domains': [cell]}) + +The `domains` argument is deprecated and will be removed in a future version of +OpenMC. Similarly, the ``only_fissionable`` argument to +:class:`openmc.stats.Box` has been replaced by a `'fissionable'` constraint. +That is, instead of specifying:: + + space = openmc.stats.Box(lower_left, upper_right, only_fissionable=True) + source = openmc.IndependentSource(space=space) + +You should now provide the constraint as:: + + space = openmc.stats.Box(lower_left, upper_right) + source = openmc.IndependentSource(space=space, constraints={'fissionable': True}) + +The :attr:`openmc.Settings.max_splits` attribute was renamed to +``max_history_splits`` and its default value has been changed to 1e7 (`#2954 +`_). + +------------ +New Features +------------ + +- When running OpenMC in volume calculation mode, only atomic weight ratio data + is loaded from data files which reduces initialization time. (`#2741 + `_) +- Introduced a ``GeometryState`` class in C++ to better separate particle and + geometry data. (`#2744 `_)) +- A new :class:`openmc.MaterialFromFilter` class allows filtering tallies by + which material a particle came from. (`#2750 + `_) +- Implemented a :meth:`openmc.deplete.MicroXS.from_multigroup_flux` method that + generates microscopic cross sections for depletion from a predetermined + multigroup flux. (`#2755 `_) +- A new :class:`openmc.MeshSource` class enables the specification of a source + distribution over a mesh, where each mesh element has a different + energy/angle/time distribution. (`#2759 + `_) +- Improve performance of depletion solver by utilizing CSR sparse matrix + representation. (`#2764 `_, + `#2771 `_) +- Added a :meth:`openmc.CylindricalMesh.get_indices_at_coords` method that + provides the mesh element index corresponding to a given point in space. + (`#2782 `_) +- Added a `path` argument to the :meth:`openmc.deplete.Integrator.integrate` + method. (`#2784 `_) +- Added a :meth:`openmc.Geometry.get_all_nuclides` method. (`#2796 + `_) +- A new capability to compute material volume fractions over mesh elements was + added in the :meth:`openmc.lib.Mesh.material_volumes` method. (`#2802 + `_) +- A new transport solver was added based on the `random ray + `_ method. (`#2823 + `_, `#2988 + `_) +- Added a :attr:`openmc.lib.Material.depletable` attribute. (`#2843 + `_) +- Added a :meth:`openmc.lib.Mesh.get_plot_bins` method and corresponding + ``openmc_mesh_get_plot_bins`` C API function that can be utilized to generate + mesh tally visualizations in the plotter application. (`#2854 + `_) +- Introduced a :func:`openmc.read_source_file` function that enables reading a + source file from the Python API. (`#2858 + `_) +- Added a ``bounding_box`` property on the :class:`openmc.RectilinearMesh` and + :class:`openmc.UnstructuredMesh` classes. (`#2861 + `_) +- Added a ``openmc_mesh_get_volumes`` C API function. (`#2869 + `_) +- The :attr:`openmc.Settings.surf_source_write` dictionary now accepts `cell`, + `cellfrom`, or `cellto` keys that limit surface source sites to those entering + or leaving specific cells. (`#2888 + `_) +- Added a :meth:`openmc.Region.plot` method that allows regions to be plotted + directly. (`#2895 `_) +- Implemented "contains" operator for the :class:`openmc.BoundingBox` class. + (`#2906 `_) +- Generalized source rejection via a new ``constraints`` argument to all source + classes. (`#2916 `_) +- Added a new :class:`openmc.MeshBornFilter` class that filters tally events + based on which mesh element a particle was born in. (`#2925 + `_) +- The :class:`openmc.Trigger` class now has a ``ignore_zeros`` argument that + results in any bins with zero score to be ignored when checking the trigger. + (`#2928 `_) +- Introduced a :attr:`openmc.Settings.max_events` attribute that controls the + maximum number of events a particle can undergo. (`#2945 + `_) +- Added support for :class:`openmc.UnstructuredMesh` in the + :class:`openmc.MeshSource` class. (`#2949 + `_) +- Added a :meth:`openmc.MeshBase.get_homogenized_materials` method that computes + homogenized materials over mesh elements. (`#2971 + `_) +- Add an ``options`` argument to :class:`openmc.UnstructuredMesh` that allows + configuring underlying data structures in MOAB. (`#2976 + `_) +- Type hints were added to several classes in the :mod:`openmc.deplete` module. + (`#2866 `_) + +--------- +Bug Fixes +--------- + +- Fix unit conversion in openmc.deplete.Results.get_mass (`#2761 `_) +- Fix Lagrangian interpolation (`#2775 `_) +- Depletion restart with MPI (`#2778 `_) +- Modify depletion transfer rates test to be more robust (`#2779 `_) +- Call simulation_finalize if needed when finalizing OpenMC (`#2790 `_) +- F90_NONE Removal (MGMC tallying optimization) (`#2785 `_) +- Correctly apply volumes to materials when using DAGMC geometries (`#2787 `_) +- Add inline to openmc::interpolate (`#2789 `_) +- Use huge_tree=True in lxml parsing (`#2791 `_) +- OpenMPMutex "Copying" (`#2794 `_) +- Do not link against several transitive dependencies of HDF5 (`#2797 `_) +- Added check to length of input arguments for IndependantOperator (`#2799 `_) +- Pytest Update Documentation (`#2801 `_) +- Move 'import lxml' to third-party block of imports (`#2803 `_) +- Fix creation of meshes when from loading settings from XML (`#2805 `_) +- Avoid high memory use when writing unstructured mesh VTK files (`#2806 `_) +- Consolidating thread information into the openmp interface header (`#2809 `_) +- Prevent underflow in calculation of speed (`#2811 `_) +- Provide error message if a cell path can't be determined (`#2812 `_) +- Fix distribcell labels for lattices used as fill in multiple cells (`#2813 `_) +- Make creation of spatial trees based on usage for unstructured mesh. (`#2815 `_) +- Ensure particle direction is normalized for plotting / volume calculations (`#2816 `_) +- Added missing meshes to documentation (`#2820 `_) +- Reset timers at correct place in deplete (`#2821 `_) +- Fix config change not propagating through to decay energies (`#2825 `_) +- Ensure that implicit complement cells appear last in DAGMC universes (`#2838 `_) +- Export model.tallies to XML in CoupledOperator (`#2840 `_) +- Fix locating h5m files references in DAGMC universes (`#2842 `_) +- Prepare for NumPy 2.0 (`#2845 `_) +- Added missing functions and classes to openmc.lib docs (`#2847 `_) +- Fix compilation on CentOS 7 (missing link to libdl) (`#2849 `_) +- Adding resulting nuclide to cross section plot legend (`#2851 `_) +- Updating file extension for Excel files when exporting MGXS data (`#2852 `_) +- Removed error raising when calling warn (`#2853 `_) +- Setting ``surf_source_`` attribute for DAGMC surfaces. (`#2857 `_) +- Changing y axis label for heating plots (`#2859 `_) +- Removed unused step_index arg from restart (`#2867 `_) +- Fix issue with Cell::get_contained_cells() utility function (`#2873 `_) +- Adding energy axis units to plot xs (`#2876 `_) +- Set OpenMCOperator materials when diff_burnable_mats = True (`#2877 `_) +- Fix expansion filter merging (`#2882 `_) +- Added checks that tolerance value is between 0 and 1 (`#2884 `_) +- Statepoint file loading refactor and CAPI function (`#2886 `_) +- Added check for length of value passed into EnergyFilter (`#2887 `_) +- Ensure that Model.run() works when specifying a custom XML path (`#2889 `_) +- Updating docker file base to bookworm (`#2890 `_) +- Clarifying documentation for cones (`#2892 `_) +- Abort on cmake config if openmp requested but not found (`#2893 `_) +- Tiny updates from experience building on Mac (`#2894 `_) +- Added damage-energy as optional reaction for micro (`#2903 `_) +- docs: add missing max_splits in settings specification (`#2910 `_) +- Changed CI to use latest actions to get away from the Node 16 deprecation. (`#2912 `_) +- Mkdir to always allow parents and exist ok (`#2914 `_) +- Fixed small sphinx typo (`#2915 `_) +- Hexagonal lattice iterators (`#2921 `_) +- Fix Chain.form_matrix to work with scipy 1.12 (`#2922 `_) +- Allow get_microxs_and_flux to use OPENMC_CHAIN_FILE environment variable (`#2934 `_) +- Polygon fix to better handle colinear points (`#2935 `_) +- Fix CMFD to work with scipy 1.13 (`#2936 `_) +- Print warning if no natural isotopes when using add_element (`#2938 `_) +- Update xtl and xtensor submodules (`#2941 `_) +- Ensure two surfaces with different boundary type are not considered redundant (`#2942 `_) +- Updated package versions in Dockerfile (`#2946 `_) +- Add MPI calls to DAGMC external test (`#2948 `_) +- Eliminate deprecation warnings from scipy and pandas (`#2951 `_) +- Update math function unit test with catch2 (`#2955 `_) +- Support track file writing for particle restart runs. (`#2957 `_) +- Make UWUW optional (`#2965 `_) +- Allow pure decay IndependentOperator (`#2966 `_) +- Added fix to cfloat_endf for length 11 endf floats (`#2967 `_) +- Moved apt get to optional CI parts (`#2970 `_) +- Update bounding_box docstrings (`#2972 `_) +- Added extra error checking on spherical mesh creation (`#2973 `_) +- Update CODEOWNERS file (`#2974 `_) +- Added error checking on cylindrical mesh (`#2977 `_) +- Correction for histogram interpolation of Tabular distributions (`#2981 `_) +- Enforce lower_left in lattice geometry (`#2982 `_) +- Update random_dist.h comment to be less specific (`#2991 `_) +- Apply memoization in get_all_universes (`#2995 `_) +- Make sure skewed dataset is cast to bool properly (`#3001 `_) +- Hexagonal lattice roundtrip (`#3003 `_) +- Fix CylinderSector and IsogonalOctagon translations (`#3018 `_) +- Sets used instead of lists when membership testing (`#3021 `_) +- Fixing plot xs for when plotting element string reaction (`#3029 `_) +- Fix shannon entropy broken link (`#3034 `_) +- Only add png or h5 extension if not present in plots.py (`#3036 `_) +- Fix non-existent path causing segmentation fault when saving plot (`#3038 `_) +- Resolve warnings related to numpy 2.0 (`#3044 `_) +- Update IsogonalOctagon to use xz basis (`#3045 `_) +- Determine whether nuclides are fissionable in volume calc mode (`#3047 `_) +- Avoiding more numpy 2.0 deprecation warnings (`#3049 `_) +- Set DAGMC cell instances on surface crossing (`#3052 `_) + +------------ +Contributors +------------ + +- `Aidan Crilly `_ +- `April Novak `_ +- `Davide Mancusi `_ +- `Baptiste Mouginot `_ +- `Chris Wagner `_ +- `Lorenzo Chierici `_ +- `Catherine Yu `_ +- `Erik Knudsen `_ +- `Ethan Peterson `_ +- `Gavin Ridley `_ +- `hsameer481 `_ +- `Hunter Belanger `_ +- `Isaac Meyer `_ +- `Jin Whan Bae `_ +- `Joffrey Dorville `_ +- `John Tramm `_ +- `Yue Jin `_ +- `Sigfrid Stjärnholm `_ +- `Kimberly Meagher `_ +- `lhchg `_ +- `Luke Labrie-Cleary `_ +- `Micah Gale `_ +- `Nicholas Linden `_ +- `pitkajuh `_ +- `Rosie Barker `_ +- `Paul Romano `_ +- `Patrick Shriwise `_ +- `Jonathan Shimwell `_ +- `Travis Labossiere-Hickman `_ +- `Vanessa Lulla `_ +- `Olek Yardas `_ +- `Perry Young `_ diff --git a/docs/source/releasenotes/index.rst b/docs/source/releasenotes/index.rst index 6923101cc..bde120589 100644 --- a/docs/source/releasenotes/index.rst +++ b/docs/source/releasenotes/index.rst @@ -7,6 +7,7 @@ Release Notes .. toctree:: :maxdepth: 1 + 0.15.0 0.14.0 0.13.3 0.13.2 diff --git a/examples/lattice/hexagonal/build_xml.py b/examples/lattice/hexagonal/build_xml.py index 96ff3f34a..9485d0aa4 100644 --- a/examples/lattice/hexagonal/build_xml.py +++ b/examples/lattice/hexagonal/build_xml.py @@ -114,8 +114,9 @@ settings_file.particles = particles # Create an initial uniform spatial source distribution over fissionable zones bounds = [-1, -1, -1, 1, 1, 1] -uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) -settings_file.source = openmc.IndependentSource(space=uniform_dist) +uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:]) +settings_file.source = openmc.IndependentSource( + space=uniform_dist, constraints={'fissionable': True}) settings_file.keff_trigger = {'type' : 'std_dev', 'threshold' : 5E-4} settings_file.trigger_active = True diff --git a/examples/lattice/nested/build_xml.py b/examples/lattice/nested/build_xml.py index 407a53a23..2db23a46b 100644 --- a/examples/lattice/nested/build_xml.py +++ b/examples/lattice/nested/build_xml.py @@ -124,8 +124,9 @@ settings_file.particles = particles # Create an initial uniform spatial source distribution over fissionable zones bounds = [-1, -1, -1, 1, 1, 1] -uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) -settings_file.source = openmc.IndependentSource(space=uniform_dist) +uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:]) +settings_file.source = openmc.IndependentSource( + space=uniform_dist, constraints={'fissionable': True}) settings_file.export_to_xml() diff --git a/examples/lattice/simple/build_xml.py b/examples/lattice/simple/build_xml.py index 0da8d8cf0..56c466121 100644 --- a/examples/lattice/simple/build_xml.py +++ b/examples/lattice/simple/build_xml.py @@ -115,8 +115,9 @@ settings_file.particles = particles # Create an initial uniform spatial source distribution over fissionable zones bounds = [-1, -1, -1, 1, 1, 1] -uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) -settings_file.source = openmc.IndependentSource(space=uniform_dist) +uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:]) +settings_file.source = openmc.IndependentSource( + space=uniform_dist, constraints={'fissionable': True}) settings_file.trigger_active = True settings_file.trigger_max_batches = 100 diff --git a/examples/pincell/build_xml.py b/examples/pincell/build_xml.py index d008c8822..aed0c790f 100644 --- a/examples/pincell/build_xml.py +++ b/examples/pincell/build_xml.py @@ -67,8 +67,9 @@ settings.particles = 1000 # Create an initial uniform spatial source distribution over fissionable zones lower_left = (-pitch/2, -pitch/2, -1) upper_right = (pitch/2, pitch/2, 1) -uniform_dist = openmc.stats.Box(lower_left, upper_right, only_fissionable=True) -settings.source = openmc.IndependentSource(space=uniform_dist) +uniform_dist = openmc.stats.Box(lower_left, upper_right) +settings.source = openmc.IndependentSource( + space=uniform_dist, constraints={'fissionable': True}) # For source convergence checks, add a mesh that can be used to calculate the # Shannon entropy diff --git a/examples/pincell_multigroup/build_xml.py b/examples/pincell_multigroup/build_xml.py index 019ae6a11..0971e5de6 100644 --- a/examples/pincell_multigroup/build_xml.py +++ b/examples/pincell_multigroup/build_xml.py @@ -113,8 +113,9 @@ settings.particles = 1000 # Create an initial uniform spatial source distribution over fissionable zones lower_left = (-pitch/2, -pitch/2, -1) upper_right = (pitch/2, pitch/2, 1) -uniform_dist = openmc.stats.Box(lower_left, upper_right, only_fissionable=True) -settings.source = openmc.IndependentSource(space=uniform_dist) +uniform_dist = openmc.stats.Box(lower_left, upper_right) +settings.source = openmc.IndependentSource( + space=uniform_dist, constraints={'fissionable': True}) settings.export_to_xml() ############################################################################### diff --git a/include/openmc/version.h.in b/include/openmc/version.h.in index e1c2b0541..a518e0d63 100644 --- a/include/openmc/version.h.in +++ b/include/openmc/version.h.in @@ -10,7 +10,7 @@ namespace openmc { constexpr int VERSION_MAJOR {@OPENMC_VERSION_MAJOR@}; constexpr int VERSION_MINOR {@OPENMC_VERSION_MINOR@}; constexpr int VERSION_RELEASE {@OPENMC_VERSION_RELEASE@}; -constexpr bool VERSION_DEV {true}; +constexpr bool VERSION_DEV {false}; constexpr std::array VERSION {VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE}; // clang-format on diff --git a/openmc/__init__.py b/openmc/__init__.py index 17b0abe2f..12a3c87d3 100644 --- a/openmc/__init__.py +++ b/openmc/__init__.py @@ -39,4 +39,4 @@ from .config import * from openmc.model import Model -__version__ = '0.14.1-dev' +__version__ = '0.15.0' diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 5d9b8a240..7bf7f1086 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -786,7 +786,7 @@ class Integrator(ABC): path : PathLike Path to file to write. Defaults to 'depletion_results.h5'. - .. versionadded:: 0.14.1 + .. versionadded:: 0.15.0 """ with change_directory(self.operator.output_dir): n = self.operator.initial_condition() @@ -993,7 +993,7 @@ class SIIntegrator(Integrator): path : PathLike Path to file to write. Defaults to 'depletion_results.h5'. - .. versionadded:: 0.14.1 + .. versionadded:: 0.15.0 """ with change_directory(self.operator.output_dir): n = self.operator.initial_condition() diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index 497a5284a..b2ae9f6c7 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -219,7 +219,7 @@ class MicroXS: sections available. MicroXS entry will be 0 if the nuclide cross section is not found. - .. versionadded:: 0.14.1 + .. versionadded:: 0.15.0 Parameters ---------- diff --git a/openmc/lib/mesh.py b/openmc/lib/mesh.py index 4da7baba7..3546112b5 100644 --- a/openmc/lib/mesh.py +++ b/openmc/lib/mesh.py @@ -174,7 +174,7 @@ class Mesh(_FortranObjectWithID): ) -> List[List[Tuple[Material, float]]]: """Determine volume of materials in each mesh element - .. versionadded:: 0.14.1 + .. versionadded:: 0.15.0 Parameters ---------- @@ -231,7 +231,7 @@ class Mesh(_FortranObjectWithID): ) -> np.ndarray: """Get mesh bin indices for a rasterized plot. - .. versionadded:: 0.14.1 + .. versionadded:: 0.15.0 Parameters ---------- diff --git a/openmc/mesh.py b/openmc/mesh.py index 5456abd1a..ee58ec3b8 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -157,7 +157,7 @@ class MeshBase(IDManagerMixin, ABC): ) -> List[openmc.Material]: """Generate homogenized materials over each element in a mesh. - .. versionadded:: 0.14.1 + .. versionadded:: 0.15.0 Parameters ---------- @@ -1487,6 +1487,8 @@ class CylindricalMesh(StructuredMesh): ) -> Tuple[int, int, int]: """Finds the index of the mesh voxel at the specified x,y,z coordinates. + .. versionadded:: 0.15.0 + Parameters ---------- coords : Sequence[float] diff --git a/openmc/plotter.py b/openmc/plotter.py index 8a9a331d8..8b205b79d 100644 --- a/openmc/plotter.py +++ b/openmc/plotter.py @@ -169,7 +169,7 @@ def plot_xs(reactions, divisor_types=None, temperature=294., axis=None, energy_axis_units : {'eV', 'keV', 'MeV'} Units used on the plot energy axis - .. versionadded:: 0.14.1 + .. versionadded:: 0.15.0 Returns ------- diff --git a/openmc/region.py b/openmc/region.py index f679129c1..e509b1528 100644 --- a/openmc/region.py +++ b/openmc/region.py @@ -344,7 +344,7 @@ class Region(ABC): def plot(self, *args, **kwargs): """Display a slice plot of the region. - .. versionadded:: 0.14.1 + .. versionadded:: 0.15.0 Parameters ---------- diff --git a/openmc/settings.py b/openmc/settings.py index 80d766ab1..fae6f6383 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -113,7 +113,7 @@ class Settings: max_particle_events : int Maximum number of allowed particle events per source particle. - .. versionadded:: 0.14.1 + .. versionadded:: 0.15.0 max_order : None or int Maximum scattering order to apply globally when in multi-group mode. max_history_splits : int @@ -158,7 +158,7 @@ class Settings: Starting ray distribution (must be uniform in space and angle) as specified by a :class:`openmc.SourceBase` object. - .. versionadded:: 0.14.1 + .. versionadded:: 0.15.0 resonance_scattering : dict Settings for resonance elastic scattering. Accepted keys are 'enable' (bool), 'method' (str), 'energy_min' (float), 'energy_max' (float), and diff --git a/openmc/source.py b/openmc/source.py index 91318f8e9..51ed74de3 100644 --- a/openmc/source.py +++ b/openmc/source.py @@ -264,7 +264,7 @@ class IndependentSource(SourceBase): Domains to reject based on, i.e., if a sampled spatial location is not within one of these domains, it will be rejected. - .. deprecated:: 0.14.1 + .. deprecated:: 0.15.0 Use the `constraints` argument instead. constraints : dict Constraints on sampled source particles. Valid keys include 'domains', @@ -482,7 +482,7 @@ class MeshSource(SourceBase): strength of the mesh source as a whole is the sum of all source strengths applied to the elements. - .. versionadded:: 0.14.1 + .. versionadded:: 0.15.0 Parameters ---------- @@ -1049,7 +1049,7 @@ def write_source_file( def read_source_file(filename: PathLike) -> typing.List[SourceParticle]: """Read a source file and return a list of source particles. - .. versionadded:: 0.14.1 + .. versionadded:: 0.15.0 Parameters ---------- diff --git a/openmc/stats/multivariate.py b/openmc/stats/multivariate.py index 212083c3c..ce9740ad2 100644 --- a/openmc/stats/multivariate.py +++ b/openmc/stats/multivariate.py @@ -769,7 +769,7 @@ class Box(Spatial): Whether spatial sites should only be accepted if they occur in fissionable materials - .. deprecated:: 0.14.1 + .. deprecated:: 0.15.0 Use the `constraints` argument when defining a source object instead. Attributes @@ -782,7 +782,7 @@ class Box(Spatial): Whether spatial sites should only be accepted if they occur in fissionable materials - .. deprecated:: 0.14.1 + .. deprecated:: 0.15.0 Use the `constraints` argument when defining a source object instead. """ diff --git a/openmc/trigger.py b/openmc/trigger.py index 79f5ea5af..be2537e3a 100644 --- a/openmc/trigger.py +++ b/openmc/trigger.py @@ -22,7 +22,7 @@ class Trigger(EqualityMixin): can cause the trigger to fire prematurely if there are zero scores in any bin at the first evaluation. - .. versionadded:: 0.14.1 + .. versionadded:: 0.15.0 Attributes ---------- diff --git a/tests/regression_tests/filter_cellfrom/inputs_true.dat b/tests/regression_tests/filter_cellfrom/inputs_true.dat index 8a37e8286..b85f63c6e 100644 --- a/tests/regression_tests/filter_cellfrom/inputs_true.dat +++ b/tests/regression_tests/filter_cellfrom/inputs_true.dat @@ -49,9 +49,12 @@ 15 5 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 1 diff --git a/tests/regression_tests/filter_cellfrom/test.py b/tests/regression_tests/filter_cellfrom/test.py index 8fb7509cb..5559b4c81 100644 --- a/tests/regression_tests/filter_cellfrom/test.py +++ b/tests/regression_tests/filter_cellfrom/test.py @@ -142,8 +142,9 @@ def model(): core_radius, core_height / 2.0, ] - distribution = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) - model.settings.source = openmc.IndependentSource(space=distribution) + distribution = openmc.stats.Box(bounds[:3], bounds[3:]) + model.settings.source = openmc.IndependentSource( + space=distribution, constraints={'fissionable': True}) # ============================================================================= # Tallies diff --git a/tests/regression_tests/surface_source_write/case-01/inputs_true.dat b/tests/regression_tests/surface_source_write/case-01/inputs_true.dat index ecca3a9f8..7368debd5 100644 --- a/tests/regression_tests/surface_source_write/case-01/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-01/inputs_true.dat @@ -42,9 +42,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 300 diff --git a/tests/regression_tests/surface_source_write/case-02/inputs_true.dat b/tests/regression_tests/surface_source_write/case-02/inputs_true.dat index 8be841ad6..12f15499f 100644 --- a/tests/regression_tests/surface_source_write/case-02/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-02/inputs_true.dat @@ -42,9 +42,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 8 diff --git a/tests/regression_tests/surface_source_write/case-03/inputs_true.dat b/tests/regression_tests/surface_source_write/case-03/inputs_true.dat index 38faa2a78..cd0f7deda 100644 --- a/tests/regression_tests/surface_source_write/case-03/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-03/inputs_true.dat @@ -42,9 +42,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 4 5 6 7 8 9 diff --git a/tests/regression_tests/surface_source_write/case-04/inputs_true.dat b/tests/regression_tests/surface_source_write/case-04/inputs_true.dat index 99a5a2899..ac3c03e37 100644 --- a/tests/regression_tests/surface_source_write/case-04/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-04/inputs_true.dat @@ -42,9 +42,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 4 5 6 7 8 9 diff --git a/tests/regression_tests/surface_source_write/case-05/inputs_true.dat b/tests/regression_tests/surface_source_write/case-05/inputs_true.dat index 49eedb7cf..143e85200 100644 --- a/tests/regression_tests/surface_source_write/case-05/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-05/inputs_true.dat @@ -42,9 +42,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 4 5 6 7 8 9 diff --git a/tests/regression_tests/surface_source_write/case-06/inputs_true.dat b/tests/regression_tests/surface_source_write/case-06/inputs_true.dat index cdb9726d6..f2dc7ea35 100644 --- a/tests/regression_tests/surface_source_write/case-06/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-06/inputs_true.dat @@ -42,9 +42,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 300 diff --git a/tests/regression_tests/surface_source_write/case-07/inputs_true.dat b/tests/regression_tests/surface_source_write/case-07/inputs_true.dat index ff73358ca..a6107d16f 100644 --- a/tests/regression_tests/surface_source_write/case-07/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-07/inputs_true.dat @@ -42,9 +42,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 300 diff --git a/tests/regression_tests/surface_source_write/case-08/inputs_true.dat b/tests/regression_tests/surface_source_write/case-08/inputs_true.dat index 8bda9795d..b87e2b207 100644 --- a/tests/regression_tests/surface_source_write/case-08/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-08/inputs_true.dat @@ -42,9 +42,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 300 diff --git a/tests/regression_tests/surface_source_write/case-09/inputs_true.dat b/tests/regression_tests/surface_source_write/case-09/inputs_true.dat index 4e893fffa..f4f6ebd87 100644 --- a/tests/regression_tests/surface_source_write/case-09/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-09/inputs_true.dat @@ -42,9 +42,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 300 diff --git a/tests/regression_tests/surface_source_write/case-10/inputs_true.dat b/tests/regression_tests/surface_source_write/case-10/inputs_true.dat index 2d8f1ce64..372e5c011 100644 --- a/tests/regression_tests/surface_source_write/case-10/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-10/inputs_true.dat @@ -42,9 +42,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 300 diff --git a/tests/regression_tests/surface_source_write/case-11/inputs_true.dat b/tests/regression_tests/surface_source_write/case-11/inputs_true.dat index 307dec547..d5401dc87 100644 --- a/tests/regression_tests/surface_source_write/case-11/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-11/inputs_true.dat @@ -42,9 +42,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 300 diff --git a/tests/regression_tests/surface_source_write/case-12/inputs_true.dat b/tests/regression_tests/surface_source_write/case-12/inputs_true.dat index 087024695..cb8f87741 100644 --- a/tests/regression_tests/surface_source_write/case-12/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-12/inputs_true.dat @@ -35,9 +35,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 4 5 6 7 8 9 diff --git a/tests/regression_tests/surface_source_write/case-13/inputs_true.dat b/tests/regression_tests/surface_source_write/case-13/inputs_true.dat index a3421298b..23bbf0455 100644 --- a/tests/regression_tests/surface_source_write/case-13/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-13/inputs_true.dat @@ -35,9 +35,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 4 5 6 7 8 9 diff --git a/tests/regression_tests/surface_source_write/case-14/inputs_true.dat b/tests/regression_tests/surface_source_write/case-14/inputs_true.dat index b4c141b5d..29acd94f2 100644 --- a/tests/regression_tests/surface_source_write/case-14/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-14/inputs_true.dat @@ -35,9 +35,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 4 5 6 7 8 9 diff --git a/tests/regression_tests/surface_source_write/case-15/inputs_true.dat b/tests/regression_tests/surface_source_write/case-15/inputs_true.dat index 9244d839d..c775f2f52 100644 --- a/tests/regression_tests/surface_source_write/case-15/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-15/inputs_true.dat @@ -35,9 +35,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 4 5 6 7 8 9 diff --git a/tests/regression_tests/surface_source_write/case-16/inputs_true.dat b/tests/regression_tests/surface_source_write/case-16/inputs_true.dat index b3139928a..969a8f31e 100644 --- a/tests/regression_tests/surface_source_write/case-16/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-16/inputs_true.dat @@ -35,9 +35,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 4 5 6 7 8 9 diff --git a/tests/regression_tests/surface_source_write/case-17/inputs_true.dat b/tests/regression_tests/surface_source_write/case-17/inputs_true.dat index 8fcb01e45..0a65db510 100644 --- a/tests/regression_tests/surface_source_write/case-17/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-17/inputs_true.dat @@ -35,9 +35,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 4 5 6 7 8 9 diff --git a/tests/regression_tests/surface_source_write/case-18/inputs_true.dat b/tests/regression_tests/surface_source_write/case-18/inputs_true.dat index 80d6253a1..89df27b47 100644 --- a/tests/regression_tests/surface_source_write/case-18/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-18/inputs_true.dat @@ -35,9 +35,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 4 5 6 7 8 9 diff --git a/tests/regression_tests/surface_source_write/case-19/inputs_true.dat b/tests/regression_tests/surface_source_write/case-19/inputs_true.dat index 5355675fe..3ea7dbffe 100644 --- a/tests/regression_tests/surface_source_write/case-19/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-19/inputs_true.dat @@ -35,9 +35,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 4 5 6 7 8 9 diff --git a/tests/regression_tests/surface_source_write/case-20/inputs_true.dat b/tests/regression_tests/surface_source_write/case-20/inputs_true.dat index 44f0d72c7..c3c768a3d 100644 --- a/tests/regression_tests/surface_source_write/case-20/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-20/inputs_true.dat @@ -35,9 +35,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 4 diff --git a/tests/regression_tests/surface_source_write/case-21/inputs_true.dat b/tests/regression_tests/surface_source_write/case-21/inputs_true.dat index 893b8b5b9..5dae8374d 100644 --- a/tests/regression_tests/surface_source_write/case-21/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-21/inputs_true.dat @@ -35,9 +35,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 4 diff --git a/tests/regression_tests/surface_source_write/case-a01/inputs_true.dat b/tests/regression_tests/surface_source_write/case-a01/inputs_true.dat index a65c9f770..5cec1b76f 100644 --- a/tests/regression_tests/surface_source_write/case-a01/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-a01/inputs_true.dat @@ -42,9 +42,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 1 2 3 diff --git a/tests/regression_tests/surface_source_write/case-d01/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d01/inputs_true.dat index b6157ba9b..b5452080c 100644 --- a/tests/regression_tests/surface_source_write/case-d01/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-d01/inputs_true.dat @@ -21,9 +21,12 @@ 5 1 - + -4 -4 -20 4 4 20 + + true + 300 diff --git a/tests/regression_tests/surface_source_write/case-d02/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d02/inputs_true.dat index 6877634f1..d6ca4143c 100644 --- a/tests/regression_tests/surface_source_write/case-d02/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-d02/inputs_true.dat @@ -21,9 +21,12 @@ 5 1 - + -4 -4 -20 4 4 20 + + true + 1 diff --git a/tests/regression_tests/surface_source_write/case-d03/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d03/inputs_true.dat index ddcde89e4..ba326c978 100644 --- a/tests/regression_tests/surface_source_write/case-d03/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-d03/inputs_true.dat @@ -21,9 +21,12 @@ 5 1 - + -4 -4 -20 4 4 20 + + true + 300 diff --git a/tests/regression_tests/surface_source_write/case-d04/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d04/inputs_true.dat index 7695ecc79..c309dd155 100644 --- a/tests/regression_tests/surface_source_write/case-d04/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-d04/inputs_true.dat @@ -21,9 +21,12 @@ 5 1 - + -4 -4 -20 4 4 20 + + true + 1 diff --git a/tests/regression_tests/surface_source_write/case-d05/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d05/inputs_true.dat index 865633fbb..c49703f83 100644 --- a/tests/regression_tests/surface_source_write/case-d05/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-d05/inputs_true.dat @@ -21,9 +21,12 @@ 5 1 - + -4 -4 -20 4 4 20 + + true + 300 diff --git a/tests/regression_tests/surface_source_write/case-d06/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d06/inputs_true.dat index 422670736..48db2c30d 100644 --- a/tests/regression_tests/surface_source_write/case-d06/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-d06/inputs_true.dat @@ -21,9 +21,12 @@ 5 1 - + -4 -4 -20 4 4 20 + + true + 300 diff --git a/tests/regression_tests/surface_source_write/case-d07/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d07/inputs_true.dat index 77498b64d..e2897125b 100644 --- a/tests/regression_tests/surface_source_write/case-d07/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-d07/inputs_true.dat @@ -35,9 +35,12 @@ 5 1 - + -4 -4 -20 4 4 20 + + true + 101 102 103 104 105 106 diff --git a/tests/regression_tests/surface_source_write/case-d08/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d08/inputs_true.dat index 561a38337..185b8629b 100644 --- a/tests/regression_tests/surface_source_write/case-d08/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-d08/inputs_true.dat @@ -35,9 +35,12 @@ 5 1 - + -4 -4 -20 4 4 20 + + true + 101 102 103 104 105 106 diff --git a/tests/regression_tests/surface_source_write/case-e01/inputs_true.dat b/tests/regression_tests/surface_source_write/case-e01/inputs_true.dat index 99a5a2899..ac3c03e37 100644 --- a/tests/regression_tests/surface_source_write/case-e01/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-e01/inputs_true.dat @@ -42,9 +42,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 4 5 6 7 8 9 diff --git a/tests/regression_tests/surface_source_write/case-e02/inputs_true.dat b/tests/regression_tests/surface_source_write/case-e02/inputs_true.dat index ff73358ca..a6107d16f 100644 --- a/tests/regression_tests/surface_source_write/case-e02/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-e02/inputs_true.dat @@ -42,9 +42,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 300 diff --git a/tests/regression_tests/surface_source_write/case-e03/inputs_true.dat b/tests/regression_tests/surface_source_write/case-e03/inputs_true.dat index a3421298b..23bbf0455 100644 --- a/tests/regression_tests/surface_source_write/case-e03/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-e03/inputs_true.dat @@ -35,9 +35,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 4 5 6 7 8 9 diff --git a/tests/regression_tests/surface_source_write/test.py b/tests/regression_tests/surface_source_write/test.py index f949d8085..38581736e 100644 --- a/tests/regression_tests/surface_source_write/test.py +++ b/tests/regression_tests/surface_source_write/test.py @@ -277,8 +277,9 @@ def model_1(): core_radius, core_height / 2.0, ] - distribution = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) - model.settings.source = openmc.IndependentSource(space=distribution) + distribution = openmc.stats.Box(bounds[:3], bounds[3:]) + model.settings.source = openmc.IndependentSource( + space=distribution, constraints={'fissionable': True}) return model @@ -379,8 +380,9 @@ def model_2(): core_radius, core_height / 2.0, ] - distribution = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) - model.settings.source = openmc.IndependentSource(space=distribution) + distribution = openmc.stats.Box(bounds[:3], bounds[3:]) + model.settings.source = openmc.IndependentSource( + space=distribution, constraints={'fissionable': True}) return model @@ -481,8 +483,9 @@ def model_3(): core_radius, core_height / 2.0, ] - distribution = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) - model.settings.source = openmc.IndependentSource(space=distribution) + distribution = openmc.stats.Box(bounds[:3], bounds[3:]) + model.settings.source = openmc.IndependentSource( + space=distribution, constraints={'fissionable': True}) return model @@ -594,8 +597,9 @@ def model_4(): core_radius, core_height / 2.0, ] - distribution = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) - model.settings.source = openmc.IndependentSource(space=distribution) + distribution = openmc.stats.Box(bounds[:3], bounds[3:]) + model.settings.source = openmc.IndependentSource( + space=distribution, constraints={'fissionable': True}) return model @@ -935,8 +939,9 @@ def model_dagmc_1(): model.settings.inactive = 1 model.settings.seed = 1 - source_box = openmc.stats.Box([-4, -4, -20], [4, 4, 20], only_fissionable=True) - model.settings.source = openmc.IndependentSource(space=source_box) + source_box = openmc.stats.Box([-4, -4, -20], [4, 4, 20]) + model.settings.source = openmc.IndependentSource( + space=source_box, constraints={'fissionable': True}) return model @@ -1069,8 +1074,9 @@ def model_dagmc_2(): model.settings.inactive = 1 model.settings.seed = 1 - source_box = openmc.stats.Box([-4, -4, -20], [4, 4, 20], only_fissionable=True) - model.settings.source = openmc.IndependentSource(space=source_box) + source_box = openmc.stats.Box([-4, -4, -20], [4, 4, 20]) + model.settings.source = openmc.IndependentSource( + space=source_box, constraints={'fissionable': True}) return model diff --git a/tests/unit_tests/test_tracks.py b/tests/unit_tests/test_tracks.py index 3951a72c6..fa8664159 100644 --- a/tests/unit_tests/test_tracks.py +++ b/tests/unit_tests/test_tracks.py @@ -168,7 +168,7 @@ def test_restart_track(run_in_tmpdir, sphere_model): # generate lost particle files with pytest.raises(RuntimeError, match='Maximum number of lost particles has been reached.'): - sphere_model.run(output=False) + sphere_model.run(output=False, threads=1) lost_particle_files = list(Path.cwd().glob('particle_*.h5')) assert len(lost_particle_files) > 0 From 8ce81d132ade670b66483887955fc1182358273a Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 24 Jun 2024 11:29:51 -0500 Subject: [PATCH 137/671] Change version number to 0.15.1-dev (#3058) --- CMakeLists.txt | 2 +- docs/source/conf.py | 2 +- include/openmc/version.h.in | 2 +- openmc/__init__.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8a000c9b9..3cbeaa024 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ project(openmc C CXX) # Set version numbers set(OPENMC_VERSION_MAJOR 0) set(OPENMC_VERSION_MINOR 15) -set(OPENMC_VERSION_RELEASE 0) +set(OPENMC_VERSION_RELEASE 1) set(OPENMC_VERSION ${OPENMC_VERSION_MAJOR}.${OPENMC_VERSION_MINOR}.${OPENMC_VERSION_RELEASE}) configure_file(include/openmc/version.h.in "${CMAKE_BINARY_DIR}/include/openmc/version.h" @ONLY) diff --git a/docs/source/conf.py b/docs/source/conf.py index 19c04d98a..726269f0b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -71,7 +71,7 @@ copyright = '2011-2024, Massachusetts Institute of Technology, UChicago Argonne # The short X.Y version. version = "0.15" # The full version, including alpha/beta/rc tags. -release = "0.15.0" +release = "0.15.1-dev" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/include/openmc/version.h.in b/include/openmc/version.h.in index a518e0d63..e1c2b0541 100644 --- a/include/openmc/version.h.in +++ b/include/openmc/version.h.in @@ -10,7 +10,7 @@ namespace openmc { constexpr int VERSION_MAJOR {@OPENMC_VERSION_MAJOR@}; constexpr int VERSION_MINOR {@OPENMC_VERSION_MINOR@}; constexpr int VERSION_RELEASE {@OPENMC_VERSION_RELEASE@}; -constexpr bool VERSION_DEV {false}; +constexpr bool VERSION_DEV {true}; constexpr std::array VERSION {VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE}; // clang-format on diff --git a/openmc/__init__.py b/openmc/__init__.py index 12a3c87d3..d18e1188e 100644 --- a/openmc/__init__.py +++ b/openmc/__init__.py @@ -39,4 +39,4 @@ from .config import * from openmc.model import Model -__version__ = '0.15.0' +__version__ = '0.15.1-dev' From a8171cbd4e9a1ee801a52f6f0c1426982b73774f Mon Sep 17 00:00:00 2001 From: Ethan Krammer <68677905+ethankrammer@users.noreply.github.com> Date: Thu, 27 Jun 2024 09:22:10 -0500 Subject: [PATCH 138/671] Implementation of Shannon Entropy for Random Ray (#3030) Co-authored-by: Ethan Krammer Co-authored-by: John Tramm --- docs/source/methods/eigenvalue.rst | 17 +++- docs/source/methods/random_ray.rst | 36 +++++++ docs/source/usersguide/random_ray.rst | 11 +++ src/eigenvalue.cpp | 2 +- src/random_ray/flat_source_domain.cpp | 38 +++++++- src/settings.cpp | 47 ++++++---- src/simulation.cpp | 3 +- .../random_ray_entropy/__init__.py | 0 .../random_ray_entropy/geometry.xml | 88 ++++++++++++++++++ .../random_ray_entropy/materials.xml | 8 ++ .../random_ray_entropy/mgxs.h5 | Bin 0 -> 10664 bytes .../random_ray_entropy/results_true.dat | 13 +++ .../random_ray_entropy/settings.xml | 17 ++++ .../random_ray_entropy/test.py | 33 +++++++ 14 files changed, 283 insertions(+), 30 deletions(-) create mode 100644 tests/regression_tests/random_ray_entropy/__init__.py create mode 100644 tests/regression_tests/random_ray_entropy/geometry.xml create mode 100644 tests/regression_tests/random_ray_entropy/materials.xml create mode 100644 tests/regression_tests/random_ray_entropy/mgxs.h5 create mode 100644 tests/regression_tests/random_ray_entropy/results_true.dat create mode 100644 tests/regression_tests/random_ray_entropy/settings.xml create mode 100644 tests/regression_tests/random_ray_entropy/test.py diff --git a/docs/source/methods/eigenvalue.rst b/docs/source/methods/eigenvalue.rst index b63723c02..8abcc0957 100644 --- a/docs/source/methods/eigenvalue.rst +++ b/docs/source/methods/eigenvalue.rst @@ -55,15 +55,17 @@ in :ref:`fission-bank-algorithms`. Source Convergence Issues ------------------------- +.. _methods-shannon-entropy: + Diagnosing Convergence with Shannon Entropy ------------------------------------------- As discussed earlier, it is necessary to converge both :math:`k_{eff}` and the source distribution before any tallies can begin. Moreover, the convergence rate -of the source distribution is in general slower than that of -:math:`k_{eff}`. One should thus examine not only the convergence of -:math:`k_{eff}` but also the convergence of the source distribution in order to -make decisions on when to start active batches. +of the source distribution is in general slower than that of :math:`k_{eff}`. +One should thus examine not only the convergence of :math:`k_{eff}` but also the +convergence of the source distribution in order to make decisions on when to +start active batches. However, the representation of the source distribution makes it a bit more difficult to analyze its convergence. Since :math:`k_{eff}` is a scalar @@ -108,6 +110,13 @@ at plots of :math:`k_{eff}` and the Shannon entropy. A number of methods have been proposed (see e.g. [Romano]_, [Ueki]_), but each of these is not without problems. +Shannon entropy is calculated differently for the random ray solver, as +described :ref:`in the random ray theory section +`. Additionally, as the Shannon entropy only +serves as a diagnostic tool for convergence of the fission source distribution, +there is currently no diagnostic to determine if the scattering source +distribution in random ray is converged. + --------------------------- Uniform Fission Site Method --------------------------- diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index 427ba1148..d46d3ad41 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -411,6 +411,8 @@ which when partially simplified becomes: Note that there are now four (seemingly identical) volume terms in this equation. +.. _methods-volume-dilemma: + ~~~~~~~~~~~~~~ Volume Dilemma ~~~~~~~~~~~~~~ @@ -745,6 +747,7 @@ How are Tallies Handled? Most tallies, filters, and scores that you would expect to work with a multigroup solver like random ray should work. For example, you can define 3D mesh tallies with energy filters and flux, fission, and nu-fission scores, etc. + There are some restrictions though. For starters, it is assumed that all filter mesh boundaries will conform to physical surface boundaries (or lattice boundaries) in the simulation geometry. It is acceptable for multiple cells @@ -754,6 +757,39 @@ behavior if a single simulation cell is able to score to multiple filter mesh cells. In the future, the capability to fully support mesh tallies may be added to OpenMC, but for now this restriction needs to be respected. +.. _methods-shannon-entropy-random-ray: + +----------------------------- +Shannon Entropy in Random Ray +----------------------------- + +As :math:`k_{eff}` is updated at each generation, the fission source at each FSR +is used to compute the Shannon entropy. This follows the :ref:`same procedure +for computing Shannon entropy in continuous-energy or multigroup Monte Carlo +simulations `, except that fission sources at FSRs are +considered, rather than fission sites of user-defined regular meshes. Thus, the +volume-weighted fission rate is considered instead, and the fraction of fission +sources is adjusted such that: + +.. math:: + :label: fraction-source-random-ray + + S_i = \frac{\text{Fission source in FSR $i \times$ Volume of FSR + $i$}}{\text{Total fission source}} = \frac{Q_{i} V_{i}}{\sum_{i=1}^{i=N} + Q_{i} V_{i}} + +The Shannon entropy is then computed normally as + +.. math:: + :label: shannon-entropy-random-ray + + H = - \sum_{i=1}^N S_i \log_2 S_i + +where :math:`N` is the number of FSRs. FSRs with no fission source (or, +occassionally, negative fission source, :ref:`due to the volume estimator +problem `) are skipped to avoid taking an undefined +logarithm in :eq:`shannon-entropy-random-ray`. + .. _usersguide_fixed_source_methods: ------------ diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index 61f074650..2a038b122 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -62,6 +62,17 @@ solver:: settings.batches = 1200 settings.inactive = 600 +--------------- +Shannon Entropy +--------------- + +Similar to Monte Carlo, :ref:`Shannon entropy +` can be used to gauge whether the fission +source has fully developed. The Shannon entropy is calculated automatically +after each batch and is printed to the statepoint file. Unlike Monte Carlo, an +entropy mesh does not need to be defined, as the Shannon entropy is calculated +over FSRs using a volume-weighted approach. + ------------------------------- Inactive Ray Length (Dead Zone) ------------------------------- diff --git a/src/eigenvalue.cpp b/src/eigenvalue.cpp index d5410094b..8669d76f9 100644 --- a/src/eigenvalue.cpp +++ b/src/eigenvalue.cpp @@ -556,7 +556,7 @@ void shannon_entropy() double H = 0.0; for (auto p_i : p) { if (p_i > 0.0) { - H -= p_i * std::log(p_i) / std::log(2.0); + H -= p_i * std::log2(p_i); } } diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 7f5d76943..792e97da0 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -1,6 +1,7 @@ #include "openmc/random_ray/flat_source_domain.h" #include "openmc/cell.h" +#include "openmc/eigenvalue.h" #include "openmc/geometry.h" #include "openmc/material.h" #include "openmc/message_passing.h" @@ -278,6 +279,9 @@ double FlatSourceDomain::compute_k_eff(double k_eff_old) const const int t = 0; const int a = 0; + // Vector for gathering fission source terms for Shannon entropy calculation + vector p(n_source_regions_, 0.0f); + #pragma omp parallel for reduction(+ : fission_rate_old, fission_rate_new) for (int sr = 0; sr < n_source_regions_; sr++) { @@ -300,12 +304,38 @@ double FlatSourceDomain::compute_k_eff(double k_eff_old) const sr_fission_source_new += nu_sigma_f * scalar_flux_new_[idx]; } - fission_rate_old += sr_fission_source_old * volume; - fission_rate_new += sr_fission_source_new * volume; + // Compute total fission rates in FSR + sr_fission_source_old *= volume; + sr_fission_source_new *= volume; + + // Accumulate totals + fission_rate_old += sr_fission_source_old; + fission_rate_new += sr_fission_source_new; + + // Store total fission rate in the FSR for Shannon calculation + p[sr] = sr_fission_source_new; } double k_eff_new = k_eff_old * (fission_rate_new / fission_rate_old); + double H = 0.0; + // defining an inverse sum for better performance + double inverse_sum = 1 / fission_rate_new; + +#pragma omp parallel for reduction(+ : H) + for (int sr = 0; sr < n_source_regions_; sr++) { + // Only if FSR has non-negative and non-zero fission source + if (p[sr] > 0.0f) { + // Normalize to total weight of bank sites. p_i for better performance + float p_i = p[sr] * inverse_sum; + // Sum values to obtain Shannon entropy. + H -= p_i * std::log2(p_i); + } + } + + // Adds entropy value to shared entropy vector in openmc namespace. + simulation::entropy.push_back(H); + return k_eff_new; } @@ -525,8 +555,8 @@ void FlatSourceDomain::random_ray_tally() #pragma omp atomic tally.results_(task.filter_idx, task.score_idx, TallyResult::VALUE) += score; - } // end tally task loop - } // end energy group loop + } + } // For flux tallies, the total volume of the spatial region is needed // for normalizing the flux. We store this volume in a separate tensor. diff --git a/src/settings.cpp b/src/settings.cpp index 6c3226b04..4ffa50cdf 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -627,30 +627,37 @@ void read_settings_xml(pugi::xml_node root) } } - // Shannon Entropy mesh - if (check_for_node(root, "entropy_mesh")) { - int temp = std::stoi(get_node_value(root, "entropy_mesh")); - if (model::mesh_map.find(temp) == model::mesh_map.end()) { - fatal_error(fmt::format( - "Mesh {} specified for Shannon entropy does not exist.", temp)); + // Shannon entropy + if (solver_type == SolverType::RANDOM_RAY) { + if (check_for_node(root, "entropy_mesh")) { + fatal_error("Random ray uses FSRs to compute the Shannon entropy. " + "No user-defined entropy mesh is supported."); } - - auto* m = - dynamic_cast(model::meshes[model::mesh_map.at(temp)].get()); - if (!m) - fatal_error("Only regular meshes can be used as an entropy mesh"); - simulation::entropy_mesh = m; - - // Turn on Shannon entropy calculation entropy_on = true; + } else if (solver_type == SolverType::MONTE_CARLO) { + if (check_for_node(root, "entropy_mesh")) { + int temp = std::stoi(get_node_value(root, "entropy_mesh")); + if (model::mesh_map.find(temp) == model::mesh_map.end()) { + fatal_error(fmt::format( + "Mesh {} specified for Shannon entropy does not exist.", temp)); + } - } else if (check_for_node(root, "entropy")) { - fatal_error( - "Specifying a Shannon entropy mesh via the element " - "is deprecated. Please create a mesh using and then reference " - "it by specifying its ID in an element."); + auto* m = dynamic_cast( + model::meshes[model::mesh_map.at(temp)].get()); + if (!m) + fatal_error("Only regular meshes can be used as an entropy mesh"); + simulation::entropy_mesh = m; + + // Turn on Shannon entropy calculation + entropy_on = true; + + } else if (check_for_node(root, "entropy")) { + fatal_error( + "Specifying a Shannon entropy mesh via the element " + "is deprecated. Please create a mesh using and then reference " + "it by specifying its ID in an element."); + } } - // Uniform fission source weighting mesh if (check_for_node(root, "ufs_mesh")) { auto temp = std::stoi(get_node_value(root, "ufs_mesh")); diff --git a/src/simulation.cpp b/src/simulation.cpp index 2d5d75aa4..be03e4855 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -531,7 +531,8 @@ void finalize_generation() if (settings::run_mode == RunMode::EIGENVALUE) { // Calculate shannon entropy - if (settings::entropy_on) + if (settings::entropy_on && + settings::solver_type == SolverType::MONTE_CARLO) shannon_entropy(); // Collect results and statistics diff --git a/tests/regression_tests/random_ray_entropy/__init__.py b/tests/regression_tests/random_ray_entropy/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_entropy/geometry.xml b/tests/regression_tests/random_ray_entropy/geometry.xml new file mode 100644 index 000000000..4c87bbbfb --- /dev/null +++ b/tests/regression_tests/random_ray_entropy/geometry.xml @@ -0,0 +1,88 @@ + + + + + + 12.5 12.5 12.5 + 8 8 8 + 0.0 0.0 0.0 + +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 + +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 + +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 + +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 + +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 + +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 + +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 + +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 + + + + + + + + diff --git a/tests/regression_tests/random_ray_entropy/materials.xml b/tests/regression_tests/random_ray_entropy/materials.xml new file mode 100644 index 000000000..5a6f93414 --- /dev/null +++ b/tests/regression_tests/random_ray_entropy/materials.xml @@ -0,0 +1,8 @@ + + + mgxs.h5 + + + + + diff --git a/tests/regression_tests/random_ray_entropy/mgxs.h5 b/tests/regression_tests/random_ray_entropy/mgxs.h5 new file mode 100644 index 0000000000000000000000000000000000000000..6ce80bf713a8d5ff29700ba59e982e18f99b5403 GIT binary patch literal 10664 zcmeHN%}*0S6o1<)ELx=~2P1yeL;JB5M`%OA4UJMuu{yU;m}}jFYub= zpCp>Wc%w?`qzL4JLLk-KJs<`=jH9M_TERFYFm72kyJxt}HjSLnQCudeLWQygKO6SV(mu^{#vDtpJMgnd zH{3=&5sl;2%Eu!cjybM9GwIIQ3|eA|^>K9`h0dQi?=_$Ct!2L!-+%vhQ~3XQ!s+(o zk0k{bjN?Gc@yAL;^%%{l`++9`bj_V4+@MK6%Q@D1c^N!HbJV{c83gv_Q{XH7@ zLUFe!NP%DEcLnpS4N4!}|Be9huotfg$Yp`!8G*|jVO6cwN1#3`^AJf<+$ z;db}k2Yj;uLxT^6kOlNSNdC@?g5Pacu$ja1uHg#02Ep%5%2c{A9m}hQ&q)QJ zb5gOo1FX=yIu|vppSS1FZtb80ix}W%XQ$2Y2?A0_%E1fc90x*l}8~J3RX;`i{jvLfPglKxS;(OKWW1?go4)haasERZ?)r} zg}UG$wGRwK45412!0~}z?V16K?LHMyO*|C8v~|7xQd)UjRl3eE`ovRdPeNFIJeKMT Fk6-_XIk5l$ literal 0 HcmV?d00001 diff --git a/tests/regression_tests/random_ray_entropy/results_true.dat b/tests/regression_tests/random_ray_entropy/results_true.dat new file mode 100644 index 000000000..254254538 --- /dev/null +++ b/tests/regression_tests/random_ray_entropy/results_true.dat @@ -0,0 +1,13 @@ +k-combined: +1.000000E+00 0.000000E+00 +entropy: +8.863421E+00 +8.933584E+00 +8.960553E+00 +8.967921E+00 +8.976016E+00 +8.981856E+00 +8.983670E+00 +8.986584E+00 +8.987732E+00 +8.988186E+00 diff --git a/tests/regression_tests/random_ray_entropy/settings.xml b/tests/regression_tests/random_ray_entropy/settings.xml new file mode 100644 index 000000000..81deaa775 --- /dev/null +++ b/tests/regression_tests/random_ray_entropy/settings.xml @@ -0,0 +1,17 @@ + + + eigenvalue + 100 + 10 + 5 + multi-group + + + + 0.0 0.0 0.0 100.0 100.0 100.0 + + + 40.0 + 400.0 + + diff --git a/tests/regression_tests/random_ray_entropy/test.py b/tests/regression_tests/random_ray_entropy/test.py new file mode 100644 index 000000000..a3cba65ad --- /dev/null +++ b/tests/regression_tests/random_ray_entropy/test.py @@ -0,0 +1,33 @@ +import glob +import os + +from openmc import StatePoint + +from tests.testing_harness import TestHarness + + +class EntropyTestHarness(TestHarness): + def _get_results(self): + """Digest info in the statepoint and return as a string.""" + # Read the statepoint file. + statepoint = glob.glob(os.path.join(os.getcwd(), self._sp_name))[0] + with StatePoint(statepoint) as sp: + # Write out k-combined. + outstr = 'k-combined:\n' + outstr += '{:12.6E} {:12.6E}\n'.format(sp.keff.n, sp.keff.s) + + # Write out entropy data. + outstr += 'entropy:\n' + results = ['{:12.6E}'.format(x) for x in sp.entropy] + outstr += '\n'.join(results) + '\n' + + return outstr + +''' +# This test is adapted from "Monte Carlo power iteration: Entropy and spatial correlations," +M. Nowak et al. The cross sections are defined explicitly so that the value for entropy +is exactly 9 and the eigenvalue is exactly 1. +''' +def test_entropy(): + harness = EntropyTestHarness('statepoint.10.h5') + harness.main() From ac0ad0bac2aaae0b71f625248b56933bc1138699 Mon Sep 17 00:00:00 2001 From: Olek <45364492+yardasol@users.noreply.github.com> Date: Fri, 28 Jun 2024 10:18:17 -0400 Subject: [PATCH 139/671] Fix hyperlinks in `random_ray.rst` (#3064) --- docs/source/methods/random_ray.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index d46d3ad41..7a29a1875 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -10,7 +10,7 @@ Random Ray What is Random Ray? ------------------- -`Random ray `_ is a stochastic transport method, closely related to +`Random ray `_ is a stochastic transport method, closely related to the deterministic Method of Characteristics (MOC) [Askew-1972]_. Rather than each ray representing a single neutron as in Monte Carlo, it represents a characteristic line through the simulation geometry upon which the transport @@ -82,7 +82,7 @@ Random Ray Numerical Derivation In this section, we will derive the numerical basis for the random ray solver mode in OpenMC. The derivation of random ray is also discussed in several papers -(`1 `_, `2 `_, `3 `_), and some of those +(`1 `_, `2 `_, `3 `_), and some of those derivations are reproduced here verbatim. Several extensions are also made to add clarity, particularly on the topic of OpenMC's treatment of cell volumes in the random ray solver. @@ -428,7 +428,7 @@ of terms. Mathematically, such cancellation allows us to arrive at the following This derivation appears mathematically sound at first glance but unfortunately raises a serious issue as discussed in more depth by `Tramm et al. -`_ and `Cosgrove and Tramm `_. Namely, the second +`_ and `Cosgrove and Tramm `_. Namely, the second term: .. math:: @@ -607,7 +607,7 @@ guess can be made by taking the isotropic source from the FSR the ray was sampled in, direct usage of this quantity would result in significant bias and error being imparted on the simulation. -Thus, an `on-the-fly approximation method `_ was developed (known +Thus, an `on-the-fly approximation method `_ was developed (known as the "dead zone"), where the first several mean free paths of a ray are considered to be "inactive" or "read only". In this sense, the angular flux is solved for using the MOC equation, but the ray does not "tally" any scalar flux From 391450ce01c4222c4a24466d749ec1dc5ac61b19 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Fri, 28 Jun 2024 11:41:20 -0500 Subject: [PATCH 140/671] Random Ray Normalization Improvements (#3051) Co-authored-by: Olek <45364492+yardasol@users.noreply.github.com> --- docs/source/methods/random_ray.rst | 14 + docs/source/usersguide/random_ray.rst | 21 + .../openmc/random_ray/flat_source_domain.h | 5 + openmc/settings.py | 12 + src/random_ray/flat_source_domain.cpp | 123 +- src/settings.cpp | 4 + .../random_ray_basic/inputs_true.dat | 1 + .../regression_tests/random_ray_basic/test.py | 1 + .../False_hybrid/inputs_true.dat | 1018 +++++++++++++++++ .../False_hybrid/results_true.dat | 9 + .../True_hybrid/inputs_true.dat | 1018 +++++++++++++++++ .../True_hybrid/results_true.dat | 9 + .../random_ray_cube/__init__.py | 0 .../regression_tests/random_ray_cube/test.py | 231 ++++ .../cell/inputs_true.dat | 1 + .../cell/results_true.dat | 86 +- .../material/inputs_true.dat | 1 + .../material/results_true.dat | 84 +- .../random_ray_fixed_source/test.py | 1 + .../universe/inputs_true.dat | 1 + .../universe/results_true.dat | 84 +- .../random_ray_vacuum/inputs_true.dat | 1 + .../random_ray_vacuum/test.py | 1 + 23 files changed, 2572 insertions(+), 154 deletions(-) create mode 100644 tests/regression_tests/random_ray_cube/False_hybrid/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_cube/False_hybrid/results_true.dat create mode 100644 tests/regression_tests/random_ray_cube/True_hybrid/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_cube/True_hybrid/results_true.dat create mode 100644 tests/regression_tests/random_ray_cube/__init__.py create mode 100644 tests/regression_tests/random_ray_cube/test.py diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index 7a29a1875..45af2b0c8 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -740,6 +740,8 @@ scalar flux value for the FSR). global::volume[fsr] += s; } +.. _methods_random_tallies: + ------------------------ How are Tallies Handled? ------------------------ @@ -757,6 +759,18 @@ behavior if a single simulation cell is able to score to multiple filter mesh cells. In the future, the capability to fully support mesh tallies may be added to OpenMC, but for now this restriction needs to be respected. +Flux tallies are handled slightly differently than in Monte Carlo. By default, +in MC, flux tallies are reported in units of tracklength (cm), so must be +manually normalized by volume by the user to produce an estimate of flux in +units of cm\ :sup:`-2`\. Alternatively, MC flux tallies can be normalized via a +separated volume calculation process as discussed in the :ref:`Volume +Calculation Section`. In random ray, as the volumes are +computed on-the-fly as part of the transport process, the flux tallies can +easily be reported either in units of flux (cm\ :sup:`-2`\) or tracklength (cm). +By default, the unnormalized flux values (units of cm) will be reported. If the +user wishes to received volume normalized flux tallies, then an option for this +is available, as described in the :ref:`User Guide`. + .. _methods-shannon-entropy-random-ray: ----------------------------- diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index 2a038b122..6df132859 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -330,6 +330,8 @@ of a two-dimensional 2x2 reflective pincell lattice: In the future, automated subdivision of FSRs via mesh overlay may be supported. +.. _usersguide_flux_norm: + ------- Tallies ------- @@ -367,6 +369,25 @@ Note that there is no difference between the analog, tracklength, and collision estimators in random ray mode as individual particles are not being simulated. Tracklength-style tally estimation is inherent to the random ray method. +As discussed in the random ray theory section on :ref:`Random Ray +Tallies`, by default flux tallies in the random ray mode +are not normalized by the spatial tally volumes such that flux tallies are in +units of cm. While the volume information is readily available as a byproduct of +random ray integration, the flux value is reported in unnormalized units of cm +so that the user will be able to compare "apples to apples" with the default +flux tallies from the Monte Carlo solver (also reported by default in units of +cm). If volume normalized flux tallies (in units of cm\ :sup:`-2`) are desired, +then the user can set the ``volume_normalized_flux_tallies`` field in the +:attr:`openmc.Settings.random_ray` dictionary to ``True``. An example is given +below: + +:: + + settings.random_ray['volume_normalized_flux_tallies'] = True + +Note that MC mode flux tallies can also be normalized by volume, as discussed in +the :ref:`Volume Calculation Section` of the user guide. + -------- Plotting -------- diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index badbb25fc..519384040 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -108,6 +108,11 @@ public: void all_reduce_replicated_source_regions(); void convert_external_sources(); void count_external_source_regions(); + double compute_fixed_source_normalization_factor() const; + + //---------------------------------------------------------------------------- + // Static Data members + static bool volume_normalized_flux_tallies_; //---------------------------------------------------------------------------- // Public Data members diff --git a/openmc/settings.py b/openmc/settings.py index fae6f6383..b048c3a8b 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -157,6 +157,12 @@ class Settings: :ray_source: Starting ray distribution (must be uniform in space and angle) as specified by a :class:`openmc.SourceBase` object. + :volume_normalized_flux_tallies: + Whether to normalize flux tallies by volume (bool). The default + is 'False'. When enabled, flux tallies will be reported in units of + cm/cm^3. When disabled, flux tallies will be reported in units + of cm (i.e., total distance traveled by neutrons in the spatial + tally region). .. versionadded:: 0.15.0 resonance_scattering : dict @@ -1084,6 +1090,8 @@ class Settings: random_ray[key], 0.0, True) elif key == 'ray_source': cv.check_type('random ray source', random_ray[key], SourceBase) + elif key == 'volume_normalized_flux_tallies': + cv.check_type('volume normalized flux tallies', random_ray[key], bool) else: raise ValueError(f'Unable to set random ray to "{key}" which is ' 'unsupported by OpenMC') @@ -1877,6 +1885,10 @@ class Settings: elif child.tag == 'source': source = SourceBase.from_xml_element(child) self.random_ray['ray_source'] = source + elif child.tag == 'volume_normalized_flux_tallies': + self.random_ray['volume_normalized_flux_tallies'] = ( + child.text in ('true', '1') + ) def to_xml_element(self, mesh_memo=None): """Create a 'settings' element to be written to an XML file. diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 792e97da0..29b72c77b 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -23,6 +23,9 @@ namespace openmc { // FlatSourceDomain implementation //============================================================================== +// Static Variable Declarations +bool FlatSourceDomain::volume_normalized_flux_tallies_ {false}; + FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) { // Count the number of source regions, compute the cell offset @@ -81,15 +84,17 @@ FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) } // Initialize tally volumes - tally_volumes_.resize(model::tallies.size()); - for (int i = 0; i < model::tallies.size(); i++) { - // Get the shape of the 3D result tensor - auto shape = model::tallies[i]->results().shape(); + if (volume_normalized_flux_tallies_) { + tally_volumes_.resize(model::tallies.size()); + for (int i = 0; i < model::tallies.size(); i++) { + // Get the shape of the 3D result tensor + auto shape = model::tallies[i]->results().shape(); - // 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]}); + // 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]}); + } } // Compute simulation domain volume based on ray source @@ -462,13 +467,65 @@ void FlatSourceDomain::convert_source_regions_to_tallies() // Set the volume accumulators to zero for all tallies void FlatSourceDomain::reset_tally_volumes() { + if (volume_normalized_flux_tallies_) { #pragma omp parallel for - for (int i = 0; i < tally_volumes_.size(); i++) { - auto& tensor = tally_volumes_[i]; - tensor.fill(0.0); // Set all elements of the tensor to 0.0 + for (int i = 0; i < tally_volumes_.size(); i++) { + auto& tensor = tally_volumes_[i]; + tensor.fill(0.0); // Set all elements of the tensor to 0.0 + } } } +// In fixed source mode, due to the way that volumetric fixed sources are +// converted and applied as volumetric sources in one or more source regions, +// we need to perform an additional normalization step to ensure that the +// reported scalar fluxes are in units per source neutron. This allows for +// direct comparison of reported tallies to Monte Carlo flux results. +// This factor needs to be computed at each iteration, as it is based on the +// volume estimate of each FSR, which improves over the course of the simulation +double FlatSourceDomain::compute_fixed_source_normalization_factor() const +{ + // If we are not in fixed source mode, then there are no external sources + // so no normalization is needed. + if (settings::run_mode != RunMode::FIXED_SOURCE) { + return 1.0; + } + + // Step 1 is to sum over all source regions and energy groups to get the + // total external source strength in the simulation. + double simulation_external_source_strength = 0.0; +#pragma omp parallel for reduction(+ : simulation_external_source_strength) + for (int sr = 0; sr < n_source_regions_; sr++) { + int material = material_[sr]; + double volume = volume_[sr] * simulation_volume_; + for (int e = 0; e < negroups_; e++) { + // Temperature and angle indices, if using multiple temperature + // data sets and/or anisotropic data sets. + // TODO: Currently assumes we are only using single temp/single + // angle data. + const int t = 0; + const int a = 0; + float sigma_t = data::mg.macro_xs_[material].get_xs( + MgxsType::TOTAL, e, nullptr, nullptr, nullptr, t, a); + simulation_external_source_strength += + external_source_[sr * negroups_ + e] * sigma_t * volume; + } + } + + // Step 2 is to determine the total user-specified external source strength + double user_external_source_strength = 0.0; + for (auto& ext_source : model::external_sources) { + user_external_source_strength += ext_source->strength(); + } + + // The correction factor is the ratio of the user-specified external source + // strength to the simulation external source strength. + double source_normalization_factor = + user_external_source_strength / simulation_external_source_strength; + + return source_normalization_factor; +} + // Tallying in random ray is not done directly during transport, rather, // it is done only once after each power iteration. This is made possible // by way of a mapping data structure that relates spatial source regions @@ -492,6 +549,9 @@ void FlatSourceDomain::random_ray_tally() const int t = 0; const int a = 0; + double source_normalization_factor = + compute_fixed_source_normalization_factor(); + // We loop over all source regions and energy groups. For each // element, we check if there are any scores needed and apply // them. @@ -510,7 +570,7 @@ void FlatSourceDomain::random_ray_tally() double material = material_[sr]; for (int g = 0; g < negroups_; g++) { int idx = sr * negroups_ + g; - double flux = scalar_flux_new_[idx]; + double flux = scalar_flux_new_[idx] * source_normalization_factor; // Determine numerical score value for (auto& task : tally_task_[idx]) { @@ -561,11 +621,13 @@ void FlatSourceDomain::random_ray_tally() // For flux tallies, the total volume of the spatial region is needed // for normalizing the flux. We store this volume in a separate tensor. // We only contribute to each volume tally bin once per FSR. - for (const auto& task : volume_task_[sr]) { - if (task.score_type == SCORE_FLUX) { + if (volume_normalized_flux_tallies_) { + for (const auto& task : volume_task_[sr]) { + if (task.score_type == SCORE_FLUX) { #pragma omp atomic - tally_volumes_[task.tally_idx](task.filter_idx, task.score_idx) += - volume; + tally_volumes_[task.tally_idx](task.filter_idx, task.score_idx) += + volume; + } } } } // end FSR loop @@ -575,16 +637,18 @@ void FlatSourceDomain::random_ray_tally() // and then scores. For each score, we check the tally data structure to // see what index that score corresponds to. If that score is a flux score, // then we divide it by volume. - for (int i = 0; i < model::tallies.size(); i++) { - Tally& tally {*model::tallies[i]}; + if (volume_normalized_flux_tallies_) { + for (int i = 0; i < model::tallies.size(); i++) { + Tally& tally {*model::tallies[i]}; #pragma omp parallel for - for (int bin = 0; bin < tally.n_filter_bins(); bin++) { - for (int score_idx = 0; score_idx < tally.n_scores(); score_idx++) { - auto score_type = tally.scores_[score_idx]; - if (score_type == SCORE_FLUX) { - double vol = tally_volumes_[i](bin, score_idx); - if (vol > 0.0) { - tally.results_(bin, score_idx, TallyResult::VALUE) /= vol; + for (int bin = 0; bin < tally.n_filter_bins(); bin++) { + for (int score_idx = 0; score_idx < tally.n_scores(); score_idx++) { + auto score_type = tally.scores_[score_idx]; + if (score_type == SCORE_FLUX) { + double vol = tally_volumes_[i](bin, score_idx); + if (vol > 0.0) { + tally.results_(bin, score_idx, TallyResult::VALUE) /= vol; + } } } } @@ -767,6 +831,9 @@ void FlatSourceDomain::output_to_vtk() const } } + double source_normalization_factor = + compute_fixed_source_normalization_factor(); + // Open file for writing std::FILE* plot = std::fopen(filename.c_str(), "wb"); @@ -786,7 +853,8 @@ void FlatSourceDomain::output_to_vtk() const std::fprintf(plot, "LOOKUP_TABLE default\n"); for (int fsr : voxel_indices) { int64_t source_element = fsr * negroups_ + g; - float flux = scalar_flux_final_[source_element]; + float flux = + scalar_flux_final_[source_element] * source_normalization_factor; flux /= (settings::n_batches - settings::n_inactive); flux = convert_to_big_endian(flux); std::fwrite(&flux, sizeof(float), 1, plot); @@ -819,7 +887,8 @@ void FlatSourceDomain::output_to_vtk() const int mat = material_[fsr]; for (int g = 0; g < negroups_; g++) { int64_t source_element = fsr * negroups_ + g; - float flux = scalar_flux_final_[source_element]; + float flux = + scalar_flux_final_[source_element] * source_normalization_factor; flux /= (settings::n_batches - settings::n_inactive); float Sigma_f = data::mg.macro_xs_[mat].get_xs( MgxsType::FISSION, g, nullptr, nullptr, nullptr, 0, 0); diff --git a/src/settings.cpp b/src/settings.cpp index 4ffa50cdf..b92b77df9 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -269,6 +269,10 @@ void get_run_parameters(pugi::xml_node node_base) } else { fatal_error("Specify random ray source in settings XML"); } + if (check_for_node(random_ray_node, "volume_normalized_flux_tallies")) { + FlatSourceDomain::volume_normalized_flux_tallies_ = + get_node_value_bool(random_ray_node, "volume_normalized_flux_tallies"); + } } } diff --git a/tests/regression_tests/random_ray_basic/inputs_true.dat b/tests/regression_tests/random_ray_basic/inputs_true.dat index 97b7906f7..a2b058f2b 100644 --- a/tests/regression_tests/random_ray_basic/inputs_true.dat +++ b/tests/regression_tests/random_ray_basic/inputs_true.dat @@ -85,6 +85,7 @@ -1.26 -1.26 -1 1.26 1.26 1 + True diff --git a/tests/regression_tests/random_ray_basic/test.py b/tests/regression_tests/random_ray_basic/test.py index 1727a6371..c00d25880 100644 --- a/tests/regression_tests/random_ray_basic/test.py +++ b/tests/regression_tests/random_ray_basic/test.py @@ -190,6 +190,7 @@ def random_ray_model() -> openmc.Model: settings.random_ray['distance_active'] = 100.0 settings.random_ray['distance_inactive'] = 20.0 settings.random_ray['ray_source'] = rr_source + settings.random_ray['volume_normalized_flux_tallies'] = True ############################################################################### # Define tallies diff --git a/tests/regression_tests/random_ray_cube/False_hybrid/inputs_true.dat b/tests/regression_tests/random_ray_cube/False_hybrid/inputs_true.dat new file mode 100644 index 000000000..82c24291b --- /dev/null +++ b/tests/regression_tests/random_ray_cube/False_hybrid/inputs_true.dat @@ -0,0 +1,1018 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 1.0 1.0 1.0 + 30 30 30 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + False + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_cube/False_hybrid/results_true.dat b/tests/regression_tests/random_ray_cube/False_hybrid/results_true.dat new file mode 100644 index 000000000..88718c5bf --- /dev/null +++ b/tests/regression_tests/random_ray_cube/False_hybrid/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +-5.920549E+04 +2.321317E+09 +tally 2: +4.761087E+02 +4.909570E+04 +tally 3: +2.206028E+01 +1.019164E+02 diff --git a/tests/regression_tests/random_ray_cube/True_hybrid/inputs_true.dat b/tests/regression_tests/random_ray_cube/True_hybrid/inputs_true.dat new file mode 100644 index 000000000..464c34404 --- /dev/null +++ b/tests/regression_tests/random_ray_cube/True_hybrid/inputs_true.dat @@ -0,0 +1,1018 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 1.0 1.0 1.0 + 30 30 30 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_cube/True_hybrid/results_true.dat b/tests/regression_tests/random_ray_cube/True_hybrid/results_true.dat new file mode 100644 index 000000000..4a2996740 --- /dev/null +++ b/tests/regression_tests/random_ray_cube/True_hybrid/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +-5.230164E+02 +1.818988E+05 +tally 2: +3.067508E-02 +2.037701E-04 +tally 3: +1.941220E-03 +7.892297E-07 diff --git a/tests/regression_tests/random_ray_cube/__init__.py b/tests/regression_tests/random_ray_cube/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_cube/test.py b/tests/regression_tests/random_ray_cube/test.py new file mode 100644 index 000000000..51d61ae79 --- /dev/null +++ b/tests/regression_tests/random_ray_cube/test.py @@ -0,0 +1,231 @@ +import os + +import numpy as np +import openmc +from openmc.utility_funcs import change_directory +import pytest + +from tests.testing_harness import TolerantPyAPITestHarness + +# Creates a 3 region cube with different fills in each region +def fill_cube(N, n_1, n_2, fill_1, fill_2, fill_3): + cube = [[[0 for _ in range(N)] for _ in range(N)] for _ in range(N)] + for i in range(N): + for j in range(N): + for k in range(N): + if i < n_1 and j >= (N-n_1) and k < n_1: + cube[i][j][k] = fill_1 + elif i < n_2 and j >= (N-n_2) and k < n_2: + cube[i][j][k] = fill_2 + else: + cube[i][j][k] = fill_3 + return cube + +class MGXSTestHarness(TolerantPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + +def create_random_ray_model(volume_normalize, estimator): + openmc.reset_auto_ids() + ############################################################################### + # Create multigroup data + + # Instantiate the energy group data + ebins = [1e-5, 20.0e6] + groups = openmc.mgxs.EnergyGroups(group_edges=ebins) + + void_sigma_a = 4.0e-6 + void_sigma_s = 3.0e-4 + void_mat_data = openmc.XSdata('void', groups) + void_mat_data.order = 0 + void_mat_data.set_total([void_sigma_a + void_sigma_s]) + void_mat_data.set_absorption([void_sigma_a]) + void_mat_data.set_scatter_matrix(np.rollaxis(np.array([[[void_sigma_s]]]),0,3)) + + absorber_sigma_a = 0.75 + absorber_sigma_s = 0.25 + absorber_mat_data = openmc.XSdata('absorber', groups) + absorber_mat_data.order = 0 + absorber_mat_data.set_total([absorber_sigma_a + absorber_sigma_s]) + absorber_mat_data.set_absorption([absorber_sigma_a]) + absorber_mat_data.set_scatter_matrix(np.rollaxis(np.array([[[absorber_sigma_s]]]),0,3)) + + multiplier = 0.1 + source_sigma_a = void_sigma_a * multiplier + source_sigma_s = void_sigma_s * multiplier + source_mat_data = openmc.XSdata('source', groups) + source_mat_data.order = 0 + print(source_sigma_a + source_sigma_s) + source_mat_data.set_total([source_sigma_a + source_sigma_s]) + source_mat_data.set_absorption([source_sigma_a]) + source_mat_data.set_scatter_matrix(np.rollaxis(np.array([[[source_sigma_s]]]),0,3)) + + mg_cross_sections_file = openmc.MGXSLibrary(groups) + mg_cross_sections_file.add_xsdatas([source_mat_data, void_mat_data, absorber_mat_data]) + mg_cross_sections_file.export_to_hdf5() + + ############################################################################### + # Create materials for the problem + + # Instantiate some Macroscopic Data + source_data = openmc.Macroscopic('source') + void_data = openmc.Macroscopic('void') + absorber_data = openmc.Macroscopic('absorber') + + # Instantiate some Materials and register the appropriate Macroscopic objects + source_mat = openmc.Material(name='source') + source_mat.set_density('macro', 1.0) + source_mat.add_macroscopic(source_data) + + void_mat = openmc.Material(name='void') + void_mat.set_density('macro', 1.0) + void_mat.add_macroscopic(void_data) + + absorber_mat = openmc.Material(name='absorber') + absorber_mat.set_density('macro', 1.0) + absorber_mat.add_macroscopic(absorber_data) + + # Instantiate a Materials collection and export to XML + materials_file = openmc.Materials([source_mat, void_mat, absorber_mat]) + materials_file.cross_sections = "mgxs.h5" + + ############################################################################### + # Define problem geometry + + source_cell = openmc.Cell(fill=source_mat, name='infinite source region') + void_cell = openmc.Cell(fill=void_mat, name='infinite void region') + absorber_cell = openmc.Cell(fill=absorber_mat, name='infinite absorber region') + + source_universe = openmc.Universe() + source_universe.add_cells([source_cell]) + + void_universe = openmc.Universe() + void_universe.add_cells([void_cell]) + + absorber_universe = openmc.Universe() + absorber_universe.add_cells([absorber_cell]) + + absorber_width = 30.0 + n_base = 6 + + # This variable can be increased above 1 to refine the FSR mesh resolution further + refinement_level = 5 + + n = n_base * refinement_level + pitch = absorber_width / n + + pattern = fill_cube(n, 1*refinement_level, 5*refinement_level, source_universe, void_universe, absorber_universe) + + lattice = openmc.RectLattice() + lattice.lower_left = [0.0, 0.0, 0.0] + lattice.pitch = [pitch, pitch, pitch] + lattice.universes = pattern + #print(lattice) + + lattice_cell = openmc.Cell(fill=lattice) + + lattice_uni = openmc.Universe() + lattice_uni.add_cells([lattice_cell]) + + x_low = openmc.XPlane(x0=0.0,boundary_type='reflective') + x_high = openmc.XPlane(x0=absorber_width,boundary_type='vacuum') + + y_low = openmc.YPlane(y0=0.0,boundary_type='reflective') + y_high = openmc.YPlane(y0=absorber_width,boundary_type='vacuum') + + z_low = openmc.ZPlane(z0=0.0,boundary_type='reflective') + z_high = openmc.ZPlane(z0=absorber_width,boundary_type='vacuum') + + full_domain = openmc.Cell(fill=lattice_uni, region=+x_low & -x_high & +y_low & -y_high & +z_low & -z_high, name='full domain') + + root = openmc.Universe(name='root universe') + root.add_cell(full_domain) + + # Create a geometry with the two cells and export to XML + geometry = openmc.Geometry(root) + + ############################################################################### + # Define problem settings + + # Instantiate a Settings object, set all runtime parameters, and export to XML + settings = openmc.Settings() + settings.energy_mode = "multi-group" + settings.inactive = 5 + settings.batches = 10 + settings.particles = 90 + settings.run_mode = 'fixed source' + + # Create an initial uniform spatial source for ray integration + lower_left_ray = [0.0, 0.0, 0.0] + upper_right_ray = [absorber_width, absorber_width, absorber_width] + uniform_dist_ray = openmc.stats.Box(lower_left_ray, upper_right_ray, only_fissionable=False) + rr_source = openmc.IndependentSource(space=uniform_dist_ray) + + settings.random_ray['distance_active'] = 500.0 + settings.random_ray['distance_inactive'] = 100.0 + settings.random_ray['ray_source'] = rr_source + settings.random_ray['volume_normalized_flux_tallies'] = volume_normalize + + #settings.random_ray['volume_estimator'] = estimator + + # Create the neutron source in the bottom right of the moderator + strengths = [1.0] # Good - fast group appears largest (besides most thermal) + midpoints = [100.0] + energy_distribution = openmc.stats.Discrete(x=midpoints,p=strengths) + + source = openmc.IndependentSource(energy=energy_distribution, constraints={'domains':[source_universe]}, strength=3.14) + + settings.source = [source] + + ############################################################################### + # Define tallies + + estimator = 'tracklength' + + absorber_filter = openmc.MaterialFilter(absorber_mat) + absorber_tally = openmc.Tally(name="Absorber Tally") + absorber_tally.filters = [absorber_filter] + absorber_tally.scores = ['flux'] + absorber_tally.estimator = estimator + + void_filter = openmc.MaterialFilter(void_mat) + void_tally = openmc.Tally(name="Void Tally") + void_tally.filters = [void_filter] + void_tally.scores = ['flux'] + void_tally.estimator = estimator + + source_filter = openmc.MaterialFilter(source_mat) + source_tally = openmc.Tally(name="Source Tally") + source_tally.filters = [source_filter] + source_tally.scores = ['flux'] + source_tally.estimator = estimator + + # Instantiate a Tallies collection and export to XML + tallies = openmc.Tallies([source_tally, void_tally, absorber_tally]) + + ############################################################################### + # Assmble Model + + model = openmc.model.Model() + model.geometry = geometry + model.materials = materials_file + model.settings = settings + model.xs_data = mg_cross_sections_file + model.tallies = tallies + + return model + +@pytest.mark.parametrize("volume_normalize, estimator", [ + (False, "hybrid"), + (True, "hybrid") +]) +def test_random_ray_cube(volume_normalize, estimator): + # Generating a unique directory name from the parameters + directory_name = f"{volume_normalize}_{estimator}" + with change_directory(directory_name): + model = create_random_ray_model(volume_normalize, estimator) + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() \ No newline at end of file diff --git a/tests/regression_tests/random_ray_fixed_source/cell/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source/cell/inputs_true.dat index 99e67745a..a8eaa0096 100644 --- a/tests/regression_tests/random_ray_fixed_source/cell/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source/cell/inputs_true.dat @@ -153,6 +153,7 @@ 400.0 100.0 + False 0.0 0.0 0.0 60.0 100.0 60.0 diff --git a/tests/regression_tests/random_ray_fixed_source/cell/results_true.dat b/tests/regression_tests/random_ray_fixed_source/cell/results_true.dat index df923ab23..ba1254d38 100644 --- a/tests/regression_tests/random_ray_fixed_source/cell/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source/cell/results_true.dat @@ -1,47 +1,47 @@ tally 1: -3.751048E+01 +3.751047E+01 2.841797E+02 -9.930788E+00 -1.988180E+01 -3.781121E+00 -3.005165E+00 -2.383139E+00 -1.232560E+00 -1.561884E+00 -6.440577E-01 -1.089787E+00 -3.724896E-01 -6.608456E-01 -1.285592E-01 -2.372611E-01 -1.601299E-02 -7.814803E-02 -1.765829E-03 -2.862108E-02 -2.460129E-04 +1.000671E+01 +2.020214E+01 +3.979569E+00 +3.330838E+00 +2.552971E+00 +1.407542E+00 +1.736764E+00 +7.948004E-01 +1.178834E+00 +4.328006E-01 +6.953895E-01 +1.408391E-01 +2.389347E-01 +1.617450E-02 +8.128516E-02 +1.903860E-03 +2.764062E-02 +2.285458E-04 tally 2: -1.089787E+00 -3.724896E-01 -3.767926E-01 -3.724399E-02 -8.614121E-02 -1.526889E-03 -3.610725E-02 -2.629885E-04 -1.466261E-02 -4.536997E-05 -4.653106E-03 -4.381672E-06 +1.178834E+00 +4.328006E-01 +3.982124E-01 +4.138078E-02 +9.676374E-02 +1.919297E-03 +3.944531E-02 +3.133733E-04 +1.554518E-02 +5.084822E-05 +4.815079E-03 +4.681450E-06 tally 3: -1.617918E-03 -6.317049E-07 -1.161473E-03 -2.789553E-07 -1.198879E-03 -3.189531E-07 -1.031737E-03 -2.207381E-07 -5.466329E-04 -6.166808E-08 -2.146062E-04 -9.937520E-09 +1.635823E-03 +6.482611E-07 +1.248235E-03 +3.227247E-07 +1.248195E-03 +3.488233E-07 +1.030935E-03 +2.206418E-07 +5.670315E-04 +6.648507E-08 +2.393638E-04 +1.242517E-08 diff --git a/tests/regression_tests/random_ray_fixed_source/material/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source/material/inputs_true.dat index 7fde0c419..e3ad3703b 100644 --- a/tests/regression_tests/random_ray_fixed_source/material/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source/material/inputs_true.dat @@ -153,6 +153,7 @@ 400.0 100.0 + False 0.0 0.0 0.0 60.0 100.0 60.0 diff --git a/tests/regression_tests/random_ray_fixed_source/material/results_true.dat b/tests/regression_tests/random_ray_fixed_source/material/results_true.dat index e3d528909..ba1254d38 100644 --- a/tests/regression_tests/random_ray_fixed_source/material/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source/material/results_true.dat @@ -1,47 +1,47 @@ tally 1: 3.751047E+01 2.841797E+02 -9.930788E+00 -1.988181E+01 -3.781121E+00 -3.005165E+00 -2.383139E+00 -1.232560E+00 -1.561884E+00 -6.440577E-01 -1.089787E+00 -3.724896E-01 -6.608456E-01 -1.285592E-01 -2.372611E-01 -1.601299E-02 -7.814803E-02 -1.765829E-03 -2.862108E-02 -2.460129E-04 +1.000671E+01 +2.020214E+01 +3.979569E+00 +3.330838E+00 +2.552971E+00 +1.407542E+00 +1.736764E+00 +7.948004E-01 +1.178834E+00 +4.328006E-01 +6.953895E-01 +1.408391E-01 +2.389347E-01 +1.617450E-02 +8.128516E-02 +1.903860E-03 +2.764062E-02 +2.285458E-04 tally 2: -1.089787E+00 -3.724896E-01 -3.767925E-01 -3.724398E-02 -8.614120E-02 -1.526889E-03 -3.610725E-02 -2.629885E-04 -1.466261E-02 -4.536997E-05 -4.653106E-03 -4.381672E-06 +1.178834E+00 +4.328006E-01 +3.982124E-01 +4.138078E-02 +9.676374E-02 +1.919297E-03 +3.944531E-02 +3.133733E-04 +1.554518E-02 +5.084822E-05 +4.815079E-03 +4.681450E-06 tally 3: -1.617918E-03 -6.317049E-07 -1.161473E-03 -2.789553E-07 -1.198879E-03 -3.189531E-07 -1.031737E-03 -2.207381E-07 -5.466329E-04 -6.166809E-08 -2.146062E-04 -9.937520E-09 +1.635823E-03 +6.482611E-07 +1.248235E-03 +3.227247E-07 +1.248195E-03 +3.488233E-07 +1.030935E-03 +2.206418E-07 +5.670315E-04 +6.648507E-08 +2.393638E-04 +1.242517E-08 diff --git a/tests/regression_tests/random_ray_fixed_source/test.py b/tests/regression_tests/random_ray_fixed_source/test.py index 41a6c35bb..7ceadca5b 100644 --- a/tests/regression_tests/random_ray_fixed_source/test.py +++ b/tests/regression_tests/random_ray_fixed_source/test.py @@ -251,6 +251,7 @@ def create_random_ray_model(domain_type): settings.random_ray['distance_active'] = 400.0 settings.random_ray['distance_inactive'] = 100.0 + settings.random_ray['volume_normalized_flux_tallies'] = False # Create an initial uniform spatial source for ray integration lower_left = (0.0, 0.0, 0.0) diff --git a/tests/regression_tests/random_ray_fixed_source/universe/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source/universe/inputs_true.dat index 349917fee..efbc03adf 100644 --- a/tests/regression_tests/random_ray_fixed_source/universe/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source/universe/inputs_true.dat @@ -153,6 +153,7 @@ 400.0 100.0 + False 0.0 0.0 0.0 60.0 100.0 60.0 diff --git a/tests/regression_tests/random_ray_fixed_source/universe/results_true.dat b/tests/regression_tests/random_ray_fixed_source/universe/results_true.dat index 36d24e939..ba1254d38 100644 --- a/tests/regression_tests/random_ray_fixed_source/universe/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source/universe/results_true.dat @@ -1,47 +1,47 @@ tally 1: 3.751047E+01 2.841797E+02 -9.930788E+00 -1.988180E+01 -3.781121E+00 -3.005165E+00 -2.383139E+00 -1.232560E+00 -1.561884E+00 -6.440577E-01 -1.089787E+00 -3.724896E-01 -6.608456E-01 -1.285592E-01 -2.372611E-01 -1.601299E-02 -7.814803E-02 -1.765829E-03 -2.862107E-02 -2.460129E-04 +1.000671E+01 +2.020214E+01 +3.979569E+00 +3.330838E+00 +2.552971E+00 +1.407542E+00 +1.736764E+00 +7.948004E-01 +1.178834E+00 +4.328006E-01 +6.953895E-01 +1.408391E-01 +2.389347E-01 +1.617450E-02 +8.128516E-02 +1.903860E-03 +2.764062E-02 +2.285458E-04 tally 2: -1.089787E+00 -3.724896E-01 -3.767926E-01 -3.724399E-02 -8.614120E-02 -1.526889E-03 -3.610725E-02 -2.629885E-04 -1.466261E-02 -4.536997E-05 -4.653106E-03 -4.381672E-06 +1.178834E+00 +4.328006E-01 +3.982124E-01 +4.138078E-02 +9.676374E-02 +1.919297E-03 +3.944531E-02 +3.133733E-04 +1.554518E-02 +5.084822E-05 +4.815079E-03 +4.681450E-06 tally 3: -1.617918E-03 -6.317049E-07 -1.161473E-03 -2.789553E-07 -1.198879E-03 -3.189531E-07 -1.031737E-03 -2.207381E-07 -5.466329E-04 -6.166808E-08 -2.146062E-04 -9.937520E-09 +1.635823E-03 +6.482611E-07 +1.248235E-03 +3.227247E-07 +1.248195E-03 +3.488233E-07 +1.030935E-03 +2.206418E-07 +5.670315E-04 +6.648507E-08 +2.393638E-04 +1.242517E-08 diff --git a/tests/regression_tests/random_ray_vacuum/inputs_true.dat b/tests/regression_tests/random_ray_vacuum/inputs_true.dat index 4ef109420..5b2fa0905 100644 --- a/tests/regression_tests/random_ray_vacuum/inputs_true.dat +++ b/tests/regression_tests/random_ray_vacuum/inputs_true.dat @@ -85,6 +85,7 @@ -1.26 -1.26 -1 1.26 1.26 1 + True diff --git a/tests/regression_tests/random_ray_vacuum/test.py b/tests/regression_tests/random_ray_vacuum/test.py index e9ca22521..3923b8a81 100644 --- a/tests/regression_tests/random_ray_vacuum/test.py +++ b/tests/regression_tests/random_ray_vacuum/test.py @@ -193,6 +193,7 @@ def random_ray_model() -> openmc.Model: settings.random_ray['distance_active'] = 100.0 settings.random_ray['distance_inactive'] = 20.0 settings.random_ray['ray_source'] = rr_source + settings.random_ray['volume_normalized_flux_tallies'] = True ############################################################################### # Define tallies From 653d8835dcefb6d4f46fab9695feec168e62302a Mon Sep 17 00:00:00 2001 From: John Vincent Cauilan <64677361+johvincau@users.noreply.github.com> Date: Wed, 3 Jul 2024 12:44:20 -0500 Subject: [PATCH 141/671] Add missing show_overlaps option to plots.xml input file documentation (#3068) --- docs/source/io_formats/plots.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/source/io_formats/plots.rst b/docs/source/io_formats/plots.rst index e6b75eafc..04d5720c8 100644 --- a/docs/source/io_formats/plots.rst +++ b/docs/source/io_formats/plots.rst @@ -151,6 +151,18 @@ attributes or sub-elements. These are not used in "voxel" plots: *Default*: 255 255 255 (white) + :show_overlaps: + Indicates whether overlapping regions of different cells are shown. + + *Default*: None + + :overlap_color: + Specifies the RGB color of overlapping regions of different cells. Does not + do anything if ``show_overlaps`` is "false" or not specified. Should be 3 + integers separated by spaces. + + *Default*: 255 0 0 (red) + :meshlines: The ``meshlines`` sub-element allows for plotting the boundaries of a regular mesh on top of a plot. Only one ``meshlines`` element is allowed per From 1b22dd28d40a9f248ffcc26deab9427c8921d497 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Wed, 3 Jul 2024 13:28:06 -0500 Subject: [PATCH 142/671] Random Ray Testing Simplification (#3061) Co-authored-by: Paul Romano --- openmc/__init__.py | 3 +- openmc/examples.py | 529 ++++++++- .../regression_tests/random_ray_basic/test.py | 233 ---- .../False_hybrid/inputs_true.dat | 1018 ----------------- .../False_hybrid/results_true.dat | 9 - .../True_hybrid/inputs_true.dat | 1018 ----------------- .../True_hybrid/results_true.dat | 9 - .../regression_tests/random_ray_cube/test.py | 231 ---- .../cell/inputs_true.dat | 205 ---- .../cell/results_true.dat | 47 - .../material/inputs_true.dat | 205 ---- .../material/results_true.dat | 47 - .../random_ray_fixed_source/test.py | 340 ------ .../universe/inputs_true.dat | 205 ---- .../universe/results_true.dat | 47 - .../__init__.py | 0 .../cell/inputs_true.dat | 244 ++++ .../cell/results_true.dat | 9 + .../material/inputs_true.dat | 244 ++++ .../material/results_true.dat | 9 + .../random_ray_fixed_source_domain/test.py | 51 + .../universe/inputs_true.dat | 244 ++++ .../universe/results_true.dat | 9 + .../False/inputs_true.dat | 244 ++++ .../False/results_true.dat | 9 + .../True/inputs_true.dat | 244 ++++ .../True/results_true.dat | 9 + .../__init__.py | 0 .../test.py | 27 + .../__init__.py | 0 .../inputs_true.dat | 0 .../results_true.dat | 0 .../regression_tests/random_ray_k_eff/test.py | 19 + .../random_ray_vacuum/__init__.py | 0 .../random_ray_vacuum/inputs_true.dat | 109 -- .../random_ray_vacuum/results_true.dat | 171 --- .../random_ray_vacuum/test.py | 236 ---- 37 files changed, 1869 insertions(+), 4155 deletions(-) delete mode 100644 tests/regression_tests/random_ray_basic/test.py delete mode 100644 tests/regression_tests/random_ray_cube/False_hybrid/inputs_true.dat delete mode 100644 tests/regression_tests/random_ray_cube/False_hybrid/results_true.dat delete mode 100644 tests/regression_tests/random_ray_cube/True_hybrid/inputs_true.dat delete mode 100644 tests/regression_tests/random_ray_cube/True_hybrid/results_true.dat delete mode 100644 tests/regression_tests/random_ray_cube/test.py delete mode 100644 tests/regression_tests/random_ray_fixed_source/cell/inputs_true.dat delete mode 100644 tests/regression_tests/random_ray_fixed_source/cell/results_true.dat delete mode 100644 tests/regression_tests/random_ray_fixed_source/material/inputs_true.dat delete mode 100644 tests/regression_tests/random_ray_fixed_source/material/results_true.dat delete mode 100644 tests/regression_tests/random_ray_fixed_source/test.py delete mode 100644 tests/regression_tests/random_ray_fixed_source/universe/inputs_true.dat delete mode 100644 tests/regression_tests/random_ray_fixed_source/universe/results_true.dat rename tests/regression_tests/{random_ray_basic => random_ray_fixed_source_domain}/__init__.py (100%) create mode 100644 tests/regression_tests/random_ray_fixed_source_domain/cell/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_domain/cell/results_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_domain/material/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_domain/material/results_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_domain/test.py create mode 100644 tests/regression_tests/random_ray_fixed_source_domain/universe/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_domain/universe/results_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_normalization/False/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_normalization/False/results_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_normalization/True/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_normalization/True/results_true.dat rename tests/regression_tests/{random_ray_cube => random_ray_fixed_source_normalization}/__init__.py (100%) create mode 100644 tests/regression_tests/random_ray_fixed_source_normalization/test.py rename tests/regression_tests/{random_ray_fixed_source => random_ray_k_eff}/__init__.py (100%) rename tests/regression_tests/{random_ray_basic => random_ray_k_eff}/inputs_true.dat (100%) rename tests/regression_tests/{random_ray_basic => random_ray_k_eff}/results_true.dat (100%) create mode 100644 tests/regression_tests/random_ray_k_eff/test.py delete mode 100644 tests/regression_tests/random_ray_vacuum/__init__.py delete mode 100644 tests/regression_tests/random_ray_vacuum/inputs_true.dat delete mode 100644 tests/regression_tests/random_ray_vacuum/results_true.dat delete mode 100644 tests/regression_tests/random_ray_vacuum/test.py diff --git a/openmc/__init__.py b/openmc/__init__.py index d18e1188e..e589d35ef 100644 --- a/openmc/__init__.py +++ b/openmc/__init__.py @@ -32,11 +32,12 @@ from openmc.plotter import * from openmc.search import * from openmc.polynomial import * from openmc.tracks import * -from . import examples from .config import * # Import a few names from the model module from openmc.model import Model +from . import examples + __version__ = '0.15.1-dev' diff --git a/openmc/examples.py b/openmc/examples.py index fc94b8b52..038c75ad2 100644 --- a/openmc/examples.py +++ b/openmc/examples.py @@ -3,10 +3,10 @@ from numbers import Integral import numpy as np import openmc -import openmc.model -def pwr_pin_cell(): + +def pwr_pin_cell() -> openmc.Model: """Create a PWR pin-cell model. This model is a single fuel pin with 2.4 w/o enriched UO2 corresponding to a @@ -16,11 +16,11 @@ def pwr_pin_cell(): Returns ------- - model : openmc.model.Model + model : openmc.Model A PWR pin-cell model """ - model = openmc.model.Model() + model = openmc.Model() # Define materials. fuel = openmc.Material(name='UO2 (2.4%)') @@ -77,7 +77,8 @@ def pwr_pin_cell(): model.settings.inactive = 5 model.settings.particles = 100 model.settings.source = openmc.IndependentSource( - space=openmc.stats.Box([-pitch/2, -pitch/2, -1], [pitch/2, pitch/2, 1]), + space=openmc.stats.Box([-pitch/2, -pitch/2, -1], + [pitch/2, pitch/2, 1]), constraints={'fissionable': True} ) @@ -89,7 +90,7 @@ def pwr_pin_cell(): return model -def pwr_core(): +def pwr_core() -> openmc.Model: """Create a PWR full-core model. This model is the OECD/NEA Monte Carlo Performance benchmark which is a @@ -99,11 +100,11 @@ def pwr_core(): Returns ------- - model : openmc.model.Model + model : openmc.Model Full-core PWR model """ - model = openmc.model.Model() + model = openmc.Model() # Define materials. fuel = openmc.Material(1, name='UOX fuel') @@ -166,7 +167,8 @@ def pwr_core(): lower_rad_ref.add_nuclide('Cr52', 0.145407678031, 'wo') lower_rad_ref.add_s_alpha_beta('c_H_in_H2O') - upper_rad_ref = openmc.Material(7, name='Upper radial reflector / Top plate region') + upper_rad_ref = openmc.Material( + 7, name='Upper radial reflector / Top plate region') upper_rad_ref.set_density('g/cm3', 4.28) upper_rad_ref.add_nuclide('H1', 0.0086117, 'wo') upper_rad_ref.add_nuclide('O16', 0.0683369, 'wo') @@ -313,13 +315,15 @@ def pwr_core(): 11, 11, 11, 11, 11, 13, 13, 14, 14, 14]) # Define fuel lattices. - l100 = openmc.RectLattice(name='Fuel assembly (lower half)', lattice_id=100) + l100 = openmc.RectLattice( + name='Fuel assembly (lower half)', lattice_id=100) l100.lower_left = (-10.71, -10.71) l100.pitch = (1.26, 1.26) l100.universes = np.tile(fuel_cold, (17, 17)) l100.universes[tube_x, tube_y] = tube_cold - l101 = openmc.RectLattice(name='Fuel assembly (upper half)', lattice_id=101) + l101 = openmc.RectLattice( + name='Fuel assembly (upper half)', lattice_id=101) l101.lower_left = (-10.71, -10.71) l101.pitch = (1.26, 1.26) l101.universes = np.tile(fuel_hot, (17, 17)) @@ -405,10 +409,14 @@ def pwr_core(): c6 = openmc.Cell(cell_id=6, fill=top_fa, region=-s5 & +s36 & -s37) c7 = openmc.Cell(cell_id=7, fill=top_nozzle, region=-s5 & +s37 & -s38) c8 = openmc.Cell(cell_id=8, fill=upper_rad_ref, region=-s7 & +s38 & -s39) - c9 = openmc.Cell(cell_id=9, fill=bot_nozzle, region=+s6 & -s7 & +s32 & -s38) - c10 = openmc.Cell(cell_id=10, fill=rpv_steel, region=+s7 & -s8 & +s31 & -s39) - c11 = openmc.Cell(cell_id=11, fill=lower_rad_ref, region=+s5 & -s6 & +s32 & -s34) - c12 = openmc.Cell(cell_id=12, fill=upper_rad_ref, region=+s5 & -s6 & +s36 & -s38) + c9 = openmc.Cell(cell_id=9, fill=bot_nozzle, + region=+s6 & -s7 & +s32 & -s38) + c10 = openmc.Cell(cell_id=10, fill=rpv_steel, + region=+s7 & -s8 & +s31 & -s39) + c11 = openmc.Cell(cell_id=11, fill=lower_rad_ref, + region=+s5 & -s6 & +s32 & -s34) + c12 = openmc.Cell(cell_id=12, fill=upper_rad_ref, + region=+s5 & -s6 & +s36 & -s38) root.add_cells((c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12)) # Assign root universe to geometry @@ -430,7 +438,7 @@ def pwr_core(): return model -def pwr_assembly(): +def pwr_assembly() -> openmc.Model: """Create a PWR assembly model. This model is a reflected 17x17 fuel assembly from the the `BEAVRS @@ -440,12 +448,12 @@ def pwr_assembly(): Returns ------- - model : openmc.model.Model + model : openmc.Model A PWR assembly model """ - model = openmc.model.Model() + model = openmc.Model() # Define materials. fuel = openmc.Material(name='Fuel') @@ -489,10 +497,10 @@ def pwr_assembly(): fuel_pin_universe = openmc.Universe(name='Fuel Pin') fuel_cell = openmc.Cell(name='fuel', fill=fuel, region=-fuel_or) clad_cell = openmc.Cell(name='clad', fill=clad, region=+fuel_or & -clad_or) - hot_water_cell = openmc.Cell(name='hot water', fill=hot_water, region=+clad_or) + hot_water_cell = openmc.Cell( + name='hot water', fill=hot_water, region=+clad_or) fuel_pin_universe.add_cells([fuel_cell, clad_cell, hot_water_cell]) - # Create a control rod guide tube universe guide_tube_universe = openmc.Universe(name='Guide Tube') gt_inner_cell = openmc.Cell(name='guide tube inner water', fill=hot_water, @@ -530,7 +538,8 @@ def pwr_assembly(): model.settings.inactive = 5 model.settings.particles = 100 model.settings.source = openmc.IndependentSource( - space=openmc.stats.Box([-pitch/2, -pitch/2, -1], [pitch/2, pitch/2, 1]), + space=openmc.stats.Box([-pitch/2, -pitch/2, -1], + [pitch/2, pitch/2, 1]), constraints={'fissionable': True} ) @@ -544,7 +553,7 @@ def pwr_assembly(): return model -def slab_mg(num_regions=1, mat_names=None, mgxslib_name='2g.h5'): +def slab_mg(num_regions=1, mat_names=None, mgxslib_name='2g.h5') -> openmc.Model: """Create a 1D slab model. Parameters @@ -561,7 +570,7 @@ def slab_mg(num_regions=1, mat_names=None, mgxslib_name='2g.h5'): Returns ------- - model : openmc.model.Model + model : openmc.Model One-group, 1D slab model """ @@ -637,10 +646,482 @@ def slab_mg(num_regions=1, mat_names=None, mgxslib_name='2g.h5'): settings_file.output = {'summary': False} - model = openmc.model.Model() + model = openmc.Model() model.geometry = geometry_file model.materials = materials_file model.settings = settings_file model.xs_data = macros return model + + +def random_ray_lattice() -> openmc.Model: + """Create a 2x2 PWR pincell asymmetrical lattic eexample. + + This model is a 2x2 reflective lattice of fuel pins with one of the lattice + locations having just moderator instead of a fuel pin. It uses 7 group + cross section data. + + Returns + ------- + model : openmc.Model + A PWR 2x2 lattice model + + """ + model = openmc.Model() + + ########################################################################### + # Create MGXS data for the problem + + # Instantiate the energy group data + group_edges = [1e-5, 0.0635, 10.0, 1.0e2, 1.0e3, 0.5e6, 1.0e6, 20.0e6] + groups = openmc.mgxs.EnergyGroups(group_edges) + + # Instantiate the 7-group (C5G7) cross section data + uo2_xsdata = openmc.XSdata('UO2', groups) + uo2_xsdata.order = 0 + uo2_xsdata.set_total( + [0.1779492, 0.3298048, 0.4803882, 0.5543674, 0.3118013, 0.3951678, + 0.5644058]) + uo2_xsdata.set_absorption([8.0248e-03, 3.7174e-03, 2.6769e-02, 9.6236e-02, + 3.0020e-02, 1.1126e-01, 2.8278e-01]) + scatter_matrix = np.array( + [[[0.1275370, 0.0423780, 0.0000094, 0.0000000, 0.0000000, 0.0000000, 0.0000000], + [0.0000000, 0.3244560, 0.0016314, 0.0000000, + 0.0000000, 0.0000000, 0.0000000], + [0.0000000, 0.0000000, 0.4509400, 0.0026792, + 0.0000000, 0.0000000, 0.0000000], + [0.0000000, 0.0000000, 0.0000000, 0.4525650, + 0.0055664, 0.0000000, 0.0000000], + [0.0000000, 0.0000000, 0.0000000, 0.0001253, + 0.2714010, 0.0102550, 0.0000000], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, + 0.0012968, 0.2658020, 0.0168090], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0085458, 0.2730800]]]) + scatter_matrix = np.rollaxis(scatter_matrix, 0, 3) + uo2_xsdata.set_scatter_matrix(scatter_matrix) + uo2_xsdata.set_fission([7.21206e-03, 8.19301e-04, 6.45320e-03, + 1.85648e-02, 1.78084e-02, 8.30348e-02, + 2.16004e-01]) + uo2_xsdata.set_nu_fission([2.005998e-02, 2.027303e-03, 1.570599e-02, + 4.518301e-02, 4.334208e-02, 2.020901e-01, + 5.257105e-01]) + uo2_xsdata.set_chi([5.8791e-01, 4.1176e-01, 3.3906e-04, 1.1761e-07, 0.0000e+00, + 0.0000e+00, 0.0000e+00]) + + h2o_xsdata = openmc.XSdata('LWTR', groups) + h2o_xsdata.order = 0 + h2o_xsdata.set_total([0.15920605, 0.412969593, 0.59030986, 0.58435, + 0.718, 1.2544497, 2.650379]) + h2o_xsdata.set_absorption([6.0105e-04, 1.5793e-05, 3.3716e-04, + 1.9406e-03, 5.7416e-03, 1.5001e-02, + 3.7239e-02]) + scatter_matrix = np.array( + [[[0.0444777, 0.1134000, 0.0007235, 0.0000037, 0.0000001, 0.0000000, 0.0000000], + [0.0000000, 0.2823340, 0.1299400, 0.0006234, + 0.0000480, 0.0000074, 0.0000010], + [0.0000000, 0.0000000, 0.3452560, 0.2245700, + 0.0169990, 0.0026443, 0.0005034], + [0.0000000, 0.0000000, 0.0000000, 0.0910284, + 0.4155100, 0.0637320, 0.0121390], + [0.0000000, 0.0000000, 0.0000000, 0.0000714, + 0.1391380, 0.5118200, 0.0612290], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, + 0.0022157, 0.6999130, 0.5373200], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.1324400, 2.4807000]]]) + scatter_matrix = np.rollaxis(scatter_matrix, 0, 3) + h2o_xsdata.set_scatter_matrix(scatter_matrix) + + mg_cross_sections = openmc.MGXSLibrary(groups) + mg_cross_sections.add_xsdatas([uo2_xsdata, h2o_xsdata]) + mg_cross_sections.export_to_hdf5('mgxs.h5') + + ########################################################################### + # Create materials for the problem + + # Instantiate some Materials and register the appropriate macroscopic data + uo2 = openmc.Material(name='UO2 fuel') + uo2.set_density('macro', 1.0) + uo2.add_macroscopic('UO2') + + water = openmc.Material(name='Water') + water.set_density('macro', 1.0) + water.add_macroscopic('LWTR') + + # Instantiate a Materials collection and export to XML + materials = openmc.Materials([uo2, water]) + materials.cross_sections = "mgxs.h5" + + ########################################################################### + # Define problem geometry + + ######################################## + # Define an unbounded pincell universe + + pitch = 1.26 + + # Create a surface for the fuel outer radius + fuel_or = openmc.ZCylinder(r=0.54, name='Fuel OR') + inner_ring_a = openmc.ZCylinder(r=0.33, name='inner ring a') + inner_ring_b = openmc.ZCylinder(r=0.45, name='inner ring b') + outer_ring_a = openmc.ZCylinder(r=0.60, name='outer ring a') + outer_ring_b = openmc.ZCylinder(r=0.69, name='outer ring b') + + # Instantiate Cells + fuel_a = openmc.Cell(fill=uo2, region=-inner_ring_a, name='fuel inner a') + fuel_b = openmc.Cell(fill=uo2, region=+inner_ring_a & - + inner_ring_b, name='fuel inner b') + fuel_c = openmc.Cell(fill=uo2, region=+inner_ring_b & - + fuel_or, name='fuel inner c') + moderator_a = openmc.Cell( + fill=water, region=+fuel_or & -outer_ring_a, name='moderator inner a') + moderator_b = openmc.Cell( + fill=water, region=+outer_ring_a & -outer_ring_b, name='moderator outer b') + moderator_c = openmc.Cell( + fill=water, region=+outer_ring_b, name='moderator outer c') + + # Create pincell universe + pincell_base = openmc.Universe() + + # Register Cells with Universe + pincell_base.add_cells( + [fuel_a, fuel_b, fuel_c, moderator_a, moderator_b, moderator_c]) + + # Create planes for azimuthal sectors + azimuthal_planes = [] + for i in range(8): + angle = 2 * i * openmc.pi / 8 + normal_vector = (-openmc.sin(angle), openmc.cos(angle), 0) + azimuthal_planes.append(openmc.Plane( + a=normal_vector[0], b=normal_vector[1], c=normal_vector[2], d=0)) + + # Create a cell for each azimuthal sector + azimuthal_cells = [] + for i in range(8): + azimuthal_cell = openmc.Cell(name=f'azimuthal_cell_{i}') + azimuthal_cell.fill = pincell_base + azimuthal_cell.region = +azimuthal_planes[i] & -azimuthal_planes[(i+1) % 8] + azimuthal_cells.append(azimuthal_cell) + + # Create a geometry with the azimuthal universes + pincell = openmc.Universe(cells=azimuthal_cells) + + ######################################## + # Define a moderator lattice universe + + moderator_infinite = openmc.Cell(fill=water, name='moderator infinite') + mu = openmc.Universe() + mu.add_cells([moderator_infinite]) + + lattice = openmc.RectLattice() + lattice.lower_left = [-pitch/2.0, -pitch/2.0] + lattice.pitch = [pitch/10.0, pitch/10.0] + lattice.universes = np.full((10, 10), mu) + + mod_lattice_cell = openmc.Cell(fill=lattice) + + mod_lattice_uni = openmc.Universe() + + mod_lattice_uni.add_cells([mod_lattice_cell]) + + ######################################## + # Define 2x2 outer lattice + lattice2x2 = openmc.RectLattice() + lattice2x2.lower_left = (-pitch, -pitch) + lattice2x2.pitch = (pitch, pitch) + lattice2x2.universes = [ + [pincell, pincell], + [pincell, mod_lattice_uni] + ] + + ######################################## + # Define cell containing lattice and other stuff + box = openmc.model.RectangularPrism( + pitch*2, pitch*2, boundary_type='reflective') + + assembly = openmc.Cell(fill=lattice2x2, region=-box, name='assembly') + + # Create a geometry with the top-level cell + geometry = openmc.Geometry([assembly]) + + ########################################################################### + # Define problem settings + + # Instantiate a Settings object, set all runtime parameters, and export to XML + settings = openmc.Settings() + settings.energy_mode = "multi-group" + settings.batches = 10 + settings.inactive = 5 + settings.particles = 100 + + # Create an initial uniform spatial source distribution over fissionable zones + lower_left = (-pitch, -pitch, -1) + upper_right = (pitch, pitch, 1) + uniform_dist = openmc.stats.Box(lower_left, upper_right) + rr_source = openmc.IndependentSource(space=uniform_dist) + + settings.random_ray['distance_active'] = 100.0 + settings.random_ray['distance_inactive'] = 20.0 + settings.random_ray['ray_source'] = rr_source + settings.random_ray['volume_normalized_flux_tallies'] = True + + ########################################################################### + # Define tallies + + # Create a mesh that will be used for tallying + mesh = openmc.RegularMesh() + mesh.dimension = (2, 2) + mesh.lower_left = (-pitch, -pitch) + mesh.upper_right = (pitch, pitch) + + # Create a mesh filter that can be used in a tally + mesh_filter = openmc.MeshFilter(mesh) + + # Create an energy group filter as well + group_edges = [1e-5, 0.0635, 10.0, 1.0e2, 1.0e3, 0.5e6, 1.0e6, 20.0e6] + energy_filter = openmc.EnergyFilter(group_edges) + + # Now use the mesh filter in a tally and indicate what scores are desired + tally = openmc.Tally(name="Mesh tally") + tally.filters = [mesh_filter, energy_filter] + tally.scores = ['flux', 'fission', 'nu-fission'] + tally.estimator = 'analog' + + # Instantiate a Tallies collection and export to XML + tallies = openmc.Tallies([tally]) + + ########################################################################### + # Exporting to OpenMC model + ########################################################################### + + model.geometry = geometry + model.materials = materials + model.settings = settings + model.tallies = tallies + return model + + +def random_ray_three_region_cube() -> openmc.Model: + """Create a three region cube model. + + This is a simple monoenergetic problem of a cube with three concentric cubic + regions. The innermost region is near void (with Sigma_t around 10^-5) and + contains an external isotropic source term, the middle region is void (with + Sigma_t around 10^-4), and the outer region of the cube is an absorber + (with Sigma_t around 1). + + Returns + ------- + model : openmc.Model + A three region cube model + + """ + + model = openmc.Model() + + ########################################################################### + # Helper function creates a 3 region cube with different fills in each region + def fill_cube(N, n_1, n_2, fill_1, fill_2, fill_3): + cube = [[[0 for _ in range(N)] for _ in range(N)] for _ in range(N)] + for i in range(N): + for j in range(N): + for k in range(N): + if i < n_1 and j >= (N-n_1) and k < n_1: + cube[i][j][k] = fill_1 + elif i < n_2 and j >= (N-n_2) and k < n_2: + cube[i][j][k] = fill_2 + else: + cube[i][j][k] = fill_3 + return cube + + ########################################################################### + # Create multigroup data + + # Instantiate the energy group data + ebins = [1e-5, 20.0e6] + groups = openmc.mgxs.EnergyGroups(group_edges=ebins) + + void_sigma_a = 4.0e-6 + void_sigma_s = 3.0e-4 + void_mat_data = openmc.XSdata('void', groups) + void_mat_data.order = 0 + void_mat_data.set_total([void_sigma_a + void_sigma_s]) + void_mat_data.set_absorption([void_sigma_a]) + void_mat_data.set_scatter_matrix( + np.rollaxis(np.array([[[void_sigma_s]]]), 0, 3)) + + absorber_sigma_a = 0.75 + absorber_sigma_s = 0.25 + absorber_mat_data = openmc.XSdata('absorber', groups) + absorber_mat_data.order = 0 + absorber_mat_data.set_total([absorber_sigma_a + absorber_sigma_s]) + absorber_mat_data.set_absorption([absorber_sigma_a]) + absorber_mat_data.set_scatter_matrix( + np.rollaxis(np.array([[[absorber_sigma_s]]]), 0, 3)) + + multiplier = 0.1 + source_sigma_a = void_sigma_a * multiplier + source_sigma_s = void_sigma_s * multiplier + source_mat_data = openmc.XSdata('source', groups) + source_mat_data.order = 0 + source_mat_data.set_total([source_sigma_a + source_sigma_s]) + source_mat_data.set_absorption([source_sigma_a]) + source_mat_data.set_scatter_matrix( + np.rollaxis(np.array([[[source_sigma_s]]]), 0, 3)) + + mg_cross_sections_file = openmc.MGXSLibrary(groups) + mg_cross_sections_file.add_xsdatas( + [source_mat_data, void_mat_data, absorber_mat_data]) + mg_cross_sections_file.export_to_hdf5() + + ########################################################################### + # Create materials for the problem + + # Instantiate some Macroscopic Data + source_data = openmc.Macroscopic('source') + void_data = openmc.Macroscopic('void') + absorber_data = openmc.Macroscopic('absorber') + + # Instantiate some Materials and register the appropriate Macroscopic objects + source_mat = openmc.Material(name='source') + source_mat.set_density('macro', 1.0) + source_mat.add_macroscopic(source_data) + + void_mat = openmc.Material(name='void') + void_mat.set_density('macro', 1.0) + void_mat.add_macroscopic(void_data) + + absorber_mat = openmc.Material(name='absorber') + absorber_mat.set_density('macro', 1.0) + absorber_mat.add_macroscopic(absorber_data) + + # Instantiate a Materials collection and export to XML + materials_file = openmc.Materials([source_mat, void_mat, absorber_mat]) + materials_file.cross_sections = "mgxs.h5" + + ########################################################################### + # Define problem geometry + + source_cell = openmc.Cell(fill=source_mat, name='infinite source region') + void_cell = openmc.Cell(fill=void_mat, name='infinite void region') + absorber_cell = openmc.Cell( + fill=absorber_mat, name='infinite absorber region') + + source_universe = openmc.Universe(name='source universe') + source_universe.add_cells([source_cell]) + + void_universe = openmc.Universe() + void_universe.add_cells([void_cell]) + + absorber_universe = openmc.Universe() + absorber_universe.add_cells([absorber_cell]) + + absorber_width = 30.0 + n_base = 6 + + # This variable can be increased above 1 to refine the FSR mesh resolution further + refinement_level = 2 + + n = n_base * refinement_level + pitch = absorber_width / n + + pattern = fill_cube(n, 1*refinement_level, 5*refinement_level, + source_universe, void_universe, absorber_universe) + + lattice = openmc.RectLattice() + lattice.lower_left = [0.0, 0.0, 0.0] + lattice.pitch = [pitch, pitch, pitch] + lattice.universes = pattern + + lattice_cell = openmc.Cell(fill=lattice) + + lattice_uni = openmc.Universe() + lattice_uni.add_cells([lattice_cell]) + + x_low = openmc.XPlane(x0=0.0, boundary_type='reflective') + x_high = openmc.XPlane(x0=absorber_width, boundary_type='vacuum') + + y_low = openmc.YPlane(y0=0.0, boundary_type='reflective') + y_high = openmc.YPlane(y0=absorber_width, boundary_type='vacuum') + + z_low = openmc.ZPlane(z0=0.0, boundary_type='reflective') + z_high = openmc.ZPlane(z0=absorber_width, boundary_type='vacuum') + + full_domain = openmc.Cell(fill=lattice_uni, region=+x_low & - + x_high & +y_low & -y_high & +z_low & -z_high, name='full domain') + + root = openmc.Universe(name='root universe') + root.add_cell(full_domain) + + # Create a geometry with the two cells and export to XML + geometry = openmc.Geometry(root) + + ########################################################################### + # Define problem settings + + # Instantiate a Settings object, set all runtime parameters, and export to XML + settings = openmc.Settings() + settings.energy_mode = "multi-group" + settings.inactive = 5 + settings.batches = 10 + settings.particles = 90 + settings.run_mode = 'fixed source' + + # Create an initial uniform spatial source for ray integration + lower_left_ray = [0.0, 0.0, 0.0] + upper_right_ray = [absorber_width, absorber_width, absorber_width] + uniform_dist_ray = openmc.stats.Box( + lower_left_ray, upper_right_ray, only_fissionable=False) + rr_source = openmc.IndependentSource(space=uniform_dist_ray) + + settings.random_ray['distance_active'] = 500.0 + settings.random_ray['distance_inactive'] = 100.0 + settings.random_ray['ray_source'] = rr_source + settings.random_ray['volume_normalized_flux_tallies'] = True + + # Create the neutron source in the bottom right of the moderator + # Good - fast group appears largest (besides most thermal) + strengths = [1.0] + midpoints = [100.0] + energy_distribution = openmc.stats.Discrete(x=midpoints, p=strengths) + + source = openmc.IndependentSource(energy=energy_distribution, constraints={ + 'domains': [source_universe]}, strength=3.14) + + settings.source = [source] + + ########################################################################### + # Define tallies + + estimator = 'tracklength' + + absorber_filter = openmc.MaterialFilter(absorber_mat) + absorber_tally = openmc.Tally(name="Absorber Tally") + absorber_tally.filters = [absorber_filter] + absorber_tally.scores = ['flux'] + absorber_tally.estimator = estimator + + void_filter = openmc.MaterialFilter(void_mat) + void_tally = openmc.Tally(name="Void Tally") + void_tally.filters = [void_filter] + void_tally.scores = ['flux'] + void_tally.estimator = estimator + + source_filter = openmc.MaterialFilter(source_mat) + source_tally = openmc.Tally(name="Source Tally") + source_tally.filters = [source_filter] + source_tally.scores = ['flux'] + source_tally.estimator = estimator + + # Instantiate a Tallies collection and export to XML + tallies = openmc.Tallies([source_tally, void_tally, absorber_tally]) + + ########################################################################### + # Assmble Model + + model.geometry = geometry + model.materials = materials_file + model.settings = settings + model.tallies = tallies + + return model diff --git a/tests/regression_tests/random_ray_basic/test.py b/tests/regression_tests/random_ray_basic/test.py deleted file mode 100644 index c00d25880..000000000 --- a/tests/regression_tests/random_ray_basic/test.py +++ /dev/null @@ -1,233 +0,0 @@ -import os - -import numpy as np -import openmc - -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 random_ray_model() -> openmc.Model: - ############################################################################### - # Create multigroup data - - # Instantiate the energy group data - group_edges = [1e-5, 0.0635, 10.0, 1.0e2, 1.0e3, 0.5e6, 1.0e6, 20.0e6] - groups = openmc.mgxs.EnergyGroups(group_edges) - - # Instantiate the 7-group (C5G7) cross section data - uo2_xsdata = openmc.XSdata('UO2', groups) - uo2_xsdata.order = 0 - uo2_xsdata.set_total( - [0.1779492, 0.3298048, 0.4803882, 0.5543674, 0.3118013, 0.3951678, - 0.5644058]) - uo2_xsdata.set_absorption([8.0248e-03, 3.7174e-03, 2.6769e-02, 9.6236e-02, - 3.0020e-02, 1.1126e-01, 2.8278e-01]) - scatter_matrix = np.array( - [[[0.1275370, 0.0423780, 0.0000094, 0.0000000, 0.0000000, 0.0000000, 0.0000000], - [0.0000000, 0.3244560, 0.0016314, 0.0000000, 0.0000000, 0.0000000, 0.0000000], - [0.0000000, 0.0000000, 0.4509400, 0.0026792, 0.0000000, 0.0000000, 0.0000000], - [0.0000000, 0.0000000, 0.0000000, 0.4525650, 0.0055664, 0.0000000, 0.0000000], - [0.0000000, 0.0000000, 0.0000000, 0.0001253, 0.2714010, 0.0102550, 0.0000000], - [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0012968, 0.2658020, 0.0168090], - [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0085458, 0.2730800]]]) - scatter_matrix = np.rollaxis(scatter_matrix, 0, 3) - uo2_xsdata.set_scatter_matrix(scatter_matrix) - uo2_xsdata.set_fission([7.21206e-03, 8.19301e-04, 6.45320e-03, - 1.85648e-02, 1.78084e-02, 8.30348e-02, - 2.16004e-01]) - uo2_xsdata.set_nu_fission([2.005998e-02, 2.027303e-03, 1.570599e-02, - 4.518301e-02, 4.334208e-02, 2.020901e-01, - 5.257105e-01]) - uo2_xsdata.set_chi([5.8791e-01, 4.1176e-01, 3.3906e-04, 1.1761e-07, 0.0000e+00, - 0.0000e+00, 0.0000e+00]) - - h2o_xsdata = openmc.XSdata('LWTR', groups) - h2o_xsdata.order = 0 - h2o_xsdata.set_total([0.15920605, 0.412969593, 0.59030986, 0.58435, - 0.718, 1.2544497, 2.650379]) - h2o_xsdata.set_absorption([6.0105e-04, 1.5793e-05, 3.3716e-04, - 1.9406e-03, 5.7416e-03, 1.5001e-02, - 3.7239e-02]) - scatter_matrix = np.array( - [[[0.0444777, 0.1134000, 0.0007235, 0.0000037, 0.0000001, 0.0000000, 0.0000000], - [0.0000000, 0.2823340, 0.1299400, 0.0006234, 0.0000480, 0.0000074, 0.0000010], - [0.0000000, 0.0000000, 0.3452560, 0.2245700, 0.0169990, 0.0026443, 0.0005034], - [0.0000000, 0.0000000, 0.0000000, 0.0910284, 0.4155100, 0.0637320, 0.0121390], - [0.0000000, 0.0000000, 0.0000000, 0.0000714, 0.1391380, 0.5118200, 0.0612290], - [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0022157, 0.6999130, 0.5373200], - [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.1324400, 2.4807000]]]) - scatter_matrix = np.rollaxis(scatter_matrix, 0, 3) - h2o_xsdata.set_scatter_matrix(scatter_matrix) - - mg_cross_sections = openmc.MGXSLibrary(groups) - mg_cross_sections.add_xsdatas([uo2_xsdata, h2o_xsdata]) - mg_cross_sections.export_to_hdf5() - - ############################################################################### - # Create materials for the problem - - # Instantiate some Materials and register the appropriate macroscopic data - uo2 = openmc.Material(name='UO2 fuel') - uo2.set_density('macro', 1.0) - uo2.add_macroscopic('UO2') - - water = openmc.Material(name='Water') - water.set_density('macro', 1.0) - water.add_macroscopic('LWTR') - - # Instantiate a Materials collection and export to XML - materials = openmc.Materials([uo2, water]) - materials.cross_sections = "mgxs.h5" - - ############################################################################### - # Define problem geometry - - ######################################## - # Define an unbounded pincell universe - - pitch = 1.26 - - # Create a surface for the fuel outer radius - fuel_or = openmc.ZCylinder(r=0.54, name='Fuel OR') - inner_ring_a = openmc.ZCylinder(r=0.33, name='inner ring a') - inner_ring_b = openmc.ZCylinder(r=0.45, name='inner ring b') - outer_ring_a = openmc.ZCylinder(r=0.60, name='outer ring a') - outer_ring_b = openmc.ZCylinder(r=0.69, name='outer ring b') - - # Instantiate Cells - fuel_a = openmc.Cell(fill=uo2, region=-inner_ring_a, name='fuel inner a') - fuel_b = openmc.Cell(fill=uo2, region=+inner_ring_a & -inner_ring_b, name='fuel inner b') - fuel_c = openmc.Cell(fill=uo2, region=+inner_ring_b & -fuel_or, name='fuel inner c') - moderator_a = openmc.Cell(fill=water, region=+fuel_or & -outer_ring_a, name='moderator inner a') - moderator_b = openmc.Cell(fill=water, region=+outer_ring_a & -outer_ring_b, name='moderator outer b') - moderator_c = openmc.Cell(fill=water, region=+outer_ring_b, name='moderator outer c') - - # Create pincell universe - pincell_base = openmc.Universe() - - # Register Cells with Universe - pincell_base.add_cells([fuel_a, fuel_b, fuel_c, moderator_a, moderator_b, moderator_c]) - - # Create planes for azimuthal sectors - azimuthal_planes = [] - for i in range(8): - angle = 2 * i * openmc.pi / 8 - normal_vector = (-openmc.sin(angle), openmc.cos(angle), 0) - azimuthal_planes.append(openmc.Plane(a=normal_vector[0], b=normal_vector[1], c=normal_vector[2], d=0)) - - # Create a cell for each azimuthal sector - azimuthal_cells = [] - for i in range(8): - azimuthal_cell = openmc.Cell(name=f'azimuthal_cell_{i}') - azimuthal_cell.fill = pincell_base - azimuthal_cell.region = +azimuthal_planes[i] & -azimuthal_planes[(i+1) % 8] - azimuthal_cells.append(azimuthal_cell) - - # Create a geometry with the azimuthal universes - pincell = openmc.Universe(cells=azimuthal_cells) - - ######################################## - # Define a moderator lattice universe - - moderator_infinite = openmc.Cell(fill=water, name='moderator infinite') - mu = openmc.Universe() - mu.add_cells([moderator_infinite]) - - lattice = openmc.RectLattice() - lattice.lower_left = [-pitch/2.0, -pitch/2.0] - lattice.pitch = [pitch/10.0, pitch/10.0] - lattice.universes = np.full((10, 10), mu) - - mod_lattice_cell = openmc.Cell(fill=lattice) - - mod_lattice_uni = openmc.Universe() - - mod_lattice_uni.add_cells([mod_lattice_cell]) - - ######################################## - # Define 2x2 outer lattice - lattice2x2 = openmc.RectLattice() - lattice2x2.lower_left = (-pitch, -pitch) - lattice2x2.pitch = (pitch, pitch) - lattice2x2.universes = [ - [pincell, pincell], - [pincell, mod_lattice_uni] - ] - - ######################################## - # Define cell containing lattice and other stuff - box = openmc.model.RectangularPrism(pitch*2, pitch*2, boundary_type='reflective') - - assembly = openmc.Cell(fill=lattice2x2, region=-box, name='assembly') - - # Create a geometry with the top-level cell - geometry = openmc.Geometry([assembly]) - - ############################################################################### - # Define problem settings - - # Instantiate a Settings object, set all runtime parameters, and export to XML - settings = openmc.Settings() - settings.energy_mode = "multi-group" - settings.batches = 10 - settings.inactive = 5 - settings.particles = 100 - - # Create an initial uniform spatial source distribution over fissionable zones - lower_left = (-pitch, -pitch, -1) - upper_right = (pitch, pitch, 1) - uniform_dist = openmc.stats.Box(lower_left, upper_right) - rr_source = openmc.IndependentSource(space=uniform_dist) - - settings.random_ray['distance_active'] = 100.0 - settings.random_ray['distance_inactive'] = 20.0 - settings.random_ray['ray_source'] = rr_source - settings.random_ray['volume_normalized_flux_tallies'] = True - - ############################################################################### - # Define tallies - - # Create a mesh that will be used for tallying - mesh = openmc.RegularMesh() - mesh.dimension = (2, 2) - mesh.lower_left = (-pitch, -pitch) - mesh.upper_right = (pitch, pitch) - - # Create a mesh filter that can be used in a tally - mesh_filter = openmc.MeshFilter(mesh) - - # Create an energy group filter as well - energy_filter = openmc.EnergyFilter(group_edges) - - # Now use the mesh filter in a tally and indicate what scores are desired - tally = openmc.Tally(name="Mesh tally") - tally.filters = [mesh_filter, energy_filter] - tally.scores = ['flux', 'fission', 'nu-fission'] - tally.estimator = 'analog' - - # Instantiate a Tallies collection and export to XML - tallies = openmc.Tallies([tally]) - - ############################################################################### - # Exporting to OpenMC model - ############################################################################### - - model = openmc.Model() - model.geometry = geometry - model.materials = materials - model.settings = settings - model.tallies = tallies - return model - - -def test_random_ray_basic(): - harness = MGXSTestHarness('statepoint.10.h5', random_ray_model()) - harness.main() diff --git a/tests/regression_tests/random_ray_cube/False_hybrid/inputs_true.dat b/tests/regression_tests/random_ray_cube/False_hybrid/inputs_true.dat deleted file mode 100644 index 82c24291b..000000000 --- a/tests/regression_tests/random_ray_cube/False_hybrid/inputs_true.dat +++ /dev/null @@ -1,1018 +0,0 @@ - - - - mgxs.h5 - - - - - - - - - - - - - - - - - - - - - 1.0 1.0 1.0 - 30 30 30 - 0.0 0.0 0.0 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - - - - - - - - - - fixed source - 90 - 10 - 5 - - - 100.0 1.0 - - - universe - 1 - - - multi-group - - 500.0 - 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - - False - - - - - 1 - - - 2 - - - 3 - - - 3 - flux - tracklength - - - 2 - flux - tracklength - - - 1 - flux - tracklength - - - diff --git a/tests/regression_tests/random_ray_cube/False_hybrid/results_true.dat b/tests/regression_tests/random_ray_cube/False_hybrid/results_true.dat deleted file mode 100644 index 88718c5bf..000000000 --- a/tests/regression_tests/random_ray_cube/False_hybrid/results_true.dat +++ /dev/null @@ -1,9 +0,0 @@ -tally 1: --5.920549E+04 -2.321317E+09 -tally 2: -4.761087E+02 -4.909570E+04 -tally 3: -2.206028E+01 -1.019164E+02 diff --git a/tests/regression_tests/random_ray_cube/True_hybrid/inputs_true.dat b/tests/regression_tests/random_ray_cube/True_hybrid/inputs_true.dat deleted file mode 100644 index 464c34404..000000000 --- a/tests/regression_tests/random_ray_cube/True_hybrid/inputs_true.dat +++ /dev/null @@ -1,1018 +0,0 @@ - - - - mgxs.h5 - - - - - - - - - - - - - - - - - - - - - 1.0 1.0 1.0 - 30 30 30 - 0.0 0.0 0.0 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - - - - - - - - - - fixed source - 90 - 10 - 5 - - - 100.0 1.0 - - - universe - 1 - - - multi-group - - 500.0 - 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - - True - - - - - 1 - - - 2 - - - 3 - - - 3 - flux - tracklength - - - 2 - flux - tracklength - - - 1 - flux - tracklength - - - diff --git a/tests/regression_tests/random_ray_cube/True_hybrid/results_true.dat b/tests/regression_tests/random_ray_cube/True_hybrid/results_true.dat deleted file mode 100644 index 4a2996740..000000000 --- a/tests/regression_tests/random_ray_cube/True_hybrid/results_true.dat +++ /dev/null @@ -1,9 +0,0 @@ -tally 1: --5.230164E+02 -1.818988E+05 -tally 2: -3.067508E-02 -2.037701E-04 -tally 3: -1.941220E-03 -7.892297E-07 diff --git a/tests/regression_tests/random_ray_cube/test.py b/tests/regression_tests/random_ray_cube/test.py deleted file mode 100644 index 51d61ae79..000000000 --- a/tests/regression_tests/random_ray_cube/test.py +++ /dev/null @@ -1,231 +0,0 @@ -import os - -import numpy as np -import openmc -from openmc.utility_funcs import change_directory -import pytest - -from tests.testing_harness import TolerantPyAPITestHarness - -# Creates a 3 region cube with different fills in each region -def fill_cube(N, n_1, n_2, fill_1, fill_2, fill_3): - cube = [[[0 for _ in range(N)] for _ in range(N)] for _ in range(N)] - for i in range(N): - for j in range(N): - for k in range(N): - if i < n_1 and j >= (N-n_1) and k < n_1: - cube[i][j][k] = fill_1 - elif i < n_2 and j >= (N-n_2) and k < n_2: - cube[i][j][k] = fill_2 - else: - cube[i][j][k] = fill_3 - return cube - -class MGXSTestHarness(TolerantPyAPITestHarness): - def _cleanup(self): - super()._cleanup() - f = 'mgxs.h5' - if os.path.exists(f): - os.remove(f) - -def create_random_ray_model(volume_normalize, estimator): - openmc.reset_auto_ids() - ############################################################################### - # Create multigroup data - - # Instantiate the energy group data - ebins = [1e-5, 20.0e6] - groups = openmc.mgxs.EnergyGroups(group_edges=ebins) - - void_sigma_a = 4.0e-6 - void_sigma_s = 3.0e-4 - void_mat_data = openmc.XSdata('void', groups) - void_mat_data.order = 0 - void_mat_data.set_total([void_sigma_a + void_sigma_s]) - void_mat_data.set_absorption([void_sigma_a]) - void_mat_data.set_scatter_matrix(np.rollaxis(np.array([[[void_sigma_s]]]),0,3)) - - absorber_sigma_a = 0.75 - absorber_sigma_s = 0.25 - absorber_mat_data = openmc.XSdata('absorber', groups) - absorber_mat_data.order = 0 - absorber_mat_data.set_total([absorber_sigma_a + absorber_sigma_s]) - absorber_mat_data.set_absorption([absorber_sigma_a]) - absorber_mat_data.set_scatter_matrix(np.rollaxis(np.array([[[absorber_sigma_s]]]),0,3)) - - multiplier = 0.1 - source_sigma_a = void_sigma_a * multiplier - source_sigma_s = void_sigma_s * multiplier - source_mat_data = openmc.XSdata('source', groups) - source_mat_data.order = 0 - print(source_sigma_a + source_sigma_s) - source_mat_data.set_total([source_sigma_a + source_sigma_s]) - source_mat_data.set_absorption([source_sigma_a]) - source_mat_data.set_scatter_matrix(np.rollaxis(np.array([[[source_sigma_s]]]),0,3)) - - mg_cross_sections_file = openmc.MGXSLibrary(groups) - mg_cross_sections_file.add_xsdatas([source_mat_data, void_mat_data, absorber_mat_data]) - mg_cross_sections_file.export_to_hdf5() - - ############################################################################### - # Create materials for the problem - - # Instantiate some Macroscopic Data - source_data = openmc.Macroscopic('source') - void_data = openmc.Macroscopic('void') - absorber_data = openmc.Macroscopic('absorber') - - # Instantiate some Materials and register the appropriate Macroscopic objects - source_mat = openmc.Material(name='source') - source_mat.set_density('macro', 1.0) - source_mat.add_macroscopic(source_data) - - void_mat = openmc.Material(name='void') - void_mat.set_density('macro', 1.0) - void_mat.add_macroscopic(void_data) - - absorber_mat = openmc.Material(name='absorber') - absorber_mat.set_density('macro', 1.0) - absorber_mat.add_macroscopic(absorber_data) - - # Instantiate a Materials collection and export to XML - materials_file = openmc.Materials([source_mat, void_mat, absorber_mat]) - materials_file.cross_sections = "mgxs.h5" - - ############################################################################### - # Define problem geometry - - source_cell = openmc.Cell(fill=source_mat, name='infinite source region') - void_cell = openmc.Cell(fill=void_mat, name='infinite void region') - absorber_cell = openmc.Cell(fill=absorber_mat, name='infinite absorber region') - - source_universe = openmc.Universe() - source_universe.add_cells([source_cell]) - - void_universe = openmc.Universe() - void_universe.add_cells([void_cell]) - - absorber_universe = openmc.Universe() - absorber_universe.add_cells([absorber_cell]) - - absorber_width = 30.0 - n_base = 6 - - # This variable can be increased above 1 to refine the FSR mesh resolution further - refinement_level = 5 - - n = n_base * refinement_level - pitch = absorber_width / n - - pattern = fill_cube(n, 1*refinement_level, 5*refinement_level, source_universe, void_universe, absorber_universe) - - lattice = openmc.RectLattice() - lattice.lower_left = [0.0, 0.0, 0.0] - lattice.pitch = [pitch, pitch, pitch] - lattice.universes = pattern - #print(lattice) - - lattice_cell = openmc.Cell(fill=lattice) - - lattice_uni = openmc.Universe() - lattice_uni.add_cells([lattice_cell]) - - x_low = openmc.XPlane(x0=0.0,boundary_type='reflective') - x_high = openmc.XPlane(x0=absorber_width,boundary_type='vacuum') - - y_low = openmc.YPlane(y0=0.0,boundary_type='reflective') - y_high = openmc.YPlane(y0=absorber_width,boundary_type='vacuum') - - z_low = openmc.ZPlane(z0=0.0,boundary_type='reflective') - z_high = openmc.ZPlane(z0=absorber_width,boundary_type='vacuum') - - full_domain = openmc.Cell(fill=lattice_uni, region=+x_low & -x_high & +y_low & -y_high & +z_low & -z_high, name='full domain') - - root = openmc.Universe(name='root universe') - root.add_cell(full_domain) - - # Create a geometry with the two cells and export to XML - geometry = openmc.Geometry(root) - - ############################################################################### - # Define problem settings - - # Instantiate a Settings object, set all runtime parameters, and export to XML - settings = openmc.Settings() - settings.energy_mode = "multi-group" - settings.inactive = 5 - settings.batches = 10 - settings.particles = 90 - settings.run_mode = 'fixed source' - - # Create an initial uniform spatial source for ray integration - lower_left_ray = [0.0, 0.0, 0.0] - upper_right_ray = [absorber_width, absorber_width, absorber_width] - uniform_dist_ray = openmc.stats.Box(lower_left_ray, upper_right_ray, only_fissionable=False) - rr_source = openmc.IndependentSource(space=uniform_dist_ray) - - settings.random_ray['distance_active'] = 500.0 - settings.random_ray['distance_inactive'] = 100.0 - settings.random_ray['ray_source'] = rr_source - settings.random_ray['volume_normalized_flux_tallies'] = volume_normalize - - #settings.random_ray['volume_estimator'] = estimator - - # Create the neutron source in the bottom right of the moderator - strengths = [1.0] # Good - fast group appears largest (besides most thermal) - midpoints = [100.0] - energy_distribution = openmc.stats.Discrete(x=midpoints,p=strengths) - - source = openmc.IndependentSource(energy=energy_distribution, constraints={'domains':[source_universe]}, strength=3.14) - - settings.source = [source] - - ############################################################################### - # Define tallies - - estimator = 'tracklength' - - absorber_filter = openmc.MaterialFilter(absorber_mat) - absorber_tally = openmc.Tally(name="Absorber Tally") - absorber_tally.filters = [absorber_filter] - absorber_tally.scores = ['flux'] - absorber_tally.estimator = estimator - - void_filter = openmc.MaterialFilter(void_mat) - void_tally = openmc.Tally(name="Void Tally") - void_tally.filters = [void_filter] - void_tally.scores = ['flux'] - void_tally.estimator = estimator - - source_filter = openmc.MaterialFilter(source_mat) - source_tally = openmc.Tally(name="Source Tally") - source_tally.filters = [source_filter] - source_tally.scores = ['flux'] - source_tally.estimator = estimator - - # Instantiate a Tallies collection and export to XML - tallies = openmc.Tallies([source_tally, void_tally, absorber_tally]) - - ############################################################################### - # Assmble Model - - model = openmc.model.Model() - model.geometry = geometry - model.materials = materials_file - model.settings = settings - model.xs_data = mg_cross_sections_file - model.tallies = tallies - - return model - -@pytest.mark.parametrize("volume_normalize, estimator", [ - (False, "hybrid"), - (True, "hybrid") -]) -def test_random_ray_cube(volume_normalize, estimator): - # Generating a unique directory name from the parameters - directory_name = f"{volume_normalize}_{estimator}" - with change_directory(directory_name): - model = create_random_ray_model(volume_normalize, estimator) - harness = MGXSTestHarness('statepoint.10.h5', model) - harness.main() \ No newline at end of file diff --git a/tests/regression_tests/random_ray_fixed_source/cell/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source/cell/inputs_true.dat deleted file mode 100644 index a8eaa0096..000000000 --- a/tests/regression_tests/random_ray_fixed_source/cell/inputs_true.dat +++ /dev/null @@ -1,205 +0,0 @@ - - - - mgxs.h5 - - - - - - - - - - - - - - - - - - - - - - - - 5.0 5.0 5.0 - 2 2 2 - -5.0 -5.0 -5.0 - -1 1 -1 1 - -1 1 -1 1 - - - 5.0 5.0 5.0 - 2 2 2 - -5.0 -5.0 -5.0 - -2 2 -2 2 - -2 2 -2 2 - - - 5.0 5.0 5.0 - 2 2 2 - -5.0 -5.0 -5.0 - -3 3 -3 3 - -3 3 -3 3 - - - 10.0 10.0 10.0 - 6 10 6 - 0.0 0.0 0.0 - -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -8 8 8 8 9 9 -8 9 9 9 9 9 -8 9 9 9 9 9 -8 9 9 9 9 9 -8 9 9 9 9 9 -7 9 9 9 9 9 - -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 8 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 - -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 8 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 - -9 9 9 8 9 9 -9 9 9 8 9 9 -9 9 9 8 9 9 -9 9 9 8 9 9 -9 9 9 8 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 - -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 - -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 - - - - - - - - - - fixed source - 1000 - 10 - 5 - - - 100.0 1.0 - - - cell - 4 - - - multi-group - - 400.0 - 100.0 - False - - - 0.0 0.0 0.0 60.0 100.0 60.0 - - - - - - - 1 10 1 - 0.0 0.0 0.0 - 10.0 100.0 10.0 - - - 6 1 1 - 0.0 50.0 0.0 - 60.0 60.0 10.0 - - - 6 1 1 - 0.0 90.0 30.0 - 60.0 100.0 40.0 - - - 1 - - - 2 - - - 3 - - - 1 - flux - analog - - - 2 - flux - analog - - - 3 - flux - analog - - - diff --git a/tests/regression_tests/random_ray_fixed_source/cell/results_true.dat b/tests/regression_tests/random_ray_fixed_source/cell/results_true.dat deleted file mode 100644 index ba1254d38..000000000 --- a/tests/regression_tests/random_ray_fixed_source/cell/results_true.dat +++ /dev/null @@ -1,47 +0,0 @@ -tally 1: -3.751047E+01 -2.841797E+02 -1.000671E+01 -2.020214E+01 -3.979569E+00 -3.330838E+00 -2.552971E+00 -1.407542E+00 -1.736764E+00 -7.948004E-01 -1.178834E+00 -4.328006E-01 -6.953895E-01 -1.408391E-01 -2.389347E-01 -1.617450E-02 -8.128516E-02 -1.903860E-03 -2.764062E-02 -2.285458E-04 -tally 2: -1.178834E+00 -4.328006E-01 -3.982124E-01 -4.138078E-02 -9.676374E-02 -1.919297E-03 -3.944531E-02 -3.133733E-04 -1.554518E-02 -5.084822E-05 -4.815079E-03 -4.681450E-06 -tally 3: -1.635823E-03 -6.482611E-07 -1.248235E-03 -3.227247E-07 -1.248195E-03 -3.488233E-07 -1.030935E-03 -2.206418E-07 -5.670315E-04 -6.648507E-08 -2.393638E-04 -1.242517E-08 diff --git a/tests/regression_tests/random_ray_fixed_source/material/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source/material/inputs_true.dat deleted file mode 100644 index e3ad3703b..000000000 --- a/tests/regression_tests/random_ray_fixed_source/material/inputs_true.dat +++ /dev/null @@ -1,205 +0,0 @@ - - - - mgxs.h5 - - - - - - - - - - - - - - - - - - - - - - - - 5.0 5.0 5.0 - 2 2 2 - -5.0 -5.0 -5.0 - -1 1 -1 1 - -1 1 -1 1 - - - 5.0 5.0 5.0 - 2 2 2 - -5.0 -5.0 -5.0 - -2 2 -2 2 - -2 2 -2 2 - - - 5.0 5.0 5.0 - 2 2 2 - -5.0 -5.0 -5.0 - -3 3 -3 3 - -3 3 -3 3 - - - 10.0 10.0 10.0 - 6 10 6 - 0.0 0.0 0.0 - -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -8 8 8 8 9 9 -8 9 9 9 9 9 -8 9 9 9 9 9 -8 9 9 9 9 9 -8 9 9 9 9 9 -7 9 9 9 9 9 - -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 8 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 - -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 8 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 - -9 9 9 8 9 9 -9 9 9 8 9 9 -9 9 9 8 9 9 -9 9 9 8 9 9 -9 9 9 8 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 - -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 - -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 - - - - - - - - - - fixed source - 1000 - 10 - 5 - - - 100.0 1.0 - - - material - 1 - - - multi-group - - 400.0 - 100.0 - False - - - 0.0 0.0 0.0 60.0 100.0 60.0 - - - - - - - 1 10 1 - 0.0 0.0 0.0 - 10.0 100.0 10.0 - - - 6 1 1 - 0.0 50.0 0.0 - 60.0 60.0 10.0 - - - 6 1 1 - 0.0 90.0 30.0 - 60.0 100.0 40.0 - - - 1 - - - 2 - - - 3 - - - 1 - flux - analog - - - 2 - flux - analog - - - 3 - flux - analog - - - diff --git a/tests/regression_tests/random_ray_fixed_source/material/results_true.dat b/tests/regression_tests/random_ray_fixed_source/material/results_true.dat deleted file mode 100644 index ba1254d38..000000000 --- a/tests/regression_tests/random_ray_fixed_source/material/results_true.dat +++ /dev/null @@ -1,47 +0,0 @@ -tally 1: -3.751047E+01 -2.841797E+02 -1.000671E+01 -2.020214E+01 -3.979569E+00 -3.330838E+00 -2.552971E+00 -1.407542E+00 -1.736764E+00 -7.948004E-01 -1.178834E+00 -4.328006E-01 -6.953895E-01 -1.408391E-01 -2.389347E-01 -1.617450E-02 -8.128516E-02 -1.903860E-03 -2.764062E-02 -2.285458E-04 -tally 2: -1.178834E+00 -4.328006E-01 -3.982124E-01 -4.138078E-02 -9.676374E-02 -1.919297E-03 -3.944531E-02 -3.133733E-04 -1.554518E-02 -5.084822E-05 -4.815079E-03 -4.681450E-06 -tally 3: -1.635823E-03 -6.482611E-07 -1.248235E-03 -3.227247E-07 -1.248195E-03 -3.488233E-07 -1.030935E-03 -2.206418E-07 -5.670315E-04 -6.648507E-08 -2.393638E-04 -1.242517E-08 diff --git a/tests/regression_tests/random_ray_fixed_source/test.py b/tests/regression_tests/random_ray_fixed_source/test.py deleted file mode 100644 index 7ceadca5b..000000000 --- a/tests/regression_tests/random_ray_fixed_source/test.py +++ /dev/null @@ -1,340 +0,0 @@ -import os - -import numpy as np -import openmc -from openmc.utility_funcs import change_directory -import pytest - -from tests.testing_harness import TolerantPyAPITestHarness - -def fill_3d_list(n, val): - """ - Generates a 3D list of dimensions nxnxn filled with copies of val. - - Parameters: - n (int): The dimension of the 3D list. - val (any): The value to fill the 3D list with. - - Returns: - list: A 3D list of dimensions nxnxn filled with val. - """ - return [[[val for _ in range(n)] for _ in range(n)] for _ in range(n)] - - -class MGXSTestHarness(TolerantPyAPITestHarness): - def _cleanup(self): - super()._cleanup() - f = 'mgxs.h5' - if os.path.exists(f): - os.remove(f) - -def create_random_ray_model(domain_type): - openmc.reset_auto_ids() - ############################################################################### - # Create multigroup data - - # Instantiate the energy group data - ebins = [1e-5, 20.0e6] - groups = openmc.mgxs.EnergyGroups(group_edges=ebins) - - # High scattering ratio means system is all scattering - # Low means fully absorbing - scattering_ratio = 0.5 - - source_total_xs = 0.1 - source_mat_data = openmc.XSdata('source', groups) - source_mat_data.order = 0 - source_mat_data.set_total([source_total_xs]) - source_mat_data.set_absorption([source_total_xs * (1.0 - scattering_ratio)]) - source_mat_data.set_scatter_matrix(np.rollaxis(np.array([[[source_total_xs * scattering_ratio]]]),0,3)) - - void_total_xs = 1.0e-4 - void_mat_data = openmc.XSdata('void', groups) - void_mat_data.order = 0 - void_mat_data.set_total([void_total_xs]) - void_mat_data.set_absorption([void_total_xs * (1.0 - scattering_ratio)]) - void_mat_data.set_scatter_matrix(np.rollaxis(np.array([[[void_total_xs * scattering_ratio]]]),0,3)) - - shield_total_xs = 0.1 - shield_mat_data = openmc.XSdata('shield', groups) - shield_mat_data.order = 0 - shield_mat_data.set_total([shield_total_xs]) - shield_mat_data.set_absorption([shield_total_xs * (1.0 - scattering_ratio)]) - shield_mat_data.set_scatter_matrix(np.rollaxis(np.array([[[shield_total_xs * scattering_ratio]]]),0,3)) - - mg_cross_sections_file = openmc.MGXSLibrary(groups) - mg_cross_sections_file.add_xsdatas([source_mat_data, void_mat_data, shield_mat_data]) - mg_cross_sections_file.export_to_hdf5() - - ############################################################################### - # Create materials for the problem - - # Instantiate some Macroscopic Data - source_data = openmc.Macroscopic('source') - void_data = openmc.Macroscopic('void') - shield_data = openmc.Macroscopic('shield') - - # Instantiate some Materials and register the appropriate Macroscopic objects - source_mat = openmc.Material(name='source') - source_mat.set_density('macro', 1.0) - source_mat.add_macroscopic(source_data) - - void_mat = openmc.Material(name='void') - void_mat.set_density('macro', 1.0) - void_mat.add_macroscopic(void_data) - - shield_mat = openmc.Material(name='shield') - shield_mat.set_density('macro', 1.0) - shield_mat.add_macroscopic(shield_data) - - # Instantiate a Materials collection and export to XML - materials_file = openmc.Materials([source_mat, void_mat, shield_mat]) - materials_file.cross_sections = "mgxs.h5" - - ############################################################################### - # Define problem geometry - - source_cell = openmc.Cell(fill=source_mat, name='infinite source region') - void_cell = openmc.Cell(fill=void_mat, name='infinite void region') - shield_cell = openmc.Cell(fill=shield_mat, name='infinite shield region') - - sub = openmc.Universe() - sub.add_cells([source_cell]) - - vub = openmc.Universe() - vub.add_cells([void_cell]) - - aub = openmc.Universe() - aub.add_cells([shield_cell]) - - # n controls the dimension of subdivision within each outer lattice element - # E.g., n = 10 results in 1cm cubic FSRs - n = 2 - delta = 10.0 / n - ll = [-5.0, -5.0, -5.0] - pitch = [delta, delta, delta] - - source_lattice = openmc.RectLattice() - source_lattice.lower_left = ll - source_lattice.pitch = pitch - source_lattice.universes = fill_3d_list(n, sub) - - void_lattice = openmc.RectLattice() - void_lattice.lower_left = ll - void_lattice.pitch = pitch - void_lattice.universes = fill_3d_list(n, vub) - - shield_lattice = openmc.RectLattice() - shield_lattice.lower_left = ll - shield_lattice.pitch = pitch - shield_lattice.universes = fill_3d_list(n, aub) - - source_lattice_cell = openmc.Cell(fill=source_lattice, name='source lattice cell') - su = openmc.Universe() - su.add_cells([source_lattice_cell]) - - void_lattice_cell = openmc.Cell(fill=void_lattice, name='void lattice cell') - vu = openmc.Universe() - vu.add_cells([void_lattice_cell]) - - shield_lattice_cell = openmc.Cell(fill=shield_lattice, name='shield lattice cell') - au = openmc.Universe() - au.add_cells([shield_lattice_cell]) - - z_base = [ - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [vu, vu, vu, vu, au, au], - [vu, au, au, au, au, au], - [vu, au, au, au, au, au], - [vu, au, au, au, au, au], - [vu, au, au, au, au, au], - [su, au, au, au, au, au] - ] - - z_col = [ - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, vu, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au] - ] - - z_high = [ - [au, au, au, vu, au, au], - [au, au, au, vu, au, au], - [au, au, au, vu, au, au], - [au, au, au, vu, au, au], - [au, au, au, vu, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au] - ] - - z_cap = [ - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au] - ] - - dogleg_pattern = [ - z_base, - z_col, - z_col, - z_high, - z_cap, - z_cap - ] - - x = 60.0 - x_dim = 6 - - y = 100.0 - y_dim = 10 - - z = 60.0 - z_dim = 6 - - lattice = openmc.RectLattice() - lattice.lower_left = [0.0, 0.0, 0.0] - lattice.pitch = [x/x_dim, y/y_dim, z/z_dim] - lattice.universes = dogleg_pattern - - lattice_cell = openmc.Cell(fill=lattice, name='dogleg lattice cell') - - lattice_uni = openmc.Universe() - lattice_uni.add_cells([lattice_cell]) - - x_low = openmc.XPlane(x0=0.0,boundary_type='reflective') - x_high = openmc.XPlane(x0=x,boundary_type='vacuum') - - y_low = openmc.YPlane(y0=0.0,boundary_type='reflective') - y_high = openmc.YPlane(y0=y,boundary_type='vacuum') - - z_low = openmc.ZPlane(z0=0.0,boundary_type='reflective') - z_high = openmc.ZPlane(z0=z,boundary_type='vacuum') - - full_domain = openmc.Cell(fill=lattice_uni, region=+x_low & -x_high & +y_low & -y_high & +z_low & -z_high, name='full domain') - - root = openmc.Universe(name='root universe') - root.add_cell(full_domain) - - # Create a geometry with the two cells and export to XML - geometry = openmc.Geometry(root) - - ############################################################################### - # Define problem settings - - # Instantiate a Settings object, set all runtime parameters, and export to XML - settings = openmc.Settings() - settings.energy_mode = "multi-group" - settings.batches = 10 - settings.inactive = 5 - settings.particles = 1000 - settings.run_mode = 'fixed source' - - settings.random_ray['distance_active'] = 400.0 - settings.random_ray['distance_inactive'] = 100.0 - settings.random_ray['volume_normalized_flux_tallies'] = False - - # Create an initial uniform spatial source for ray integration - lower_left = (0.0, 0.0, 0.0) - upper_right = (x, y, z) - uniform_dist = openmc.stats.Box(lower_left, upper_right) - settings.random_ray['ray_source']= openmc.IndependentSource(space=uniform_dist) - - # Create the neutron source in the bottom right of the moderator - strengths = [1.0] - midpoints = [100.0] - energy_distribution = openmc.stats.Discrete(x=midpoints,p=strengths) - if domain_type == 'cell': - domain = source_lattice_cell - elif domain_type == 'material': - domain = source_mat - elif domain_type == 'universe': - domain = sub - source = openmc.IndependentSource( - energy=energy_distribution, - constraints={'domains': [domain]} - ) - settings.source = [source] - - ############################################################################### - # Define tallies - - estimator = 'analog' - - # Case 3A - mesh_3A = openmc.RegularMesh() - mesh_3A.dimension = (1, y_dim, 1) - mesh_3A.lower_left = (0.0, 0.0, 0.0) - mesh_3A.upper_right = (10.0, y, 10.0) - mesh_filter_3A = openmc.MeshFilter(mesh_3A) - - tally_3A = openmc.Tally(name="Case 3A") - tally_3A.filters = [mesh_filter_3A] - tally_3A.scores = ['flux'] - tally_3A.estimator = estimator - - # Case 3B - mesh_3B = openmc.RegularMesh() - mesh_3B.dimension = (x_dim, 1, 1) - mesh_3B.lower_left = (0.0, 50.0, 0.0) - mesh_3B.upper_right = (x, 60.0, 10.0) - mesh_filter_3B = openmc.MeshFilter(mesh_3B) - - tally_3B = openmc.Tally(name="Case 3B") - tally_3B.filters = [mesh_filter_3B] - tally_3B.scores = ['flux'] - tally_3B.estimator = estimator - - # Case 3C - mesh_3C = openmc.RegularMesh() - mesh_3C.dimension = (x_dim, 1, 1) - mesh_3C.lower_left = (0.0, 90.0, 30.0) - mesh_3C.upper_right = (x, 100.0, 40.0) - mesh_filter_3C = openmc.MeshFilter(mesh_3C) - - tally_3C = openmc.Tally(name="Case 3C") - tally_3C.filters = [mesh_filter_3C] - tally_3C.scores = ['flux'] - tally_3C.estimator = estimator - - # Instantiate a Tallies collection and export to XML - tallies = openmc.Tallies([tally_3A, tally_3B, tally_3C]) - - ############################################################################### - # Assmble Model - - model = openmc.model.Model() - model.geometry = geometry - model.materials = materials_file - model.settings = settings - model.xs_data = mg_cross_sections_file - model.tallies = tallies - - return model - -@pytest.mark.parametrize("domain_type", ["cell", "material", "universe"]) -def test_random_ray_fixed_source(domain_type): - with change_directory(domain_type): - model = create_random_ray_model(domain_type) - - harness = MGXSTestHarness('statepoint.10.h5', model) - harness.main() diff --git a/tests/regression_tests/random_ray_fixed_source/universe/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source/universe/inputs_true.dat deleted file mode 100644 index efbc03adf..000000000 --- a/tests/regression_tests/random_ray_fixed_source/universe/inputs_true.dat +++ /dev/null @@ -1,205 +0,0 @@ - - - - mgxs.h5 - - - - - - - - - - - - - - - - - - - - - - - - 5.0 5.0 5.0 - 2 2 2 - -5.0 -5.0 -5.0 - -1 1 -1 1 - -1 1 -1 1 - - - 5.0 5.0 5.0 - 2 2 2 - -5.0 -5.0 -5.0 - -2 2 -2 2 - -2 2 -2 2 - - - 5.0 5.0 5.0 - 2 2 2 - -5.0 -5.0 -5.0 - -3 3 -3 3 - -3 3 -3 3 - - - 10.0 10.0 10.0 - 6 10 6 - 0.0 0.0 0.0 - -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -8 8 8 8 9 9 -8 9 9 9 9 9 -8 9 9 9 9 9 -8 9 9 9 9 9 -8 9 9 9 9 9 -7 9 9 9 9 9 - -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 8 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 - -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 8 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 - -9 9 9 8 9 9 -9 9 9 8 9 9 -9 9 9 8 9 9 -9 9 9 8 9 9 -9 9 9 8 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 - -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 - -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 - - - - - - - - - - fixed source - 1000 - 10 - 5 - - - 100.0 1.0 - - - universe - 1 - - - multi-group - - 400.0 - 100.0 - False - - - 0.0 0.0 0.0 60.0 100.0 60.0 - - - - - - - 1 10 1 - 0.0 0.0 0.0 - 10.0 100.0 10.0 - - - 6 1 1 - 0.0 50.0 0.0 - 60.0 60.0 10.0 - - - 6 1 1 - 0.0 90.0 30.0 - 60.0 100.0 40.0 - - - 1 - - - 2 - - - 3 - - - 1 - flux - analog - - - 2 - flux - analog - - - 3 - flux - analog - - - diff --git a/tests/regression_tests/random_ray_fixed_source/universe/results_true.dat b/tests/regression_tests/random_ray_fixed_source/universe/results_true.dat deleted file mode 100644 index ba1254d38..000000000 --- a/tests/regression_tests/random_ray_fixed_source/universe/results_true.dat +++ /dev/null @@ -1,47 +0,0 @@ -tally 1: -3.751047E+01 -2.841797E+02 -1.000671E+01 -2.020214E+01 -3.979569E+00 -3.330838E+00 -2.552971E+00 -1.407542E+00 -1.736764E+00 -7.948004E-01 -1.178834E+00 -4.328006E-01 -6.953895E-01 -1.408391E-01 -2.389347E-01 -1.617450E-02 -8.128516E-02 -1.903860E-03 -2.764062E-02 -2.285458E-04 -tally 2: -1.178834E+00 -4.328006E-01 -3.982124E-01 -4.138078E-02 -9.676374E-02 -1.919297E-03 -3.944531E-02 -3.133733E-04 -1.554518E-02 -5.084822E-05 -4.815079E-03 -4.681450E-06 -tally 3: -1.635823E-03 -6.482611E-07 -1.248235E-03 -3.227247E-07 -1.248195E-03 -3.488233E-07 -1.030935E-03 -2.206418E-07 -5.670315E-04 -6.648507E-08 -2.393638E-04 -1.242517E-08 diff --git a/tests/regression_tests/random_ray_basic/__init__.py b/tests/regression_tests/random_ray_fixed_source_domain/__init__.py similarity index 100% rename from tests/regression_tests/random_ray_basic/__init__.py rename to tests/regression_tests/random_ray_fixed_source_domain/__init__.py diff --git a/tests/regression_tests/random_ray_fixed_source_domain/cell/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/cell/inputs_true.dat new file mode 100644 index 000000000..d6ca8918c --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_domain/cell/inputs_true.dat @@ -0,0 +1,244 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + cell + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_fixed_source_domain/cell/results_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/cell/results_true.dat new file mode 100644 index 000000000..5f2975860 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_domain/cell/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +-5.745886E+02 +9.758367E+04 +tally 2: +2.971927E-02 +1.827222E-04 +tally 3: +1.978393E-03 +7.951531E-07 diff --git a/tests/regression_tests/random_ray_fixed_source_domain/material/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/material/inputs_true.dat new file mode 100644 index 000000000..7fae491ae --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_domain/material/inputs_true.dat @@ -0,0 +1,244 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + material + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_fixed_source_domain/material/results_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/material/results_true.dat new file mode 100644 index 000000000..5f2975860 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_domain/material/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +-5.745886E+02 +9.758367E+04 +tally 2: +2.971927E-02 +1.827222E-04 +tally 3: +1.978393E-03 +7.951531E-07 diff --git a/tests/regression_tests/random_ray_fixed_source_domain/test.py b/tests/regression_tests/random_ray_fixed_source_domain/test.py new file mode 100644 index 000000000..5885a9200 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_domain/test.py @@ -0,0 +1,51 @@ +import os + +import openmc +from openmc.utility_funcs import change_directory +from openmc.examples import random_ray_three_region_cube +import pytest + +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) + + +@pytest.mark.parametrize("domain_type", ["cell", "material", "universe"]) +def test_random_ray_fixed_source(domain_type): + with change_directory(domain_type): + openmc.reset_auto_ids() + model = random_ray_three_region_cube() + + # Based on the parameter, we need to adjust + # the particle source constraints + source = model.settings.source[0] + constraints = source.constraints + + if domain_type == 'cell': + cells = model.geometry.get_all_cells() + for key, cell in cells.items(): + print(cell.name) + if cell.name == 'infinite source region': + constraints['domain_type'] = 'cell' + constraints['domain_ids'] = [cell.id] + elif domain_type == 'material': + materials = model.materials + for material in materials: + if material.name == 'source': + constraints['domain_type'] = 'material' + constraints['domain_ids'] = [material.id] + elif domain_type == 'universe': + universes = model.geometry.get_all_universes() + for key, universe in universes.items(): + if universe.name == 'source universe': + constraints['domain_type'] = 'universe' + constraints['domain_ids'] = [universe.id] + + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_fixed_source_domain/universe/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/universe/inputs_true.dat new file mode 100644 index 000000000..75d84efe9 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_domain/universe/inputs_true.dat @@ -0,0 +1,244 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_fixed_source_domain/universe/results_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/universe/results_true.dat new file mode 100644 index 000000000..5f2975860 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_domain/universe/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +-5.745886E+02 +9.758367E+04 +tally 2: +2.971927E-02 +1.827222E-04 +tally 3: +1.978393E-03 +7.951531E-07 diff --git a/tests/regression_tests/random_ray_fixed_source_normalization/False/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_normalization/False/inputs_true.dat new file mode 100644 index 000000000..559cff0f0 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_normalization/False/inputs_true.dat @@ -0,0 +1,244 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + False + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_fixed_source_normalization/False/results_true.dat b/tests/regression_tests/random_ray_fixed_source_normalization/False/results_true.dat new file mode 100644 index 000000000..dbe778df9 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_normalization/False/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +-6.614590E+04 +1.283943E+09 +tally 2: +4.612657E+02 +4.402080E+04 +tally 3: +2.248302E+01 +1.026884E+02 diff --git a/tests/regression_tests/random_ray_fixed_source_normalization/True/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_normalization/True/inputs_true.dat new file mode 100644 index 000000000..75d84efe9 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_normalization/True/inputs_true.dat @@ -0,0 +1,244 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_fixed_source_normalization/True/results_true.dat b/tests/regression_tests/random_ray_fixed_source_normalization/True/results_true.dat new file mode 100644 index 000000000..5f2975860 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_normalization/True/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +-5.745886E+02 +9.758367E+04 +tally 2: +2.971927E-02 +1.827222E-04 +tally 3: +1.978393E-03 +7.951531E-07 diff --git a/tests/regression_tests/random_ray_cube/__init__.py b/tests/regression_tests/random_ray_fixed_source_normalization/__init__.py similarity index 100% rename from tests/regression_tests/random_ray_cube/__init__.py rename to tests/regression_tests/random_ray_fixed_source_normalization/__init__.py diff --git a/tests/regression_tests/random_ray_fixed_source_normalization/test.py b/tests/regression_tests/random_ray_fixed_source_normalization/test.py new file mode 100644 index 000000000..3fa4ba2a6 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_normalization/test.py @@ -0,0 +1,27 @@ +import os + +import openmc +from openmc.utility_funcs import change_directory +from openmc.examples import random_ray_three_region_cube +import pytest + +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) + + +@pytest.mark.parametrize("normalize", [True, False]) +def test_random_ray_fixed_source(normalize): + with change_directory(str(normalize)): + openmc.reset_auto_ids() + model = random_ray_three_region_cube() + model.settings.random_ray['volume_normalized_flux_tallies'] = normalize + + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_fixed_source/__init__.py b/tests/regression_tests/random_ray_k_eff/__init__.py similarity index 100% rename from tests/regression_tests/random_ray_fixed_source/__init__.py rename to tests/regression_tests/random_ray_k_eff/__init__.py diff --git a/tests/regression_tests/random_ray_basic/inputs_true.dat b/tests/regression_tests/random_ray_k_eff/inputs_true.dat similarity index 100% rename from tests/regression_tests/random_ray_basic/inputs_true.dat rename to tests/regression_tests/random_ray_k_eff/inputs_true.dat diff --git a/tests/regression_tests/random_ray_basic/results_true.dat b/tests/regression_tests/random_ray_k_eff/results_true.dat similarity index 100% rename from tests/regression_tests/random_ray_basic/results_true.dat rename to tests/regression_tests/random_ray_k_eff/results_true.dat diff --git a/tests/regression_tests/random_ray_k_eff/test.py b/tests/regression_tests/random_ray_k_eff/test.py new file mode 100644 index 000000000..8dd0dd915 --- /dev/null +++ b/tests/regression_tests/random_ray_k_eff/test.py @@ -0,0 +1,19 @@ +import os + +from openmc.examples import random_ray_lattice + +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_basic(): + model = random_ray_lattice() + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_vacuum/__init__.py b/tests/regression_tests/random_ray_vacuum/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/regression_tests/random_ray_vacuum/inputs_true.dat b/tests/regression_tests/random_ray_vacuum/inputs_true.dat deleted file mode 100644 index 5b2fa0905..000000000 --- a/tests/regression_tests/random_ray_vacuum/inputs_true.dat +++ /dev/null @@ -1,109 +0,0 @@ - - - - mgxs.h5 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0.126 0.126 - 10 10 - -0.63 -0.63 - -3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 - - - 1.26 1.26 - 2 2 - -1.26 -1.26 - -2 2 -2 5 - - - - - - - - - - - - - - - - - - - - - eigenvalue - 100 - 10 - 5 - multi-group - - 100.0 - 20.0 - - - -1.26 -1.26 -1 1.26 1.26 1 - - - True - - - - - 2 2 - -1.26 -1.26 - 1.26 1.26 - - - 1 - - - 1e-05 0.0635 10.0 100.0 1000.0 500000.0 1000000.0 20000000.0 - - - 1 2 - flux fission nu-fission - analog - - - diff --git a/tests/regression_tests/random_ray_vacuum/results_true.dat b/tests/regression_tests/random_ray_vacuum/results_true.dat deleted file mode 100644 index c25999aad..000000000 --- a/tests/regression_tests/random_ray_vacuum/results_true.dat +++ /dev/null @@ -1,171 +0,0 @@ -k-combined: -1.010455E-01 1.585558E-02 -tally 1: -7.466984E-01 -1.245920E-01 -2.771079E-01 -1.714504E-02 -6.744252E-01 -1.015566E-01 -1.634870E-01 -6.288701E-03 -2.405120E-02 -1.362874E-04 -5.853580E-02 -8.072823E-04 -1.641162E-02 -6.568765E-05 -5.223801E-04 -6.753516E-08 -1.271369E-03 -4.000365E-07 -3.014736E-02 -1.922973E-04 -9.765325E-04 -2.043645E-07 -2.376685E-03 -1.210529E-06 -1.562379E-01 -4.906526E-03 -1.746664E-03 -6.141658E-07 -4.251084E-03 -3.638028E-06 -1.826385E+00 -6.678141E-01 -3.095716E-03 -1.920117E-06 -7.660132E-03 -1.175650E-05 -2.013809E+00 -8.136726E-01 -3.015545E-02 -1.832201E-04 -8.387586E-02 -1.417475E-03 -1.573069E+00 -5.388374E-01 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.867849E-01 -1.864798E-02 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.136372E-02 -1.035499E-04 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -3.973323E-02 -3.229766E-04 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.779974E-01 -6.350613E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.036886E+00 -2.151147E-01 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.109991E+00 -2.468005E-01 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -5.813217E-01 -7.704274E-02 -2.160141E-01 -1.062660E-02 -5.257350E-01 -6.294540E-02 -1.358032E-01 -4.462239E-03 -2.030839E-02 -1.002755E-04 -4.942655E-02 -5.939703E-04 -1.612888E-02 -6.604109E-05 -5.274425E-04 -7.250333E-08 -1.283689E-03 -4.294649E-07 -2.985723E-02 -1.925418E-04 -9.868482E-04 -2.142916E-07 -2.401791E-03 -1.269331E-06 -1.590678E-01 -5.110940E-03 -1.800903E-03 -6.566490E-07 -4.383091E-03 -3.889678E-06 -1.928611E+00 -7.475889E-01 -3.310101E-03 -2.209547E-06 -8.190613E-03 -1.352862E-05 -2.172777E+00 -9.544513E-01 -3.334566E-02 -2.267149E-04 -9.274926E-02 -1.753971E-03 -7.566911E-01 -1.278105E-01 -2.881892E-01 -1.854375E-02 -7.013949E-01 -1.098417E-01 -1.662732E-01 -6.495387E-03 -2.515274E-02 -1.488430E-04 -6.121675E-02 -8.816538E-04 -1.682747E-02 -6.933017E-05 -5.514441E-04 -7.567901E-08 -1.342104E-03 -4.482757E-07 -3.068773E-02 -1.997062E-04 -1.021094E-03 -2.240938E-07 -2.485139E-03 -1.327393E-06 -1.578722E-01 -5.013656E-03 -1.812622E-03 -6.620083E-07 -4.411613E-03 -3.921424E-06 -1.922798E+00 -7.400474E-01 -3.401690E-03 -2.320513E-06 -8.417243E-03 -1.420805E-05 -2.178318E+00 -9.546106E-01 -3.451316E-02 -2.421994E-04 -9.599661E-02 -1.873766E-03 diff --git a/tests/regression_tests/random_ray_vacuum/test.py b/tests/regression_tests/random_ray_vacuum/test.py deleted file mode 100644 index 3923b8a81..000000000 --- a/tests/regression_tests/random_ray_vacuum/test.py +++ /dev/null @@ -1,236 +0,0 @@ -import os - -import numpy as np -import openmc - -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 random_ray_model() -> openmc.Model: - ############################################################################### - # Create multigroup data - - # Instantiate the energy group data - group_edges = [1e-5, 0.0635, 10.0, 1.0e2, 1.0e3, 0.5e6, 1.0e6, 20.0e6] - groups = openmc.mgxs.EnergyGroups(group_edges) - - # Instantiate the 7-group (C5G7) cross section data - uo2_xsdata = openmc.XSdata('UO2', groups) - uo2_xsdata.order = 0 - uo2_xsdata.set_total( - [0.1779492, 0.3298048, 0.4803882, 0.5543674, 0.3118013, 0.3951678, - 0.5644058]) - uo2_xsdata.set_absorption([8.0248e-03, 3.7174e-03, 2.6769e-02, 9.6236e-02, - 3.0020e-02, 1.1126e-01, 2.8278e-01]) - scatter_matrix = np.array( - [[[0.1275370, 0.0423780, 0.0000094, 0.0000000, 0.0000000, 0.0000000, 0.0000000], - [0.0000000, 0.3244560, 0.0016314, 0.0000000, 0.0000000, 0.0000000, 0.0000000], - [0.0000000, 0.0000000, 0.4509400, 0.0026792, 0.0000000, 0.0000000, 0.0000000], - [0.0000000, 0.0000000, 0.0000000, 0.4525650, 0.0055664, 0.0000000, 0.0000000], - [0.0000000, 0.0000000, 0.0000000, 0.0001253, 0.2714010, 0.0102550, 0.0000000], - [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0012968, 0.2658020, 0.0168090], - [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0085458, 0.2730800]]]) - scatter_matrix = np.rollaxis(scatter_matrix, 0, 3) - uo2_xsdata.set_scatter_matrix(scatter_matrix) - uo2_xsdata.set_fission([7.21206e-03, 8.19301e-04, 6.45320e-03, - 1.85648e-02, 1.78084e-02, 8.30348e-02, - 2.16004e-01]) - uo2_xsdata.set_nu_fission([2.005998e-02, 2.027303e-03, 1.570599e-02, - 4.518301e-02, 4.334208e-02, 2.020901e-01, - 5.257105e-01]) - uo2_xsdata.set_chi([5.8791e-01, 4.1176e-01, 3.3906e-04, 1.1761e-07, 0.0000e+00, - 0.0000e+00, 0.0000e+00]) - - h2o_xsdata = openmc.XSdata('LWTR', groups) - h2o_xsdata.order = 0 - h2o_xsdata.set_total([0.15920605, 0.412969593, 0.59030986, 0.58435, - 0.718, 1.2544497, 2.650379]) - h2o_xsdata.set_absorption([6.0105e-04, 1.5793e-05, 3.3716e-04, - 1.9406e-03, 5.7416e-03, 1.5001e-02, - 3.7239e-02]) - scatter_matrix = np.array( - [[[0.0444777, 0.1134000, 0.0007235, 0.0000037, 0.0000001, 0.0000000, 0.0000000], - [0.0000000, 0.2823340, 0.1299400, 0.0006234, 0.0000480, 0.0000074, 0.0000010], - [0.0000000, 0.0000000, 0.3452560, 0.2245700, 0.0169990, 0.0026443, 0.0005034], - [0.0000000, 0.0000000, 0.0000000, 0.0910284, 0.4155100, 0.0637320, 0.0121390], - [0.0000000, 0.0000000, 0.0000000, 0.0000714, 0.1391380, 0.5118200, 0.0612290], - [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0022157, 0.6999130, 0.5373200], - [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.1324400, 2.4807000]]]) - scatter_matrix = np.rollaxis(scatter_matrix, 0, 3) - h2o_xsdata.set_scatter_matrix(scatter_matrix) - - mg_cross_sections = openmc.MGXSLibrary(groups) - mg_cross_sections.add_xsdatas([uo2_xsdata, h2o_xsdata]) - mg_cross_sections.export_to_hdf5() - - ############################################################################### - # Create materials for the problem - - # Instantiate some Materials and register the appropriate Macroscopic objects - uo2 = openmc.Material(name='UO2 fuel') - uo2.set_density('macro', 1.0) - uo2.add_macroscopic('UO2') - - water = openmc.Material(name='Water') - water.set_density('macro', 1.0) - water.add_macroscopic('LWTR') - - # Instantiate a Materials collection and export to XML - materials = openmc.Materials([uo2, water]) - materials.cross_sections = "mgxs.h5" - - ############################################################################### - # Define problem geometry - - ######################################## - # Define an unbounded pincell universe - - pitch = 1.26 - - # Create a surface for the fuel outer radius - fuel_or = openmc.ZCylinder(r=0.54, name='Fuel OR') - inner_ring_a = openmc.ZCylinder(r=0.33, name='inner ring a') - inner_ring_b = openmc.ZCylinder(r=0.45, name='inner ring b') - outer_ring_a = openmc.ZCylinder(r=0.60, name='outer ring a') - outer_ring_b = openmc.ZCylinder(r=0.69, name='outer ring b') - - # Instantiate Cells - fuel_a = openmc.Cell(fill=uo2, region=-inner_ring_a, name='fuel inner a') - fuel_b = openmc.Cell(fill=uo2, region=+inner_ring_a & -inner_ring_b, name='fuel inner b') - fuel_c = openmc.Cell(fill=uo2, region=+inner_ring_b & -fuel_or, name='fuel inner c') - moderator_a = openmc.Cell(fill=water, region=+fuel_or & -outer_ring_a, name='moderator inner a') - moderator_b = openmc.Cell(fill=water, region=+outer_ring_a & -outer_ring_b, name='moderator outer b') - moderator_c = openmc.Cell(fill=water, region=+outer_ring_b, name='moderator outer c') - - # Create pincell universe - pincell_base = openmc.Universe() - - # Register Cells with Universe - pincell_base.add_cells([fuel_a, fuel_b, fuel_c, moderator_a, moderator_b, moderator_c]) - - # Create planes for azimuthal sectors - azimuthal_planes = [] - for i in range(8): - angle = 2 * i * openmc.pi / 8 - normal_vector = (-openmc.sin(angle), openmc.cos(angle), 0) - azimuthal_planes.append(openmc.Plane(a=normal_vector[0], b=normal_vector[1], c=normal_vector[2], d=0)) - - # Create a cell for each azimuthal sector - azimuthal_cells = [] - for i in range(8): - azimuthal_cell = openmc.Cell(name=f'azimuthal_cell_{i}') - azimuthal_cell.fill = pincell_base - azimuthal_cell.region = +azimuthal_planes[i] & -azimuthal_planes[(i+1) % 8] - azimuthal_cells.append(azimuthal_cell) - - # Create a geometry with the azimuthal universes - pincell = openmc.Universe(cells=azimuthal_cells) - - ######################################## - # Define a moderator lattice universe - - moderator_infinite = openmc.Cell(fill=water, name='moderator infinite') - mu = openmc.Universe() - mu.add_cells([moderator_infinite]) - - lattice = openmc.RectLattice() - lattice.lower_left = [-pitch/2.0, -pitch/2.0] - lattice.pitch = [pitch/10.0, pitch/10.0] - lattice.universes = np.full((10, 10), mu) - - mod_lattice_cell = openmc.Cell(fill=lattice) - - mod_lattice_uni = openmc.Universe() - - mod_lattice_uni.add_cells([mod_lattice_cell]) - - ######################################## - # Define 2x2 outer lattice - lattice2x2 = openmc.RectLattice() - lattice2x2.lower_left = [-pitch, -pitch] - lattice2x2.pitch = [pitch, pitch] - lattice2x2.universes = [ - [pincell, pincell], - [pincell, mod_lattice_uni] - ] - - ######################################## - # Define cell containing lattice and other stuff - box = openmc.model.RectangularPrism(pitch*2, pitch*2, boundary_type='vacuum') - - assembly = openmc.Cell(fill=lattice2x2, region=-box, name='assembly') - - root = openmc.Universe(name='root universe') - root.add_cell(assembly) - - # Create a geometry with the two cells and export to XML - geometry = openmc.Geometry(root) - - ############################################################################### - # Define problem settings - - # Instantiate a Settings object, set all runtime parameters, and export to XML - settings = openmc.Settings() - settings.energy_mode = "multi-group" - settings.batches = 10 - settings.inactive = 5 - settings.particles = 100 - - # Create an initial uniform spatial source distribution over fissionable zones - lower_left = (-pitch, -pitch, -1) - upper_right = (pitch, pitch, 1) - uniform_dist = openmc.stats.Box(lower_left, upper_right) - rr_source = openmc.IndependentSource(space=uniform_dist) - - settings.random_ray['distance_active'] = 100.0 - settings.random_ray['distance_inactive'] = 20.0 - settings.random_ray['ray_source'] = rr_source - settings.random_ray['volume_normalized_flux_tallies'] = True - - ############################################################################### - # Define tallies - - # Create a mesh that will be used for tallying - mesh = openmc.RegularMesh() - mesh.dimension = (2, 2) - mesh.lower_left = (-pitch, -pitch) - mesh.upper_right = (pitch, pitch) - - # Create a mesh filter that can be used in a tally - mesh_filter = openmc.MeshFilter(mesh) - - # Create an energy group filter as well - energy_filter = openmc.EnergyFilter(group_edges) - - # Now use the mesh filter in a tally and indicate what scores are desired - tally = openmc.Tally(name="Mesh tally") - tally.filters = [mesh_filter, energy_filter] - tally.scores = ['flux', 'fission', 'nu-fission'] - tally.estimator = 'analog' - - # Instantiate a Tallies collection and export to XML - tallies = openmc.Tallies([tally]) - - ############################################################################### - # Exporting to OpenMC model - ############################################################################### - - model = openmc.Model() - model.geometry = geometry - model.materials = materials - model.settings = settings - model.tallies = tallies - return model - - -def test_random_ray_vacuum(): - harness = MGXSTestHarness('statepoint.10.h5', random_ray_model()) - harness.main() From b11f8b73ca3a53913d75846562c4a4457076a1ef Mon Sep 17 00:00:00 2001 From: John Vincent Cauilan <64677361+johvincau@users.noreply.github.com> Date: Mon, 8 Jul 2024 09:37:17 -0500 Subject: [PATCH 143/671] Enforce sequence type when setting Setting.track (#3071) --- openmc/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openmc/settings.py b/openmc/settings.py index b048c3a8b..9731f54ec 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -1,4 +1,4 @@ -from collections.abc import Iterable, Mapping, MutableSequence +from collections.abc import Iterable, Mapping, MutableSequence, Sequence from enum import Enum import itertools from math import ceil @@ -819,7 +819,7 @@ class Settings: @track.setter def track(self, track: typing.Iterable[typing.Iterable[int]]): - cv.check_type('track', track, Iterable) + cv.check_type('track', track, Sequence) for t in track: if len(t) != 3: msg = f'Unable to set the track to "{t}" since its length is not 3' From e74dc5037ebfc575a72ed963c2ab148fc374b5ee Mon Sep 17 00:00:00 2001 From: John Vincent Cauilan <64677361+johvincau@users.noreply.github.com> Date: Mon, 8 Jul 2024 16:29:49 -0500 Subject: [PATCH 144/671] Enforce non-negative percents for material.add_nuclide to prevent unintended ao/wo flipping (#3075) --- openmc/material.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openmc/material.py b/openmc/material.py index 6edc37216..4b871b77d 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -519,6 +519,7 @@ class Material(IDManagerMixin): cv.check_type('nuclide', nuclide, str) cv.check_type('percent', percent, Real) cv.check_value('percent type', percent_type, {'ao', 'wo'}) + cv.check_greater_than('percent', percent, 0, equality=True) if self._macroscopic is not None: msg = 'Unable to add a Nuclide to Material ID="{}" as a ' \ @@ -727,6 +728,7 @@ class Material(IDManagerMixin): cv.check_type('nuclide', element, str) cv.check_type('percent', percent, Real) + cv.check_greater_than('percent', percent, 0, equality=True) cv.check_value('percent type', percent_type, {'ao', 'wo'}) # Make sure element name is just that From a5a26bb749d08618ea88e160af955890e799afe0 Mon Sep 17 00:00:00 2001 From: Nicolas Linden <57541749+nplinden@users.noreply.github.com> Date: Tue, 9 Jul 2024 19:30:09 +0200 Subject: [PATCH 145/671] Add -DCMAKE_BUILD_TYPE=Release flag for MOAB in Dockerfile (#3077) Co-authored-by: Nicolas Linden --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 107df4afe..35b9cf578 100644 --- a/Dockerfile +++ b/Dockerfile @@ -111,7 +111,8 @@ RUN if [ "$build_dagmc" = "on" ]; then \ mkdir -p $HOME/MOAB && cd $HOME/MOAB \ && git clone --single-branch -b ${MOAB_TAG} --depth 1 ${MOAB_REPO} \ && mkdir build && cd build \ - && cmake ../moab -DENABLE_HDF5=ON \ + && cmake ../moab -DCMAKE_BUILD_TYPE=Release \ + -DENABLE_HDF5=ON \ -DENABLE_NETCDF=ON \ -DBUILD_SHARED_LIBS=OFF \ -DENABLE_FORTRAN=OFF \ From cbec1ab035a4b4aaca8a102889de59a864b16813 Mon Sep 17 00:00:00 2001 From: John Vincent Cauilan <64677361+johvincau@users.noreply.github.com> Date: Wed, 10 Jul 2024 17:04:09 -0500 Subject: [PATCH 146/671] Correct openmc.Geometry initializer to accept iterables of openmc.Cell (#3081) --- openmc/geometry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmc/geometry.py b/openmc/geometry.py index db927c46e..e37a69c73 100644 --- a/openmc/geometry.py +++ b/openmc/geometry.py @@ -41,7 +41,7 @@ class Geometry: def __init__( self, - root: typing.Optional[openmc.UniverseBase] = None, + root: openmc.UniverseBase | typing.Iterable[openmc.Cell] | None = None, merge_surfaces: bool = False, surface_precision: int = 10 ): From 2107af5e2fb279d4dc8842e7c6055e72ecd3da86 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Fri, 12 Jul 2024 20:17:25 +0200 Subject: [PATCH 147/671] Moving most of setup.py to pyproject.toml (#3074) Co-authored-by: Jon Shimwell Co-authored-by: Paul Romano --- .readthedocs.yaml | 7 ++-- docs/requirements-rtd.txt | 13 ------- openmc/__init__.py | 3 +- pyproject.toml | 72 +++++++++++++++++++++++++++++++++++++++ setup.py | 67 +----------------------------------- 5 files changed, 80 insertions(+), 82 deletions(-) delete mode 100644 docs/requirements-rtd.txt diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 98ca8b581..b7b69f9fe 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -3,11 +3,14 @@ version: 2 build: os: "ubuntu-20.04" tools: - python: "3.9" + python: "3.10" sphinx: configuration: docs/source/conf.py python: install: - - requirements: docs/requirements-rtd.txt + - method: pip + path: . + extra_requirements: + - docs diff --git a/docs/requirements-rtd.txt b/docs/requirements-rtd.txt deleted file mode 100644 index df0333517..000000000 --- a/docs/requirements-rtd.txt +++ /dev/null @@ -1,13 +0,0 @@ -sphinx==5.0.2 -sphinx_rtd_theme==1.0.0 -sphinx-numfig -jupyter -sphinxcontrib-katex -sphinxcontrib-svg2pdfconverter -numpy -scipy -h5py -pandas -uncertainties -matplotlib -lxml diff --git a/openmc/__init__.py b/openmc/__init__.py index e589d35ef..566d28706 100644 --- a/openmc/__init__.py +++ b/openmc/__init__.py @@ -1,3 +1,4 @@ +import importlib.metadata from openmc.arithmetic import * from openmc.bounding_box import * from openmc.cell import * @@ -40,4 +41,4 @@ from openmc.model import Model from . import examples -__version__ = '0.15.1-dev' +__version__ = importlib.metadata.version("openmc") diff --git a/pyproject.toml b/pyproject.toml index d5970617a..ca646323b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,74 @@ [build-system] requires = ["setuptools", "wheel", "numpy", "cython"] + +[project] +name = "openmc" +authors = [ + {name = "The OpenMC Development Team", email = "openmc@anl.gov"}, +] +description = "OpenMC" +version = "0.15.1-dev" +requires-python = ">=3.10" +license = {file = "LICENSE"} +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: End Users/Desktop", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Topic :: Scientific/Engineering", + "Programming Language :: C++", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dependencies = [ + "numpy", + "h5py", + "scipy", + "ipython", + "matplotlib", + "pandas", + "lxml", + "uncertainties", + "setuptools", +] + +[project.optional-dependencies] +depletion-mpi = ["mpi4py"] +docs = [ + "sphinx==5.0.2", + "sphinxcontrib-katex", + "sphinx-numfig", + "jupyter", + "sphinxcontrib-svg2pdfconverter", + "sphinx-rtd-theme==1.0.0" +] +test = ["pytest", "pytest-cov", "colorama", "openpyxl"] +vtk = ["vtk"] + +[project.urls] +Homepage = "https://openmc.org" +Documentation = "https://docs.openmc.org" +Repository = "https://github.com/openmc-dev/openmc" +Issues = "https://github.com/openmc-dev/openmc/issues" + +[tool.setuptools.packages.find] +include = ['openmc*', 'scripts*'] +exclude = ['tests*'] + +[tool.setuptools.package-data] +"openmc.data.effective_dose" = ["*.txt"] +"openmc.data" = ["*.txt", "*.DAT", "*.json", "*.h5"] +"openmc.lib" = ["libopenmc.dylib", "libopenmc.so"] + +[project.scripts] +openmc-ace-to-hdf5 = "scripts.openmc_ace_to_hdf5:main" +openmc-plot-mesh-tally = "scripts.openmc_plot_mesh_tally:main" +openmc-track-combine = "scripts.openmc_track_combine:main" +openmc-track-to-vtk = "scripts.openmc_track_to_vtk:main" +openmc-update-inputs = "scripts.openmc_update_inputs:main" +openmc-update-mgxs = "scripts.openmc_update_mgxs:main" +openmc-voxel-to-vtk = "scripts.openmc_voxel_to_vtk:main" diff --git a/setup.py b/setup.py index de87e1490..4e24f48a1 100755 --- a/setup.py +++ b/setup.py @@ -1,76 +1,11 @@ #!/usr/bin/env python -import glob -import sys import numpy as np - -from setuptools import setup, find_packages +from setuptools import setup from Cython.Build import cythonize -# Determine shared library suffix -if sys.platform == 'darwin': - suffix = 'dylib' -else: - suffix = 'so' - -# Get version information from __init__.py. This is ugly, but more reliable than -# using an import. -with open('openmc/__init__.py', 'r') as f: - version = f.readlines()[-1].split()[-1].strip("'") - kwargs = { - 'name': 'openmc', - 'version': version, - 'packages': find_packages(exclude=['tests*']), - 'scripts': glob.glob('scripts/openmc-*'), - - # Data files and libraries - 'package_data': { - 'openmc.lib': ['libopenmc.{}'.format(suffix)], - 'openmc.data': ['mass_1.mas20.txt', 'BREMX.DAT', 'half_life.json', '*.h5'], - 'openmc.data.effective_dose': ['*.txt'] - }, - - # Metadata - 'author': 'The OpenMC Development Team', - 'author_email': 'openmc@anl.gov', - 'description': 'OpenMC', - 'url': 'https://openmc.org', - 'download_url': 'https://github.com/openmc-dev/openmc/releases', - 'project_urls': { - 'Issue Tracker': 'https://github.com/openmc-dev/openmc/issues', - 'Documentation': 'https://docs.openmc.org', - 'Source Code': 'https://github.com/openmc-dev/openmc', - }, - 'classifiers': [ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'Intended Audience :: End Users/Desktop', - 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: MIT License', - 'Natural Language :: English', - 'Topic :: Scientific/Engineering' - 'Programming Language :: C++', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - ], - - # Dependencies - 'python_requires': '>=3.10', - 'install_requires': [ - 'numpy>=1.9', 'h5py', 'scipy', 'ipython', 'matplotlib', - 'pandas', 'lxml', 'uncertainties', 'setuptools' - ], - 'extras_require': { - 'depletion-mpi': ['mpi4py'], - 'docs': ['sphinx', 'sphinxcontrib-katex', 'sphinx-numfig', 'jupyter', - 'sphinxcontrib-svg2pdfconverter', 'sphinx-rtd-theme'], - 'test': ['pytest', 'pytest-cov', 'colorama', 'openpyxl'], - 'vtk': ['vtk'], - }, # Cython is used to add resonance reconstruction and fast float_endf 'ext_modules': cythonize('openmc/data/*.pyx'), 'include_dirs': [np.get_include()] From 58f9092a68ec758240252c226720014c252b3039 Mon Sep 17 00:00:00 2001 From: John Vincent Cauilan <64677361+johvincau@users.noreply.github.com> Date: Tue, 16 Jul 2024 16:59:16 -0500 Subject: [PATCH 148/671] Include batch statistics discussion in methodology introduction (#3076) Co-authored-by: Paul Romano --- docs/source/methods/tallies.rst | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/source/methods/tallies.rst b/docs/source/methods/tallies.rst index d833c157d..4b56d3955 100644 --- a/docs/source/methods/tallies.rst +++ b/docs/source/methods/tallies.rst @@ -4,9 +4,9 @@ Tallies ======= -Note that the methods discussed in this section are written specifically for -continuous-energy mode but equivalent apply to the multi-group mode if the -particle's energy is replaced with the particle's group +The methods discussed in this section are written specifically for continuous- +energy mode. However, they can also apply to the multi-group mode if the +particle's energy is instead interpreted as the particle's group. ------------------ Filters and Scores @@ -207,6 +207,8 @@ the change-in-angle), we must use an analog estimator. .. TODO: Add description of surface current tallies +.. _tallies_statistics: + ---------- Statistics ---------- @@ -268,6 +270,14 @@ normal, log-normal, Weibull, etc. The central limit theorem states that as Estimating Statistics of a Random Variable ------------------------------------------ +After running OpenMC, each tallied quantity has a reported mean and standard +deviation. The below sections explain how these quantities are computed. Note +that OpenMC uses **batch statistics**, meaning that each observation for a tally +random variable corresponds to the aggregation of tally contributions from +multiple source particles that are grouped together into a single batch. See +:ref:`usersguide_particles` for more information on how the number of source +particles and statistical batches are specified. + Mean ++++ From fd47df4bb23f011f2d3ab2b59597efa21d0258f6 Mon Sep 17 00:00:00 2001 From: Rufus <123552806+RufusN@users.noreply.github.com> Date: Wed, 17 Jul 2024 01:53:40 +0100 Subject: [PATCH 149/671] Linear Source Random Ray (#3072) Co-authored-by: John Tramm Co-authored-by: Paul Romano --- CMakeLists.txt | 2 + docs/source/methods/random_ray.rst | 184 ++++++++++- docs/source/usersguide/random_ray.rst | 28 ++ include/openmc/constants.h | 2 + .../openmc/random_ray/flat_source_domain.h | 21 +- .../openmc/random_ray/linear_source_domain.h | 61 ++++ include/openmc/random_ray/moment_matrix.h | 90 ++++++ include/openmc/random_ray/random_ray.h | 13 +- .../openmc/random_ray/random_ray_simulation.h | 3 +- openmc/settings.py | 8 + src/random_ray/flat_source_domain.cpp | 28 +- src/random_ray/linear_source_domain.cpp | 269 ++++++++++++++++ src/random_ray/moment_matrix.cpp | 84 +++++ src/random_ray/random_ray.cpp | 291 +++++++++++++++++- src/random_ray/random_ray_simulation.cpp | 78 +++-- src/settings.cpp | 13 + .../__init__.py | 0 .../linear/inputs_true.dat | 245 +++++++++++++++ .../linear/results_true.dat | 9 + .../linear_xy/inputs_true.dat | 245 +++++++++++++++ .../linear_xy/results_true.dat | 9 + .../random_ray_fixed_source_linear/test.py | 27 ++ .../random_ray_linear/__init__.py | 0 .../random_ray_linear/linear/inputs_true.dat | 110 +++++++ .../random_ray_linear/linear/results_true.dat | 171 ++++++++++ .../linear_xy/inputs_true.dat | 110 +++++++ .../linear_xy/results_true.dat | 171 ++++++++++ .../random_ray_linear/test.py | 26 ++ 28 files changed, 2235 insertions(+), 63 deletions(-) create mode 100644 include/openmc/random_ray/linear_source_domain.h create mode 100644 include/openmc/random_ray/moment_matrix.h create mode 100644 src/random_ray/linear_source_domain.cpp create mode 100644 src/random_ray/moment_matrix.cpp create mode 100644 tests/regression_tests/random_ray_fixed_source_linear/__init__.py create mode 100644 tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_linear/linear/results_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_linear/linear_xy/results_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_linear/test.py create mode 100644 tests/regression_tests/random_ray_linear/__init__.py create mode 100644 tests/regression_tests/random_ray_linear/linear/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_linear/linear/results_true.dat create mode 100644 tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_linear/linear_xy/results_true.dat create mode 100644 tests/regression_tests/random_ray_linear/test.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 3cbeaa024..0f4cc1b52 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -380,6 +380,8 @@ list(APPEND libopenmc_SOURCES src/random_ray/random_ray_simulation.cpp src/random_ray/random_ray.cpp src/random_ray/flat_source_domain.cpp + src/random_ray/linear_source_domain.cpp + src/random_ray/moment_matrix.cpp src/reaction.cpp src/reaction_product.cpp src/scattdata.cpp diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index 45af2b0c8..9f8eb84d8 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -218,9 +218,9 @@ Following the multigroup discretization, another assumption made is that a large and complex problem can be broken up into small constant cross section regions, and that these regions have group dependent, flat, isotropic sources (fission and scattering), :math:`Q_g`. Anisotropic as well as higher order sources are -also possible with MOC-based methods but are not used yet in OpenMC for -simplicity. With these key assumptions, the multigroup MOC form of the neutron -transport equation can be written as in Equation :eq:`moc_final`. +also possible with MOC-based methods. With these key assumptions, the multigroup +MOC form of the neutron transport equation can be written as in Equation +:eq:`moc_final`. .. math:: :label: moc_final @@ -287,7 +287,7 @@ final expression for the average angular flux for a ray crossing a region as: .. math:: :label: average_psi_final - \overline{\psi}_{r,i,g} = \frac{Q_{i,g}}{\Sigma_{t,i,g}} + \frac{\Delta \psi_{r,g}}{\ell_r \Sigma_{t,i,g}} + \overline{\psi}_{r,i,g} = \frac{Q_{i,g}}{\Sigma_{t,i,g}} + \frac{\Delta \psi_{r,g}}{\ell_r \Sigma_{t,i,g}}. ~~~~~~~~~~~ Random Rays @@ -771,6 +771,170 @@ By default, the unnormalized flux values (units of cm) will be reported. If the user wishes to received volume normalized flux tallies, then an option for this is available, as described in the :ref:`User Guide`. +-------------- +Linear Sources +-------------- + +Instead of making a flat source approximation, as in the previous section, a +Linear Source (LS) approximation can be used. Different LS approximations have +been developed; the OpenMC implementation follows the MOC LS scheme described by +`Ferrer `_. The LS source along a characteristic is given by: + +.. math:: + :label: linear_source + + Q_{i,g}(s) = \bar{Q}_{r,i,g} + \hat{Q}_{r,i,g}(s-\ell_{r}/2), + +where the source, :math:`Q_{i,g}(s)`, varies linearly along the track and +:math:`\bar{Q}_{r,i,g}` and :math:`\hat{Q}_{r,i,g}` are track specific source +terms to define shortly. Integrating the source, as done in Equation +:eq:`moc_final`, leads to + +.. math:: + :label: lsr_attenuation + + \psi^{out}_{r,g}=\psi^{in}_{r,g} + \left(\frac{\bar{Q}_{r, i, g}}{\Sigma_{\mathrm{t}, i, g}}-\psi^{in}_{r,g}\right) + F_{1}\left(\tau_{i,g}\right)+\frac{\hat{Q}_{r, i, g}^{g}}{2\left(\Sigma_{\mathrm{t}, i,g}\right)^{2}} F_{2}\left(\tau_{i,g}\right), + +where for simplicity the term :math:`\tau_{i,g}` and the expoentials :math:`F_1` +and :math:`F_2` are introduced, given by: + +.. math:: + :label: tau + + \tau_{i,g} = \Sigma_{\mathrm{t,i,g}} \ell_{r} + +.. math:: + :label: f1 + + F_1(\tau) = 1 - e^{-\tau}, + +and + +.. math:: + :label: f2 + + F_{2}\left(\tau\right) = 2\left[\tau-F_{1}\left(\tau\right)\right]-\tau F_{1}\left(\tau\right). + + +To solve for the track specific source terms in Equation :eq:`linear_source` we +first define a local reference frame. If we now refer to :math:`\mathbf{r}` as +the global coordinate and introduce the source region specific coordinate +:math:`\mathbf{u}` such that, + +.. math:: + :label: local_coord + + \mathbf{u}_{r} = \mathbf{r}-\mathbf{r}_{\mathrm{c}}, + +where :math:`\mathbf{r}_{\mathrm{c}}` is the centroid of the source region of +interest. In turn :math:`\mathbf{u}_{r,\mathrm{c}}` and :math:`\mathbf{u}_{r,0}` +are the local centroid and entry positions of a ray. The computation of the +local and global centroids are described further by `Gunow `_. + +Using the local position, the source in a source region is given by: + +.. math:: + :label: region_source + + \tilde{Q}(\boldsymbol{x}) ={Q}_{i,g}+ \boldsymbol{\vec{Q}}_{i,g} \cdot \mathbf{u}_{r}\;\mathrm{,} + +This definition allows us to solve for our characteric source terms resulting in: + +.. math:: + :label: source_term_1 + + \bar{Q}_{r, i, g} = Q_{i,g} + \left[\mathbf{u}_{r,\mathrm{c}} \cdot \boldsymbol{\vec{Q}}_{i,g}\right], + +.. math:: + :label: source_term_2 + + \hat{Q}_{r, i, g} = \left[\boldsymbol{\Omega} \cdot \boldsymbol{\vec{Q}}_{i,g}\right]\;\mathrm{,} + +:math:`\boldsymbol{\Omega}` being the direction vector of the ray. The next step +is to solve for the LS source vector :math:`\boldsymbol{\vec{Q}}_{i,g}`. A +relationship between the LS source vector and the source moments, +:math:`\boldsymbol{\vec{q}}_{i,g}` can be derived, as in `Ferrer +`_ and `Gunow `_: + +.. math:: + :label: m_equation + + \mathbf{M}_{i} \boldsymbol{\vec{Q}}_{i,g} = \boldsymbol{\vec{q}}_{i,g} \;\mathrm{.} + +The spatial moments matrix :math:`M_i` in region :math:`i` represents the +spatial distribution of the 3D object composing the `source region +`_. This matrix is independent of the material of the source +region, fluxes, and any transport effects -- it is a purely geometric quantity. +It is a symmetric :math:`3\times3` matrix. While :math:`M_i` is not known +apriori to the simulation, similar to the source region volume, it can be +computed "on-the-fly" as a byproduct of the random ray integration process. Each +time a ray randomly crosses the region within its active length, an estimate of +the spatial moments matrix can be computed by using the midpoint of the ray as +an estimate of the centroid, and the distance and direction of the ray can be +used to inform the other spatial moments within the matrix. As this information +is purely geometric, the stochastic estimate of the centroid and spatial moments +matrix can be accumulated and improved over the entire duration of the +simulation, converging towards their true quantities. + +With an estimate of the spatial moments matrix :math:`M_i` resulting from the +ray tracing process naturally, the LS source vector +:math:`\boldsymbol{\vec{Q}}_{i,g}` can be obtained via a linear solve of +:eq:`m_equation`, or by the direct inversion of :math:`M_i`. However, to +accomplish this, we must first know the source moments +:math:`\boldsymbol{\vec{q}}_{i,g}`. Fortunately, the source moments are also +defined by the definition of the source: + +.. math:: + :label: source_moments + + q_{v, i, g}= \frac{\chi_{i,g}}{k_{eff}} \sum_{g^{\prime}=1}^{G} \nu + \Sigma_{\mathrm{f},i, g^{\prime}} \hat{\phi}_{v, i, g^{\prime}} + \sum_{g^{\prime}=1}^{G} + \Sigma_{\mathrm{s}, i, g^{\prime}\rightarrow g} \hat{\phi}_{v, i, g^{\prime}}\quad \forall v \in(x, y, z)\;\mathrm{,} + +where :math:`v` indicates the direction vector component, and we have introduced +the scalar flux moments :math:`\hat{\phi}`. The scalar flux moments can be +solved for by taking the `integral definition `_ of a spatial +moment, allowing us to derive a "simulation averaged" estimator for the scalar +moment, as in Equation :eq:`phi_sim`, + +.. math:: + :label: scalar_moments_sim + + \hat{\phi}_{v,i,g}^{simulation} = \frac{\sum\limits_{r=1}^{N_i} + \ell_{r} \left[\Omega_{v} \hat{\psi}_{r,i,g} + u_{r,v,0} \bar{\psi}_{r,i,g}\right]} + {\Sigma_{t,i,g} \frac{\sum\limits^{B}_{b}\sum\limits^{N_i}_{r} \ell_{b,r} }{B}} + \quad \forall v \in(x, y, z)\;\mathrm{,} + + +where the average angular flux is given by Equation :eq:`average_psi_final`, and +the angular flux spatial moments :math:`\hat{\psi}_{r,i,g}` by: + +.. math:: + :label: angular_moments + + \hat{\psi}_{r, i, g} = \frac{\ell_{r}\psi^{in}_{r,g}}{2} + + \left(\frac{\bar{Q}_{r,i, g}}{\Sigma_{\mathrm{t}, i, g}}-\psi^{in}_{r,g}\right) + \frac{G_{1}\left(\tau_{i,g}\right)}{\Sigma_{\mathrm{t}, i, g}} + \frac{\ell_{r}\hat{Q}_{r,i,g}} + {2\left(\Sigma_{\mathrm{t}, i, g}\right)^{2}}G_{2}\left(\tau_{i,g}\right)\;\mathrm{.} + + +The new exponentials introduced, again for simplicity, are simply: + +.. math:: + :label: G1 + + G_{1}(\tau) = 1+\frac{\tau}{2}-\left(1+\frac{1}{\tau}\right) F_{1}(\tau), + +.. math:: + :label: G2 + + G_{2}(\tau) = \frac{2}{3} \tau-\left(1+\frac{2}{\tau}\right) G_{1}(\tau) + +The contents of this section, alongside the equations for the flat source and +scalar flux, Equations :eq:`source_update` and :eq:`phi_sim` respectively, +completes the set of equations for LS. + .. _methods-shannon-entropy-random-ray: ----------------------------- @@ -789,7 +953,7 @@ sources is adjusted such that: :label: fraction-source-random-ray S_i = \frac{\text{Fission source in FSR $i \times$ Volume of FSR - $i$}}{\text{Total fission source}} = \frac{Q_{i} V_{i}}{\sum_{i=1}^{i=N} + $i$}}{\text{Total fission source}} = \frac{Q_{i} V_{i}}{\sum_{i=1}^{i=N} Q_{i} V_{i}} The Shannon entropy is then computed normally as @@ -852,13 +1016,13 @@ in random ray particle transport are: areas typically have solutions that are highly effective at mitigating bias, error stemming from multigroup energy discretization is much harder to remedy. - - **Flat Source Approximation:**. In OpenMC, a "flat" (0th order) source - approximation is made, wherein the scattering and fission sources within a + - **Source Approximation:**. In OpenMC, a "flat" (0th order) source + approximation is often made, wherein the scattering and fission sources within a cell are assumed to be spatially uniform. As the source in reality is a continuous function, this leads to bias, although the bias can be reduced to acceptable levels if the flat source regions are sufficiently small. - The bias can also be mitigated by assuming a higher-order source (e.g., - linear or quadratic), although OpenMC does not yet have this capability. + The bias can also be mitigated by assuming a higher-order source such as the + linear source approximation currently implemented into OpenMC. In practical terms, this source of bias can become very large if cells are large (with dimensions beyond that of a typical particle mean free path), but the subdivision of cells can often reduce this bias to trivial levels. @@ -882,6 +1046,8 @@ in random ray particle transport are: .. _Tramm-2018: https://dspace.mit.edu/handle/1721.1/119038 .. _Tramm-2020: https://doi.org/10.1051/EPJCONF/202124703021 .. _Cosgrove-2023: https://doi.org/10.1080/00295639.2023.2270618 +.. _Ferrer-2016: https://doi.org/10.13182/NSE15-6 +.. _Gunow-2018: https://dspace.mit.edu/handle/1721.1/119030 .. only:: html diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index 6df132859..117d5e23f 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -447,6 +447,34 @@ in the `OpenMC Jupyter notebook collection separate materials can be defined each with a separate multigroup dataset corresponding to a given temperature. +-------------- +Linear Sources +-------------- + +Linear Sources (LS), are supported with the eigenvalue and fixed source random +ray solvers. General 3D LS can be toggled by setting the ``source_shape`` field +in the :attr:`openmc.Settings.random_ray` dictionary to ``'linear'`` as:: + + settings.random_ray['source_shape'] = 'linear' + +LS enables the use of coarser mesh discretizations and lower ray populations, +offsetting the increased computation per ray. + +While OpenMC has no specific mode for 2D simulations, such simulations can be +performed implicitly by leaving one of the dimensions of the geometry unbounded +or by imposing reflective boundary conditions with no variation in between them +in that dimension. When 3D linear sources are used in a 2D random ray +simulation, the extremely long (or potentially infinite) spatial dimension along +one of the axes can cause the linear source to become noisy, leading to +potentially large increases in variance. To mitigate this, the user can force +the z-terms of the linear source to zero by setting the ``source_shape`` field +as:: + + settings.random_ray['source_shape'] = 'linear_xy' + +which will greatly improve the quality of the linear source term in 2D +simulations. + --------------------------------- Fixed Source and Eigenvalue Modes --------------------------------- diff --git a/include/openmc/constants.h b/include/openmc/constants.h index 1c683e5f5..e502506c9 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -342,6 +342,8 @@ enum class RunMode { enum class SolverType { MONTE_CARLO, RANDOM_RAY }; +enum class RandomRaySourceShape { FLAT, LINEAR, LINEAR_XY }; + //============================================================================== // Geometry Constants diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index 519384040..33c5661dc 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -89,25 +89,28 @@ struct TallyTask { class FlatSourceDomain { public: //---------------------------------------------------------------------------- - // Constructors + // Constructors and Destructors FlatSourceDomain(); + virtual ~FlatSourceDomain() = default; //---------------------------------------------------------------------------- // Methods - void update_neutron_source(double k_eff); + virtual void update_neutron_source(double k_eff); double compute_k_eff(double k_eff_old) const; - void normalize_scalar_flux_and_volumes( + virtual void normalize_scalar_flux_and_volumes( double total_active_distance_per_iteration); - int64_t add_source_to_scalar_flux(); - void batch_reset(); + virtual int64_t add_source_to_scalar_flux(); + virtual void batch_reset(); void convert_source_regions_to_tallies(); void reset_tally_volumes(); void random_ray_tally(); - void accumulate_iteration_flux(); + virtual void accumulate_iteration_flux(); void output_to_vtk() const; - void all_reduce_replicated_source_regions(); + virtual void all_reduce_replicated_source_regions(); void convert_external_sources(); void count_external_source_regions(); + virtual void flux_swap(); + virtual double evaluate_flux_at_point(Position r, int64_t sr, int g) const; double compute_fixed_source_normalization_factor() const; //---------------------------------------------------------------------------- @@ -131,6 +134,7 @@ public: vector lock_; vector was_hit_; vector volume_; + vector volume_t_; vector position_recorded_; vector position_; @@ -141,7 +145,7 @@ public: vector source_; vector external_source_; -private: +protected: //---------------------------------------------------------------------------- // Methods void apply_external_source_to_source_region( @@ -174,7 +178,6 @@ private: // 1D arrays representing values for all source regions vector material_; - vector volume_t_; // 2D arrays stored in 1D representing values for all source regions x energy // groups diff --git a/include/openmc/random_ray/linear_source_domain.h b/include/openmc/random_ray/linear_source_domain.h new file mode 100644 index 000000000..5010ffddd --- /dev/null +++ b/include/openmc/random_ray/linear_source_domain.h @@ -0,0 +1,61 @@ +#ifndef OPENMC_RANDOM_RAY_LINEAR_SOURCE_DOMAIN_H +#define OPENMC_RANDOM_RAY_LINEAR_SOURCE_DOMAIN_H + +#include "openmc/random_ray/flat_source_domain.h" +#include "openmc/random_ray/moment_matrix.h" + +#include "openmc/openmp_interface.h" +#include "openmc/position.h" +#include "openmc/source.h" + +namespace openmc { + +/* + * The LinearSourceDomain class encompasses data and methods for storing + * scalar flux and source region for all linear source regions in a + * random ray simulation domain. + */ + +class LinearSourceDomain : public FlatSourceDomain { +public: + //---------------------------------------------------------------------------- + // Constructors + LinearSourceDomain(); + + //---------------------------------------------------------------------------- + // Methods + void update_neutron_source(double k_eff) override; + double compute_k_eff(double k_eff_old) const; + void normalize_scalar_flux_and_volumes( + double total_active_distance_per_iteration) override; + int64_t add_source_to_scalar_flux() override; + void batch_reset() override; + void convert_source_regions_to_tallies(); + void reset_tally_volumes(); + void random_ray_tally(); + void accumulate_iteration_flux() override; + void output_to_vtk() const; + void all_reduce_replicated_source_regions() override; + void convert_external_sources(); + void count_external_source_regions(); + void flux_swap() override; + double evaluate_flux_at_point(Position r, int64_t sr, int g) const override; + + //---------------------------------------------------------------------------- + // Public Data members + + vector source_gradients_; + vector flux_moments_old_; + vector flux_moments_new_; + vector flux_moments_t_; + vector centroid_; + vector centroid_iteration_; + vector centroid_t_; + vector mom_matrix_; + vector mom_matrix_t_; + +}; // class LinearSourceDomain + +} // namespace openmc + +#endif // OPENMC_RANDOM_RAY_LINEAR_SOURCE_DOMAIN_H diff --git a/include/openmc/random_ray/moment_matrix.h b/include/openmc/random_ray/moment_matrix.h new file mode 100644 index 000000000..c95bb2c12 --- /dev/null +++ b/include/openmc/random_ray/moment_matrix.h @@ -0,0 +1,90 @@ +#ifndef OPENMC_MOMENT_MATRIX_H +#define OPENMC_MOMENT_MATRIX_H + +#include + +#include "openmc/position.h" + +namespace openmc { + +// The MomentArray class is a 3-element array representing the x, y, and z +// moments. It is defined as an alias for the Position class to allow for +// dot products and other operations with Position objects. +// TODO: This class could in theory have 32-bit instead of 64-bit FP values. +using MomentArray = Position; + +// The MomentMatrix class is a sparse representation a 3x3 symmetric +// matrix, with elements labeled as follows: +// +// | a b c | +// | b d e | +// | c e f | +// +// This class uses FP64 values as objects that are accumulated to over many +// iterations. +class MomentMatrix { +public: + //---------------------------------------------------------------------------- + // Public data members + double a; + double b; + double c; + double d; + double e; + double f; + + //---------------------------------------------------------------------------- + // Constructors + MomentMatrix() = default; + MomentMatrix(double a, double b, double c, double d, double e, double f) + : a {a}, b {b}, c {c}, d {d}, e {e}, f {f} + {} + + //---------------------------------------------------------------------------- + // Methods + MomentMatrix inverse() const; + double determinant() const; + void compute_spatial_moments_matrix( + const Position& r, const Direction& u, const double& distance); + + inline void set_to_zero() { a = b = c = d = e = f = 0; } + + inline MomentMatrix& operator*=(double x) + { + a *= x; + b *= x; + c *= x; + d *= x; + e *= x; + f *= x; + return *this; + } + + inline MomentMatrix operator*(double x) const + { + MomentMatrix m_copy = *this; + m_copy *= x; + return m_copy; + } + + inline MomentMatrix& operator+=(const MomentMatrix& rhs) + { + a += rhs.a; + b += rhs.b; + c += rhs.c; + d += rhs.d; + e += rhs.e; + f += rhs.f; + return *this; + } + + MomentArray operator*(const MomentArray& rhs) const + { + return {a * rhs.x + b * rhs.y + c * rhs.z, + b * rhs.x + d * rhs.y + e * rhs.z, c * rhs.x + e * rhs.y + f * rhs.z}; + } +}; + +} // namespace openmc + +#endif // OPENMC_MOMENT_MATRIX_H diff --git a/include/openmc/random_ray/random_ray.h b/include/openmc/random_ray/random_ray.h index 5ee64574e..913a9af4a 100644 --- a/include/openmc/random_ray/random_ray.h +++ b/include/openmc/random_ray/random_ray.h @@ -4,6 +4,7 @@ #include "openmc/memory.h" #include "openmc/particle.h" #include "openmc/random_ray/flat_source_domain.h" +#include "openmc/random_ray/moment_matrix.h" #include "openmc/source.h" namespace openmc { @@ -25,14 +26,18 @@ public: // Methods void event_advance_ray(); void attenuate_flux(double distance, bool is_active); + void attenuate_flux_flat_source(double distance, bool is_active); + void attenuate_flux_linear_source(double distance, bool is_active); + void initialize_ray(uint64_t ray_id, FlatSourceDomain* domain); uint64_t transport_history_based_single_ray(); //---------------------------------------------------------------------------- // Static data members - static double distance_inactive_; // Inactive (dead zone) ray length - static double distance_active_; // Active ray length - static unique_ptr ray_source_; // Starting source for ray sampling + static double distance_inactive_; // Inactive (dead zone) ray length + static double distance_active_; // Active ray length + static unique_ptr ray_source_; // Starting source for ray sampling + static RandomRaySourceShape source_shape_; // Flag for linear source //---------------------------------------------------------------------------- // Public data members @@ -42,6 +47,8 @@ private: //---------------------------------------------------------------------------- // Private data members vector delta_psi_; + vector delta_moments_; + int negroups_; FlatSourceDomain* domain_ {nullptr}; // pointer to domain that has flat source // data needed for ray transport diff --git a/include/openmc/random_ray/random_ray_simulation.h b/include/openmc/random_ray/random_ray_simulation.h index b439574bf..c1d47821d 100644 --- a/include/openmc/random_ray/random_ray_simulation.h +++ b/include/openmc/random_ray/random_ray_simulation.h @@ -2,6 +2,7 @@ #define OPENMC_RANDOM_RAY_SIMULATION_H #include "openmc/random_ray/flat_source_domain.h" +#include "openmc/random_ray/linear_source_domain.h" namespace openmc { @@ -31,7 +32,7 @@ public: // Data members private: // Contains all flat source region data - FlatSourceDomain domain_; + unique_ptr domain_; // Random ray eigenvalue double k_eff_ {1.0}; diff --git a/openmc/settings.py b/openmc/settings.py index 9731f54ec..807662581 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -157,6 +157,9 @@ class Settings: :ray_source: Starting ray distribution (must be uniform in space and angle) as specified by a :class:`openmc.SourceBase` object. + :source_shape: + Assumed shape of the source distribution within each source + region. Options are 'flat' (default), 'linear', or 'linear_xy'. :volume_normalized_flux_tallies: Whether to normalize flux tallies by volume (bool). The default is 'False'. When enabled, flux tallies will be reported in units of @@ -1090,6 +1093,9 @@ class Settings: random_ray[key], 0.0, True) elif key == 'ray_source': cv.check_type('random ray source', random_ray[key], SourceBase) + elif key == 'source_shape': + cv.check_value('source shape', random_ray[key], + ('flat', 'linear', 'linear_xy')) elif key == 'volume_normalized_flux_tallies': cv.check_type('volume normalized flux tallies', random_ray[key], bool) else: @@ -1885,6 +1891,8 @@ class Settings: elif child.tag == 'source': source = SourceBase.from_xml_element(child) self.random_ray['ray_source'] = source + elif child.tag == 'source_shape': + self.random_ray['source_shape'] = child.text elif child.tag == 'volume_normalized_flux_tallies': self.random_ray['volume_normalized_flux_tallies'] = ( child.text in ('true', '1') diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 29b72c77b..8b6cda93f 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -751,6 +751,13 @@ void FlatSourceDomain::all_reduce_replicated_source_regions() #endif } +double FlatSourceDomain::evaluate_flux_at_point( + Position r, int64_t sr, int g) const +{ + return scalar_flux_final_[sr * negroups_ + g] / + (settings::n_batches - settings::n_inactive); +} + // Outputs all basic material, FSR ID, multigroup flux, and // fission source data to .vtk file that can be directly // loaded and displayed by Paraview. Note that .vtk binary @@ -811,6 +818,7 @@ void FlatSourceDomain::output_to_vtk() const // Relate voxel spatial locations to random ray source regions vector voxel_indices(Nx * Ny * Nz); + vector voxel_positions(Nx * Ny * Nz); #pragma omp parallel for collapse(3) for (int z = 0; z < Nz; z++) { @@ -827,6 +835,7 @@ void FlatSourceDomain::output_to_vtk() const int64_t source_region_idx = source_region_offsets_[i_cell] + p.cell_instance(); voxel_indices[z * Ny * Nx + y * Nx + x] = source_region_idx; + voxel_positions[z * Ny * Nx + y * Nx + x] = sample; } } } @@ -851,11 +860,10 @@ void FlatSourceDomain::output_to_vtk() const for (int g = 0; g < negroups_; g++) { std::fprintf(plot, "SCALARS flux_group_%d float\n", g); std::fprintf(plot, "LOOKUP_TABLE default\n"); - for (int fsr : voxel_indices) { + for (int i = 0; i < Nx * Ny * Nz; i++) { + int64_t fsr = voxel_indices[i]; int64_t source_element = fsr * negroups_ + g; - float flux = - scalar_flux_final_[source_element] * source_normalization_factor; - flux /= (settings::n_batches - settings::n_inactive); + float flux = evaluate_flux_at_point(voxel_positions[i], fsr, g); flux = convert_to_big_endian(flux); std::fwrite(&flux, sizeof(float), 1, plot); } @@ -882,14 +890,14 @@ void FlatSourceDomain::output_to_vtk() const // Plot fission source std::fprintf(plot, "SCALARS total_fission_source float\n"); std::fprintf(plot, "LOOKUP_TABLE default\n"); - for (int fsr : voxel_indices) { + for (int i = 0; i < Nx * Ny * Nz; i++) { + int64_t fsr = voxel_indices[i]; + float total_fission = 0.0; int mat = material_[fsr]; for (int g = 0; g < negroups_; g++) { int64_t source_element = fsr * negroups_ + g; - float flux = - scalar_flux_final_[source_element] * source_normalization_factor; - flux /= (settings::n_batches - settings::n_inactive); + float flux = evaluate_flux_at_point(voxel_positions[i], fsr, g); float Sigma_f = data::mg.macro_xs_[mat].get_xs( MgxsType::FISSION, g, nullptr, nullptr, nullptr, 0, 0); total_fission += Sigma_f * flux; @@ -1027,5 +1035,9 @@ void FlatSourceDomain::convert_external_sources() } } } +void FlatSourceDomain::flux_swap() +{ + scalar_flux_old_.swap(scalar_flux_new_); +} } // namespace openmc diff --git a/src/random_ray/linear_source_domain.cpp b/src/random_ray/linear_source_domain.cpp new file mode 100644 index 000000000..1603ec24a --- /dev/null +++ b/src/random_ray/linear_source_domain.cpp @@ -0,0 +1,269 @@ +#include "openmc/random_ray/linear_source_domain.h" + +#include "openmc/cell.h" +#include "openmc/geometry.h" +#include "openmc/material.h" +#include "openmc/message_passing.h" +#include "openmc/mgxs_interface.h" +#include "openmc/output.h" +#include "openmc/plot.h" +#include "openmc/random_ray/random_ray.h" +#include "openmc/simulation.h" +#include "openmc/tallies/filter.h" +#include "openmc/tallies/tally.h" +#include "openmc/tallies/tally_scoring.h" +#include "openmc/timer.h" + +namespace openmc { + +//============================================================================== +// LinearSourceDomain implementation +//============================================================================== + +LinearSourceDomain::LinearSourceDomain() : FlatSourceDomain() +{ + // First order spatial moment of the scalar flux + flux_moments_old_.assign(n_source_elements_, {0.0, 0.0, 0.0}); + flux_moments_new_.assign(n_source_elements_, {0.0, 0.0, 0.0}); + flux_moments_t_.assign(n_source_elements_, {0.0, 0.0, 0.0}); + // Source gradients given by M inverse multiplied by source moments + source_gradients_.assign(n_source_elements_, {0.0, 0.0, 0.0}); + + centroid_.assign(n_source_regions_, {0.0, 0.0, 0.0}); + centroid_iteration_.assign(n_source_regions_, {0.0, 0.0, 0.0}); + centroid_t_.assign(n_source_regions_, {0.0, 0.0, 0.0}); + mom_matrix_.assign(n_source_regions_, {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}); + mom_matrix_t_.assign(n_source_regions_, {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}); +} + +void LinearSourceDomain::batch_reset() +{ + FlatSourceDomain::batch_reset(); +#pragma omp parallel for + for (int64_t se = 0; se < n_source_elements_; se++) { + flux_moments_new_[se] = {0.0, 0.0, 0.0}; + } +#pragma omp parallel for + for (int64_t sr = 0; sr < n_source_regions_; sr++) { + centroid_iteration_[sr] = {0.0, 0.0, 0.0}; + mom_matrix_[sr] = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; + } +} + +void LinearSourceDomain::update_neutron_source(double k_eff) +{ + simulation::time_update_src.start(); + + double inverse_k_eff = 1.0 / k_eff; + + // Temperature and angle indices, if using multiple temperature + // data sets and/or anisotropic data sets. + // TODO: Currently assumes we are only using single temp/single + // angle data. + const int t = 0; + const int a = 0; + +#pragma omp parallel for + for (int sr = 0; sr < n_source_regions_; sr++) { + + int material = material_[sr]; + MomentMatrix invM = mom_matrix_[sr].inverse(); + + for (int e_out = 0; e_out < negroups_; e_out++) { + float sigma_t = data::mg.macro_xs_[material].get_xs( + MgxsType::TOTAL, e_out, nullptr, nullptr, nullptr, t, a); + + float scatter_flat = 0.0f; + float fission_flat = 0.0f; + MomentArray scatter_linear = {0.0, 0.0, 0.0}; + MomentArray fission_linear = {0.0, 0.0, 0.0}; + + for (int e_in = 0; e_in < negroups_; e_in++) { + // Handles for the flat and linear components of the flux + float flux_flat = scalar_flux_old_[sr * negroups_ + e_in]; + MomentArray flux_linear = flux_moments_old_[sr * negroups_ + e_in]; + + // Handles for cross sections + float sigma_s = data::mg.macro_xs_[material].get_xs( + MgxsType::NU_SCATTER, e_in, &e_out, nullptr, nullptr, t, a); + float nu_sigma_f = data::mg.macro_xs_[material].get_xs( + MgxsType::NU_FISSION, e_in, nullptr, nullptr, nullptr, t, a); + float chi = data::mg.macro_xs_[material].get_xs( + MgxsType::CHI_PROMPT, e_in, &e_out, nullptr, nullptr, t, a); + + // Compute source terms for flat and linear components of the flux + scatter_flat += sigma_s * flux_flat; + fission_flat += nu_sigma_f * flux_flat * chi; + scatter_linear += sigma_s * flux_linear; + fission_linear += nu_sigma_f * flux_linear * chi; + } + + // Compute the flat source term + source_[sr * negroups_ + e_out] = + (scatter_flat + fission_flat * inverse_k_eff) / sigma_t; + + // Compute the linear source terms + if (simulation::current_batch > 2) { + source_gradients_[sr * negroups_ + e_out] = + invM * ((scatter_linear + fission_linear * inverse_k_eff) / sigma_t); + } + } + } + + if (settings::run_mode == RunMode::FIXED_SOURCE) { +// Add external source to flat source term if in fixed source mode +#pragma omp parallel for + for (int se = 0; se < n_source_elements_; se++) { + source_[se] += external_source_[se]; + } + } + + simulation::time_update_src.stop(); +} + +void LinearSourceDomain::normalize_scalar_flux_and_volumes( + double total_active_distance_per_iteration) +{ + float normalization_factor = 1.0 / total_active_distance_per_iteration; + double volume_normalization_factor = + 1.0 / (total_active_distance_per_iteration * simulation::current_batch); + +// Normalize flux to total distance travelled by all rays this iteration +#pragma omp parallel for + for (int64_t e = 0; e < scalar_flux_new_.size(); e++) { + scalar_flux_new_[e] *= normalization_factor; + flux_moments_new_[e] *= normalization_factor; + } + +// Accumulate cell-wise ray length tallies collected this iteration, then +// update the simulation-averaged cell-wise volume estimates +#pragma omp parallel for + for (int64_t sr = 0; sr < n_source_regions_; sr++) { + centroid_t_[sr] += centroid_iteration_[sr]; + mom_matrix_t_[sr] += mom_matrix_[sr]; + volume_t_[sr] += volume_[sr]; + volume_[sr] = volume_t_[sr] * volume_normalization_factor; + if (volume_t_[sr] > 0.0) { + double inv_volume = 1.0 / volume_t_[sr]; + centroid_[sr] = centroid_t_[sr]; + centroid_[sr] *= inv_volume; + mom_matrix_[sr] = mom_matrix_t_[sr]; + mom_matrix_[sr] *= inv_volume; + } + } +} + +int64_t LinearSourceDomain::add_source_to_scalar_flux() +{ + int64_t n_hits = 0; + + // Temperature and angle indices, if using multiple temperature + // data sets and/or anisotropic data sets. + // TODO: Currently assumes we are only using single temp/single + // angle data. + const int t = 0; + const int a = 0; + +#pragma omp parallel for reduction(+ : n_hits) + for (int sr = 0; sr < n_source_regions_; sr++) { + + double volume = volume_[sr]; + int material = material_[sr]; + + // Check if this cell was hit this iteration + int was_cell_hit = was_hit_[sr]; + if (was_cell_hit) { + n_hits++; + } + + for (int g = 0; g < negroups_; g++) { + int64_t idx = (sr * negroups_) + g; + // There are three scenarios we need to consider: + if (was_cell_hit) { + // 1. If the FSR was hit this iteration, then the new flux is equal to + // the flat source from the previous iteration plus the contributions + // from rays passing through the source region (computed during the + // transport sweep) + scalar_flux_new_[idx] /= volume; + scalar_flux_new_[idx] += source_[idx]; + flux_moments_new_[idx] *= (1.0 / volume); + } else if (volume > 0.0) { + // 2. If the FSR was not hit this iteration, but has been hit some + // previous iteration, then we simply set the new scalar flux to be + // equal to the contribution from the flat source alone. + scalar_flux_new_[idx] = source_[idx]; + } else { + // If the FSR was not hit this iteration, and it has never been hit in + // any iteration (i.e., volume is zero), then we want to set this to 0 + // to avoid dividing anything by a zero volume. + scalar_flux_new_[idx] = 0.0f; + flux_moments_new_[idx] *= 0.0; + } + } + } + + return n_hits; +} + +void LinearSourceDomain::flux_swap() +{ + FlatSourceDomain::flux_swap(); + flux_moments_old_.swap(flux_moments_new_); +} + +void LinearSourceDomain::accumulate_iteration_flux() +{ + // Accumulate scalar flux + FlatSourceDomain::accumulate_iteration_flux(); + + // Accumulate scalar flux moments +#pragma omp parallel for + for (int64_t se = 0; se < n_source_elements_; se++) { + flux_moments_t_[se] += flux_moments_new_[se]; + } +} + +void LinearSourceDomain::all_reduce_replicated_source_regions() +{ +#ifdef OPENMC_MPI + FlatSourceDomain::all_reduce_replicated_source_regions(); + simulation::time_bank_sendrecv.start(); + + // We are going to assume we can safely cast Position, MomentArray, + // and MomentMatrix to contiguous arrays of doubles for the MPI + // allreduce operation. This is a safe assumption as typically + // compilers will at most pad to 8 byte boundaries. If a new FP32 MomentArray + // type is introduced, then there will likely be padding, in which case this + // function will need to become more complex. + if (sizeof(MomentArray) != 3 * sizeof(double) || + sizeof(MomentMatrix) != 6 * sizeof(double)) { + fatal_error("Unexpected buffer padding in linear source domain reduction."); + } + + MPI_Allreduce(MPI_IN_PLACE, static_cast(flux_moments_new_.data()), + n_source_elements_ * 3, MPI_DOUBLE, MPI_SUM, mpi::intracomm); + MPI_Allreduce(MPI_IN_PLACE, static_cast(mom_matrix_.data()), + n_source_regions_ * 6, MPI_DOUBLE, MPI_SUM, mpi::intracomm); + MPI_Allreduce(MPI_IN_PLACE, static_cast(centroid_iteration_.data()), + n_source_regions_ * 3, MPI_DOUBLE, MPI_SUM, mpi::intracomm); + + simulation::time_bank_sendrecv.stop(); +#endif +} + +double LinearSourceDomain::evaluate_flux_at_point( + Position r, int64_t sr, int g) const +{ + float phi_flat = FlatSourceDomain::evaluate_flux_at_point(r, sr, g); + + Position local_r = r - centroid_[sr]; + MomentArray phi_linear = flux_moments_t_[sr * negroups_ + g]; + phi_linear *= 1.0 / (settings::n_batches - settings::n_inactive); + + MomentMatrix invM = mom_matrix_[sr].inverse(); + MomentArray phi_solved = invM * phi_linear; + + return phi_flat + phi_solved.dot(local_r); +} + +} // namespace openmc diff --git a/src/random_ray/moment_matrix.cpp b/src/random_ray/moment_matrix.cpp new file mode 100644 index 000000000..0324a1494 --- /dev/null +++ b/src/random_ray/moment_matrix.cpp @@ -0,0 +1,84 @@ +#include "openmc/random_ray/moment_matrix.h" +#include "openmc/error.h" + +#include + +namespace openmc { + +//============================================================================== +// UpperTriangular implementation +//============================================================================== + +// Inverts a 3x3 smmetric matrix labeled as: +// +// | a b c | +// | b d e | +// | c e f | +// +// We first check the determinant to ensure it is non-zero before proceeding +// with the inversion. If the determinant is zero, we return a matrix of zeros. +// Inversion is calculated by computing the adjoint matrix first, and then the +// inverse can be computed as: A^-1 = 1/det(A) * adj(A) +MomentMatrix MomentMatrix::inverse() const +{ + MomentMatrix inv; + + // Check if the determinant is zero + double det = determinant(); + if (det < std::abs(1.0e-10)) { + // Set the inverse to zero. In effect, this will + // result in all the linear terms of the source becoming + // zero, leaving just the flat source. + inv.set_to_zero(); + return inv; + } + + // Compute the adjoint matrix + inv.a = d * f - e * e; + inv.b = c * e - b * f; + inv.c = b * e - c * d; + inv.d = a * f - c * c; + inv.e = b * c - a * e; + inv.f = a * d - b * b; + + // A^-1 = 1/det(A) * adj(A) + inv *= 1.0 / det; + + return inv; +} + +// Computes the determinant of a 3x3 symmetric +// matrix, with elements labeled as follows: +// +// | a b c | +// | b d e | +// | c e f | +double MomentMatrix::determinant() const +{ + return a * (d * f - e * e) - b * (b * f - c * e) + c * (b * e - c * d); +} + +// Compute a 3x3 spatial moment matrix based on a single ray crossing. +// The matrix is symmetric, and is defined as: +// +// | a b c | +// | b d e | +// | c e f | +// +// The estimate of the obect's spatial moments matrix is computed based on the +// midpoint of the ray's crossing, the direction of the ray, and the distance +// the ray traveled through the 3D object. +void MomentMatrix::compute_spatial_moments_matrix( + const Position& r, const Direction& u, const double& distance) +{ + constexpr double one_over_twelve = 1.0 / 12.0; + const double distance2_12 = distance * distance * one_over_twelve; + a = r[0] * r[0] + u[0] * u[0] * distance2_12; + b = r[0] * r[1] + u[0] * u[1] * distance2_12; + c = r[0] * r[2] + u[0] * u[2] * distance2_12; + d = r[1] * r[1] + u[1] * u[1] * distance2_12; + e = r[1] * r[2] + u[1] * u[2] * distance2_12; + f = r[2] * r[2] + u[2] * u[2] * distance2_12; +} + +} // namespace openmc \ No newline at end of file diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp index 63d728cce..a5bf6ec10 100644 --- a/src/random_ray/random_ray.cpp +++ b/src/random_ray/random_ray.cpp @@ -1,9 +1,11 @@ #include "openmc/random_ray/random_ray.h" +#include "openmc/constants.h" #include "openmc/geometry.h" #include "openmc/message_passing.h" #include "openmc/mgxs_interface.h" #include "openmc/random_ray/flat_source_domain.h" +#include "openmc/random_ray/linear_source_domain.h" #include "openmc/search.h" #include "openmc/settings.h" #include "openmc/simulation.h" @@ -60,6 +62,118 @@ float cjosey_exponential(float tau) return num / den; } +// The below two functions (exponentialG and exponentialG2) were developed +// by Colin Josey. The implementation of these functions is closely based +// on the OpenMOC versions of these functions. The OpenMOC license is given +// below: + +// Copyright (C) 2012-2023 Massachusetts Institute of Technology and OpenMOC +// contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// Computes y = 1/x-(1-exp(-x))/x**2 using a 5/6th order rational +// approximation. It is accurate to 2e-7 over [0, 1e5]. Developed by Colin +// Josey using Remez's algorithm, with original implementation in OpenMOC at: +// https://github.com/mit-crpg/OpenMOC/blob/develop/src/exponentials.h +float exponentialG(float tau) +{ + // Numerator coefficients in rational approximation for 1/x - (1 - exp(-x)) / + // x^2 + constexpr float d0n = 0.5f; + constexpr float d1n = 0.176558112351595f; + constexpr float d2n = 0.04041584305811143f; + constexpr float d3n = 0.006178333902037397f; + constexpr float d4n = 0.0006429894635552992f; + constexpr float d5n = 0.00006064409107557148f; + + // Denominator coefficients in rational approximation for 1/x - (1 - exp(-x)) + // / x^2 + constexpr float d0d = 1.0f; + constexpr float d1d = 0.6864462055546078f; + constexpr float d2d = 0.2263358514260129f; + constexpr float d3d = 0.04721469893686252f; + constexpr float d4d = 0.006883236664917246f; + constexpr float d5d = 0.0007036272419147752f; + constexpr float d6d = 0.00006064409107557148f; + + float x = tau; + + float num = d5n; + num = num * x + d4n; + num = num * x + d3n; + num = num * x + d2n; + num = num * x + d1n; + num = num * x + d0n; + + float den = d6d; + den = den * x + d5d; + den = den * x + d4d; + den = den * x + d3d; + den = den * x + d2d; + den = den * x + d1d; + den = den * x + d0d; + + return num / den; +} + +// Computes G2 : y = 2/3 - (1 + 2/x) * (1/x + 0.5 - (1 + 1/x) * (1-exp(-x)) / +// x) using a 5/5th order rational approximation. It is accurate to 1e-6 over +// [0, 1e6]. Developed by Colin Josey using Remez's algorithm, with original +// implementation in OpenMOC at: +// https://github.com/mit-crpg/OpenMOC/blob/develop/src/exponentials.h +float exponentialG2(float tau) +{ + + // Coefficients for numerator in rational approximation + constexpr float g1n = -0.08335775885589858f; + constexpr float g2n = -0.003603942303847604f; + constexpr float g3n = 0.0037673183263550827f; + constexpr float g4n = 0.00001124183494990467f; + constexpr float g5n = 0.00016837426505799449f; + + // Coefficients for denominator in rational approximation + constexpr float g1d = 0.7454048371823628f; + constexpr float g2d = 0.23794300531408347f; + constexpr float g3d = 0.05367250964303789f; + constexpr float g4d = 0.006125197988351906f; + constexpr float g5d = 0.0010102514456857377f; + + float x = tau; + + float num = g5n; + num = num * x + g4n; + num = num * x + g3n; + num = num * x + g2n; + num = num * x + g1n; + num = num * x; + + float den = g5d; + den = den * x + g4d; + den = den * x + g3d; + den = den * x + g2d; + den = den * x + g1d; + den = den * x + 1.0f; + + return num / den; +} + //============================================================================== // RandomRay implementation //============================================================================== @@ -68,12 +182,18 @@ float cjosey_exponential(float tau) double RandomRay::distance_inactive_; double RandomRay::distance_active_; unique_ptr RandomRay::ray_source_; +RandomRaySourceShape RandomRay::source_shape_ {RandomRaySourceShape::FLAT}; RandomRay::RandomRay() : angular_flux_(data::mg.num_energy_groups_), delta_psi_(data::mg.num_energy_groups_), negroups_(data::mg.num_energy_groups_) -{} +{ + if (source_shape_ == RandomRaySourceShape::LINEAR || + source_shape_ == RandomRaySourceShape::LINEAR_XY) { + delta_moments_.resize(negroups_); + } +} RandomRay::RandomRay(uint64_t ray_id, FlatSourceDomain* domain) : RandomRay() { @@ -152,6 +272,21 @@ void RandomRay::event_advance_ray() } } +void RandomRay::attenuate_flux(double distance, bool is_active) +{ + switch (source_shape_) { + case RandomRaySourceShape::FLAT: + attenuate_flux_flat_source(distance, is_active); + break; + case RandomRaySourceShape::LINEAR: + case RandomRaySourceShape::LINEAR_XY: + attenuate_flux_linear_source(distance, is_active); + break; + default: + fatal_error("Unknown source shape for random ray transport."); + } +} + // This function forms the inner loop of the random ray transport process. // It is responsible for several tasks. Based on the incoming angular flux // of the ray and the source term in the region, the outgoing angular flux @@ -165,7 +300,7 @@ void RandomRay::event_advance_ray() // than use of many atomic operations corresponding to each energy group // individually (at least on CPU). Several other bookkeeping tasks are also // performed when inside the lock. -void RandomRay::attenuate_flux(double distance, bool is_active) +void RandomRay::attenuate_flux_flat_source(double distance, bool is_active) { // The number of geometric intersections is counted for reporting purposes n_event()++; @@ -236,6 +371,158 @@ void RandomRay::attenuate_flux(double distance, bool is_active) } } +void RandomRay::attenuate_flux_linear_source(double distance, bool is_active) +{ + // Cast domain to LinearSourceDomain + LinearSourceDomain* domain = dynamic_cast(domain_); + if (!domain) { + fatal_error("RandomRay::attenuate_flux_linear_source() called with " + "non-LinearSourceDomain domain."); + } + + // The number of geometric intersections is counted for reporting purposes + n_event()++; + + // Determine source region index etc. + int i_cell = lowest_coord().cell; + + // The source region is the spatial region index + int64_t source_region = + domain_->source_region_offsets_[i_cell] + cell_instance(); + + // The source element is the energy-specific region index + int64_t source_element = source_region * negroups_; + int material = this->material(); + + // Temperature and angle indices, if using multiple temperature + // data sets and/or anisotropic data sets. + // TODO: Currently assumes we are only using single temp/single + // angle data. + const int t = 0; + const int a = 0; + + Position& centroid = domain->centroid_[source_region]; + Position midpoint = r() + u() * (distance / 2.0); + + // Determine the local position of the midpoint and the ray origin + // relative to the source region's centroid + Position rm_local; + Position r0_local; + + // In the first few iterations of the simulation, the source region + // may not yet have had any ray crossings, in which case there will + // be no estimate of its centroid. We detect this by checking if it has + // any accumulated volume. If its volume is zero, just use the midpoint + // of the ray as the region's centroid. + if (domain->volume_t_[source_region]) { + rm_local = midpoint - centroid; + r0_local = r() - centroid; + } else { + rm_local = {0.0, 0.0, 0.0}; + r0_local = -u() * 0.5 * distance; + } + double distance_2 = distance * distance; + + // Linear Source MOC incoming flux attenuation + source + // contribution/attenuation equation + for (int g = 0; g < negroups_; g++) { + + // Compute tau, the optical thickness of the ray segment + float sigma_t = data::mg.macro_xs_[material].get_xs( + MgxsType::TOTAL, g, NULL, NULL, NULL, t, a); + float tau = sigma_t * distance; + + // If tau is very small, set it to zero to avoid numerical issues. + // The following computations will still work with tau = 0. + if (tau < 1.0e-8f) { + tau = 0.0f; + } + + // Compute linear source terms, spatial and directional (dir), + // calculated from the source gradients dot product with local centroid + // and direction, respectively. + float spatial_source = + domain_->source_[source_element + g] + + rm_local.dot(domain->source_gradients_[source_element + g]); + float dir_source = u().dot(domain->source_gradients_[source_element + g]); + + float gn = exponentialG(tau); + float f1 = 1.0f - tau * gn; + float f2 = (2.0f * gn - f1) * distance_2; + float new_delta_psi = (angular_flux_[g] - spatial_source) * f1 * distance - + 0.5 * dir_source * f2; + + float h1 = f1 - gn; + float g1 = 0.5f - h1; + float g2 = exponentialG2(tau); + g1 = g1 * spatial_source; + g2 = g2 * dir_source * distance * 0.5f; + h1 = h1 * angular_flux_[g]; + h1 = (g1 + g2 + h1) * distance_2; + spatial_source = spatial_source * distance + new_delta_psi; + + // Store contributions for this group into arrays, so that they can + // be accumulated into the source region's estimates inside of the locked + // region. + delta_psi_[g] = new_delta_psi; + delta_moments_[g] = r0_local * spatial_source + u() * h1; + + // Update the angular flux for this group + angular_flux_[g] -= new_delta_psi * sigma_t; + + // If 2D mode is enabled, the z-component of the flux moments is forced + // to zero + if (source_shape_ == RandomRaySourceShape::LINEAR_XY) { + delta_moments_[g].z = 0.0; + } + } + + // If ray is in the active phase (not in dead zone), make contributions to + // source region bookkeeping + if (is_active) { + // Compute an estimate of the spatial moments matrix for the source + // region based on parameters from this ray's crossing + MomentMatrix moment_matrix_estimate; + moment_matrix_estimate.compute_spatial_moments_matrix( + rm_local, u(), distance); + + // Aquire lock for source region + domain_->lock_[source_region].lock(); + + // Accumulate deltas into the new estimate of source region flux for this + // iteration + for (int g = 0; g < negroups_; g++) { + domain_->scalar_flux_new_[source_element + g] += delta_psi_[g]; + domain->flux_moments_new_[source_element + g] += delta_moments_[g]; + } + + // Accumulate the volume (ray segment distance), centroid, and spatial + // momement estimates into the running totals for the iteration for this + // source region. The centroid and spatial momements estimates are scaled by + // the ray segment length as part of length averaging of the estimates. + domain_->volume_[source_region] += distance; + domain->centroid_iteration_[source_region] += midpoint * distance; + moment_matrix_estimate *= distance; + domain->mom_matrix_[source_region] += moment_matrix_estimate; + + // If the source region hasn't been hit yet this iteration, + // indicate that it now has + if (domain_->was_hit_[source_region] == 0) { + domain_->was_hit_[source_region] = 1; + } + + // Tally valid position inside the source region (e.g., midpoint of + // the ray) if not done already + if (!domain_->position_recorded_[source_region]) { + domain_->position_[source_region] = midpoint; + domain_->position_recorded_[source_region] = 1; + } + + // Release lock + domain_->lock_[source_region].unlock(); + } +} + void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain) { domain_ = domain; diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index 17866c302..4bc77645b 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -6,6 +6,7 @@ #include "openmc/mgxs_interface.h" #include "openmc/output.h" #include "openmc/plot.h" +#include "openmc/random_ray/flat_source_domain.h" #include "openmc/random_ray/random_ray.h" #include "openmc/simulation.h" #include "openmc/source.h" @@ -241,14 +242,26 @@ RandomRaySimulation::RandomRaySimulation() // Random ray mode does not have an inner loop over generations within a // batch, so set the current gen to 1 simulation::current_gen = 1; + + switch (RandomRay::source_shape_) { + case RandomRaySourceShape::FLAT: + domain_ = make_unique(); + break; + case RandomRaySourceShape::LINEAR: + case RandomRaySourceShape::LINEAR_XY: + domain_ = make_unique(); + break; + default: + fatal_error("Unknown random ray source shape"); + } } void RandomRaySimulation::simulate() { if (settings::run_mode == RunMode::FIXED_SOURCE) { // Transfer external source user inputs onto random ray source regions - domain_.convert_external_sources(); - domain_.count_external_source_regions(); + domain_->convert_external_sources(); + domain_->count_external_source_regions(); } // Random ray power iteration loop @@ -262,11 +275,11 @@ void RandomRaySimulation::simulate() simulation::total_weight = 1.0; // Update source term (scattering + fission) - domain_.update_neutron_source(k_eff_); + domain_->update_neutron_source(k_eff_); // Reset scalar fluxes, iteration volume tallies, and region hit flags to // zero - domain_.batch_reset(); + domain_->batch_reset(); // Start timer for transport simulation::time_transport.start(); @@ -275,7 +288,7 @@ void RandomRaySimulation::simulate() #pragma omp parallel for schedule(dynamic) \ reduction(+ : total_geometric_intersections_) for (int i = 0; i < simulation::work_per_rank; i++) { - RandomRay ray(i, &domain_); + RandomRay ray(i, domain_.get()); total_geometric_intersections_ += ray.transport_history_based_single_ray(); } @@ -283,18 +296,18 @@ void RandomRaySimulation::simulate() simulation::time_transport.stop(); // If using multiple MPI ranks, perform all reduce on all transport results - domain_.all_reduce_replicated_source_regions(); + domain_->all_reduce_replicated_source_regions(); // Normalize scalar flux and update volumes - domain_.normalize_scalar_flux_and_volumes( + domain_->normalize_scalar_flux_and_volumes( settings::n_particles * RandomRay::distance_active_); // Add source to scalar flux, compute number of FSR hits - int64_t n_hits = domain_.add_source_to_scalar_flux(); + int64_t n_hits = domain_->add_source_to_scalar_flux(); if (settings::run_mode == RunMode::EIGENVALUE) { // Compute random ray k-eff - k_eff_ = domain_.compute_k_eff(k_eff_); + k_eff_ = domain_->compute_k_eff(k_eff_); // Store random ray k-eff into OpenMC's native k-eff variable global_tally_tracklength = k_eff_; @@ -304,19 +317,19 @@ void RandomRaySimulation::simulate() if (simulation::current_batch > settings::n_inactive && mpi::master) { // Generate mapping between source regions and tallies - if (!domain_.mapped_all_tallies_) { - domain_.convert_source_regions_to_tallies(); + if (!domain_->mapped_all_tallies_) { + domain_->convert_source_regions_to_tallies(); } // Use above mapping to contribute FSR flux data to appropriate tallies - domain_.random_ray_tally(); + domain_->random_ray_tally(); // Add this iteration's scalar flux estimate to final accumulated estimate - domain_.accumulate_iteration_flux(); + domain_->accumulate_iteration_flux(); } // Set phi_old = phi_new - domain_.scalar_flux_old_.swap(domain_.scalar_flux_new_); + domain_->flux_swap(); // Check for any obvious insabilities/nans/infs instability_check(n_hits, k_eff_, avg_miss_rate_); @@ -347,9 +360,9 @@ void RandomRaySimulation::output_simulation_results() const if (mpi::master) { print_results_random_ray(total_geometric_intersections_, avg_miss_rate_ / settings::n_batches, negroups_, - domain_.n_source_regions_, domain_.n_external_source_regions_); + domain_->n_source_regions_, domain_->n_external_source_regions_); if (model::plots.size() > 0) { - domain_.output_to_vtk(); + domain_->output_to_vtk(); } } } @@ -359,25 +372,28 @@ void RandomRaySimulation::output_simulation_results() const void RandomRaySimulation::instability_check( int64_t n_hits, double k_eff, double& avg_miss_rate) const { - double percent_missed = ((domain_.n_source_regions_ - n_hits) / - static_cast(domain_.n_source_regions_)) * + double percent_missed = ((domain_->n_source_regions_ - n_hits) / + static_cast(domain_->n_source_regions_)) * 100.0; avg_miss_rate += percent_missed; - if (percent_missed > 10.0) { - warning(fmt::format( - "Very high FSR miss rate detected ({:.3f}%). Instability may occur. " - "Increase ray density by adding more rays and/or active distance.", - percent_missed)); - } else if (percent_missed > 0.01) { - warning(fmt::format("Elevated FSR miss rate detected ({:.3f}%). Increasing " - "ray density by adding more rays and/or active " - "distance may improve simulation efficiency.", - percent_missed)); - } + if (mpi::master) { + if (percent_missed > 10.0) { + warning(fmt::format( + "Very high FSR miss rate detected ({:.3f}%). Instability may occur. " + "Increase ray density by adding more rays and/or active distance.", + percent_missed)); + } else if (percent_missed > 0.01) { + warning( + fmt::format("Elevated FSR miss rate detected ({:.3f}%). Increasing " + "ray density by adding more rays and/or active " + "distance may improve simulation efficiency.", + percent_missed)); + } - if (k_eff > 10.0 || k_eff < 0.01 || !(std::isfinite(k_eff))) { - fatal_error("Instability detected"); + if (k_eff > 10.0 || k_eff < 0.01 || !(std::isfinite(k_eff))) { + fatal_error("Instability detected"); + } } } diff --git a/src/settings.cpp b/src/settings.cpp index b92b77df9..db956abf5 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -269,6 +269,19 @@ void get_run_parameters(pugi::xml_node node_base) } else { fatal_error("Specify random ray source in settings XML"); } + if (check_for_node(random_ray_node, "source_shape")) { + std::string temp_str = + get_node_value(random_ray_node, "source_shape", true, true); + if (temp_str == "flat") { + RandomRay::source_shape_ = RandomRaySourceShape::FLAT; + } else if (temp_str == "linear") { + RandomRay::source_shape_ = RandomRaySourceShape::LINEAR; + } else if (temp_str == "linear_xy") { + RandomRay::source_shape_ = RandomRaySourceShape::LINEAR_XY; + } else { + fatal_error("Unrecognized source shape: " + temp_str); + } + } if (check_for_node(random_ray_node, "volume_normalized_flux_tallies")) { FlatSourceDomain::volume_normalized_flux_tallies_ = get_node_value_bool(random_ray_node, "volume_normalized_flux_tallies"); diff --git a/tests/regression_tests/random_ray_fixed_source_linear/__init__.py b/tests/regression_tests/random_ray_fixed_source_linear/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat new file mode 100644 index 000000000..e2972c5f2 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat @@ -0,0 +1,245 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + linear + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_fixed_source_linear/linear/results_true.dat b/tests/regression_tests/random_ray_fixed_source_linear/linear/results_true.dat new file mode 100644 index 000000000..7fb01a0ea --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_linear/linear/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +-5.745718E+02 +9.757661E+04 +tally 2: +3.074428E-02 +1.952251E-04 +tally 3: +1.980876E-03 +7.970502E-07 diff --git a/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat new file mode 100644 index 000000000..5b958d65c --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat @@ -0,0 +1,245 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + linear_xy + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/results_true.dat b/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/results_true.dat new file mode 100644 index 000000000..22b39edcf --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +-5.745810E+02 +9.758220E+04 +tally 2: +3.022777E-02 +1.884091E-04 +tally 3: +1.980651E-03 +7.968779E-07 diff --git a/tests/regression_tests/random_ray_fixed_source_linear/test.py b/tests/regression_tests/random_ray_fixed_source_linear/test.py new file mode 100644 index 000000000..25335dea6 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_linear/test.py @@ -0,0 +1,27 @@ +import os + +import numpy as np +import openmc +from openmc.utility_funcs import change_directory +from openmc.examples import random_ray_three_region_cube +import pytest + +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) + + +@pytest.mark.parametrize("shape", ["linear", "linear_xy"]) +def test_random_ray_fixed_source_linear(shape): + with change_directory(shape): + openmc.reset_auto_ids() + model = random_ray_three_region_cube() + model.settings.random_ray['source_shape'] = shape + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_linear/__init__.py b/tests/regression_tests/random_ray_linear/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_linear/linear/inputs_true.dat b/tests/regression_tests/random_ray_linear/linear/inputs_true.dat new file mode 100644 index 000000000..c580be3d1 --- /dev/null +++ b/tests/regression_tests/random_ray_linear/linear/inputs_true.dat @@ -0,0 +1,110 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.126 0.126 + 10 10 + -0.63 -0.63 + +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 + + + 1.26 1.26 + 2 2 + -1.26 -1.26 + +2 2 +2 5 + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + multi-group + + 100.0 + 20.0 + + + -1.26 -1.26 -1 1.26 1.26 1 + + + True + linear + + + + + 2 2 + -1.26 -1.26 + 1.26 1.26 + + + 1 + + + 1e-05 0.0635 10.0 100.0 1000.0 500000.0 1000000.0 20000000.0 + + + 1 2 + flux fission nu-fission + analog + + + diff --git a/tests/regression_tests/random_ray_linear/linear/results_true.dat b/tests/regression_tests/random_ray_linear/linear/results_true.dat new file mode 100644 index 000000000..a3e24dc75 --- /dev/null +++ b/tests/regression_tests/random_ray_linear/linear/results_true.dat @@ -0,0 +1,171 @@ +k-combined: +8.273022E-01 1.347623E-02 +tally 1: +5.004109E+00 +5.022655E+00 +1.844047E+00 +6.833376E-01 +4.488042E+00 +4.047669E+00 +2.824818E+00 +1.599927E+00 +4.182704E-01 +3.509796E-02 +1.017987E+00 +2.078987E-01 +1.676761E+00 +5.682729E-01 +5.385624E-02 +5.857594E-04 +1.310753E-01 +3.469677E-03 +2.353602E+00 +1.127695E+00 +7.721312E-02 +1.212202E-03 +1.879213E-01 +7.180339E-03 +7.082957E+00 +1.019023E+01 +8.203580E-02 +1.366996E-03 +1.996612E-01 +8.097436E-03 +2.034293E+01 +8.321967E+01 +3.099116E-02 +1.933713E-04 +7.668546E-02 +1.183975E-03 +1.311478E+01 +3.442575E+01 +1.778200E-01 +6.347187E-03 +4.945973E-01 +4.910476E-02 +7.576546E+00 +1.148094E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.386885E+00 +2.295318E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.823720E+00 +6.769694E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.703042E+00 +1.494228E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +7.465612E+00 +1.132769E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.820337E+01 +6.657534E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.127686E+01 +2.549122E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.561053E+00 +4.169936E+00 +1.674166E+00 +5.624648E-01 +4.074585E+00 +3.331694E+00 +2.723008E+00 +1.487311E+00 +4.059013E-01 +3.307772E-02 +9.878827E-01 +1.959320E-01 +1.663524E+00 +5.584734E-01 +5.399658E-02 +5.877094E-04 +1.314169E-01 +3.481227E-03 +2.310994E+00 +1.086435E+00 +7.627297E-02 +1.181877E-03 +1.856331E-01 +7.000707E-03 +7.100201E+00 +1.023633E+01 +8.285591E-02 +1.393520E-03 +2.016572E-01 +8.254554E-03 +2.119292E+01 +9.022917E+01 +3.281214E-02 +2.163583E-04 +8.119135E-02 +1.324719E-03 +1.380975E+01 +3.815787E+01 +1.919186E-01 +7.384267E-03 +5.338118E-01 +5.712809E-02 +5.024478E+00 +5.056216E+00 +1.888826E+00 +7.148091E-01 +4.597025E+00 +4.234087E+00 +2.839930E+00 +1.616473E+00 +4.294394E-01 +3.697612E-02 +1.045170E+00 +2.190237E-01 +1.688196E+00 +5.765397E-01 +5.539650E-02 +6.200086E-04 +1.348240E-01 +3.672547E-03 +2.361633E+00 +1.136540E+00 +7.901052E-02 +1.270359E-03 +1.922958E-01 +7.524822E-03 +7.100780E+00 +1.024481E+01 +8.389610E-02 +1.429594E-03 +2.041888E-01 +8.468240E-03 +2.049187E+01 +8.438141E+01 +3.195576E-02 +2.052140E-04 +7.907229E-02 +1.256485E-03 +1.327412E+01 +3.524466E+01 +1.850084E-01 +6.847675E-03 +5.145915E-01 +5.297677E-02 diff --git a/tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat b/tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat new file mode 100644 index 000000000..5c4076de7 --- /dev/null +++ b/tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat @@ -0,0 +1,110 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.126 0.126 + 10 10 + -0.63 -0.63 + +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 + + + 1.26 1.26 + 2 2 + -1.26 -1.26 + +2 2 +2 5 + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + multi-group + + 100.0 + 20.0 + + + -1.26 -1.26 -1 1.26 1.26 1 + + + True + linear_xy + + + + + 2 2 + -1.26 -1.26 + 1.26 1.26 + + + 1 + + + 1e-05 0.0635 10.0 100.0 1000.0 500000.0 1000000.0 20000000.0 + + + 1 2 + flux fission nu-fission + analog + + + diff --git a/tests/regression_tests/random_ray_linear/linear_xy/results_true.dat b/tests/regression_tests/random_ray_linear/linear_xy/results_true.dat new file mode 100644 index 000000000..f79b3fa78 --- /dev/null +++ b/tests/regression_tests/random_ray_linear/linear_xy/results_true.dat @@ -0,0 +1,171 @@ +k-combined: +8.368882E-01 8.107070E-03 +tally 1: +5.072700E+00 +5.152684E+00 +1.876680E+00 +7.051697E-01 +4.567463E+00 +4.176989E+00 +2.858196E+00 +1.636775E+00 +4.239946E-01 +3.601884E-02 +1.031918E+00 +2.133534E-01 +1.692006E+00 +5.789664E-01 +5.442292E-02 +5.988675E-04 +1.324545E-01 +3.547321E-03 +2.371801E+00 +1.146513E+00 +7.804414E-02 +1.241074E-03 +1.899438E-01 +7.351359E-03 +7.134037E+00 +1.034554E+01 +8.269855E-02 +1.390936E-03 +2.012742E-01 +8.239245E-03 +2.043174E+01 +8.386878E+01 +3.098171E-02 +1.929186E-04 +7.666208E-02 +1.181203E-03 +1.312800E+01 +3.447362E+01 +1.763141E-01 +6.217438E-03 +4.904088E-01 +4.810096E-02 +7.608165E+00 +1.157716E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.392187E+00 +2.302667E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.821339E+00 +6.737984E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.695742E+00 +1.483104E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +7.457904E+00 +1.129343E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.824331E+01 +6.687165E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.138096E+01 +2.591161E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.582534E+00 +4.206969E+00 +1.707129E+00 +5.837143E-01 +4.154811E+00 +3.457563E+00 +2.727459E+00 +1.491183E+00 +4.100516E-01 +3.371280E-02 +9.979836E-01 +1.996938E-01 +1.660765E+00 +5.570066E-01 +5.425719E-02 +5.941503E-04 +1.320511E-01 +3.519379E-03 +2.306630E+00 +1.083425E+00 +7.695103E-02 +1.205473E-03 +1.872834E-01 +7.140475E-03 +7.077631E+00 +1.018246E+01 +8.321109E-02 +1.408010E-03 +2.025216E-01 +8.340388E-03 +2.094896E+01 +8.817186E+01 +3.233088E-02 +2.099749E-04 +8.000050E-02 +1.285635E-03 +1.356905E+01 +3.683229E+01 +1.859858E-01 +6.921278E-03 +5.173102E-01 +5.354619E-02 +5.056029E+00 +5.118575E+00 +1.910007E+00 +7.308066E-01 +4.648575E+00 +4.328846E+00 +2.856363E+00 +1.634636E+00 +4.328679E-01 +3.755784E-02 +1.053514E+00 +2.224694E-01 +1.692444E+00 +5.793158E-01 +5.559366E-02 +6.243434E-04 +1.353038E-01 +3.698224E-03 +2.368251E+00 +1.142832E+00 +7.949453E-02 +1.285913E-03 +1.934738E-01 +7.616955E-03 +7.118354E+00 +1.029753E+01 +8.426801E-02 +1.442436E-03 +2.050940E-01 +8.544309E-03 +2.046889E+01 +8.419814E+01 +3.182500E-02 +2.035334E-04 +7.874874E-02 +1.246195E-03 +1.326056E+01 +3.517079E+01 +1.833225E-01 +6.723237E-03 +5.099023E-01 +5.201406E-02 diff --git a/tests/regression_tests/random_ray_linear/test.py b/tests/regression_tests/random_ray_linear/test.py new file mode 100644 index 000000000..10262789d --- /dev/null +++ b/tests/regression_tests/random_ray_linear/test.py @@ -0,0 +1,26 @@ +import os + +import openmc +from openmc.examples import random_ray_lattice +from openmc.utility_funcs import change_directory +import pytest + +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) + + +@pytest.mark.parametrize("shape", ["linear", "linear_xy"]) +def test_random_ray_source(shape): + with change_directory(shape): + openmc.reset_auto_ids() + model = random_ray_lattice() + model.settings.random_ray['source_shape'] = shape + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() From 32440ad203116d13e893a5ac37ab848df684011b Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 18 Jul 2024 11:54:06 -0500 Subject: [PATCH 150/671] Remove use of pkg_resources package (#3069) --- openmc/lib/__init__.py | 8 +++----- pyproject.toml | 2 +- tests/regression_tests/conftest.py | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/openmc/lib/__init__.py b/openmc/lib/__init__.py index 42d779441..9bb2efb38 100644 --- a/openmc/lib/__init__.py +++ b/openmc/lib/__init__.py @@ -13,11 +13,10 @@ functions or objects in :mod:`openmc.lib`, for example: """ from ctypes import CDLL, c_bool, c_int +import importlib.resources import os import sys -import pkg_resources - # Determine shared-library suffix if sys.platform == 'darwin': @@ -27,9 +26,8 @@ else: if os.environ.get('READTHEDOCS', None) != 'True': # Open shared library - _filename = pkg_resources.resource_filename( - __name__, f'libopenmc.{_suffix}') - _dll = CDLL(_filename) + _filename = importlib.resources.files(__name__) / f'libopenmc.{_suffix}' + _dll = CDLL(str(_filename)) # TODO: Remove str() when Python 3.12+ else: # For documentation builds, we don't actually have the shared library # available. Instead, we create a mock object so that when the modules diff --git a/pyproject.toml b/pyproject.toml index ca646323b..d1b2c335c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ docs = [ "sphinxcontrib-svg2pdfconverter", "sphinx-rtd-theme==1.0.0" ] -test = ["pytest", "pytest-cov", "colorama", "openpyxl"] +test = ["packaging", "pytest", "pytest-cov", "colorama", "openpyxl"] vtk = ["vtk"] [project.urls] diff --git a/tests/regression_tests/conftest.py b/tests/regression_tests/conftest.py index b4a764476..1cdf414e7 100644 --- a/tests/regression_tests/conftest.py +++ b/tests/regression_tests/conftest.py @@ -1,12 +1,12 @@ import numpy as np import openmc -from pkg_resources import parse_version +from packaging.version import parse import pytest @pytest.fixture(scope='module', autouse=True) def numpy_version_requirement(): - assert parse_version(np.__version__) >= parse_version("1.14"), \ + assert parse(np.__version__) >= parse("1.14"), \ "Regression tests require NumPy 1.14 or greater" From 4c0e08bae86c9d40c70740bc98e043b81d295a1f Mon Sep 17 00:00:00 2001 From: John Vincent Cauilan <64677361+johvincau@users.noreply.github.com> Date: Thu, 18 Jul 2024 12:40:42 -0500 Subject: [PATCH 151/671] Replace all deprecated Python typing imports and syntax with updated forms (#3085) Co-authored-by: Paul Romano --- openmc/bounding_box.py | 2 +- openmc/checkvalue.py | 3 +- openmc/data/data.py | 5 +- openmc/data/decay.py | 3 +- openmc/deplete/coupled_operator.py | 3 +- openmc/deplete/independent_operator.py | 3 +- openmc/deplete/microxs.py | 24 ++++---- openmc/deplete/openmc_operator.py | 3 +- openmc/deplete/reaction_rates.py | 7 +-- openmc/deplete/results.py | 35 ++++++----- openmc/geometry.py | 37 ++++++------ openmc/lib/mesh.py | 7 +-- openmc/material.py | 82 +++++++++++++------------- openmc/mesh.py | 56 +++++++++--------- openmc/model/model.py | 15 +++-- openmc/model/surface_composite.py | 3 +- openmc/plots.py | 3 +- openmc/settings.py | 26 ++++---- openmc/source.py | 48 ++++++++------- openmc/stats/multivariate.py | 18 +++--- openmc/stats/univariate.py | 25 ++++---- openmc/utility_funcs.py | 3 +- openmc/weight_windows.py | 20 +++---- 23 files changed, 204 insertions(+), 227 deletions(-) diff --git a/openmc/bounding_box.py b/openmc/bounding_box.py index 6e58ca8ba..5f3d3a6cf 100644 --- a/openmc/bounding_box.py +++ b/openmc/bounding_box.py @@ -1,5 +1,5 @@ from __future__ import annotations -from typing import Iterable +from collections.abc import Iterable import numpy as np diff --git a/openmc/checkvalue.py b/openmc/checkvalue.py index 42df3e5ef..4fa205b14 100644 --- a/openmc/checkvalue.py +++ b/openmc/checkvalue.py @@ -1,12 +1,11 @@ import copy import os -import typing # required to prevent typing.Union namespace overwriting Union from collections.abc import Iterable import numpy as np # Type for arguments that accept file paths -PathLike = typing.Union[str, os.PathLike] +PathLike = str | os.PathLike def check_type(name, value, expected_type, expected_iter_type=None, *, none_ok=False): diff --git a/openmc/data/data.py b/openmc/data/data.py index 7caf31e26..d94d6aaaa 100644 --- a/openmc/data/data.py +++ b/openmc/data/data.py @@ -5,7 +5,6 @@ import re from pathlib import Path from math import sqrt, log from warnings import warn -from typing import Dict # Isotopic abundances from Meija J, Coplen T B, et al, "Isotopic compositions # of the elements 2013 (IUPAC Technical Report)", Pure. Appl. Chem. 88 (3), @@ -283,13 +282,13 @@ AVOGADRO = 6.02214076e23 NEUTRON_MASS = 1.00866491595 # Used in atomic_mass function as a cache -_ATOMIC_MASS: Dict[str, float] = {} +_ATOMIC_MASS: dict[str, float] = {} # Regex for GNDS nuclide names (used in zam function) _GNDS_NAME_RE = re.compile(r'([A-Zn][a-z]*)(\d+)((?:_[em]\d+)?)') # Used in half_life function as a cache -_HALF_LIFE: Dict[str, float] = {} +_HALF_LIFE: dict[str, float] = {} _LOG_TWO = log(2.0) def atomic_mass(isotope): diff --git a/openmc/data/decay.py b/openmc/data/decay.py index be3dab77a..66acb2212 100644 --- a/openmc/data/decay.py +++ b/openmc/data/decay.py @@ -2,7 +2,6 @@ from collections.abc import Iterable from io import StringIO from math import log import re -from typing import Optional from warnings import warn import numpy as np @@ -579,7 +578,7 @@ class Decay(EqualityMixin): _DECAY_PHOTON_ENERGY = {} -def decay_photon_energy(nuclide: str) -> Optional[Univariate]: +def decay_photon_energy(nuclide: str) -> Univariate | None: """Get photon energy distribution resulting from the decay of a nuclide This function relies on data stored in a depletion chain. Before calling it diff --git a/openmc/deplete/coupled_operator.py b/openmc/deplete/coupled_operator.py index acdcf467c..17af935f4 100644 --- a/openmc/deplete/coupled_operator.py +++ b/openmc/deplete/coupled_operator.py @@ -10,7 +10,6 @@ filesystem. import copy from warnings import warn -from typing import Optional import numpy as np from uncertainties import ufloat @@ -34,7 +33,7 @@ from .helpers import ( __all__ = ["CoupledOperator", "Operator", "OperatorResult"] -def _find_cross_sections(model: Optional[str] = None): +def _find_cross_sections(model: str | None = None): """Determine cross sections to use for depletion Parameters diff --git a/openmc/deplete/independent_operator.py b/openmc/deplete/independent_operator.py index 250b94deb..759abde13 100644 --- a/openmc/deplete/independent_operator.py +++ b/openmc/deplete/independent_operator.py @@ -8,7 +8,6 @@ transport solver by using user-provided one-group cross sections. from __future__ import annotations from collections.abc import Iterable import copy -from typing import List, Set import numpy as np from uncertainties import ufloat @@ -279,7 +278,7 @@ class IndependentOperator(OpenMCOperator): new_res = res_obj.distribute(self.local_mats, mat_indexes) self.prev_res.append(new_res) - def _get_nuclides_with_data(self, cross_sections: List[MicroXS]) -> Set[str]: + def _get_nuclides_with_data(self, cross_sections: list[MicroXS]) -> set[str]: """Finds nuclides with cross section data""" return set(cross_sections[0].nuclides) diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index b2ae9f6c7..39753529b 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -5,8 +5,8 @@ IndependentOperator class for depletion. """ from __future__ import annotations +from collections.abc import Iterable, Sequence from tempfile import TemporaryDirectory -from typing import List, Tuple, Iterable, Optional, Union, Sequence import pandas as pd import numpy as np @@ -27,7 +27,7 @@ _valid_rxns.append('fission') _valid_rxns.append('damage-energy') -def _resolve_chain_file_path(chain_file: str): +def _resolve_chain_file_path(chain_file: str | None): if chain_file is None: chain_file = openmc.config.get('chain_file') if 'chain_file' not in openmc.config: @@ -41,12 +41,12 @@ def _resolve_chain_file_path(chain_file: str): def get_microxs_and_flux( model: openmc.Model, domains, - nuclides: Optional[Iterable[str]] = None, - reactions: Optional[Iterable[str]] = None, - energies: Optional[Union[Iterable[float], str]] = None, - chain_file: Optional[PathLike] = None, + nuclides: Iterable[str] | None = None, + reactions: Iterable[str] | None = None, + energies: Iterable[float] | str | None = None, + chain_file: PathLike | None = None, run_kwargs=None - ) -> Tuple[List[np.ndarray], List[MicroXS]]: + ) -> tuple[list[np.ndarray], list[MicroXS]]: """Generate a microscopic cross sections and flux from a Model .. versionadded:: 0.14.0 @@ -183,7 +183,7 @@ class MicroXS: :data:`openmc.deplete.chain.REACTIONS` """ - def __init__(self, data: np.ndarray, nuclides: List[str], reactions: List[str]): + def __init__(self, data: np.ndarray, nuclides: list[str], reactions: list[str]): # Validate inputs if data.shape[:2] != (len(nuclides), len(reactions)): raise ValueError( @@ -205,12 +205,12 @@ class MicroXS: @classmethod def from_multigroup_flux( cls, - energies: Union[Sequence[float], str], + energies: Sequence[float] | str, multigroup_flux: Sequence[float], - chain_file: Optional[PathLike] = None, + chain_file: PathLike | None = None, temperature: float = 293.6, - nuclides: Optional[Sequence[str]] = None, - reactions: Optional[Sequence[str]] = None, + nuclides: Sequence[str] | None = None, + reactions: Sequence[str] | None = None, **init_kwargs: dict, ) -> MicroXS: """Generated microscopic cross sections from a known flux. diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py index 75f0925c4..c0ff568bc 100644 --- a/openmc/deplete/openmc_operator.py +++ b/openmc/deplete/openmc_operator.py @@ -7,7 +7,6 @@ transport-independent transport operators. from abc import abstractmethod from warnings import warn -from typing import List, Tuple, Dict import numpy as np @@ -185,7 +184,7 @@ class OpenMCOperator(TransportOperator): """Assign distribmats for each burnable material""" pass - def _get_burnable_mats(self) -> Tuple[List[str], Dict[str, float], List[str]]: + def _get_burnable_mats(self) -> tuple[list[str], dict[str, float], list[str]]: """Determine depletable materials, volumes, and nuclides Returns diff --git a/openmc/deplete/reaction_rates.py b/openmc/deplete/reaction_rates.py index 4aaf829a9..714d9048b 100644 --- a/openmc/deplete/reaction_rates.py +++ b/openmc/deplete/reaction_rates.py @@ -2,7 +2,6 @@ An ndarray to store reaction rates with string, integer, or slice indexing. """ -from typing import Dict import numpy as np @@ -53,9 +52,9 @@ class ReactionRates(np.ndarray): # the __array_finalize__ method (discussed here: # https://docs.scipy.org/doc/numpy/user/basics.subclassing.html) - index_mat: Dict[str, int] - index_nuc: Dict[str, int] - index_rx: Dict[str, int] + index_mat: dict[str, int] + index_nuc: dict[str, int] + index_rx: dict[str, int] def __new__(cls, local_mats, nuclides, reactions, from_results=False): # Create appropriately-sized zeroed-out ndarray diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index 2e537a1b7..f897a8842 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -1,8 +1,7 @@ import numbers import bisect import math -import typing # required to prevent typing.Union namespace overwriting Union -from typing import Iterable, Optional, Tuple, List +from collections.abc import Iterable from warnings import warn import h5py @@ -97,11 +96,11 @@ class Results(list): def get_activity( self, - mat: typing.Union[Material, str], + mat: Material | str, units: str = "Bq/cm3", by_nuclide: bool = False, - volume: Optional[float] = None - ) -> Tuple[np.ndarray, typing.Union[np.ndarray, List[dict]]]: + volume: float | None = None + ) -> tuple[np.ndarray, np.ndarray | list[dict]]: """Get activity of material over time. .. versionadded:: 0.14.0 @@ -152,11 +151,11 @@ class Results(list): def get_atoms( self, - mat: typing.Union[Material, str], + mat: Material | str, nuc: str, nuc_units: str = "atoms", time_units: str = "s" - ) -> Tuple[np.ndarray, np.ndarray]: + ) -> tuple[np.ndarray, np.ndarray]: """Get number of nuclides over time from a single material Parameters @@ -215,11 +214,11 @@ class Results(list): def get_decay_heat( self, - mat: typing.Union[Material, str], + mat: Material | str, units: str = "W", by_nuclide: bool = False, - volume: Optional[float] = None - ) -> Tuple[np.ndarray, typing.Union[np.ndarray, List[dict]]]: + volume: float | None = None + ) -> tuple[np.ndarray, np.ndarray | list[dict]]: """Get decay heat of material over time. .. versionadded:: 0.14.0 @@ -242,7 +241,7 @@ class Results(list): ------- times : numpy.ndarray Array of times in [s] - decay_heat : numpy.ndarray or List[dict] + decay_heat : numpy.ndarray or list[dict] Array of total decay heat values if by_nuclide = False (default) or list of dictionaries of decay heat values by nuclide if by_nuclide = True. @@ -270,11 +269,11 @@ class Results(list): return times, decay_heat def get_mass(self, - mat: typing.Union[Material, str], + mat: Material | str, nuc: str, mass_units: str = "g", time_units: str = "s" - ) -> Tuple[np.ndarray, np.ndarray]: + ) -> tuple[np.ndarray, np.ndarray]: """Get mass of nuclides over time from a single material .. versionadded:: 0.14.0 @@ -324,10 +323,10 @@ class Results(list): def get_reaction_rate( self, - mat: typing.Union[Material, str], + mat: Material | str, nuc: str, rx: str - ) -> Tuple[np.ndarray, np.ndarray]: + ) -> tuple[np.ndarray, np.ndarray]: """Get reaction rate in a single material/nuclide over time Parameters @@ -364,7 +363,7 @@ class Results(list): return times, rates - def get_keff(self, time_units: str = 's') -> Tuple[np.ndarray, np.ndarray]: + def get_keff(self, time_units: str = 's') -> tuple[np.ndarray, np.ndarray]: """Evaluates the eigenvalue from a results list. .. versionadded:: 0.13.1 @@ -400,7 +399,7 @@ class Results(list): times = _get_time_as(times, time_units) return times, eigenvalues - def get_eigenvalue(self, time_units: str = 's') -> Tuple[np.ndarray, np.ndarray]: + def get_eigenvalue(self, time_units: str = 's') -> tuple[np.ndarray, np.ndarray]: warn("The get_eigenvalue(...) function has been renamed get_keff and " "will be removed in a future version of OpenMC.", FutureWarning) return self.get_keff(time_units) @@ -526,7 +525,7 @@ class Results(list): def export_to_materials( self, burnup_index: int, - nuc_with_data: Optional[Iterable[str]] = None, + nuc_with_data: Iterable[str] | None = None, path: PathLike = 'materials.xml' ) -> Materials: """Return openmc.Materials object based on results at a given step diff --git a/openmc/geometry.py b/openmc/geometry.py index e37a69c73..6cce4c18c 100644 --- a/openmc/geometry.py +++ b/openmc/geometry.py @@ -1,6 +1,5 @@ from __future__ import annotations import os -import typing from collections import defaultdict from copy import deepcopy from collections.abc import Iterable @@ -41,7 +40,7 @@ class Geometry: def __init__( self, - root: openmc.UniverseBase | typing.Iterable[openmc.Cell] | None = None, + root: openmc.UniverseBase | Iterable[openmc.Cell] | None = None, merge_surfaces: bool = False, surface_precision: int = 10 ): @@ -267,7 +266,7 @@ class Geometry: def from_xml( cls, path: PathLike = 'geometry.xml', - materials: typing.Optional[typing.Union[PathLike, 'openmc.Materials']] = 'materials.xml' + materials: PathLike | 'openmc.Materials' | None = 'materials.xml' ) -> Geometry: """Generate geometry from XML file @@ -316,7 +315,7 @@ class Geometry: """ return self.root_universe.find(point) - def get_instances(self, paths) -> typing.Union[int, typing.List[int]]: + def get_instances(self, paths) -> int | list[int]: """Return the instance number(s) for a cell/material in a geometry path. The instance numbers are used as indices into distributed @@ -363,7 +362,7 @@ class Geometry: return indices if return_list else indices[0] - def get_all_cells(self) -> typing.Dict[int, openmc.Cell]: + def get_all_cells(self) -> dict[int, openmc.Cell]: """Return all cells in the geometry. Returns @@ -377,7 +376,7 @@ class Geometry: else: return {} - def get_all_universes(self) -> typing.Dict[int, openmc.Universe]: + def get_all_universes(self) -> dict[int, openmc.Universe]: """Return all universes in the geometry. Returns @@ -392,7 +391,7 @@ class Geometry: universes.update(self.root_universe.get_all_universes()) return universes - def get_all_nuclides(self) -> typing.List[str]: + def get_all_nuclides(self) -> list[str]: """Return all nuclides within the geometry. Returns @@ -406,7 +405,7 @@ class Geometry: all_nuclides |= set(material.get_nuclides()) return sorted(all_nuclides) - def get_all_materials(self) -> typing.Dict[int, openmc.Material]: + def get_all_materials(self) -> dict[int, openmc.Material]: """Return all materials within the geometry. Returns @@ -421,7 +420,7 @@ class Geometry: else: return {} - def get_all_material_cells(self) -> typing.Dict[int, openmc.Cell]: + def get_all_material_cells(self) -> dict[int, openmc.Cell]: """Return all cells filled by a material Returns @@ -440,7 +439,7 @@ class Geometry: return material_cells - def get_all_material_universes(self) -> typing.Dict[int, openmc.Universe]: + def get_all_material_universes(self) -> dict[int, openmc.Universe]: """Return all universes having at least one material-filled cell. This method can be used to find universes that have at least one cell @@ -463,7 +462,7 @@ class Geometry: return material_universes - def get_all_lattices(self) -> typing.Dict[int, openmc.Lattice]: + def get_all_lattices(self) -> dict[int, openmc.Lattice]: """Return all lattices defined Returns @@ -481,7 +480,7 @@ class Geometry: return lattices - def get_all_surfaces(self) -> typing.Dict[int, openmc.Surface]: + def get_all_surfaces(self) -> dict[int, openmc.Surface]: """ Return all surfaces used in the geometry @@ -517,7 +516,7 @@ class Geometry: def get_materials_by_name( self, name, case_sensitive=False, matching=False - ) -> typing.List[openmc.Material]: + ) -> list[openmc.Material]: """Return a list of materials with matching names. Parameters @@ -540,7 +539,7 @@ class Geometry: def get_cells_by_name( self, name, case_sensitive=False, matching=False - ) -> typing.List[openmc.Cell]: + ) -> list[openmc.Cell]: """Return a list of cells with matching names. Parameters @@ -563,7 +562,7 @@ class Geometry: def get_surfaces_by_name( self, name, case_sensitive=False, matching=False - ) -> typing.List[openmc.Surface]: + ) -> list[openmc.Surface]: """Return a list of surfaces with matching names. .. versionadded:: 0.13.3 @@ -588,7 +587,7 @@ class Geometry: def get_cells_by_fill_name( self, name, case_sensitive=False, matching=False - ) -> typing.List[openmc.Cell]: + ) -> list[openmc.Cell]: """Return a list of cells with fills with matching names. Parameters @@ -635,7 +634,7 @@ class Geometry: def get_universes_by_name( self, name, case_sensitive=False, matching=False - ) -> typing.List[openmc.Universe]: + ) -> list[openmc.Universe]: """Return a list of universes with matching names. Parameters @@ -658,7 +657,7 @@ class Geometry: def get_lattices_by_name( self, name, case_sensitive=False, matching=False - ) -> typing.List[openmc.Lattice]: + ) -> list[openmc.Lattice]: """Return a list of lattices with matching names. Parameters @@ -679,7 +678,7 @@ class Geometry: """ return self._get_domains_by_name(name, case_sensitive, matching, 'lattice') - def remove_redundant_surfaces(self) -> typing.Dict[int, openmc.Surface]: + def remove_redundant_surfaces(self) -> dict[int, openmc.Surface]: """Remove and return all of the redundant surfaces. Uses surface_precision attribute of Geometry instance for rounding and diff --git a/openmc/lib/mesh.py b/openmc/lib/mesh.py index 3546112b5..78566d449 100644 --- a/openmc/lib/mesh.py +++ b/openmc/lib/mesh.py @@ -1,8 +1,7 @@ -from collections.abc import Mapping +from collections.abc import Mapping, Sequence from ctypes import (c_int, c_int32, c_char_p, c_double, POINTER, Structure, create_string_buffer, c_uint64, c_size_t) from random import getrandbits -from typing import Optional, List, Tuple, Sequence from weakref import WeakValueDictionary import numpy as np @@ -170,8 +169,8 @@ class Mesh(_FortranObjectWithID): def material_volumes( self, n_samples: int = 10_000, - prn_seed: Optional[int] = None - ) -> List[List[Tuple[Material, float]]]: + prn_seed: int | None = None + ) -> list[list[tuple[Material, float]]]: """Determine volume of materials in each mesh element .. versionadded:: 0.15.0 diff --git a/openmc/material.py b/openmc/material.py index 4b871b77d..5b958c75a 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -5,9 +5,7 @@ from copy import deepcopy from numbers import Real from pathlib import Path import re -import typing # imported separately as py3.8 requires typing.Iterable import warnings -from typing import Optional, List, Union, Dict import lxml.etree as ET import numpy as np @@ -161,11 +159,11 @@ class Material(IDManagerMixin): return string @property - def name(self) -> Optional[str]: + def name(self) -> str | None: return self._name @name.setter - def name(self, name: Optional[str]): + def name(self, name: str | None): if name is not None: cv.check_type(f'name for Material ID="{self._id}"', name, str) @@ -174,17 +172,17 @@ class Material(IDManagerMixin): self._name = '' @property - def temperature(self) -> Optional[float]: + def temperature(self) -> float | None: return self._temperature @temperature.setter - def temperature(self, temperature: Optional[Real]): + def temperature(self, temperature: Real | None): cv.check_type(f'Temperature for Material ID="{self._id}"', temperature, (Real, type(None))) self._temperature = temperature @property - def density(self) -> Optional[float]: + def density(self) -> float | None: return self._density @property @@ -202,7 +200,7 @@ class Material(IDManagerMixin): self._depletable = depletable @property - def paths(self) -> List[str]: + def paths(self) -> list[str]: if self._paths is None: raise ValueError('Material instance paths have not been determined. ' 'Call the Geometry.determine_paths() method.') @@ -217,15 +215,15 @@ class Material(IDManagerMixin): return self._num_instances @property - def nuclides(self) -> List[namedtuple]: + def nuclides(self) -> list[namedtuple]: return self._nuclides @property - def isotropic(self) -> List[str]: + def isotropic(self) -> list[str]: return self._isotropic @isotropic.setter - def isotropic(self, isotropic: typing.Iterable[str]): + def isotropic(self, isotropic: Iterable[str]): cv.check_iterable_type('Isotropic scattering nuclides', isotropic, str) self._isotropic = list(isotropic) @@ -248,7 +246,7 @@ class Material(IDManagerMixin): return mass / moles @property - def volume(self) -> Optional[float]: + def volume(self) -> float | None: return self._volume @volume.setter @@ -258,7 +256,7 @@ class Material(IDManagerMixin): self._volume = volume @property - def ncrystal_cfg(self) -> Optional[str]: + def ncrystal_cfg(self) -> str | None: return self._ncrystal_cfg @property @@ -274,7 +272,7 @@ class Material(IDManagerMixin): return density*self.volume @property - def decay_photon_energy(self) -> Optional[Univariate]: + def decay_photon_energy(self) -> Univariate | None: warnings.warn( "The 'decay_photon_energy' property has been replaced by the " "get_decay_photon_energy() method and will be removed in a future " @@ -285,8 +283,8 @@ class Material(IDManagerMixin): self, clip_tolerance: float = 1e-6, units: str = 'Bq', - volume: Optional[float] = None - ) -> Optional[Univariate]: + volume: float | None = None + ) -> Univariate | None: r"""Return energy distribution of decay photons from unstable nuclides. .. versionadded:: 0.14.0 @@ -471,7 +469,7 @@ class Material(IDManagerMixin): else: raise ValueError(f'No volume information found for material ID={self.id}.') - def set_density(self, units: str, density: Optional[float] = None): + def set_density(self, units: str, density: float | None = None): """Set the density of the material Parameters @@ -685,10 +683,10 @@ class Material(IDManagerMixin): self._macroscopic = None def add_element(self, element: str, percent: float, percent_type: str = 'ao', - enrichment: Optional[float] = None, - enrichment_target: Optional[str] = None, - enrichment_type: Optional[str] = None, - cross_sections: Optional[str] = None): + enrichment: float | None = None, + enrichment_target: str | None = None, + enrichment_type: str | None = None, + cross_sections: str | None = None): """Add a natural element to the material Parameters @@ -799,9 +797,9 @@ class Material(IDManagerMixin): self.add_nuclide(*nuclide) def add_elements_from_formula(self, formula: str, percent_type: str = 'ao', - enrichment: Optional[float] = None, - enrichment_target: Optional[str] = None, - enrichment_type: Optional[str] = None): + enrichment: float | None = None, + enrichment_target: str | None = None, + enrichment_type: str | None = None): """Add a elements from a chemical formula to the material. .. versionadded:: 0.12 @@ -930,7 +928,7 @@ class Material(IDManagerMixin): def make_isotropic_in_lab(self): self.isotropic = [x.name for x in self._nuclides] - def get_elements(self) -> List[str]: + def get_elements(self) -> list[str]: """Returns all elements in the material .. versionadded:: 0.12 @@ -944,7 +942,7 @@ class Material(IDManagerMixin): return sorted({re.split(r'(\d+)', i)[0] for i in self.get_nuclides()}) - def get_nuclides(self, element: Optional[str] = None) -> List[str]: + def get_nuclides(self, element: str | None = None) -> list[str]: """Returns a list of all nuclides in the material, if the element argument is specified then just nuclides of that element are returned. @@ -974,7 +972,7 @@ class Material(IDManagerMixin): return matching_nuclides - def get_nuclide_densities(self) -> Dict[str, tuple]: + def get_nuclide_densities(self) -> dict[str, tuple]: """Returns all nuclides in the material and their densities Returns @@ -992,7 +990,7 @@ class Material(IDManagerMixin): return nuclides - def get_nuclide_atom_densities(self, nuclide: Optional[str] = None) -> Dict[str, float]: + def get_nuclide_atom_densities(self, nuclide: str | None = None) -> dict[str, float]: """Returns one or all nuclides in the material and their atomic densities in units of atom/b-cm @@ -1078,7 +1076,7 @@ class Material(IDManagerMixin): return nuclides def get_activity(self, units: str = 'Bq/cm3', by_nuclide: bool = False, - volume: Optional[float] = None) -> Union[Dict[str, float], float]: + volume: float | None = None) -> dict[str, float] | float: """Returns the activity of the material or for each nuclide in the material in units of [Bq], [Bq/g] or [Bq/cm3]. @@ -1101,7 +1099,7 @@ class Material(IDManagerMixin): Returns ------- - typing.Union[dict, float] + Union[dict, float] If by_nuclide is True then a dictionary whose keys are nuclide names and values are activity is returned. Otherwise the activity of the material is returned as a float. @@ -1125,7 +1123,7 @@ class Material(IDManagerMixin): return activity if by_nuclide else sum(activity.values()) def get_decay_heat(self, units: str = 'W', by_nuclide: bool = False, - volume: Optional[float] = None) -> Union[Dict[str, float], float]: + volume: float | None = None) -> dict[str, float] | float: """Returns the decay heat of the material or for each nuclide in the material in units of [W], [W/g] or [W/cm3]. @@ -1173,7 +1171,7 @@ class Material(IDManagerMixin): return decayheat if by_nuclide else sum(decayheat.values()) - def get_nuclide_atoms(self, volume: Optional[float] = None) -> Dict[str, float]: + def get_nuclide_atoms(self, volume: float | None = None) -> dict[str, float]: """Return number of atoms of each nuclide in the material .. versionadded:: 0.13.1 @@ -1202,7 +1200,7 @@ class Material(IDManagerMixin): atoms[nuclide] = 1.0e24 * atom_per_bcm * volume return atoms - def get_mass_density(self, nuclide: Optional[str] = None) -> float: + def get_mass_density(self, nuclide: str | None = None) -> float: """Return mass density of one or all nuclides Parameters @@ -1224,7 +1222,7 @@ class Material(IDManagerMixin): mass_density += density_i return mass_density - def get_mass(self, nuclide: Optional[str] = None, volume: Optional[float] = None) -> float: + def get_mass(self, nuclide: str | None = None, volume: float | None = None) -> float: """Return mass of one or all nuclides. Note that this method requires that the :attr:`Material.volume` has @@ -1254,7 +1252,7 @@ class Material(IDManagerMixin): raise ValueError("Volume must be set in order to determine mass.") return volume*self.get_mass_density(nuclide) - def clone(self, memo: Optional[dict] = None) -> Material: + def clone(self, memo: dict | None = None) -> Material: """Create a copy of this material with a new unique ID. Parameters @@ -1311,8 +1309,8 @@ class Material(IDManagerMixin): return xml_element def _get_nuclides_xml( - self, nuclides: typing.Iterable[NuclideTuple], - nuclides_to_ignore: Optional[typing.Iterable[str]] = None)-> List[ET.Element]: + self, nuclides: Iterable[NuclideTuple], + nuclides_to_ignore: Iterable[str] | None = None)-> list[ET.Element]: xml_elements = [] # Remove any nuclides to ignore from the XML export @@ -1324,7 +1322,7 @@ class Material(IDManagerMixin): return xml_elements def to_xml_element( - self, nuclides_to_ignore: Optional[typing.Iterable[str]] = None) -> ET.Element: + self, nuclides_to_ignore: Iterable[str] | None = None) -> ET.Element: """Return XML representation of the material Parameters @@ -1398,8 +1396,8 @@ class Material(IDManagerMixin): return element @classmethod - def mix_materials(cls, materials, fracs: typing.Iterable[float], - percent_type: str = 'ao', name: Optional[str] = None) -> Material: + def mix_materials(cls, materials, fracs: Iterable[float], + percent_type: str = 'ao', name: str | None = None) -> Material: """Mix materials together based on atom, weight, or volume fractions .. versionadded:: 0.12 @@ -1596,7 +1594,7 @@ class Materials(cv.CheckedList): self += materials @property - def cross_sections(self) -> Optional[Path]: + def cross_sections(self) -> Path | None: return self._cross_sections @cross_sections.setter @@ -1686,7 +1684,7 @@ class Materials(cv.CheckedList): file.write(indentation) def export_to_xml(self, path: PathLike = 'materials.xml', - nuclides_to_ignore: Optional[typing.Iterable[str]] = None): + nuclides_to_ignore: Iterable[str] | None = None): """Export material collection to an XML file. Parameters diff --git a/openmc/mesh.py b/openmc/mesh.py index ee58ec3b8..85af6a009 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -1,14 +1,12 @@ from __future__ import annotations -import typing import warnings from abc import ABC, abstractmethod -from collections.abc import Iterable +from collections.abc import Iterable, Sequence from functools import wraps from math import pi, sqrt, atan2 from numbers import Integral, Real from pathlib import Path import tempfile -from typing import Optional, Sequence, Tuple, List import h5py import lxml.etree as ET @@ -49,7 +47,7 @@ class MeshBase(IDManagerMixin, ABC): next_id = 1 used_ids = set() - def __init__(self, mesh_id: Optional[int] = None, name: str = ''): + def __init__(self, mesh_id: int | None = None, name: str = ''): # Initialize Mesh class attributes self.id = mesh_id self.name = name @@ -151,10 +149,10 @@ class MeshBase(IDManagerMixin, ABC): self, model: openmc.Model, n_samples: int = 10_000, - prn_seed: Optional[int] = None, + prn_seed: int | None = None, include_void: bool = True, **kwargs - ) -> List[openmc.Material]: + ) -> list[openmc.Material]: """Generate homogenized materials over each element in a mesh. .. versionadded:: 0.15.0 @@ -393,7 +391,7 @@ class StructuredMesh(MeshBase): def write_data_to_vtk(self, filename: PathLike, - datasets: Optional[dict] = None, + datasets: dict | None = None, volume_normalization: bool = True, curvilinear: bool = False): """Creates a VTK object of the mesh @@ -642,7 +640,7 @@ class RegularMesh(StructuredMesh): """ - def __init__(self, mesh_id: Optional[int] = None, name: str = ''): + def __init__(self, mesh_id: int | None = None, name: str = ''): super().__init__(mesh_id, name) self._dimension = None @@ -655,7 +653,7 @@ class RegularMesh(StructuredMesh): return tuple(self._dimension) @dimension.setter - def dimension(self, dimension: typing.Iterable[int]): + def dimension(self, dimension: Iterable[int]): cv.check_type('mesh dimension', dimension, Iterable, Integral) cv.check_length('mesh dimension', dimension, 1, 3) self._dimension = dimension @@ -672,7 +670,7 @@ class RegularMesh(StructuredMesh): return self._lower_left @lower_left.setter - def lower_left(self, lower_left: typing.Iterable[Real]): + def lower_left(self, lower_left: Iterable[Real]): cv.check_type('mesh lower_left', lower_left, Iterable, Real) cv.check_length('mesh lower_left', lower_left, 1, 3) self._lower_left = lower_left @@ -692,7 +690,7 @@ class RegularMesh(StructuredMesh): return [l + w * d for l, w, d in zip(ls, ws, dims)] @upper_right.setter - def upper_right(self, upper_right: typing.Iterable[Real]): + def upper_right(self, upper_right: Iterable[Real]): cv.check_type('mesh upper_right', upper_right, Iterable, Real) cv.check_length('mesh upper_right', upper_right, 1, 3) self._upper_right = upper_right @@ -716,7 +714,7 @@ class RegularMesh(StructuredMesh): return [(u - l) / d for u, l, d in zip(us, ls, dims)] @width.setter - def width(self, width: typing.Iterable[Real]): + def width(self, width: Iterable[Real]): cv.check_type('mesh width', width, Iterable, Real) cv.check_length('mesh width', width, 1, 3) self._width = width @@ -815,7 +813,7 @@ class RegularMesh(StructuredMesh): cls, lattice: 'openmc.RectLattice', division: int = 1, - mesh_id: Optional[int] = None, + mesh_id: int | None = None, name: str = '' ): """Create mesh from an existing rectangular lattice @@ -853,9 +851,9 @@ class RegularMesh(StructuredMesh): @classmethod def from_domain( cls, - domain: typing.Union['openmc.Cell', 'openmc.Region', 'openmc.Universe', 'openmc.Geometry'], + domain: 'openmc.Cell' | 'openmc.Region' | 'openmc.Universe' | 'openmc.Geometry', dimension: Sequence[int] = (10, 10, 10), - mesh_id: Optional[int] = None, + mesh_id: int | None = None, name: str = '' ): """Create mesh from an existing openmc cell, region, universe or @@ -962,7 +960,7 @@ class RegularMesh(StructuredMesh): return mesh - def build_cells(self, bc: Optional[str] = None): + def build_cells(self, bc: str | None = None): """Generates a lattice of universes with the same dimensionality as the mesh object. The individual cells/universes produced will not have material definitions applied and so downstream code @@ -1363,7 +1361,7 @@ class CylindricalMesh(StructuredMesh): z_grid: Sequence[float], phi_grid: Sequence[float] = (0, 2*pi), origin: Sequence[float] = (0., 0., 0.), - mesh_id: Optional[int] = None, + mesh_id: int | None = None, name: str = '', ): super().__init__(mesh_id, name) @@ -1484,7 +1482,7 @@ class CylindricalMesh(StructuredMesh): def get_indices_at_coords( self, coords: Sequence[float] - ) -> Tuple[int, int, int]: + ) -> tuple[int, int, int]: """Finds the index of the mesh voxel at the specified x,y,z coordinates. .. versionadded:: 0.15.0 @@ -1496,7 +1494,7 @@ class CylindricalMesh(StructuredMesh): Returns ------- - Tuple[int, int, int] + tuple[int, int, int] The r, phi, z indices """ @@ -1562,9 +1560,9 @@ class CylindricalMesh(StructuredMesh): @classmethod def from_domain( cls, - domain: typing.Union['openmc.Cell', 'openmc.Region', 'openmc.Universe', 'openmc.Geometry'], + domain: 'openmc.Cell' | 'openmc.Region' | 'openmc.Universe' | 'openmc.Geometry', dimension: Sequence[int] = (10, 10, 10), - mesh_id: Optional[int] = None, + mesh_id: int | None = None, phi_grid_bounds: Sequence[float] = (0.0, 2*pi), name: str = '' ): @@ -1813,7 +1811,7 @@ class SphericalMesh(StructuredMesh): phi_grid: Sequence[float] = (0, 2*pi), theta_grid: Sequence[float] = (0, pi), origin: Sequence[float] = (0., 0., 0.), - mesh_id: Optional[int] = None, + mesh_id: int | None = None, name: str = '', ): super().__init__(mesh_id, name) @@ -2139,9 +2137,9 @@ class UnstructuredMesh(MeshBase): _LINEAR_TET = 0 _LINEAR_HEX = 1 - def __init__(self, filename: PathLike, library: str, mesh_id: Optional[int] = None, + def __init__(self, filename: PathLike, library: str, mesh_id: int | None = None, name: str = '', length_multiplier: float = 1.0, - options: Optional[str] = None): + options: str | None = None): super().__init__(mesh_id, name) self.filename = filename self._volumes = None @@ -2173,11 +2171,11 @@ class UnstructuredMesh(MeshBase): self._library = lib @property - def options(self) -> Optional[str]: + def options(self) -> str | None: return self._options @options.setter - def options(self, options: Optional[str]): + def options(self, options: str | None): cv.check_type('options', options, (str, type(None))) self._options = options @@ -2215,7 +2213,7 @@ class UnstructuredMesh(MeshBase): return self._volumes @volumes.setter - def volumes(self, volumes: typing.Iterable[Real]): + def volumes(self, volumes: Iterable[Real]): cv.check_type("Unstructured mesh volumes", volumes, Iterable, Real) self._volumes = volumes @@ -2353,8 +2351,8 @@ class UnstructuredMesh(MeshBase): def write_data_to_vtk( self, - filename: Optional[PathLike] = None, - datasets: Optional[dict] = None, + filename: PathLike | None = None, + datasets: dict | None = None, volume_normalization: bool = True ): """Map data to unstructured VTK mesh elements. diff --git a/openmc/model/model.py b/openmc/model/model.py index 2160c97e6..2ea579ab7 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -6,7 +6,6 @@ from pathlib import Path from numbers import Integral from tempfile import NamedTemporaryFile import warnings -from typing import Optional, Dict import h5py import lxml.etree as ET @@ -83,7 +82,7 @@ class Model: self.plots = plots @property - def geometry(self) -> Optional[openmc.Geometry]: + def geometry(self) -> openmc.Geometry | None: return self._geometry @geometry.setter @@ -92,7 +91,7 @@ class Model: self._geometry = geometry @property - def materials(self) -> Optional[openmc.Materials]: + def materials(self) -> openmc.Materials | None: return self._materials @materials.setter @@ -106,7 +105,7 @@ class Model: self._materials.append(mat) @property - def settings(self) -> Optional[openmc.Settings]: + def settings(self) -> openmc.Settings | None: return self._settings @settings.setter @@ -115,7 +114,7 @@ class Model: self._settings = settings @property - def tallies(self) -> Optional[openmc.Tallies]: + def tallies(self) -> openmc.Tallies | None: return self._tallies @tallies.setter @@ -129,7 +128,7 @@ class Model: self._tallies.append(tally) @property - def plots(self) -> Optional[openmc.Plots]: + def plots(self) -> openmc.Plots | None: return self._plots @plots.setter @@ -169,7 +168,7 @@ class Model: @property @lru_cache(maxsize=None) - def _cells_by_name(self) -> Dict[int, openmc.Cell]: + def _cells_by_name(self) -> dict[int, openmc.Cell]: # Get the names maps, but since names are not unique, store a set for # each name key. In this way when the user requests a change by a name, # the change will be applied to all of the same name. @@ -182,7 +181,7 @@ class Model: @property @lru_cache(maxsize=None) - def _materials_by_name(self) -> Dict[int, openmc.Material]: + def _materials_by_name(self) -> dict[int, openmc.Material]: if self.materials is None: mats = self.geometry.get_all_materials().values() else: diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index 2945f73b7..29411fe46 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -1,12 +1,11 @@ from abc import ABC, abstractmethod -from collections.abc import Iterable +from collections.abc import Iterable, Sequence from copy import copy from functools import partial from math import sqrt, pi, sin, cos, isclose from numbers import Real import warnings import operator -from typing import Sequence import numpy as np from scipy.spatial import ConvexHull, Delaunay diff --git a/openmc/plots.py b/openmc/plots.py index 0d9dca30e..7532d9d5c 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -1,7 +1,6 @@ from collections.abc import Iterable, Mapping from numbers import Integral, Real from pathlib import Path -from typing import Optional import h5py import lxml.etree as ET @@ -942,7 +941,7 @@ class Plot(PlotBase): # Return produced image return _get_plot_image(self, cwd) - def to_vtk(self, output: Optional[PathLike] = None, + def to_vtk(self, output: PathLike | None = None, openmc_exec: str = 'openmc', cwd: str = '.'): """Render plot as an voxel image diff --git a/openmc/settings.py b/openmc/settings.py index 807662581..ce97f138f 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -4,8 +4,6 @@ import itertools from math import ceil from numbers import Integral, Real from pathlib import Path -import typing # required to prevent typing.Union namespace overwriting Union -from typing import Optional import lxml.etree as ET @@ -514,7 +512,7 @@ class Settings: return self._max_order @max_order.setter - def max_order(self, max_order: Optional[int]): + def max_order(self, max_order: int | None): if max_order is not None: cv.check_type('maximum scattering order', max_order, Integral) cv.check_greater_than('maximum scattering order', max_order, 0, @@ -522,11 +520,11 @@ class Settings: self._max_order = max_order @property - def source(self) -> typing.List[SourceBase]: + def source(self) -> list[SourceBase]: return self._source @source.setter - def source(self, source: typing.Union[SourceBase, typing.Iterable[SourceBase]]): + def source(self, source: SourceBase | Iterable[SourceBase]): if not isinstance(source, MutableSequence): source = [source] self._source = cv.CheckedList(SourceBase, 'source distributions', source) @@ -804,7 +802,7 @@ class Settings: self._temperature = temperature @property - def trace(self) -> typing.Iterable: + def trace(self) -> Iterable: return self._trace @trace.setter @@ -817,11 +815,11 @@ class Settings: self._trace = trace @property - def track(self) -> typing.Iterable[typing.Iterable[int]]: + def track(self) -> Iterable[Iterable[int]]: return self._track @track.setter - def track(self, track: typing.Iterable[typing.Iterable[int]]): + def track(self, track: Iterable[Iterable[int]]): cv.check_type('track', track, Sequence) for t in track: if len(t) != 3: @@ -904,12 +902,12 @@ class Settings: self._resonance_scattering = res @property - def volume_calculations(self) -> typing.List[VolumeCalculation]: + def volume_calculations(self) -> list[VolumeCalculation]: return self._volume_calculations @volume_calculations.setter def volume_calculations( - self, vol_calcs: typing.Union[VolumeCalculation, typing.Iterable[VolumeCalculation]] + self, vol_calcs: VolumeCalculation | Iterable[VolumeCalculation] ): if not isinstance(vol_calcs, MutableSequence): vol_calcs = [vol_calcs] @@ -1003,11 +1001,11 @@ class Settings: self._write_initial_source = value @property - def weight_windows(self) -> typing.List[WeightWindows]: + def weight_windows(self) -> list[WeightWindows]: return self._weight_windows @weight_windows.setter - def weight_windows(self, value: typing.Union[WeightWindows, typing.Iterable[WeightWindows]]): + def weight_windows(self, value: WeightWindows | Iterable[WeightWindows]): if not isinstance(value, MutableSequence): value = [value] self._weight_windows = cv.CheckedList(WeightWindows, 'weight windows', value) @@ -1056,7 +1054,7 @@ class Settings: self._max_tracks = value @property - def weight_windows_file(self) -> Optional[PathLike]: + def weight_windows_file(self) -> PathLike | None: return self._weight_windows_file @weight_windows_file.setter @@ -1065,7 +1063,7 @@ class Settings: self._weight_windows_file = value @property - def weight_window_generators(self) -> typing.List[WeightWindowGenerator]: + def weight_window_generators(self) -> list[WeightWindowGenerator]: return self._weight_window_generators @weight_window_generators.setter diff --git a/openmc/source.py b/openmc/source.py index 51ed74de3..e35e62f5f 100644 --- a/openmc/source.py +++ b/openmc/source.py @@ -1,12 +1,10 @@ from __future__ import annotations from abc import ABC, abstractmethod -from collections.abc import Iterable +from collections.abc import Iterable, Sequence from enum import IntEnum from numbers import Real import warnings -import typing # imported separately as py3.8 requires typing.Iterable -# also required to prevent typing.Union namespace overwriting Union -from typing import Optional, Sequence, Dict, Any +from typing import Any import lxml.etree as ET import numpy as np @@ -57,8 +55,8 @@ class SourceBase(ABC): def __init__( self, - strength: Optional[float] = 1.0, - constraints: Optional[Dict[str, Any]] = None + strength: float | None = 1.0, + constraints: dict[str, Any] | None = None ): self.strength = strength self.constraints = constraints @@ -75,11 +73,11 @@ class SourceBase(ABC): self._strength = strength @property - def constraints(self) -> Dict[str, Any]: + def constraints(self) -> dict[str, Any]: return self._constraints @constraints.setter - def constraints(self, constraints: Optional[Dict[str, Any]]): + def constraints(self, constraints: dict[str, Any] | None): self._constraints = {} if constraints is None: return @@ -200,7 +198,7 @@ class SourceBase(ABC): raise ValueError(f'Source type {source_type} is not recognized') @staticmethod - def _get_constraints(elem: ET.Element) -> Dict[str, Any]: + def _get_constraints(elem: ET.Element) -> dict[str, Any]: # Find element containing constraints constraints_elem = elem.find("constraints") elem = constraints_elem if constraints_elem is not None else elem @@ -308,14 +306,14 @@ class IndependentSource(SourceBase): def __init__( self, - space: Optional[openmc.stats.Spatial] = None, - angle: Optional[openmc.stats.UnitSphere] = None, - energy: Optional[openmc.stats.Univariate] = None, - time: Optional[openmc.stats.Univariate] = None, + space: openmc.stats.Spatial | None = None, + angle: openmc.stats.UnitSphere | None = None, + energy: openmc.stats.Univariate | None = None, + time: openmc.stats.Univariate | None = None, strength: float = 1.0, particle: str = 'neutron', - domains: Optional[Sequence[typing.Union[openmc.Cell, openmc.Material, openmc.Universe]]] = None, - constraints: Optional[Dict[str, Any]] = None + domains: Sequence[openmc.Cell | openmc.Material | openmc.Universe] | None = None, + constraints: dict[str, Any] | None = None ): if domains is not None: warnings.warn("The 'domains' arguments has been replaced by the " @@ -528,7 +526,7 @@ class MeshSource(SourceBase): self, mesh: MeshBase, sources: Sequence[SourceBase], - constraints: Optional[Dict[str, Any]] = None, + constraints: dict[str, Any] | None = None, ): super().__init__(strength=None, constraints=constraints) self.mesh = mesh @@ -702,10 +700,10 @@ class CompiledSource(SourceBase): """ def __init__( self, - library: Optional[str] = None, - parameters: Optional[str] = None, + library: str | None = None, + parameters: str | None = None, strength: float = 1.0, - constraints: Optional[Dict[str, Any]] = None + constraints: dict[str, Any] | None = None ) -> None: super().__init__(strength=strength, constraints=constraints) @@ -829,9 +827,9 @@ class FileSource(SourceBase): def __init__( self, - path: Optional[PathLike] = None, + path: PathLike | None = None, strength: float = 1.0, - constraints: Optional[Dict[str, Any]] = None + constraints: dict[str, Any] | None = None ): super().__init__(strength=strength, constraints=constraints) self._path = None @@ -966,8 +964,8 @@ class SourceParticle: """ def __init__( self, - r: typing.Iterable[float] = (0., 0., 0.), - u: typing.Iterable[float] = (0., 0., 1.), + r: Iterable[float] = (0., 0., 0.), + u: Iterable[float] = (0., 0., 1.), E: float = 1.0e6, time: float = 0.0, wgt: float = 1.0, @@ -1003,7 +1001,7 @@ class SourceParticle: def write_source_file( - source_particles: typing.Iterable[SourceParticle], + source_particles: Iterable[SourceParticle], filename: PathLike, **kwargs ): """Write a source file using a collection of source particles @@ -1046,7 +1044,7 @@ def write_source_file( fh.create_dataset('source_bank', data=arr, dtype=source_dtype) -def read_source_file(filename: PathLike) -> typing.List[SourceParticle]: +def read_source_file(filename: PathLike) -> list[SourceParticle]: """Read a source file and return a list of source particles. .. versionadded:: 0.15.0 diff --git a/openmc/stats/multivariate.py b/openmc/stats/multivariate.py index ce9740ad2..06c65896f 100644 --- a/openmc/stats/multivariate.py +++ b/openmc/stats/multivariate.py @@ -1,7 +1,6 @@ from __future__ import annotations -import typing from abc import ABC, abstractmethod -from collections.abc import Iterable +from collections.abc import Iterable, Sequence from math import cos, pi from numbers import Real from warnings import warn @@ -9,6 +8,7 @@ from warnings import warn import lxml.etree as ET import numpy as np +import openmc import openmc.checkvalue as cv from .._xml import get_text from ..mesh import MeshBase @@ -212,7 +212,7 @@ class Monodirectional(UnitSphere): """ - def __init__(self, reference_uvw: typing.Sequence[float] = [1., 0., 0.]): + def __init__(self, reference_uvw: Sequence[float] = [1., 0., 0.]): super().__init__(reference_uvw) def to_xml_element(self): @@ -789,8 +789,8 @@ class Box(Spatial): def __init__( self, - lower_left: typing.Sequence[float], - upper_right: typing.Sequence[float], + lower_left: Sequence[float], + upper_right: Sequence[float], only_fissionable: bool = False ): self.lower_left = lower_left @@ -889,7 +889,7 @@ class Point(Spatial): """ - def __init__(self, xyz: typing.Sequence[float] = (0., 0., 0.)): + def __init__(self, xyz: Sequence[float] = (0., 0., 0.)): self.xyz = xyz @property @@ -939,9 +939,9 @@ class Point(Spatial): def spherical_uniform( r_outer: float, r_inner: float = 0.0, - thetas: typing.Sequence[float] = (0., pi), - phis: typing.Sequence[float] = (0., 2*pi), - origin: typing.Sequence[float] = (0., 0., 0.) + thetas: Sequence[float] = (0., pi), + phis: Sequence[float] = (0., 2*pi), + origin: Sequence[float] = (0., 0., 0.) ): """Return a uniform spatial distribution over a spherical shell. diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index 4069c5a9e..2d93b1f1b 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -1,9 +1,8 @@ from __future__ import annotations import math -import typing from abc import ABC, abstractmethod from collections import defaultdict -from collections.abc import Iterable +from collections.abc import Iterable, Sequence from copy import deepcopy from numbers import Real from warnings import warn @@ -68,7 +67,7 @@ class Univariate(EqualityMixin, ABC): return Mixture.from_xml_element(elem) @abstractmethod - def sample(n_samples: int = 1, seed: typing.Optional[int] = None): + def sample(n_samples: int = 1, seed: int | None = None): """Sample the univariate distribution Parameters @@ -210,8 +209,8 @@ class Discrete(Univariate): @classmethod def merge( cls, - dists: typing.Sequence[Discrete], - probs: typing.Sequence[int] + dists: Sequence[Discrete], + probs: Sequence[int] ): """Merge multiple discrete distributions into a single distribution @@ -859,8 +858,8 @@ class Tabular(Univariate): def __init__( self, - x: typing.Sequence[float], - p: typing.Sequence[float], + x: Sequence[float], + p: Sequence[float], interpolation: str = 'linear-linear', ignore_negative: bool = False ): @@ -958,7 +957,7 @@ class Tabular(Univariate): """Normalize the probabilities stored on the distribution""" self._p /= self.cdf().max() - def sample(self, n_samples: int = 1, seed: typing.Optional[int] = None): + def sample(self, n_samples: int = 1, seed: int | None = None): rng = np.random.RandomState(seed) xi = rng.random(n_samples) @@ -1100,7 +1099,7 @@ class Legendre(Univariate): """ - def __init__(self, coefficients: typing.Sequence[float]): + def __init__(self, coefficients: Sequence[float]): self.coefficients = coefficients self._legendre_poly = None @@ -1156,8 +1155,8 @@ class Mixture(Univariate): def __init__( self, - probability: typing.Sequence[float], - distribution: typing.Sequence[Univariate] + probability: Sequence[float], + distribution: Sequence[Univariate] ): self.probability = probability self.distribution = distribution @@ -1319,8 +1318,8 @@ class Mixture(Univariate): def combine_distributions( - dists: typing.Sequence[Univariate], - probs: typing.Sequence[float] + dists: Sequence[Univariate], + probs: Sequence[float] ): """Combine distributions with specified probabilities diff --git a/openmc/utility_funcs.py b/openmc/utility_funcs.py index 4eb307c93..3dff45380 100644 --- a/openmc/utility_funcs.py +++ b/openmc/utility_funcs.py @@ -2,12 +2,11 @@ from contextlib import contextmanager import os from pathlib import Path from tempfile import TemporaryDirectory -from typing import Optional from .checkvalue import PathLike @contextmanager -def change_directory(working_dir: Optional[PathLike] = None, *, tmpdir: bool = False): +def change_directory(working_dir: PathLike | None = None, *, tmpdir: bool = False): """Context manager for executing in a provided working directory Parameters diff --git a/openmc/weight_windows.py b/openmc/weight_windows.py index 96f1db892..a10fd2a65 100644 --- a/openmc/weight_windows.py +++ b/openmc/weight_windows.py @@ -1,6 +1,6 @@ from __future__ import annotations from numbers import Real, Integral -from typing import Iterable, List, Optional, Dict, Sequence +from collections.abc import Iterable, Sequence import warnings import lxml.etree as ET @@ -110,15 +110,15 @@ class WeightWindows(IDManagerMixin): self, mesh: MeshBase, lower_ww_bounds: Iterable[float], - upper_ww_bounds: Optional[Iterable[float]] = None, - upper_bound_ratio: Optional[float] = None, - energy_bounds: Optional[Iterable[Real]] = None, + upper_ww_bounds: Iterable[float] | None = None, + upper_bound_ratio: float | None = None, + energy_bounds: Iterable[Real] | None = None, particle_type: str = 'neutron', survival_ratio: float = 3, - max_lower_bound_ratio: Optional[float] = None, + max_lower_bound_ratio: float | None = None, max_split: int = 10, weight_cutoff: float = 1.e-38, - id: Optional[int] = None + id: int | None = None ): self.mesh = mesh self.id = id @@ -353,7 +353,7 @@ class WeightWindows(IDManagerMixin): return element @classmethod - def from_xml_element(cls, elem: ET.Element, meshes: Dict[int, MeshBase]) -> WeightWindows: + def from_xml_element(cls, elem: ET.Element, meshes: dict[int, MeshBase]) -> WeightWindows: """Generate weight window settings from an XML element Parameters @@ -407,7 +407,7 @@ class WeightWindows(IDManagerMixin): ) @classmethod - def from_hdf5(cls, group: h5py.Group, meshes: Dict[int, MeshBase]) -> WeightWindows: + def from_hdf5(cls, group: h5py.Group, meshes: dict[int, MeshBase]) -> WeightWindows: """Create weight windows from HDF5 group Parameters @@ -457,7 +457,7 @@ class WeightWindows(IDManagerMixin): ) -def wwinp_to_wws(path: PathLike) -> List[WeightWindows]: +def wwinp_to_wws(path: PathLike) -> list[WeightWindows]: """Create WeightWindows instances from a wwinp file .. versionadded:: 0.13.1 @@ -698,7 +698,7 @@ class WeightWindowGenerator: def __init__( self, mesh: openmc.MeshBase, - energy_bounds: Optional[Sequence[float]] = None, + energy_bounds: Sequence[float] | None = None, particle_type: str = 'neutron', method: str = 'magic', max_realizations: int = 1, From e5cc925db3ce5685cfd5afeb3822050ff8720824 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Wed, 24 Jul 2024 03:42:03 +0100 Subject: [PATCH 152/671] removed unused which function in CI scripts (#3095) --- tools/ci/gha-install.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tools/ci/gha-install.py b/tools/ci/gha-install.py index f4b2fbb31..f046e8634 100644 --- a/tools/ci/gha-install.py +++ b/tools/ci/gha-install.py @@ -2,22 +2,6 @@ import os import shutil import subprocess -def which(program): - def is_exe(fpath): - return os.path.isfile(fpath) and os.access(fpath, os.X_OK) - - fpath, fname = os.path.split(program) - if fpath: - if is_exe(program): - return program - else: - for path in os.environ["PATH"].split(os.pathsep): - path = path.strip('"') - exe_file = os.path.join(path, program) - if is_exe(exe_file): - return exe_file - return None - def install(omp=False, mpi=False, phdf5=False, dagmc=False, libmesh=False, ncrystal=False): # Create build directory and change to it From 0ad003307a67e5eb6c161fed2f3c6b571649de79 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Wed, 24 Jul 2024 13:27:39 +0100 Subject: [PATCH 153/671] packages used for testing moved to tests section of pyprojects.tom (#3094) --- pyproject.toml | 1 + tools/ci/gha-install.sh | 8 +------- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d1b2c335c..2b2764cea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,7 @@ docs = [ "sphinx-rtd-theme==1.0.0" ] test = ["packaging", "pytest", "pytest-cov", "colorama", "openpyxl"] +ci = ["cpp-coveralls", "coveralls"] vtk = ["vtk"] [project.urls] diff --git a/tools/ci/gha-install.sh b/tools/ci/gha-install.sh index 2cef974d3..87952fda9 100755 --- a/tools/ci/gha-install.sh +++ b/tools/ci/gha-install.sh @@ -48,10 +48,4 @@ fi python tools/ci/gha-install.py # Install Python API in editable mode -pip install -e .[test,vtk] - -# For coverage testing of the C++ source files -pip install cpp-coveralls - -# For coverage testing of the Python source files -pip install coveralls +pip install -e .[test,vtk,ci] From 467b8e99abc62d5361f628ea37e1bcd4afcf3245 Mon Sep 17 00:00:00 2001 From: John Vincent Cauilan <64677361+johvincau@users.noreply.github.com> Date: Tue, 30 Jul 2024 07:27:35 -0500 Subject: [PATCH 154/671] Fix ParticleFilter to work with set inputs (#3092) --- openmc/filter.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openmc/filter.py b/openmc/filter.py index e005140ee..0f884cfcd 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -1,6 +1,6 @@ from __future__ import annotations from abc import ABCMeta -from collections.abc import Iterable +from collections.abc import Iterable, Sequence import hashlib from itertools import product from numbers import Real, Integral @@ -736,7 +736,7 @@ class ParticleFilter(Filter): Parameters ---------- - bins : str, or iterable of str + bins : str, or sequence of str The particles to tally represented as strings ('neutron', 'photon', 'electron', 'positron'). filter_id : int @@ -744,7 +744,7 @@ class ParticleFilter(Filter): Attributes ---------- - bins : iterable of str + bins : sequence of str The particles to tally id : int Unique identifier for the filter @@ -764,8 +764,8 @@ class ParticleFilter(Filter): @Filter.bins.setter def bins(self, bins): - bins = np.atleast_1d(bins) - cv.check_iterable_type('filter bins', bins, str) + cv.check_type('bins', bins, Sequence, str) + bins = np.atleast_1d(bins) for edge in bins: cv.check_value('filter bin', edge, _PARTICLES) self._bins = bins From 9d9b2daceba82354f4720315ee2491b11d1d6e23 Mon Sep 17 00:00:00 2001 From: "Juan M. Valderrama" <119371594+valderrama-juan@users.noreply.github.com> Date: Tue, 30 Jul 2024 14:47:00 -0500 Subject: [PATCH 155/671] Improve description of probabilities for openmc.stats.Tabular class (#3099) Co-authored-by: Paul Romano --- openmc/stats/univariate.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index 2d93b1f1b..a47f4f320 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -837,7 +837,8 @@ class Tabular(Univariate): Tabulated values of the random variable p : Iterable of float Tabulated probabilities. For histogram interpolation, if the length of - `p` is the same as `x`, the last value is ignored. + `p` is the same as `x`, the last value is ignored. Probabilities `p` are + given per unit of `x`. interpolation : {'histogram', 'linear-linear', 'linear-log', 'log-linear', 'log-log'}, optional Indicates how the density function is interpolated between tabulated points. Defaults to 'linear-linear'. @@ -854,6 +855,15 @@ class Tabular(Univariate): Indicates how the density function is interpolated between tabulated points. Defaults to 'linear-linear'. + Notes + ----- + The probabilities `p` are interpreted per unit of the corresponding + independent variable `x`. This follows the definition of a probability + density function (PDF) in probability theory, where the PDF represents the + relative likelihood of the random variable taking on a particular value per + unit of the variable. For example, if `x` represents energy in eV, then `p` + should represent probabilities per eV. + """ def __init__( From 346f751deb61f7f3b373855d1f4b2e7a27ae8228 Mon Sep 17 00:00:00 2001 From: John Vincent Cauilan <64677361+johvincau@users.noreply.github.com> Date: Tue, 13 Aug 2024 16:51:30 -0500 Subject: [PATCH 156/671] Adjust decay data reader to better handle non-normalized branching ratios (#3080) Co-authored-by: Paul Romano --- openmc/deplete/chain.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index 1d3834980..2e81250dc 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -386,24 +386,31 @@ class Chain: if not data.nuclide['stable'] and data.half_life.nominal_value != 0.0: nuclide.half_life = data.half_life.nominal_value nuclide.decay_energy = data.decay_energy.nominal_value - sum_br = 0.0 - for i, mode in enumerate(data.modes): + branch_ratios = [] + branch_ids = [] + for mode in data.modes: type_ = ','.join(mode.modes) if mode.daughter in decay_data: target = mode.daughter else: print('missing {} {} {}'.format( - parent, ','.join(mode.modes), mode.daughter)) + parent, type_, mode.daughter)) target = replace_missing(mode.daughter, decay_data) - - # Write branching ratio, taking care to ensure sum is unity br = mode.branching_ratio.nominal_value - sum_br += br - if i == len(data.modes) - 1 and sum_br != 1.0: - br = 1.0 - sum(m.branching_ratio.nominal_value - for m in data.modes[:-1]) + branch_ratios.append(br) + branch_ids.append((type_, target)) - # Append decay mode + if not math.isclose(sum(branch_ratios), 1.0): + max_br = max(branch_ratios) + max_index = branch_ratios.index(max_br) + + # Adjust maximum branching ratio so they sum to unity + new_br = max_br - sum(branch_ratios) + 1.0 + branch_ratios[max_index] = new_br + assert math.isclose(sum(branch_ratios), 1.0) + + # Append decay modes + for br, (type_, target) in zip(branch_ratios, branch_ids): nuclide.add_decay_mode(type_, target, br) nuclide.sources = data.sources From 3ef54ec349ac7a847df8c0b2de02c2bd2cf3155f Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Wed, 14 Aug 2024 05:43:44 +0100 Subject: [PATCH 157/671] Replacing endf c functions with package (#3101) --- openmc/data/_endf.pyx | 8 ------ openmc/data/endf.c | 57 ------------------------------------------- openmc/data/endf.py | 10 +------- pyproject.toml | 1 + setup.py | 2 +- 5 files changed, 3 insertions(+), 75 deletions(-) delete mode 100644 openmc/data/_endf.pyx delete mode 100644 openmc/data/endf.c diff --git a/openmc/data/_endf.pyx b/openmc/data/_endf.pyx deleted file mode 100644 index 991ee015b..000000000 --- a/openmc/data/_endf.pyx +++ /dev/null @@ -1,8 +0,0 @@ -# cython: c_string_type=str, c_string_encoding=ascii - -cdef extern from "endf.c": - double cfloat_endf(const char* buffer, int n) - -def float_endf(s): - cdef const char* c_string = s - return cfloat_endf(c_string, len(s)) diff --git a/openmc/data/endf.c b/openmc/data/endf.c deleted file mode 100644 index dd5eee541..000000000 --- a/openmc/data/endf.c +++ /dev/null @@ -1,57 +0,0 @@ -#include - -//! Convert string representation of a floating point number into a double -// -//! This function handles converting floating point numbers from an ENDF 11 -//! character field into a double, covering all the corner cases. Floating point -//! numbers are allowed to contain whitespace (which is ignored). Also, in -//! exponential notation, it allows the 'e' to be omitted. A field containing -//! only whitespace is to be interpreted as a zero. -// -//! \param buffer character input from an ENDF file -//! \param n Length of character input -//! \return Floating point number - -double cfloat_endf(const char* buffer, int n) -{ - char arr[13]; // 11 characters plus e and a null terminator - int j = 0; // current position in arr - int found_significand = 0; - int found_exponent = 0; - - // limit n to 11 characters - n = n > 11 ? 11 : n; - - int i; - for (i = 0; i < n; ++i) { - char c = buffer[i]; - - // Skip whitespace characters - if (c == ' ') continue; - - if (found_significand) { - if (!found_exponent) { - if (c == '+' || c == '-') { - // In the case that we encounter +/- and we haven't yet encountered - // e/E, we manually add it - arr[j++] = 'e'; - found_exponent = 1; - - } else if (c == 'e' || c == 'E' || c == 'd' || c == 'D') { - arr[j++] = 'e'; - found_exponent = 1; - continue; - } - } - } else if (c == '.' || (c >= '0' && c <= '9')) { - found_significand = 1; - } - - // Copy character - arr[j++] = c; - } - - // Done copying. Add null terminator and convert to double - arr[j] = '\0'; - return atof(arr); -} diff --git a/openmc/data/endf.py b/openmc/data/endf.py index 73299723a..63cd092eb 100644 --- a/openmc/data/endf.py +++ b/openmc/data/endf.py @@ -14,11 +14,7 @@ import numpy as np from .data import gnds_name from .function import Tabulated1D -try: - from ._endf import float_endf - _CYTHON = True -except ImportError: - _CYTHON = False +from endf.records import float_endf _LIBRARY = {0: 'ENDF/B', 1: 'ENDF/A', 2: 'JEFF', 3: 'EFF', @@ -91,10 +87,6 @@ def py_float_endf(s): return float(ENDF_FLOAT_RE.sub(r'\1e\2\3', s)) -if not _CYTHON: - float_endf = py_float_endf - - def int_endf(s): """Convert string of integer number in ENDF to int. diff --git a/pyproject.toml b/pyproject.toml index 2b2764cea..98b1d152f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ dependencies = [ "lxml", "uncertainties", "setuptools", + "endf", ] [project.optional-dependencies] diff --git a/setup.py b/setup.py index 4e24f48a1..88a45a360 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ from Cython.Build import cythonize kwargs = { - # Cython is used to add resonance reconstruction and fast float_endf + # Cython is used to add resonance reconstruction 'ext_modules': cythonize('openmc/data/*.pyx'), 'include_dirs': [np.get_include()] } From ae245e0fb7b4dc20df389d42a1b31f956e05f024 Mon Sep 17 00:00:00 2001 From: pitkajuh Date: Wed, 14 Aug 2024 06:52:15 +0000 Subject: [PATCH 158/671] Ensure RegularMesh repr shows value for width of the mesh (#3100) Co-authored-by: Paul Romano --- openmc/mesh.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openmc/mesh.py b/openmc/mesh.py index 85af6a009..a706b8fa8 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -787,8 +787,8 @@ class RegularMesh(StructuredMesh): string += '{0: <16}{1}{2}\n'.format('\tDimensions', '=\t', self.n_dimension) string += '{0: <16}{1}{2}\n'.format('\tVoxels', '=\t', self._dimension) string += '{0: <16}{1}{2}\n'.format('\tLower left', '=\t', self._lower_left) - string += '{0: <16}{1}{2}\n'.format('\tUpper Right', '=\t', self._upper_right) - string += '{0: <16}{1}{2}\n'.format('\tWidth', '=\t', self._width) + string += '{0: <16}{1}{2}\n'.format('\tUpper Right', '=\t', self.upper_right) + string += '{0: <16}{1}{2}\n'.format('\tWidth', '=\t', self.width) return string @classmethod From 9483cce0bc334467748cd842cb122fefc6f3305d Mon Sep 17 00:00:00 2001 From: Jon Shimwell Date: Wed, 14 Aug 2024 15:34:17 +0100 Subject: [PATCH 159/671] Remove resonance reconstruction and Cython dependency (#3111) Co-authored-by: Paul Romano --- Dockerfile | 2 +- MANIFEST.in | 2 - docs/source/usersguide/install.rst | 4 - openmc/data/function.py | 22 -- openmc/data/neutron.py | 88 +---- openmc/data/njoy.py | 15 +- openmc/data/reconstruct.pyx | 522 -------------------------- pyproject.toml | 3 +- setup.py | 14 - tests/unit_tests/test_data_neutron.py | 27 +- tools/ci/gha-install.sh | 3 +- 11 files changed, 33 insertions(+), 669 deletions(-) delete mode 100644 openmc/data/reconstruct.pyx delete mode 100755 setup.py diff --git a/Dockerfile b/Dockerfile index 35b9cf578..4a94e3c0b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -95,7 +95,7 @@ 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 \ - && pip install --upgrade numpy "cython<3.0" \ + && pip install --upgrade numpy \ # Clone and install EMBREE && mkdir -p $HOME/EMBREE && cd $HOME/EMBREE \ && git clone --single-branch -b ${EMBREE_TAG} --depth 1 ${EMBREE_REPO} \ diff --git a/MANIFEST.in b/MANIFEST.in index afd016cb0..b73218af0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -26,8 +26,6 @@ recursive-include include *.h recursive-include include *.h.in recursive-include include *.hh recursive-include man *.1 -recursive-include openmc *.pyx -recursive-include openmc *.c recursive-include src *.cc recursive-include src *.cpp recursive-include src *.rnc diff --git a/docs/source/usersguide/install.rst b/docs/source/usersguide/install.rst index 130e96c0a..1c0b7fa5b 100644 --- a/docs/source/usersguide/install.rst +++ b/docs/source/usersguide/install.rst @@ -584,10 +584,6 @@ distributions. parallel runs. This package is needed if you plan on running depletion simulations in parallel using MPI. - `Cython `_ - Cython is used for resonance reconstruction for ENDF data converted to - :class:`openmc.data.IncidentNeutron`. - `vtk `_ The Python VTK bindings are needed to convert voxel and track files to VTK format. diff --git a/openmc/data/function.py b/openmc/data/function.py index 23fd5e9d4..c5914f513 100644 --- a/openmc/data/function.py +++ b/openmc/data/function.py @@ -708,28 +708,6 @@ class ResonancesWithBackground(EqualityMixin): self.background = background self.mt = mt - def __call__(self, x): - # Get background cross section - xs = self.background(x) - - for r in self.resonances: - if not isinstance(r, openmc.data.resonance._RESOLVED): - continue - - if isinstance(x, Iterable): - # Determine which energies are within resolved resonance range - within = (r.energy_min <= x) & (x <= r.energy_max) - - # Get resonance cross sections and add to background - resonant_xs = r.reconstruct(x[within]) - xs[within] += resonant_xs[self.mt] - else: - if r.energy_min <= x <= r.energy_max: - resonant_xs = r.reconstruct(x) - xs += resonant_xs[self.mt] - - return xs - @property def background(self): return self._background diff --git a/openmc/data/neutron.py b/openmc/data/neutron.py index 894be1871..95a3424ea 100644 --- a/openmc/data/neutron.py +++ b/openmc/data/neutron.py @@ -16,8 +16,7 @@ from .endf import ( Evaluation, SUM_RULES, get_head_record, get_tab1_record, get_evaluations) from .fission_energy import FissionEnergyRelease from .function import Tabulated1D, Sum, ResonancesWithBackground -from .grid import linearize, thin -from .njoy import make_ace +from .njoy import make_ace, make_pendf from .product import Product from .reaction import Reaction, _get_photon_products_ace, FISSION_MTS from . import resonance as res @@ -286,7 +285,7 @@ class IncidentNeutron(EqualityMixin): if strT in data.urr: self.urr[strT] = data.urr[strT] - def add_elastic_0K_from_endf(self, filename, overwrite=False): + def add_elastic_0K_from_endf(self, filename, overwrite=False, **kwargs): """Append 0K elastic scattering cross section from an ENDF file. Parameters @@ -297,6 +296,8 @@ class IncidentNeutron(EqualityMixin): If existing 0 K data is present, this flag can be used to indicate that it should be overwritten. Otherwise, an exception will be thrown. + **kwargs + Keyword arguments passed to :func:`openmc.data.njoy.make_pendf` Raises ------ @@ -309,75 +310,22 @@ class IncidentNeutron(EqualityMixin): if '0K' in self.energy and not overwrite: raise ValueError('0 K data already exists for this nuclide.') - data = type(self).from_endf(filename) - if data.resonances is not None: - x = [] - y = [] - for rr in data.resonances: - if isinstance(rr, res.RMatrixLimited): - raise TypeError('R-Matrix Limited not supported.') - elif isinstance(rr, res.Unresolved): - continue + with tempfile.TemporaryDirectory() as tmpdir: + # Set arguments for make_pendf + pendf_path = os.path.join(tmpdir, 'pendf') + kwargs.setdefault('output_dir', tmpdir) + kwargs.setdefault('pendf', pendf_path) - # Get energies/widths for resonances - e_peak = rr.parameters['energy'].values - if isinstance(rr, res.MultiLevelBreitWigner): - gamma = rr.parameters['totalWidth'].values - elif isinstance(rr, res.ReichMoore): - df = rr.parameters - gamma = (df['neutronWidth'] + - df['captureWidth'] + - abs(df['fissionWidthA']) + - abs(df['fissionWidthB'])).values + # Run NJOY to create a pointwise ENDF file + make_pendf(filename, **kwargs) - # Determine peak energies and widths - e_min, e_max = rr.energy_min, rr.energy_max - in_range = (e_peak > e_min) & (e_peak < e_max) - e_peak = e_peak[in_range] - gamma = gamma[in_range] - - # Get midpoints between resonances (use min/max energy of - # resolved region as absolute lower/upper bound) - e_mid = np.concatenate( - ([e_min], (e_peak[1:] + e_peak[:-1])/2, [e_max])) - - # Add grid around each resonance that includes the peak +/- the - # width times each value in _RESONANCE_ENERGY_GRID. Values are - # constrained so that points around one resonance don't overlap - # with points around another. This algorithm is from Fudge - # (https://doi.org/10.1063/1.1945057). - energies = [] - for e, g, e_lower, e_upper in zip(e_peak, gamma, e_mid[:-1], - e_mid[1:]): - e_left = e - g*_RESONANCE_ENERGY_GRID - energies.append(e_left[e_left > e_lower][::-1]) - e_right = e + g*_RESONANCE_ENERGY_GRID[1:] - energies.append(e_right[e_right < e_upper]) - - # Concatenate all points - energies = np.concatenate(energies) - - # Create 1000 equal log-spaced energies over RRR, combine with - # resonance peaks and half-height energies - e_log = np.logspace(log10(e_min), log10(e_max), 1000) - energies = np.union1d(e_log, energies) - - # Linearize and thin cross section - xi, yi = linearize(energies, data[2].xs['0K']) - xi, yi = thin(xi, yi) - - # If there are multiple resolved resonance ranges (e.g. Pu239 in - # ENDF/B-VII.1), combine them - x = np.concatenate((x, xi)) - y = np.concatenate((y, yi)) - else: - energies = data[2].xs['0K'].x - x, y = linearize(energies, data[2].xs['0K']) - x, y = thin(x, y) - - # Set 0K energy grid and elastic scattering cross section - self.energy['0K'] = x - self[2].xs['0K'] = Tabulated1D(x, y) + # Add 0K elastic scattering cross section + pendf = Evaluation(pendf_path) + file_obj = StringIO(pendf.section[3, 2]) + get_head_record(file_obj) + params, xs = get_tab1_record(file_obj) + self.energy['0K'] = xs.x + self[2].xs['0K'] = xs def get_reaction_components(self, mt): """Determine what reactions make up redundant reaction. diff --git a/openmc/data/njoy.py b/openmc/data/njoy.py index ac1b5e345..1bf44891e 100644 --- a/openmc/data/njoy.py +++ b/openmc/data/njoy.py @@ -221,7 +221,7 @@ def run(commands, tapein, tapeout, input_filename=None, stdout=False, shutil.move(tmpfilename, str(filename)) -def make_pendf(filename, pendf='pendf', error=0.001, stdout=False): +def make_pendf(filename, pendf='pendf', **kwargs): """Generate pointwise ENDF file from an ENDF file Parameters @@ -230,10 +230,9 @@ def make_pendf(filename, pendf='pendf', error=0.001, stdout=False): Path to ENDF file pendf : str, optional Path of pointwise ENDF file to write - error : float, optional - Fractional error tolerance for NJOY processing - stdout : bool - Whether to display NJOY standard output + **kwargs + Keyword arguments passed to :func:`openmc.data.njoy.make_ace`. All NJOY + module arguments other than pendf default to False. Raises ------ @@ -241,9 +240,9 @@ def make_pendf(filename, pendf='pendf', error=0.001, stdout=False): If the NJOY process returns with a non-zero status """ - - make_ace(filename, pendf=pendf, error=error, broadr=False, - heatr=False, purr=False, acer=False, stdout=stdout) + for key in ('broadr', 'heatr', 'gaspr', 'purr', 'acer'): + kwargs.setdefault(key, False) + make_ace(filename, pendf=pendf, **kwargs) def make_ace(filename, temperatures=None, acer=True, xsdir=None, diff --git a/openmc/data/reconstruct.pyx b/openmc/data/reconstruct.pyx deleted file mode 100644 index cd0bbc38b..000000000 --- a/openmc/data/reconstruct.pyx +++ /dev/null @@ -1,522 +0,0 @@ -from libc.stdlib cimport malloc, calloc, free -from libc.math cimport cos, sin, sqrt, atan, M_PI - -cimport numpy as np -import numpy as np -from numpy.linalg import inv -cimport cython - - -cdef extern from "complex.h": - double cabs(double complex) - double complex conj(double complex) - double creal(complex double) - double cimag(complex double) - double complex cexp(double complex) - -# Physical constants are from CODATA 2014 -cdef double NEUTRON_MASS_ENERGY = 939.5654133e6 # eV/c^2 -cdef double HBAR_C = 197.3269788e5 # eV-b^0.5 - - -@cython.cdivision(True) -def wave_number(double A, double E): - r"""Neutron wave number in center-of-mass system. - - ENDF-102 defines the neutron wave number in the center-of-mass system in - Equation D.10 as - - .. math:: - k = \frac{2m_n}{\hbar} \frac{A}{A + 1} \sqrt{|E|} - - Parameters - ---------- - A : double - Ratio of target mass to neutron mass - E : double - Energy in eV - - Returns - ------- - double - Neutron wave number in b^-0.5 - - """ - return A/(A + 1)*sqrt(2*NEUTRON_MASS_ENERGY*abs(E))/HBAR_C - -@cython.cdivision(True) -cdef double _wave_number(double A, double E): - return A/(A + 1)*sqrt(2*NEUTRON_MASS_ENERGY*abs(E))/HBAR_C - - -@cython.cdivision(True) -cdef double phaseshift(int l, double rho): - """Calculate hardsphere phase shift as given in ENDF-102, Equation D.13 - - Parameters - ---------- - l : int - Angular momentum quantum number - rho : float - Product of the wave number and the channel radius - - Returns - ------- - double - Hardsphere phase shift - - """ - if l == 0: - return rho - elif l == 1: - return rho - atan(rho) - elif l == 2: - return rho - atan(3*rho/(3 - rho**2)) - elif l == 3: - return rho - atan((15*rho - rho**3)/(15 - 6*rho**2)) - elif l == 4: - return rho - atan((105*rho - 10*rho**3)/(105 - 45*rho**2 + rho**4)) - - -@cython.cdivision(True) -def penetration_shift(int l, double rho): - r"""Calculate shift and penetration factors as given in ENDF-102, Equations D.11 - and D.12. - - Parameters - ---------- - l : int - Angular momentum quantum number - rho : float - Product of the wave number and the channel radius - - Returns - ------- - double - Penetration factor for given :math:`l` - double - Shift factor for given :math:`l` - - """ - cdef double den - - if l == 0: - return rho, 0. - elif l == 1: - den = 1 + rho**2 - return rho**3/den, -1/den - elif l == 2: - den = 9 + 3*rho**2 + rho**4 - return rho**5/den, -(18 + 3*rho**2)/den - elif l == 3: - den = 225 + 45*rho**2 + 6*rho**4 + rho**6 - return rho**7/den, -(675 + 90*rho**2 + 6*rho**4)/den - elif l == 4: - den = 11025 + 1575*rho**2 + 135*rho**4 + 10*rho**6 + rho**8 - return rho**9/den, -(44100 + 4725*rho**2 + 270*rho**4 + 10*rho**6)/den - - -@cython.boundscheck(False) -@cython.wraparound(False) -@cython.cdivision(True) -def reconstruct_mlbw(mlbw, double E): - """Evaluate cross section using MLBW data. - - Parameters - ---------- - mlbw : openmc.data.MultiLevelBreitWigner - Multi-level Breit-Wigner resonance parameters - E : double - Energy in eV at which to evaluate the cross section - - Returns - ------- - elastic : double - Elastic scattering cross section in barns - capture : double - Radiative capture cross section in barns - fission : double - Fission cross section in barns - - """ - cdef int i, nJ, ij, l, n_res, i_res - cdef double elastic, capture, fission - cdef double A, k, rho, rhohat, I - cdef double P, S, phi, cos2phi, sin2phi - cdef double Ex, Q, rhoc, rhochat, P_c, S_c - cdef double jmin, jmax, j, Dl - cdef double E_r, gt, gn, gg, gf, gx, P_r, S_r, P_rx - cdef double gnE, gtE, Eprime, x, f - cdef double *g - cdef double (*s)[2] - cdef double [:,:] params - - I = mlbw.target_spin - A = mlbw.atomic_weight_ratio - k = _wave_number(A, E) - - elastic = 0. - capture = 0. - fission = 0. - - for i, l in enumerate(mlbw._l_values): - params = mlbw._parameter_matrix[l] - - rho = k*mlbw.channel_radius[l](E) - rhohat = k*mlbw.scattering_radius[l](E) - P, S = penetration_shift(l, rho) - phi = phaseshift(l, rhohat) - cos2phi = cos(2*phi) - sin2phi = sin(2*phi) - - # Determine shift and penetration at modified energy - if mlbw._competitive[i]: - Ex = E + mlbw.q_value[l]*(A + 1)/A - rhoc = mlbw.channel_radius[l](Ex) - rhochat = mlbw.scattering_radius[l](Ex) - P_c, S_c = penetration_shift(l, rhoc) - if Ex < 0: - P_c = 0 - - # Determine range of total angular momentum values based on equation - # 41 in LA-UR-12-27079 - jmin = abs(abs(I - l) - 0.5) - jmax = I + l + 0.5 - nJ = int(jmax - jmin + 1) - - # Determine Dl factor using Equation 43 in LA-UR-12-27079 - Dl = 2*l + 1 - g = malloc(nJ*sizeof(double)) - for ij in range(nJ): - j = jmin + ij - g[ij] = (2*j + 1)/(4*I + 2) - Dl -= g[ij] - - s = calloc(2*nJ, sizeof(double)) - for i_res in range(params.shape[0]): - # Copy resonance parameters - E_r = params[i_res, 0] - j = params[i_res, 2] - ij = int(j - jmin) - gt = params[i_res, 3] - gn = params[i_res, 4] - gg = params[i_res, 5] - gf = params[i_res, 6] - gx = params[i_res, 7] - P_r = params[i_res, 8] - S_r = params[i_res, 9] - P_rx = params[i_res, 10] - - # Calculate neutron and total width at energy E - gnE = P*gn/P_r # ENDF-102, Equation D.7 - gtE = gnE + gg + gf - if gx > 0: - gtE += gx*P_c/P_rx - - Eprime = E_r + (S_r - S)/(2*P_r)*gn # ENDF-102, Equation D.9 - x = 2*(E - Eprime)/gtE # LA-UR-12-27079, Equation 26 - f = 2*gnE/(gtE*(1 + x*x)) # Common factor in Equation 40 - s[ij][0] += f # First sum in Equation 40 - s[ij][1] += f*x # Second sum in Equation 40 - capture += f*g[ij]*gg/gtE - if gf > 0: - fission += f*g[ij]*gf/gtE - - for ij in range(nJ): - # Add all but last term of LA-UR-12-27079, Equation 40 - elastic += g[ij]*((1 - cos2phi - s[ij][0])**2 + - (sin2phi + s[ij][1])**2) - - # Add final term with Dl from Equation 40 - elastic += 2*Dl*(1 - cos2phi) - - # Free memory - free(g) - free(s) - - capture *= 2*M_PI/(k*k) - fission *= 2*M_PI/(k*k) - elastic *= M_PI/(k*k) - - return (elastic, capture, fission) - - -@cython.boundscheck(False) -@cython.wraparound(False) -@cython.cdivision(True) -def reconstruct_slbw(slbw, double E): - """Evaluate cross section using SLBW data. - - Parameters - ---------- - slbw : openmc.data.SingleLevelBreitWigner - Single-level Breit-Wigner resonance parameters - E : double - Energy in eV at which to evaluate the cross section - - Returns - ------- - elastic : double - Elastic scattering cross section in barns - capture : double - Radiative capture cross section in barns - fission : double - Fission cross section in barns - - """ - cdef int i, l, i_res - cdef double elastic, capture, fission - cdef double A, k, rho, rhohat, I - cdef double P, S, phi, cos2phi, sin2phi, sinphi2 - cdef double Ex, rhoc, rhochat, P_c, S_c - cdef double E_r, J, gt, gn, gg, gf, gx, P_r, S_r, P_rx - cdef double gnE, gtE, Eprime, f - cdef double x, theta, psi, chi - cdef double [:,:] params - - I = slbw.target_spin - A = slbw.atomic_weight_ratio - k = _wave_number(A, E) - - elastic = 0. - capture = 0. - fission = 0. - - for i, l in enumerate(slbw._l_values): - params = slbw._parameter_matrix[l] - - rho = k*slbw.channel_radius[l](E) - rhohat = k*slbw.scattering_radius[l](E) - P, S = penetration_shift(l, rho) - phi = phaseshift(l, rhohat) - cos2phi = cos(2*phi) - sin2phi = sin(2*phi) - sinphi2 = sin(phi)**2 - - # Add potential scattering -- first term in ENDF-102, Equation D.2 - elastic += 4*M_PI/(k*k)*(2*l + 1)*sinphi2 - - # Determine shift and penetration at modified energy - if slbw._competitive[i]: - Ex = E + slbw.q_value[l]*(A + 1)/A - rhoc = k*slbw.channel_radius[l](Ex) - rhochat = k*slbw.scattering_radius[l](Ex) - P_c, S_c = penetration_shift(l, rhoc) - if Ex < 0: - P_c = 0 - - for i_res in range(params.shape[0]): - # Copy resonance parameters - E_r = params[i_res, 0] - J = params[i_res, 2] - gt = params[i_res, 3] - gn = params[i_res, 4] - gg = params[i_res, 5] - gf = params[i_res, 6] - gx = params[i_res, 7] - P_r = params[i_res, 8] - S_r = params[i_res, 9] - P_rx = params[i_res, 10] - - # Calculate neutron and total width at energy E - gnE = P*gn/P_r # Equation D.7 - gtE = gnE + gg + gf - if gx > 0: - gtE += gx*P_c/P_rx - - Eprime = E_r + (S_r - S)/(2*P_r)*gn # Equation D.9 - gJ = (2*J + 1)/(4*I + 2) # Mentioned in section D.1.1.4 - - # Calculate common factor for elastic, capture, and fission - # cross sections - f = M_PI/(k*k)*gJ*gnE/((E - Eprime)**2 + gtE**2/4) - - # Add contribution to elastic per Equation D.2 - elastic += f*(gnE*cos2phi - 2*(gg + gf)*sinphi2 - + 2*(E - Eprime)*sin2phi) - - # Add contribution to capture per Equation D.3 - capture += f*gg - - # Add contribution to fission per Equation D.6 - if gf > 0: - fission += f*gf - - return (elastic, capture, fission) - - -@cython.boundscheck(False) -@cython.wraparound(False) -@cython.cdivision(True) -def reconstruct_rm(rm, double E): - """Evaluate cross section using Reich-Moore data. - - Parameters - ---------- - rm : openmc.data.ReichMoore - Reich-Moore resonance parameters - E : double - Energy in eV at which to evaluate the cross section - - Returns - ------- - elastic : double - Elastic scattering cross section in barns - capture : double - Radiative capture cross section in barns - fission : double - Fission cross section in barns - - """ - cdef int i, l, m, n, i_res - cdef int i_s, num_s, i_J, num_J - cdef double elastic, capture, fission, total - cdef double A, k, rho, rhohat, I - cdef double P, S, phi - cdef double smin, smax, s, Jmin, Jmax, J, j - cdef double E_r, gn, gg, gfa, gfb, P_r - cdef double E_diff, abs_value, gJ - cdef double Kr, Ki, x - cdef double complex Ubar, U_, factor - cdef bint hasfission - cdef np.ndarray[double, ndim=2] one - cdef np.ndarray[double complex, ndim=2] K, Imat, U - cdef double [:,:] params - - # Get nuclear spin - I = rm.target_spin - - elastic = 0. - fission = 0. - total = 0. - A = rm.atomic_weight_ratio - k = _wave_number(A, E) - one = np.eye(3) - K = np.zeros((3,3), dtype=complex) - - for i, l in enumerate(rm._l_values): - # Check for l-dependent scattering radius - rho = k*rm.channel_radius[l](E) - rhohat = k*rm.scattering_radius[l](E) - - # Calculate shift and penetrability - P, S = penetration_shift(l, rho) - - # Calculate phase shift - phi = phaseshift(l, rhohat) - - # Calculate common factor on collision matrix terms (term outside curly - # braces in ENDF-102, Eq. D.27) - Ubar = cexp(-2j*phi) - - # The channel spin is the vector sum of the target spin, I, and the - # neutron spin, 1/2, so can take on values of |I - 1/2| < s < I + 1/2 - smin = abs(I - 0.5) - smax = I + 0.5 - num_s = int(smax - smin + 1) - - for i_s in range(num_s): - s = i_s + smin - - # Total angular momentum is the vector sum of l and s and can assume - # values between |l - s| < J < l + s - Jmin = abs(l - s) - Jmax = l + s - num_J = int(Jmax - Jmin + 1) - - for i_J in range(num_J): - J = i_J + Jmin - - # Initialize K matrix - for m in range(3): - for n in range(3): - K[m,n] = 0.0 - - hasfission = False - if (l, J) in rm._parameter_matrix: - params = rm._parameter_matrix[l, J] - - for i_res in range(params.shape[0]): - # Sometimes, the same (l, J) quantum numbers can occur - # for different values of the channel spin, s. In this - # case, the sign of the channel spin indicates which - # spin is to be used. If the spin is negative assume - # this resonance comes from the I - 1/2 channel and vice - # versa. - j = params[i_res, 2] - if l > 0: - if (j < 0 and s != smin) or (j > 0 and s != smax): - continue - - # Copy resonance parameters - E_r = params[i_res, 0] - gn = params[i_res, 3] - gg = params[i_res, 4] - gfa = params[i_res, 5] - gfb = params[i_res, 6] - P_r = params[i_res, 7] - - # Calculate neutron width at energy E - gn = sqrt(P*gn/P_r) - - # Calculate j/2 * inverse of denominator of K matrix terms - factor = 0.5j/(E_r - E - 0.5j*gg) - - # Upper triangular portion of K matrix -- see ENDF-102, - # Equation D.28 - K[0,0] = K[0,0] + gn*gn*factor - if gfa != 0.0 or gfb != 0.0: - # Negate fission widths if necessary - gfa = (-1 if gfa < 0 else 1)*sqrt(abs(gfa)) - gfb = (-1 if gfb < 0 else 1)*sqrt(abs(gfb)) - - K[0,1] = K[0,1] + gn*gfa*factor - K[0,2] = K[0,2] + gn*gfb*factor - K[1,1] = K[1,1] + gfa*gfa*factor - K[1,2] = K[1,2] + gfa*gfb*factor - K[2,2] = K[2,2] + gfb*gfb*factor - hasfission = True - - # Get collision matrix - gJ = (2*J + 1)/(4*I + 2) - if hasfission: - # Copy upper triangular portion of K to lower triangular - K[1,0] = K[0,1] - K[2,0] = K[0,2] - K[2,1] = K[1,2] - - Imat = inv(one - K) - U = Ubar*(2*Imat - one) # ENDF-102, Eq. D.27 - elastic += gJ*cabs(1 - U[0,0])**2 # ENDF-102, Eq. D.24 - total += 2*gJ*(1 - creal(U[0,0])) # ENDF-102, Eq. D.23 - - # Calculate fission from ENDF-102, Eq. D.26 - fission += 4*gJ*(cabs(Imat[1,0])**2 + cabs(Imat[2,0])**2) - else: - U_ = Ubar*(2/(1 - K[0,0]) - 1) - if abs(creal(K[0,0])) < 3e-4 and abs(phi) < 3e-4: - # If K and phi are both very small, the calculated cross - # sections can lose precision because the real part of U - # ends up very close to unity. To get around this, we - # use Euler's formula to express Ubar by real and - # imaginary parts, expand cos(2phi) = 1 - 2phi^2 + - # O(phi^4), and then simplify - Kr = creal(K[0,0]) - Ki = cimag(K[0,0]) - x = 2*(-Kr + (Kr*Kr + Ki*Ki)*(1 - phi*phi) + phi*phi - - sin(2*phi)*Ki)/((1 - Kr)*(1 - Kr) + Ki*Ki) - total += 2*gJ*x - elastic += gJ*(x*x + cimag(U_)**2) - else: - total += 2*gJ*(1 - creal(U_)) # ENDF-102, Eq. D.23 - elastic += gJ*cabs(1 - U_)**2 # ENDF-102, Eq. D.24 - - # Calculate capture as difference of other cross sections as per ENDF-102, - # Equation D.25 - capture = total - elastic - fission - - elastic *= M_PI/(k*k) - capture *= M_PI/(k*k) - fission *= M_PI/(k*k) - - return (elastic, capture, fission) diff --git a/pyproject.toml b/pyproject.toml index 98b1d152f..39aa261c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,6 @@ [build-system] -requires = ["setuptools", "wheel", "numpy", "cython"] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" [project] name = "openmc" diff --git a/setup.py b/setup.py deleted file mode 100755 index 88a45a360..000000000 --- a/setup.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env python - -import numpy as np -from setuptools import setup -from Cython.Build import cythonize - - -kwargs = { - # Cython is used to add resonance reconstruction - 'ext_modules': cythonize('openmc/data/*.pyx'), - 'include_dirs': [np.get_include()] -} - -setup(**kwargs) diff --git a/tests/unit_tests/test_data_neutron.py b/tests/unit_tests/test_data_neutron.py index c0d6a1f15..db6ae1eb8 100644 --- a/tests/unit_tests/test_data_neutron.py +++ b/tests/unit_tests/test_data_neutron.py @@ -282,10 +282,6 @@ def test_slbw(xe135): s = resolved.parameters.iloc[0] assert s['energy'] == pytest.approx(0.084) - xs = resolved.reconstruct([10., 30., 100.]) - assert sorted(xs.keys()) == [2, 18, 102] - assert np.all(xs[18] == 0.0) - def test_mlbw(sm150): resolved = sm150.resonances.resolved @@ -294,10 +290,6 @@ def test_mlbw(sm150): assert resolved.energy_max == pytest.approx(1570.) assert resolved.target_spin == 0.0 - xs = resolved.reconstruct([10., 100., 1000.]) - assert sorted(xs.keys()) == [2, 18, 102] - assert np.all(xs[18] == 0.0) - def test_reichmoore(gd154): res = gd154.resonances @@ -319,7 +311,6 @@ def test_reichmoore(gd154): elastic = gd154.reactions[2].xs['0K'] assert isinstance(elastic, openmc.data.ResonancesWithBackground) - assert elastic(0.0253) == pytest.approx(5.7228949796394524) def test_rml(cl35): @@ -347,8 +338,6 @@ def test_mlbw_cov_lcomp0(cf252): assert not subset.parameters.empty assert (subset.file2res.parameters['energy'] < 100).all() samples = cov.sample(1) - xs = samples[0].reconstruct([10., 100., 1000.]) - assert sorted(xs.keys()) == [2, 18, 102] def test_mlbw_cov_lcomp1(ti50): @@ -365,9 +354,7 @@ def test_mlbw_cov_lcomp1(ti50): subset = cov.subset('L', [1, 1]) assert not subset.parameters.empty assert (subset.file2res.parameters['L'] == 1).all() - samples = cov.sample(1) - xs = samples[0].reconstruct([10., 100., 1000.]) - assert sorted(xs.keys()) == [2, 18, 102] + cov.sample(1) def test_mlbw_cov_lcomp2(na23): @@ -384,9 +371,7 @@ def test_mlbw_cov_lcomp2(na23): subset = cov.subset('L', [1, 1]) assert not subset.parameters.empty assert (subset.file2res.parameters['L'] == 1).all() - samples = cov.sample(1) - xs = samples[0].reconstruct([10., 100., 1000.]) - assert sorted(xs.keys()) == [2, 18, 102] + cov.sample(1) def test_rmcov_lcomp1(gd154): @@ -403,9 +388,7 @@ def test_rmcov_lcomp1(gd154): subset = cov.subset('energy', [0, 100]) assert not subset.parameters.empty assert (subset.file2res.parameters['energy'] < 100).all() - samples = cov.sample(1) - xs = samples[0].reconstruct([10., 100., 1000.]) - assert sorted(xs.keys()) == [2, 18, 102] + cov.sample(1) def test_rmcov_lcomp2(th232): @@ -422,9 +405,7 @@ def test_rmcov_lcomp2(th232): subset = cov.subset('energy', [0, 100]) assert not subset.parameters.empty assert (subset.file2res.parameters['energy'] < 100).all() - samples = cov.sample(1) - xs = samples[0].reconstruct([10., 100., 1000.]) - assert sorted(xs.keys()) == [2, 18, 102] + cov.sample(1) def test_madland_nix(am241): diff --git a/tools/ci/gha-install.sh b/tools/ci/gha-install.sh index 87952fda9..cff7dc834 100755 --- a/tools/ci/gha-install.sh +++ b/tools/ci/gha-install.sh @@ -40,8 +40,7 @@ if [[ $MPI == 'y' ]]; then export CC=mpicc export HDF5_MPI=ON export HDF5_DIR=/usr/lib/x86_64-linux-gnu/hdf5/mpich - pip install wheel "cython<3.0" - pip install --no-binary=h5py --no-build-isolation h5py + pip install --no-binary=h5py h5py fi # Build and install OpenMC executable From 10c511a0e24b8b0a0703441bb47ca4fd1012f23b Mon Sep 17 00:00:00 2001 From: Zoe Prieto <101403129+zoeprieto@users.noreply.github.com> Date: Thu, 15 Aug 2024 12:10:26 -0300 Subject: [PATCH 160/671] Nuclide temperatures - solution to issue #3102 (#3110) --- include/openmc/string_utils.h | 18 +++++++++++++++++- src/mgxs.cpp | 33 ++++++++++++++++++--------------- src/nuclide.cpp | 19 +++++++++++-------- src/string_utils.cpp | 3 +-- src/thermal.cpp | 24 ++++++++++++++---------- 5 files changed, 61 insertions(+), 36 deletions(-) diff --git a/include/openmc/string_utils.h b/include/openmc/string_utils.h index 6b4c69d48..2e8b0d14f 100644 --- a/include/openmc/string_utils.h +++ b/include/openmc/string_utils.h @@ -1,6 +1,7 @@ #ifndef OPENMC_STRING_UTILS_H #define OPENMC_STRING_UTILS_H +#include #include #include "openmc/vector.h" @@ -15,7 +16,7 @@ std::string to_element(const std::string& name); void to_lower(std::string& str); -int word_count(std::string const& str); +int word_count(const std::string& str); vector split(const std::string& in); @@ -23,5 +24,20 @@ bool ends_with(const std::string& value, const std::string& ending); bool starts_with(const std::string& value, const std::string& beginning); +template +inline std::string concatenate(const T& values, const std::string& del = ", ") +{ + if (values.size() == 0) + return ""; + + std::stringstream oss; + auto it = values.begin(); + oss << *it++; + while (it != values.end()) { + oss << del << *it++; + } + return oss.str(); +} + } // namespace openmc #endif // OPENMC_STRING_UTILS_H diff --git a/src/mgxs.cpp b/src/mgxs.cpp index 9eae5a7da..eae3e5817 100644 --- a/src/mgxs.cpp +++ b/src/mgxs.cpp @@ -72,18 +72,18 @@ void Mgxs::metadata_from_hdf5(hid_t xs_id, const vector& temperature, } get_datasets(kT_group, dset_names); vector shape = {num_temps}; - xt::xarray available_temps(shape); + xt::xarray temps_available(shape); for (int i = 0; i < num_temps; i++) { - read_double(kT_group, dset_names[i], &available_temps[i], true); + read_double(kT_group, dset_names[i], &temps_available[i], true); // convert eV to Kelvin - available_temps[i] /= K_BOLTZMANN; + temps_available[i] = std::round(temps_available[i] / K_BOLTZMANN); // Done with dset_names, so delete it delete[] dset_names[i]; } delete[] dset_names; - std::sort(available_temps.begin(), available_temps.end()); + std::sort(temps_available.begin(), temps_available.end()); // If only one temperature is available, lets just use nearest temperature // interpolation @@ -99,17 +99,20 @@ void Mgxs::metadata_from_hdf5(hid_t xs_id, const vector& temperature, case TemperatureMethod::NEAREST: // Determine actual temperatures to read for (const auto& T : temperature) { - auto i_closest = xt::argmin(xt::abs(available_temps - T))[0]; - double temp_actual = available_temps[i_closest]; + auto i_closest = xt::argmin(xt::abs(temps_available - T))[0]; + double temp_actual = temps_available[i_closest]; if (std::fabs(temp_actual - T) < settings::temperature_tolerance) { if (std::find(temps_to_read.begin(), temps_to_read.end(), std::round(temp_actual)) == temps_to_read.end()) { temps_to_read.push_back(std::round(temp_actual)); } } else { - fatal_error(fmt::format("MGXS library does not contain cross sections " - "for {} at or near {} K.", - in_name, std::round(T))); + fatal_error(fmt::format( + "MGXS library does not contain cross sections " + "for {} at or near {} K. Available temperatures " + "are {} K. Consider making use of openmc.Settings.temperature " + "to specify how intermediate temperatures are treated.", + in_name, std::round(T), concatenate(temps_available))); } } break; @@ -122,16 +125,16 @@ void Mgxs::metadata_from_hdf5(hid_t xs_id, const vector& temperature, in_name + " at temperatures that bound " + std::to_string(std::round(temperature[i]))); } - if ((available_temps[j] <= temperature[i]) && - (temperature[i] < available_temps[j + 1])) { + if ((temps_available[j] <= temperature[i]) && + (temperature[i] < temps_available[j + 1])) { if (std::find(temps_to_read.begin(), temps_to_read.end(), - std::round(available_temps[j])) == temps_to_read.end()) { - temps_to_read.push_back(std::round((int)available_temps[j])); + temps_available[j]) == temps_to_read.end()) { + temps_to_read.push_back(temps_available[j]); } if (std::find(temps_to_read.begin(), temps_to_read.end(), - std::round(available_temps[j + 1])) == temps_to_read.end()) { - temps_to_read.push_back(std::round((int)available_temps[j + 1])); + temps_available[j + 1]) == temps_to_read.end()) { + temps_to_read.push_back(temps_available[j + 1]); } break; } diff --git a/src/nuclide.cpp b/src/nuclide.cpp index 04ec11073..f720f848b 100644 --- a/src/nuclide.cpp +++ b/src/nuclide.cpp @@ -86,7 +86,7 @@ Nuclide::Nuclide(hid_t group, const vector& temperature) for (const auto& name : dset_names) { double T; read_dataset(kT_group, name.c_str(), T); - temps_available.push_back(T / K_BOLTZMANN); + temps_available.push_back(std::round(T / K_BOLTZMANN)); } std::sort(temps_available.begin(), temps_available.end()); @@ -158,9 +158,12 @@ Nuclide::Nuclide(hid_t group, const vector& temperature) } } } else { - fatal_error( - "Nuclear data library does not contain cross sections for " + name_ + - " at or near " + std::to_string(T_desired) + " K."); + fatal_error(fmt::format( + "Nuclear data library does not contain cross sections " + "for {} at or near {} K. Available temperatures " + "are {} K. Consider making use of openmc.Settings.temperature " + "to specify how intermediate temperatures are treated.", + name_, std::to_string(T_desired), concatenate(temps_available))); } } break; @@ -173,8 +176,8 @@ Nuclide::Nuclide(hid_t group, const vector& temperature) for (int j = 0; j < temps_available.size() - 1; ++j) { if (temps_available[j] <= T_desired && T_desired < temps_available[j + 1]) { - int T_j = std::round(temps_available[j]); - int T_j1 = std::round(temps_available[j + 1]); + int T_j = temps_available[j]; + int T_j1 = temps_available[j + 1]; if (!contains(temps_to_read, T_j)) { temps_to_read.push_back(T_j); } @@ -191,14 +194,14 @@ Nuclide::Nuclide(hid_t group, const vector& temperature) if (std::abs(T_desired - temps_available.front()) <= settings::temperature_tolerance) { if (!contains(temps_to_read, temps_available.front())) { - temps_to_read.push_back(std::round(temps_available.front())); + temps_to_read.push_back(temps_available.front()); } continue; } if (std::abs(T_desired - temps_available.back()) <= settings::temperature_tolerance) { if (!contains(temps_to_read, temps_available.back())) { - temps_to_read.push_back(std::round(temps_available.back())); + temps_to_read.push_back(temps_available.back()); } continue; } diff --git a/src/string_utils.cpp b/src/string_utils.cpp index 3b1c1b4a6..74f048e8d 100644 --- a/src/string_utils.cpp +++ b/src/string_utils.cpp @@ -2,7 +2,6 @@ #include // for equal #include // for tolower, isspace -#include namespace openmc { @@ -36,7 +35,7 @@ void to_lower(std::string& str) str[i] = std::tolower(str[i]); } -int word_count(std::string const& str) +int word_count(const std::string& str) { std::stringstream stream(str); std::string dum; diff --git a/src/thermal.cpp b/src/thermal.cpp index 741f89ed1..cbe0983ed 100644 --- a/src/thermal.cpp +++ b/src/thermal.cpp @@ -19,6 +19,7 @@ #include "openmc/secondary_correlated.h" #include "openmc/secondary_thermal.h" #include "openmc/settings.h" +#include "openmc/string_utils.h" namespace openmc { @@ -59,7 +60,7 @@ ThermalScattering::ThermalScattering( // Read temperature value double T; read_dataset(kT_group, dset_names[i].data(), T); - temps_available[i] = T / K_BOLTZMANN; + temps_available[i] = std::round(T / K_BOLTZMANN); } std::sort(temps_available.begin(), temps_available.end()); @@ -89,9 +90,12 @@ ThermalScattering::ThermalScattering( temps_to_read.push_back(std::round(temp_actual)); } } else { - fatal_error(fmt::format("Nuclear data library does not contain cross " - "sections for {} at or near {} K.", - name_, std::round(T))); + fatal_error(fmt::format( + "Nuclear data library does not contain cross sections " + "for {} at or near {} K. Available temperatures " + "are {} K. Consider making use of openmc.Settings.temperature " + "to specify how intermediate temperatures are treated.", + name_, std::round(T), concatenate(temps_available))); } } break; @@ -103,8 +107,8 @@ ThermalScattering::ThermalScattering( bool found = false; for (int j = 0; j < temps_available.size() - 1; ++j) { if (temps_available[j] <= T && T < temps_available[j + 1]) { - int T_j = std::round(temps_available[j]); - int T_j1 = std::round(temps_available[j + 1]); + int T_j = temps_available[j]; + int T_j1 = temps_available[j + 1]; if (std::find(temps_to_read.begin(), temps_to_read.end(), T_j) == temps_to_read.end()) { temps_to_read.push_back(T_j); @@ -122,14 +126,14 @@ ThermalScattering::ThermalScattering( if (std::abs(T - temps_available[0]) <= settings::temperature_tolerance) { if (std::find(temps_to_read.begin(), temps_to_read.end(), - std::round(temps_available[0])) == temps_to_read.end()) { - temps_to_read.push_back(std::round(temps_available[0])); + temps_available[0]) == temps_to_read.end()) { + temps_to_read.push_back(temps_available[0]); } } else if (std::abs(T - temps_available[n - 1]) <= settings::temperature_tolerance) { if (std::find(temps_to_read.begin(), temps_to_read.end(), - std::round(temps_available[n - 1])) == temps_to_read.end()) { - temps_to_read.push_back(std::round(temps_available[n - 1])); + temps_available[n - 1]) == temps_to_read.end()) { + temps_to_read.push_back(temps_available[n - 1]); } } else { fatal_error( From e7bc9ba23c896f758c3a4139133cd58eb2b9c1f9 Mon Sep 17 00:00:00 2001 From: Youssef Badr <104090877+ybadr16@users.noreply.github.com> Date: Fri, 16 Aug 2024 00:56:22 +0300 Subject: [PATCH 161/671] Added error if cross sections path is a folder (#3115) --- src/cross_sections.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/cross_sections.cpp b/src/cross_sections.cpp index a7bd86095..248ae9019 100644 --- a/src/cross_sections.cpp +++ b/src/cross_sections.cpp @@ -283,6 +283,10 @@ void read_ce_cross_sections_xml() { // Check if cross_sections.xml exists const auto& filename = settings::path_cross_sections; + if (dir_exists(filename)) { + fatal_error("OPENMC_CROSS_SECTIONS is set to a directory. " + "It should be set to an XML file."); + } if (!file_exists(filename)) { // Could not find cross_sections.xml file fatal_error("Cross sections XML file '" + filename + "' does not exist."); From b22656e57f9469d25f14a203831bcdb390cd84db Mon Sep 17 00:00:00 2001 From: John Tramm Date: Thu, 15 Aug 2024 20:47:05 -0500 Subject: [PATCH 162/671] Fix random ray solver to correctly simulate fixed source problems with fissionable materials (#3106) Co-authored-by: Paul Romano --- openmc/examples.py | 2 +- src/random_ray/flat_source_domain.cpp | 40 ++--- .../__init__.py | 0 .../flat/inputs_true.dat | 140 +++++++++++++++ .../flat/results_true.dat | 169 ++++++++++++++++++ .../linear_xy/inputs_true.dat | 140 +++++++++++++++ .../linear_xy/results_true.dat | 169 ++++++++++++++++++ .../test.py | 133 ++++++++++++++ 8 files changed, 772 insertions(+), 21 deletions(-) create mode 100644 tests/regression_tests/random_ray_fixed_source_subcritical/__init__.py create mode 100644 tests/regression_tests/random_ray_fixed_source_subcritical/flat/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_subcritical/flat/results_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/results_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_subcritical/test.py diff --git a/openmc/examples.py b/openmc/examples.py index 038c75ad2..538b6ea4a 100644 --- a/openmc/examples.py +++ b/openmc/examples.py @@ -804,7 +804,7 @@ def random_ray_lattice() -> openmc.Model: azimuthal_cells.append(azimuthal_cell) # Create a geometry with the azimuthal universes - pincell = openmc.Universe(cells=azimuthal_cells) + pincell = openmc.Universe(cells=azimuthal_cells, name='pincell') ######################################## # Define a moderator lattice universe diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 8b6cda93f..584b3a7ed 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -158,31 +158,31 @@ void FlatSourceDomain::update_neutron_source(double k_eff) } } - if (settings::run_mode == RunMode::EIGENVALUE) { - // Add fission source if in eigenvalue mode + // Add fission source #pragma omp parallel for - for (int sr = 0; sr < n_source_regions_; sr++) { - int material = material_[sr]; + for (int sr = 0; sr < n_source_regions_; sr++) { + int material = material_[sr]; - for (int e_out = 0; e_out < negroups_; e_out++) { - float sigma_t = data::mg.macro_xs_[material].get_xs( - MgxsType::TOTAL, e_out, nullptr, nullptr, nullptr, t, a); - float fission_source = 0.0f; + for (int e_out = 0; e_out < negroups_; e_out++) { + float sigma_t = data::mg.macro_xs_[material].get_xs( + MgxsType::TOTAL, e_out, nullptr, nullptr, nullptr, t, a); + float fission_source = 0.0f; - for (int e_in = 0; e_in < negroups_; e_in++) { - float scalar_flux = scalar_flux_old_[sr * negroups_ + e_in]; - float nu_sigma_f = data::mg.macro_xs_[material].get_xs( - MgxsType::NU_FISSION, e_in, nullptr, nullptr, nullptr, t, a); - float chi = data::mg.macro_xs_[material].get_xs( - MgxsType::CHI_PROMPT, e_in, &e_out, nullptr, nullptr, t, a); - fission_source += nu_sigma_f * scalar_flux * chi; - } - source_[sr * negroups_ + e_out] += - fission_source * inverse_k_eff / sigma_t; + for (int e_in = 0; e_in < negroups_; e_in++) { + float scalar_flux = scalar_flux_old_[sr * negroups_ + e_in]; + float nu_sigma_f = data::mg.macro_xs_[material].get_xs( + MgxsType::NU_FISSION, e_in, nullptr, nullptr, nullptr, t, a); + float chi = data::mg.macro_xs_[material].get_xs( + MgxsType::CHI_PROMPT, e_in, &e_out, nullptr, nullptr, t, a); + fission_source += nu_sigma_f * scalar_flux * chi; } + source_[sr * negroups_ + e_out] += + fission_source * inverse_k_eff / sigma_t; } - } else { -// Add external source if in fixed source mode + } + + // Add external source if in fixed source mode + if (settings::run_mode == RunMode::FIXED_SOURCE) { #pragma omp parallel for for (int se = 0; se < n_source_elements_; se++) { source_[se] += external_source_[se]; diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/__init__.py b/tests/regression_tests/random_ray_fixed_source_subcritical/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/flat/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_subcritical/flat/inputs_true.dat new file mode 100644 index 000000000..0bf4cfdc9 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_subcritical/flat/inputs_true.dat @@ -0,0 +1,140 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.126 0.126 + 10 10 + -0.63 -0.63 + +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 + + + 0.126 0.126 + 10 10 + -0.63 -0.63 + +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 9 + + + 1.26 1.26 + 2 2 + -1.26 -1.26 + +2 12 +12 13 + + + + + + + + + + + + + + + + + + + + + fixed source + 30 + 125 + 100 + + + 1.134 -1.26 -1.0 1.26 -1.134 1.0 + + + 2e-05 0.0735 20.0 200.0 2000.0 750000.0 2000000.0 0.14285714285714285 0.14285714285714285 0.14285714285714285 0.14285714285714285 0.14285714285714285 0.14285714285714285 0.14285714285714285 + + + universe + 9 + + + multi-group + + 40.0 + 40.0 + + + -1.26 -1.26 -1 1.26 1.26 1 + + + False + flat + + + + + 2 2 + -1.26 -1.26 + 1.26 1.26 + + + 1 + + + 1e-05 0.0635 10.0 100.0 1000.0 500000.0 1000000.0 20000000.0 + + + 1 2 + flux fission nu-fission + analog + + + diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/flat/results_true.dat b/tests/regression_tests/random_ray_fixed_source_subcritical/flat/results_true.dat new file mode 100644 index 000000000..e9d2a733d --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_subcritical/flat/results_true.dat @@ -0,0 +1,169 @@ +tally 1: +1.582116E+02 +1.005480E+03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.897563E+01 +1.396228E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.038901E+01 +1.667203E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.465551E+01 +2.438101E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.260230E+01 +1.110141E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +9.438906E+01 +3.579643E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.698287E+01 +1.314208E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.002152E+02 +1.608182E+03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +7.374897E+01 +2.181176E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.636086E+01 +2.787512E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.000395E+01 +3.611990E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.824715E+01 +1.360847E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +9.621963E+01 +3.714397E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.954480E+01 +1.436984E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.067903E+02 +4.585529E+02 +1.257560E+01 +6.364831E+00 +3.060650E+01 +3.770132E+01 +4.983480E+01 +9.972910E+01 +2.349541E+00 +2.217300E-01 +5.718312E+00 +1.313392E+00 +1.965462E+01 +1.548888E+01 +2.011494E-01 +1.622334E-03 +4.895573E-01 +9.609705E-03 +2.318671E+01 +2.155841E+01 +2.445032E-01 +2.397352E-03 +5.950720E-01 +1.420043E-02 +5.322867E+01 +1.136138E+02 +1.968847E-01 +1.554358E-03 +4.791838E-01 +9.207284E-03 +1.191175E+02 +5.692803E+02 +5.806982E-02 +1.354810E-04 +1.436897E-01 +8.295235E-04 +8.022963E+01 +2.589171E+02 +3.465139E-01 +4.882327E-03 +9.638109E-01 +3.777193E-02 +1.555305E+02 +9.711601E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.836421E+01 +1.367538E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.042089E+01 +1.673401E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.477708E+01 +2.463124E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.317851E+01 +1.134205E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +9.634473E+01 +3.724422E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.879813E+01 +1.396454E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/inputs_true.dat new file mode 100644 index 000000000..f7572a072 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/inputs_true.dat @@ -0,0 +1,140 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.126 0.126 + 10 10 + -0.63 -0.63 + +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 + + + 0.126 0.126 + 10 10 + -0.63 -0.63 + +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 9 + + + 1.26 1.26 + 2 2 + -1.26 -1.26 + +2 12 +12 13 + + + + + + + + + + + + + + + + + + + + + fixed source + 30 + 125 + 100 + + + 1.134 -1.26 -1.0 1.26 -1.134 1.0 + + + 2e-05 0.0735 20.0 200.0 2000.0 750000.0 2000000.0 0.14285714285714285 0.14285714285714285 0.14285714285714285 0.14285714285714285 0.14285714285714285 0.14285714285714285 0.14285714285714285 + + + universe + 9 + + + multi-group + + 40.0 + 40.0 + + + -1.26 -1.26 -1 1.26 1.26 1 + + + False + linear_xy + + + + + 2 2 + -1.26 -1.26 + 1.26 1.26 + + + 1 + + + 1e-05 0.0635 10.0 100.0 1000.0 500000.0 1000000.0 20000000.0 + + + 1 2 + flux fission nu-fission + analog + + + diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/results_true.dat b/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/results_true.dat new file mode 100644 index 000000000..90be5c569 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/results_true.dat @@ -0,0 +1,169 @@ +tally 1: +1.573422E+02 +9.945961E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.868035E+01 +1.382578E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.028997E+01 +1.651549E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.453137E+01 +2.414255E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.232506E+01 +1.098683E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +9.385720E+01 +3.539613E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.664229E+01 +1.298333E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.001324E+02 +1.606858E+03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +7.358736E+01 +2.171786E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.625416E+01 +2.765491E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.987675E+01 +3.582038E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.797311E+01 +1.348247E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +9.573509E+01 +3.677339E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.926615E+01 +1.423707E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.060513E+02 +4.521166E+02 +1.247077E+01 +6.257701E+00 +3.035137E+01 +3.706675E+01 +4.962504E+01 +9.887387E+01 +2.338861E+00 +2.196709E-01 +5.692320E+00 +1.301195E+00 +1.957564E+01 +1.536441E+01 +2.003053E-01 +1.608650E-03 +4.875030E-01 +9.528647E-03 +2.309275E+01 +2.138375E+01 +2.434816E-01 +2.377213E-03 +5.925856E-01 +1.408114E-02 +5.300741E+01 +1.126617E+02 +1.960322E-01 +1.540737E-03 +4.771089E-01 +9.126597E-03 +1.184809E+02 +5.630856E+02 +5.774249E-02 +1.339082E-04 +1.428798E-01 +8.198938E-04 +7.971031E+01 +2.554943E+02 +3.441954E-01 +4.814135E-03 +9.573621E-01 +3.724437E-02 +1.549617E+02 +9.639834E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.808547E+01 +1.354629E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.029953E+01 +1.654123E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.462331E+01 +2.433425E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.283851E+01 +1.120036E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +9.572799E+01 +3.677288E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.842566E+01 +1.378719E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/test.py b/tests/regression_tests/random_ray_fixed_source_subcritical/test.py new file mode 100644 index 000000000..6c2c569c6 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_subcritical/test.py @@ -0,0 +1,133 @@ +import os + +import openmc +from openmc.examples import random_ray_lattice +from openmc.utility_funcs import change_directory +import pytest + +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) + + +@pytest.mark.parametrize("shape", ["flat", "linear_xy"]) +def test_random_ray_source(shape): + with change_directory(shape): + openmc.reset_auto_ids() + + # The general strategy is to reuse the random_ray_lattice model, + # but redfine some of the geometry to make it a good + # subcritical multiplication problem. We then also add in + # a fixed source term. + + model = random_ray_lattice() + + # Begin by updating the random ray settings for fixed source + settings = model.settings + settings.random_ray['source_shape'] = shape + settings.run_mode = 'fixed source' + settings.particles = 30 + settings.random_ray['distance_active'] = 40.0 + settings.random_ray['distance_inactive'] = 40.0 + settings.random_ray['volume_normalized_flux_tallies'] = False + + # This problem needs about 2k iterations to converge, + # but for regression testing we only need a few hundred + # to ensure things are working as expected. With + # only 100 inactive batches, tallies will still be off + # by 3x or more. For validation against MGMC, be sure + # to increase the batch counts. + settings.batches = 125 + settings.inactive = 100 + + ######################################## + # Define the alternative geometry + + pitch = 1.26 + + for material in model.materials: + if material.name == 'Water': + water = material + + # The new geometry replaces two of the fuel pins with + # moderator, reducing k-eff to around 0.84. We also + # add a special universe in the corner of one of the moderator + # regions to use as a domain constraint for the source + moderator_infinite = openmc.Cell(fill=water, name='moderator infinite') + mu = openmc.Universe(cells=[moderator_infinite]) + + moderator_infinite2 = openmc.Cell(fill=water, name='moderator infinite 2') + mu2 = openmc.Universe(cells=[moderator_infinite2]) + + n_sub = 10 + + lattice = openmc.RectLattice() + lattice.lower_left = [-pitch/2.0, -pitch/2.0] + lattice.pitch = [pitch/n_sub, pitch/n_sub] + lattice.universes = [[mu] * n_sub for _ in range(n_sub)] + + lattice2 = openmc.RectLattice() + lattice2.lower_left = [-pitch/2.0, -pitch/2.0] + lattice2.pitch = [pitch/n_sub, pitch/n_sub] + lattice2.universes = [[mu] * n_sub for _ in range(n_sub)] + lattice2.universes[n_sub-1][n_sub-1] = mu2 + + mod_lattice_cell = openmc.Cell(fill=lattice) + mod_lattice_uni = openmc.Universe(cells=[mod_lattice_cell]) + + mod_lattice_cell2 = openmc.Cell(fill=lattice2) + mod_lattice_uni2 = openmc.Universe(cells=[mod_lattice_cell2]) + + lattice2x2 = openmc.RectLattice() + lattice2x2.lower_left = [-pitch, -pitch] + lattice2x2.pitch = [pitch, pitch] + + universes = model.geometry.get_all_universes() + for universe in universes.values(): + if universe.name == 'pincell': + pincell = universe + + lattice2x2.universes = [ + [pincell, mod_lattice_uni], + [mod_lattice_uni, mod_lattice_uni2] + ] + + box = openmc.model.RectangularPrism( + pitch*2, pitch*2, boundary_type='reflective') + + assembly = openmc.Cell(fill=lattice2x2, region=-box, name='assembly') + + root = openmc.Universe(name='root universe', cells=[assembly]) + model.geometry = openmc.Geometry(root) + + ######################################## + # Define the fixed source term + + s = 1.0 / 7.0 + strengths = [s, s, s, s, s, s, s] + midpoints = [2.0e-5, 0.0735, 20.0, 2.0e2, 2.0e3, 0.75e6, 2.0e6] + energy_distribution = openmc.stats.Discrete(x=midpoints, p=strengths) + + lower_left_src = [pitch - pitch/10.0, -pitch, -1.0] + upper_right_src = [pitch, -pitch + pitch/10.0, 1.0] + spatial_distribution = openmc.stats.Box( + lower_left_src, upper_right_src, only_fissionable=False) + + settings.source = openmc.IndependentSource( + space=spatial_distribution, + energy=energy_distribution, + constraints={'domains': [mu2]}, + strength=1.0 + ) + + ######################################## + # Run test + + harness = MGXSTestHarness('statepoint.125.h5', model) + harness.main() From 54c28b7705519c8e769de52c10ff1db4c81fc8e6 Mon Sep 17 00:00:00 2001 From: Lorenzo Chierici Date: Fri, 16 Aug 2024 13:57:32 +0200 Subject: [PATCH 163/671] run microxs with mpi (#3028) --- openmc/deplete/microxs.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index 39753529b..5be9875f3 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -21,6 +21,7 @@ import openmc from .chain import Chain, REACTIONS from .coupled_operator import _find_cross_sections, _get_nuclides_with_data import openmc.lib +from openmc.mpi import comm _valid_rxns = list(REACTIONS) _valid_rxns.append('fission') @@ -124,6 +125,15 @@ def get_microxs_and_flux( flux_tally.scores = ['flux'] model.tallies = openmc.Tallies([rr_tally, flux_tally]) + if openmc.lib.is_initialized: + openmc.lib.finalize() + + if comm.rank == 0: + model.export_to_model_xml() + comm.barrier() + # Reinitialize with tallies + openmc.lib.init(intracomm=comm) + # create temporary run with TemporaryDirectory() as temp_dir: if run_kwargs is None: @@ -133,12 +143,15 @@ def get_microxs_and_flux( run_kwargs.setdefault('cwd', temp_dir) statepoint_path = model.run(**run_kwargs) - with StatePoint(statepoint_path) as sp: - rr_tally = sp.tallies[rr_tally.id] - rr_tally._read_results() - flux_tally = sp.tallies[flux_tally.id] - flux_tally._read_results() + if comm.rank == 0: + with StatePoint(statepoint_path) as sp: + rr_tally = sp.tallies[rr_tally.id] + rr_tally._read_results() + flux_tally = sp.tallies[flux_tally.id] + flux_tally._read_results() + rr_tally = comm.bcast(rr_tally) + flux_tally = comm.bcast(flux_tally) # Get reaction rates and flux values reaction_rates = rr_tally.get_reshaped_data() # (domains, groups, nuclides, reactions) flux = flux_tally.get_reshaped_data() # (domains, groups, 1, 1) @@ -371,4 +384,3 @@ class MicroXS: ) df = pd.DataFrame({'xs': self.data.flatten()}, index=multi_index) df.to_csv(*args, **kwargs) - From 4ef1faf766818a7c01569386d27451774406d542 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 16 Aug 2024 11:33:59 -0500 Subject: [PATCH 164/671] Add delta_function convenience function (#3090) --- docs/source/pythonapi/stats.rst | 1 + openmc/stats/univariate.py | 21 +++++++++++++++++++++ tests/unit_tests/test_stats.py | 7 +++++++ 3 files changed, 29 insertions(+) diff --git a/docs/source/pythonapi/stats.rst b/docs/source/pythonapi/stats.rst index ffb4fcda2..b72896c18 100644 --- a/docs/source/pythonapi/stats.rst +++ b/docs/source/pythonapi/stats.rst @@ -28,6 +28,7 @@ Univariate Probability Distributions :nosignatures: :template: myfunction.rst + openmc.stats.delta_function openmc.stats.muir Angular Distributions diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index a47f4f320..10822c06f 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -312,6 +312,27 @@ class Discrete(Univariate): return type(self)(new_x, new_p) +def delta_function(value: float, intensity: float = 1.0) -> Discrete: + """Return a discrete distribution with a single point. + + .. versionadded:: 0.15.1 + + Parameters + ---------- + value : float + Value of the random variable. + intensity : float, optional + When used for an energy distribution, this can be used to assign an + intensity. + + Returns + ------- + Discrete distribution with a single point + + """ + return Discrete([value], [intensity]) + + class Uniform(Univariate): """Distribution with constant probability over a finite interval [a,b] diff --git a/tests/unit_tests/test_stats.py b/tests/unit_tests/test_stats.py index 5451c0a2c..0414fc225 100644 --- a/tests/unit_tests/test_stats.py +++ b/tests/unit_tests/test_stats.py @@ -50,6 +50,13 @@ def test_discrete(): assert_sample_mean(samples, exp_mean) +def test_delta_function(): + d = openmc.stats.delta_function(14.1e6) + assert isinstance(d, openmc.stats.Discrete) + np.testing.assert_array_equal(d.x, [14.1e6]) + np.testing.assert_array_equal(d.p, [1.0]) + + def test_merge_discrete(): x1 = [0.0, 1.0, 10.0] p1 = [0.3, 0.2, 0.5] From 39a2d46e2636707d11f951fb4f206c2155c77198 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Sun, 18 Aug 2024 23:09:32 -0500 Subject: [PATCH 165/671] Implement bounding_box operation for meshes (#3119) --- cmake/Modules/FindLIBMESH.cmake | 2 +- include/openmc/bounding_box.h | 61 +++++++++++++++++++++++++++++++++ include/openmc/cell.h | 1 + include/openmc/mesh.h | 52 +++++++++++++++++++++++++--- include/openmc/surface.h | 50 +-------------------------- include/openmc/universe.h | 1 + openmc/bounding_box.py | 3 +- openmc/lib/mesh.py | 30 ++++++++++++++++ src/mesh.cpp | 60 +++++++++++++++++++++++++++++--- src/particle.cpp | 2 +- tests/unit_tests/test_lib.py | 21 ++++++++++++ 11 files changed, 223 insertions(+), 60 deletions(-) create mode 100644 include/openmc/bounding_box.h diff --git a/cmake/Modules/FindLIBMESH.cmake b/cmake/Modules/FindLIBMESH.cmake index df5b5b9d8..048dfc2a8 100644 --- a/cmake/Modules/FindLIBMESH.cmake +++ b/cmake/Modules/FindLIBMESH.cmake @@ -14,7 +14,7 @@ if(DEFINED ENV{METHOD}) message(STATUS "Using environment variable METHOD to determine libMesh build: ${LIBMESH_PC_FILE}") endif() -include(FindPkgConfig) +find_package(PkgConfig REQUIRED) set(ENV{PKG_CONFIG_PATH} "$ENV{PKG_CONFIG_PATH}:${LIBMESH_PC}") set(PKG_CONFIG_USE_CMAKE_PREFIX_PATH True) pkg_check_modules(LIBMESH REQUIRED ${LIBMESH_PC_FILE}>=1.7.0 IMPORTED_TARGET) diff --git a/include/openmc/bounding_box.h b/include/openmc/bounding_box.h new file mode 100644 index 000000000..40f603583 --- /dev/null +++ b/include/openmc/bounding_box.h @@ -0,0 +1,61 @@ +#ifndef OPENMC_BOUNDING_BOX_H +#define OPENMC_BOUNDING_BOX_H + +#include // for min, max + +#include "openmc/constants.h" + +namespace openmc { + +//============================================================================== +//! Coordinates for an axis-aligned cuboid that bounds a geometric object. +//============================================================================== + +struct BoundingBox { + double xmin = -INFTY; + double xmax = INFTY; + double ymin = -INFTY; + double ymax = INFTY; + double zmin = -INFTY; + double zmax = INFTY; + + inline BoundingBox operator&(const BoundingBox& other) + { + BoundingBox result = *this; + return result &= other; + } + + inline BoundingBox operator|(const BoundingBox& other) + { + BoundingBox result = *this; + return result |= other; + } + + // intersect operator + inline BoundingBox& operator&=(const BoundingBox& other) + { + xmin = std::max(xmin, other.xmin); + xmax = std::min(xmax, other.xmax); + ymin = std::max(ymin, other.ymin); + ymax = std::min(ymax, other.ymax); + zmin = std::max(zmin, other.zmin); + zmax = std::min(zmax, other.zmax); + return *this; + } + + // union operator + inline BoundingBox& operator|=(const BoundingBox& other) + { + xmin = std::min(xmin, other.xmin); + xmax = std::max(xmax, other.xmax); + ymin = std::min(ymin, other.ymin); + ymax = std::max(ymax, other.ymax); + zmin = std::min(zmin, other.zmin); + zmax = std::max(zmax, other.zmax); + return *this; + } +}; + +} // namespace openmc + +#endif diff --git a/include/openmc/cell.h b/include/openmc/cell.h index d78614f05..70843140b 100644 --- a/include/openmc/cell.h +++ b/include/openmc/cell.h @@ -12,6 +12,7 @@ #include "pugixml.hpp" #include +#include "openmc/bounding_box.h" #include "openmc/constants.h" #include "openmc/memory.h" // for unique_ptr #include "openmc/neighbor_list.h" diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index 3917e6368..6f3f2b0a4 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -11,6 +11,7 @@ #include "xtensor/xtensor.hpp" #include +#include "openmc/bounding_box.h" #include "openmc/error.h" #include "openmc/memory.h" // for unique_ptr #include "openmc/particle.h" @@ -185,9 +186,24 @@ public: vector material_volumes( int n_sample, int bin, uint64_t* seed) const; + //! Determine bounding box of mesh + // + //! \return Bounding box of mesh + BoundingBox bounding_box() const + { + auto ll = this->lower_left(); + auto ur = this->upper_right(); + return {ll.x, ur.x, ll.y, ur.y, ll.z, ur.z}; + } + + virtual Position lower_left() const = 0; + virtual Position upper_right() const = 0; + // Data members - int id_ {-1}; //!< User-specified ID - int n_dimension_ {-1}; //!< Number of dimensions + xt::xtensor lower_left_; //!< Lower-left coordinates of mesh + xt::xtensor upper_right_; //!< Upper-right coordinates of mesh + int id_ {-1}; //!< User-specified ID + int n_dimension_ {-1}; //!< Number of dimensions }; class StructuredMesh : public Mesh { @@ -325,14 +341,30 @@ public: return this->volume(get_indices_from_bin(bin)); } + Position lower_left() const override + { + int n = lower_left_.size(); + Position ll {lower_left_[0], 0.0, 0.0}; + ll.y = (n >= 2) ? lower_left_[1] : -INFTY; + ll.z = (n == 3) ? lower_left_[2] : -INFTY; + return ll; + }; + + Position upper_right() const override + { + int n = upper_right_.size(); + Position ur {upper_right_[0], 0.0, 0.0}; + ur.y = (n >= 2) ? upper_right_[1] : INFTY; + ur.z = (n == 3) ? upper_right_[2] : INFTY; + return ur; + }; + //! Get the volume of a specified element //! \param[in] ijk Mesh index to return the volume for //! \return Volume of the bin virtual double volume(const MeshIndex& ijk) const = 0; // Data members - xt::xtensor lower_left_; //!< Lower-left coordinates of mesh - xt::xtensor upper_right_; //!< Upper-right coordinates of mesh std::array shape_; //!< Number of mesh elements in each dimension protected: @@ -655,6 +687,15 @@ public: ElementType element_type(int bin) const; + Position lower_left() const override + { + return {lower_left_[0], lower_left_[1], lower_left_[2]}; + } + Position upper_right() const override + { + return {upper_right_[0], upper_right_[1], upper_right_[2]}; + } + protected: //! Set the length multiplier to apply to each point in the mesh void set_length_multiplier(const double length_multiplier); @@ -672,6 +713,9 @@ protected: -1.0}; //!< Multiplicative factor applied to mesh coordinates std::string options_; //!< Options for search data structures + //! Determine lower-left and upper-right bounds of mesh + void determine_bounds(); + private: //! Setup method for the mesh. Builds data structures, //! sets up element mapping, creates bounding boxes, etc. diff --git a/include/openmc/surface.h b/include/openmc/surface.h index 350775123..af235301c 100644 --- a/include/openmc/surface.h +++ b/include/openmc/surface.h @@ -9,6 +9,7 @@ #include "pugixml.hpp" #include "openmc/boundary_condition.h" +#include "openmc/bounding_box.h" #include "openmc/constants.h" #include "openmc/memory.h" // for unique_ptr #include "openmc/particle.h" @@ -28,55 +29,6 @@ extern std::unordered_map surface_map; extern vector> surfaces; } // namespace model -//============================================================================== -//! Coordinates for an axis-aligned cuboid that bounds a geometric object. -//============================================================================== - -struct BoundingBox { - double xmin = -INFTY; - double xmax = INFTY; - double ymin = -INFTY; - double ymax = INFTY; - double zmin = -INFTY; - double zmax = INFTY; - - inline BoundingBox operator&(const BoundingBox& other) - { - BoundingBox result = *this; - return result &= other; - } - - inline BoundingBox operator|(const BoundingBox& other) - { - BoundingBox result = *this; - return result |= other; - } - - // intersect operator - inline BoundingBox& operator&=(const BoundingBox& other) - { - xmin = std::max(xmin, other.xmin); - xmax = std::min(xmax, other.xmax); - ymin = std::max(ymin, other.ymin); - ymax = std::min(ymax, other.ymax); - zmin = std::max(zmin, other.zmin); - zmax = std::min(zmax, other.zmax); - return *this; - } - - // union operator - inline BoundingBox& operator|=(const BoundingBox& other) - { - xmin = std::min(xmin, other.xmin); - xmax = std::max(xmax, other.xmax); - ymin = std::min(ymin, other.ymin); - ymax = std::max(ymax, other.ymax); - zmin = std::min(zmin, other.zmin); - zmax = std::max(zmax, other.zmax); - return *this; - } -}; - //============================================================================== //! A geometry primitive used to define regions of 3D space. //============================================================================== diff --git a/include/openmc/universe.h b/include/openmc/universe.h index 26f33cb38..9fea06bcc 100644 --- a/include/openmc/universe.h +++ b/include/openmc/universe.h @@ -1,6 +1,7 @@ #ifndef OPENMC_UNIVERSE_H #define OPENMC_UNIVERSE_H +#include "openmc/bounding_box.h" #include "openmc/cell.h" namespace openmc { diff --git a/openmc/bounding_box.py b/openmc/bounding_box.py index 5f3d3a6cf..f0dc06a4a 100644 --- a/openmc/bounding_box.py +++ b/openmc/bounding_box.py @@ -42,7 +42,8 @@ class BoundingBox: def __repr__(self) -> str: return "BoundingBox(lower_left={}, upper_right={})".format( - tuple(self.lower_left), tuple(self.upper_right)) + tuple(float(x) for x in self.lower_left), + tuple(float(x) for x in self.upper_right)) def __getitem__(self, key) -> np.ndarray: return self._bounds[key] diff --git a/openmc/lib/mesh.py b/openmc/lib/mesh.py index 78566d449..16bec0198 100644 --- a/openmc/lib/mesh.py +++ b/openmc/lib/mesh.py @@ -2,6 +2,7 @@ from collections.abc import Mapping, Sequence from ctypes import (c_int, c_int32, c_char_p, c_double, POINTER, Structure, create_string_buffer, c_uint64, c_size_t) from random import getrandbits +import sys from weakref import WeakValueDictionary import numpy as np @@ -13,6 +14,7 @@ from .core import _FortranObjectWithID from .error import _error_handler from .material import Material from .plot import _Position +from ..bounding_box import BoundingBox __all__ = [ 'Mesh', 'RegularMesh', 'RectilinearMesh', 'CylindricalMesh', @@ -44,6 +46,10 @@ _dll.openmc_mesh_get_n_elements.errcheck = _error_handler _dll.openmc_mesh_get_volumes.argtypes = [c_int32, POINTER(c_double)] _dll.openmc_mesh_get_volumes.restype = c_int _dll.openmc_mesh_get_volumes.errcheck = _error_handler +_dll.openmc_mesh_bounding_box.argtypes = [ + c_int32, POINTER(c_double), POINTER(c_double)] +_dll.openmc_mesh_bounding_box.restype = c_int +_dll.openmc_mesh_bounding_box.errcheck = _error_handler _dll.openmc_mesh_material_volumes.argtypes = [ c_int32, c_int, c_int, c_int, POINTER(_MaterialVolume), POINTER(c_int), POINTER(c_uint64)] @@ -166,6 +172,22 @@ class Mesh(_FortranObjectWithID): self._index, volumes.ctypes.data_as(POINTER(c_double))) return volumes + @property + def bounding_box(self) -> BoundingBox: + inf = sys.float_info.max + ll = np.zeros(3) + ur = np.zeros(3) + _dll.openmc_mesh_bounding_box( + self._index, + ll.ctypes.data_as(POINTER(c_double)), + ur.ctypes.data_as(POINTER(c_double)) + ) + ll[ll == inf] = np.inf + ur[ur == inf] = np.inf + ll[ll == -inf] = -np.inf + ur[ur == -inf] = -np.inf + return BoundingBox(ll, ur) + def material_volumes( self, n_samples: int = 10_000, @@ -292,6 +314,8 @@ class RegularMesh(Mesh): Total number of mesh elements. volumes : numpy.ndarray Volume of each mesh element in [cm^3] + bounding_box : openmc.BoundingBox + Axis-aligned bounding box of the mesh """ mesh_type = 'regular' @@ -378,6 +402,8 @@ class RectilinearMesh(Mesh): Total number of mesh elements. volumes : numpy.ndarray Volume of each mesh element in [cm^3] + bounding_box : openmc.BoundingBox + Axis-aligned bounding box of the mesh """ mesh_type = 'rectilinear' @@ -481,6 +507,8 @@ class CylindricalMesh(Mesh): Total number of mesh elements. volumes : numpy.ndarray Volume of each mesh element in [cm^3] + bounding_box : openmc.BoundingBox + Axis-aligned bounding box of the mesh """ mesh_type = 'cylindrical' @@ -584,6 +612,8 @@ class SphericalMesh(Mesh): Total number of mesh elements. volumes : numpy.ndarray Volume of each mesh element in [cm^3] + bounding_box : openmc.BoundingBox + Axis-aligned bounding box of the mesh """ mesh_type = 'spherical' diff --git a/src/mesh.cpp b/src/mesh.cpp index c57dd5dcc..0f8b4b14e 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -319,6 +319,28 @@ UnstructuredMesh::UnstructuredMesh(pugi::xml_node node) : Mesh(node) } } +void UnstructuredMesh::determine_bounds() +{ + double xmin = INFTY; + double ymin = INFTY; + double zmin = INFTY; + double xmax = -INFTY; + double ymax = -INFTY; + double zmax = -INFTY; + int n = this->n_vertices(); + for (int i = 0; i < n; ++i) { + auto v = this->vertex(i); + xmin = std::min(v.x, xmin); + ymin = std::min(v.y, ymin); + zmin = std::min(v.z, zmin); + xmax = std::max(v.x, xmax); + ymax = std::max(v.y, ymax); + zmax = std::max(v.z, zmax); + } + lower_left_ = {xmin, ymin, zmin}; + upper_right_ = {xmax, ymax, zmax}; +} + Position UnstructuredMesh::sample_tet( std::array coords, uint64_t* seed) const { @@ -1372,8 +1394,10 @@ int CylindricalMesh::set_grid() full_phi_ = (grid_[1].front() == 0.0) && (grid_[1].back() == 2.0 * PI); - lower_left_ = {grid_[0].front(), grid_[1].front(), grid_[2].front()}; - upper_right_ = {grid_[0].back(), grid_[1].back(), grid_[2].back()}; + lower_left_ = {origin_[0] - grid_[0].back(), origin_[1] - grid_[0].back(), + origin_[2] + grid_[2].front()}; + upper_right_ = {origin_[0] + grid_[0].back(), origin_[1] + grid_[0].back(), + origin_[2] + grid_[2].back()}; return 0; } @@ -1687,8 +1711,9 @@ int SphericalMesh::set_grid() full_theta_ = (grid_[1].front() == 0.0) && (grid_[1].back() == PI); full_phi_ = (grid_[2].front() == 0.0) && (grid_[2].back() == 2 * PI); - lower_left_ = {grid_[0].front(), grid_[1].front(), grid_[2].front()}; - upper_right_ = {grid_[0].back(), grid_[1].back(), grid_[2].back()}; + double r = grid_[0].back(); + lower_left_ = {origin_[0] - r, origin_[1] - r, origin_[2] - r}; + upper_right_ = {origin_[0] + r, origin_[1] + r, origin_[2] + r}; return 0; } @@ -1899,6 +1924,26 @@ extern "C" int openmc_mesh_get_volumes(int32_t index, double* volumes) return 0; } +//! Get the bounding box of a mesh +extern "C" int openmc_mesh_bounding_box(int32_t index, double* ll, double* ur) +{ + if (int err = check_mesh(index)) + return err; + + BoundingBox bbox = model::meshes[index]->bounding_box(); + + // set lower left corner values + ll[0] = bbox.xmin; + ll[1] = bbox.ymin; + ll[2] = bbox.zmin; + + // set upper right corner values + ur[0] = bbox.xmax; + ur[1] = bbox.ymax; + ur[2] = bbox.zmax; + return 0; +} + extern "C" int openmc_mesh_material_volumes(int32_t index, int n_sample, int bin, int result_size, void* result, int* hits, uint64_t* seed) { @@ -2265,6 +2310,9 @@ void MOABMesh::initialize() } } } + + // Determine bounds of mesh + this->determine_bounds(); } void MOABMesh::prepare_for_tallies() @@ -2952,6 +3000,10 @@ void LibMesh::initialize() // bounding box for the mesh for quick rejection checks bbox_ = libMesh::MeshTools::create_bounding_box(*m_); + libMesh::Point ll = bbox_.min(); + libMesh::Point ur = bbox_.max(); + lower_left_ = {ll(0), ll(1), ll(2)}; + upper_right_ = {ur(0), ur(1), ur(2)}; } // Sample position within a tet for LibMesh type tets diff --git a/src/particle.cpp b/src/particle.cpp index 7d30e26bd..64c50c943 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -296,7 +296,7 @@ void Particle::event_cross_surface() if (surf->surf_source_ && surf->bc_) { add_surf_source_to_bank(*this, *surf); } - 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); diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index e3c7ce3b7..64c16c238 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -570,6 +570,12 @@ def test_regular_mesh(lib_init): np.testing.assert_allclose(mesh.volumes, 1.0) + # bounding box + mesh.set_parameters(lower_left=ll, upper_right=ur) + bbox = mesh.bounding_box + np.testing.assert_allclose(bbox.lower_left, ll) + np.testing.assert_allclose(bbox.upper_right, ur) + meshes = openmc.lib.meshes assert isinstance(meshes, Mapping) assert len(meshes) == 1 @@ -650,6 +656,11 @@ def test_rectilinear_mesh(lib_init): np.testing.assert_allclose(mesh.volumes, 1000.0) + # bounding box + bbox = mesh.bounding_box + np.testing.assert_allclose(bbox.lower_left, (-10., 0., 10.)) + np.testing.assert_allclose(bbox.upper_right, (10., 20., 30.)) + with pytest.raises(exc.AllocationError): mesh2 = openmc.lib.RectilinearMesh(mesh.id) @@ -697,6 +708,11 @@ def test_cylindrical_mesh(lib_init): np.testing.assert_allclose(mesh.volumes[::2], 10/360 * pi * 5**2 * 10) np.testing.assert_allclose(mesh.volumes[1::2], 10/360 * pi * (10**2 - 5**2) * 10) + # bounding box + bbox = mesh.bounding_box + np.testing.assert_allclose(bbox.lower_left, (-10., -10., 10.)) + np.testing.assert_allclose(bbox.upper_right, (10., 10., 30.)) + with pytest.raises(exc.AllocationError): mesh2 = openmc.lib.CylindricalMesh(mesh.id) @@ -750,6 +766,11 @@ def test_spherical_mesh(lib_init): np.testing.assert_allclose(mesh.volumes[2::4], f * 5**3 * dtheta(10., 20.)) np.testing.assert_allclose(mesh.volumes[3::4], f * (10**3 - 5**3) * dtheta(10., 20.)) + # bounding box + bbox = mesh.bounding_box + np.testing.assert_allclose(bbox.lower_left, (-10., -10., -10.)) + np.testing.assert_allclose(bbox.upper_right, (10., 10., 10.)) + with pytest.raises(exc.AllocationError): mesh2 = openmc.lib.SphericalMesh(mesh.id) From 86fc40a6456e789676eec3410afff38e340ea8f8 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 19 Aug 2024 10:07:28 -0500 Subject: [PATCH 166/671] Add OrthogonalBox composite surface (#3118) --- docs/source/pythonapi/model.rst | 1 + openmc/model/surface_composite.py | 80 ++++++++++++++++++++++ openmc/surface.py | 13 +++- tests/unit_tests/test_surface_composite.py | 54 +++++++++++++++ 4 files changed, 145 insertions(+), 3 deletions(-) diff --git a/docs/source/pythonapi/model.rst b/docs/source/pythonapi/model.rst index 99459f64d..21944018e 100644 --- a/docs/source/pythonapi/model.rst +++ b/docs/source/pythonapi/model.rst @@ -26,6 +26,7 @@ Composite Surfaces openmc.model.CylinderSector openmc.model.HexagonalPrism openmc.model.IsogonalOctagon + openmc.model.OrthogonalBox openmc.model.Polygon openmc.model.RectangularParallelepiped openmc.model.RectangularPrism diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index 29411fe46..a2cb02438 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -645,6 +645,86 @@ class RectangularParallelepiped(CompositeSurface): return +self.xmax | -self.xmin | +self.ymax | -self.ymin | +self.zmax | -self.zmin +class OrthogonalBox(CompositeSurface): + """Arbitrarily oriented orthogonal box + + This composite surface is composed of four or six planar surfaces that form + an arbitrarily oriented orthogonal box when combined. + + Parameters + ---------- + v : iterable of float + (x,y,z) coordinates of a corner of the box + a1 : iterable of float + Vector of first side starting from ``v`` + a2 : iterable of float + Vector of second side starting from ``v`` + a3 : iterable of float, optional + Vector of third side starting from ``v``. When not specified, it is + assumed that the box will be infinite along the vector normal to the + plane specified by ``a1`` and ``a2``. + **kwargs + Keyword arguments passed to underlying plane classes + + Attributes + ---------- + ax1_min, ax1_max : openmc.Plane + Planes representing minimum and maximum along first axis + ax2_min, ax2_max : openmc.Plane + Planes representing minimum and maximum along second axis + ax3_min, ax3_max : openmc.Plane + Planes representing minimum and maximum along third axis + + """ + _surface_names = ('ax1_min', 'ax1_max', 'ax2_min', 'ax2_max', 'ax3_min', 'ax3_max') + + def __init__(self, v, a1, a2, a3=None, **kwargs): + v = np.array(v) + a1 = np.array(a1) + a2 = np.array(a2) + if has_a3 := a3 is not None: + a3 = np.array(a3) + else: + a3 = np.cross(a1, a2) # normal to plane specified by a1 and a2 + + # Generate corners of box + p1 = v + p2 = v + a1 + p3 = v + a2 + p4 = v + a3 + p5 = v + a1 + a2 + p6 = v + a2 + a3 + p7 = v + a1 + a3 + + # Generate 6 planes of box + self.ax1_min = openmc.Plane.from_points(p1, p3, p4, **kwargs) + self.ax1_max = openmc.Plane.from_points(p2, p5, p7, **kwargs) + self.ax2_min = openmc.Plane.from_points(p1, p4, p2, **kwargs) + self.ax2_max = openmc.Plane.from_points(p3, p6, p5, **kwargs) + if has_a3: + self.ax3_min = openmc.Plane.from_points(p1, p2, p3, **kwargs) + self.ax3_max = openmc.Plane.from_points(p4, p7, p6, **kwargs) + + # Make sure a point inside the box produces the correct senses. If not, + # flip the plane coefficients so it does. + mid_point = v + (a1 + a2 + a3)/2 + nums = (1, 2, 3) if has_a3 else (1, 2) + for num in nums: + min_surf = getattr(self, f'ax{num}_min') + max_surf = getattr(self, f'ax{num}_max') + if mid_point in -min_surf: + min_surf.flip_normal() + if mid_point in +max_surf: + max_surf.flip_normal() + + def __neg__(self): + region = (+self.ax1_min & -self.ax1_max & + +self.ax2_min & -self.ax2_max) + if hasattr(self, 'ax3_min'): + region &= (+self.ax3_min & -self.ax3_max) + return region + + class XConeOneSided(CompositeSurface): """One-sided cone parallel the x-axis diff --git a/openmc/surface.py b/openmc/surface.py index 0fa2ae439..2f10750a8 100644 --- a/openmc/surface.py +++ b/openmc/surface.py @@ -802,6 +802,13 @@ class Plane(PlaneMixin, Surface): d = np.dot(n, p1) return cls(a=a, b=b, c=c, d=d, **kwargs) + def flip_normal(self): + """Modify plane coefficients to reverse the normal vector.""" + self.a = -self.a + self.b = -self.b + self.c = -self.c + self.d = -self.d + class XPlane(PlaneMixin, Surface): """A plane perpendicular to the x axis of the form :math:`x - x_0 = 0` @@ -1762,7 +1769,7 @@ class Cone(QuadricMixin, Surface): z-coordinate of the apex in [cm]. Defaults to 0. r2 : float, optional Parameter related to the aperture [:math:`\\rm cm^2`]. - It can be interpreted as the increase in the radius squared per cm along + It can be interpreted as the increase in the radius squared per cm along the cone's axis of revolution. dx : float, optional x-component of the vector representing the axis of the cone. @@ -1920,7 +1927,7 @@ class XCone(QuadricMixin, Surface): z-coordinate of the apex in [cm]. Defaults to 0. r2 : float, optional Parameter related to the aperture [:math:`\\rm cm^2`]. - It can be interpreted as the increase in the radius squared per cm along + It can be interpreted as the increase in the radius squared per cm along the cone's axis of revolution. boundary_type : {'transmission, 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the @@ -2021,7 +2028,7 @@ class YCone(QuadricMixin, Surface): z-coordinate of the apex in [cm]. Defaults to 0. r2 : float, optional Parameter related to the aperture [:math:`\\rm cm^2`]. - It can be interpreted as the increase in the radius squared per cm along + It can be interpreted as the increase in the radius squared per cm along the cone's axis of revolution. boundary_type : {'transmission, 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the diff --git a/tests/unit_tests/test_surface_composite.py b/tests/unit_tests/test_surface_composite.py index 01c827223..d862ae6b0 100644 --- a/tests/unit_tests/test_surface_composite.py +++ b/tests/unit_tests/test_surface_composite.py @@ -498,3 +498,57 @@ def test_cruciform_prism(axis): openmc.model.CruciformPrism([1.0, 0.5, 2.0, 3.0]) with pytest.raises(ValueError): openmc.model.CruciformPrism([3.0, 2.0, 0.5, 1.0]) + + +def test_box(): + v = (-1.0, -1.0, -2.5) + a1 = (2.0, -1.0, 0.0) + a2 = (1.0, 2.0, 0.0) + a3 = (0.0, 0.0, 5.0) + s = openmc.model.OrthogonalBox(v, a1, a2, a3) + for num in (1, 2, 3): + assert isinstance(getattr(s, f'ax{num}_min'), openmc.Plane) + assert isinstance(getattr(s, f'ax{num}_max'), openmc.Plane) + + # Make sure boundary condition propagates + s.boundary_type = 'reflective' + assert s.boundary_type == 'reflective' + for num in (1, 2, 3): + assert getattr(s, f'ax{num}_min').boundary_type == 'reflective' + assert getattr(s, f'ax{num}_max').boundary_type == 'reflective' + + # Check bounding box + ll, ur = (+s).bounding_box + assert np.all(np.isinf(ll)) + assert np.all(np.isinf(ur)) + ll, ur = (-s).bounding_box + assert ll[2] == pytest.approx(-2.5) + assert ur[2] == pytest.approx(2.5) + + # __contains__ on associated half-spaces + assert (0., 0., 0.) in -s + assert (-2., 0., 0.) not in -s + assert (0., 0.9, 0.) in -s + assert (0., 0., -3.) in +s + assert (0., 0., 3.) in +s + + # translate method + s_t = s.translate((1., 1., 0.)) + assert (-0.01, 0., 0.) in +s_t + assert (0.01, 0., 0.) in -s_t + + # Make sure repr works + repr(s) + + # Version with infinite 3rd dimension + s = openmc.model.OrthogonalBox(v, a1, a2) + assert not hasattr(s, 'ax3_min') + assert not hasattr(s, 'ax3_max') + ll, ur = (-s).bounding_box + assert np.all(np.isinf(ll)) + assert np.all(np.isinf(ur)) + assert (0., 0., 0.) in -s + assert (-2., 0., 0.) not in -s + assert (0., 0.9, 0.) in -s + assert (0., 0., -3.) not in +s + assert (0., 0., 3.) not in +s From 5bc04b5d78b83684685ccf53564498493e2b6a93 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Fri, 23 Aug 2024 11:56:26 -0500 Subject: [PATCH 167/671] Alternative Random Ray Volume Estimators (#3060) Co-authored-by: Olek <45364492+yardasol@users.noreply.github.com> --- docs/source/methods/random_ray.rst | 124 ++++++-- docs/source/usersguide/random_ray.rst | 58 ++++ include/openmc/constants.h | 1 + .../openmc/random_ray/flat_source_domain.h | 19 +- .../openmc/random_ray/linear_source_domain.h | 9 +- include/openmc/random_ray/random_ray.h | 2 + .../openmc/random_ray/random_ray_simulation.h | 1 + openmc/settings.py | 10 + src/random_ray/flat_source_domain.cpp | 173 +++++++---- src/random_ray/linear_source_domain.cpp | 82 ++--- src/random_ray/random_ray.cpp | 12 - src/random_ray/random_ray_simulation.cpp | 18 +- src/settings.cpp | 14 + .../cell/results_true.dat | 12 +- .../material/results_true.dat | 12 +- .../universe/results_true.dat | 12 +- .../linear/inputs_true.dat | 4 +- .../linear/results_true.dat | 12 +- .../linear_xy/inputs_true.dat | 4 +- .../linear_xy/results_true.dat | 12 +- .../random_ray_fixed_source_linear/test.py | 4 +- .../False/results_true.dat | 12 +- .../True/results_true.dat | 12 +- .../flat/results_true.dat | 168 +++++------ .../linear_xy/results_true.dat | 168 +++++------ .../random_ray_k_eff/results_true.dat | 124 ++++---- .../random_ray_linear/linear/inputs_true.dat | 4 +- .../random_ray_linear/linear/results_true.dat | 282 +++++++++--------- .../linear_xy/inputs_true.dat | 4 +- .../linear_xy/results_true.dat | 282 +++++++++--------- .../random_ray_linear/test.py | 4 +- .../random_ray_volume_estimator/__init__.py | 0 .../hybrid/inputs_true.dat | 245 +++++++++++++++ .../hybrid/results_true.dat | 9 + .../naive/inputs_true.dat | 245 +++++++++++++++ .../naive/results_true.dat | 9 + .../simulation_averaged/inputs_true.dat | 245 +++++++++++++++ .../simulation_averaged/results_true.dat | 9 + .../random_ray_volume_estimator/test.py | 30 ++ .../__init__.py | 0 .../hybrid/inputs_true.dat | 246 +++++++++++++++ .../hybrid/results_true.dat | 9 + .../naive/inputs_true.dat | 246 +++++++++++++++ .../naive/results_true.dat | 9 + .../simulation_averaged/inputs_true.dat | 246 +++++++++++++++ .../simulation_averaged/results_true.dat | 9 + .../test.py | 32 ++ 47 files changed, 2517 insertions(+), 727 deletions(-) create mode 100644 tests/regression_tests/random_ray_volume_estimator/__init__.py create mode 100644 tests/regression_tests/random_ray_volume_estimator/hybrid/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator/hybrid/results_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator/naive/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator/naive/results_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator/simulation_averaged/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator/simulation_averaged/results_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator/test.py create mode 100644 tests/regression_tests/random_ray_volume_estimator_linear/__init__.py create mode 100644 tests/regression_tests/random_ray_volume_estimator_linear/hybrid/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator_linear/hybrid/results_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator_linear/naive/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator_linear/naive/results_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/results_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator_linear/test.py diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index 9f8eb84d8..3d98747e4 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -411,7 +411,7 @@ which when partially simplified becomes: Note that there are now four (seemingly identical) volume terms in this equation. -.. _methods-volume-dilemma: +.. _methods_random_ray_vol: ~~~~~~~~~~~~~~ Volume Dilemma @@ -440,9 +440,11 @@ features stochastic variables (the sums over random ray lengths and angular fluxes) in both the numerator and denominator, making it a stochastic ratio estimator, which is inherently biased. In practice, usage of the naive estimator does result in a biased, but "consistent" estimator (i.e., it is biased, but -the bias tends towards zero as the sample size increases). Experimentally, the -right answer can be obtained with this estimator, though a very fine ray density -is required to eliminate the bias. +the bias tends towards zero as the sample size increases). Empirically, this +bias tends to effect eigenvalue calculations much more significantly than in +fixed source simulations. Experimentally, the right answer can be obtained with +this estimator, though for eigenvalue simulations a very fine ray density is +required to eliminate the bias. How might we solve the biased ratio estimator problem? While there is no obvious way to alter the numerator term (which arises from the characteristic @@ -463,17 +465,17 @@ replace the actual tracklength that was accumulated inside that FSR each iteration with the expected value. If we know the analytical volumes, then those can be used to directly compute -the expected value of the tracklength in each cell. However, as the analytical -volumes are not typically known in OpenMC due to the usage of user-defined -constructive solid geometry, we need to source this quantity from elsewhere. An -obvious choice is to simply accumulate the total tracklength through each FSR -across all iterations (batches) and to use that sum to compute the expected -average length per iteration, as: +the expected value of the tracklength in each cell, :math:`L_{avg}`. However, as +the analytical volumes are not typically known in OpenMC due to the usage of +user-defined constructive solid geometry, we need to source this quantity from +elsewhere. An obvious choice is to simply accumulate the total tracklength +through each FSR across all iterations (batches) and to use that sum to compute +the expected average length per iteration, as: .. math:: - :label: sim_estimator + :label: L_avg - \sum\limits^{}_{i} \ell_i \approx \frac{\sum\limits^{B}_{b}\sum\limits^{N_i}_{r} \ell_{b,r} }{B} + \sum\limits^{}_{i} \ell_i \approx L_{avg} = \frac{\sum\limits^{B}_{b}\sum\limits^{N_i}_{r=1} \ell_{b,r} }{B} where :math:`b` is a single batch in :math:`B` total batches simulated so far. @@ -486,7 +488,7 @@ averaged" estimator is therefore: .. math:: :label: phi_sim - \phi_{i,g}^{simulation} = \frac{Q_{i,g} }{\Sigma_{t,i,g}} + \frac{\sum\limits_{r=1}^{N_i} \Delta \psi_{r,g}}{\Sigma_{t,i,g} \frac{\sum\limits^{B}_{b}\sum\limits^{N_i}_{r} \ell_{b,r} }{B}} + \phi_{i,g}^{simulation} = \frac{Q_{i,g} }{\Sigma_{t,i,g}} + \frac{\sum\limits_{r=1}^{N_i} \Delta \psi_{r,g}}{\Sigma_{t,i,g} L_{avg}} In practical terms, the "simulation averaged" estimator is virtually indistinguishable numerically from use of the true analytical volume to estimate @@ -500,17 +502,81 @@ in which case the denominator served as a normalization term for the numerator integral in Equation :eq:`integral`. Essentially, we have now used a different term for the volume in the numerator as compared to the normalizing volume in the denominator. The inevitable mismatch (due to noise) between these two -quantities results in a significant increase in variance. Notably, the same -problem occurs if using a tracklength estimate based on the analytical volume, -as again the numerator integral and the normalizing denominator integral no -longer match on a per-iteration basis. +quantities results in a significant increase in variance, and can even result in +the generation of negative fluxes. Notably, the same problem occurs if using a +tracklength estimate based on the analytical volume, as again the numerator +integral and the normalizing denominator integral no longer match on a +per-iteration basis. -In practice, the simulation averaged method does completely remove the bias, -though at the cost of a notable increase in variance. Empirical testing reveals -that on most problems, the simulation averaged estimator does win out overall in -numerical performance, as a much coarser quadrature can be used resulting in -faster runtimes overall. Thus, OpenMC uses the simulation averaged estimator in -its random ray mode. +In practice, the simulation averaged method does completely remove the bias seen +when using the naive estimator, though at the cost of a notable increase in +variance. Empirical testing reveals that on most eigenvalue problems, the +simulation averaged estimator does win out overall in numerical performance, as +a much coarser quadrature can be used resulting in faster runtimes overall. +Thus, OpenMC uses the simulation averaged estimator as default in its random ray +mode for eigenvalue solves. + +OpenMC also features a "hybrid" volume estimator that uses the naive estimator +for all regions containing an external (fixed) source term. For all other +source regions, the "simulation averaged" estimator is used. This typically achieves +a best of both worlds result, with the benefits of the low bias simulation averaged +estimator in most regions, while preventing instability and/or large biases in regions +with external source terms via use of the naive estimator. In general, it is +recommended to use the "hybrid" estimator, which is the default method used +in OpenMC. If instability is encountered despite high ray densities, then +the naive estimator may be preferable. + +A table that summarizes the pros and cons, as well as recommendations for +different use cases, is given in the :ref:`volume +estimators` section of the user guide. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +What Happens When a Source Region is Missed? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Given the stochastic nature of random ray, when low ray densities are used it is +common for small source regions to occasionally not be hit by any rays in a +particular power iteration :math:`n`. This naturally collapses the flux estimate +in that cell for the iteration from Equation :eq:`phi_naive` to: + +.. math:: + :label: phi_missed_one + + \phi_{i,g,n}^{missed} = \frac{Q_{i,g,n} }{\Sigma_{t,i,g}} + +as the streaming operator has gone to zero. While this is obviously innacurate +as it ignores transport, for most problems where the region is only occasionally +missed this estimator does not tend to introduce any significant bias. + +However, in cases where the total cross section in the region is very small +(e.g., a void-like material) and where a strong external fixed source has been +placed, then this treatment causes major issues. In this pathological case, the +lack of transport forces the entirety of the fixed source to effectively be +contained and collided within the cell, which for a low cross section region is +highly unphysical. The net effect is that a very high estimate of the flux +(often orders of magnitude higher than is expected) is generated that iteration, +which cannot be washed out even with hundreds or thousands of iterations. Thus, +huge biases are often seen in spatial tallies containing void-like regions with +external sources unless a high enough ray density is used such that all source +regions are always hit each iteration. This is particularly problematic as +external sources placed in void-like regions are very common in many types of +fixed source analysis. + +For regions where external sources are present, to eliminate this bias it is +therefore preferable to simply use the previous iteration's estimate of the flux +in that cell, as: + +.. math:: + :label: phi_missed_two + + \phi_{i,g,n}^{missed} = \phi_{i,g,n-1} . + +When linear sources are present, the flux moments from the previous iteration +are used in the same manner. While this introduces some small degree of +correlation to the simulation, for miss rates on the order of a few percent the +correlations are trivial and the bias is eliminated. Thus, in OpenMC the +previous iteration's scalar flux estimate is applied to cells that are missed +where there is an external source term present within the cell. ~~~~~~~~~~~~~~~ Power Iteration @@ -563,15 +629,15 @@ total spatial- and energy-integrated fission rate :math:`F^{n-1}` in iteration Notably, the volume term :math:`V_i` appears in the eigenvalue update equation. The same logic applies to the treatment of this term as was discussed earlier. -In OpenMC, we use the "simulation averaged" volume derived from summing over all -ray tracklength contributions to a FSR over all iterations and dividing by the -total integration tracklength to date. Thus, Equation :eq:`fission_source` -becomes: +In OpenMC, we use the "simulation averaged" volume (Equation :eq:`L_avg`) +derived from summing over all ray tracklength contributions to a FSR over all +iterations and dividing by the total integration tracklength to date. Thus, +Equation :eq:`fission_source` becomes: .. math:: :label: fission_source_volumed - F^n = \sum\limits^{M}_{i} \left( \frac{\sum\limits^{B}_{b}\sum\limits^{N_i}_{r} \ell_{b,r} }{B} \sum\limits^{G}_{g} \nu \Sigma_f(i, g) \phi^{n}(g) \right) + F^n = \sum\limits^{M}_{i} \left( L_{avg} \sum\limits^{G}_{g} \nu \Sigma_f(i, g) \phi^{n}(g) \right) and a similar substitution can be made to update Equation :eq:`fission_source_prev` . In OpenMC, the most up-to-date version of the volume @@ -965,7 +1031,7 @@ The Shannon entropy is then computed normally as where :math:`N` is the number of FSRs. FSRs with no fission source (or, occassionally, negative fission source, :ref:`due to the volume estimator -problem `) are skipped to avoid taking an undefined +problem `) are skipped to avoid taking an undefined logarithm in :eq:`shannon-entropy-random-ray`. .. _usersguide_fixed_source_methods: diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index 117d5e23f..5ca0ab6be 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -535,6 +535,64 @@ points of 1.0e-2 and 1.0e1. # Add fixed source and ray sampling source to settings file settings.source = [neutron_source] +.. _usersguide_vol_estimators: + +----------------------------- +Alternative Volume Estimators +----------------------------- + +As discussed in the random ray theory section on :ref:`volume estimators +`, there are several possible derivations for the scalar +flux estimate. These options deal with different ways of treating the +accumulation over ray lengths crossing each FSR (a quantity directly +proportional to volume), which can be computed using several methods. The +following methods are currently available in OpenMC: + +.. list-table:: Comparison of Estimators + :header-rows: 1 + :widths: 10 30 30 30 + + * - Estimator + - Description + - Pros + - Cons + * - ``simulation_averaged`` + - Accumulates total active ray lengths in each FSR over all iterations, + improving the estimate of the volume in each cell each iteration. + - * Virtually unbiased after several iterations + * Asymptotically approaches the true analytical volume + * Typically most efficient in terms of speed vs. accuracy + - * Higher variance + * Can lead to negative fluxes and numerical instability in pathological + cases + * - ``naive`` + - Treats the volume as composed only of the active ray length through each + FSR per iteration, being a biased but numerically consistent ratio + estimator. + - * Low variance + * Unlikely to result in negative fluxes + * Recommended in cases where the simulation averaged estimator is + unstable + - * Biased estimator + * Requires more rays or longer active ray length to mitigate bias + * - ``hybrid`` (default) + - Applies the naive estimator to all cells that contain an external (fixed) + source contribution. Applies the simulation averaged estimator to all + other cells. + - * High accuracy/low bias of the simulation averaged estimator in most + cells + * Stability of the naive estimator in cells with fixed sources + - * Can lead to slightly negative fluxes in cells where the simulation + averaged estimator is used + +These estimators can be selected by setting the ``volume_estimator`` field in the +:attr:`openmc.Settings.random_ray` dictionary. For example, to use the naive +estimator, the following code would be used: + +:: + + settings.random_ray['volume_estimator'] = 'naive' + --------------------------------------- Putting it All Together: Example Inputs --------------------------------------- diff --git a/include/openmc/constants.h b/include/openmc/constants.h index e502506c9..605ae1839 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -342,6 +342,7 @@ enum class RunMode { enum class SolverType { MONTE_CARLO, RANDOM_RAY }; +enum class RandomRayVolumeEstimator { NAIVE, SIMULATION_AVERAGED, HYBRID }; enum class RandomRaySourceShape { FLAT, LINEAR, LINEAR_XY }; //============================================================================== diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index 33c5661dc..5c50f7fb0 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -1,6 +1,7 @@ #ifndef OPENMC_RANDOM_RAY_FLAT_SOURCE_DOMAIN_H #define OPENMC_RANDOM_RAY_FLAT_SOURCE_DOMAIN_H +#include "openmc/constants.h" #include "openmc/openmp_interface.h" #include "openmc/position.h" #include "openmc/source.h" @@ -99,7 +100,8 @@ public: double compute_k_eff(double k_eff_old) const; virtual void normalize_scalar_flux_and_volumes( double total_active_distance_per_iteration); - virtual int64_t add_source_to_scalar_flux(); + + int64_t add_source_to_scalar_flux(); virtual void batch_reset(); void convert_source_regions_to_tallies(); void reset_tally_volumes(); @@ -117,6 +119,10 @@ public: // Static Data members static bool volume_normalized_flux_tallies_; + //---------------------------------------------------------------------------- + // Static data members + static RandomRayVolumeEstimator volume_estimator_; + //---------------------------------------------------------------------------- // Public Data members @@ -132,7 +138,6 @@ public: // 1D arrays representing values for all source regions vector lock_; - vector was_hit_; vector volume_; vector volume_t_; vector position_recorded_; @@ -140,10 +145,11 @@ public: // 2D arrays stored in 1D representing values for all source regions x energy // groups - vector scalar_flux_old_; - vector scalar_flux_new_; + vector scalar_flux_old_; + vector scalar_flux_new_; vector source_; vector external_source_; + vector external_source_present_; protected: //---------------------------------------------------------------------------- @@ -155,6 +161,10 @@ protected: const vector& instances); void apply_external_source_to_cell_and_children(int32_t i_cell, Discrete* discrete, double strength_factor, int32_t target_material_id); + virtual void set_flux_to_flux_plus_source( + int64_t idx, double volume, int material, int g); + void set_flux_to_source(int64_t idx); + virtual void set_flux_to_old_flux(int64_t idx); //---------------------------------------------------------------------------- // Private data members @@ -178,6 +188,7 @@ protected: // 1D arrays representing values for all source regions vector material_; + vector volume_naive_; // 2D arrays stored in 1D representing values for all source regions x energy // groups diff --git a/include/openmc/random_ray/linear_source_domain.h b/include/openmc/random_ray/linear_source_domain.h index 5010ffddd..4812d1433 100644 --- a/include/openmc/random_ray/linear_source_domain.h +++ b/include/openmc/random_ray/linear_source_domain.h @@ -28,7 +28,7 @@ public: double compute_k_eff(double k_eff_old) const; void normalize_scalar_flux_and_volumes( double total_active_distance_per_iteration) override; - int64_t add_source_to_scalar_flux() override; + void batch_reset() override; void convert_source_regions_to_tallies(); void reset_tally_volumes(); @@ -54,6 +54,13 @@ public: vector mom_matrix_; vector mom_matrix_t_; +protected: + //---------------------------------------------------------------------------- + // Methods + void set_flux_to_flux_plus_source( + int64_t idx, double volume, int material, int g) override; + void set_flux_to_old_flux(int64_t idx) override; + }; // class LinearSourceDomain } // namespace openmc diff --git a/include/openmc/random_ray/random_ray.h b/include/openmc/random_ray/random_ray.h index 913a9af4a..96c38da7b 100644 --- a/include/openmc/random_ray/random_ray.h +++ b/include/openmc/random_ray/random_ray.h @@ -43,6 +43,8 @@ public: // Public data members vector angular_flux_; + bool ray_trace_only_ {false}; // If true, only perform geometry operations + private: //---------------------------------------------------------------------------- // Private data members diff --git a/include/openmc/random_ray/random_ray_simulation.h b/include/openmc/random_ray/random_ray_simulation.h index c1d47821d..55bac6905 100644 --- a/include/openmc/random_ray/random_ray_simulation.h +++ b/include/openmc/random_ray/random_ray_simulation.h @@ -19,6 +19,7 @@ public: //---------------------------------------------------------------------------- // Methods + void compute_segment_correction_factors(); void simulate(); void reduce_simulation_statistics(); void output_simulation_results() const; diff --git a/openmc/settings.py b/openmc/settings.py index ce97f138f..ddaac0401 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -155,6 +155,10 @@ class Settings: :ray_source: Starting ray distribution (must be uniform in space and angle) as specified by a :class:`openmc.SourceBase` object. + :volume_estimator: + Choice of volume estimator for the random ray solver. Options are + 'naive', 'simulation_averaged', or 'hybrid'. + The default is 'hybrid'. :source_shape: Assumed shape of the source distribution within each source region. Options are 'flat' (default), 'linear', or 'linear_xy'. @@ -1091,6 +1095,10 @@ class Settings: random_ray[key], 0.0, True) elif key == 'ray_source': cv.check_type('random ray source', random_ray[key], SourceBase) + elif key == 'volume_estimator': + cv.check_value('volume estimator', random_ray[key], + ('naive', 'simulation_averaged', + 'hybrid')) elif key == 'source_shape': cv.check_value('source shape', random_ray[key], ('flat', 'linear', 'linear_xy')) @@ -1889,6 +1897,8 @@ class Settings: elif child.tag == 'source': source = SourceBase.from_xml_element(child) self.random_ray['ray_source'] = source + elif child.tag == 'volume_estimator': + self.random_ray['volume_estimator'] = child.text elif child.tag == 'source_shape': self.random_ray['source_shape'] = child.text elif child.tag == 'volume_normalized_flux_tallies': diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 584b3a7ed..62768b55f 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -24,6 +24,8 @@ namespace openmc { //============================================================================== // Static Variable Declarations +RandomRayVolumeEstimator FlatSourceDomain::volume_estimator_ { + RandomRayVolumeEstimator::HYBRID}; bool FlatSourceDomain::volume_normalized_flux_tallies_ {false}; FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) @@ -49,13 +51,13 @@ FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) position_.resize(n_source_regions_); volume_.assign(n_source_regions_, 0.0); volume_t_.assign(n_source_regions_, 0.0); - was_hit_.assign(n_source_regions_, 0); + volume_naive_.assign(n_source_regions_, 0.0); // Initialize element-wise arrays scalar_flux_new_.assign(n_source_elements_, 0.0); scalar_flux_final_.assign(n_source_elements_, 0.0); source_.resize(n_source_elements_); - external_source_.assign(n_source_elements_, 0.0); + tally_task_.resize(n_source_elements_); volume_task_.resize(n_source_regions_); @@ -64,7 +66,10 @@ FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) scalar_flux_old_.assign(n_source_elements_, 1.0); } else { // If in fixed source mode, set starting flux to guess of zero + // and initialize external source arrays scalar_flux_old_.assign(n_source_elements_, 0.0); + external_source_.assign(n_source_elements_, 0.0); + external_source_present_.assign(n_source_regions_, false); } // Initialize material array @@ -109,9 +114,8 @@ void FlatSourceDomain::batch_reset() { // Reset scalar fluxes, iteration volume tallies, and region hit flags to // zero - parallel_fill(scalar_flux_new_, 0.0f); + parallel_fill(scalar_flux_new_, 0.0); parallel_fill(volume_, 0.0); - parallel_fill(was_hit_, 0); } void FlatSourceDomain::accumulate_iteration_flux() @@ -142,14 +146,14 @@ void FlatSourceDomain::update_neutron_source(double k_eff) int material = material_[sr]; for (int e_out = 0; e_out < negroups_; e_out++) { - float sigma_t = data::mg.macro_xs_[material].get_xs( + double sigma_t = data::mg.macro_xs_[material].get_xs( MgxsType::TOTAL, e_out, nullptr, nullptr, nullptr, t, a); - float scatter_source = 0.0f; + double scatter_source = 0.0f; for (int e_in = 0; e_in < negroups_; e_in++) { - float scalar_flux = scalar_flux_old_[sr * negroups_ + e_in]; + double scalar_flux = scalar_flux_old_[sr * negroups_ + e_in]; - float sigma_s = data::mg.macro_xs_[material].get_xs( + double sigma_s = data::mg.macro_xs_[material].get_xs( MgxsType::NU_SCATTER, e_in, &e_out, nullptr, nullptr, t, a); scatter_source += sigma_s * scalar_flux; } @@ -164,15 +168,15 @@ void FlatSourceDomain::update_neutron_source(double k_eff) int material = material_[sr]; for (int e_out = 0; e_out < negroups_; e_out++) { - float sigma_t = data::mg.macro_xs_[material].get_xs( + double sigma_t = data::mg.macro_xs_[material].get_xs( MgxsType::TOTAL, e_out, nullptr, nullptr, nullptr, t, a); - float fission_source = 0.0f; + double fission_source = 0.0f; for (int e_in = 0; e_in < negroups_; e_in++) { - float scalar_flux = scalar_flux_old_[sr * negroups_ + e_in]; - float nu_sigma_f = data::mg.macro_xs_[material].get_xs( + double scalar_flux = scalar_flux_old_[sr * negroups_ + e_in]; + double nu_sigma_f = data::mg.macro_xs_[material].get_xs( MgxsType::NU_FISSION, e_in, nullptr, nullptr, nullptr, t, a); - float chi = data::mg.macro_xs_[material].get_xs( + double chi = data::mg.macro_xs_[material].get_xs( MgxsType::CHI_PROMPT, e_in, &e_out, nullptr, nullptr, t, a); fission_source += nu_sigma_f * scalar_flux * chi; } @@ -196,7 +200,7 @@ void FlatSourceDomain::update_neutron_source(double k_eff) void FlatSourceDomain::normalize_scalar_flux_and_volumes( double total_active_distance_per_iteration) { - float normalization_factor = 1.0 / total_active_distance_per_iteration; + double normalization_factor = 1.0 / total_active_distance_per_iteration; double volume_normalization_factor = 1.0 / (total_active_distance_per_iteration * simulation::current_batch); @@ -211,58 +215,114 @@ void FlatSourceDomain::normalize_scalar_flux_and_volumes( #pragma omp parallel for for (int64_t sr = 0; sr < n_source_regions_; sr++) { volume_t_[sr] += volume_[sr]; + volume_naive_[sr] = volume_[sr] * normalization_factor; volume_[sr] = volume_t_[sr] * volume_normalization_factor; } } +void FlatSourceDomain::set_flux_to_flux_plus_source( + int64_t idx, double volume, int material, int g) +{ + // Temperature and angle indices, if using multiple temperature + // data sets and/or anisotropic data sets. + // TODO: Currently assumes we are only using single temp/single + // angle data. + const int t = 0; + const int a = 0; + + double sigma_t = data::mg.macro_xs_[material].get_xs( + MgxsType::TOTAL, g, nullptr, nullptr, nullptr, t, a); + + scalar_flux_new_[idx] /= (sigma_t * volume); + scalar_flux_new_[idx] += source_[idx]; +} + +void FlatSourceDomain::set_flux_to_old_flux(int64_t idx) +{ + scalar_flux_new_[idx] = scalar_flux_old_[idx]; +} + +void FlatSourceDomain::set_flux_to_source(int64_t idx) +{ + scalar_flux_new_[idx] = source_[idx]; +} + // Combine transport flux contributions and flat source contributions from the // previous iteration to generate this iteration's estimate of scalar flux. int64_t FlatSourceDomain::add_source_to_scalar_flux() { int64_t n_hits = 0; - // Temperature and angle indices, if using multiple temperature - // data sets and/or anisotropic data sets. - // TODO: Currently assumes we are only using single temp/single - // angle data. - const int t = 0; - const int a = 0; - #pragma omp parallel for reduction(+ : n_hits) for (int sr = 0; sr < n_source_regions_; sr++) { - // Check if this cell was hit this iteration - int was_cell_hit = was_hit_[sr]; - if (was_cell_hit) { + double volume_simulation_avg = volume_[sr]; + double volume_iteration = volume_naive_[sr]; + + // Increment the number of hits if cell was hit this iteration + if (volume_iteration) { n_hits++; } - double volume = volume_[sr]; + // Check if an external source is present in this source region + bool external_source_present = + external_source_present_.size() && external_source_present_[sr]; + + // The volume treatment depends on the volume estimator type + // and whether or not an external source is present in the cell. + double volume; + switch (volume_estimator_) { + case RandomRayVolumeEstimator::NAIVE: + volume = volume_iteration; + break; + case RandomRayVolumeEstimator::SIMULATION_AVERAGED: + volume = volume_simulation_avg; + break; + case RandomRayVolumeEstimator::HYBRID: + if (external_source_present) { + volume = volume_iteration; + } else { + volume = volume_simulation_avg; + } + break; + default: + fatal_error("Invalid volume estimator type"); + } + int material = material_[sr]; for (int g = 0; g < negroups_; g++) { int64_t idx = (sr * negroups_) + g; // There are three scenarios we need to consider: - if (was_cell_hit) { + if (volume_iteration > 0.0) { // 1. If the FSR was hit this iteration, then the new flux is equal to // the flat source from the previous iteration plus the contributions // from rays passing through the source region (computed during the // transport sweep) - float sigma_t = data::mg.macro_xs_[material].get_xs( - MgxsType::TOTAL, g, nullptr, nullptr, nullptr, t, a); - scalar_flux_new_[idx] /= (sigma_t * volume); - scalar_flux_new_[idx] += source_[idx]; - } else if (volume > 0.0) { + set_flux_to_flux_plus_source(idx, volume, material, g); + } else if (volume_simulation_avg > 0.0) { // 2. If the FSR was not hit this iteration, but has been hit some - // previous iteration, then we simply set the new scalar flux to be - // equal to the contribution from the flat source alone. - scalar_flux_new_[idx] = source_[idx]; - } else { - // If the FSR was not hit this iteration, and it has never been hit in - // any iteration (i.e., volume is zero), then we want to set this to 0 - // to avoid dividing anything by a zero volume. - scalar_flux_new_[idx] = 0.0f; + // previous iteration, then we need to make a choice about what + // to do. Naively we will usually want to set the flux to be equal + // to the reduced source. However, in fixed source problems where + // there is a strong external source present in the cell, and where + // the cell has a very low cross section, this approximation will + // cause a huge upward bias in the flux estimate of the cell (in these + // conditions, the flux estimate can be orders of magnitude too large). + // Thus, to avoid this bias, if any external source is present + // in the cell we will use the previous iteration's flux estimate. This + // injects a small degree of correlation into the simulation, but this + // is going to be trivial when the miss rate is a few percent or less. + if (external_source_present) { + set_flux_to_old_flux(idx); + } else { + set_flux_to_source(idx); + } } + // If the FSR was not hit this iteration, and it has never been hit in + // any iteration (i.e., volume is zero), then we want to set this to 0 + // to avoid dividing anything by a zero volume. This happens implicitly + // given that the new scalar flux arrays are set to zero each iteration. } } @@ -482,7 +542,8 @@ void FlatSourceDomain::reset_tally_volumes() // reported scalar fluxes are in units per source neutron. This allows for // direct comparison of reported tallies to Monte Carlo flux results. // This factor needs to be computed at each iteration, as it is based on the -// volume estimate of each FSR, which improves over the course of the simulation +// volume estimate of each FSR, which improves over the course of the +// simulation double FlatSourceDomain::compute_fixed_source_normalization_factor() const { // If we are not in fixed source mode, then there are no external sources @@ -505,7 +566,7 @@ double FlatSourceDomain::compute_fixed_source_normalization_factor() const // angle data. const int t = 0; const int a = 0; - float sigma_t = data::mg.macro_xs_[material].get_xs( + double sigma_t = data::mg.macro_xs_[material].get_xs( MgxsType::TOTAL, e, nullptr, nullptr, nullptr, t, a); simulation_external_source_strength += external_source_[sr * negroups_ + e] * sigma_t * volume; @@ -559,12 +620,13 @@ void FlatSourceDomain::random_ray_tally() for (int sr = 0; sr < n_source_regions_; sr++) { // The fsr.volume_ is the unitless fractional simulation averaged volume // (i.e., it is the FSR's fraction of the overall simulation volume). The - // simulation_volume_ is the total 3D physical volume in cm^3 of the entire - // global simulation domain (as defined by the ray source box). Thus, the - // FSR's true 3D spatial volume in cm^3 is found by multiplying its fraction - // of the total volume by the total volume. Not important in eigenvalue - // solves, but useful in fixed source solves for returning the flux shape - // with a magnitude that makes sense relative to the fixed source strength. + // simulation_volume_ is the total 3D physical volume in cm^3 of the + // entire global simulation domain (as defined by the ray source box). + // Thus, the FSR's true 3D spatial volume in cm^3 is found by multiplying + // its fraction of the total volume by the total volume. Not important in + // eigenvalue solves, but useful in fixed source solves for returning the + // flux shape with a magnitude that makes sense relative to the fixed + // source strength. double volume = volume_[sr] * simulation_volume_; double material = material_[sr]; @@ -741,11 +803,8 @@ void FlatSourceDomain::all_reduce_replicated_source_regions() MPI_Allreduce(MPI_IN_PLACE, volume_.data(), n_source_regions_, MPI_DOUBLE, MPI_SUM, mpi::intracomm); - MPI_Allreduce(MPI_IN_PLACE, was_hit_.data(), n_source_regions_, MPI_INT, - MPI_SUM, mpi::intracomm); - MPI_Allreduce(MPI_IN_PLACE, scalar_flux_new_.data(), n_source_elements_, - MPI_FLOAT, MPI_SUM, mpi::intracomm); + MPI_DOUBLE, MPI_SUM, mpi::intracomm); simulation::time_bank_sendrecv.stop(); #endif @@ -913,6 +972,8 @@ void FlatSourceDomain::output_to_vtk() const void FlatSourceDomain::apply_external_source_to_source_region( Discrete* discrete, double strength_factor, int64_t source_region) { + external_source_present_[source_region] = true; + const auto& discrete_energies = discrete->x(); const auto& discrete_probs = discrete->prob(); @@ -968,14 +1029,10 @@ void FlatSourceDomain::apply_external_source_to_cell_and_children( void FlatSourceDomain::count_external_source_regions() { + n_external_source_regions_ = 0; #pragma omp parallel for reduction(+ : n_external_source_regions_) for (int sr = 0; sr < n_source_regions_; sr++) { - float total = 0.f; - for (int e = 0; e < negroups_; e++) { - int64_t se = sr * negroups_ + e; - total += external_source_[se]; - } - if (total != 0.f) { + if (external_source_present_[sr]) { n_external_source_regions_++; } } @@ -1029,7 +1086,7 @@ void FlatSourceDomain::convert_external_sources() for (int sr = 0; sr < n_source_regions_; sr++) { int material = material_[sr]; for (int e = 0; e < negroups_; e++) { - float sigma_t = data::mg.macro_xs_[material].get_xs( + double sigma_t = data::mg.macro_xs_[material].get_xs( MgxsType::TOTAL, e, nullptr, nullptr, nullptr, t, a); external_source_[sr * negroups_ + e] /= sigma_t; } diff --git a/src/random_ray/linear_source_domain.cpp b/src/random_ray/linear_source_domain.cpp index 1603ec24a..5c3fa91c1 100644 --- a/src/random_ray/linear_source_domain.cpp +++ b/src/random_ray/linear_source_domain.cpp @@ -70,25 +70,25 @@ void LinearSourceDomain::update_neutron_source(double k_eff) MomentMatrix invM = mom_matrix_[sr].inverse(); for (int e_out = 0; e_out < negroups_; e_out++) { - float sigma_t = data::mg.macro_xs_[material].get_xs( + double sigma_t = data::mg.macro_xs_[material].get_xs( MgxsType::TOTAL, e_out, nullptr, nullptr, nullptr, t, a); - float scatter_flat = 0.0f; - float fission_flat = 0.0f; + double scatter_flat = 0.0f; + double fission_flat = 0.0f; MomentArray scatter_linear = {0.0, 0.0, 0.0}; MomentArray fission_linear = {0.0, 0.0, 0.0}; for (int e_in = 0; e_in < negroups_; e_in++) { // Handles for the flat and linear components of the flux - float flux_flat = scalar_flux_old_[sr * negroups_ + e_in]; + double flux_flat = scalar_flux_old_[sr * negroups_ + e_in]; MomentArray flux_linear = flux_moments_old_[sr * negroups_ + e_in]; // Handles for cross sections - float sigma_s = data::mg.macro_xs_[material].get_xs( + double sigma_s = data::mg.macro_xs_[material].get_xs( MgxsType::NU_SCATTER, e_in, &e_out, nullptr, nullptr, t, a); - float nu_sigma_f = data::mg.macro_xs_[material].get_xs( + double nu_sigma_f = data::mg.macro_xs_[material].get_xs( MgxsType::NU_FISSION, e_in, nullptr, nullptr, nullptr, t, a); - float chi = data::mg.macro_xs_[material].get_xs( + double chi = data::mg.macro_xs_[material].get_xs( MgxsType::CHI_PROMPT, e_in, &e_out, nullptr, nullptr, t, a); // Compute source terms for flat and linear components of the flux @@ -103,7 +103,10 @@ void LinearSourceDomain::update_neutron_source(double k_eff) (scatter_flat + fission_flat * inverse_k_eff) / sigma_t; // Compute the linear source terms - if (simulation::current_batch > 2) { + // In the first 10 iterations when the centroids and spatial moments + // are not well known, we will leave the source gradients as zero + // so as to avoid causing any numerical instability. + if (simulation::current_batch > 10) { source_gradients_[sr * negroups_ + e_out] = invM * ((scatter_linear + fission_linear * inverse_k_eff) / sigma_t); } @@ -124,7 +127,7 @@ void LinearSourceDomain::update_neutron_source(double k_eff) void LinearSourceDomain::normalize_scalar_flux_and_volumes( double total_active_distance_per_iteration) { - float normalization_factor = 1.0 / total_active_distance_per_iteration; + double normalization_factor = 1.0 / total_active_distance_per_iteration; double volume_normalization_factor = 1.0 / (total_active_distance_per_iteration * simulation::current_batch); @@ -142,6 +145,7 @@ void LinearSourceDomain::normalize_scalar_flux_and_volumes( centroid_t_[sr] += centroid_iteration_[sr]; mom_matrix_t_[sr] += mom_matrix_[sr]; volume_t_[sr] += volume_[sr]; + volume_naive_[sr] = volume_[sr] * normalization_factor; volume_[sr] = volume_t_[sr] * volume_normalization_factor; if (volume_t_[sr] > 0.0) { double inv_volume = 1.0 / volume_t_[sr]; @@ -153,56 +157,18 @@ void LinearSourceDomain::normalize_scalar_flux_and_volumes( } } -int64_t LinearSourceDomain::add_source_to_scalar_flux() +void LinearSourceDomain::set_flux_to_flux_plus_source( + int64_t idx, double volume, int material, int g) { - int64_t n_hits = 0; + scalar_flux_new_[idx] /= volume; + scalar_flux_new_[idx] += source_[idx]; + flux_moments_new_[idx] *= (1.0 / volume); +} - // Temperature and angle indices, if using multiple temperature - // data sets and/or anisotropic data sets. - // TODO: Currently assumes we are only using single temp/single - // angle data. - const int t = 0; - const int a = 0; - -#pragma omp parallel for reduction(+ : n_hits) - for (int sr = 0; sr < n_source_regions_; sr++) { - - double volume = volume_[sr]; - int material = material_[sr]; - - // Check if this cell was hit this iteration - int was_cell_hit = was_hit_[sr]; - if (was_cell_hit) { - n_hits++; - } - - for (int g = 0; g < negroups_; g++) { - int64_t idx = (sr * negroups_) + g; - // There are three scenarios we need to consider: - if (was_cell_hit) { - // 1. If the FSR was hit this iteration, then the new flux is equal to - // the flat source from the previous iteration plus the contributions - // from rays passing through the source region (computed during the - // transport sweep) - scalar_flux_new_[idx] /= volume; - scalar_flux_new_[idx] += source_[idx]; - flux_moments_new_[idx] *= (1.0 / volume); - } else if (volume > 0.0) { - // 2. If the FSR was not hit this iteration, but has been hit some - // previous iteration, then we simply set the new scalar flux to be - // equal to the contribution from the flat source alone. - scalar_flux_new_[idx] = source_[idx]; - } else { - // If the FSR was not hit this iteration, and it has never been hit in - // any iteration (i.e., volume is zero), then we want to set this to 0 - // to avoid dividing anything by a zero volume. - scalar_flux_new_[idx] = 0.0f; - flux_moments_new_[idx] *= 0.0; - } - } - } - - return n_hits; +void LinearSourceDomain::set_flux_to_old_flux(int64_t idx) +{ + scalar_flux_new_[idx] = scalar_flux_old_[idx]; + flux_moments_new_[idx] = flux_moments_old_[idx]; } void LinearSourceDomain::flux_swap() @@ -254,7 +220,7 @@ void LinearSourceDomain::all_reduce_replicated_source_regions() double LinearSourceDomain::evaluate_flux_at_point( Position r, int64_t sr, int g) const { - float phi_flat = FlatSourceDomain::evaluate_flux_at_point(r, sr, g); + double phi_flat = FlatSourceDomain::evaluate_flux_at_point(r, sr, g); Position local_r = r - centroid_[sr]; MomentArray phi_linear = flux_moments_t_[sr * negroups_ + g]; diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp index a5bf6ec10..eb64cb7d2 100644 --- a/src/random_ray/random_ray.cpp +++ b/src/random_ray/random_ray.cpp @@ -348,12 +348,6 @@ void RandomRay::attenuate_flux_flat_source(double distance, bool is_active) domain_->scalar_flux_new_[source_element + g] += delta_psi_[g]; } - // If the source region hasn't been hit yet this iteration, - // indicate that it now has - if (domain_->was_hit_[source_region] == 0) { - domain_->was_hit_[source_region] = 1; - } - // Accomulate volume (ray distance) into this iteration's estimate // of the source region's volume domain_->volume_[source_region] += distance; @@ -505,12 +499,6 @@ void RandomRay::attenuate_flux_linear_source(double distance, bool is_active) moment_matrix_estimate *= distance; domain->mom_matrix_[source_region] += moment_matrix_estimate; - // If the source region hasn't been hit yet this iteration, - // indicate that it now has - if (domain_->was_hit_[source_region] == 0) { - domain_->was_hit_[source_region] = 1; - } - // Tally valid position inside the source region (e.g., midpoint of // the ray) if not done already if (!domain_->position_recorded_[source_region]) { diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index 4bc77645b..57959e801 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -383,7 +383,7 @@ void RandomRaySimulation::instability_check( "Very high FSR miss rate detected ({:.3f}%). Instability may occur. " "Increase ray density by adding more rays and/or active distance.", percent_missed)); - } else if (percent_missed > 0.01) { + } else if (percent_missed > 1.0) { warning( fmt::format("Elevated FSR miss rate detected ({:.3f}%). Increasing " "ray density by adding more rays and/or active " @@ -432,6 +432,22 @@ void RandomRaySimulation::print_results_random_ray( fmt::print(" Avg per Iteration = {:.4e}\n", total_integrations / settings::n_batches); + std::string estimator; + switch (domain_->volume_estimator_) { + case RandomRayVolumeEstimator::SIMULATION_AVERAGED: + estimator = "Simulation Averaged"; + break; + case RandomRayVolumeEstimator::NAIVE: + estimator = "Naive"; + break; + case RandomRayVolumeEstimator::HYBRID: + estimator = "Hybrid"; + break; + default: + fatal_error("Invalid volume estimator type"); + } + fmt::print(" Volume Estimator Type = {}\n", estimator); + header("Timing Statistics", 4); show_time("Total time for initialization", time_initialize.elapsed()); show_time("Reading cross sections", time_read_xs.elapsed(), 1); diff --git a/src/settings.cpp b/src/settings.cpp index db956abf5..ba451e219 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -269,6 +269,20 @@ void get_run_parameters(pugi::xml_node node_base) } else { fatal_error("Specify random ray source in settings XML"); } + if (check_for_node(random_ray_node, "volume_estimator")) { + std::string temp_str = + get_node_value(random_ray_node, "volume_estimator", true, true); + if (temp_str == "simulation_averaged") { + FlatSourceDomain::volume_estimator_ = + RandomRayVolumeEstimator::SIMULATION_AVERAGED; + } else if (temp_str == "naive") { + FlatSourceDomain::volume_estimator_ = RandomRayVolumeEstimator::NAIVE; + } else if (temp_str == "hybrid") { + FlatSourceDomain::volume_estimator_ = RandomRayVolumeEstimator::HYBRID; + } else { + fatal_error("Unrecognized volume estimator: " + temp_str); + } + } if (check_for_node(random_ray_node, "source_shape")) { std::string temp_str = get_node_value(random_ray_node, "source_shape", true, true); diff --git a/tests/regression_tests/random_ray_fixed_source_domain/cell/results_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/cell/results_true.dat index 5f2975860..6da51a711 100644 --- a/tests/regression_tests/random_ray_fixed_source_domain/cell/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_domain/cell/results_true.dat @@ -1,9 +1,9 @@ tally 1: --5.745886E+02 -9.758367E+04 +5.934460E-01 +7.058894E-02 tally 2: -2.971927E-02 -1.827222E-04 +3.206214E-02 +2.063370E-04 tally 3: -1.978393E-03 -7.951531E-07 +2.096411E-03 +8.804924E-07 diff --git a/tests/regression_tests/random_ray_fixed_source_domain/material/results_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/material/results_true.dat index 5f2975860..6da51a711 100644 --- a/tests/regression_tests/random_ray_fixed_source_domain/material/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_domain/material/results_true.dat @@ -1,9 +1,9 @@ tally 1: --5.745886E+02 -9.758367E+04 +5.934460E-01 +7.058894E-02 tally 2: -2.971927E-02 -1.827222E-04 +3.206214E-02 +2.063370E-04 tally 3: -1.978393E-03 -7.951531E-07 +2.096411E-03 +8.804924E-07 diff --git a/tests/regression_tests/random_ray_fixed_source_domain/universe/results_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/universe/results_true.dat index 5f2975860..6da51a711 100644 --- a/tests/regression_tests/random_ray_fixed_source_domain/universe/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_domain/universe/results_true.dat @@ -1,9 +1,9 @@ tally 1: --5.745886E+02 -9.758367E+04 +5.934460E-01 +7.058894E-02 tally 2: -2.971927E-02 -1.827222E-04 +3.206214E-02 +2.063370E-04 tally 3: -1.978393E-03 -7.951531E-07 +2.096411E-03 +8.804924E-07 diff --git a/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat index e2972c5f2..6ef3f0871 100644 --- a/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat @@ -192,8 +192,8 @@ fixed source 90 - 10 - 5 + 40 + 20 100.0 1.0 diff --git a/tests/regression_tests/random_ray_fixed_source_linear/linear/results_true.dat b/tests/regression_tests/random_ray_fixed_source_linear/linear/results_true.dat index 7fb01a0ea..46200b19c 100644 --- a/tests/regression_tests/random_ray_fixed_source_linear/linear/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_linear/linear/results_true.dat @@ -1,9 +1,9 @@ tally 1: --5.745718E+02 -9.757661E+04 +2.339084E+00 +2.747299E-01 tally 2: -3.074428E-02 -1.952251E-04 +1.090051E-01 +6.073265E-04 tally 3: -1.980876E-03 -7.970502E-07 +7.300117E-03 +2.715425E-06 diff --git a/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat index 5b958d65c..805c53fe6 100644 --- a/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat @@ -192,8 +192,8 @@ fixed source 90 - 10 - 5 + 40 + 20 100.0 1.0 diff --git a/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/results_true.dat b/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/results_true.dat index 22b39edcf..ea711a925 100644 --- a/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/results_true.dat @@ -1,9 +1,9 @@ tally 1: --5.745810E+02 -9.758220E+04 +2.335701E+00 +2.742860E-01 tally 2: -3.022777E-02 -1.884091E-04 +1.081600E-01 +5.980902E-04 tally 3: -1.980651E-03 -7.968779E-07 +7.295015E-03 +2.711589E-06 diff --git a/tests/regression_tests/random_ray_fixed_source_linear/test.py b/tests/regression_tests/random_ray_fixed_source_linear/test.py index 25335dea6..99211024e 100644 --- a/tests/regression_tests/random_ray_fixed_source_linear/test.py +++ b/tests/regression_tests/random_ray_fixed_source_linear/test.py @@ -23,5 +23,7 @@ def test_random_ray_fixed_source_linear(shape): openmc.reset_auto_ids() model = random_ray_three_region_cube() model.settings.random_ray['source_shape'] = shape - harness = MGXSTestHarness('statepoint.10.h5', model) + model.settings.inactive = 20 + model.settings.batches = 40 + harness = MGXSTestHarness('statepoint.40.h5', model) harness.main() diff --git a/tests/regression_tests/random_ray_fixed_source_normalization/False/results_true.dat b/tests/regression_tests/random_ray_fixed_source_normalization/False/results_true.dat index dbe778df9..d8f78493c 100644 --- a/tests/regression_tests/random_ray_fixed_source_normalization/False/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_normalization/False/results_true.dat @@ -1,9 +1,9 @@ tally 1: --6.614590E+04 -1.283943E+09 +6.840321E+01 +9.376316E+02 tally 2: -4.612657E+02 -4.402080E+04 +4.976182E+02 +4.970407E+04 tally 3: -2.248302E+01 -1.026884E+02 +2.382441E+01 +1.137148E+02 diff --git a/tests/regression_tests/random_ray_fixed_source_normalization/True/results_true.dat b/tests/regression_tests/random_ray_fixed_source_normalization/True/results_true.dat index 5f2975860..6da51a711 100644 --- a/tests/regression_tests/random_ray_fixed_source_normalization/True/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_normalization/True/results_true.dat @@ -1,9 +1,9 @@ tally 1: --5.745886E+02 -9.758367E+04 +5.934460E-01 +7.058894E-02 tally 2: -2.971927E-02 -1.827222E-04 +3.206214E-02 +2.063370E-04 tally 3: -1.978393E-03 -7.951531E-07 +2.096411E-03 +8.804924E-07 diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/flat/results_true.dat b/tests/regression_tests/random_ray_fixed_source_subcritical/flat/results_true.dat index e9d2a733d..831eac501 100644 --- a/tests/regression_tests/random_ray_fixed_source_subcritical/flat/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_subcritical/flat/results_true.dat @@ -1,168 +1,168 @@ tally 1: -1.582116E+02 -1.005480E+03 +1.591301E+02 +1.016825E+03 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.897563E+01 -1.396228E+02 +5.916980E+01 +1.404518E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.038901E+01 -1.667203E+01 +2.036992E+01 +1.662864E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.465551E+01 -2.438101E+01 +2.464728E+01 +2.434669E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.260230E+01 -1.110141E+02 +5.260923E+01 +1.109616E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -9.438906E+01 -3.579643E+02 +9.443775E+01 +3.580658E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.698287E+01 -1.314208E+02 +5.708323E+01 +1.318058E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.002152E+02 -1.608182E+03 +2.004495E+02 +1.610586E+03 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -7.374897E+01 -2.181176E+02 +7.321594E+01 +2.147152E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.636086E+01 -2.787512E+01 +2.573858E+01 +2.655227E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -3.000395E+01 -3.611990E+01 +2.944079E+01 +3.474938E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.824715E+01 -1.360847E+02 +5.758548E+01 +1.328297E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -9.621963E+01 -3.714397E+02 +9.536577E+01 +3.644652E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.954480E+01 -1.436984E+02 +5.824884E+01 +1.368347E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.067903E+02 -4.585529E+02 -1.257560E+01 -6.364831E+00 -3.060650E+01 -3.770132E+01 -4.983480E+01 -9.972910E+01 -2.349541E+00 -2.217300E-01 -5.718312E+00 -1.313392E+00 -1.965462E+01 -1.548888E+01 -2.011494E-01 -1.622334E-03 -4.895573E-01 -9.609705E-03 -2.318671E+01 -2.155841E+01 -2.445032E-01 -2.397352E-03 -5.950720E-01 -1.420043E-02 -5.322867E+01 -1.136138E+02 -1.968847E-01 -1.554358E-03 -4.791838E-01 -9.207284E-03 -1.191175E+02 -5.692803E+02 -5.806982E-02 -1.354810E-04 -1.436897E-01 -8.295235E-04 -8.022963E+01 -2.589171E+02 -3.465139E-01 -4.882327E-03 -9.638109E-01 -3.777193E-02 -1.555305E+02 -9.711601E+02 +1.073356E+02 +4.630895E+02 +1.263975E+01 +6.427678E+00 +3.076263E+01 +3.807359E+01 +4.995302E+01 +1.001379E+02 +2.355060E+00 +2.226283E-01 +5.731746E+00 +1.318712E+00 +1.959843E+01 +1.538887E+01 +2.005479E-01 +1.611455E-03 +4.880935E-01 +9.545263E-03 +2.315398E+01 +2.148264E+01 +2.441408E-01 +2.388612E-03 +5.941898E-01 +1.414866E-02 +5.319555E+01 +1.134034E+02 +1.967517E-01 +1.551313E-03 +4.788601E-01 +9.189243E-03 +1.192041E+02 +5.698491E+02 +5.811654E-02 +1.356369E-04 +1.438053E-01 +8.304781E-04 +8.044764E+01 +2.602455E+02 +3.474828E-01 +4.908567E-03 +9.665057E-01 +3.797493E-02 +1.561720E+02 +9.788256E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.836421E+01 -1.367538E+02 +5.840869E+01 +1.368656E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.042089E+01 -1.673401E+01 +2.029506E+01 +1.650985E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.477708E+01 -2.463124E+01 +2.467632E+01 +2.440891E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.317851E+01 -1.134205E+02 +5.301033E+01 +1.126277E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -9.634473E+01 -3.724422E+02 +9.608414E+01 +3.702737E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.879813E+01 -1.396454E+02 +5.881578E+01 +1.396463E+02 0.000000E+00 0.000000E+00 0.000000E+00 diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/results_true.dat b/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/results_true.dat index 90be5c569..43a6d8258 100644 --- a/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/results_true.dat @@ -1,168 +1,168 @@ tally 1: -1.573422E+02 -9.945961E+02 +1.583461E+02 +1.007023E+03 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.868035E+01 -1.382578E+02 +5.891511E+01 +1.392800E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.028997E+01 -1.651549E+01 +2.028333E+01 +1.649217E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.453137E+01 -2.414255E+01 +2.453681E+01 +2.413486E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.232506E+01 -1.098683E+02 +5.235799E+01 +1.099237E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -9.385720E+01 -3.539613E+02 +9.394382E+01 +3.543494E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.664229E+01 -1.298333E+02 +5.676269E+01 +1.303097E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.001324E+02 -1.606858E+03 +2.007853E+02 +1.616208E+03 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -7.358736E+01 -2.171786E+02 +7.319692E+01 +2.146291E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.625416E+01 -2.765491E+01 +2.566725E+01 +2.640687E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.987675E+01 -3.582038E+01 +2.934697E+01 +3.453012E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.797311E+01 -1.348247E+02 +5.737202E+01 +1.318574E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -9.573509E+01 -3.677339E+02 +9.495251E+01 +3.613355E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.926615E+01 -1.423707E+02 +5.799236E+01 +1.356172E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.060513E+02 -4.521166E+02 -1.247077E+01 -6.257701E+00 -3.035137E+01 -3.706675E+01 -4.962504E+01 -9.887387E+01 -2.338861E+00 -2.196709E-01 -5.692320E+00 -1.301195E+00 -1.957564E+01 -1.536441E+01 -2.003053E-01 -1.608650E-03 -4.875030E-01 -9.528647E-03 -2.309275E+01 -2.138375E+01 -2.434816E-01 -2.377213E-03 -5.925856E-01 -1.408114E-02 -5.300741E+01 -1.126617E+02 -1.960322E-01 -1.540737E-03 -4.771089E-01 -9.126597E-03 -1.184809E+02 -5.630856E+02 -5.774249E-02 -1.339082E-04 -1.428798E-01 -8.198938E-04 -7.971031E+01 -2.554943E+02 -3.441954E-01 -4.814135E-03 -9.573621E-01 -3.724437E-02 -1.549617E+02 -9.639834E+02 +1.066411E+02 +4.570264E+02 +1.254011E+01 +6.325603E+00 +3.052011E+01 +3.746896E+01 +4.977289E+01 +9.940494E+01 +2.345784E+00 +2.208432E-01 +5.709169E+00 +1.308139E+00 +1.953078E+01 +1.528285E+01 +1.998229E-01 +1.599785E-03 +4.863290E-01 +9.476135E-03 +2.307190E+01 +2.133049E+01 +2.432475E-01 +2.371063E-03 +5.920158E-01 +1.404471E-02 +5.299849E+01 +1.125570E+02 +1.959909E-01 +1.539183E-03 +4.770085E-01 +9.117394E-03 +1.186130E+02 +5.641029E+02 +5.781214E-02 +1.341752E-04 +1.430521E-01 +8.215282E-04 +7.995712E+01 +2.570085E+02 +3.452925E-01 +4.844032E-03 +9.604136E-01 +3.747567E-02 +1.556790E+02 +9.726171E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.808547E+01 -1.354629E+02 +5.816710E+01 +1.357523E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.029953E+01 -1.654123E+01 +2.018475E+01 +1.633543E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.462331E+01 -2.433425E+01 +2.453470E+01 +2.413604E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.283851E+01 -1.120036E+02 +5.269366E+01 +1.113096E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -9.572799E+01 -3.677288E+02 +9.550190E+01 +3.658245E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.842566E+01 -1.378719E+02 +5.846305E+01 +1.379618E+02 0.000000E+00 0.000000E+00 0.000000E+00 diff --git a/tests/regression_tests/random_ray_k_eff/results_true.dat b/tests/regression_tests/random_ray_k_eff/results_true.dat index ee2e4000d..a21929d53 100644 --- a/tests/regression_tests/random_ray_k_eff/results_true.dat +++ b/tests/regression_tests/random_ray_k_eff/results_true.dat @@ -1,36 +1,36 @@ k-combined: -8.400322E-01 8.023350E-03 +8.400321E-01 8.023358E-03 tally 1: -5.086560E+00 -5.180937E+00 +5.086559E+00 +5.180935E+00 1.885166E+00 -7.115505E-01 -4.588117E+00 -4.214785E+00 -2.860401E+00 -1.639329E+00 +7.115503E-01 +4.588116E+00 +4.214784E+00 +2.860400E+00 +1.639328E+00 4.245221E-01 -3.610930E-02 +3.610929E-02 1.033202E+00 2.138892E-01 1.692631E+00 -5.793967E-01 +5.793966E-01 5.445818E-02 5.996625E-04 1.325403E-01 3.552030E-03 -2.372249E+00 +2.372248E+00 1.146944E+00 -7.808143E-02 -1.242279E-03 +7.808142E-02 +1.242278E-03 1.900346E-01 -7.358492E-03 -7.134949E+00 +7.358490E-03 +7.134948E+00 1.034824E+01 -8.272648E-02 -1.391872E-03 -2.013422E-01 -8.244790E-03 +8.272647E-02 +1.391871E-03 +2.013421E-01 +8.244788E-03 2.043539E+01 8.389902E+01 3.099367E-02 @@ -40,23 +40,23 @@ tally 1: 1.313212E+01 3.449537E+01 1.764293E-01 -6.225587E-03 -4.907293E-01 -4.816401E-02 -7.567717E+00 +6.225586E-03 +4.907292E-01 +4.816400E-02 +7.567715E+00 1.145439E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 3.383194E+00 -2.290469E+00 +2.290468E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.819673E+00 -6.726159E-01 +1.819672E+00 +6.726158E-01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -67,7 +67,7 @@ tally 1: 0.000000E+00 0.000000E+00 0.000000E+00 -7.453759E+00 +7.453758E+00 1.128171E+01 0.000000E+00 0.000000E+00 @@ -85,12 +85,12 @@ tally 1: 0.000000E+00 0.000000E+00 0.000000E+00 -4.601917E+00 -4.242626E+00 +4.601916E+00 +4.242624E+00 1.719723E+00 -5.923467E-01 -4.185463E+00 -3.508696E+00 +5.923465E-01 +4.185462E+00 +3.508695E+00 2.730305E+00 1.494324E+00 4.108214E-01 @@ -98,67 +98,67 @@ tally 1: 9.998572E-01 2.004436E-01 1.660852E+00 -5.570433E-01 +5.570432E-01 5.428709E-02 5.947848E-04 1.321239E-01 3.523137E-03 2.306069E+00 -1.082856E+00 -7.697032E-02 -1.206037E-03 -1.873304E-01 -7.143816E-03 +1.082855E+00 +7.697031E-02 +1.206036E-03 +1.873303E-01 +7.143815E-03 7.075194E+00 1.017519E+01 8.322052E-02 -1.408295E-03 +1.408294E-03 2.025446E-01 -8.342072E-03 +8.342070E-03 2.094832E+01 -8.816716E+01 +8.816715E+01 3.234739E-02 2.101889E-04 8.004135E-02 1.286945E-03 1.357413E+01 -3.685984E+01 +3.685983E+01 1.861680E-01 -6.934828E-03 +6.934827E-03 5.178169E-01 -5.365103E-02 -5.072150E+00 -5.151431E+00 -1.916644E+00 -7.358713E-01 -4.664727E+00 -4.358847E+00 -2.859465E+00 +5.365102E-02 +5.072149E+00 +5.151429E+00 +1.916643E+00 +7.358710E-01 +4.664726E+00 +4.358845E+00 +2.859464E+00 1.638250E+00 4.332944E-01 -3.763171E-02 +3.763170E-02 1.054552E+00 2.229070E-01 1.693008E+00 -5.796672E-01 +5.796671E-01 5.561096E-02 6.247543E-04 1.353459E-01 3.700658E-03 2.368860E+00 1.143296E+00 -7.951820E-02 +7.951819E-02 1.286690E-03 1.935314E-01 -7.621558E-03 -7.119587E+00 +7.621556E-03 +7.119586E+00 1.030023E+01 -8.428176E-02 -1.442932E-03 -2.051275E-01 -8.547244E-03 +8.428175E-02 +1.442931E-03 +2.051274E-01 +8.547243E-03 2.046758E+01 -8.418768E+01 +8.418767E+01 3.181946E-02 2.034766E-04 7.873502E-02 @@ -166,6 +166,6 @@ tally 1: 1.325834E+01 3.515919E+01 1.832838E-01 -6.720556E-03 +6.720555E-03 5.097947E-01 5.199331E-02 diff --git a/tests/regression_tests/random_ray_linear/linear/inputs_true.dat b/tests/regression_tests/random_ray_linear/linear/inputs_true.dat index c580be3d1..4df51bad5 100644 --- a/tests/regression_tests/random_ray_linear/linear/inputs_true.dat +++ b/tests/regression_tests/random_ray_linear/linear/inputs_true.dat @@ -74,8 +74,8 @@ eigenvalue 100 - 10 - 5 + 40 + 20 multi-group 100.0 diff --git a/tests/regression_tests/random_ray_linear/linear/results_true.dat b/tests/regression_tests/random_ray_linear/linear/results_true.dat index a3e24dc75..6617c7821 100644 --- a/tests/regression_tests/random_ray_linear/linear/results_true.dat +++ b/tests/regression_tests/random_ray_linear/linear/results_true.dat @@ -1,171 +1,171 @@ k-combined: -8.273022E-01 1.347623E-02 +1.095967E+00 1.543581E-02 tally 1: -5.004109E+00 -5.022655E+00 -1.844047E+00 -6.833376E-01 -4.488042E+00 -4.047669E+00 -2.824818E+00 -1.599927E+00 -4.182704E-01 -3.509796E-02 -1.017987E+00 -2.078987E-01 -1.676761E+00 -5.682729E-01 -5.385624E-02 -5.857594E-04 -1.310753E-01 -3.469677E-03 -2.353602E+00 -1.127695E+00 -7.721312E-02 -1.212202E-03 -1.879213E-01 -7.180339E-03 -7.082957E+00 -1.019023E+01 -8.203580E-02 -1.366996E-03 -1.996612E-01 -8.097436E-03 -2.034293E+01 -8.321967E+01 -3.099116E-02 -1.933713E-04 -7.668546E-02 -1.183975E-03 -1.311478E+01 -3.442575E+01 -1.778200E-01 -6.347187E-03 -4.945973E-01 -4.910476E-02 -7.576546E+00 -1.148094E+01 +2.548108E+01 +3.269093E+01 +9.271804E+00 +4.327275E+00 +2.256572E+01 +2.563210E+01 +1.816107E+01 +1.653421E+01 +2.659203E+00 +3.544951E-01 +6.471969E+00 +2.099810E+00 +1.364193E+01 +9.308675E+00 +4.362828E-01 +9.521133E-03 +1.061825E+00 +5.639730E-02 +1.746102E+01 +1.524680E+01 +5.733016E-01 +1.643671E-02 +1.395301E+00 +9.736091E-02 +4.539598E+01 +1.030472E+02 +5.263055E-01 +1.385088E-02 +1.280938E+00 +8.204609E-02 +9.945736E+01 +4.946716E+02 +1.505228E-01 +1.133424E-03 +3.724582E-01 +6.939732E-03 +5.324914E+01 +1.418809E+02 +7.219589E-01 +2.614875E-02 +2.008092E+00 +2.022988E-01 +4.188246E+01 +8.843468E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -3.386885E+00 -2.295318E+00 +2.224363E+01 +2.481131E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.823720E+00 -6.769694E-01 +1.436097E+01 +1.031496E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.703042E+00 -1.494228E+00 +1.901248E+01 +1.807657E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -7.465612E+00 -1.132769E+01 +4.559337E+01 +1.039424E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.820337E+01 -6.657534E+01 +8.802992E+01 +3.874925E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.127686E+01 -2.549122E+01 +4.506602E+01 +1.016497E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -4.561053E+00 -4.169936E+00 -1.674166E+00 -5.624648E-01 -4.074585E+00 -3.331694E+00 -2.723008E+00 -1.487311E+00 -4.059013E-01 -3.307772E-02 -9.878827E-01 -1.959320E-01 -1.663524E+00 -5.584734E-01 -5.399658E-02 -5.877094E-04 -1.314169E-01 -3.481227E-03 -2.310994E+00 -1.086435E+00 -7.627297E-02 -1.181877E-03 -1.856331E-01 -7.000707E-03 -7.100201E+00 -1.023633E+01 -8.285591E-02 -1.393520E-03 -2.016572E-01 -8.254554E-03 -2.119292E+01 -9.022917E+01 -3.281214E-02 -2.163583E-04 -8.119135E-02 -1.324719E-03 -1.380975E+01 -3.815787E+01 -1.919186E-01 -7.384267E-03 -5.338118E-01 -5.712809E-02 -5.024478E+00 -5.056216E+00 -1.888826E+00 -7.148091E-01 -4.597025E+00 -4.234087E+00 -2.839930E+00 -1.616473E+00 -4.294394E-01 -3.697612E-02 -1.045170E+00 -2.190237E-01 -1.688196E+00 -5.765397E-01 -5.539650E-02 -6.200086E-04 -1.348240E-01 -3.672547E-03 -2.361633E+00 -1.136540E+00 -7.901052E-02 -1.270359E-03 -1.922958E-01 -7.524822E-03 -7.100780E+00 -1.024481E+01 -8.389610E-02 -1.429594E-03 -2.041888E-01 -8.468240E-03 -2.049187E+01 -8.438141E+01 -3.195576E-02 -2.052140E-04 -7.907229E-02 -1.256485E-03 -1.327412E+01 -3.524466E+01 -1.850084E-01 -6.847675E-03 -5.145915E-01 -5.297677E-02 +2.207833E+01 +2.455068E+01 +8.139772E+00 +3.337974E+00 +1.981058E+01 +1.977210E+01 +1.710407E+01 +1.466648E+01 +2.542287E+00 +3.240467E-01 +6.187418E+00 +1.919453E+00 +1.342690E+01 +9.017908E+00 +4.362263E-01 +9.518695E-03 +1.061688E+00 +5.638286E-02 +1.713924E+01 +1.469065E+01 +5.714028E-01 +1.632839E-02 +1.390680E+00 +9.671931E-02 +4.539193E+01 +1.030326E+02 +5.345253E-01 +1.428719E-02 +1.300944E+00 +8.463057E-02 +1.013270E+02 +5.134168E+02 +1.555517E-01 +1.210145E-03 +3.849017E-01 +7.409480E-03 +5.377836E+01 +1.446537E+02 +7.374053E-01 +2.722908E-02 +2.051056E+00 +2.106567E-01 +2.522726E+01 +3.202007E+01 +9.366659E+00 +4.412278E+00 +2.279657E+01 +2.613561E+01 +1.803368E+01 +1.629756E+01 +2.696490E+00 +3.643066E-01 +6.562716E+00 +2.157928E+00 +1.357447E+01 +9.216150E+00 +4.437305E-01 +9.847287E-03 +1.079951E+00 +5.832923E-02 +1.735848E+01 +1.506775E+01 +5.825173E-01 +1.696803E-02 +1.417731E+00 +1.005081E-01 +4.519297E+01 +1.021280E+02 +5.357712E-01 +1.435322E-02 +1.303976E+00 +8.502167E-02 +9.934508E+01 +4.935314E+02 +1.538761E-01 +1.184229E-03 +3.807556E-01 +7.250804E-03 +5.335839E+01 +1.424221E+02 +7.404823E-01 +2.747396E-02 +2.059614E+00 +2.125512E-01 diff --git a/tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat b/tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat index 5c4076de7..113771438 100644 --- a/tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat +++ b/tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat @@ -74,8 +74,8 @@ eigenvalue 100 - 10 - 5 + 40 + 20 multi-group 100.0 diff --git a/tests/regression_tests/random_ray_linear/linear_xy/results_true.dat b/tests/regression_tests/random_ray_linear/linear_xy/results_true.dat index f79b3fa78..abfd03c06 100644 --- a/tests/regression_tests/random_ray_linear/linear_xy/results_true.dat +++ b/tests/regression_tests/random_ray_linear/linear_xy/results_true.dat @@ -1,171 +1,171 @@ k-combined: -8.368882E-01 8.107070E-03 +1.104727E+00 1.593303E-02 tally 1: -5.072700E+00 -5.152684E+00 -1.876680E+00 -7.051697E-01 -4.567463E+00 -4.176989E+00 -2.858196E+00 -1.636775E+00 -4.239946E-01 -3.601884E-02 -1.031918E+00 -2.133534E-01 -1.692006E+00 -5.789664E-01 -5.442292E-02 -5.988675E-04 -1.324545E-01 -3.547321E-03 -2.371801E+00 -1.146513E+00 -7.804414E-02 -1.241074E-03 -1.899438E-01 -7.351359E-03 -7.134037E+00 -1.034554E+01 -8.269855E-02 -1.390936E-03 -2.012742E-01 -8.239245E-03 -2.043174E+01 -8.386878E+01 -3.098171E-02 -1.929186E-04 -7.666208E-02 -1.181203E-03 -1.312800E+01 -3.447362E+01 -1.763141E-01 -6.217438E-03 -4.904088E-01 -4.810096E-02 -7.608165E+00 -1.157716E+01 +2.566934E+01 +3.317503E+01 +9.417202E+00 +4.465518E+00 +2.291958E+01 +2.645097E+01 +1.823903E+01 +1.667438E+01 +2.679931E+00 +3.600420E-01 +6.522415E+00 +2.132667E+00 +1.365623E+01 +9.327448E+00 +4.370682E-01 +9.554968E-03 +1.063736E+00 +5.659772E-02 +1.750634E+01 +1.532609E+01 +5.762870E-01 +1.660889E-02 +1.402567E+00 +9.838082E-02 +4.543609E+01 +1.032286E+02 +5.271287E-01 +1.389456E-02 +1.282941E+00 +8.230483E-02 +9.881678E+01 +4.882586E+02 +1.487616E-01 +1.106634E-03 +3.681003E-01 +6.775702E-03 +5.260781E+01 +1.384126E+02 +7.018594E-01 +2.464953E-02 +1.952187E+00 +1.907001E-01 +4.184779E+01 +8.826530E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -3.392187E+00 -2.302667E+00 +2.226932E+01 +2.486554E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.821339E+00 -6.737984E-01 +1.441107E+01 +1.038682E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.695742E+00 -1.483104E+00 +1.909194E+01 +1.822767E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -7.457904E+00 -1.129343E+01 +4.579319E+01 +1.048559E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.824331E+01 -6.687165E+01 +8.833276E+01 +3.901410E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.138096E+01 -2.591161E+01 +4.528911E+01 +1.025740E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -4.582534E+00 -4.206969E+00 -1.707129E+00 -5.837143E-01 -4.154811E+00 -3.457563E+00 -2.727459E+00 -1.491183E+00 -4.100516E-01 -3.371280E-02 -9.979836E-01 -1.996938E-01 -1.660765E+00 -5.570066E-01 -5.425719E-02 -5.941503E-04 -1.320511E-01 -3.519379E-03 -2.306630E+00 -1.083425E+00 -7.695103E-02 -1.205473E-03 -1.872834E-01 -7.140475E-03 -7.077631E+00 -1.018246E+01 -8.321109E-02 -1.408010E-03 -2.025216E-01 -8.340388E-03 -2.094896E+01 -8.817186E+01 -3.233088E-02 -2.099749E-04 -8.000050E-02 -1.285635E-03 -1.356905E+01 -3.683229E+01 -1.859858E-01 -6.921278E-03 -5.173102E-01 -5.354619E-02 -5.056029E+00 -5.118575E+00 -1.910007E+00 -7.308066E-01 -4.648575E+00 -4.328846E+00 -2.856363E+00 -1.634636E+00 -4.328679E-01 -3.755784E-02 -1.053514E+00 -2.224694E-01 -1.692444E+00 -5.793158E-01 -5.559366E-02 -6.243434E-04 -1.353038E-01 -3.698224E-03 -2.368251E+00 -1.142832E+00 -7.949453E-02 -1.285913E-03 -1.934738E-01 -7.616955E-03 -7.118354E+00 -1.029753E+01 -8.426801E-02 -1.442436E-03 -2.050940E-01 -8.544309E-03 -2.046889E+01 -8.419814E+01 -3.182500E-02 -2.035334E-04 -7.874874E-02 -1.246195E-03 -1.326056E+01 -3.517079E+01 -1.833225E-01 -6.723237E-03 -5.099023E-01 -5.201406E-02 +2.218278E+01 +2.477434E+01 +8.203467E+00 +3.389915E+00 +1.996560E+01 +2.007976E+01 +1.714818E+01 +1.473927E+01 +2.551034E+00 +3.262450E-01 +6.208707E+00 +1.932474E+00 +1.343470E+01 +9.027502E+00 +4.363259E-01 +9.522121E-03 +1.061930E+00 +5.640315E-02 +1.717034E+01 +1.474381E+01 +5.729720E-01 +1.641841E-02 +1.394499E+00 +9.725251E-02 +4.542715E+01 +1.031896E+02 +5.348128E-01 +1.430232E-02 +1.301643E+00 +8.472019E-02 +1.009076E+02 +5.091264E+02 +1.543547E-01 +1.191320E-03 +3.819398E-01 +7.294216E-03 +5.342751E+01 +1.427427E+02 +7.255554E-01 +2.633014E-02 +2.018096E+00 +2.037021E-01 +2.540053E+01 +3.247261E+01 +9.483956E+00 +4.526477E+00 +2.308205E+01 +2.681205E+01 +1.812760E+01 +1.646855E+01 +2.717374E+00 +3.700301E-01 +6.613545E+00 +2.191830E+00 +1.362672E+01 +9.286907E+00 +4.458292E-01 +9.940508E-03 +1.085059E+00 +5.888142E-02 +1.744511E+01 +1.521876E+01 +5.866632E-01 +1.721091E-02 +1.427821E+00 +1.019468E-01 +4.538426E+01 +1.029941E+02 +5.385934E-01 +1.450495E-02 +1.310845E+00 +8.592045E-02 +9.921424E+01 +4.921945E+02 +1.532444E-01 +1.174272E-03 +3.791926E-01 +7.189835E-03 +5.301633E+01 +1.405571E+02 +7.285745E-01 +2.655093E-02 +2.026493E+00 +2.054102E-01 diff --git a/tests/regression_tests/random_ray_linear/test.py b/tests/regression_tests/random_ray_linear/test.py index 10262789d..510c57de8 100644 --- a/tests/regression_tests/random_ray_linear/test.py +++ b/tests/regression_tests/random_ray_linear/test.py @@ -22,5 +22,7 @@ def test_random_ray_source(shape): openmc.reset_auto_ids() model = random_ray_lattice() model.settings.random_ray['source_shape'] = shape - harness = MGXSTestHarness('statepoint.10.h5', model) + model.settings.inactive = 20 + model.settings.batches = 40 + harness = MGXSTestHarness('statepoint.40.h5', model) harness.main() diff --git a/tests/regression_tests/random_ray_volume_estimator/__init__.py b/tests/regression_tests/random_ray_volume_estimator/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_volume_estimator/hybrid/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator/hybrid/inputs_true.dat new file mode 100644 index 000000000..9c15ec97d --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator/hybrid/inputs_true.dat @@ -0,0 +1,245 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + hybrid + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_volume_estimator/hybrid/results_true.dat b/tests/regression_tests/random_ray_volume_estimator/hybrid/results_true.dat new file mode 100644 index 000000000..6da51a711 --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator/hybrid/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +5.934460E-01 +7.058894E-02 +tally 2: +3.206214E-02 +2.063370E-04 +tally 3: +2.096411E-03 +8.804924E-07 diff --git a/tests/regression_tests/random_ray_volume_estimator/naive/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator/naive/inputs_true.dat new file mode 100644 index 000000000..7d05f0978 --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator/naive/inputs_true.dat @@ -0,0 +1,245 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + naive + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_volume_estimator/naive/results_true.dat b/tests/regression_tests/random_ray_volume_estimator/naive/results_true.dat new file mode 100644 index 000000000..f8d6d10b0 --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator/naive/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +5.935538E-01 +7.061433E-02 +tally 2: +3.263210E-02 +2.134164E-04 +tally 3: +2.107977E-03 +8.905227E-07 diff --git a/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/inputs_true.dat new file mode 100644 index 000000000..301671e6d --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/inputs_true.dat @@ -0,0 +1,245 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + simulation_averaged + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/results_true.dat b/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/results_true.dat new file mode 100644 index 000000000..5f2975860 --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +-5.745886E+02 +9.758367E+04 +tally 2: +2.971927E-02 +1.827222E-04 +tally 3: +1.978393E-03 +7.951531E-07 diff --git a/tests/regression_tests/random_ray_volume_estimator/test.py b/tests/regression_tests/random_ray_volume_estimator/test.py new file mode 100644 index 000000000..fba4bbbbe --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator/test.py @@ -0,0 +1,30 @@ +import os + +import openmc +from openmc.utility_funcs import change_directory +from openmc.examples import random_ray_three_region_cube +import pytest + +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) + + +@pytest.mark.parametrize("estimator", ["hybrid", + "simulation_averaged", + "naive" + ]) +def test_random_ray_volume_estimator(estimator): + with change_directory(estimator): + openmc.reset_auto_ids() + model = random_ray_three_region_cube() + model.settings.random_ray['volume_estimator'] = estimator + + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/__init__.py b/tests/regression_tests/random_ray_volume_estimator_linear/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/inputs_true.dat new file mode 100644 index 000000000..6440cca04 --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/inputs_true.dat @@ -0,0 +1,246 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 40 + 20 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + linear + hybrid + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/results_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/results_true.dat new file mode 100644 index 000000000..46200b19c --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +2.339084E+00 +2.747299E-01 +tally 2: +1.090051E-01 +6.073265E-04 +tally 3: +7.300117E-03 +2.715425E-06 diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/naive/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/naive/inputs_true.dat new file mode 100644 index 000000000..fcf93b046 --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator_linear/naive/inputs_true.dat @@ -0,0 +1,246 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 40 + 20 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + linear + naive + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/naive/results_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/naive/results_true.dat new file mode 100644 index 000000000..02bfc5266 --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator_linear/naive/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +2.339575E+00 +2.748441E-01 +tally 2: +1.086360E-01 +6.030557E-04 +tally 3: +7.298937E-03 +2.741185E-06 diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/inputs_true.dat new file mode 100644 index 000000000..1df3204a3 --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/inputs_true.dat @@ -0,0 +1,246 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 40 + 20 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + linear + simulation_averaged + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/results_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/results_true.dat new file mode 100644 index 000000000..93e54feea --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +2.670874E+02 +4.432876E+05 +tally 2: +1.117459E-01 +6.497788E-04 +tally 3: +7.563753E-03 +2.947210E-06 diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/test.py b/tests/regression_tests/random_ray_volume_estimator_linear/test.py new file mode 100644 index 000000000..94a14f3ad --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator_linear/test.py @@ -0,0 +1,32 @@ +import os + +import openmc +from openmc.utility_funcs import change_directory +from openmc.examples import random_ray_three_region_cube +import pytest + +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) + + +@pytest.mark.parametrize("estimator", ["hybrid", + "simulation_averaged", + "naive" + ]) +def test_random_ray_volume_estimator_linear(estimator): + with change_directory(estimator): + openmc.reset_auto_ids() + model = random_ray_three_region_cube() + model.settings.random_ray['source_shape'] = 'linear' + model.settings.random_ray['volume_estimator'] = estimator + model.settings.inactive = 20 + model.settings.batches = 40 + harness = MGXSTestHarness('statepoint.40.h5', model) + harness.main() From bd43d751633126d98dee9b5d7b414882780d3a70 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Thu, 5 Sep 2024 14:00:08 -0500 Subject: [PATCH 168/671] Tweaking title of feature issue template (#3127) --- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index ddf49b894..978f88d14 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,5 +1,5 @@ --- -name: Feature request +name: Feature or ehancement request about: Suggest a new feature or enhancement to existing capabilities title: '' labels: '' From 57816e6b8cf23ed0e9b020b72752ed6aeb9501dd Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Sat, 7 Sep 2024 17:50:38 -0500 Subject: [PATCH 169/671] Fix a typo in feature request template (#3128) --- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 978f88d14..3b13abdbc 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,5 +1,5 @@ --- -name: Feature or ehancement request +name: Feature or enhancement request about: Suggest a new feature or enhancement to existing capabilities title: '' labels: '' From 890cab524295b3455fa2ba8a50e2ed5c85b5e6ab Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Thu, 26 Sep 2024 06:45:13 -0500 Subject: [PATCH 170/671] Correct failure due to progress bar values (#3143) --- src/plot.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/plot.cpp b/src/plot.cpp index f29653f80..348138570 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -971,10 +971,6 @@ void Plot::create_voxel() const ProgressBar pb; for (int z = 0; z < pixels_[2]; z++) { - // update progress bar - pb.set_value( - 100. * static_cast(z) / static_cast((pixels_[2] - 1))); - // update z coordinate pltbase.origin_.z = ll.z + z * vox[2]; @@ -989,6 +985,10 @@ void Plot::create_voxel() const // Write to HDF5 dataset voxel_write_slice(z, dspace, dset, memspace, data_flipped.data()); + + // update progress bar + pb.set_value( + 100. * static_cast(z + 1) / static_cast((pixels_[2]))); } voxel_finalize(dspace, dset, memspace); From 8b77a8dd3b3a1c65b1e08f7f032bfee7c86ee51d Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Fri, 27 Sep 2024 00:20:40 +0200 Subject: [PATCH 171/671] Adding source option to plot (#2863) Co-authored-by: Jon Shimwell Co-authored-by: Paul Romano --- openmc/geometry.py | 5 +- openmc/lib/core.py | 9 ++- openmc/model/model.py | 116 +++++++++++++++++++++++++++++++++ openmc/universe.py | 3 +- tests/unit_tests/test_model.py | 29 +++++++++ 5 files changed, 155 insertions(+), 7 deletions(-) diff --git a/openmc/geometry.py b/openmc/geometry.py index 6cce4c18c..a52dc4418 100644 --- a/openmc/geometry.py +++ b/openmc/geometry.py @@ -7,8 +7,6 @@ from pathlib import Path import warnings import lxml.etree as ET -import numpy as np - import openmc import openmc._xml as xml from .checkvalue import check_type, check_less_than, check_greater_than, PathLike @@ -67,7 +65,7 @@ class Geometry: self._root_universe = root_universe @property - def bounding_box(self) -> np.ndarray: + def bounding_box(self) -> openmc.BoundingBox: return self.root_universe.bounding_box @property @@ -800,6 +798,7 @@ class Geometry: Units used on the plot axis **kwargs Keyword arguments passed to :func:`matplotlib.pyplot.imshow` + Returns ------- matplotlib.axes.Axes diff --git a/openmc/lib/core.py b/openmc/lib/core.py index a9a549fa0..e646a9ae1 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -474,8 +474,11 @@ def run(output=True): _dll.openmc_run() -def sample_external_source(n_samples=1, prn_seed=None): - """Sample external source +def sample_external_source( + n_samples: int = 1000, + prn_seed: int | None = None +) -> list[openmc.SourceParticle]: + """Sample external source and return source particles. .. versionadded:: 0.13.1 @@ -490,7 +493,7 @@ def sample_external_source(n_samples=1, prn_seed=None): Returns ------- list of openmc.SourceParticle - List of samples source particles + List of sampled source particles """ if n_samples <= 0: diff --git a/openmc/model/model.py b/openmc/model/model.py index 2ea579ab7..78f743a03 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -9,6 +9,7 @@ import warnings import h5py import lxml.etree as ET +import numpy as np import openmc import openmc._xml as xml @@ -793,6 +794,121 @@ class Model: openmc.lib.materials[domain_id].volume = \ vol_calc.volumes[domain_id].n + def plot( + self, + n_samples: int | None = None, + plane_tolerance: float = 1., + source_kwargs: dict | None = None, + **kwargs, + ): + """Display a slice plot of the geometry. + + .. versionadded:: 0.15.1 + + Parameters + ---------- + n_samples : dict + The number of source particles to sample and add to plot. Defaults + to None which doesn't plot any particles on the plot. + plane_tolerance: float + When plotting a plane the source locations within the plane +/- + the plane_tolerance will be included and those outside of the + plane_tolerance will not be shown + source_kwargs : dict + Keyword arguments passed to :func:`matplotlib.pyplot.scatter`. + **kwargs + Keyword arguments passed to :func:`openmc.Universe.plot` + + Returns + ------- + matplotlib.axes.Axes + Axes containing resulting image + """ + + check_type('n_samples', n_samples, int | None) + check_type('plane_tolerance', plane_tolerance, float) + if source_kwargs is None: + source_kwargs = {} + source_kwargs.setdefault('marker', 'x') + + ax = self.geometry.plot(**kwargs) + if n_samples: + # Sample external source particles + particles = self.sample_external_source(n_samples) + + # Determine plotting parameters and bounding box of geometry + bbox = self.geometry.bounding_box + origin = kwargs.get('origin', None) + basis = kwargs.get('basis', 'xy') + indices = {'xy': (0, 1, 2), 'xz': (0, 2, 1), 'yz': (1, 2, 0)}[basis] + + # Infer origin if not provided + if np.isinf(bbox.extent[basis]).any(): + if origin is None: + origin = (0, 0, 0) + else: + if origin is None: + # if nan values in the bbox.center they get replaced with 0.0 + # this happens when the bounding_box contains inf values + with warnings.catch_warnings(): + warnings.simplefilter("ignore", RuntimeWarning) + origin = np.nan_to_num(bbox.center) + + slice_index = indices[2] + slice_value = origin[slice_index] + + xs = [] + ys = [] + tol = plane_tolerance + for particle in particles: + if (slice_value - tol < particle.r[slice_index] < slice_value + tol): + xs.append(particle.r[indices[0]]) + ys.append(particle.r[indices[1]]) + ax.scatter(xs, ys, **source_kwargs) + return ax + + def sample_external_source( + self, + n_samples: int = 1000, + prn_seed: int | None = None, + **init_kwargs + ) -> list[openmc.SourceParticle]: + """Sample external source and return source particles. + + .. versionadded:: 0.15.1 + + Parameters + ---------- + n_samples : int + Number of samples + prn_seed : int + Pseudorandom number generator (PRNG) seed; if None, one will be + generated randomly. + **init_kwargs + Keyword arguments passed to :func:`openmc.lib.init` + + Returns + ------- + list of openmc.SourceParticle + List of samples source particles + """ + import openmc.lib + + # Silence output by default. Also set arguments to start in volume + # calculation mode to avoid loading cross sections + init_kwargs.setdefault('output', False) + init_kwargs.setdefault('args', ['-c']) + + with change_directory(tmpdir=True): + # Export model within temporary directory + self.export_to_model_xml() + + # Sample external source sites + with openmc.lib.run_in_memory(**init_kwargs): + return openmc.lib.sample_external_source( + n_samples=n_samples, prn_seed=prn_seed + ) + def plot_geometry(self, output=True, cwd='.', openmc_exec='openmc'): """Creates plot images as specified by the Model.plots attribute diff --git a/openmc/universe.py b/openmc/universe.py index 9fab9ae51..d424c243b 100644 --- a/openmc/universe.py +++ b/openmc/universe.py @@ -1,3 +1,4 @@ +from __future__ import annotations import math from abc import ABC, abstractmethod from collections.abc import Iterable @@ -230,7 +231,7 @@ class Universe(UniverseBase): return self._cells @property - def bounding_box(self): + def bounding_box(self) -> openmc.BoundingBox: regions = [c.region for c in self.cells.values() if c.region is not None] if regions: diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 404235bef..4b567c56d 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -591,3 +591,32 @@ def test_single_xml_exec(run_in_tmpdir): os.mkdir('subdir') pincell_model.run(path='subdir') + + +def test_model_plot(): + # plots the geometry with source location and checks the resulting + # matplotlib includes the correct coordinates for the scatter plot for all + # basis. + + surface = openmc.Sphere(r=600, boundary_type="vacuum") + cell = openmc.Cell(region=-surface) + geometry = openmc.Geometry([cell]) + source = openmc.IndependentSource(space=openmc.stats.Point((1, 2, 3))) + settings = openmc.Settings(particles=1, batches=1, source=source) + model = openmc.Model(geometry, settings=settings) + + plot = model.plot(n_samples=1, plane_tolerance=4.0, basis="xy") + coords = plot.axes.collections[0].get_offsets().data.flatten() + assert (coords == np.array([1.0, 2.0])).all() + + plot = model.plot(n_samples=1, plane_tolerance=4.0, basis="xz") + coords = plot.axes.collections[0].get_offsets().data.flatten() + assert (coords == np.array([1.0, 3.0])).all() + + plot = model.plot(n_samples=1, plane_tolerance=4.0, basis="yz") + coords = plot.axes.collections[0].get_offsets().data.flatten() + assert (coords == np.array([2.0, 3.0])).all() + + plot = model.plot(n_samples=1, plane_tolerance=0.1, basis="xy") + coords = plot.axes.collections[0].get_offsets().data.flatten() + assert (coords == np.array([])).all() From 1645e3bb8719e299a574bf4bac268691abab2f10 Mon Sep 17 00:00:00 2001 From: azimG Date: Fri, 27 Sep 2024 02:31:59 +0200 Subject: [PATCH 172/671] Mat ids reset (#3125) Co-authored-by: azimgivron Co-authored-by: azim_givron Co-authored-by: Paul Romano --- openmc/deplete/independent_operator.py | 1 - openmc/deplete/openmc_operator.py | 5 ---- openmc/mixin.py | 9 +++++-- tests/regression_tests/__init__.py | 11 +++++++++ .../deplete_no_transport/test.py | 24 ++++--------------- .../deplete_with_transfer_rates/test.py | 6 +++-- tests/unit_tests/test_deplete_decay.py | 7 +++--- .../unit_tests/test_deplete_transfer_rates.py | 1 + 8 files changed, 31 insertions(+), 33 deletions(-) diff --git a/openmc/deplete/independent_operator.py b/openmc/deplete/independent_operator.py index 759abde13..cc1a05bd9 100644 --- a/openmc/deplete/independent_operator.py +++ b/openmc/deplete/independent_operator.py @@ -244,7 +244,6 @@ class IndependentOperator(OpenMCOperator): """Puts nuclide list into an openmc.Materials object. """ - openmc.reset_auto_ids() mat = openmc.Material() if nuc_units == 'atom/b-cm': for nuc, conc in nuclides.items(): diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py index c0ff568bc..5837cd2c1 100644 --- a/openmc/deplete/openmc_operator.py +++ b/openmc/deplete/openmc_operator.py @@ -146,8 +146,6 @@ class OpenMCOperator(TransportOperator): # Determine which nuclides have cross section data # This nuclides variables contains every nuclides # for which there is an entry in the micro_xs parameter - openmc.reset_auto_ids() - self.nuclides_with_data = self._get_nuclides_with_data( self.cross_sections) @@ -396,9 +394,6 @@ class OpenMCOperator(TransportOperator): self.number.set_density(vec) self._update_materials() - # Prevent OpenMC from complaining about re-creating tallies - openmc.reset_auto_ids() - # Update tally nuclides data in preparation for transport solve nuclides = self._get_reaction_nuclides() self._rate_helper.nuclides = nuclides diff --git a/openmc/mixin.py b/openmc/mixin.py index 31c26ec76..0bc4128b0 100644 --- a/openmc/mixin.py +++ b/openmc/mixin.py @@ -72,12 +72,17 @@ class IDManagerMixin: cls.used_ids.add(uid) self._id = uid + @classmethod + def reset_ids(cls): + """Reset counters""" + cls.used_ids.clear() + cls.next_id = 1 + def reset_auto_ids(): """Reset counters for all auto-generated IDs""" for cls in IDManagerMixin.__subclasses__(): - cls.used_ids.clear() - cls.next_id = 1 + cls.reset_ids() def reserve_ids(ids, cls=None): diff --git a/tests/regression_tests/__init__.py b/tests/regression_tests/__init__.py index ba48fc01e..e1cb56f1d 100644 --- a/tests/regression_tests/__init__.py +++ b/tests/regression_tests/__init__.py @@ -12,6 +12,17 @@ config = { } +def assert_same_mats(res_ref, res_test): + for mat in res_ref[0].index_mat: + assert mat in res_test[0].index_mat, f"Material {mat} not in new results." + for nuc in res_ref[0].index_nuc: + assert nuc in res_test[0].index_nuc, f"Nuclide {nuc} not in new results." + for mat in res_test[0].index_mat: + assert mat in res_ref[0].index_mat, f"Material {mat} not in old results." + for nuc in res_test[0].index_nuc: + assert nuc in res_ref[0].index_nuc, f"Nuclide {nuc} not in old results." + + def assert_atoms_equal(res_ref, res_test, tol=1e-5): for mat in res_test[0].index_mat: for nuc in res_test[0].index_nuc: diff --git a/tests/regression_tests/deplete_no_transport/test.py b/tests/regression_tests/deplete_no_transport/test.py index 54c29cd5b..63ae584e1 100644 --- a/tests/regression_tests/deplete_no_transport/test.py +++ b/tests/regression_tests/deplete_no_transport/test.py @@ -10,7 +10,7 @@ import openmc.deplete from openmc.deplete import IndependentOperator, MicroXS from tests.regression_tests import config, assert_atoms_equal, \ - assert_reaction_rates_equal + assert_reaction_rates_equal, assert_same_mats @pytest.fixture(scope="module") @@ -101,7 +101,7 @@ def test_against_self(run_in_tmpdir, res_ref = openmc.deplete.Results(path_reference) # Assert same mats - _assert_same_mats(res_test, res_ref) + assert_same_mats(res_ref, res_test) tol = 1.0e-14 assert_atoms_equal(res_ref, res_test, tol) @@ -155,7 +155,7 @@ def test_against_coupled(run_in_tmpdir, res_ref = openmc.deplete.Results(path_reference) # Assert same mats - _assert_same_mats(res_test, res_ref) + assert_same_mats(res_test, res_ref) assert_atoms_equal(res_ref, res_test, atom_tol) assert_reaction_rates_equal(res_ref, res_test, rx_tol) @@ -172,6 +172,7 @@ def _create_operator(from_nuclides, for nuc, dens in fuel.get_nuclide_atom_densities().items(): nuclides[nuc] = dens + openmc.reset_auto_ids() op = IndependentOperator.from_nuclides(fuel.volume, nuclides, flux, @@ -187,20 +188,3 @@ def _create_operator(from_nuclides, normalization_mode=normalization_mode) return op - - -def _assert_same_mats(res_ref, res_test): - for mat in res_ref[0].index_mat: - assert mat in res_test[0].index_mat, \ - f"Material {mat} not in new results." - for nuc in res_ref[0].index_nuc: - assert nuc in res_test[0].index_nuc, \ - f"Nuclide {nuc} not in new results." - - for mat in res_test[0].index_mat: - assert mat in res_ref[0].index_mat, \ - f"Material {mat} not in old results." - for nuc in res_test[0].index_nuc: - assert nuc in res_ref[0].index_nuc, \ - f"Nuclide {nuc} not in old results." - diff --git a/tests/regression_tests/deplete_with_transfer_rates/test.py b/tests/regression_tests/deplete_with_transfer_rates/test.py index 9bd5a052b..10d60866a 100644 --- a/tests/regression_tests/deplete_with_transfer_rates/test.py +++ b/tests/regression_tests/deplete_with_transfer_rates/test.py @@ -11,11 +11,12 @@ import openmc.deplete from openmc.deplete import CoupledOperator from tests.regression_tests import config, assert_reaction_rates_equal, \ - assert_atoms_equal + assert_atoms_equal, assert_same_mats @pytest.fixture def model(): + openmc.reset_auto_ids() f = openmc.Material(name="f") f.add_element("U", 1, percent_type="ao", enrichment=4.25) f.add_element("O", 2) @@ -66,7 +67,7 @@ def test_transfer_rates(run_in_tmpdir, model, rate, dest_mat, power, ref_result) integrator = openmc.deplete.PredictorIntegrator( op, [1], power, timestep_units = 'd') integrator.add_transfer_rate('f', transfer_elements, rate, - destination_material=dest_mat) + destination_material=dest_mat) integrator.integrate() # Get path to test and reference results @@ -82,5 +83,6 @@ def test_transfer_rates(run_in_tmpdir, model, rate, dest_mat, power, ref_result) res_ref = openmc.deplete.Results(path_reference) res_test = openmc.deplete.Results(path_test) + assert_same_mats(res_ref, res_test) assert_atoms_equal(res_ref, res_test, 1e-6) assert_reaction_rates_equal(res_ref, res_test) diff --git a/tests/unit_tests/test_deplete_decay.py b/tests/unit_tests/test_deplete_decay.py index aca812560..cebbc16fa 100644 --- a/tests/unit_tests/test_deplete_decay.py +++ b/tests/unit_tests/test_deplete_decay.py @@ -40,8 +40,9 @@ def test_deplete_decay_products(run_in_tmpdir): # Get concentration of H1 and He4 results = openmc.deplete.Results('depletion_results.h5') - _, h1 = results.get_atoms("1", "H1") - _, he4 = results.get_atoms("1", "He4") + mat_id = op.materials[0].id + _, h1 = results.get_atoms(f"{mat_id}", "H1") + _, he4 = results.get_atoms(f"{mat_id}", "He4") # Since we started with 1e24 atoms of Li5, we should have 1e24 atoms of both # H1 and He4 @@ -78,6 +79,6 @@ def test_deplete_decay_step_fissionable(run_in_tmpdir): # Get concentration of U238. It should be unchanged since this chain has no U238 decay. results = openmc.deplete.Results('depletion_results.h5') - _, u238 = results.get_atoms("1", "U238") + _, u238 = results.get_atoms(f"{mat.id}", "U238") assert u238[1] == pytest.approx(original_atoms) diff --git a/tests/unit_tests/test_deplete_transfer_rates.py b/tests/unit_tests/test_deplete_transfer_rates.py index 847606be4..140777cd6 100644 --- a/tests/unit_tests/test_deplete_transfer_rates.py +++ b/tests/unit_tests/test_deplete_transfer_rates.py @@ -71,6 +71,7 @@ def model(): def test_get_set(model, case_name, transfer_rates): """Tests the get/set methods""" + openmc.reset_auto_ids() op = CoupledOperator(model, CHAIN_PATH) transfer = TransferRates(op, model) From b54de4d76176c43ebb2edee3ddbc78e709b3c910 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 2 Oct 2024 12:12:44 -0500 Subject: [PATCH 173/671] Fix check for trigger score name (#3155) --- src/tallies/tally.cpp | 2 +- tests/unit_tests/test_triggers.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index 674987b8f..ba899611c 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -714,7 +714,7 @@ void Tally::init_triggers(pugi::xml_node node) } else { int i_score = 0; for (; i_score < this->scores_.size(); ++i_score) { - if (reaction_name(this->scores_[i_score]) == score_str) + if (this->scores_[i_score] == reaction_type(score_str)) break; } if (i_score == this->scores_.size()) { diff --git a/tests/unit_tests/test_triggers.py b/tests/unit_tests/test_triggers.py index 6b9e54eeb..14bda0cce 100644 --- a/tests/unit_tests/test_triggers.py +++ b/tests/unit_tests/test_triggers.py @@ -113,3 +113,34 @@ def test_tally_trigger_zero_ignored(run_in_tmpdir): total_batches = sp.n_realizations + sp.n_inactive assert total_batches < pincell.settings.trigger_max_batches + + +def test_trigger_he3_production(run_in_tmpdir): + li6 = openmc.Material() + li6.set_density('g/cm3', 1.0) + li6.add_nuclide('Li6', 1.0) + + sph = openmc.Sphere(r=20, boundary_type='vacuum') + outer_cell = openmc.Cell(fill=li6, region=-sph) + model = openmc.Model() + model.geometry = openmc.Geometry([outer_cell]) + model.settings.source = openmc.IndependentSource( + energy=openmc.stats.delta_function(14.1e6) + ) + model.settings.batches = 10 + model.settings.particles = 100 + model.settings.run_mode = 'fixed source' + model.settings.trigger_active = True + model.settings.trigger_batch_interval = 10 + model.settings.trigger_max_batches = 30 + + # Define tally with trigger + trigger = openmc.Trigger(trigger_type='rel_err', threshold=0.0001) + trigger.scores = ['He3-production'] + he3_production_tally = openmc.Tally() + he3_production_tally.scores = ['He3-production'] + he3_production_tally.triggers = [trigger] + model.tallies = openmc.Tallies([he3_production_tally]) + + # Run model to verify that trigger works + model.run() From 9686851e7a1bf1dc49ad313673829bbbabd3945e Mon Sep 17 00:00:00 2001 From: Zoe Prieto <101403129+zoeprieto@users.noreply.github.com> Date: Thu, 3 Oct 2024 19:32:03 -0300 Subject: [PATCH 174/671] Write surface source files per batch (#3124) Co-authored-by: Patrick Shriwise Co-authored-by: Paul Romano --- docs/source/io_formats/settings.rst | 9 +++ docs/source/usersguide/settings.rst | 11 ++++ include/openmc/settings.h | 15 +++-- include/openmc/simulation.h | 1 + include/openmc/state_point.h | 8 ++- openmc/settings.py | 13 ++-- src/finalize.cpp | 5 ++ src/settings.cpp | 13 +++- src/simulation.cpp | 60 +++++++++++-------- src/state_point.cpp | 20 ++++++- tests/regression_tests/dagmc/legacy/test.py | 3 +- tests/regression_tests/surface_source/test.py | 2 +- .../surface_source_write/test.py | 2 +- tests/unit_tests/test_surface_source_write.py | 53 ++++++++++++++++ 14 files changed, 167 insertions(+), 48 deletions(-) diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index e395ada68..2c6c32656 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -923,6 +923,15 @@ attributes/sub-elements: *Default*: None + :max_source_files: + An integer value indicating the number of surface source files to be written + containing the maximum number of particles each. The surface source bank + will be cleared in simulation memory each time a surface source file is + written. By default a ``surface_source.h5`` file will be created when the + maximum number of saved particles is reached. + + *Default*: 1 + :mcpl: An optional boolean which indicates if the banked particles should be written to a file in the MCPL_-format instead of the native HDF5-based diff --git a/docs/source/usersguide/settings.rst b/docs/source/usersguide/settings.rst index d73d90cbb..4111481a9 100644 --- a/docs/source/usersguide/settings.rst +++ b/docs/source/usersguide/settings.rst @@ -336,6 +336,17 @@ or particles going to a cell:: .. note:: The ``cell``, ``cellfrom`` and ``cellto`` attributes cannot be used simultaneously. +To generate more than one surface source files when the maximum number of stored +particles is reached, ``max_source_files`` is available. The surface source bank +will be cleared in simulation memory each time a surface source file is written. +As an example, to write a maximum of three surface source files::: + + settings.surf_source_write = { + 'surfaces_ids': [1, 2, 3], + 'max_particles': 10000, + 'max_source_files': 3 + } + .. _compiled_source: Compiled Sources diff --git a/include/openmc/settings.h b/include/openmc/settings.h index a95f1ced9..1e44c0880 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -129,14 +129,17 @@ extern std::unordered_set statepoint_batch; //!< Batches when state should be written extern std::unordered_set source_write_surf_id; //!< Surface ids where sources will be written + extern int max_history_splits; //!< maximum number of particle splits for weight windows -extern int64_t max_surface_particles; //!< maximum number of particles to be - //!< banked on surfaces per process -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 int64_t ssw_max_particles; //!< maximum number of particles to be + //!< banked on surfaces per process +extern int64_t ssw_max_files; //!< maximum number of surface source files + //!< to be created +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 TemperatureMethod temperature_method; //!< method for choosing temperatures extern double diff --git a/include/openmc/simulation.h b/include/openmc/simulation.h index dd8bb7cdf..3e4e24e1d 100644 --- a/include/openmc/simulation.h +++ b/include/openmc/simulation.h @@ -37,6 +37,7 @@ extern "C" int n_lost_particles; //!< cumulative number of lost particles extern "C" bool need_depletion_rx; //!< need to calculate depletion rx? extern "C" int restart_batch; //!< batch at which a restart job resumed extern "C" bool satisfy_triggers; //!< have tally triggers been satisfied? +extern int ssw_current_file; //!< current surface source file extern "C" int total_gen; //!< total number of generations simulated extern double total_weight; //!< Total source weight in a batch extern int64_t work_per_rank; //!< number of particles per MPI rank diff --git a/include/openmc/state_point.h b/include/openmc/state_point.h index 95524f6b6..f0d41e169 100644 --- a/include/openmc/state_point.h +++ b/include/openmc/state_point.h @@ -2,6 +2,7 @@ #define OPENMC_STATE_POINT_H #include +#include #include @@ -33,8 +34,11 @@ void load_state_point(); // values on each rank, used to create global indexing. This vector // can be created by calling calculate_parallel_index_vector on // source_bank.size() if such a vector is not already available. -void write_source_point(const char* filename, gsl::span source_bank, - const vector& bank_index); +void write_h5_source_point(const char* filename, + gsl::span source_bank, const vector& bank_index); + +void write_source_point(std::string, gsl::span source_bank, + const vector& bank_index, bool use_mcpl); // This appends a source bank specification to an HDF5 file // that's already open. It is used internally by write_source_point. diff --git a/openmc/settings.py b/openmc/settings.py index ddaac0401..96e6368e4 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -211,6 +211,7 @@ class Settings: banked (int) :max_particles: Maximum number of particles to be banked on surfaces per process (int) + :max_source_files: Maximum number of surface source files to be created (int) :mcpl: Output in the form of an MCPL-file (bool) :cell: Cell ID used to determine if particles crossing identified surfaces are to be banked. Particles coming from or going to this @@ -718,7 +719,7 @@ class Settings: cv.check_value( "surface source writing key", key, - ("surface_ids", "max_particles", "mcpl", "cell", "cellfrom", "cellto"), + ("surface_ids", "max_particles", "max_source_files", "mcpl", "cell", "cellfrom", "cellto"), ) if key == "surface_ids": cv.check_type( @@ -726,11 +727,13 @@ class Settings: ) for surf_id in value: cv.check_greater_than("surface id for source banking", surf_id, 0) + elif key == "mcpl": cv.check_type("write to an MCPL-format file", value, bool) - elif key in ("max_particles", "cell", "cellfrom", "cellto"): + elif key in ("max_particles", "max_source_files", "cell", "cellfrom", "cellto"): name = { "max_particles": "maximum particle banks on surfaces per process", + "max_source_files": "maximun surface source files to be written", "cell": "Cell ID for source banking (from or to)", "cellfrom": "Cell ID for source banking (from only)", "cellto": "Cell ID for source banking (to only)", @@ -1251,7 +1254,7 @@ class Settings: if "mcpl" in self._surf_source_write: subelement = ET.SubElement(element, "mcpl") subelement.text = str(self._surf_source_write["mcpl"]).lower() - for key in ("max_particles", "cell", "cellfrom", "cellto"): + for key in ("max_particles", "max_source_files", "cell", "cellfrom", "cellto"): if key in self._surf_source_write: subelement = ET.SubElement(element, key) subelement.text = str(self._surf_source_write[key]) @@ -1650,14 +1653,14 @@ class Settings: elem = root.find('surf_source_write') if elem is None: return - for key in ('surface_ids', 'max_particles', 'mcpl', 'cell', 'cellto', 'cellfrom'): + for key in ('surface_ids', 'max_particles', 'max_source_files', 'mcpl', 'cell', 'cellto', 'cellfrom'): value = get_text(elem, key) if value is not None: if key == 'surface_ids': value = [int(x) for x in value.split()] elif key == 'mcpl': value = value in ('true', '1') - elif key in ('max_particles', 'cell', 'cellfrom', 'cellto'): + elif key in ('max_particles', 'max_source_files', 'cell', 'cellfrom', 'cellto'): value = int(value) self.surf_source_write[key] = value diff --git a/src/finalize.cpp b/src/finalize.cpp index 2bde0e338..08c2fced3 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -120,6 +120,10 @@ int openmc_finalize() settings::source_latest = false; settings::source_separate = false; settings::source_write = true; + settings::ssw_cell_id = C_NONE; + settings::ssw_cell_type = SSWCellType::None; + settings::ssw_max_particles = 0; + settings::ssw_max_files = 1; settings::survival_biasing = false; settings::temperature_default = 293.6; settings::temperature_method = TemperatureMethod::NEAREST; @@ -141,6 +145,7 @@ int openmc_finalize() simulation::keff = 1.0; simulation::need_depletion_rx = false; + simulation::ssw_current_file = 1; simulation::total_gen = 0; simulation::entropy_mesh = nullptr; diff --git a/src/settings.cpp b/src/settings.cpp index ba451e219..3a905bdf9 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -118,7 +118,8 @@ SolverType solver_type {SolverType::MONTE_CARLO}; std::unordered_set sourcepoint_batch; std::unordered_set statepoint_batch; std::unordered_set source_write_surf_id; -int64_t max_surface_particles; +int64_t ssw_max_particles; +int64_t ssw_max_files; int64_t ssw_cell_id {C_NONE}; SSWCellType ssw_cell_type {SSWCellType::None}; TemperatureMethod temperature_method {TemperatureMethod::NEAREST}; @@ -799,14 +800,20 @@ void read_settings_xml(pugi::xml_node root) // Get maximum number of particles to be banked per surface if (check_for_node(node_ssw, "max_particles")) { - max_surface_particles = - std::stoll(get_node_value(node_ssw, "max_particles")); + ssw_max_particles = std::stoll(get_node_value(node_ssw, "max_particles")); } else { fatal_error("A maximum number of particles needs to be specified " "using the 'max_particles' parameter to store surface " "source points."); } + // Get maximum number of surface source files to be created + if (check_for_node(node_ssw, "max_source_files")) { + ssw_max_files = std::stoll(get_node_value(node_ssw, "max_source_files")); + } else { + ssw_max_files = 1; + } + if (check_for_node(node_ssw, "mcpl")) { surf_mcpl_write = get_node_value_bool(node_ssw, "mcpl"); diff --git a/src/simulation.cpp b/src/simulation.cpp index be03e4855..09b376473 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -117,6 +117,7 @@ int openmc_simulation_init() // Reset global variables -- this is done before loading state point (as that // will potentially populate k_generation and entropy) simulation::current_batch = 0; + simulation::ssw_current_file = 1; simulation::k_generation.clear(); simulation::entropy.clear(); openmc_reset(); @@ -308,6 +309,7 @@ int n_lost_particles {0}; bool need_depletion_rx {false}; int restart_batch; bool satisfy_triggers {false}; +int ssw_current_file; int total_gen {0}; double total_weight; int64_t work_per_rank; @@ -337,7 +339,7 @@ void allocate_banks() if (settings::surf_source_write) { // Allocate surface source bank - simulation::surf_source_bank.reserve(settings::max_surface_particles); + simulation::surf_source_bank.reserve(settings::ssw_max_particles); } } @@ -345,7 +347,6 @@ void initialize_batch() { // Increment current batch ++simulation::current_batch; - if (settings::run_mode == RunMode::FIXED_SOURCE) { if (settings::solver_type == SolverType::RANDOM_RAY && simulation::current_batch < settings::n_inactive + 1) { @@ -439,42 +440,49 @@ void finalize_batch() std::string source_point_filename = fmt::format("{0}source.{1:0{2}}", settings::path_output, simulation::current_batch, w); gsl::span bankspan(simulation::source_bank); - if (settings::source_mcpl_write) { - write_mcpl_source_point( - source_point_filename.c_str(), bankspan, simulation::work_index); - } else { - write_source_point( - source_point_filename.c_str(), bankspan, simulation::work_index); - } + write_source_point(source_point_filename, bankspan, + simulation::work_index, settings::source_mcpl_write); } // Write a continously-overwritten source point if requested. if (settings::source_latest) { - // note: correct file extension appended automatically auto filename = settings::path_output + "source"; gsl::span bankspan(simulation::source_bank); - if (settings::source_mcpl_write) { - write_mcpl_source_point( - filename.c_str(), bankspan, simulation::work_index); - } else { - write_source_point(filename.c_str(), bankspan, simulation::work_index); - } + write_source_point(filename.c_str(), bankspan, simulation::work_index, + settings::source_mcpl_write); } } // Write out surface source if requested. if (settings::surf_source_write && - simulation::current_batch == settings::n_batches) { - auto filename = settings::path_output + "surface_source"; - auto surf_work_index = - mpi::calculate_parallel_index_vector(simulation::surf_source_bank.size()); - gsl::span surfbankspan(simulation::surf_source_bank.begin(), - simulation::surf_source_bank.size()); - if (settings::surf_mcpl_write) { - write_mcpl_source_point(filename.c_str(), surfbankspan, surf_work_index); - } else { - write_source_point(filename.c_str(), surfbankspan, surf_work_index); + simulation::ssw_current_file <= settings::ssw_max_files) { + bool last_batch = (simulation::current_batch == settings::n_batches); + if (simulation::surf_source_bank.full() || last_batch) { + // Determine appropriate filename + auto filename = fmt::format("{}surface_source.{}", settings::path_output, + simulation::current_batch); + if (settings::ssw_max_files == 1 || + (simulation::ssw_current_file == 1 && last_batch)) { + filename = settings::path_output + "surface_source"; + } + + // Get span of source bank and calculate parallel index vector + auto surf_work_index = mpi::calculate_parallel_index_vector( + simulation::surf_source_bank.size()); + gsl::span surfbankspan(simulation::surf_source_bank.begin(), + simulation::surf_source_bank.size()); + + // Write surface source file + write_source_point( + filename, surfbankspan, surf_work_index, settings::surf_mcpl_write); + + // Reset surface source bank and increment counter + simulation::surf_source_bank.clear(); + if (!last_batch && settings::ssw_max_files >= 1) { + simulation::surf_source_bank.reserve(settings::ssw_max_particles); + } + ++simulation::ssw_current_file; } } } diff --git a/src/state_point.cpp b/src/state_point.cpp index c7b7d6ad8..adc026fa3 100644 --- a/src/state_point.cpp +++ b/src/state_point.cpp @@ -15,6 +15,7 @@ #include "openmc/error.h" #include "openmc/file_utils.h" #include "openmc/hdf5_interface.h" +#include "openmc/mcpl_interface.h" #include "openmc/mesh.h" #include "openmc/message_passing.h" #include "openmc/mgxs_interface.h" @@ -568,8 +569,23 @@ hid_t h5banktype() return banktype; } -void write_source_point(const char* filename, gsl::span source_bank, - const vector& bank_index) +void write_source_point(std::string filename, gsl::span source_bank, + const vector& bank_index, bool use_mcpl) +{ + std::string ext = use_mcpl ? "mcpl" : "h5"; + write_message("Creating source file {}.{} with {} particles ...", filename, + ext, source_bank.size(), 5); + + // Dispatch to appropriate function based on file type + if (use_mcpl) { + write_mcpl_source_point(filename.c_str(), source_bank, bank_index); + } else { + write_h5_source_point(filename.c_str(), source_bank, bank_index); + } +} + +void write_h5_source_point(const char* filename, + gsl::span source_bank, const vector& bank_index) { // When using parallel HDF5, the file is written to collectively by all // processes. With MPI-only, the file is opened and written by the master diff --git a/tests/regression_tests/dagmc/legacy/test.py b/tests/regression_tests/dagmc/legacy/test.py index 884264f30..b6b1e376b 100644 --- a/tests/regression_tests/dagmc/legacy/test.py +++ b/tests/regression_tests/dagmc/legacy/test.py @@ -104,5 +104,4 @@ def test_surf_source(model): def test_dagmc(model): harness = PyAPITestHarness('statepoint.5.h5', model) - harness.main() - + harness.main() \ No newline at end of file diff --git a/tests/regression_tests/surface_source/test.py b/tests/regression_tests/surface_source/test.py index 81bba0c94..251e9e9ad 100644 --- a/tests/regression_tests/surface_source/test.py +++ b/tests/regression_tests/surface_source/test.py @@ -132,4 +132,4 @@ def test_surface_source_read(model): harness = SurfaceSourceTestHarness('statepoint.10.h5', model, 'inputs_true_read.dat') - harness.main() + harness.main() \ No newline at end of file diff --git a/tests/regression_tests/surface_source_write/test.py b/tests/regression_tests/surface_source_write/test.py index 38581736e..fe11d68a6 100644 --- a/tests/regression_tests/surface_source_write/test.py +++ b/tests/regression_tests/surface_source_write/test.py @@ -1129,4 +1129,4 @@ def test_surface_source_cell_dagmc( harness = SurfaceSourceWriteTestHarness( "statepoint.5.h5", model=model, workdir=folder ) - harness.main() + harness.main() \ No newline at end of file diff --git a/tests/unit_tests/test_surface_source_write.py b/tests/unit_tests/test_surface_source_write.py index 472363073..6f18d32b7 100644 --- a/tests/unit_tests/test_surface_source_write.py +++ b/tests/unit_tests/test_surface_source_write.py @@ -32,6 +32,7 @@ def geometry(): {"max_particles": 200, "surface_ids": [2], "cell": 1}, {"max_particles": 200, "surface_ids": [2], "cellto": 1}, {"max_particles": 200, "surface_ids": [2], "cellfrom": 1}, + {"max_particles": 200, "surface_ids": [2], "max_source_files": 1}, ], ) def test_xml_serialization(parameter, run_in_tmpdir): @@ -44,6 +45,58 @@ def test_xml_serialization(parameter, run_in_tmpdir): assert read_settings.surf_source_write == parameter +@pytest.fixture(scope="module") +def model(): + """Simple hydrogen sphere geometry""" + openmc.reset_auto_ids() + model = openmc.Model() + + # Material + h1 = openmc.Material(name="H1") + h1.add_nuclide("H1", 1.0) + h1.set_density('g/cm3', 1e-7) + + # Geometry + radius = 1.0 + sphere = openmc.Sphere(r=radius, boundary_type="vacuum") + cell = openmc.Cell(region=-sphere, fill=h1) + model.geometry = openmc.Geometry([cell]) + + # Settings + model.settings = openmc.Settings() + model.settings.run_mode = "fixed source" + model.settings.particles = 100 + model.settings.batches = 3 + model.settings.seed = 1 + + distribution = openmc.stats.Point() + model.settings.source = openmc.IndependentSource(space=distribution) + return model + + +@pytest.mark.parametrize( + "max_particles, max_source_files", + [ + (100, 2), + (100, 3), + (100, 1), + ], +) +def test_number_surface_source_file_created(max_particles, max_source_files, + run_in_tmpdir, model): + """Check the number of surface source files written.""" + model.settings.surf_source_write = { + "max_particles": max_particles, + "max_source_files": max_source_files + } + model.run() + should_be_numbered = max_source_files > 1 + for i in range(1, max_source_files + 1): + if should_be_numbered: + assert Path(f"surface_source.{i}.h5").exists() + if not should_be_numbered: + assert Path("surface_source.h5").exists() + ERROR_MSG_1 = ( "A maximum number of particles needs to be specified " "using the 'max_particles' parameter to store surface " From 3a5b218728e09fe802d031d96c0350f3414ce671 Mon Sep 17 00:00:00 2001 From: Rayan HADDAD <103775910+rayanhaddad169@users.noreply.github.com> Date: Fri, 4 Oct 2024 01:13:18 +0200 Subject: [PATCH 175/671] adapt the openmc-update-inputs script for surfaces (#3131) Co-authored-by: r.haddad --- scripts/openmc-update-inputs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/scripts/openmc-update-inputs b/scripts/openmc-update-inputs index 2d49c0626..5ee1f1ca0 100755 --- a/scripts/openmc-update-inputs +++ b/scripts/openmc-update-inputs @@ -1,7 +1,5 @@ #!/usr/bin/env python3 -"""Update OpenMC's input XML files to the latest format. - -""" +"""Update OpenMC's input XML files to the latest format.""" import argparse from itertools import chain @@ -201,6 +199,16 @@ def update_geometry(geometry_root): was_updated = True + for surface in root.findall('surface'): + for attribute in ['type', 'coeffs', 'boundary']: + if surface.find(attribute) is not None: + value = surface.find(attribute).text + surface.attrib[attribute] = value + + child_element = surface.find(attribute) + surface.remove(child_element) + was_updated = True + # Remove 'type' from lattice definitions. for lat in root.iter('lattice'): elem = lat.find('type') @@ -296,4 +304,4 @@ if __name__ == '__main__': move(fname, fname + '.original') # Write a new geometry file. - tree.write(fname, xml_declaration=True) + tree.write(fname, xml_declaration=True) \ No newline at end of file From 836428666d5264343676bbb977b15bc67be45a3f Mon Sep 17 00:00:00 2001 From: rzehumat <49885819+rzehumat@users.noreply.github.com> Date: Fri, 4 Oct 2024 02:51:12 +0200 Subject: [PATCH 176/671] [docs] theory on PCG random number generator (#3134) Co-authored-by: Matej Rzehulka Co-authored-by: Paul Romano --- docs/source/methods/random_numbers.rst | 43 ++++++++++++++++++-------- src/random_lcg.cpp | 3 +- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/docs/source/methods/random_numbers.rst b/docs/source/methods/random_numbers.rst index 0cefc9156..4376bbdb0 100644 --- a/docs/source/methods/random_numbers.rst +++ b/docs/source/methods/random_numbers.rst @@ -7,7 +7,7 @@ Random Number Generation In order to sample probability distributions, one must be able to produce random numbers. The standard technique to do this is to generate numbers on the interval :math:`[0,1)` from a deterministic sequence that has properties that -make it appear to be random, e.g. being uniformly distributed and not exhibiting +make it appear to be random, e.g., being uniformly distributed and not exhibiting correlation between successive terms. Since the numbers produced this way are not truly "random" in a strict sense, they are typically referred to as pseudorandom numbers, and the techniques used to generate them are pseudorandom @@ -15,6 +15,11 @@ number generators (PRNGs). Numbers sampled on the unit interval can then be transformed for the purpose of sampling other continuous or discrete probability distributions. +There are many different algorithms for pseudorandom number generation. OpenMC +currently uses `permuted congruential generator`_ (PCG), which builds on top of +the simpler linear congruential generator (LCG). Both algorithms are described +below. + ------------------------------ Linear Congruential Generators ------------------------------ @@ -37,8 +42,8 @@ be generated with a method chosen at random. Some theory should be used." Typically, :math:`M` is chosen to be a power of two as this enables :math:`x \mod M` to be performed using the bitwise AND operator with a bit mask. The constants for the linear congruential generator used by default in OpenMC are -:math:`g = 2806196910506780709`, :math:`c = 1`, and :math:`M = 2^{63}` (see -`L'Ecuyer`_). +:math:`g = 2806196910506780709`, :math:`c = 1`, and :math:`M = 2^{63}` (from +`L'Ecuyer `_). Skip-ahead Capability --------------------- @@ -50,7 +55,8 @@ want to skip ahead :math:`N` random numbers and :math:`N` is large, the cost of sampling :math:`N` random numbers to get to that position may be prohibitively expensive. Fortunately, algorithms have been developed that allow us to skip ahead in :math:`O(\log_2 N)` operations instead of :math:`O(N)`. One algorithm -to do so is described in a paper by Brown_. This algorithm relies on the following +to do so is described in a `paper by Brown +`_. This algorithm relies on the following relationship: .. math:: @@ -58,15 +64,26 @@ relationship: \xi_{i+k} = g^k \xi_i + c \frac{g^k - 1}{g - 1} \mod M -Note that equation :eq:`lcg-skipahead` has the same general form as equation :eq:`lcg`, so -the idea is to determine the new multiplicative and additive constants in -:math:`O(\log_2 N)` operations. - -.. only:: html - - .. rubric:: References +Note that equation :eq:`lcg-skipahead` has the same general form as equation +:eq:`lcg`, so the idea is to determine the new multiplicative and additive +constants in :math:`O(\log_2 N)` operations. -.. _L'Ecuyer: https://doi.org/10.1090/S0025-5718-99-00996-5 -.. _Brown: https://laws.lanl.gov/vhosts/mcnp.lanl.gov/pdf_files/anl-rn-arb-stride.pdf +-------------------------------- +Permuted Congruential Generators +-------------------------------- + +The `permuted congruential generator`_ (PCG) algorithm aims to improve upon the +LCG algorithm by permuting the output. The algorithm works on the basic +principle of first advancing the generator state using the LCG algorithm and +then applying a permutation function on the LCG state to obtain the output. This +results in increased statistical quality as measured by common statistical tests +while exhibiting a very small performance overhead relative to the LCG algorithm +and an equivalent memory footprint. For further details, see the original +technical report by `O'Neill +`_. OpenMC uses the +PCG-RXS-M-XS variant with a 64-bit state and 64-bit output. + .. _linear congruential generator: https://en.wikipedia.org/wiki/Linear_congruential_generator + +.. _permuted congruential generator: https://en.wikipedia.org/wiki/Permuted_congruential_generator diff --git a/src/random_lcg.cpp b/src/random_lcg.cpp index 581d69617..ca4467719 100644 --- a/src/random_lcg.cpp +++ b/src/random_lcg.cpp @@ -17,7 +17,8 @@ constexpr uint64_t prn_stride {152917LL}; // stride between particles //============================================================================== // 64 bit implementation of the PCG-RXS-M-XS 64-bit state / 64-bit output -// geneator Adapted from: https://github.com/imneme/pcg-c +// geneator Adapted from: https://github.com/imneme/pcg-c, in particular +// https://github.com/imneme/pcg-c/blob/83252d9c23df9c82ecb42210afed61a7b42402d7/include/pcg_variants.h#L188-L192 // @techreport{oneill:pcg2014, // title = "PCG: A Family of Simple Fast Space-Efficient Statistically Good // Algorithms for Random Number Generation", author = "Melissa E. O'Neill", From 1a520c9f4d598f96b4e6712fefa1d25b7d4da404 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Fri, 4 Oct 2024 03:37:51 +0200 Subject: [PATCH 177/671] Adding material.get_element_atom_densities (#3103) Co-authored-by: Paul Romano --- openmc/data/data.py | 20 +++++++++----- openmc/material.py | 43 +++++++++++++++++++++++++++++++ tests/unit_tests/test_material.py | 22 ++++++++++++++++ 3 files changed, 78 insertions(+), 7 deletions(-) diff --git a/openmc/data/data.py b/openmc/data/data.py index d94d6aaaa..408adf2e4 100644 --- a/openmc/data/data.py +++ b/openmc/data/data.py @@ -549,7 +549,18 @@ def gnds_name(Z, A, m=0): return f'{ATOMIC_SYMBOL[Z]}{A}' -def isotopes(element): + +def _get_element_symbol(element: str) -> str: + if len(element) > 2: + symbol = ELEMENT_SYMBOL.get(element.lower()) + if symbol is None: + raise ValueError(f'Element name "{element}" not recognized') + return symbol + else: + return element + + +def isotopes(element: str) -> list[tuple[str, float]]: """Return naturally occurring isotopes and their abundances .. versionadded:: 0.12.1 @@ -570,12 +581,7 @@ def isotopes(element): If the element name is not recognized """ - # Convert name to symbol if needed - if len(element) > 2: - symbol = ELEMENT_SYMBOL.get(element.lower()) - if symbol is None: - raise ValueError(f'Element name "{element}" not recognised') - element = symbol + element = _get_element_symbol(element) # Get the nuclides present in nature result = [] diff --git a/openmc/material.py b/openmc/material.py index 5b958c75a..f550fd649 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -18,6 +18,7 @@ from ._xml import clean_indentation, reorder_attributes from .mixin import IDManagerMixin from openmc.checkvalue import PathLike from openmc.stats import Univariate, Discrete, Mixture +from openmc.data.data import _get_element_symbol # Units for density supported by OpenMC @@ -1075,6 +1076,48 @@ class Material(IDManagerMixin): return nuclides + def get_element_atom_densities(self, element: str | None = None) -> dict[str, float]: + """Returns one or all elements in the material and their atomic + densities in units of atom/b-cm + + .. versionadded:: 0.15.1 + + Parameters + ---------- + element : str, optional + Element for which atom density is desired. If not specified, the + atom density for each element in the material is given. + + Returns + ------- + elements : dict + Dictionary whose keys are element names and values are densities in + [atom/b-cm] + + """ + if element is not None: + element = _get_element_symbol(element) + + nuc_densities = self.get_nuclide_atom_densities() + + # Initialize an empty dictionary for summed values + densities = {} + + # Accumulate densities for each nuclide + for nuclide, density in nuc_densities.items(): + nuc_element = openmc.data.ATOMIC_SYMBOL[openmc.data.zam(nuclide)[0]] + if element is None or element == nuc_element: + if nuc_element not in densities: + densities[nuc_element] = 0.0 + densities[nuc_element] += float(density) + + # If specific element was requested, make sure it is present + if element is not None and element not in densities: + raise ValueError(f'Element {element} not found in material.') + + return densities + + def get_activity(self, units: str = 'Bq/cm3', by_nuclide: bool = False, volume: float | None = None) -> dict[str, float] | float: """Returns the activity of the material or for each nuclide in the diff --git a/tests/unit_tests/test_material.py b/tests/unit_tests/test_material.py index 44c0730fb..94ba82571 100644 --- a/tests/unit_tests/test_material.py +++ b/tests/unit_tests/test_material.py @@ -380,6 +380,28 @@ def test_get_nuclide_atom_densities_specific(uo2): assert all_nuc['O16'] == one_nuc['O16'] +def test_get_element_atom_densities(uo2): + for element, density in uo2.get_element_atom_densities().items(): + assert element in ('U', 'O') + assert density > 0 + + +def test_get_element_atom_densities_specific(uo2): + one_nuc = uo2.get_element_atom_densities('O') + assert list(one_nuc.keys()) == ['O'] + assert list(one_nuc.values())[0] > 0 + + one_nuc = uo2.get_element_atom_densities('uranium') + assert list(one_nuc.keys()) == ['U'] + assert list(one_nuc.values())[0] > 0 + + with pytest.raises(ValueError, match='not found'): + uo2.get_element_atom_densities('Li') + + with pytest.raises(ValueError, match='not recognized'): + uo2.get_element_atom_densities('proximium') + + def test_get_nuclide_atoms(): mat = openmc.Material() mat.add_nuclide('Li6', 1.0) From c285a2c4ce8a549a02f28acbce1836c11876b8ab Mon Sep 17 00:00:00 2001 From: Zoe Prieto <101403129+zoeprieto@users.noreply.github.com> Date: Fri, 4 Oct 2024 02:07:03 -0300 Subject: [PATCH 178/671] Implement filter for cosine of angle of surface crossing (#2768) Co-authored-by: Paul Romano --- CMakeLists.txt | 1 + docs/source/pythonapi/base.rst | 1 + include/openmc/tallies/filter.h | 1 + include/openmc/tallies/filter_mu.h | 2 +- include/openmc/tallies/filter_musurface.h | 34 +++++++++++++++ openmc/filter.py | 42 +++++++++++++++++- openmc/lib/filter.py | 11 +++-- src/tallies/filter.cpp | 3 ++ src/tallies/filter_musurface.cpp | 36 ++++++++++++++++ .../filter_musurface/__init__.py | 0 .../filter_musurface/inputs_true.dat | 37 ++++++++++++++++ .../filter_musurface/results_true.dat | 11 +++++ .../regression_tests/filter_musurface/test.py | 43 +++++++++++++++++++ tests/unit_tests/test_filter_musurface.py | 38 ++++++++++++++++ 14 files changed, 254 insertions(+), 6 deletions(-) create mode 100644 include/openmc/tallies/filter_musurface.h create mode 100644 src/tallies/filter_musurface.cpp create mode 100644 tests/regression_tests/filter_musurface/__init__.py create mode 100644 tests/regression_tests/filter_musurface/inputs_true.dat create mode 100644 tests/regression_tests/filter_musurface/results_true.dat create mode 100644 tests/regression_tests/filter_musurface/test.py create mode 100644 tests/unit_tests/test_filter_musurface.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 0f4cc1b52..b4011434e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -416,6 +416,7 @@ list(APPEND libopenmc_SOURCES src/tallies/filter_meshborn.cpp src/tallies/filter_meshsurface.cpp src/tallies/filter_mu.cpp + src/tallies/filter_musurface.cpp src/tallies/filter_particle.cpp src/tallies/filter_polar.cpp src/tallies/filter_sph_harm.cpp diff --git a/docs/source/pythonapi/base.rst b/docs/source/pythonapi/base.rst index 5ae9f20ed..611d2c216 100644 --- a/docs/source/pythonapi/base.rst +++ b/docs/source/pythonapi/base.rst @@ -133,6 +133,7 @@ Constructing Tallies openmc.EnergyFilter openmc.EnergyoutFilter openmc.MuFilter + openmc.MuSurfaceFilter openmc.PolarFilter openmc.AzimuthalFilter openmc.DistribcellFilter diff --git a/include/openmc/tallies/filter.h b/include/openmc/tallies/filter.h index 1166c0eee..210ab284b 100644 --- a/include/openmc/tallies/filter.h +++ b/include/openmc/tallies/filter.h @@ -36,6 +36,7 @@ enum class FilterType { MESHBORN, MESH_SURFACE, MU, + MUSURFACE, PARTICLE, POLAR, SPHERICAL_HARMONICS, diff --git a/include/openmc/tallies/filter_mu.h b/include/openmc/tallies/filter_mu.h index 942ee60c2..b0ee40eac 100644 --- a/include/openmc/tallies/filter_mu.h +++ b/include/openmc/tallies/filter_mu.h @@ -40,7 +40,7 @@ public: void set_bins(gsl::span bins); -private: +protected: //---------------------------------------------------------------------------- // Data members diff --git a/include/openmc/tallies/filter_musurface.h b/include/openmc/tallies/filter_musurface.h new file mode 100644 index 000000000..2ca19e3a2 --- /dev/null +++ b/include/openmc/tallies/filter_musurface.h @@ -0,0 +1,34 @@ +#ifndef OPENMC_TALLIES_FILTER_MU_SURFACE_H +#define OPENMC_TALLIES_FILTER_MU_SURFACE_H + +#include + +#include "openmc/tallies/filter_mu.h" +#include "openmc/vector.h" + +namespace openmc { + +//============================================================================== +//! Bins the incoming-outgoing direction cosine. This is only used for surface +//! crossings. +//============================================================================== + +class MuSurfaceFilter : public MuFilter { +public: + //---------------------------------------------------------------------------- + // Constructors, destructors + + ~MuSurfaceFilter() = default; + + //---------------------------------------------------------------------------- + // Methods + + std::string type_str() const override { return "musurface"; } + FilterType type() const override { return FilterType::MUSURFACE; } + + void get_all_bins(const Particle& p, TallyEstimator estimator, + FilterMatch& match) const override; +}; + +} // namespace openmc +#endif // OPENMC_TALLIES_FILTER_MU_SURFACE_H diff --git a/openmc/filter.py b/openmc/filter.py index 0f884cfcd..b5926af5a 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -22,7 +22,7 @@ from ._xml import get_text _FILTER_TYPES = ( 'universe', 'material', 'cell', 'cellborn', 'surface', 'mesh', 'energy', - 'energyout', 'mu', 'polar', 'azimuthal', 'distribcell', 'delayedgroup', + 'energyout', 'mu', 'musurface', 'polar', 'azimuthal', 'distribcell', 'delayedgroup', 'energyfunction', 'cellfrom', 'materialfrom', 'legendre', 'spatiallegendre', 'sphericalharmonics', 'zernike', 'zernikeradial', 'particle', 'cellinstance', 'collision', 'time' @@ -765,7 +765,7 @@ class ParticleFilter(Filter): @Filter.bins.setter def bins(self, bins): cv.check_type('bins', bins, Sequence, str) - bins = np.atleast_1d(bins) + bins = np.atleast_1d(bins) for edge in bins: cv.check_value('filter bin', edge, _PARTICLES) self._bins = bins @@ -1850,6 +1850,44 @@ class MuFilter(RealFilter): cv.check_less_than('filter value', x, 1., equality=True) +class MuSurfaceFilter(MuFilter): + """Bins tally events based on the angle of surface crossing. + + This filter bins events based on the cosine of the angle between the + direction of the particle and the normal to the surface at the point it + crosses. Only used in conjunction with a SurfaceFilter and current score. + + .. versionadded:: 0.15.1 + + Parameters + ---------- + values : int or Iterable of Real + A grid of surface crossing angles which the events will be divided into. + Values represent the cosine of the angle between the direction of the + particle and the normal to the surface at the point it crosses. If an + iterable is given, the values will be used explicitly as grid points. If + a single int is given, the range [-1, 1] will be divided equally into + that number of bins. + filter_id : int + Unique identifier for the filter + + Attributes + ---------- + values : numpy.ndarray + An array of values for which each successive pair constitutes a range of + surface crossing angle cosines for a single bin. + id : int + Unique identifier for the filter + bins : numpy.ndarray + An array of shape (N, 2) where each row is a pair of cosines of surface + crossing angle for a single filter + num_bins : Integral + The number of filter bins + + """ + # Note: inherits implementation from MuFilter + + class PolarFilter(RealFilter): """Bins tally events based on the incident particle's direction. diff --git a/openmc/lib/filter.py b/openmc/lib/filter.py index 340c2fa34..b30f5e662 100644 --- a/openmc/lib/filter.py +++ b/openmc/lib/filter.py @@ -21,9 +21,9 @@ __all__ = [ 'CellInstanceFilter', 'CollisionFilter', 'DistribcellFilter', 'DelayedGroupFilter', 'EnergyFilter', 'EnergyoutFilter', 'EnergyFunctionFilter', 'LegendreFilter', 'MaterialFilter', 'MaterialFromFilter', 'MeshFilter', 'MeshBornFilter', - 'MeshSurfaceFilter', 'MuFilter', 'ParticleFilter', - 'PolarFilter', 'SphericalHarmonicsFilter', 'SpatialLegendreFilter', 'SurfaceFilter', - 'UniverseFilter', 'ZernikeFilter', 'ZernikeRadialFilter', 'filters' + 'MeshSurfaceFilter', 'MuFilter', 'MuSurfaceFilter', 'ParticleFilter', 'PolarFilter', + 'SphericalHarmonicsFilter', 'SpatialLegendreFilter', 'SurfaceFilter', 'UniverseFilter', + 'ZernikeFilter', 'ZernikeRadialFilter', 'filters' ] # Tally functions @@ -540,6 +540,10 @@ class MuFilter(Filter): filter_type = 'mu' +class MuSurfaceFilter(Filter): + filter_type = 'musurface' + + class ParticleFilter(Filter): filter_type = 'particle' @@ -642,6 +646,7 @@ _FILTER_TYPE_MAP = { 'meshborn': MeshBornFilter, 'meshsurface': MeshSurfaceFilter, 'mu': MuFilter, + 'musurface': MuSurfaceFilter, 'particle': ParticleFilter, 'polar': PolarFilter, 'sphericalharmonics': SphericalHarmonicsFilter, diff --git a/src/tallies/filter.cpp b/src/tallies/filter.cpp index ff7a3416b..074212db4 100644 --- a/src/tallies/filter.cpp +++ b/src/tallies/filter.cpp @@ -26,6 +26,7 @@ #include "openmc/tallies/filter_meshborn.h" #include "openmc/tallies/filter_meshsurface.h" #include "openmc/tallies/filter_mu.h" +#include "openmc/tallies/filter_musurface.h" #include "openmc/tallies/filter_particle.h" #include "openmc/tallies/filter_polar.h" #include "openmc/tallies/filter_sph_harm.h" @@ -133,6 +134,8 @@ Filter* Filter::create(const std::string& type, int32_t id) return Filter::create(id); } else if (type == "mu") { return Filter::create(id); + } else if (type == "musurface") { + return Filter::create(id); } else if (type == "particle") { return Filter::create(id); } else if (type == "polar") { diff --git a/src/tallies/filter_musurface.cpp b/src/tallies/filter_musurface.cpp new file mode 100644 index 000000000..58fe39b91 --- /dev/null +++ b/src/tallies/filter_musurface.cpp @@ -0,0 +1,36 @@ +#include "openmc/tallies/filter_musurface.h" + +#include // for abs, copysign + +#include "openmc/search.h" +#include "openmc/surface.h" +#include "openmc/tallies/tally_scoring.h" + +namespace openmc { + +void MuSurfaceFilter::get_all_bins( + const Particle& p, TallyEstimator estimator, FilterMatch& match) const +{ + // Get surface normal (and make sure it is a unit vector) + const auto surf {model::surfaces[std::abs(p.surface()) - 1].get()}; + auto n = surf->normal(p.r()); + n /= n.norm(); + + // Determine whether normal should be pointing in or out + if (p.surface() < 0) + n *= -1; + + // Determine cosine of angle between normal and particle direction + double mu = p.u().dot(n); + if (std::abs(mu) > 1.0) + mu = std::copysign(1.0, mu); + + // Find matching bin + if (mu >= bins_.front() && mu <= bins_.back()) { + auto bin = lower_bound_index(bins_.begin(), bins_.end(), mu); + match.bins_.push_back(bin); + match.weights_.push_back(1.0); + } +} + +} // namespace openmc diff --git a/tests/regression_tests/filter_musurface/__init__.py b/tests/regression_tests/filter_musurface/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/filter_musurface/inputs_true.dat b/tests/regression_tests/filter_musurface/inputs_true.dat new file mode 100644 index 000000000..031f62159 --- /dev/null +++ b/tests/regression_tests/filter_musurface/inputs_true.dat @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + eigenvalue + 1000 + 5 + 0 + + + + 1 + + + -1.0 -0.5 0.0 0.5 1.0 + + + 1 2 + current + + + diff --git a/tests/regression_tests/filter_musurface/results_true.dat b/tests/regression_tests/filter_musurface/results_true.dat new file mode 100644 index 000000000..39c9f2b92 --- /dev/null +++ b/tests/regression_tests/filter_musurface/results_true.dat @@ -0,0 +1,11 @@ +k-combined: +1.157005E-01 7.587090E-03 +tally 1: +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +8.770000E-01 +1.608710E-01 +3.909000E+00 +3.063035E+00 diff --git a/tests/regression_tests/filter_musurface/test.py b/tests/regression_tests/filter_musurface/test.py new file mode 100644 index 000000000..f2ec96b49 --- /dev/null +++ b/tests/regression_tests/filter_musurface/test.py @@ -0,0 +1,43 @@ +import numpy as np +from math import pi + +import openmc +import pytest + +from tests.testing_harness import PyAPITestHarness + + +@pytest.fixture +def model(): + model = openmc.Model() + fuel = openmc.Material() + fuel.set_density('g/cm3', 10.0) + fuel.add_nuclide('U235', 1.0) + zr = openmc.Material() + zr.set_density('g/cm3', 1.0) + zr.add_nuclide('Zr90', 1.0) + + cyl1 = openmc.ZCylinder(r=1.0) + cyl2 = openmc.ZCylinder(r=3.0, boundary_type='vacuum') + cell1 = openmc.Cell(fill=fuel, region=-cyl1) + cell2 = openmc.Cell(fill=zr, region=+cyl1 & -cyl2) + model.geometry = openmc.Geometry([cell1, cell2]) + + model.settings.batches = 5 + model.settings.inactive = 0 + model.settings.particles = 1000 + + # Create a tally for current through the first surface binned by mu + surf_filter = openmc.SurfaceFilter([cyl1]) + 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'] + model.tallies.append(tally) + + return model + + +def test_filter_musurface(model): + harness = PyAPITestHarness('statepoint.5.h5', model) + harness.main() diff --git a/tests/unit_tests/test_filter_musurface.py b/tests/unit_tests/test_filter_musurface.py new file mode 100644 index 000000000..ca0db71f0 --- /dev/null +++ b/tests/unit_tests/test_filter_musurface.py @@ -0,0 +1,38 @@ +import openmc + + +def test_musurface(run_in_tmpdir): + sphere = openmc.Sphere(r=1.0, boundary_type='vacuum') + cell = openmc.Cell(region=-sphere) + model = openmc.Model() + model.geometry = openmc.Geometry([cell]) + model.settings.particles = 1000 + model.settings.batches = 10 + E = 1.0 + model.settings.source = openmc.IndependentSource( + space=openmc.stats.Point(), + angle=openmc.stats.Isotropic(), + energy=openmc.stats.delta_function(E), + ) + model.settings.run_mode = "fixed source" + + filter1 = openmc.MuSurfaceFilter(200) + filter2 = openmc.SurfaceFilter(sphere) + tally = openmc.Tally() + tally.filters = [filter1, filter2] + tally.scores = ['current'] + model.tallies = openmc.Tallies([tally]) + + # Run OpenMC + sp_filename = model.run() + + # Get current binned by mu + with openmc.StatePoint(sp_filename) as sp: + current_mu = sp.tallies[tally.id].mean.ravel() + + # All contributions should show up in last bin + assert current_mu[-1] == 1.0 + for element in current_mu[:-1]: + assert element == 0.0 + + From 9070b8b220153e2b4d8c8e5238ffec99cfd06add Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Fri, 4 Oct 2024 22:59:41 -0500 Subject: [PATCH 179/671] Prepare point query data structures on meshes when applying Weight Windows (#3157) Co-authored-by: Paul Romano --- include/openmc/mesh.h | 6 +-- openmc/weight_windows.py | 5 ++- src/mesh.cpp | 5 ++- src/tallies/filter_mesh.cpp | 2 +- src/weight_windows.cpp | 7 ++-- tests/unit_tests/weightwindows/test.py | 38 ++++++++++++++++++- .../weightwindows/test_mesh_tets.exo | 1 + vendor/fmt | 2 +- 8 files changed, 53 insertions(+), 13 deletions(-) create mode 120000 tests/unit_tests/weightwindows/test_mesh_tets.exo diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index 6f3f2b0a4..f0ad57247 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -83,8 +83,8 @@ public: virtual ~Mesh() = default; // Methods - //! Perform any preparation needed to support use in mesh filters - virtual void prepare_for_tallies() {}; + //! Perform any preparation needed to support point location within the mesh + virtual void prepare_for_point_location() {}; //! Update a position to the local coordinates of the mesh virtual void local_coords(Position& r) const {}; @@ -737,7 +737,7 @@ public: // Overridden Methods //! Perform any preparation needed to support use in mesh filters - void prepare_for_tallies() override; + void prepare_for_point_location() override; Position sample_element(int32_t bin, uint64_t* seed) const override; diff --git a/openmc/weight_windows.py b/openmc/weight_windows.py index a10fd2a65..5df9f71dc 100644 --- a/openmc/weight_windows.py +++ b/openmc/weight_windows.py @@ -328,8 +328,9 @@ class WeightWindows(IDManagerMixin): subelement = ET.SubElement(element, 'particle_type') subelement.text = self.particle_type - subelement = ET.SubElement(element, 'energy_bounds') - subelement.text = ' '.join(str(e) for e in self.energy_bounds) + if self.energy_bounds is not None: + subelement = ET.SubElement(element, 'energy_bounds') + subelement.text = ' '.join(str(e) for e in self.energy_bounds) subelement = ET.SubElement(element, 'lower_ww_bounds') subelement.text = ' '.join(str(b) for b in self.lower_ww_bounds.ravel('F')) diff --git a/src/mesh.cpp b/src/mesh.cpp index 0f8b4b14e..b90fead03 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -2315,7 +2315,7 @@ void MOABMesh::initialize() this->determine_bounds(); } -void MOABMesh::prepare_for_tallies() +void MOABMesh::prepare_for_point_location() { // if the KDTree has already been constructed, do nothing if (kdtree_) @@ -2365,7 +2365,8 @@ void MOABMesh::build_kdtree(const moab::Range& all_tets) all_tets_and_tris.merge(all_tris); // create a kd-tree instance - write_message("Building adaptive k-d tree for tet mesh...", 7); + write_message( + 7, "Building adaptive k-d tree for tet mesh with ID {}...", id_); kdtree_ = make_unique(mbi_.get()); // Determine what options to use diff --git a/src/tallies/filter_mesh.cpp b/src/tallies/filter_mesh.cpp index 5b01da1f6..03f7da978 100644 --- a/src/tallies/filter_mesh.cpp +++ b/src/tallies/filter_mesh.cpp @@ -80,7 +80,7 @@ void MeshFilter::set_mesh(int32_t mesh) // perform any additional perparation for mesh tallies here mesh_ = mesh; n_bins_ = model::meshes[mesh_]->n_bins(); - model::meshes[mesh_]->prepare_for_tallies(); + model::meshes[mesh_]->prepare_for_point_location(); } void MeshFilter::set_translation(const Position& translation) diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index 2eb930965..68f7550ae 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -147,8 +147,8 @@ WeightWindows::WeightWindows(int32_t id) WeightWindows::WeightWindows(pugi::xml_node node) { // Make sure required elements are present - const vector required_elems {"id", "particle_type", - "energy_bounds", "lower_ww_bounds", "upper_ww_bounds"}; + const vector required_elems { + "id", "particle_type", "lower_ww_bounds", "upper_ww_bounds"}; for (const auto& elem : required_elems) { if (!check_for_node(node, elem.c_str())) { fatal_error(fmt::format("Must specify <{}> for weight windows.", elem)); @@ -165,7 +165,7 @@ WeightWindows::WeightWindows(pugi::xml_node node) // Determine associated mesh int32_t mesh_id = std::stoi(get_node_value(node, "mesh")); - mesh_idx_ = model::mesh_map.at(mesh_id); + set_mesh(model::mesh_map.at(mesh_id)); // energy bounds if (check_for_node(node, "energy_bounds")) @@ -340,6 +340,7 @@ void WeightWindows::set_mesh(int32_t mesh_idx) fatal_error(fmt::format("Could not find a mesh for index {}", mesh_idx)); mesh_idx_ = mesh_idx; + model::meshes[mesh_idx_]->prepare_for_point_location(); allocate_ww_bounds(); } diff --git a/tests/unit_tests/weightwindows/test.py b/tests/unit_tests/weightwindows/test.py index d3da0cd11..79aadbef0 100644 --- a/tests/unit_tests/weightwindows/test.py +++ b/tests/unit_tests/weightwindows/test.py @@ -296,4 +296,40 @@ def test_ww_attrs_capi(run_in_tmpdir, model): assert wws.id == 2 assert wws.particle == openmc.ParticleType.PHOTON - openmc.lib.finalize() \ No newline at end of file + openmc.lib.finalize() + + +@pytest.mark.parametrize('library', ('libmesh', 'moab')) +def test_unstructured_mesh_applied_wws(request, run_in_tmpdir, library): + """ + Ensure that weight windows on unstructured mesh work when + they aren't part of a tally or weight window generator + """ + + if library == 'libmesh' and not openmc.lib._libmesh_enabled(): + pytest.skip('LibMesh not enabled in this build.') + if library == 'moab' and not openmc.lib._dagmc_enabled(): + pytest.skip('DAGMC (and MOAB) mesh not enabled in this build.') + + water = openmc.Material(name='water') + water.add_nuclide('H1', 2.0) + water.add_nuclide('O16', 1.0) + water.set_density('g/cc', 1.0) + box = openmc.model.RectangularParallelepiped(*(3*[-10, 10]), boundary_type='vacuum') + cell = openmc.Cell(region=-box, fill=water) + + geometry = openmc.Geometry([cell]) + mesh_file = str(request.fspath.dirpath() / 'test_mesh_tets.exo') + mesh = openmc.UnstructuredMesh(mesh_file, library) + + dummy_wws = np.ones((12_000,)) + + wws = openmc.WeightWindows(mesh, dummy_wws, upper_bound_ratio=5.0) + + model = openmc.Model(geometry) + model.settings.weight_windows = wws + model.settings.weight_windows_on = True + model.settings.run_mode = 'fixed source' + model.settings.particles = 100 + model.settings.batches = 2 + model.run() diff --git a/tests/unit_tests/weightwindows/test_mesh_tets.exo b/tests/unit_tests/weightwindows/test_mesh_tets.exo new file mode 120000 index 000000000..5bf23b369 --- /dev/null +++ b/tests/unit_tests/weightwindows/test_mesh_tets.exo @@ -0,0 +1 @@ +../../regression_tests/unstructured_mesh/test_mesh_tets.e \ No newline at end of file diff --git a/vendor/fmt b/vendor/fmt index d141cdbeb..65ac626c5 160000 --- a/vendor/fmt +++ b/vendor/fmt @@ -1 +1 @@ -Subproject commit d141cdbeb0fb422a3fb7173b285fd38e0d1772dc +Subproject commit 65ac626c5856f5aad1f1542e79407a6714357043 From 2450eef4246cae85c7ee8b895458729b7e6dc97c Mon Sep 17 00:00:00 2001 From: Zoe Prieto <101403129+zoeprieto@users.noreply.github.com> Date: Sat, 5 Oct 2024 03:51:56 -0300 Subject: [PATCH 180/671] Introduce ParticleList class for manipulating a list of source particles (#3148) Co-authored-by: Paul Romano --- docs/source/pythonapi/base.rst | 1 + openmc/lib/core.py | 9 +- openmc/source.py | 248 +++++++++++++++--- .../surface_source_write/test.py | 45 ++-- tests/unit_tests/test_source_file.py | 30 ++- 5 files changed, 259 insertions(+), 74 deletions(-) diff --git a/docs/source/pythonapi/base.rst b/docs/source/pythonapi/base.rst index 611d2c216..23df02f2e 100644 --- a/docs/source/pythonapi/base.rst +++ b/docs/source/pythonapi/base.rst @@ -191,6 +191,7 @@ Post-processing :template: myclass.rst openmc.Particle + openmc.ParticleList openmc.ParticleTrack openmc.StatePoint openmc.Summary diff --git a/openmc/lib/core.py b/openmc/lib/core.py index e646a9ae1..8561602e6 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -477,7 +477,7 @@ def run(output=True): def sample_external_source( n_samples: int = 1000, prn_seed: int | None = None -) -> list[openmc.SourceParticle]: +) -> openmc.ParticleList: """Sample external source and return source particles. .. versionadded:: 0.13.1 @@ -492,7 +492,7 @@ def sample_external_source( Returns ------- - list of openmc.SourceParticle + openmc.ParticleList List of sampled source particles """ @@ -506,14 +506,13 @@ def sample_external_source( _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.SourceParticle( + 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) ) for site in sites_array - ] + ]) def simulation_init(): diff --git a/openmc/source.py b/openmc/source.py index e35e62f5f..7c8e6af8d 100644 --- a/openmc/source.py +++ b/openmc/source.py @@ -5,10 +5,12 @@ from enum import IntEnum from numbers import Real import warnings from typing import Any +from pathlib import Path import lxml.etree as ET import numpy as np import h5py +import pandas as pd import openmc import openmc.checkvalue as cv @@ -917,6 +919,34 @@ class ParticleType(IntEnum): except KeyError: raise ValueError(f"Invalid string for creation of {cls.__name__}: {value}") + @classmethod + def from_pdg_number(cls, pdg_number: int) -> ParticleType: + """Constructs a ParticleType instance from a PDG number. + + The Particle Data Group at LBNL publishes a Monte Carlo particle + numbering scheme as part of the `Review of Particle Physics + <10.1103/PhysRevD.110.030001>`_. This method maps PDG numbers to the + corresponding :class:`ParticleType`. + + Parameters + ---------- + pdg_number : int + The PDG number of the particle type. + + Returns + ------- + The corresponding ParticleType instance. + """ + try: + return { + 2112: ParticleType.NEUTRON, + 22: ParticleType.PHOTON, + 11: ParticleType.ELECTRON, + -11: ParticleType.POSITRON, + }[pdg_number] + except KeyError: + raise ValueError(f"Unrecognized PDG number: {pdg_number}") + def __repr__(self) -> str: """ Returns a string representation of the ParticleType instance. @@ -930,11 +960,6 @@ class ParticleType(IntEnum): def __str__(self) -> str: return self.__repr__() - # needed for <= 3.7, IntEnum will use the mixed-in type's `__format__` method otherwise - # this forces it to default to the standard object format, relying on __str__ under the hood - def __format__(self, spec): - return object.__format__(self, spec) - class SourceParticle: """Source particle @@ -1020,31 +1045,179 @@ def write_source_file( openmc.SourceParticle """ - # Create compound datatype for source particles - pos_dtype = np.dtype([('x', ' list[SourceParticle]: +class ParticleList(list): + """A collection of SourceParticle objects. + + Parameters + ---------- + particles : list of SourceParticle + Particles to collect into the list + + """ + @classmethod + def from_hdf5(cls, filename: PathLike) -> ParticleList: + """Create particle list from an HDF5 file. + + Parameters + ---------- + filename : path-like + Path to source file to read. + + Returns + ------- + ParticleList instance + + """ + with h5py.File(filename, 'r') as fh: + filetype = fh.attrs['filetype'] + arr = fh['source_bank'][...] + + if filetype != b'source': + raise ValueError(f'File {filename} is not a source file') + + source_particles = [ + SourceParticle(*params, ParticleType(particle)) + for *params, particle in arr + ] + return cls(source_particles) + + @classmethod + def from_mcpl(cls, filename: PathLike) -> ParticleList: + """Create particle list from an MCPL file. + + Parameters + ---------- + filename : path-like + Path to MCPL file to read. + + Returns + ------- + ParticleList instance + + """ + import mcpl + # Process .mcpl file + particles = [] + with mcpl.MCPLFile(filename) as f: + for particle in f.particles: + # Determine particle type based on the PDG number + try: + particle_type = ParticleType.from_pdg_number(particle.pdgcode) + except ValueError: + particle_type = "UNKNOWN" + + # Create a source particle instance. Note that MCPL stores + # energy in MeV and time in ms. + source_particle = SourceParticle( + r=tuple(particle.position), + u=tuple(particle.direction), + E=1.0e6*particle.ekin, + time=1.0e-3*particle.time, + wgt=particle.weight, + particle=particle_type + ) + particles.append(source_particle) + + return cls(particles) + + def __getitem__(self, index): + """ + Return a new ParticleList object containing the particle(s) + at the specified index or slice. + + Parameters + ---------- + index : int, slice or list + The index, slice or list to select from the list of particles + + Returns + ------- + openmc.ParticleList or openmc.SourceParticle + A new object with the selected particle(s) + """ + if isinstance(index, int): + # If it's a single integer, return the corresponding particle + return super().__getitem__(index) + elif isinstance(index, slice): + # If it's a slice, return a new ParticleList object with the + # sliced particles + return ParticleList(super().__getitem__(index)) + elif isinstance(index, list): + # If it's a list of integers, return a new ParticleList object with + # the selected particles. Note that Python 3.10 gets confused if you + # use super() here, so we call list.__getitem__ directly. + return ParticleList([list.__getitem__(self, i) for i in index]) + else: + raise TypeError(f"Invalid index type: {type(index)}. Must be int, " + "slice, or list of int.") + + def to_dataframe(self) -> pd.DataFrame: + """A dataframe representing the source particles + + Returns + ------- + pandas.DataFrame + DataFrame containing the source particles attributes. + """ + # Extract the attributes of the source particles into a list of tuples + data = [(sp.r[0], sp.r[1], sp.r[2], sp.u[0], sp.u[1], sp.u[2], + sp.E, sp.time, sp.wgt, sp.delayed_group, sp.surf_id, + sp.particle.name.lower()) for sp in self] + + # Define the column names for the DataFrame + columns = ['x', 'y', 'z', 'u_x', 'u_y', 'u_z', 'E', 'time', 'wgt', + 'delayed_group', 'surf_id', 'particle'] + + # Create the pandas DataFrame from the data + return pd.DataFrame(data, columns=columns) + + def export_to_hdf5(self, filename: PathLike, **kwargs): + """Export particle list to an HDF5 file. + + This method write out an .h5 file that can be used as a source file in + conjunction with the :class:`openmc.FileSource` class. + + Parameters + ---------- + filename : path-like + Path to source file to write + **kwargs + Keyword arguments to pass to :class:`h5py.File` + + See Also + -------- + openmc.FileSource + + """ + # Create compound datatype for source particles + pos_dtype = np.dtype([('x', ' ParticleList: """Read a source file and return a list of source particles. .. versionadded:: 0.15.0 @@ -1056,23 +1229,18 @@ def read_source_file(filename: PathLike) -> list[SourceParticle]: Returns ------- - list of SourceParticle - Source particles read from file + openmc.ParticleList See Also -------- openmc.SourceParticle """ - with h5py.File(filename, 'r') as fh: - filetype = fh.attrs['filetype'] - arr = fh['source_bank'][...] + filename = Path(filename) + if filename.suffix not in ('.h5', '.mcpl'): + raise ValueError('Source file must have a .h5 or .mcpl extension.') - if filetype != b'source': - raise ValueError(f'File {filename} is not a source file') - - source_particles = [] - for *params, particle in arr: - source_particles.append(SourceParticle(*params, ParticleType(particle))) - - return source_particles + if filename.suffix == '.h5': + return ParticleList.from_hdf5(filename) + else: + return ParticleList.from_mcpl(filename) diff --git a/tests/regression_tests/surface_source_write/test.py b/tests/regression_tests/surface_source_write/test.py index fe11d68a6..f144eb82a 100644 --- a/tests/regression_tests/surface_source_write/test.py +++ b/tests/regression_tests/surface_source_write/test.py @@ -608,11 +608,6 @@ def return_surface_source_data(filepath): """Read a surface source file and return a sorted array composed of flatten arrays of source data for each surface source point. - TODO: - - - use read_source_file from source.py instead. Or a dedicated function - to produce sorted list of source points for a given file. - Parameters ---------- filepath : str @@ -629,27 +624,25 @@ def return_surface_source_data(filepath): keys = [] # Read source file - with h5py.File(filepath, "r") as f: - for point in f["source_bank"]: - r = point["r"] - u = point["u"] - e = point["E"] - time = point["time"] - wgt = point["wgt"] - delayed_group = point["delayed_group"] - surf_id = point["surf_id"] - particle = point["particle"] + source = openmc.read_source_file(filepath) - key = ( - f"{r[0]:.10e} {r[1]:.10e} {r[2]:.10e} {u[0]:.10e} {u[1]:.10e} {u[2]:.10e}" - f"{e:.10e} {time:.10e} {wgt:.10e} {delayed_group} {surf_id} {particle}" - ) - - keys.append(key) - - values = [*r, *u, e, time, wgt, delayed_group, surf_id, particle] - assert len(values) == 12 - data.append(values) + for point in source: + r = point.r + u = point.u + e = point.E + time = point.time + wgt = point.wgt + delayed_group = point.delayed_group + surf_id = point.surf_id + particle = point.particle + key = ( + f"{r[0]:.10e} {r[1]:.10e} {r[2]:.10e} {u[0]:.10e} {u[1]:.10e} {u[2]:.10e}" + f"{e:.10e} {time:.10e} {wgt:.10e} {delayed_group} {surf_id} {particle}" + ) + keys.append(key) + values = [*r, *u, e, time, wgt, delayed_group, surf_id, particle] + assert len(values) == 12 + data.append(values) data = np.array(data) keys = np.array(keys) @@ -1129,4 +1122,4 @@ def test_surface_source_cell_dagmc( harness = SurfaceSourceWriteTestHarness( "statepoint.5.h5", model=model, workdir=folder ) - harness.main() \ No newline at end of file + harness.main() diff --git a/tests/unit_tests/test_source_file.py b/tests/unit_tests/test_source_file.py index 1b5549b00..41906c80f 100644 --- a/tests/unit_tests/test_source_file.py +++ b/tests/unit_tests/test_source_file.py @@ -44,11 +44,9 @@ def test_source_file(run_in_tmpdir): assert np.all(arr['delayed_group'] == 0) assert np.all(arr['particle'] == 0) - # Ensure sites read in are consistent - sites = openmc.read_source_file('test_source.h5') + sites = openmc.ParticleList.from_hdf5('test_source.h5') - assert filetype == b'source' xs = np.array([site.r[0] for site in sites]) ys = np.array([site.r[1] for site in sites]) zs = np.array([site.r[2] for site in sites]) @@ -68,6 +66,32 @@ def test_source_file(run_in_tmpdir): p_types = np.array([s.particle for s in sites]) assert np.all(p_types == 0) + # Ensure a ParticleList item is a SourceParticle + site = sites[0] + assert isinstance(site, openmc.SourceParticle) + assert site.E == pytest.approx(n) + + # Ensure site slice read in and exported are consistent + sites_slice = sites[:10] + sites_slice.export_to_hdf5("test_source_slice.h5") + sites_slice = openmc.ParticleList.from_hdf5('test_source_slice.h5') + + assert isinstance(sites_slice, openmc.ParticleList) + assert len(sites_slice) == 10 + E = np.array([s.E for s in sites_slice]) + np.testing.assert_allclose(E, n - np.arange(10)) + + # Ensure site list read in and exported are consistent + df = sites.to_dataframe() + sites_filtered = sites[df[df.E <= 10.0].index.tolist()] + sites_filtered.export_to_hdf5("test_source_filtered.h5") + sites_filtered = openmc.read_source_file('test_source_filtered.h5') + + assert isinstance(sites_filtered, openmc.ParticleList) + assert len(sites_filtered) == 10 + E = np.array([s.E for s in sites_filtered]) + np.testing.assert_allclose(E, np.arange(10, 0, -1)) + def test_wrong_source_attributes(run_in_tmpdir): # Create a source file with animal attributes From 34f04267a5a6bfe17479c11bce98c1adbbfd4a29 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Sat, 5 Oct 2024 12:28:22 -0500 Subject: [PATCH 181/671] Update fmt submodule to version 11.0.2 (#3162) --- include/openmc/output.h | 2 +- include/openmc/position.h | 2 +- vendor/fmt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/include/openmc/output.h b/include/openmc/output.h index 1ece3de96..549d2ae32 100644 --- a/include/openmc/output.h +++ b/include/openmc/output.h @@ -76,7 +76,7 @@ struct formatter> { } template - auto format(const std::array& arr, FormatContext& ctx) + auto format(const std::array& arr, FormatContext& ctx) const { return format_to(ctx.out(), "({}, {})", arr[0], arr[1]); } diff --git a/include/openmc/position.h b/include/openmc/position.h index 11ea37647..e6939dc0e 100644 --- a/include/openmc/position.h +++ b/include/openmc/position.h @@ -221,7 +221,7 @@ namespace fmt { template<> struct formatter : formatter { template - auto format(const openmc::Position& pos, FormatContext& ctx) + auto format(const openmc::Position& pos, FormatContext& ctx) const { return formatter::format( fmt::format("({}, {}, {})", pos.x, pos.y, pos.z), ctx); diff --git a/vendor/fmt b/vendor/fmt index 65ac626c5..0c9fce2ff 160000 --- a/vendor/fmt +++ b/vendor/fmt @@ -1 +1 @@ -Subproject commit 65ac626c5856f5aad1f1542e79407a6714357043 +Subproject commit 0c9fce2ffefecfdce794e1859584e25877b7b592 From c0acc28038cc13c7d88b3eae17c86615c590f30f Mon Sep 17 00:00:00 2001 From: Matteo Zammataro <103496190+MatteoZammataro@users.noreply.github.com> Date: Tue, 8 Oct 2024 20:29:55 +0200 Subject: [PATCH 182/671] Add dose coefficients from ICRP 74 (#3020) Co-authored-by: matteo.zammataro Co-authored-by: Paul Romano --- openmc/data/effective_dose/dose.py | 98 ++++++++++++------- .../{ => icrp116}/electrons.txt | 0 .../{ => icrp116}/helium_ions.txt | 0 .../{ => icrp116}/negative_muons.txt | 0 .../{ => icrp116}/negative_pions.txt | 0 .../effective_dose/{ => icrp116}/neutrons.txt | 0 .../effective_dose/{ => icrp116}/photons.txt | 0 .../{ => icrp116}/photons_kerma.txt | 0 .../{ => icrp116}/positive_muons.txt | 0 .../{ => icrp116}/positive_pions.txt | 0 .../{ => icrp116}/positrons.txt | 0 .../effective_dose/{ => icrp116}/protons.txt | 0 .../icrp74/generate_photon_effective_dose.py | 69 +++++++++++++ .../data/effective_dose/icrp74/neutrons.txt | 50 ++++++++++ openmc/data/effective_dose/icrp74/photons.txt | 26 +++++ tests/unit_tests/test_data_dose.py | 14 +++ 16 files changed, 222 insertions(+), 35 deletions(-) rename openmc/data/effective_dose/{ => icrp116}/electrons.txt (100%) rename openmc/data/effective_dose/{ => icrp116}/helium_ions.txt (100%) rename openmc/data/effective_dose/{ => icrp116}/negative_muons.txt (100%) rename openmc/data/effective_dose/{ => icrp116}/negative_pions.txt (100%) rename openmc/data/effective_dose/{ => icrp116}/neutrons.txt (100%) rename openmc/data/effective_dose/{ => icrp116}/photons.txt (100%) rename openmc/data/effective_dose/{ => icrp116}/photons_kerma.txt (100%) rename openmc/data/effective_dose/{ => icrp116}/positive_muons.txt (100%) rename openmc/data/effective_dose/{ => icrp116}/positive_pions.txt (100%) rename openmc/data/effective_dose/{ => icrp116}/positrons.txt (100%) rename openmc/data/effective_dose/{ => icrp116}/protons.txt (100%) create mode 100644 openmc/data/effective_dose/icrp74/generate_photon_effective_dose.py create mode 100644 openmc/data/effective_dose/icrp74/neutrons.txt create mode 100644 openmc/data/effective_dose/icrp74/photons.txt diff --git a/openmc/data/effective_dose/dose.py b/openmc/data/effective_dose/dose.py index ae981ee7d..c7f458d1c 100644 --- a/openmc/data/effective_dose/dose.py +++ b/openmc/data/effective_dose/dose.py @@ -2,40 +2,61 @@ from pathlib import Path import numpy as np -_FILES = ( - ('electron', 'electrons.txt'), - ('helium', 'helium_ions.txt'), - ('mu-', 'negative_muons.txt'), - ('pi-', 'negative_pions.txt'), - ('neutron', 'neutrons.txt'), - ('photon', 'photons.txt'), - ('photon kerma', 'photons_kerma.txt'), - ('mu+', 'positive_muons.txt'), - ('pi+', 'positive_pions.txt'), - ('positron', 'positrons.txt'), - ('proton', 'protons.txt') -) +import openmc.checkvalue as cv -_DOSE_ICRP116 = {} +_FILES = { + ('icrp74', 'neutron'): Path('icrp74') / 'neutrons.txt', + ('icrp74', 'photon'): Path('icrp74') / 'photons.txt', + ('icrp116', 'electron'): Path('icrp116') / 'electrons.txt', + ('icrp116', 'helium'): Path('icrp116') / 'helium_ions.txt', + ('icrp116', 'mu-'): Path('icrp116') / 'negative_muons.txt', + ('icrp116', 'pi-'): Path('icrp116') / 'negative_pions.txt', + ('icrp116', 'neutron'): Path('icrp116') / 'neutrons.txt', + ('icrp116', 'photon'): Path('icrp116') / 'photons.txt', + ('icrp116', 'photon kerma'): Path('icrp116') / 'photons_kerma.txt', + ('icrp116', 'mu+'): Path('icrp116') / 'positive_muons.txt', + ('icrp116', 'pi+'): Path('icrp116') / 'positive_pions.txt', + ('icrp116', 'positron'): Path('icrp116') / 'positrons.txt', + ('icrp116', 'proton'): Path('icrp116') / 'protons.txt', +} + +_DOSE_TABLES = {} -def _load_dose_icrp116(): - """Load effective dose tables from text files""" - for particle, filename in _FILES: - path = Path(__file__).parent / filename - data = np.loadtxt(path, skiprows=3, encoding='utf-8') - data[:, 0] *= 1e6 # Change energies to eV - _DOSE_ICRP116[particle] = data +def _load_dose_icrp(data_source: str, particle: str): + """Load effective dose tables from text files. + + Parameters + ---------- + data_source : {'icrp74', 'icrp116'} + The dose conversion data source to use + particle : {'neutron', 'photon', 'photon kerma', 'electron', 'positron'} + Incident particle + + """ + path = Path(__file__).parent / _FILES[data_source, particle] + data = np.loadtxt(path, skiprows=3, encoding='utf-8') + data[:, 0] *= 1e6 # Change energies to eV + _DOSE_TABLES[data_source, particle] = data -def dose_coefficients(particle, geometry='AP'): - """Return effective dose conversion coefficients from ICRP-116 +def dose_coefficients(particle, geometry='AP', data_source='icrp116'): + """Return effective dose conversion coefficients. - This function provides fluence (and air kerma) to effective dose conversion - coefficients for various types of external exposures based on values in - `ICRP Publication 116 `_. - Corrected values found in a correigendum are used rather than the values in - theoriginal report. + This function provides fluence (and air kerma) to effective or ambient dose + (H*(10)) conversion coefficients for various types of external exposures + based on values in ICRP publications. Corrected values found in a + corrigendum are used rather than the values in the original report. + Available libraries include `ICRP Publication 74 + ` and `ICRP Publication 116 + `. + + For ICRP 74 data, the photon effective dose per fluence is determined by + multiplying the air kerma per fluence values (Table A.1) by the effective + dose per air kerma (Table A.17). The neutron effective dose per fluence is + found in Table A.41. For ICRP 116 data, the photon effective dose per + fluence is found in Table A.1 and the neutron effective dose per fluence is + found in Table A.5. Parameters ---------- @@ -44,6 +65,8 @@ def dose_coefficients(particle, geometry='AP'): geometry : {'AP', 'PA', 'LLAT', 'RLAT', 'ROT', 'ISO'} Irradiation geometry assumed. Refer to ICRP-116 (Section 3.2) for the meaning of the options here. + data_source : {'icrp74', 'icrp116'} + The data source for the effective dose conversion coefficients. Returns ------- @@ -54,19 +77,24 @@ def dose_coefficients(particle, geometry='AP'): 'photon kerma', the coefficients are given in [Sv/Gy]. """ - if not _DOSE_ICRP116: - _load_dose_icrp116() + + cv.check_value('geometry', geometry, {'AP', 'PA', 'LLAT', 'RLAT', 'ROT', 'ISO'}) + cv.check_value('data_source', data_source, {'icrp74', 'icrp116'}) + + if (data_source, particle) not in _FILES: + raise ValueError(f"{particle} has no dose data in data source {data_source}.") + elif (data_source, particle) not in _DOSE_TABLES: + _load_dose_icrp(data_source, particle) # Get all data for selected particle - data = _DOSE_ICRP116.get(particle) - if data is None: - raise ValueError(f"{particle} has no effective dose data") + data = _DOSE_TABLES[data_source, particle] # Determine index for selected geometry if particle in ('neutron', 'photon', 'proton', 'photon kerma'): - index = ('AP', 'PA', 'LLAT', 'RLAT', 'ROT', 'ISO').index(geometry) + columns = ('AP', 'PA', 'LLAT', 'RLAT', 'ROT', 'ISO') else: - index = ('AP', 'PA', 'ISO').index(geometry) + columns = ('AP', 'PA', 'ISO') + index = columns.index(geometry) # Pull out energy and dose from table energy = data[:, 0].copy() diff --git a/openmc/data/effective_dose/electrons.txt b/openmc/data/effective_dose/icrp116/electrons.txt similarity index 100% rename from openmc/data/effective_dose/electrons.txt rename to openmc/data/effective_dose/icrp116/electrons.txt diff --git a/openmc/data/effective_dose/helium_ions.txt b/openmc/data/effective_dose/icrp116/helium_ions.txt similarity index 100% rename from openmc/data/effective_dose/helium_ions.txt rename to openmc/data/effective_dose/icrp116/helium_ions.txt diff --git a/openmc/data/effective_dose/negative_muons.txt b/openmc/data/effective_dose/icrp116/negative_muons.txt similarity index 100% rename from openmc/data/effective_dose/negative_muons.txt rename to openmc/data/effective_dose/icrp116/negative_muons.txt diff --git a/openmc/data/effective_dose/negative_pions.txt b/openmc/data/effective_dose/icrp116/negative_pions.txt similarity index 100% rename from openmc/data/effective_dose/negative_pions.txt rename to openmc/data/effective_dose/icrp116/negative_pions.txt diff --git a/openmc/data/effective_dose/neutrons.txt b/openmc/data/effective_dose/icrp116/neutrons.txt similarity index 100% rename from openmc/data/effective_dose/neutrons.txt rename to openmc/data/effective_dose/icrp116/neutrons.txt diff --git a/openmc/data/effective_dose/photons.txt b/openmc/data/effective_dose/icrp116/photons.txt similarity index 100% rename from openmc/data/effective_dose/photons.txt rename to openmc/data/effective_dose/icrp116/photons.txt diff --git a/openmc/data/effective_dose/photons_kerma.txt b/openmc/data/effective_dose/icrp116/photons_kerma.txt similarity index 100% rename from openmc/data/effective_dose/photons_kerma.txt rename to openmc/data/effective_dose/icrp116/photons_kerma.txt diff --git a/openmc/data/effective_dose/positive_muons.txt b/openmc/data/effective_dose/icrp116/positive_muons.txt similarity index 100% rename from openmc/data/effective_dose/positive_muons.txt rename to openmc/data/effective_dose/icrp116/positive_muons.txt diff --git a/openmc/data/effective_dose/positive_pions.txt b/openmc/data/effective_dose/icrp116/positive_pions.txt similarity index 100% rename from openmc/data/effective_dose/positive_pions.txt rename to openmc/data/effective_dose/icrp116/positive_pions.txt diff --git a/openmc/data/effective_dose/positrons.txt b/openmc/data/effective_dose/icrp116/positrons.txt similarity index 100% rename from openmc/data/effective_dose/positrons.txt rename to openmc/data/effective_dose/icrp116/positrons.txt diff --git a/openmc/data/effective_dose/protons.txt b/openmc/data/effective_dose/icrp116/protons.txt similarity index 100% rename from openmc/data/effective_dose/protons.txt rename to openmc/data/effective_dose/icrp116/protons.txt diff --git a/openmc/data/effective_dose/icrp74/generate_photon_effective_dose.py b/openmc/data/effective_dose/icrp74/generate_photon_effective_dose.py new file mode 100644 index 000000000..f8e970137 --- /dev/null +++ b/openmc/data/effective_dose/icrp74/generate_photon_effective_dose.py @@ -0,0 +1,69 @@ +from prettytable import PrettyTable +import numpy as np + +# Data from Table A.1 (air kerma per fluence) +energy_a1 = np.array([ + 0.01, 0.015, 0.02, 0.03, 0.04, 0.05, 0.06, 0.08, 0.1, 0.15, 0.2, + 0.3, 0.4, 0.5, 0.6, 0.8, 1.0, 1.5, 2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 10.0 +]) +air_kerma = np.array([7.43, 3.12, 1.68, 0.721, 0.429, 0.323, 0.289, 0.307, 0.371, 0.599, 0.856, 1.38, + 1.89, 2.38, 2.84, 3.69, 4.47, 6.14, 7.55, 9.96, 12.1, 14.1, 16.1, 20.1, 24.0]) + +# Data from Table A.17 (effective dose per air kerma) +energy_a17 = np.array([ + 0.01, 0.015, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08, 0.1, 0.15, 0.2, 0.3, + 0.4, 0.5, 0.6, 0.8, 1.0, 2.0, 4.0, 6.0, 8.0, 10.0 +]) +dose_per_airkerma = { + 'AP': np.array([ + 0.00653, 0.0402, 0.122, 0.416, 0.788, 1.106, 1.308, 1.407, 1.433, 1.394, + 1.256, 1.173, 1.093, 1.056, 1.036, 1.024, 1.010, 1.003, 0.992, 0.993, + 0.993, 0.991, 0.990 + ]), + 'PA': np.array([ + 0.00248, 0.00586, 0.0181, 0.128, 0.370, 0.640, 0.846, 0.966, 1.019, + 1.030, 0.959, 0.915, 0.880, 0.871, 0.869, 0.870, 0.875, 0.880, 0.901, + 0.918, 0.924, 0.927, 0.929 + ]), + 'RLAT': np.array([ + 0.00172, 0.00549, 0.0151, 0.0782, 0.205, 0.345, 0.455, 0.522, 0.554, + 0.571, 0.551, 0.549, 0.557, 0.570, 0.585, 0.600, 0.628, 0.651, 0.728, + 0.796, 0.827, 0.846, 0.860 + ]), + 'LLAT': np.array([ + 0.00172, 0.00549, 0.0155, 0.0904, 0.241, 0.405, 0.528, 0.598, 0.628, + 0.641, 0.620, 0.615, 0.615, 0.623, 0.635, 0.648, 0.670, 0.691, 0.757, + 0.813, 0.836, 0.850, 0.859 + ]), + 'ROT': np.array([ + 0.00326, 0.0153, 0.0462, 0.191, 0.426, 0.661, 0.828, 0.924, 0.961, + 0.960, 0.892, 0.854, 0.824, 0.814, 0.812, 0.814, 0.821, 0.831, 0.871, + 0.909, 0.925, 0.934, 0.941 + ]), + 'ISO': np.array([ + 0.00271, 0.0123, 0.0362, 0.143, 0.326, 0.511, 0.642, 0.720, 0.749, + 0.748, 0.700, 0.679, 0.664, 0.667, 0.675, 0.684, 0.703, 0.719, 0.774, + 0.824, 0.846, 0.859, 0.868 + ]) +} + +# Interpolate air kerma onto energy grid for Table A.17 +air_kerma = np.interp(energy_a17, energy_a1, air_kerma) + +# Compute effective dose per fluence +dose_per_fluence = { + geometry: air_kerma * dose_per_airkerma + for geometry, dose_per_airkerma in dose_per_airkerma.items() +} + +# Create table +table = PrettyTable() +table.field_names = ['Energy (MeV)', 'AP', 'PA', 'LLAT', 'RLAT', 'ROT', 'ISO'] +table.float_format = '.7' +for i, energy in enumerate(energy_a17): + row = [energy] + for geometry in table.field_names[1:]: + row.append(dose_per_fluence[geometry][i]) + table.add_row(row) +print('Photons: Effective dose per fluence, in units of pSv cm², for monoenergetic particles incident in various geometries.\n') +print(table.get_string(border=False)) diff --git a/openmc/data/effective_dose/icrp74/neutrons.txt b/openmc/data/effective_dose/icrp74/neutrons.txt new file mode 100644 index 000000000..14aab48bd --- /dev/null +++ b/openmc/data/effective_dose/icrp74/neutrons.txt @@ -0,0 +1,50 @@ +Neutrons: Effective dose per fluence, in units of pSv cm², for monoenergetic particles incident in various geometries. + +Energy (MeV) AP PA LLAT RLAT ROT ISO +1.00E-09 5.24 3.52 1.68 1.36 2.99 2.4 +1.00E-08 6.55 4.39 2.04 1.7 3.72 2.89 +2.50E-08 7.6 5.16 2.31 1.99 4.4 3.3 +1.00E-07 9.95 6.77 2.86 2.58 5.75 4.13 +2.00E-07 11.2 7.63 3.21 2.92 6.43 4.59 +5.00E-07 12.8 8.76 3.72 3.35 7.27 5.2 +1.00E-06 13.8 9.55 4.12 3.67 7.84 5.63 +2.00E-06 14.5 10.2 4.39 3.89 8.31 5.96 +5.00E-06 15 10.7 4.66 4.08 8.72 6.28 +1.00E-05 15.1 11 4.8 4.16 8.9 6.44 +2.00E-05 15.1 11.1 4.89 4.2 8.92 6.51 +5.00E-05 14.8 11.1 4.95 4.19 8.82 6.51 +1.00E-04 14.6 11 4.95 4.15 8.69 6.45 +2.00E-04 14.4 10.9 4.92 4.1 8.56 6.32 +5.00E-04 14.2 10.7 4.86 4.03 8.4 6.14 +1.00E-03 14.2 10.7 4.84 4 8.34 6.04 +2.00E-03 14.4 10.8 4.87 4 8.39 6.05 +5.00E-03 15.7 11.6 5.25 4.29 9.06 6.52 +1.00E-02 18.3 13.5 6.14 5.02 10.6 7.7 +2.00E-02 23.8 17.3 7.95 6.48 13.8 10.2 +3.00E-02 29 21 9.74 7.93 16.9 12.7 +5.00E-02 38.5 27.6 13.1 10.6 22.7 17.3 +7.00E-02 47.2 33.5 16.1 13.1 27.8 21.5 +1.00E-01 59.8 41.3 20.1 16.4 34.8 27.2 +1.50E-01 80.2 52.2 25.5 21.2 45.4 35.2 +2.00E-01 99 61.5 30.3 25.6 54.8 42.4 +3.00E-01 133 77.1 38.6 33.4 71.6 54.7 +5.00E-01 188 103 53.2 46.8 99.4 75 +7.00E-01 231 124 66.6 58.3 123 92.8 +9.00E-01 267 144 79.6 69.1 144 108 +1 282 154 86 74.5 154 116 +1.2 310 175 99.8 85.8 173 130 +2 383 247 153 129 234 178 +3 432 308 195 171 283 220 +4 458 345 224 198 315 250 +5 474 366 244 217 335 272 +6 483 380 261 232 348 282 +7 490 391 274 244 358 290 +8 494 399 285 253 366 297 +9 497 406 294 261 373 303 +1.00E+01 499 412 302 268 378 309 +1.20E+01 499 422 315 278 385 322 +1.40E+01 496 429 324 286 390 333 +1.50E+01 494 431 328 290 391 338 +1.60E+01 491 433 331 293 393 342 +1.80E+01 486 435 335 299 394 345 +2.00E+01 480 436 338 305 395 343 diff --git a/openmc/data/effective_dose/icrp74/photons.txt b/openmc/data/effective_dose/icrp74/photons.txt new file mode 100644 index 000000000..1ce3d67e0 --- /dev/null +++ b/openmc/data/effective_dose/icrp74/photons.txt @@ -0,0 +1,26 @@ +Photons: Effective dose per fluence, in units of pSv cm², for monoenergetic particles incident in various geometries. + + Energy (MeV) AP PA LLAT RLAT ROT ISO + 0.0100000 0.0485179 0.0184264 0.0127796 0.0127796 0.0242218 0.0201353 + 0.0150000 0.1254240 0.0182832 0.0171288 0.0171288 0.0477360 0.0383760 + 0.0200000 0.2049600 0.0304080 0.0260400 0.0253680 0.0776160 0.0608160 + 0.0300000 0.2999360 0.0922880 0.0651784 0.0563822 0.1377110 0.1031030 + 0.0400000 0.3380520 0.1587300 0.1033890 0.0879450 0.1827540 0.1398540 + 0.0500000 0.3572380 0.2067200 0.1308150 0.1114350 0.2135030 0.1650530 + 0.0600000 0.3780120 0.2444940 0.1525920 0.1314950 0.2392920 0.1855380 + 0.0700000 0.4192860 0.2878680 0.1782040 0.1555560 0.2753520 0.2145600 + 0.0800000 0.4399310 0.3128330 0.1927960 0.1700780 0.2950270 0.2299430 + 0.1000000 0.5171740 0.3821300 0.2378110 0.2118410 0.3561600 0.2775080 + 0.1500000 0.7523440 0.5744410 0.3713800 0.3300490 0.5343080 0.4193000 + 0.2000000 1.0040880 0.7832400 0.5264400 0.4699440 0.7310240 0.5812240 + 0.3000000 1.5083400 1.2144000 0.8487000 0.7686600 1.1371200 0.9163200 + 0.4000000 1.9958400 1.6461900 1.1774700 1.0773000 1.5384600 1.2606300 + 0.5000000 2.4656800 2.0682200 1.5113000 1.3923000 1.9325600 1.6065000 + 0.6000000 2.9081600 2.4708000 1.8403200 1.7040000 2.3117600 1.9425600 + 0.8000000 3.7269000 3.2287500 2.4723000 2.3173200 3.0294900 2.5940700 + 1.0000000 4.4834100 3.9336000 3.0887700 2.9099700 3.7145700 3.2139300 + 2.0000000 7.4896000 6.8025500 5.7153500 5.4964000 6.5760500 5.8437000 + 4.0000000 12.0153000 11.1078000 9.8373000 9.6316000 10.9989000 9.9704000 + 6.0000000 15.9873000 14.8764000 13.4596000 13.3147000 14.8925000 13.6206000 + 8.0000000 19.9191000 18.6327000 17.0850000 17.0046000 18.7734000 17.2659000 + 10.0000000 23.7600000 22.2960000 20.6160000 20.6400000 22.5840000 20.8320000 diff --git a/tests/unit_tests/test_data_dose.py b/tests/unit_tests/test_data_dose.py index 348143e0b..2d80cf838 100644 --- a/tests/unit_tests/test_data_dose.py +++ b/tests/unit_tests/test_data_dose.py @@ -22,8 +22,22 @@ def test_dose_coefficients(): assert energy[-1] == approx(10e9) assert dose[-1] == approx(699.0) + energy, dose = dose_coefficients('photon', data_source='icrp74') + assert energy[0] == approx(0.01e6) + assert dose[0] == approx(7.43*0.00653) + assert energy[-1] == approx(10.0e6) + assert dose[-1] == approx(24.0*0.990) + + energy, dose = dose_coefficients('neutron', 'LLAT', data_source='icrp74') + assert energy[0] == approx(1e-3) + assert dose[0] == approx(1.68) + assert energy[-1] == approx(20.0e6) + assert dose[-1] == approx(338.0) + # Invalid particle/geometry should raise an exception with raises(ValueError): dose_coefficients('slime', 'LAT') with raises(ValueError): dose_coefficients('neutron', 'ZZ') + with raises(ValueError): + dose_coefficients('neutron', data_source='icrp7000') From e0471388333b7cd85187e30ebb712c241611cd50 Mon Sep 17 00:00:00 2001 From: Ahnaf Tahmid Chowdhury Date: Wed, 9 Oct 2024 07:57:14 +0600 Subject: [PATCH 183/671] Fix for UWUW Macro Conflict (#3150) Co-authored-by: Patrick Shriwise --- .github/workflows/ci.yml | 5 +++++ CMakeLists.txt | 13 ++++++++----- cmake/OpenMCConfig.cmake.in | 6 +++--- src/dagmc.cpp | 34 ++++++++++++++++++---------------- src/output.cpp | 2 +- 5 files changed, 35 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9293e319b..c67f19359 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -131,6 +131,11 @@ jobs: echo "$HOME/NJOY2016/build" >> $GITHUB_PATH $GITHUB_WORKSPACE/tools/ci/gha-install.sh + - name: display-config + shell: bash + run: | + openmc -v + - name: cache-xs uses: actions/cache@v4 with: diff --git a/CMakeLists.txt b/CMakeLists.txt index b4011434e..575e45373 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -510,6 +510,14 @@ endif() if(OPENMC_USE_DAGMC) target_compile_definitions(libopenmc PRIVATE DAGMC) target_link_libraries(libopenmc dagmc-shared) + + if(OPENMC_USE_UWUW) + target_compile_definitions(libopenmc PRIVATE OPENMC_UWUW) + target_link_libraries(libopenmc uwuw-shared) + endif() +elseif(OPENMC_USE_UWUW) + set(OPENMC_USE_UWUW OFF) + message(FATAL_ERROR "DAGMC must be enabled when UWUW is enabled.") endif() if(OPENMC_USE_LIBMESH) @@ -546,11 +554,6 @@ if(OPENMC_USE_NCRYSTAL) target_link_libraries(libopenmc NCrystal::NCrystal) endif() -if (OPENMC_USE_UWUW) - target_compile_definitions(libopenmc PRIVATE UWUW) - target_link_libraries(libopenmc uwuw-shared) -endif() - #=============================================================================== # Log build info that this executable can report later #=============================================================================== diff --git a/cmake/OpenMCConfig.cmake.in b/cmake/OpenMCConfig.cmake.in index 44a5e0d5a..b3b901de4 100644 --- a/cmake/OpenMCConfig.cmake.in +++ b/cmake/OpenMCConfig.cmake.in @@ -39,6 +39,6 @@ if(@OPENMC_USE_MCPL@) find_package(MCPL REQUIRED) endif() -if(@OPENMC_USE_UWUW@) - find_package(UWUW REQUIRED) -endif() +if(@OPENMC_USE_UWUW@ AND NOT ${DAGMC_BUILD_UWUW}) + message(FATAL_ERROR "UWUW is enabled in OpenMC but the DAGMC installation discovered was not configured with UWUW.") +endif() \ No newline at end of file diff --git a/src/dagmc.cpp b/src/dagmc.cpp index a29a2589f..b79676c36 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -11,7 +11,7 @@ #include "openmc/settings.h" #include "openmc/string_utils.h" -#ifdef UWUW +#ifdef OPENMC_UWUW #include "uwuw.hpp" #endif #include @@ -29,7 +29,7 @@ const bool DAGMC_ENABLED = true; const bool DAGMC_ENABLED = false; #endif -#ifdef UWUW +#ifdef OPENMC_UWUW const bool UWUW_ENABLED = true; #else const bool UWUW_ENABLED = false; @@ -112,6 +112,11 @@ void DAGUniverse::initialize() { geom_type() = GeometryType::DAG; +#ifdef OPENMC_UWUW + // read uwuw materials from the .h5m file if present + read_uwuw_materials(); +#endif + init_dagmc(); init_metadata(); @@ -431,16 +436,16 @@ void DAGUniverse::to_hdf5(hid_t universes_group) const bool DAGUniverse::uses_uwuw() const { -#ifdef UWUW +#ifdef OPENMC_UWUW return uwuw_ && !uwuw_->material_library.empty(); #else return false; -#endif // UWUW +#endif // OPENMC_UWUW } std::string DAGUniverse::get_uwuw_materials_xml() const { -#ifdef UWUW +#ifdef OPENMC_UWUW if (!uses_uwuw()) { throw std::runtime_error("This DAGMC Universe does not use UWUW materials"); } @@ -460,12 +465,12 @@ std::string DAGUniverse::get_uwuw_materials_xml() const return ss.str(); #else fatal_error("DAGMC was not configured with UWUW."); -#endif // UWUW +#endif // OPENMC_UWUW } void DAGUniverse::write_uwuw_materials_xml(const std::string& outfile) const { -#ifdef UWUW +#ifdef OPENMC_UWUW if (!uses_uwuw()) { throw std::runtime_error( "This DAGMC universe does not use UWUW materials."); @@ -478,7 +483,7 @@ void DAGUniverse::write_uwuw_materials_xml(const std::string& outfile) const mats_xml.close(); #else fatal_error("DAGMC was not configured with UWUW."); -#endif +#endif // OPENMC_UWUW } void DAGUniverse::legacy_assign_material( @@ -540,7 +545,7 @@ void DAGUniverse::legacy_assign_material( void DAGUniverse::read_uwuw_materials() { -#ifdef UWUW +#ifdef OPENMC_UWUW // If no filename was provided, don't read UWUW materials if (filename_ == "") return; @@ -580,16 +585,13 @@ void DAGUniverse::read_uwuw_materials() } #else fatal_error("DAGMC was not configured with UWUW."); -#endif +#endif // OPENMC_UWUW } void DAGUniverse::uwuw_assign_material( moab::EntityHandle vol_handle, std::unique_ptr& c) const { -#ifdef UWUW - // read materials from uwuw material file - read_uwuw_materials(); - +#ifdef OPENMC_UWUW // lookup material in uwuw if present std::string uwuw_mat = dmd_ptr->volume_material_property_data_eh[vol_handle]; if (uwuw_->material_library.count(uwuw_mat) != 0) { @@ -601,11 +603,11 @@ void DAGUniverse::uwuw_assign_material( } else { fatal_error(fmt::format("Material with value '{}' not found in the " "UWUW material library", - mat_str)); + uwuw_mat)); } #else fatal_error("DAGMC was not configured with UWUW."); -#endif +#endif // OPENMC_UWUW } //============================================================================== // DAGMC Cell implementation diff --git a/src/output.cpp b/src/output.cpp index 5fdbea130..a430fe9a6 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -347,7 +347,7 @@ void print_build_info() #ifdef COVERAGEBUILD coverage = y; #endif -#ifdef UWUW +#ifdef OPENMC_UWUW uwuw = y; #endif From fb3aaa46ac6dac05315c7fe84c3f105fbb74ae46 Mon Sep 17 00:00:00 2001 From: Ahnaf Tahmid Chowdhury Date: Thu, 10 Oct 2024 22:05:32 +0600 Subject: [PATCH 184/671] Improve Detection of libMesh Installation via `LIBMESH_ROOT` and CMake's PkgConfig (#3149) --- .github/workflows/ci.yml | 1 - cmake/Modules/FindLIBMESH.cmake | 4 ++-- tools/ci/gha-install-libmesh.sh | 1 - tools/ci/gha-install.py | 7 ++++--- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c67f19359..5973c3cd4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,7 +96,6 @@ jobs: - name: Environment Variables run: | - echo "DAGMC_ROOT=$HOME/DAGMC" echo "OPENMC_CROSS_SECTIONS=$HOME/nndc_hdf5/cross_sections.xml" >> $GITHUB_ENV echo "OPENMC_ENDF_DATA=$HOME/endf-b-vii.1" >> $GITHUB_ENV diff --git a/cmake/Modules/FindLIBMESH.cmake b/cmake/Modules/FindLIBMESH.cmake index 048dfc2a8..df9208c18 100644 --- a/cmake/Modules/FindLIBMESH.cmake +++ b/cmake/Modules/FindLIBMESH.cmake @@ -15,7 +15,7 @@ if(DEFINED ENV{METHOD}) endif() find_package(PkgConfig REQUIRED) -set(ENV{PKG_CONFIG_PATH} "$ENV{PKG_CONFIG_PATH}:${LIBMESH_PC}") -set(PKG_CONFIG_USE_CMAKE_PREFIX_PATH True) + +set(PKG_CONFIG_USE_CMAKE_PREFIX_PATH TRUE) pkg_check_modules(LIBMESH REQUIRED ${LIBMESH_PC_FILE}>=1.7.0 IMPORTED_TARGET) pkg_get_variable(LIBMESH_PREFIX ${LIBMESH_PC_FILE} prefix) diff --git a/tools/ci/gha-install-libmesh.sh b/tools/ci/gha-install-libmesh.sh index cb808ae5b..d4557d2d3 100755 --- a/tools/ci/gha-install-libmesh.sh +++ b/tools/ci/gha-install-libmesh.sh @@ -16,7 +16,6 @@ else ../libmesh/configure --prefix=$HOME/LIBMESH --enable-exodus --disable-netcdf-4 --disable-eigen --disable-lapack --disable-mpi fi make -j4 install -export LIBMESH_PC=$HOME/LIBMESH/lib/pkgconfig/ rm -rf $HOME/LIBMESH/build popd diff --git a/tools/ci/gha-install.py b/tools/ci/gha-install.py index f046e8634..282389a8f 100644 --- a/tools/ci/gha-install.py +++ b/tools/ci/gha-install.py @@ -31,7 +31,8 @@ def install(omp=False, mpi=False, phdf5=False, dagmc=False, libmesh=False, ncrys if dagmc: cmake_cmd.append('-DOPENMC_USE_DAGMC=ON') - cmake_cmd.append('-DCMAKE_PREFIX_PATH=~/DAGMC') + dagmc_path = os.environ.get('HOME') + '/DAGMC' + cmake_cmd.append('-DCMAKE_PREFIX_PATH=' + dagmc_path) if libmesh: cmake_cmd.append('-DOPENMC_USE_LIBMESH=ON') @@ -40,8 +41,8 @@ def install(omp=False, mpi=False, phdf5=False, dagmc=False, libmesh=False, ncrys if ncrystal: cmake_cmd.append('-DOPENMC_USE_NCRYSTAL=ON') - ncrystal_cmake_path = os.environ.get('HOME') + '/ncrystal_inst/lib/cmake' - cmake_cmd.append(f'-DCMAKE_PREFIX_PATH={ncrystal_cmake_path}') + ncrystal_path = os.environ.get('HOME') + '/ncrystal_inst' + cmake_cmd.append(f'-DCMAKE_PREFIX_PATH={ncrystal_path}') # Build in coverage mode for coverage testing cmake_cmd.append('-DOPENMC_ENABLE_COVERAGE=on') From 579777a3e5f84ace43b19d26379dd4f85af5d30c Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 10 Oct 2024 12:17:40 -0500 Subject: [PATCH 185/671] Consistency in treatment of paths for files specified within the Model class (#3153) --- docs/source/usersguide/geometry.rst | 25 ++++++++---- openmc/config.py | 27 +++++++++++-- openmc/material.py | 3 +- openmc/mesh.py | 9 ++--- openmc/settings.py | 38 +++++++++++-------- openmc/source.py | 38 +++++++++---------- openmc/universe.py | 14 +++---- openmc/utility_funcs.py | 22 +++++++++++ tests/conftest.py | 7 ++++ tests/regression_tests/source_dlopen/test.py | 3 +- .../source_parameterized_dlopen/test.py | 3 +- tests/unit_tests/test_config.py | 12 +++++- tests/unit_tests/test_settings.py | 2 +- tests/unit_tests/test_source.py | 8 ++-- tests/unit_tests/test_temp_interp.py | 4 +- 15 files changed, 143 insertions(+), 72 deletions(-) diff --git a/docs/source/usersguide/geometry.rst b/docs/source/usersguide/geometry.rst index 3a3d02231..6f14ebfa5 100644 --- a/docs/source/usersguide/geometry.rst +++ b/docs/source/usersguide/geometry.rst @@ -474,7 +474,7 @@ applied as universes in the OpenMC geometry file. A geometry represented entirely by a DAGMC geometry will contain only the DAGMC universe. Using a :class:`openmc.DAGMCUniverse` looks like the following:: - dag_univ = openmc.DAGMCUniverse(filename='dagmc.h5m') + dag_univ = openmc.DAGMCUniverse('dagmc.h5m') geometry = openmc.Geometry(dag_univ) geometry.export_to_xml() @@ -495,13 +495,22 @@ It is important in these cases to understand the DAGMC model's position with respect to the CSG geometry. DAGMC geometries can be plotted with OpenMC to verify that the model matches one's expectations. -**Note:** DAGMC geometries used in OpenMC are currently required to be clean, -meaning that all surfaces have been `imprinted and merged -`_ successfully -and that the model is `watertight -`_. -Future implementations of DAGMC geometry will support small volume overlaps and -un-merged surfaces. +By default, when you specify a .h5m file for a :class:`~openmc.DAGMCUniverse` +instance, it will store the absolute path to the .h5m file. If you prefer to +store the relative path, you can set the ``'resolve_paths'`` configuration +variable:: + + openmc.config['resolve_paths'] = False + dag_univ = openmc.DAGMCUniverse('dagmc.h5m') + +.. note:: + DAGMC geometries used in OpenMC are currently required to be clean, + meaning that all surfaces have been `imprinted and merged + `_ successfully + and that the model is `watertight + `_. + Future implementations of DAGMC geometry will support small volume overlaps and + un-merged surfaces. Cell, Surface, and Material IDs ------------------------------- diff --git a/openmc/config.py b/openmc/config.py index b823d6b06..ab53ab61b 100644 --- a/openmc/config.py +++ b/openmc/config.py @@ -1,4 +1,5 @@ from collections.abc import MutableMapping +from contextlib import contextmanager import os from pathlib import Path import warnings @@ -11,7 +12,7 @@ __all__ = ["config"] class _Config(MutableMapping): def __init__(self, data=()): - self._mapping = {} + self._mapping = {'resolve_paths': True} self.update(data) def __getitem__(self, key): @@ -42,10 +43,12 @@ class _Config(MutableMapping): # Reset photon source data since it relies on chain file _DECAY_PHOTON_ENERGY.clear() _DECAY_ENERGY.clear() + elif key == 'resolve_paths': + self._mapping[key] = value else: raise KeyError(f'Unrecognized config key: {key}. Acceptable keys ' - 'are "cross_sections", "mg_cross_sections" and ' - '"chain_file"') + 'are "cross_sections", "mg_cross_sections", ' + '"chain_file", and "resolve_paths".') def __iter__(self): return iter(self._mapping) @@ -61,6 +64,24 @@ class _Config(MutableMapping): if not p.exists(): warnings.warn(f"'{value}' does not exist.") + @contextmanager + def patch(self, key, value): + """Temporarily change a value in the configuration. + + Parameters + ---------- + key : str + Key to change + value : object + New value + """ + previous_value = self.get(key) + self[key] = value + yield + if previous_value is None: + del self[key] + else: + self[key] = previous_value def _default_config(): """Return default configuration""" diff --git a/openmc/material.py b/openmc/material.py index f550fd649..74a403c15 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -16,6 +16,7 @@ import openmc.data import openmc.checkvalue as cv from ._xml import clean_indentation, reorder_attributes from .mixin import IDManagerMixin +from .utility_funcs import input_path from openmc.checkvalue import PathLike from openmc.stats import Univariate, Discrete, Mixture from openmc.data.data import _get_element_symbol @@ -1643,7 +1644,7 @@ class Materials(cv.CheckedList): @cross_sections.setter def cross_sections(self, cross_sections): if cross_sections is not None: - self._cross_sections = Path(cross_sections) + self._cross_sections = input_path(cross_sections) def append(self, material): """Append material to collection diff --git a/openmc/mesh.py b/openmc/mesh.py index a706b8fa8..6afe5d36e 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -5,8 +5,6 @@ from collections.abc import Iterable, Sequence from functools import wraps from math import pi, sqrt, atan2 from numbers import Integral, Real -from pathlib import Path -import tempfile import h5py import lxml.etree as ET @@ -19,6 +17,7 @@ from openmc.utility_funcs import change_directory from ._xml import get_text from .mixin import IDManagerMixin from .surface import _BOUNDARY_TYPES +from .utility_funcs import input_path class MeshBase(IDManagerMixin, ABC): @@ -2072,7 +2071,7 @@ class UnstructuredMesh(MeshBase): Parameters ---------- - filename : str or pathlib.Path + filename : path-like Location of the unstructured mesh file library : {'moab', 'libmesh'} Mesh library used for the unstructured mesh tally @@ -2158,8 +2157,8 @@ class UnstructuredMesh(MeshBase): @filename.setter def filename(self, filename): - cv.check_type('Unstructured Mesh filename', filename, (str, Path)) - self._filename = filename + cv.check_type('Unstructured Mesh filename', filename, PathLike) + self._filename = input_path(filename) @property def library(self): diff --git a/openmc/settings.py b/openmc/settings.py index 96e6368e4..0a78fb564 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -8,12 +8,14 @@ from pathlib import Path import lxml.etree as ET import openmc.checkvalue as cv -from openmc.stats.multivariate import MeshSpatial -from . import (RegularMesh, SourceBase, MeshSource, IndependentSource, - VolumeCalculation, WeightWindows, WeightWindowGenerator) -from ._xml import clean_indentation, get_text, reorder_attributes from openmc.checkvalue import PathLike -from .mesh import _read_meshes +from openmc.stats.multivariate import MeshSpatial +from ._xml import clean_indentation, get_text, reorder_attributes +from .mesh import _read_meshes, RegularMesh +from .source import SourceBase, MeshSource, IndependentSource +from .utility_funcs import input_path +from .volume import VolumeCalculation +from .weight_windows import WeightWindows, WeightWindowGenerator class RunMode(Enum): @@ -699,14 +701,18 @@ class Settings: return self._surf_source_read @surf_source_read.setter - def surf_source_read(self, surf_source_read: dict): - cv.check_type('surface source reading options', surf_source_read, Mapping) - for key, value in surf_source_read.items(): + def surf_source_read(self, ssr: dict): + cv.check_type('surface source reading options', ssr, Mapping) + for key, value in ssr.items(): cv.check_value('surface source reading key', key, ('path')) if key == 'path': - cv.check_type('path to surface source file', value, str) - self._surf_source_read = surf_source_read + cv.check_type('path to surface source file', value, PathLike) + self._surf_source_read = dict(ssr) + + # Resolve path to surface source file + if 'path' in ssr: + self._surf_source_read['path'] = input_path(ssr['path']) @property def surf_source_write(self) -> dict: @@ -1066,8 +1072,8 @@ class Settings: @weight_windows_file.setter def weight_windows_file(self, value: PathLike): - cv.check_type('weight windows file', value, (str, Path)) - self._weight_windows_file = value + cv.check_type('weight windows file', value, PathLike) + self._weight_windows_file = input_path(value) @property def weight_window_generators(self) -> list[WeightWindowGenerator]: @@ -1241,7 +1247,7 @@ class Settings: element = ET.SubElement(root, "surf_source_read") if 'path' in self._surf_source_read: subelement = ET.SubElement(element, "path") - subelement.text = self._surf_source_read['path'] + subelement.text = str(self._surf_source_read['path']) def _create_surf_source_write_subelement(self, root): if self._surf_source_write: @@ -1501,7 +1507,7 @@ class Settings: def _create_weight_windows_file_element(self, root): if self.weight_windows_file is not None: element = ET.Element("weight_windows_file") - element.text = self.weight_windows_file + element.text = str(self.weight_windows_file) root.append(element) def _create_weight_window_checkpoints_subelement(self, root): @@ -1645,9 +1651,11 @@ class Settings: def _surf_source_read_from_xml_element(self, root): elem = root.find('surf_source_read') if elem is not None: + ssr = {} value = get_text(elem, 'path') if value is not None: - self.surf_source_read['path'] = value + ssr['path'] = value + self.surf_source_read = ssr def _surf_source_write_from_xml_element(self, root): elem = root.find('surf_source_write') diff --git a/openmc/source.py b/openmc/source.py index 7c8e6af8d..878f52c3b 100644 --- a/openmc/source.py +++ b/openmc/source.py @@ -3,6 +3,7 @@ from abc import ABC, abstractmethod from collections.abc import Iterable, Sequence from enum import IntEnum from numbers import Real +from pathlib import Path import warnings from typing import Any from pathlib import Path @@ -19,6 +20,7 @@ from openmc.stats.multivariate import UnitSphere, Spatial from openmc.stats.univariate import Univariate from ._xml import get_text from .mesh import MeshBase, StructuredMesh, UnstructuredMesh +from .utility_funcs import input_path class SourceBase(ABC): @@ -664,7 +666,7 @@ class CompiledSource(SourceBase): Parameters ---------- - library : str or None + library : path-like Path to a compiled shared library parameters : str Parameters to be provided to the compiled shared library function @@ -686,7 +688,7 @@ class CompiledSource(SourceBase): Attributes ---------- - library : str or None + library : pathlib.Path Path to a compiled shared library parameters : str Parameters to be provided to the compiled shared library function @@ -702,17 +704,13 @@ class CompiledSource(SourceBase): """ def __init__( self, - library: str | None = None, + library: PathLike, parameters: str | None = None, strength: float = 1.0, constraints: dict[str, Any] | None = None ) -> None: super().__init__(strength=strength, constraints=constraints) - - self._library = None - if library is not None: - self.library = library - + self.library = library self._parameters = None if parameters is not None: self.parameters = parameters @@ -722,13 +720,13 @@ class CompiledSource(SourceBase): return "compiled" @property - def library(self) -> str: + def library(self) -> Path: return self._library @library.setter - def library(self, library_name): - cv.check_type('library', library_name, str) - self._library = library_name + def library(self, library_name: PathLike): + cv.check_type('library', library_name, PathLike) + self._library = input_path(library_name) @property def parameters(self) -> str: @@ -748,7 +746,7 @@ class CompiledSource(SourceBase): XML element containing source data """ - element.set("library", self.library) + element.set("library", str(self.library)) if self.parameters is not None: element.set("parameters", self.parameters) @@ -794,7 +792,7 @@ class FileSource(SourceBase): Parameters ---------- - path : str or pathlib.Path + path : path-like Path to the source file from which sites should be sampled strength : float Strength of the source (default is 1.0) @@ -829,14 +827,12 @@ class FileSource(SourceBase): def __init__( self, - path: PathLike | None = None, + path: PathLike, strength: float = 1.0, constraints: dict[str, Any] | None = None ): super().__init__(strength=strength, constraints=constraints) - self._path = None - if path is not None: - self.path = path + self.path = path @property def type(self) -> str: @@ -848,8 +844,8 @@ class FileSource(SourceBase): @path.setter def path(self, p: PathLike): - cv.check_type('source file', p, str) - self._path = p + cv.check_type('source file', p, PathLike) + self._path = input_path(p) def populate_xml_element(self, element): """Add necessary file source information to an XML element @@ -861,7 +857,7 @@ class FileSource(SourceBase): """ if self.path is not None: - element.set("file", self.path) + element.set("file", str(self.path)) @classmethod def from_xml_element(cls, elem: ET.Element) -> openmc.FileSource: diff --git a/openmc/universe.py b/openmc/universe.py index d424c243b..648b773df 100644 --- a/openmc/universe.py +++ b/openmc/universe.py @@ -17,6 +17,7 @@ from ._xml import get_text from .checkvalue import check_type, check_value from .mixin import IDManagerMixin from .surface import _BOUNDARY_TYPES +from .utility_funcs import input_path class UniverseBase(ABC, IDManagerMixin): @@ -766,7 +767,7 @@ class DAGMCUniverse(UniverseBase): Parameters ---------- - filename : str + filename : path-like Path to the DAGMC file used to represent this universe. universe_id : int, optional Unique identifier of the universe. If not specified, an identifier will @@ -820,7 +821,7 @@ class DAGMCUniverse(UniverseBase): """ def __init__(self, - filename, + filename: cv.PathLike, universe_id=None, name='', auto_geom_ids=False, @@ -850,9 +851,9 @@ class DAGMCUniverse(UniverseBase): return self._filename @filename.setter - def filename(self, val): - cv.check_type('DAGMC filename', val, (Path, str)) - self._filename = val + def filename(self, val: cv.PathLike): + cv.check_type('DAGMC filename', val, cv.PathLike) + self._filename = input_path(val) @property def auto_geom_ids(self): @@ -915,8 +916,7 @@ class DAGMCUniverse(UniverseBase): def decode_str_tag(tag_val): return tag_val.tobytes().decode().replace('\x00', '') - dagmc_filepath = Path(self.filename).resolve() - with h5py.File(dagmc_filepath) as dagmc_file: + with h5py.File(self.filename) as dagmc_file: category_data = dagmc_file['tstt/tags/CATEGORY/values'] category_strs = map(decode_str_tag, category_data) n = sum([v == geom_type.capitalize() for v in category_strs]) diff --git a/openmc/utility_funcs.py b/openmc/utility_funcs.py index 3dff45380..da9f73b16 100644 --- a/openmc/utility_funcs.py +++ b/openmc/utility_funcs.py @@ -3,8 +3,10 @@ import os from pathlib import Path from tempfile import TemporaryDirectory +import openmc from .checkvalue import PathLike + @contextmanager def change_directory(working_dir: PathLike | None = None, *, tmpdir: bool = False): """Context manager for executing in a provided working directory @@ -35,3 +37,23 @@ def change_directory(working_dir: PathLike | None = None, *, tmpdir: bool = Fals os.chdir(orig_dir) if tmpdir: tmp.cleanup() + + +def input_path(filename: PathLike) -> Path: + """Return a path object for an input file based on global configuration + + Parameters + ---------- + filename : PathLike + Path to input file + + Returns + ------- + pathlib.Path + Path object + + """ + if openmc.config['resolve_paths']: + return Path(filename).resolve() + else: + return Path(filename) diff --git a/tests/conftest.py b/tests/conftest.py index 639d669f3..cd86da539 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ import pytest +import openmc from tests.regression_tests import config as regression_config @@ -27,3 +28,9 @@ def run_in_tmpdir(tmpdir): yield finally: orig.chdir() + + +@pytest.fixture(scope='session', autouse=True) +def resolve_paths(): + with openmc.config.patch('resolve_paths', False): + yield diff --git a/tests/regression_tests/source_dlopen/test.py b/tests/regression_tests/source_dlopen/test.py index 88ff9dd85..0581d6dee 100644 --- a/tests/regression_tests/source_dlopen/test.py +++ b/tests/regression_tests/source_dlopen/test.py @@ -72,8 +72,7 @@ def model(): model.tallies = openmc.Tallies([tally]) # custom source from shared library - source = openmc.CompiledSource() - source.library = 'build/libsource.so' + source = openmc.CompiledSource('build/libsource.so') model.settings.source = source return model diff --git a/tests/regression_tests/source_parameterized_dlopen/test.py b/tests/regression_tests/source_parameterized_dlopen/test.py index 1cc253528..151fb3735 100644 --- a/tests/regression_tests/source_parameterized_dlopen/test.py +++ b/tests/regression_tests/source_parameterized_dlopen/test.py @@ -71,8 +71,7 @@ def model(): model.tallies = openmc.Tallies([tally]) # custom source from shared library - source = openmc.CompiledSource() - source.library = 'build/libsource.so' + source = openmc.CompiledSource('build/libsource.so') source.parameters = '1e3' model.settings.source = source diff --git a/tests/unit_tests/test_config.py b/tests/unit_tests/test_config.py index 1d3c0f173..9d3f53a74 100644 --- a/tests/unit_tests/test_config.py +++ b/tests/unit_tests/test_config.py @@ -19,7 +19,10 @@ def test_config_basics(): assert isinstance(openmc.config, Mapping) for key, value in openmc.config.items(): assert isinstance(key, str) - assert isinstance(value, os.PathLike) + if key == 'resolve_paths': + assert isinstance(value, bool) + else: + assert isinstance(value, os.PathLike) # Set and delete openmc.config['cross_sections'] = '/path/to/cross_sections.xml' @@ -32,6 +35,13 @@ def test_config_basics(): openmc.config['🐖'] = '/like/to/eat/bacon' +def test_config_patch(): + openmc.config['cross_sections'] = '/path/to/cross_sections.xml' + with openmc.config.patch('cross_sections', '/path/to/other.xml'): + assert str(openmc.config['cross_sections']) == '/path/to/other.xml' + assert str(openmc.config['cross_sections']) == '/path/to/cross_sections.xml' + + def test_config_set_envvar(): openmc.config['cross_sections'] = '/path/to/cross_sections.xml' assert os.environ['OPENMC_CROSS_SECTIONS'] == '/path/to/cross_sections.xml' diff --git a/tests/unit_tests/test_settings.py b/tests/unit_tests/test_settings.py index 650bfd186..02a476251 100644 --- a/tests/unit_tests/test_settings.py +++ b/tests/unit_tests/test_settings.py @@ -91,7 +91,7 @@ def test_export_to_xml(run_in_tmpdir): assert s.sourcepoint == {'batches': [50, 150, 500, 1000], 'separate': True, 'write': True, 'overwrite': True, 'mcpl': True} assert s.statepoint == {'batches': [50, 150, 500, 1000]} - assert s.surf_source_read == {'path': 'surface_source_1.h5'} + assert s.surf_source_read['path'].name == 'surface_source_1.h5' assert s.surf_source_write == {'surface_ids': [2], 'max_particles': 200} assert s.confidence_intervals assert s.ptables diff --git a/tests/unit_tests/test_source.py b/tests/unit_tests/test_source.py index 9a19f6f24..32650d549 100644 --- a/tests/unit_tests/test_source.py +++ b/tests/unit_tests/test_source.py @@ -53,7 +53,7 @@ def test_spherical_uniform(): def test_source_file(): filename = 'source.h5' src = openmc.FileSource(path=filename) - assert src.path == filename + assert src.path.name == filename elem = src.to_xml_element() assert 'strength' in elem.attrib @@ -61,9 +61,9 @@ def test_source_file(): def test_source_dlopen(): - library = './libsource.so' - src = openmc.CompiledSource(library=library) - assert src.library == library + library = 'libsource.so' + src = openmc.CompiledSource(library) + assert src.library.name == library elem = src.to_xml_element() assert 'library' in elem.attrib diff --git a/tests/unit_tests/test_temp_interp.py b/tests/unit_tests/test_temp_interp.py index 4c2882347..4566070cf 100644 --- a/tests/unit_tests/test_temp_interp.py +++ b/tests/unit_tests/test_temp_interp.py @@ -152,7 +152,7 @@ def model(tmp_path_factory): mat = openmc.Material() mat.add_nuclide('U235', 1.0) model.materials.append(mat) - model.materials.cross_sections = str(Path('cross_sections_fake.xml').resolve()) + model.materials.cross_sections = 'cross_sections_fake.xml' sph = openmc.Sphere(r=100.0, boundary_type='reflective') cell = openmc.Cell(fill=mat, region=-sph) @@ -257,7 +257,7 @@ def test_temperature_slightly_above(run_in_tmpdir): mat2.add_nuclide('U235', 1.0) mat2.temperature = 600.0 model.materials.extend([mat1, mat2]) - model.materials.cross_sections = str(Path('cross_sections_fake.xml').resolve()) + model.materials.cross_sections = 'cross_sections_fake.xml' sph1 = openmc.Sphere(r=1.0) sph2 = openmc.Sphere(r=4.0, boundary_type='reflective') From 91fd60be69caf42b615f79f3b75c52843009033e Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 10 Oct 2024 12:58:15 -0500 Subject: [PATCH 186/671] Immediately resolve complement operators for regions (#3145) --- openmc/model/surface_composite.py | 8 ----- openmc/region.py | 30 +++++++++---------- openmc/surface.py | 3 +- .../filter_mesh/inputs_true.dat | 2 +- .../filter_translations/inputs_true.dat | 2 +- .../mgxs_library_mesh/inputs_true.dat | 2 +- .../photon_production_inputs_true.dat | 2 +- .../photon_production/inputs_true.dat | 2 +- .../score_current/inputs_true.dat | 2 +- tests/unit_tests/test_region.py | 2 +- 10 files changed, 24 insertions(+), 31 deletions(-) diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index a2cb02438..e2a5f49c4 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -778,12 +778,6 @@ class XConeOneSided(CompositeSurface): def __neg__(self): return -self.cone & (+self.plane if self.up else -self.plane) - def __pos__(self): - if self.up: - return (+self.cone & +self.plane) | -self.plane - else: - return (+self.cone & -self.plane) | +self.plane - class YConeOneSided(CompositeSurface): """One-sided cone parallel the y-axis @@ -836,7 +830,6 @@ class YConeOneSided(CompositeSurface): self.up = up __neg__ = XConeOneSided.__neg__ - __pos__ = XConeOneSided.__pos__ class ZConeOneSided(CompositeSurface): @@ -890,7 +883,6 @@ class ZConeOneSided(CompositeSurface): self.up = up __neg__ = XConeOneSided.__neg__ - __pos__ = XConeOneSided.__pos__ class Polygon(CompositeSurface): diff --git a/openmc/region.py b/openmc/region.py index e509b1528..e1cb83475 100644 --- a/openmc/region.py +++ b/openmc/region.py @@ -1,3 +1,4 @@ +from __future__ import annotations from abc import ABC, abstractmethod from collections.abc import MutableSequence from copy import deepcopy @@ -30,8 +31,9 @@ class Region(ABC): def __or__(self, other): return Union((self, other)) - def __invert__(self): - return Complement(self) + @abstractmethod + def __invert__(self) -> Region: + pass @abstractmethod def __contains__(self, point): @@ -442,6 +444,9 @@ class Intersection(Region, MutableSequence): self.append(other) return self + def __invert__(self) -> Union: + return Union(~n for n in self) + # Implement mutable sequence protocol by delegating to list def __getitem__(self, key): return self._nodes[key] @@ -530,6 +535,9 @@ class Union(Region, MutableSequence): self.append(other) return self + def __invert__(self) -> Intersection: + return Intersection(~n for n in self) + # Implement mutable sequence protocol by delegating to list def __getitem__(self, key): return self._nodes[key] @@ -603,7 +611,7 @@ class Complement(Region): """ - def __init__(self, node): + def __init__(self, node: Region): self.node = node def __contains__(self, point): @@ -622,6 +630,9 @@ class Complement(Region): """ return point not in self.node + def __invert__(self) -> Region: + return self.node + def __str__(self): return '~' + str(self.node) @@ -637,18 +648,7 @@ class Complement(Region): @property def bounding_box(self) -> BoundingBox: - # Use De Morgan's laws to distribute the complement operator so that it - # only applies to surface half-spaces, thus allowing us to calculate the - # bounding box in the usual recursive manner. - if isinstance(self.node, Union): - temp_region = Intersection(~n for n in self.node) - elif isinstance(self.node, Intersection): - temp_region = Union(~n for n in self.node) - elif isinstance(self.node, Complement): - temp_region = self.node.node - else: - temp_region = ~self.node - return temp_region.bounding_box + return (~self.node).bounding_box def get_surfaces(self, surfaces=None): """Recursively find and return all the surfaces referenced by the node diff --git a/openmc/surface.py b/openmc/surface.py index 2f10750a8..537823ba1 100644 --- a/openmc/surface.py +++ b/openmc/surface.py @@ -1,3 +1,4 @@ +from __future__ import annotations from abc import ABC, abstractmethod from collections.abc import Iterable from copy import deepcopy @@ -2631,7 +2632,7 @@ class Halfspace(Region): else: return Union((self, other)) - def __invert__(self): + def __invert__(self) -> Halfspace: return -self.surface if self.side == '+' else +self.surface def __contains__(self, point): diff --git a/tests/regression_tests/filter_mesh/inputs_true.dat b/tests/regression_tests/filter_mesh/inputs_true.dat index a58d58bab..0667c0341 100644 --- a/tests/regression_tests/filter_mesh/inputs_true.dat +++ b/tests/regression_tests/filter_mesh/inputs_true.dat @@ -12,7 +12,7 @@ - + diff --git a/tests/regression_tests/filter_translations/inputs_true.dat b/tests/regression_tests/filter_translations/inputs_true.dat index a80ccd876..5004c3217 100644 --- a/tests/regression_tests/filter_translations/inputs_true.dat +++ b/tests/regression_tests/filter_translations/inputs_true.dat @@ -12,7 +12,7 @@ - + diff --git a/tests/regression_tests/mgxs_library_mesh/inputs_true.dat b/tests/regression_tests/mgxs_library_mesh/inputs_true.dat index bb9c99026..1ecb7a2d3 100644 --- a/tests/regression_tests/mgxs_library_mesh/inputs_true.dat +++ b/tests/regression_tests/mgxs_library_mesh/inputs_true.dat @@ -12,7 +12,7 @@ - + diff --git a/tests/regression_tests/model_xml/photon_production_inputs_true.dat b/tests/regression_tests/model_xml/photon_production_inputs_true.dat index ee6cb8862..10f0bad98 100644 --- a/tests/regression_tests/model_xml/photon_production_inputs_true.dat +++ b/tests/regression_tests/model_xml/photon_production_inputs_true.dat @@ -9,7 +9,7 @@ - + diff --git a/tests/regression_tests/photon_production/inputs_true.dat b/tests/regression_tests/photon_production/inputs_true.dat index ee6cb8862..10f0bad98 100644 --- a/tests/regression_tests/photon_production/inputs_true.dat +++ b/tests/regression_tests/photon_production/inputs_true.dat @@ -9,7 +9,7 @@ - + diff --git a/tests/regression_tests/score_current/inputs_true.dat b/tests/regression_tests/score_current/inputs_true.dat index 9ea8e5e4a..ddbd6c24b 100644 --- a/tests/regression_tests/score_current/inputs_true.dat +++ b/tests/regression_tests/score_current/inputs_true.dat @@ -12,7 +12,7 @@ - + diff --git a/tests/unit_tests/test_region.py b/tests/unit_tests/test_region.py index 8c9e0afe4..cbcd19831 100644 --- a/tests/unit_tests/test_region.py +++ b/tests/unit_tests/test_region.py @@ -106,7 +106,7 @@ def test_complement(reset): assert_unbounded(outside_equiv) # string represention - assert str(inside) == '~(1 | -2 | 3)' + assert str(inside) == '(-1 2 -3)' # evaluate method assert (0, 0, 0) in inside From b4a796e9b42022501798d297d5f4039a1a5dc246 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 10 Oct 2024 13:39:16 -0500 Subject: [PATCH 187/671] Avoid writing subnormal nuclide densities to XML (#3144) --- openmc/material.py | 14 ++++++++++++-- tests/unit_tests/test_material.py | 14 ++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/openmc/material.py b/openmc/material.py index 74a403c15..63994b305 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -5,6 +5,7 @@ from copy import deepcopy from numbers import Real from pathlib import Path import re +import sys import warnings import lxml.etree as ET @@ -26,6 +27,9 @@ from openmc.data.data import _get_element_symbol DENSITY_UNITS = ('g/cm3', 'g/cc', 'kg/m3', 'atom/b-cm', 'atom/cm3', 'sum', 'macro') +# Smallest normalized floating point number +_SMALLEST_NORMAL = sys.float_info.min + NuclideTuple = namedtuple('NuclideTuple', ['name', 'percent', 'percent_type']) @@ -1339,10 +1343,16 @@ class Material(IDManagerMixin): xml_element = ET.Element("nuclide") xml_element.set("name", nuclide.name) + # Prevent subnormal numbers from being written to XML, which causes an + # exception on the C++ side when calling std::stod + val = nuclide.percent + if abs(val) < _SMALLEST_NORMAL: + val = 0.0 + if nuclide.percent_type == 'ao': - xml_element.set("ao", str(nuclide.percent)) + xml_element.set("ao", str(val)) else: - xml_element.set("wo", str(nuclide.percent)) + xml_element.set("wo", str(val)) return xml_element diff --git a/tests/unit_tests/test_material.py b/tests/unit_tests/test_material.py index 94ba82571..c6a07cff9 100644 --- a/tests/unit_tests/test_material.py +++ b/tests/unit_tests/test_material.py @@ -683,3 +683,17 @@ def test_decay_photon_energy(): stable.add_nuclide('Gd156', 1.0) stable.volume = 1.0 assert stable.get_decay_photon_energy() is None + + +def test_avoid_subnormal(run_in_tmpdir): + # Write a materials.xml with a material that has a nuclide density that is + # represented as a subnormal floating point value + mat = openmc.Material() + mat.add_nuclide('H1', 1.0) + mat.add_nuclide('H2', 1.0e-315) + mats = openmc.Materials([mat]) + mats.export_to_xml() + + # When read back in, the density should be zero + mats = openmc.Materials.from_xml() + assert mats[0].get_nuclide_atom_densities()['H2'] == 0.0 From 04ecf5490796ce023192076e35e8ffe815da8f62 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 11 Oct 2024 06:13:10 -0500 Subject: [PATCH 188/671] Improve clipping of Mixture distributions (#3154) Co-authored-by: Ethan Peterson --- openmc/material.py | 7 +- openmc/stats/univariate.py | 126 ++++++++++++------ src/distribution.cpp | 3 + tests/regression_tests/source/inputs_true.dat | 18 +-- tests/unit_tests/test_stats.py | 18 ++- 5 files changed, 122 insertions(+), 50 deletions(-) diff --git a/openmc/material.py b/openmc/material.py index 63994b305..1213ea669 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -326,7 +326,7 @@ class Material(IDManagerMixin): probs = [] for nuc, atoms_per_bcm in self.get_nuclide_atom_densities().items(): source_per_atom = openmc.data.decay_photon_energy(nuc) - if source_per_atom is not None: + if source_per_atom is not None and atoms_per_bcm > 0.0: dists.append(source_per_atom) probs.append(1e24 * atoms_per_bcm * multiplier) @@ -339,6 +339,11 @@ class Material(IDManagerMixin): if isinstance(combined, (Discrete, Mixture)): combined.clip(clip_tolerance, inplace=True) + # If clipping resulted in a single distribution within a mixture, pick + # out that single distribution + if isinstance(combined, Mixture) and len(combined.distribution) == 1: + combined = combined.distribution[0] + return combined @classmethod diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index 10822c06f..0dc6f3856 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -97,6 +97,45 @@ class Univariate(EqualityMixin, ABC): return 1.0 +def _intensity_clip(intensity: Sequence[float], tolerance: float = 1e-6) -> np.ndarray: + """Clip low-importance points from an array of intensities. + + Given an array of intensities, this function returns an array of indices for + points that contribute non-negligibly to the total sum of intensities. + + Parameters + ---------- + intensity : sequence of float + Intensities in arbitrary units. + tolerance : float + Maximum fraction of intensities that will be discarded. + + Returns + ------- + Array of indices + + """ + # Get indices of intensities from largest to smallest + index_sort = np.argsort(intensity)[::-1] + + # Get intensities from largest to smallest + sorted_intensity = np.asarray(intensity)[index_sort] + + # Determine cumulative sum of probabilities + cumsum = np.cumsum(sorted_intensity) + cumsum /= cumsum[-1] + + # Find index that satisfies cutoff + index_cutoff = np.searchsorted(cumsum, 1.0 - tolerance) + + # Now get indices up to cutoff + new_indices = index_sort[:index_cutoff + 1] + + # Put back in the order of the original array and return + new_indices.sort() + return new_indices + + class Discrete(Univariate): """Distribution characterized by a probability mass function. @@ -283,32 +322,20 @@ class Discrete(Univariate): cv.check_less_than("tolerance", tolerance, 1.0, equality=True) cv.check_greater_than("tolerance", tolerance, 0.0, equality=True) - # Determine (reversed) sorted order of probabilities + # Compute intensities intensity = self.p * self.x - index_sort = np.argsort(intensity)[::-1] - # Get probabilities in above order - sorted_intensity = intensity[index_sort] - - # Determine cumulative sum of probabilities - cumsum = np.cumsum(sorted_intensity) - cumsum /= cumsum[-1] - - # Find index which satisfies cutoff - index_cutoff = np.searchsorted(cumsum, 1.0 - tolerance) - - # Now get indices up to cutoff - new_indices = index_sort[:index_cutoff + 1] - new_indices.sort() + # Get indices for intensities above threshold + indices = _intensity_clip(intensity, tolerance=tolerance) # Create new discrete distribution if inplace: - self.x = self.x[new_indices] - self.p = self.p[new_indices] + self.x = self.x[indices] + self.p = self.p[indices] return self else: - new_x = self.x[new_indices] - new_p = self.p[new_indices] + new_x = self.x[indices] + new_p = self.p[indices] return type(self)(new_x, new_p) @@ -1206,7 +1233,7 @@ class Mixture(Univariate): for p in probability: cv.check_greater_than('mixture distribution probabilities', p, 0.0, True) - self._probability = probability + self._probability = np.array(probability, dtype=float) @property def distribution(self): @@ -1312,40 +1339,63 @@ class Mixture(Univariate): ]) def clip(self, tolerance: float = 1e-6, inplace: bool = False) -> Mixture: - r"""Remove low-importance points from contained discrete distributions. + r"""Remove low-importance points / distributions - Given a probability mass function :math:`p(x)` with :math:`\{x_1, x_2, - x_3, \dots\}` the possible values of the random variable with - corresponding probabilities :math:`\{p_1, p_2, p_3, \dots\}`, this - function will remove any low-importance points such that :math:`\sum_i - x_i p_i` is preserved to within some threshold. + Like :meth:`Discrete.clip`, this method will remove low-importance + points from discrete distributions contained within the mixture but it + will also clip any distributions that have negligible contributions to + the overall intensity. .. versionadded:: 0.14.0 Parameters ---------- tolerance : float - Maximum fraction of :math:`\sum_i x_i p_i` that will be discarded - for any discrete distributions within the mixture distribution. + Maximum fraction of intensities that will be discarded. inplace : bool Whether to modify the current object in-place or return a new one. Returns ------- - Discrete distribution with low-importance points removed + Distribution with low-importance points / distributions removed """ + # Determine integral of original distribution to compare later + original_integral = self.integral() + + # Determine indices for any distributions that contribute non-negligibly + # to overall intensity + intensities = [prob*dist.integral() for prob, dist in + zip(self.probability, self.distribution)] + indices = _intensity_clip(intensities, tolerance=tolerance) + + # Clip mixture of distributions + probability = self.probability[indices] + distribution = [self.distribution[i] for i in indices] + + # Clip points from Discrete distributions + distribution = [ + dist.clip(tolerance, inplace) if isinstance(dist, Discrete) else dist + for dist in distribution + ] + if inplace: - for dist in self.distribution: - if isinstance(dist, Discrete): - dist.clip(tolerance, inplace=True) - return self + # Set attributes of current object and return + self.probability = probability + self.distribution = distribution + new_dist = self else: - distribution = [ - dist.clip(tolerance) if isinstance(dist, Discrete) else dist - for dist in self.distribution - ] - return type(self)(self.probability, distribution) + # Create new distribution + new_dist = type(self)(probability, distribution) + + # Show warning if integral of new distribution is not within + # tolerance of original + diff = (original_integral - new_dist.integral())/original_integral + if diff > tolerance: + warn("Clipping mixture distribution resulted in an integral that is " + f"lower by a fraction of {diff} when tolerance={tolerance}.") + + return new_dist def combine_distributions( diff --git a/src/distribution.cpp b/src/distribution.cpp index 3026630b3..a6b4acd58 100644 --- a/src/distribution.cpp +++ b/src/distribution.cpp @@ -394,6 +394,9 @@ Mixture::Mixture(pugi::xml_node node) distribution_.push_back(std::make_pair(cumsum, std::move(dist))); } + // Save integral of distribution + integral_ = cumsum; + // Normalize cummulative probabilities to 1 for (auto& pair : distribution_) { pair.first /= cumsum; diff --git a/tests/regression_tests/source/inputs_true.dat b/tests/regression_tests/source/inputs_true.dat index c1a616dd1..e787f24d4 100644 --- a/tests/regression_tests/source/inputs_true.dat +++ b/tests/regression_tests/source/inputs_true.dat @@ -85,13 +85,13 @@ - + - + - + 1.0 1.3894954943731377 1.93069772888325 2.6826957952797255 3.72759372031494 5.17947467923121 7.196856730011519 10.0 13.894954943731374 19.306977288832496 26.826957952797247 37.2759372031494 51.7947467923121 71.96856730011518 100.0 138.94954943731375 193.06977288832496 268.26957952797244 372.7593720314938 517.9474679231207 719.6856730011514 1000.0 1389.4954943731375 1930.6977288832495 2682.6957952797247 3727.593720314938 5179.474679231207 7196.856730011514 10000.0 13894.95494373136 19306.977288832495 26826.95795279722 37275.93720314938 51794.74679231213 71968.56730011514 100000.0 138949.5494373136 193069.77288832495 268269.5795279722 372759.3720314938 517947.4679231202 719685.6730011514 1000000.0 1389495.494373136 1930697.7288832497 2682695.7952797217 3727593.720314938 5179474.679231202 7196856.730011513 10000000.0 0.0 2.9086439299358713e-08 5.80533561806147e-08 8.67817193689187e-08 1.1515347785771536e-07 1.4305204600565115e-07 1.7036278261198208e-07 1.9697346200185813e-07 2.227747351856934e-07 2.4766057919761985e-07 2.715287327665956e-07 2.9428111652990295e-07 3.1582423606228735e-07 3.360695660646056e-07 3.549339141332686e-07 3.723397626156721e-07 3.882155871468592e-07 4.024961505584776e-07 4.151227709522976e-07 4.260435628367196e-07 4.3521365033538783e-07 4.4259535159179273e-07 4.4815833361210174e-07 4.5187973690993757e-07 4.5374426944091084e-07 4.5374426944091084e-07 4.5187973690993757e-07 4.4815833361210174e-07 4.4259535159179273e-07 4.352136503353879e-07 4.2604356283671966e-07 4.1512277095229767e-07 4.0249615055847764e-07 3.8821558714685926e-07 3.723397626156722e-07 3.5493391413326864e-07 3.360695660646057e-07 3.158242360622874e-07 2.942811165299031e-07 2.715287327665957e-07 2.4766057919762e-07 2.2277473518569352e-07 1.9697346200185819e-07 1.7036278261198226e-07 1.4305204600565126e-07 1.1515347785771556e-07 8.678171936891881e-08 5.805335618061493e-08 2.9086439299358858e-08 5.559621115282002e-23 @@ -108,13 +108,13 @@ - + - + - + 1.0 1.3894954943731377 1.93069772888325 2.6826957952797255 3.72759372031494 5.17947467923121 7.196856730011519 10.0 13.894954943731374 19.306977288832496 26.826957952797247 37.2759372031494 51.7947467923121 71.96856730011518 100.0 138.94954943731375 193.06977288832496 268.26957952797244 372.7593720314938 517.9474679231207 719.6856730011514 1000.0 1389.4954943731375 1930.6977288832495 2682.6957952797247 3727.593720314938 5179.474679231207 7196.856730011514 10000.0 13894.95494373136 19306.977288832495 26826.95795279722 37275.93720314938 51794.74679231213 71968.56730011514 100000.0 138949.5494373136 193069.77288832495 268269.5795279722 372759.3720314938 517947.4679231202 719685.6730011514 1000000.0 1389495.494373136 1930697.7288832497 2682695.7952797217 3727593.720314938 5179474.679231202 7196856.730011513 10000000.0 0.0 2.9086439299358713e-08 5.80533561806147e-08 8.67817193689187e-08 1.1515347785771536e-07 1.4305204600565115e-07 1.7036278261198208e-07 1.9697346200185813e-07 2.227747351856934e-07 2.4766057919761985e-07 2.715287327665956e-07 2.9428111652990295e-07 3.1582423606228735e-07 3.360695660646056e-07 3.549339141332686e-07 3.723397626156721e-07 3.882155871468592e-07 4.024961505584776e-07 4.151227709522976e-07 4.260435628367196e-07 4.3521365033538783e-07 4.4259535159179273e-07 4.4815833361210174e-07 4.5187973690993757e-07 4.5374426944091084e-07 4.5374426944091084e-07 4.5187973690993757e-07 4.4815833361210174e-07 4.4259535159179273e-07 4.352136503353879e-07 4.2604356283671966e-07 4.1512277095229767e-07 4.0249615055847764e-07 3.8821558714685926e-07 3.723397626156722e-07 3.5493391413326864e-07 3.360695660646057e-07 3.158242360622874e-07 2.942811165299031e-07 2.715287327665957e-07 2.4766057919762e-07 2.2277473518569352e-07 1.9697346200185819e-07 1.7036278261198226e-07 1.4305204600565126e-07 1.1515347785771556e-07 8.678171936891881e-08 5.805335618061493e-08 2.9086439299358858e-08 5.559621115282002e-23 @@ -132,13 +132,13 @@ - + - + - + 1.0 1.3894954943731377 1.93069772888325 2.6826957952797255 3.72759372031494 5.17947467923121 7.196856730011519 10.0 13.894954943731374 19.306977288832496 26.826957952797247 37.2759372031494 51.7947467923121 71.96856730011518 100.0 138.94954943731375 193.06977288832496 268.26957952797244 372.7593720314938 517.9474679231207 719.6856730011514 1000.0 1389.4954943731375 1930.6977288832495 2682.6957952797247 3727.593720314938 5179.474679231207 7196.856730011514 10000.0 13894.95494373136 19306.977288832495 26826.95795279722 37275.93720314938 51794.74679231213 71968.56730011514 100000.0 138949.5494373136 193069.77288832495 268269.5795279722 372759.3720314938 517947.4679231202 719685.6730011514 1000000.0 1389495.494373136 1930697.7288832497 2682695.7952797217 3727593.720314938 5179474.679231202 7196856.730011513 10000000.0 0.0 2.9086439299358713e-08 5.80533561806147e-08 8.67817193689187e-08 1.1515347785771536e-07 1.4305204600565115e-07 1.7036278261198208e-07 1.9697346200185813e-07 2.227747351856934e-07 2.4766057919761985e-07 2.715287327665956e-07 2.9428111652990295e-07 3.1582423606228735e-07 3.360695660646056e-07 3.549339141332686e-07 3.723397626156721e-07 3.882155871468592e-07 4.024961505584776e-07 4.151227709522976e-07 4.260435628367196e-07 4.3521365033538783e-07 4.4259535159179273e-07 4.4815833361210174e-07 4.5187973690993757e-07 4.5374426944091084e-07 4.5374426944091084e-07 4.5187973690993757e-07 4.4815833361210174e-07 4.4259535159179273e-07 4.352136503353879e-07 4.2604356283671966e-07 4.1512277095229767e-07 4.0249615055847764e-07 3.8821558714685926e-07 3.723397626156722e-07 3.5493391413326864e-07 3.360695660646057e-07 3.158242360622874e-07 2.942811165299031e-07 2.715287327665957e-07 2.4766057919762e-07 2.2277473518569352e-07 1.9697346200185819e-07 1.7036278261198226e-07 1.4305204600565126e-07 1.1515347785771556e-07 8.678171936891881e-08 5.805335618061493e-08 2.9086439299358858e-08 5.559621115282002e-23 diff --git a/tests/unit_tests/test_stats.py b/tests/unit_tests/test_stats.py index 0414fc225..643e11556 100644 --- a/tests/unit_tests/test_stats.py +++ b/tests/unit_tests/test_stats.py @@ -262,7 +262,7 @@ def test_mixture(): d2 = openmc.stats.Uniform(3, 7) p = [0.5, 0.5] mix = openmc.stats.Mixture(p, [d1, d2]) - assert mix.probability == p + np.testing.assert_allclose(mix.probability, p) assert mix.distribution == [d1, d2] assert len(mix) == 4 @@ -274,7 +274,7 @@ def test_mixture(): elem = mix.to_xml_element('distribution') d = openmc.stats.Mixture.from_xml_element(elem) - assert d.probability == p + np.testing.assert_allclose(d.probability, p) assert d.distribution == [d1, d2] assert len(d) == 4 @@ -296,6 +296,20 @@ def test_mixture_clip(): mix_same = mix.clip(1e-6, inplace=True) assert mix_same is mix + # Make sure clip removes low probability distributions + d_small = openmc.stats.Uniform(0., 1.) + d_large = openmc.stats.Uniform(2., 5.) + mix = openmc.stats.Mixture([1e-10, 1.0], [d_small, d_large]) + mix_clip = mix.clip(1e-3) + assert mix_clip.distribution == [d_large] + + # Make sure warning is raised if tolerance is exceeded + d1 = openmc.stats.Discrete([1.0, 1.001], [1.0, 0.7e-6]) + d2 = openmc.stats.Tabular([0.0, 1.0], [0.7e-6], interpolation='histogram') + mix = openmc.stats.Mixture([1.0, 1.0], [d1, d2]) + with pytest.warns(UserWarning): + mix_clip = mix.clip(1e-6) + def test_polar_azimuthal(): # default polar-azimuthal should be uniform in mu and phi From fc3de1cbef87780fbdd5d591d80fa54484a1882a Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 11 Oct 2024 06:15:15 -0500 Subject: [PATCH 189/671] Add ConicalFrustum composite surface (#3151) Co-authored-by: Ethan Peterson --- docs/source/pythonapi/model.rst | 1 + openmc/model/surface_composite.py | 120 +++++++++++++++++++++ tests/unit_tests/test_surface_composite.py | 44 ++++++++ 3 files changed, 165 insertions(+) diff --git a/docs/source/pythonapi/model.rst b/docs/source/pythonapi/model.rst index 21944018e..e7d6d320f 100644 --- a/docs/source/pythonapi/model.rst +++ b/docs/source/pythonapi/model.rst @@ -22,6 +22,7 @@ Composite Surfaces :nosignatures: :template: myclass.rst + openmc.model.ConicalFrustum openmc.model.CruciformPrism openmc.model.CylinderSector openmc.model.HexagonalPrism diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index e2a5f49c4..df2903296 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -1717,3 +1717,123 @@ class HexagonalPrism(CompositeSurface): prism &= ~corners return prism + + +def _rotation_matrix(v1, v2): + """Compute rotation matrix that would rotate v1 into v2. + + Parameters + ---------- + v1 : numpy.ndarray + Unrotated vector + v2 : numpy.ndarray + Rotated vector + + Returns + ------- + 3x3 rotation matrix + + """ + # Normalize vectors and compute cosine + u1 = v1 / np.linalg.norm(v1) + u2 = v2 / np.linalg.norm(v2) + cos_angle = np.dot(u1, u2) + + I = np.identity(3) + + # Handle special case where vectors are parallel or anti-parallel + if isclose(abs(cos_angle), 1.0, rel_tol=1e-8): + return np.sign(cos_angle)*I + else: + # Calculate rotation angle + sin_angle = np.sqrt(1 - cos_angle*cos_angle) + + # Calculate axis of rotation + axis = np.cross(u1, u2) + axis /= np.linalg.norm(axis) + + # Create cross-product matrix K + kx, ky, kz = axis + K = np.array([ + [0.0, -kz, ky], + [kz, 0.0, -kx], + [-ky, kx, 0.0] + ]) + + # Create rotation matrix using Rodrigues' rotation formula + return I + K * sin_angle + (K @ K) * (1 - cos_angle) + + +class ConicalFrustum(CompositeSurface): + """Conical frustum. + + A conical frustum, also known as a right truncated cone, is a cone that is + truncated by two parallel planes that are perpendicular to the axis of the + cone. The lower and upper base of the conical frustum are circular faces. + This surface is equivalent to the TRC macrobody in MCNP. + + .. versionadded:: 0.15.1 + + Parameters + ---------- + center_base : iterable of float + Cartesian coordinates of the center of the bottom planar face. + axis : iterable of float + Vector from the center of the bottom planar face to the center of the + top planar face that defines the axis of the cone. The length of this + vector is the height of the conical frustum. + r1 : float + Radius of the lower cone base + r2 : float + Radius of the upper cone base + **kwargs + Keyword arguments passed to underlying plane classes + + Attributes + ---------- + cone : openmc.Cone + Cone surface + plane_bottom : openmc.Plane + Plane surface defining the bottom of the frustum + plane_top : openmc.Plane + Plane surface defining the top of the frustum + + """ + _surface_names = ('cone', 'plane_bottom', 'plane_top') + + def __init__(self, center_base: Sequence[float], axis: Sequence[float], + r1: float, r2: float, **kwargs): + center_base = np.array(center_base) + axis = np.array(axis) + + # Determine length of axis height vector + h = np.linalg.norm(axis) + + # To create the frustum oriented with the correct axis, first we will + # create a cone along the z axis and then rotate it according to the + # given axis. Thus, we first need to determine the apex using the z axis + # as a reference. + x0, y0, z0 = center_base + if r1 != r2: + apex = z0 + r1*h/(r1 - r2) + r_sq = ((r1 - r2)/h)**2 + cone = openmc.ZCone(x0, y0, apex, r2=r_sq, **kwargs) + else: + # In the degenerate case r1 == r2, the cone becomes a cylinder + cone = openmc.ZCylinder(x0, y0, r1, **kwargs) + + # Create the parallel planes + plane_bottom = openmc.ZPlane(z0, **kwargs) + plane_top = openmc.ZPlane(z0 + h, **kwargs) + + # Determine rotation matrix corresponding to specified axis + u = np.array([0., 0., 1.]) + rotation = _rotation_matrix(u, axis) + + # Rotate the surfaces + self.cone = cone.rotate(rotation, pivot=center_base) + self.plane_bottom = plane_bottom.rotate(rotation, pivot=center_base) + self.plane_top = plane_top.rotate(rotation, pivot=center_base) + + def __neg__(self) -> openmc.Region: + return +self.plane_bottom & -self.plane_top & -self.cone diff --git a/tests/unit_tests/test_surface_composite.py b/tests/unit_tests/test_surface_composite.py index d862ae6b0..963bbe00d 100644 --- a/tests/unit_tests/test_surface_composite.py +++ b/tests/unit_tests/test_surface_composite.py @@ -552,3 +552,47 @@ def test_box(): assert (0., 0.9, 0.) in -s assert (0., 0., -3.) not in +s assert (0., 0., 3.) not in +s + + +def test_conical_frustum(): + center_base = (0.0, 0.0, -3) + axis = (0., 0., 3.) + r1 = 2.0 + r2 = 0.5 + s = openmc.model.ConicalFrustum(center_base, axis, r1, r2) + assert isinstance(s.cone, openmc.Cone) + assert isinstance(s.plane_bottom, openmc.Plane) + assert isinstance(s.plane_top, openmc.Plane) + + # Make sure boundary condition propagates + s.boundary_type = 'reflective' + assert s.boundary_type == 'reflective' + assert s.cone.boundary_type == 'reflective' + assert s.plane_bottom.boundary_type == 'reflective' + assert s.plane_top.boundary_type == 'reflective' + + # Check bounding box + ll, ur = (+s).bounding_box + assert np.all(np.isinf(ll)) + assert np.all(np.isinf(ur)) + ll, ur = (-s).bounding_box + assert ll[2] == pytest.approx(-3.0) + assert ur[2] == pytest.approx(0.0) + + # __contains__ on associated half-spaces + assert (0., 0., -1.) in -s + assert (0., 0., -4.) not in -s + assert (0., 0., 1.) not in -s + assert (1., 1., -2.99) in -s + assert (1., 1., -0.01) in +s + + # translate method + s_t = s.translate((1., 1., 0.)) + assert (1., 1., -0.01) in -s_t + + # Make sure repr works + repr(s) + + # Denegenerate case with r1 = r2 + s = openmc.model.ConicalFrustum(center_base, axis, r1, r1) + assert (1., 1., -0.01) in -s From 8263f05e7ed62290a1a3891ea9ff81f05b13f955 Mon Sep 17 00:00:00 2001 From: Soha <120593053+sohhae@users.noreply.github.com> Date: Fri, 11 Oct 2024 23:11:27 -0500 Subject: [PATCH 190/671] Update quickinstall instructions for macOS (#3130) Co-authored-by: Paul Romano --- docs/source/quickinstall.rst | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/source/quickinstall.rst b/docs/source/quickinstall.rst index 7f222f77c..323cd7fd4 100644 --- a/docs/source/quickinstall.rst +++ b/docs/source/quickinstall.rst @@ -137,12 +137,11 @@ packages should be installed, for example in Homebrew via: 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 -by OpenMC. Consequently, the C++ compiler should explicitly be set before -proceeding: - -.. code-block:: sh - - export CXX=/opt/homebrew/opt/llvm/bin/clang++ +by OpenMC. To ensure CMake picks up the correct compiler, make sure that either +the :envvar:`CXX` environment variable is set to the brew-installed ``clang++`` +or that the directory containing it is on your :envvar:`PATH` environment +variable. Common locations for the brew-installed compiler are +``/opt/homebrew/opt/llvm/bin`` and ``/usr/local/opt/llvm/bin``. After the packages have been installed, follow the instructions to build from source below. From dcb25575ca0691ddde36af5141376c29aab884bc Mon Sep 17 00:00:00 2001 From: Lorenzo Chierici Date: Mon, 14 Oct 2024 21:47:22 +0200 Subject: [PATCH 191/671] avoid zero division if source rate of previous result is zero (#3169) --- openmc/deplete/abc.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 7bf7f1086..784023f26 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -754,8 +754,9 @@ class Integrator(ABC): rates = res.rates[0] k = ufloat(res.k[0, 0], res.k[0, 1]) - # Scale reaction rates by ratio of source rates - rates *= source_rate / res.source_rate + if res.source_rate != 0.0: + # Scale reaction rates by ratio of source rates + rates *= source_rate / res.source_rate return bos_conc, OperatorResult(k, rates) def _get_start_data(self): From c19b9b1beb41ba807e4b79a413b15209ef5127b6 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Sat, 19 Oct 2024 00:12:18 +0100 Subject: [PATCH 192/671] added subfolders to txt search command in pyproject (#3174) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 39aa261c1..d0419d550 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ include = ['openmc*', 'scripts*'] exclude = ['tests*'] [tool.setuptools.package-data] -"openmc.data.effective_dose" = ["*.txt"] +"openmc.data.effective_dose" = ["**/*.txt"] "openmc.data" = ["*.txt", "*.DAT", "*.json", "*.h5"] "openmc.lib" = ["libopenmc.dylib", "libopenmc.so"] From 82a6f9e40b66dccca225a97202f4e8fce1b193cd Mon Sep 17 00:00:00 2001 From: Ahnaf Tahmid Chowdhury Date: Sat, 19 Oct 2024 05:18:16 +0600 Subject: [PATCH 193/671] Update `fmt` Formatters for Compatibility with Versions below 11 (#3172) --- include/openmc/output.h | 10 +++++++--- include/openmc/position.h | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/include/openmc/output.h b/include/openmc/output.h index 549d2ae32..940ea78ce 100644 --- a/include/openmc/output.h +++ b/include/openmc/output.h @@ -76,10 +76,14 @@ struct formatter> { } template - auto format(const std::array& arr, FormatContext& ctx) const +#if FMT_VERSION >= 110000 // Version 11.0.0 and above + auto format(const std::array& arr, FormatContext& ctx) const { +#else // For versions below 11.0.0 + auto format(const std::array& arr, FormatContext& ctx) { +#endif return format_to(ctx.out(), "({}, {})", arr[0], arr[1]); - } -}; +} +}; // namespace fmt } // namespace fmt diff --git a/include/openmc/position.h b/include/openmc/position.h index e6939dc0e..dc60ab35a 100644 --- a/include/openmc/position.h +++ b/include/openmc/position.h @@ -221,12 +221,16 @@ namespace fmt { template<> struct formatter : formatter { template - auto format(const openmc::Position& pos, FormatContext& ctx) const +#if FMT_VERSION >= 110000 // Version 11.0.0 and above + auto format(const openmc::Position& pos, FormatContext& ctx) const { +#else // For versions below 11.0.0 + auto format(const openmc::Position& pos, FormatContext& ctx) { +#endif return formatter::format( fmt::format("({}, {}, {})", pos.x, pos.y, pos.z), ctx); - } -}; +} +}; // namespace fmt } // namespace fmt From 9c9a13c48ea7dbb2984467b287314d849a1dca18 Mon Sep 17 00:00:00 2001 From: Rayan HADDAD <103775910+rayanhaddad169@users.noreply.github.com> Date: Mon, 28 Oct 2024 17:28:59 +0100 Subject: [PATCH 194/671] enable polymorphisme for mix_materials (#3180) Co-authored-by: r.haddad --- openmc/material.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmc/material.py b/openmc/material.py index 1213ea669..a6401216a 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -1536,7 +1536,7 @@ class Material(IDManagerMixin): if name is None: name = '-'.join([f'{m.name}({f})' for m, f in zip(materials, fracs)]) - new_mat = openmc.Material(name=name) + new_mat = cls(name=name) # Compute atom fractions of nuclides and add them to the new material tot_nuclides_per_cc = np.sum([dens for dens in nuclides_per_cc.values()]) From 7552123c762a13be49269bf3c5bf393635109766 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Tue, 29 Oct 2024 18:05:40 +0100 Subject: [PATCH 195/671] added list to doc string arg for plot_xs (#3178) --- openmc/plotter.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/openmc/plotter.py b/openmc/plotter.py index 8b205b79d..c3b951ad8 100644 --- a/openmc/plotter.py +++ b/openmc/plotter.py @@ -1,5 +1,10 @@ +# annotations package is required to enable postponed evaluation of type +# hints in conjugation with the | operator. This avoids TypeError: +# unsupported operand type(s) for |: 'str' and 'NoneType' error +from __future__ import annotations from itertools import chain from numbers import Integral, Real +from typing import Dict, Iterable, List import numpy as np @@ -120,18 +125,29 @@ def _get_title(reactions): return 'Cross Section Plot' -def plot_xs(reactions, divisor_types=None, temperature=294., axis=None, - sab_name=None, ce_cross_sections=None, mg_cross_sections=None, - enrichment=None, plot_CE=True, orders=None, divisor_orders=None, - energy_axis_units="eV", **kwargs): +def plot_xs( + reactions: Dict[str, openmc.Material | List[str]], + divisor_types: Iterable[str] | None = None, + temperature: float = 294.0, + axis: "plt.Axes" | None = None, + sab_name: str | None = None, + ce_cross_sections: str | None = None, + mg_cross_sections: str | None = None, + enrichment: float | None = None, + plot_CE: bool = True, + orders: Iterable[int] | None = None, + divisor_orders: Iterable[int] | None = None, + energy_axis_units: str = "eV", + **kwargs, +) -> "plt.Figure": """Creates a figure of continuous-energy cross sections for this item. Parameters ---------- reactions : dict keys can be either a nuclide or element in string form or an - openmc.Material object. Values are the type of cross sections to - include in the plot. + openmc.Material object. Values are a list of the types of + cross sections to include in the plot. divisor_types : Iterable of values of PLOT_TYPES, optional Cross section types which will divide those produced by types before plotting. A type of 'unity' can be used to effectively not From 339d78c5fae8ba3a815ae10097238d0099a142c4 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 6 Nov 2024 05:59:25 -0600 Subject: [PATCH 196/671] Fix plot_xs type hint (#3184) --- openmc/plotter.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/openmc/plotter.py b/openmc/plotter.py index c3b951ad8..85c4963a7 100644 --- a/openmc/plotter.py +++ b/openmc/plotter.py @@ -1,6 +1,3 @@ -# annotations package is required to enable postponed evaluation of type -# hints in conjugation with the | operator. This avoids TypeError: -# unsupported operand type(s) for |: 'str' and 'NoneType' error from __future__ import annotations from itertools import chain from numbers import Integral, Real @@ -126,7 +123,7 @@ def _get_title(reactions): def plot_xs( - reactions: Dict[str, openmc.Material | List[str]], + reactions: Dict[str | openmc.Material, List[str]], divisor_types: Iterable[str] | None = None, temperature: float = 294.0, axis: "plt.Axes" | None = None, @@ -139,7 +136,7 @@ def plot_xs( divisor_orders: Iterable[int] | None = None, energy_axis_units: str = "eV", **kwargs, -) -> "plt.Figure": +) -> "plt.Figure" | None: """Creates a figure of continuous-energy cross sections for this item. Parameters @@ -291,7 +288,6 @@ def plot_xs( return fig - def calculate_cexs(this, types, temperature=294., sab_name=None, cross_sections=None, enrichment=None, ncrystal_cfg=None): """Calculates continuous-energy cross sections of a requested type. From 754f6fa44ab7c1bbb55cdaacc54aff642f1446f5 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Fri, 8 Nov 2024 15:18:15 -0600 Subject: [PATCH 197/671] Reset values of lattice offset tables when allocated (#3188) --- include/openmc/lattice.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/openmc/lattice.h b/include/openmc/lattice.h index 429d81ddd..fb374ab75 100644 --- a/include/openmc/lattice.h +++ b/include/openmc/lattice.h @@ -71,7 +71,8 @@ public: //! Allocate offset table for distribcell. void allocate_offset_table(int n_maps) { - offsets_.resize(n_maps * universes_.size(), C_NONE); + offsets_.resize(n_maps * universes_.size()); + std::fill(offsets_.begin(), offsets_.end(), C_NONE); } //! Populate the distribcell offset tables. From 9983ee1a7eb9b95bd45123a9d44a021f789f0410 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Sat, 9 Nov 2024 00:52:40 +0100 Subject: [PATCH 198/671] allowing varible offsets for polygon.offset (#3120) Co-authored-by: Paul Romano --- openmc/model/surface_composite.py | 27 ++++++++++++++++++---- tests/unit_tests/test_surface_composite.py | 11 +++++---- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index df2903296..7a5ab8f10 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -1,3 +1,4 @@ +from __future__ import annotations from abc import ABC, abstractmethod from collections.abc import Iterable, Sequence from copy import copy @@ -1316,25 +1317,43 @@ class Polygon(CompositeSurface): surfsets.append(surf_ops) return surfsets - def offset(self, distance): + def offset(self, distance: float | Sequence[float] | np.ndarray) -> Polygon: """Offset this polygon by a set distance Parameters ---------- - distance : float + distance : float or sequence of float or np.ndarray The distance to offset the polygon by. Positive is outward - (expanding) and negative is inward (shrinking). + (expanding) and negative is inward (shrinking). If a float is + provided, the same offset is applied to all vertices. If a list or + tuple is provided, each vertex gets a different offset. If an + iterable or numpy array is provided, each vertex gets a different + offset. Returns ------- offset_polygon : openmc.model.Polygon """ + + if isinstance(distance, float): + distance = np.full(len(self.points), distance) + elif isinstance(distance, Sequence): + distance = np.array(distance) + elif not isinstance(distance, np.ndarray): + raise TypeError("Distance must be a float or sequence of float.") + + if len(distance) != len(self.points): + raise ValueError( + f"Length of distance {len(distance)} array must " + f"match number of polygon points {len(self.points)}" + ) + normals = np.insert(self._normals, 0, self._normals[-1, :], axis=0) cos2theta = np.sum(normals[1:, :]*normals[:-1, :], axis=-1, keepdims=True) costheta = np.cos(np.arccos(cos2theta) / 2) nvec = (normals[1:, :] + normals[:-1, :]) unit_nvec = nvec / np.linalg.norm(nvec, axis=-1, keepdims=True) - disp_vec = distance / costheta * unit_nvec + disp_vec = distance[:, np.newaxis] / costheta * unit_nvec return type(self)(self.points + disp_vec, basis=self.basis) diff --git a/tests/unit_tests/test_surface_composite.py b/tests/unit_tests/test_surface_composite.py index 963bbe00d..015ac667e 100644 --- a/tests/unit_tests/test_surface_composite.py +++ b/tests/unit_tests/test_surface_composite.py @@ -347,10 +347,13 @@ def test_polygon(): assert any([points_in[i] in reg for reg in star_poly.regions]) assert points_in[i] not in +star_poly assert (0, 0, 0) not in -star_poly - if basis != 'rz': - offset_star = star_poly.offset(.6) - assert (0, 0, 0) in -offset_star - assert any([(0, 0, 0) in reg for reg in offset_star.regions]) + if basis != "rz": + for offsets in [0.6, np.array([0.6] * 10), [0.6] * 10]: + offset_star = star_poly.offset(offsets) + assert (0, 0, 0) in -offset_star + assert any([(0, 0, 0) in reg for reg in offset_star.regions]) + with pytest.raises(ValueError): + star_poly.offset([0.6, 0.6]) # check invalid Polygon input points # duplicate points not just at start and end From 70807b146fedecd47808a9541a3ab96ca529fd20 Mon Sep 17 00:00:00 2001 From: azimG Date: Sat, 9 Nov 2024 17:56:37 +0100 Subject: [PATCH 199/671] Update surface_composite.py (#3189) --- openmc/model/surface_composite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index 7a5ab8f10..cc570cc56 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -27,7 +27,7 @@ class CompositeSurface(ABC): return surf def rotate(self, rotation, pivot=(0., 0., 0.), order='xyz', inplace=False): - surf = copy(self) + surf = self if inplace else copy(self) for name in self._surface_names: s = getattr(surf, name) setattr(surf, name, s.rotate(rotation, pivot, order, inplace)) From c05132cb0de27f9a5db39b7cb5a9882439c823d1 Mon Sep 17 00:00:00 2001 From: Nicolas Linden Date: Sat, 9 Nov 2024 20:33:14 +0100 Subject: [PATCH 200/671] add export_model_xml arguments to Model.plot_geometry and Model.calculate_volumes (#3190) Co-authored-by: Nicolas Linden Co-authored-by: Paul Romano --- openmc/model/model.py | 41 +++++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index 78f743a03..0ea6db8db 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -719,7 +719,8 @@ class Model: def calculate_volumes(self, threads=None, output=True, cwd='.', openmc_exec='openmc', mpi_args=None, - apply_volumes=True): + apply_volumes=True, export_model_xml=True, + **export_kwargs): """Runs an OpenMC stochastic volume calculation and, if requested, applies volumes to the model @@ -748,6 +749,13 @@ class Model: apply_volumes : bool, optional Whether apply the volume calculation results from this calculation to the model. Defaults to applying the volumes. + export_model_xml : bool, optional + Exports a single model.xml file rather than separate files. Defaults + to True. + **export_kwargs + Keyword arguments passed to either :meth:`Model.export_to_model_xml` + or :meth:`Model.export_to_xml`. + """ if len(self.settings.volume_calculations) == 0: @@ -769,10 +777,15 @@ class Model: openmc.lib.calculate_volumes(output) else: - self.export_to_xml() - openmc.calculate_volumes(threads=threads, output=output, - openmc_exec=openmc_exec, - mpi_args=mpi_args) + if export_model_xml: + self.export_to_model_xml(**export_kwargs) + else: + self.export_to_xml(**export_kwargs) + path_input = export_kwargs.get("path", None) + openmc.calculate_volumes( + threads=threads, output=output, openmc_exec=openmc_exec, + mpi_args=mpi_args, path_input=path_input + ) # Now we apply the volumes if apply_volumes: @@ -909,7 +922,8 @@ class Model: n_samples=n_samples, prn_seed=prn_seed ) - def plot_geometry(self, output=True, cwd='.', openmc_exec='openmc'): + def plot_geometry(self, output=True, cwd='.', openmc_exec='openmc', + export_model_xml=True, **export_kwargs): """Creates plot images as specified by the Model.plots attribute .. versionadded:: 0.13.0 @@ -924,6 +938,12 @@ class Model: openmc_exec : str, optional Path to OpenMC executable. Defaults to 'openmc'. This only applies to the case when not using the C API. + export_model_xml : bool, optional + Exports a single model.xml file rather than separate files. Defaults + to True. + **export_kwargs + Keyword arguments passed to either :meth:`Model.export_to_model_xml` + or :meth:`Model.export_to_xml`. """ @@ -937,8 +957,13 @@ class Model: # Compute the volumes openmc.lib.plot_geometry(output) else: - self.export_to_xml() - openmc.plot_geometry(output=output, openmc_exec=openmc_exec) + if export_model_xml: + self.export_to_model_xml(**export_kwargs) + else: + self.export_to_xml(**export_kwargs) + path_input = export_kwargs.get("path", None) + openmc.plot_geometry(output=output, openmc_exec=openmc_exec, + path_input=path_input) def _change_py_lib_attribs(self, names_or_ids, value, obj_type, attrib_name, density_units='atom/b-cm'): From c9207795d87cf4407c92bf054e1930b8c592be76 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 11 Nov 2024 16:03:31 -0600 Subject: [PATCH 201/671] Fixes in MicroXS.from_multigroup_flux (#3192) --- openmc/deplete/independent_operator.py | 5 +++-- openmc/deplete/microxs.py | 10 ++++++---- tests/unit_tests/test_deplete_decay.py | 4 ++-- tests/unit_tests/test_deplete_independent_operator.py | 6 +++--- tests/unit_tests/test_deplete_microxs.py | 8 +++++--- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/openmc/deplete/independent_operator.py b/openmc/deplete/independent_operator.py index cc1a05bd9..226682ffa 100644 --- a/openmc/deplete/independent_operator.py +++ b/openmc/deplete/independent_operator.py @@ -44,7 +44,7 @@ class IndependentOperator(OpenMCOperator): Parameters ---------- - materials : openmc.Materials + materials : iterable of openmc.Material Materials to deplete. fluxes : list of numpy.ndarray Flux in each group in [n-cm/src] for each domain @@ -127,8 +127,9 @@ class IndependentOperator(OpenMCOperator): reduce_chain_level=None, fission_yield_opts=None): # Validate micro-xs parameters - check_type('materials', materials, openmc.Materials) + check_type('materials', materials, Iterable, openmc.Material) check_type('micros', micros, Iterable, MicroXS) + materials = openmc.Materials(materials) if not (len(fluxes) == len(micros) == len(materials)): msg = (f'The length of fluxes ({len(fluxes)}) should be equal to ' diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index 5be9875f3..bbe9b2a43 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -198,6 +198,8 @@ class MicroXS: """ def __init__(self, data: np.ndarray, nuclides: list[str], reactions: list[str]): # Validate inputs + if len(data.shape) != 3: + raise ValueError('Data array must be 3D.') if data.shape[:2] != (len(nuclides), len(reactions)): raise ValueError( f'Nuclides list of length {len(nuclides)} and ' @@ -291,11 +293,11 @@ class MicroXS: mts = [REACTION_MT[name] for name in reactions] # Normalize multigroup flux - multigroup_flux = np.asarray(multigroup_flux) + multigroup_flux = np.array(multigroup_flux) multigroup_flux /= multigroup_flux.sum() - # Create 2D array for microscopic cross sections - microxs_arr = np.zeros((len(nuclides), len(mts))) + # Create 3D array for microscopic cross sections + microxs_arr = np.zeros((len(nuclides), len(mts), 1)) # Create a material with all nuclides mat_all_nucs = openmc.Material() @@ -327,7 +329,7 @@ class MicroXS: xs = lib_nuc.collapse_rate( mt, temperature, energies, multigroup_flux ) - microxs_arr[nuc_index, mt_index] = xs + microxs_arr[nuc_index, mt_index, 0] = xs return cls(microxs_arr, nuclides, reactions) diff --git a/tests/unit_tests/test_deplete_decay.py b/tests/unit_tests/test_deplete_decay.py index cebbc16fa..6e7b0b101 100644 --- a/tests/unit_tests/test_deplete_decay.py +++ b/tests/unit_tests/test_deplete_decay.py @@ -20,7 +20,7 @@ def test_deplete_decay_products(run_in_tmpdir): """) # Create MicroXS object with no cross sections - micro_xs = openmc.deplete.MicroXS(np.empty((0, 0)), [], []) + micro_xs = openmc.deplete.MicroXS(np.empty((0, 0, 0)), [], []) # Create depletion operator with no reactions op = openmc.deplete.IndependentOperator.from_nuclides( @@ -59,7 +59,7 @@ def test_deplete_decay_step_fissionable(run_in_tmpdir): """ # Set up a pure decay operator - micro_xs = openmc.deplete.MicroXS(np.empty((0, 0)), [], []) + micro_xs = openmc.deplete.MicroXS(np.empty((0, 0, 0)), [], []) mat = openmc.Material() mat.name = 'I do not decay.' mat.add_nuclide('U238', 1.0, 'ao') diff --git a/tests/unit_tests/test_deplete_independent_operator.py b/tests/unit_tests/test_deplete_independent_operator.py index 9129cf064..c765d0650 100644 --- a/tests/unit_tests/test_deplete_independent_operator.py +++ b/tests/unit_tests/test_deplete_independent_operator.py @@ -6,7 +6,7 @@ from pathlib import Path import pytest -from openmc import Material, Materials +from openmc import Material from openmc.deplete import IndependentOperator, MicroXS CHAIN_PATH = Path(__file__).parents[1] / "chain_simple.xml" @@ -34,7 +34,7 @@ def test_operator_init(): fuel.set_density("g/cc", 10.4) fuel.depletable = True fuel.volume = 1 - materials = Materials([fuel]) + materials = [fuel] fluxes = [1.0] micros = [micro_xs] IndependentOperator(materials, fluxes, micros, CHAIN_PATH) @@ -47,7 +47,7 @@ def test_error_handling(): fuel.set_density("g/cc", 1) fuel.depletable = True fuel.volume = 1 - materials = Materials([fuel]) + materials = [fuel] fluxes = [1.0, 2.0] micros = [micro_xs] with pytest.raises(ValueError, match=r"The length of fluxes \(2\)"): diff --git a/tests/unit_tests/test_deplete_microxs.py b/tests/unit_tests/test_deplete_microxs.py index ad54026f0..073b3f162 100644 --- a/tests/unit_tests/test_deplete_microxs.py +++ b/tests/unit_tests/test_deplete_microxs.py @@ -46,9 +46,7 @@ def test_from_array(): data.shape = (12, 2, 1) MicroXS(data, nuclides, reactions) - with pytest.raises(ValueError, match=r'Nuclides list of length \d* and ' - r'reactions array of length \d* do not ' - r'match dimensions of data array of shape \(\d*\, \d*\)'): + with pytest.raises(ValueError, match='Data array must be 3D'): MicroXS(data[:, 0], nuclides, reactions) @@ -96,9 +94,13 @@ def test_multigroup_flux_same(): energies = [0., 6.25e-1, 5.53e3, 8.21e5, 2.e7] flux_per_ev = [0.3, 0.3, 1.0, 1.0] flux = flux_per_ev * np.diff(energies) + flux_sum = flux.sum() microxs_4g = MicroXS.from_multigroup_flux( energies=energies, multigroup_flux=flux, chain_file=chain_file) + # from_multigroup_flux should not modify the flux + assert flux.sum() == flux_sum + # Generate micro XS based on 2-group flux, where the boundaries line up with # the 4 group flux and have the same flux per eV across the full energy # range From 3e2a042eb199bc49c0a9e21d06591d26134837cf Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Tue, 12 Nov 2024 14:58:15 -0600 Subject: [PATCH 202/671] Fix documentation typo in `boundary_type` (#3196) --- openmc/surface.py | 66 +++++++++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/openmc/surface.py b/openmc/surface.py index 537823ba1..c95025f94 100644 --- a/openmc/surface.py +++ b/openmc/surface.py @@ -119,7 +119,7 @@ class Surface(IDManagerMixin, ABC): surface_id : int, optional Unique identifier for the surface. If not specified, an identifier will automatically be assigned. - boundary_type : {'transmission, 'vacuum', 'reflective', 'periodic', 'white'}, optional + boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. Note that periodic boundary conditions @@ -135,7 +135,7 @@ class Surface(IDManagerMixin, ABC): Attributes ---------- - boundary_type : {'transmission, 'vacuum', 'reflective', 'periodic', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -683,7 +683,7 @@ class Plane(PlaneMixin, Surface): The 'C' parameter for the plane. Defaults to 0. d : float, optional The 'D' parameter for the plane. Defaults to 0. - boundary_type : {'transmission, 'vacuum', 'reflective', 'periodic', 'white'}, optional + boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. @@ -707,7 +707,7 @@ class Plane(PlaneMixin, Surface): The 'C' parameter for the plane d : float The 'D' parameter for the plane - boundary_type : {'transmission, 'vacuum', 'reflective', 'periodic', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -818,7 +818,7 @@ class XPlane(PlaneMixin, Surface): ---------- x0 : float, optional Location of the plane in [cm]. Defaults to 0. - boundary_type : {'transmission, 'vacuum', 'reflective', 'periodic', 'white'}, optional + boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. Only axis-aligned periodicity is @@ -837,7 +837,7 @@ class XPlane(PlaneMixin, Surface): ---------- x0 : float Location of the plane in [cm] - boundary_type : {'transmission, 'vacuum', 'reflective', 'periodic', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -883,7 +883,7 @@ class YPlane(PlaneMixin, Surface): ---------- y0 : float, optional Location of the plane in [cm] - boundary_type : {'transmission, 'vacuum', 'reflective', 'periodic', 'white'}, optional + boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. Only axis-aligned periodicity is @@ -902,7 +902,7 @@ class YPlane(PlaneMixin, Surface): ---------- y0 : float Location of the plane in [cm] - boundary_type : {'transmission, 'vacuum', 'reflective', 'periodic', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -948,7 +948,7 @@ class ZPlane(PlaneMixin, Surface): ---------- z0 : float, optional Location of the plane in [cm]. Defaults to 0. - boundary_type : {'transmission, 'vacuum', 'reflective', 'periodic', 'white'}, optional + boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. Only axis-aligned periodicity is @@ -967,7 +967,7 @@ class ZPlane(PlaneMixin, Surface): ---------- z0 : float Location of the plane in [cm] - boundary_type : {'transmission, 'vacuum', 'reflective', 'periodic', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -1197,7 +1197,7 @@ class Cylinder(QuadricMixin, Surface): dz : float, optional z-component of the vector representing the axis of the cylinder. Defaults to 1. - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'}, optional + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. @@ -1228,7 +1228,7 @@ class Cylinder(QuadricMixin, Surface): y-component of the vector representing the axis of the cylinder dz : float z-component of the vector representing the axis of the cylinder - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -1372,7 +1372,7 @@ class XCylinder(QuadricMixin, Surface): z-coordinate for the origin of the Cylinder in [cm]. Defaults to 0 r : float, optional Radius of the cylinder in [cm]. Defaults to 1. - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'}, optional + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. @@ -1395,7 +1395,7 @@ class XCylinder(QuadricMixin, Surface): z-coordinate for the origin of the Cylinder in [cm] r : float Radius of the cylinder in [cm] - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -1470,7 +1470,7 @@ class YCylinder(QuadricMixin, Surface): z-coordinate for the origin of the Cylinder in [cm]. Defaults to 0 r : float, optional Radius of the cylinder in [cm]. Defaults to 1. - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'}, optional + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. @@ -1493,7 +1493,7 @@ class YCylinder(QuadricMixin, Surface): z-coordinate for the origin of the Cylinder in [cm] r : float Radius of the cylinder in [cm] - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -1568,7 +1568,7 @@ class ZCylinder(QuadricMixin, Surface): y-coordinate for the origin of the Cylinder in [cm]. Defaults to 0 r : float, optional Radius of the cylinder in [cm]. Defaults to 1. - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'}, optional + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. @@ -1591,7 +1591,7 @@ class ZCylinder(QuadricMixin, Surface): y-coordinate for the origin of the Cylinder in [cm] r : float Radius of the cylinder in [cm] - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -1667,7 +1667,7 @@ class Sphere(QuadricMixin, Surface): z-coordinate of the center of the sphere in [cm]. Defaults to 0. r : float, optional Radius of the sphere in [cm]. Defaults to 1. - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'}, optional + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. @@ -1691,7 +1691,7 @@ class Sphere(QuadricMixin, Surface): z-coordinate of the center of the sphere in [cm] r : float Radius of the sphere in [cm] - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -1784,7 +1784,7 @@ class Cone(QuadricMixin, Surface): surface_id : int, optional Unique identifier for the surface. If not specified, an identifier will automatically be assigned. - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'}, optional + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. @@ -1812,7 +1812,7 @@ class Cone(QuadricMixin, Surface): y-component of the vector representing the axis of the cone. dz : float z-component of the vector representing the axis of the cone. - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -1930,7 +1930,7 @@ class XCone(QuadricMixin, Surface): Parameter related to the aperture [:math:`\\rm cm^2`]. It can be interpreted as the increase in the radius squared per cm along the cone's axis of revolution. - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'}, optional + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. @@ -1954,7 +1954,7 @@ class XCone(QuadricMixin, Surface): z-coordinate of the apex in [cm] r2 : float Parameter related to the aperature - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -2031,7 +2031,7 @@ class YCone(QuadricMixin, Surface): Parameter related to the aperture [:math:`\\rm cm^2`]. It can be interpreted as the increase in the radius squared per cm along the cone's axis of revolution. - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'}, optional + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. @@ -2055,7 +2055,7 @@ class YCone(QuadricMixin, Surface): z-coordinate of the apex in [cm] r2 : float Parameter related to the aperature - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -2132,7 +2132,7 @@ class ZCone(QuadricMixin, Surface): Parameter related to the aperature [cm^2]. This is the square of the radius of the cone 1 cm from. This can also be treated as the square of the slope of the cone relative to its axis. - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'}, optional + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. @@ -2156,7 +2156,7 @@ class ZCone(QuadricMixin, Surface): z-coordinate of the apex in [cm] r2 : float Parameter related to the aperature - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -2221,7 +2221,7 @@ class Quadric(QuadricMixin, Surface): ---------- a, b, c, d, e, f, g, h, j, k : float, optional coefficients for the surface. All default to 0. - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'}, optional + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. @@ -2239,7 +2239,7 @@ class Quadric(QuadricMixin, Surface): ---------- a, b, c, d, e, f, g, h, j, k : float coefficients for the surface - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -2391,7 +2391,7 @@ class XTorus(TorusMixin, Surface): Minor radius of the torus in [cm] (parallel to axis of revolution) c : float Minor radius of the torus in [cm] (perpendicular to axis of revolution) - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -2466,7 +2466,7 @@ class YTorus(TorusMixin, Surface): Minor radius of the torus in [cm] (parallel to axis of revolution) c : float Minor radius of the torus (perpendicular to axis of revolution) - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -2541,7 +2541,7 @@ class ZTorus(TorusMixin, Surface): Minor radius of the torus in [cm] (parallel to axis of revolution) c : float Minor radius of the torus in [cm] (perpendicular to axis of revolution) - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float From 0ecd45c90651698518673368290fc58d90e8a5e8 Mon Sep 17 00:00:00 2001 From: Seda Yilmaz <133794379+nsedayilmaz@users.noreply.github.com> Date: Wed, 13 Nov 2024 02:11:59 +0300 Subject: [PATCH 203/671] Add a vessel composite surface with ellipsoids on top and bottom. (#3168) Co-authored-by: Patrick Shriwise Co-authored-by: Paul Romano --- docs/source/pythonapi/model.rst | 1 + openmc/model/surface_composite.py | 105 ++++++++++++++++++++- tests/unit_tests/test_surface_composite.py | 49 ++++++++++ 3 files changed, 150 insertions(+), 5 deletions(-) diff --git a/docs/source/pythonapi/model.rst b/docs/source/pythonapi/model.rst index e7d6d320f..3034826bd 100644 --- a/docs/source/pythonapi/model.rst +++ b/docs/source/pythonapi/model.rst @@ -32,6 +32,7 @@ Composite Surfaces openmc.model.RectangularParallelepiped openmc.model.RectangularPrism openmc.model.RightCircularCylinder + openmc.model.Vessel openmc.model.XConeOneSided openmc.model.YConeOneSided openmc.model.ZConeOneSided diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index cc570cc56..1f25b9ca6 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -41,9 +41,11 @@ class CompositeSurface(ABC): def boundary_type(self, boundary_type): # Set boundary type on underlying surfaces, but not for ambiguity plane # on one-sided cones + classes = (XConeOneSided, YConeOneSided, ZConeOneSided, Vessel) for name in self._surface_names: - if name != 'plane': - getattr(self, name).boundary_type = boundary_type + if isinstance(self, classes) and name.startswith('plane'): + continue + getattr(self, name).boundary_type = boundary_type def __repr__(self): return f"<{type(self).__name__} at 0x{id(self):x}>" @@ -90,8 +92,8 @@ class CylinderSector(CompositeSurface): counterclockwise direction with respect to the first basis axis (+y, +z, or +x). Must be greater than :attr:`theta1`. center : iterable of float - Coordinate for central axes of cylinders in the (y, z), (x, z), or (x, y) - basis. Defaults to (0,0). + Coordinate for central axes of cylinders in the (y, z), (x, z), or (x, y) + basis. Defaults to (0,0). axis : {'x', 'y', 'z'} Central axis of the cylinders defining the inner and outer surfaces of the sector. Defaults to 'z'. @@ -119,7 +121,7 @@ class CylinderSector(CompositeSurface): r2, theta1, theta2, - center=(0.,0.), + center=(0., 0.), axis='z', **kwargs): @@ -1856,3 +1858,96 @@ class ConicalFrustum(CompositeSurface): def __neg__(self) -> openmc.Region: return +self.plane_bottom & -self.plane_top & -self.cone + + +class Vessel(CompositeSurface): + """Vessel composed of cylinder with semi-ellipsoid top and bottom. + + This composite surface is represented by a finite cylinder with ellipsoidal + top and bottom surfaces. This surface is equivalent to the 'vesesl' surface + in Serpent. + + .. versionadded:: 0.15.1 + + Parameters + ---------- + r : float + Radius of vessel. + p1 : float + Minimum coordinate for cylindrical part of vessel. + p2 : float + Maximum coordinate for cylindrical part of vessel. + h1 : float + Height of bottom ellipsoidal part of vessel. + h2 : float + Height of top ellipsoidal part of vessel. + center : 2-tuple of float + Coordinate for central axis of the cylinder in the (y, z), (x, z), or + (x, y) basis. Defaults to (0,0). + axis : {'x', 'y', 'z'} + Central axis of the cylinder. + + """ + + _surface_names = ('cyl', 'plane_bottom', 'plane_top', 'bottom', 'top') + + def __init__(self, r: float, p1: float, p2: float, h1: float, h2: float, + center: Sequence[float] = (0., 0.), axis: str = 'z', **kwargs): + if p1 >= p2: + raise ValueError('p1 must be less than p2') + check_value('axis', axis, {'x', 'y', 'z'}) + + c1, c2 = center + cyl_class = getattr(openmc, f'{axis.upper()}Cylinder') + plane_class = getattr(openmc, f'{axis.upper()}Plane') + self.cyl = cyl_class(c1, c2, r, **kwargs) + self.plane_bottom = plane_class(p1) + self.plane_top = plane_class(p2) + + # General equation for an ellipsoid: + # (x-x₀)²/r² + (y-y₀)²/r² + (z-z₀)²/h² = 1 + # (x-x₀)² + (y-y₀)² + (z-z₀)²s² = r² + # Let s = r/h: + # (x² - 2x₀x + x₀²) + (y² - 2y₀y + y₀²) + (z² - 2z₀z + z₀²)s² = r² + # x² + y² + s²z² - 2x₀x - 2y₀y - 2s²z₀z + (x₀² + y₀² + z₀²s² - r²) = 0 + + sb = (r/h1) + st = (r/h2) + kwargs['a'] = kwargs['b'] = kwargs['c'] = 1.0 + kwargs_bottom = kwargs + kwargs_top = kwargs.copy() + + sb2 = sb*sb + st2 = st*st + kwargs_bottom['k'] = c1*c1 + c2*c2 + p1*p1*sb2 - r*r + kwargs_top['k'] = c1*c1 + c2*c2 + p2*p2*st2 - r*r + + if axis == 'x': + kwargs_bottom['a'] *= sb2 + kwargs_top['a'] *= st2 + kwargs_bottom['g'] = -2*p1*sb2 + kwargs_top['g'] = -2*p2*st2 + kwargs_top['h'] = kwargs_bottom['h'] = -2*c1 + kwargs_top['j'] = kwargs_bottom['j'] = -2*c2 + elif axis == 'y': + kwargs_bottom['b'] *= sb2 + kwargs_top['b'] *= st2 + kwargs_top['g'] = kwargs_bottom['g'] = -2*c1 + kwargs_bottom['h'] = -2*p1*sb2 + kwargs_top['h'] = -2*p2*st2 + kwargs_top['j'] = kwargs_bottom['j'] = -2*c2 + elif axis == 'z': + kwargs_bottom['c'] *= sb2 + kwargs_top['c'] *= st2 + kwargs_top['g'] = kwargs_bottom['g'] = -2*c1 + kwargs_top['h'] = kwargs_bottom['h'] = -2*c2 + kwargs_bottom['j'] = -2*p1*sb2 + kwargs_top['j'] = -2*p2*st2 + + self.bottom = openmc.Quadric(**kwargs_bottom) + self.top = openmc.Quadric(**kwargs_top) + + def __neg__(self): + return ((-self.cyl & +self.plane_bottom & -self.plane_top) | + (-self.bottom & -self.plane_bottom) | + (-self.top & +self.plane_top)) diff --git a/tests/unit_tests/test_surface_composite.py b/tests/unit_tests/test_surface_composite.py index 015ac667e..da7ffbcc4 100644 --- a/tests/unit_tests/test_surface_composite.py +++ b/tests/unit_tests/test_surface_composite.py @@ -599,3 +599,52 @@ def test_conical_frustum(): # Denegenerate case with r1 = r2 s = openmc.model.ConicalFrustum(center_base, axis, r1, r1) assert (1., 1., -0.01) in -s + + +def test_vessel(): + center = (3.0, 2.0) + r = 1.0 + p1, p2 = -5.0, 5.0 + h1 = h2 = 1.0 + s = openmc.model.Vessel(r, p1, p2, h1, h2, center) + assert isinstance(s.cyl, openmc.Cylinder) + assert isinstance(s.plane_bottom, openmc.Plane) + assert isinstance(s.plane_top, openmc.Plane) + assert isinstance(s.bottom, openmc.Quadric) + assert isinstance(s.top, openmc.Quadric) + + # Make sure boundary condition propagates (but not for planes) + s.boundary_type = 'reflective' + assert s.boundary_type == 'reflective' + assert s.cyl.boundary_type == 'reflective' + assert s.bottom.boundary_type == 'reflective' + assert s.top.boundary_type == 'reflective' + assert s.plane_bottom.boundary_type == 'transmission' + assert s.plane_top.boundary_type == 'transmission' + + # Check bounding box + ll, ur = (+s).bounding_box + assert np.all(np.isinf(ll)) + assert np.all(np.isinf(ur)) + ll, ur = (-s).bounding_box + assert np.all(np.isinf(ll)) + assert np.all(np.isinf(ur)) + + # __contains__ on associated half-spaces + assert (3., 2., 0.) in -s + assert (3., 2., -5.0) in -s + assert (3., 2., 5.0) in -s + assert (3., 2., -5.9) in -s + assert (3., 2., 5.9) in -s + assert (3., 2., -6.1) not in -s + assert (3., 2., 6.1) not in -s + assert (4.5, 2., 0.) in +s + assert (3., 3.2, 0.) in +s + assert (3., 2., 7.) in +s + + # translate method + s_t = s.translate((0., 0., 1.)) + assert (3., 2., 6.1) in -s_t + + # Make sure repr works + repr(s) From 58400cbf1099fdaa73cc4ae17167cd0e8b52c97d Mon Sep 17 00:00:00 2001 From: Paul Wilson Date: Wed, 13 Nov 2024 15:39:41 -0600 Subject: [PATCH 204/671] Add PointCloud spatial distribution (#3161) Co-authored-by: Patrick Shriwise Co-authored-by: Paul Romano --- docs/source/io_formats/settings.rst | 56 ++++++++++--- docs/source/pythonapi/stats.rst | 1 + docs/source/usersguide/settings.rst | 4 +- include/openmc/distribution_spatial.h | 20 +++++ include/openmc/xml_interface.h | 3 + openmc/stats/multivariate.py | 114 ++++++++++++++++++++++++++ src/distribution_spatial.cpp | 45 ++++++++++ src/xml_interface.cpp | 19 +++++ tests/unit_tests/test_source.py | 56 +++++++++++++ 9 files changed, 306 insertions(+), 12 deletions(-) diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 2c6c32656..34b73fc55 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -579,24 +579,38 @@ attributes/sub-elements: :type: The type of spatial distribution. Valid options are "box", "fission", - "point", "cartesian", "cylindrical", and "spherical". A "box" spatial - distribution has coordinates sampled uniformly in a parallelepiped. A - "fission" spatial distribution samples locations from a "box" + "point", "cartesian", "cylindrical", "spherical", "mesh", and "cloud". + + A "box" spatial distribution has coordinates sampled uniformly in a + parallelepiped. + + A "fission" spatial distribution samples locations from a "box" distribution but only locations in fissionable materials are accepted. + A "point" spatial distribution has coordinates specified by a triplet. + A "cartesian" spatial distribution specifies independent distributions of - x-, y-, and z-coordinates. A "cylindrical" spatial distribution specifies - independent distributions of r-, phi-, and z-coordinates where phi is the - azimuthal angle and the origin for the cylindrical coordinate system is - specified by origin. A "spherical" spatial distribution specifies - independent distributions of r-, cos_theta-, and phi-coordinates where - cos_theta is the cosine of the angle with respect to the z-axis, phi is - the azimuthal angle, and the sphere is centered on the coordinate - (x0,y0,z0). A "mesh" spatial distribution samples source sites from a mesh element + x-, y-, and z-coordinates. + + A "cylindrical" spatial distribution specifies independent distributions + of r-, phi-, and z-coordinates where phi is the azimuthal angle and the + origin for the cylindrical coordinate system is specified by origin. + + A "spherical" spatial distribution specifies independent distributions of + r-, cos_theta-, and phi-coordinates where cos_theta is the cosine of the + angle with respect to the z-axis, phi is the azimuthal angle, and the + sphere is centered on the coordinate (x0,y0,z0). + + A "mesh" spatial distribution samples source sites from a mesh element based on the relative strengths provided in the node. Source locations within an element are sampled isotropically. If no strengths are provided, the space within the mesh is uniformly sampled. + A "cloud" spatial distribution samples source sites from a list of spatial + positions provided in the node, based on the relative strengths provided + in the node. If no strengths are provided, the positions are uniformly + sampled. + *Default*: None :parameters: @@ -662,6 +676,26 @@ attributes/sub-elements: For "cylindrical and "spherical" distributions, this element specifies the coordinates for the origin of the coordinate system. + :mesh_id: + For "mesh" spatial distributions, this element specifies which mesh ID to + use for the geometric description of the mesh. + + :coords: + For "cloud" distributions, this element specifies a list of coordinates + for each of the points in the cloud. + + :strengths: + For "mesh" and "cloud" spatial distributions, this element specifies the + relative source strength of each mesh element or each point in the cloud. + + :volume_normalized: + For "mesh" spatial distrubtions, this optional boolean element specifies + whether the vector of relative strengths should be multiplied by the mesh + element volume. This is most common if the strengths represent a source + per unit volume. + + *Default*: false + :angle: An element specifying the angular distribution of source sites. This element has the following attributes: diff --git a/docs/source/pythonapi/stats.rst b/docs/source/pythonapi/stats.rst index b72896c18..c8318ba86 100644 --- a/docs/source/pythonapi/stats.rst +++ b/docs/source/pythonapi/stats.rst @@ -59,6 +59,7 @@ Spatial Distributions openmc.stats.Box openmc.stats.Point openmc.stats.MeshSpatial + openmc.stats.PointCloud .. autosummary:: :toctree: generated diff --git a/docs/source/usersguide/settings.rst b/docs/source/usersguide/settings.rst index 4111481a9..349aa34d0 100644 --- a/docs/source/usersguide/settings.rst +++ b/docs/source/usersguide/settings.rst @@ -192,7 +192,9 @@ distributions using spherical or cylindrical coordinates, you can use :class:`openmc.stats.SphericalIndependent` or :class:`openmc.stats.CylindricalIndependent`, respectively. Meshes can also be used to represent spatial distributions with :class:`openmc.stats.MeshSpatial` -by specifying a mesh and source strengths for each mesh element. +by specifying a mesh and source strengths for each mesh element. It is also +possible to define a "cloud" of source points, each with a different relative +probability, using :class:`openmc.stats.PointCloud`. The angular distribution can be set equal to a sub-class of :class:`openmc.stats.UnitSphere` such as :class:`openmc.stats.Isotropic`, diff --git a/include/openmc/distribution_spatial.h b/include/openmc/distribution_spatial.h index 28568c890..8ff766d13 100644 --- a/include/openmc/distribution_spatial.h +++ b/include/openmc/distribution_spatial.h @@ -136,6 +136,26 @@ private: //!< mesh element indices }; +//============================================================================== +//! Distribution of points +//============================================================================== + +class PointCloud : public SpatialDistribution { +public: + explicit PointCloud(pugi::xml_node node); + explicit PointCloud( + std::vector point_cloud, gsl::span strengths); + + //! Sample a position from the distribution + //! \param seed Pseudorandom number seed pointer + //! \return Sampled position + Position sample(uint64_t* seed) const override; + +private: + std::vector point_cloud_; + DiscreteIndex point_idx_dist_; //!< Distribution of Position indices +}; + //============================================================================== //! Uniform distribution of points over a box //============================================================================== diff --git a/include/openmc/xml_interface.h b/include/openmc/xml_interface.h index bd6554c13..f49613ecd 100644 --- a/include/openmc/xml_interface.h +++ b/include/openmc/xml_interface.h @@ -50,6 +50,9 @@ xt::xarray get_node_xarray( return xt::adapt(v, shape); } +std::vector get_node_position_array( + pugi::xml_node node, const char* name, bool lowercase = false); + Position get_node_position( pugi::xml_node node, const char* name, bool lowercase = false); diff --git a/openmc/stats/multivariate.py b/openmc/stats/multivariate.py index 06c65896f..cd474fa9b 100644 --- a/openmc/stats/multivariate.py +++ b/openmc/stats/multivariate.py @@ -279,6 +279,8 @@ class Spatial(ABC): return Point.from_xml_element(elem) elif distribution == 'mesh': return MeshSpatial.from_xml_element(elem, meshes) + elif distribution == 'cloud': + return PointCloud.from_xml_element(elem) class CartesianIndependent(Spatial): @@ -756,6 +758,118 @@ class MeshSpatial(Spatial): return cls(meshes[mesh_id], strengths, volume_normalized) +class PointCloud(Spatial): + """Spatial distribution from a point cloud. + + This distribution specifies a discrete list of points, with corresponding + relative probabilities. + + .. versionadded:: 0.15.1 + + Parameters + ---------- + positions : iterable of 3-tuples + The points in space to be sampled + strengths : iterable of float, optional + An iterable of values that represents the relative probabilty of each + point. + + Attributes + ---------- + positions : numpy.ndarray + The points in space to be sampled with shape (N, 3) + strengths : numpy.ndarray or None + An array of relative probabilities for each mesh point + """ + + def __init__( + self, + positions: Sequence[Sequence[float]], + strengths: Sequence[float] | None = None + ): + self.positions = positions + self.strengths = strengths + + @property + def positions(self) -> np.ndarray: + return self._positions + + @positions.setter + def positions(self, positions): + positions = np.array(positions, dtype=float) + if positions.ndim != 2: + raise ValueError('positions must be a 2D array') + elif positions.shape[1] != 3: + raise ValueError('Each position must have 3 values') + self._positions = positions + + @property + def strengths(self) -> np.ndarray: + return self._strengths + + @strengths.setter + def strengths(self, strengths): + if strengths is not None: + strengths = np.array(strengths, dtype=float) + if strengths.ndim != 1: + raise ValueError('strengths must be a 1D array') + elif strengths.size != self.positions.shape[0]: + raise ValueError('strengths must have the same length as positions') + self._strengths = strengths + + @property + def num_strength_bins(self) -> int: + if self.strengths is None: + raise ValueError('Strengths are not set') + return self.strengths.size + + def to_xml_element(self) -> ET.Element: + """Return XML representation of the spatial distribution + + Returns + ------- + element : lxml.etree._Element + XML element containing spatial distribution data + + """ + element = ET.Element('space') + element.set('type', 'cloud') + + subelement = ET.SubElement(element, 'coords') + subelement.text = ' '.join(str(e) for e in self.positions.flatten()) + + if self.strengths is not None: + subelement = ET.SubElement(element, 'strengths') + subelement.text = ' '.join(str(e) for e in self.strengths) + + return element + + @classmethod + def from_xml_element(cls, elem: ET.Element) -> PointCloud: + """Generate spatial distribution from an XML element + + Parameters + ---------- + elem : lxml.etree._Element + XML element + + Returns + ------- + openmc.stats.PointCloud + Spatial distribution generated from XML element + + + """ + coord_data = get_text(elem, 'coords') + positions = np.array([float(b) for b in coord_data.split()]).reshape((-1, 3)) + + strengths = get_text(elem, 'strengths') + if strengths is not None: + strengths = [float(b) for b in strengths.split()] + + return cls(positions, strengths) + + class Box(Spatial): """Uniform distribution of coordinates in a rectangular cuboid. diff --git a/src/distribution_spatial.cpp b/src/distribution_spatial.cpp index 8dbd7d887..5c193a95d 100644 --- a/src/distribution_spatial.cpp +++ b/src/distribution_spatial.cpp @@ -26,6 +26,8 @@ unique_ptr SpatialDistribution::create(pugi::xml_node node) return UPtrSpace {new SphericalIndependent(node)}; } else if (type == "mesh") { return UPtrSpace {new MeshSpatial(node)}; + } else if (type == "cloud") { + return UPtrSpace {new PointCloud(node)}; } else if (type == "box") { return UPtrSpace {new SpatialBox(node)}; } else if (type == "fission") { @@ -298,6 +300,49 @@ Position MeshSpatial::sample(uint64_t* seed) const return this->sample_mesh(seed).second; } +//============================================================================== +// PointCloud implementation +//============================================================================== + +PointCloud::PointCloud(pugi::xml_node node) +{ + if (check_for_node(node, "coords")) { + point_cloud_ = get_node_position_array(node, "coords"); + } else { + fatal_error("No coordinates were provided for the PointCloud " + "spatial distribution"); + } + + std::vector strengths; + + if (check_for_node(node, "strengths")) + strengths = get_node_array(node, "strengths"); + else + strengths.resize(point_cloud_.size(), 1.0); + + if (strengths.size() != point_cloud_.size()) { + fatal_error( + fmt::format("Number of entries for the strengths array {} does " + "not match the number of spatial points provided {}.", + strengths.size(), point_cloud_.size())); + } + + point_idx_dist_.assign(strengths); +} + +PointCloud::PointCloud( + std::vector point_cloud, gsl::span strengths) +{ + point_cloud_.assign(point_cloud.begin(), point_cloud.end()); + point_idx_dist_.assign(strengths); +} + +Position PointCloud::sample(uint64_t* seed) const +{ + int32_t index = point_idx_dist_.sample(seed); + return point_cloud_[index]; +} + //============================================================================== // SpatialBox implementation //============================================================================== diff --git a/src/xml_interface.cpp b/src/xml_interface.cpp index 6ce465baf..840d3f5b8 100644 --- a/src/xml_interface.cpp +++ b/src/xml_interface.cpp @@ -4,6 +4,7 @@ #include "openmc/error.h" #include "openmc/string_utils.h" +#include "openmc/vector.h" namespace openmc { @@ -48,6 +49,24 @@ bool get_node_value_bool(pugi::xml_node node, const char* name) return false; } +vector get_node_position_array( + pugi::xml_node node, const char* name, bool lowercase) +{ + vector coords = get_node_array(node, name, lowercase); + if (coords.size() % 3 != 0) { + fatal_error(fmt::format( + "Incorect number of coordinates in Position array ({}) for \"{}\"", + coords.size(), name)); + } + vector positions; + positions.reserve(coords.size() / 3); + auto it = coords.begin(); + for (size_t i = 0; i < coords.size(); i += 3) { + positions.push_back({coords[i], coords[i + 1], coords[i + 2]}); + } + return positions; +} + Position get_node_position( pugi::xml_node node, const char* name, bool lowercase) { diff --git a/tests/unit_tests/test_source.py b/tests/unit_tests/test_source.py index 32650d549..c88fbcbe6 100644 --- a/tests/unit_tests/test_source.py +++ b/tests/unit_tests/test_source.py @@ -1,3 +1,4 @@ +from collections import Counter from math import pi import openmc @@ -49,6 +50,61 @@ def test_spherical_uniform(): 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] + + space = openmc.stats.PointCloud(positions, strengths) + np.testing.assert_equal(space.positions, positions) + np.testing.assert_equal(space.strengths, strengths) + + src = openmc.IndependentSource(space=space) + assert src.space == space + np.testing.assert_equal(src.space.positions, positions) + np.testing.assert_equal(src.space.strengths, strengths) + + elem = src.to_xml_element() + src = openmc.IndependentSource.from_xml_element(elem) + np.testing.assert_equal(src.space.positions, positions) + np.testing.assert_equal(src.space.strengths, strengths) + + +def test_point_cloud_invalid(): + with pytest.raises(ValueError, match='2D'): + openmc.stats.PointCloud([1, 0, 2, 0, 1, 0]) + + with pytest.raises(ValueError, match='3 values'): + openmc.stats.PointCloud([(1, 0, 2, 3), (4, 5, 2, 3)]) + + with pytest.raises(ValueError, match='1D'): + openmc.stats.PointCloud([(1, 0, 2), (4, 5, 2)], [(1, 2), (3, 4)]) + + with pytest.raises(ValueError, match='same length'): + openmc.stats.PointCloud([(1, 0, 2), (4, 5, 2)], [1, 2, 4]) + + +def test_point_cloud_strengths(run_in_tmpdir, sphere_box_model): + positions = [(1., 0., 2.), (0., 1., 0.), (0., 0., 3.), (-1., -1., 2.)] + strengths = [1, 2, 3, 4] + space = openmc.stats.PointCloud(positions, strengths) + + model = sphere_box_model[0] + model.settings.run_mode = 'fixed source' + model.settings.source = openmc.IndependentSource(space=space) + + try: + model.init_lib() + n_samples = 50_000 + sites = openmc.lib.sample_external_source(n_samples) + finally: + model.finalize_lib() + + count = Counter(s.r for s in sites) + for i, (strength, position) in enumerate(zip(strengths, positions)): + sampled_strength = count[position] / n_samples + expected_strength = pytest.approx(strength/sum(strengths), abs=0.02) + assert sampled_strength == expected_strength, f'Strength incorrect for {positions[i]}' + def test_source_file(): filename = 'source.h5' From d30b2e801413faaee07282969955991abdf7baeb Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 13 Nov 2024 15:58:22 -0600 Subject: [PATCH 205/671] Fix docstring for Model.plot (#3198) --- openmc/model/model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index 0ea6db8db..ffad8f503 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -820,14 +820,14 @@ class Model: Parameters ---------- - n_samples : dict + n_samples : int, optional The number of source particles to sample and add to plot. Defaults to None which doesn't plot any particles on the plot. plane_tolerance: float When plotting a plane the source locations within the plane +/- the plane_tolerance will be included and those outside of the plane_tolerance will not be shown - source_kwargs : dict + source_kwargs : dict, optional Keyword arguments passed to :func:`matplotlib.pyplot.scatter`. **kwargs Keyword arguments passed to :func:`openmc.Universe.plot` From fbb115921c71997d37e62748a077bdd369667786 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Mon, 18 Nov 2024 08:52:59 -0600 Subject: [PATCH 206/671] Apply weight windows at collisions in multigroup transport mode. (#3199) --- src/physics_mg.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/physics_mg.cpp b/src/physics_mg.cpp index 361cf5aff..3cc0532d2 100644 --- a/src/physics_mg.cpp +++ b/src/physics_mg.cpp @@ -19,6 +19,7 @@ #include "openmc/settings.h" #include "openmc/simulation.h" #include "openmc/tallies/tally.h" +#include "openmc/weight_windows.h" namespace openmc { @@ -27,6 +28,9 @@ void collision_mg(Particle& p) // Add to the collision counter for the particle p.n_collision()++; + if (settings::weight_window_checkpoint_collision) + apply_weight_windows(p); + // Sample the reaction type sample_reaction(p); From 172867b1df18e98c3c0ff5d5645e966f8583a63e Mon Sep 17 00:00:00 2001 From: John Tramm Date: Wed, 20 Nov 2024 14:15:41 -0600 Subject: [PATCH 207/671] Random Ray Adjoint Mode (#3191) Co-authored-by: Paul Romano --- docs/source/methods/random_ray.rst | 45 +++- docs/source/usersguide/random_ray.rst | 29 ++- include/openmc/mgxs.h | 2 + .../openmc/random_ray/flat_source_domain.h | 21 +- .../openmc/random_ray/random_ray_simulation.h | 9 +- openmc/deplete/openmc_operator.py | 2 +- openmc/settings.py | 9 + src/mgxs_interface.cpp | 2 +- src/random_ray/flat_source_domain.cpp | 218 +++++++++------- src/random_ray/linear_source_domain.cpp | 40 ++- src/random_ray/random_ray.cpp | 20 +- src/random_ray/random_ray_simulation.cpp | 151 +++++++++-- src/settings.cpp | 4 + .../__init__.py | 0 .../inputs_true.dat | 245 ++++++++++++++++++ .../results_true.dat | 9 + .../random_ray_adjoint_fixed_source/test.py | 20 ++ .../random_ray_adjoint_k_eff/__init__.py | 0 .../random_ray_adjoint_k_eff/inputs_true.dat | 110 ++++++++ .../random_ray_adjoint_k_eff/results_true.dat | 171 ++++++++++++ .../random_ray_adjoint_k_eff/test.py | 20 ++ 21 files changed, 958 insertions(+), 169 deletions(-) create mode 100644 tests/regression_tests/random_ray_adjoint_fixed_source/__init__.py create mode 100644 tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_adjoint_fixed_source/results_true.dat create mode 100644 tests/regression_tests/random_ray_adjoint_fixed_source/test.py create mode 100644 tests/regression_tests/random_ray_adjoint_k_eff/__init__.py create mode 100644 tests/regression_tests/random_ray_adjoint_k_eff/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_adjoint_k_eff/results_true.dat create mode 100644 tests/regression_tests/random_ray_adjoint_k_eff/test.py diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index 3d98747e4..c827c351d 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -542,7 +542,7 @@ in that cell for the iteration from Equation :eq:`phi_naive` to: .. math:: :label: phi_missed_one - \phi_{i,g,n}^{missed} = \frac{Q_{i,g,n} }{\Sigma_{t,i,g}} + \phi_{i,g,n}^{missed} = \frac{Q_{i,g,n} }{\Sigma_{t,i,g}} as the streaming operator has gone to zero. While this is obviously innacurate as it ignores transport, for most problems where the region is only occasionally @@ -1060,6 +1060,49 @@ random ray and Monte Carlo, however. develop the scattering source by way of inactive batches before beginning active batches. +------------------------ +Adjoint Flux Solver Mode +------------------------ + +The random ray solver in OpenMC can also be used to solve for the adjoint flux, +:math:`\psi^{\dagger}`. In combination with the regular (forward) flux solution, +the adjoint flux is useful for perturbation methods as well as for computing +weight windows for subsequent Monte Carlo simulations. The adjoint flux can be +thought of as the "backwards" flux, representing the flux where a particle is +born at an absoprtion point (and typical absorption energy), and then undergoes +transport with a transposed scattering matrix. That is, instead of sampling a +particle and seeing where it might go as in a standard forward solve, we will +sample an absorption location and see where the particle that was absorbed there +might have come from. Notably, for typical neutron absorption at low energy +levels, this means that adjoint flux particles are typically sampled at a low +energy and then upscatter (via a transposed scattering matrix) over their +lifetimes. + +In OpenMC, the random ray adjoint solver is implemented simply by transposing +the scattering matrix, swapping :math:`\nu\Sigma_f` and :math:`\chi`, and then +running a normal transport solve. When no external fixed source is present, no +additional changes are needed in the transport process. However, if an external +fixed forward source is present in the simulation problem, then an additional +step is taken to compute the accompanying fixed adjoint source. In OpenMC, the +adjoint flux does *not* represent a response function for a particular detector +region. Rather, the adjoint flux is the global response, making it appropriate +for use with weight window generation schemes for global variance reduction. +Thus, if using a fixed source, the external source for the adjoint mode is +simply computed as being :math:`1 / \phi`, where :math:`\phi` is the forward +scalar flux that results from a normal forward solve (which OpenMC will run +first automatically when in adjoint mode). The adjoint external source will be +computed for each source region in the simulation mesh, independent of any +tallies. The adjoint external source is always flat, even when a linear +scattering and fission source shape is used. When in adjoint mode, all reported +results (e.g., tallies, eigenvalues, etc.) are derived from the adjoint flux, +even when the physical meaning is not necessarily obvious. These values are +still reported, though we emphasize that the primary use case for adjoint mode +is for producing adjoint flux tallies to support subsequent perturbation studies +and weight window generation. + +Note that the adjoint :math:`k_{eff}` is statistically the same as the forward +:math:`k_{eff}`, despite the flux distributions taking different shapes. + --------------------------- Fundamental Sources of Bias --------------------------- diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index 5ca0ab6be..2b9cf6724 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -558,7 +558,7 @@ following methods are currently available in OpenMC: - Cons * - ``simulation_averaged`` - Accumulates total active ray lengths in each FSR over all iterations, - improving the estimate of the volume in each cell each iteration. + improving the estimate of the volume in each cell each iteration. - * Virtually unbiased after several iterations * Asymptotically approaches the true analytical volume * Typically most efficient in terms of speed vs. accuracy @@ -593,6 +593,33 @@ estimator, the following code would be used: settings.random_ray['volume_estimator'] = 'naive' +----------------- +Adjoint Flux Mode +----------------- + +The adjoint flux random ray solver mode can be enabled as: +entire +:: + + settings.random_ray['adjoint'] = True + +When enabled, OpenMC will first run a forward transport simulation followed by +an adjoint transport simulation. The purpose of the forward solve is to compute +the adjoint external source when an external source is present in the +simulation. Simulation settings (e.g., number of rays, batches, etc.) will be +identical for both simulations. At the conclusion of the run, all results (e.g., +tallies, plots, etc.) will be derived from the adjoint flux rather than the +forward flux but are not labeled any differently. The initial forward flux +solution will not be stored or available in the final statepoint file. Those +wishing to do analysis requiring both the forward and adjoint solutions will +need to run two separate simulations and load both statepoint files. + +.. note:: + When adjoint mode is selected, OpenMC will always perform a full forward + solve and then run a full adjoint solve immediately afterwards. Statepoint + and tally results will be derived from the adjoint flux, but will not be + labeled any differently. + --------------------------------------- Putting it All Together: Example Inputs --------------------------------------- diff --git a/include/openmc/mgxs.h b/include/openmc/mgxs.h index 3fb060810..9b1602f29 100644 --- a/include/openmc/mgxs.h +++ b/include/openmc/mgxs.h @@ -86,8 +86,10 @@ public: bool fissionable; // Is this fissionable bool is_isotropic { true}; // used to skip search for angle indices if isotropic + bool exists_in_model {true}; // Is this present in model Mgxs() = default; + Mgxs(bool exists) : exists_in_model(exists) {} //! \brief Constructor that loads the Mgxs object from the HDF5 file //! diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index 5c50f7fb0..b5b5db706 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -111,13 +111,17 @@ public: virtual void all_reduce_replicated_source_regions(); void convert_external_sources(); void count_external_source_regions(); + void set_adjoint_sources(const vector& forward_flux); virtual void flux_swap(); virtual double evaluate_flux_at_point(Position r, int64_t sr, int g) const; double compute_fixed_source_normalization_factor() const; + void flatten_xs(); + void transpose_scattering_matrix(); //---------------------------------------------------------------------------- // Static Data members static bool volume_normalized_flux_tallies_; + static bool adjoint_; // If the user wants outputs based on the adjoint flux //---------------------------------------------------------------------------- // Static data members @@ -150,6 +154,19 @@ public: vector source_; vector external_source_; vector external_source_present_; + vector scalar_flux_final_; + + // 2D arrays stored in 1D representing values for all materials x energy + // groups + int n_materials_; + vector sigma_t_; + vector nu_sigma_f_; + vector sigma_f_; + vector chi_; + + // 3D arrays stored in 1D representing values for all materials x energy + // groups x energy groups + vector sigma_s_; protected: //---------------------------------------------------------------------------- @@ -190,10 +207,6 @@ protected: vector material_; vector volume_naive_; - // 2D arrays stored in 1D representing values for all source regions x energy - // groups - vector scalar_flux_final_; - // 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 diff --git a/include/openmc/random_ray/random_ray_simulation.h b/include/openmc/random_ray/random_ray_simulation.h index 55bac6905..01f12bffb 100644 --- a/include/openmc/random_ray/random_ray_simulation.h +++ b/include/openmc/random_ray/random_ray_simulation.h @@ -20,6 +20,8 @@ public: //---------------------------------------------------------------------------- // Methods void compute_segment_correction_factors(); + void prepare_fixed_sources(); + void prepare_fixed_sources_adjoint(vector& forward_flux); void simulate(); void reduce_simulation_statistics(); void output_simulation_results() const; @@ -30,8 +32,13 @@ public: int64_t n_external_source_regions) const; //---------------------------------------------------------------------------- - // Data members + // Accessors + FlatSourceDomain* domain() const { return domain_.get(); } + private: + //---------------------------------------------------------------------------- + // Data members + // Contains all flat source region data unique_ptr domain_; diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py index 5837cd2c1..049e0dd37 100644 --- a/openmc/deplete/openmc_operator.py +++ b/openmc/deplete/openmc_operator.py @@ -208,7 +208,7 @@ class OpenMCOperator(TransportOperator): if nuclide in self.nuclides_with_data or self._decay_nucs: model_nuclides.add(nuclide) else: - msg = (f"Nuclilde {nuclide} in material {mat.id} is not " + msg = (f"Nuclide {nuclide} in material {mat.id} is not " "present in the depletion chain and has no cross " "section data.") warn(msg) diff --git a/openmc/settings.py b/openmc/settings.py index 0a78fb564..a350de72e 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -170,6 +170,9 @@ class Settings: cm/cm^3. When disabled, flux tallies will be reported in units of cm (i.e., total distance traveled by neutrons in the spatial tally region). + :adjoint: + Whether to run the random ray solver in adjoint mode (bool). The + default is 'False'. .. versionadded:: 0.15.0 resonance_scattering : dict @@ -1113,6 +1116,8 @@ class Settings: ('flat', 'linear', 'linear_xy')) elif key == 'volume_normalized_flux_tallies': cv.check_type('volume normalized flux tallies', random_ray[key], bool) + elif key == 'adjoint': + cv.check_type('adjoint', random_ray[key], bool) else: raise ValueError(f'Unable to set random ray to "{key}" which is ' 'unsupported by OpenMC') @@ -1916,6 +1921,10 @@ class Settings: self.random_ray['volume_normalized_flux_tallies'] = ( child.text in ('true', '1') ) + elif child.tag == 'adjoint': + self.random_ray['adjoint'] = ( + child.text in ('true', '1') + ) def to_xml_element(self, mesh_memo=None): """Create a 'settings' element to be written to an XML file. diff --git a/src/mgxs_interface.cpp b/src/mgxs_interface.cpp index 0febc612b..ed734d401 100644 --- a/src/mgxs_interface.cpp +++ b/src/mgxs_interface.cpp @@ -146,7 +146,7 @@ void MgxsInterface::create_macro_xs() num_energy_groups_, num_delayed_groups_); } else { // Preserve the ordering of materials by including a blank entry - macro_xs_.emplace_back(); + macro_xs_.emplace_back(false); } } } diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 62768b55f..19913c03c 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -27,6 +27,7 @@ namespace openmc { RandomRayVolumeEstimator FlatSourceDomain::volume_estimator_ { RandomRayVolumeEstimator::HYBRID}; bool FlatSourceDomain::volume_normalized_flux_tallies_ {false}; +bool FlatSourceDomain::adjoint_ {false}; FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) { @@ -134,31 +135,23 @@ void FlatSourceDomain::update_neutron_source(double k_eff) double inverse_k_eff = 1.0 / k_eff; - // Temperature and angle indices, if using multiple temperature - // data sets and/or anisotropic data sets. - // TODO: Currently assumes we are only using single temp/single angle data. - const int t = 0; - const int a = 0; - // Add scattering source #pragma omp parallel for for (int sr = 0; sr < n_source_regions_; sr++) { int material = material_[sr]; - for (int e_out = 0; e_out < negroups_; e_out++) { - double sigma_t = data::mg.macro_xs_[material].get_xs( - MgxsType::TOTAL, e_out, nullptr, nullptr, nullptr, t, a); - double scatter_source = 0.0f; + for (int g_out = 0; g_out < negroups_; g_out++) { + double sigma_t = sigma_t_[material * negroups_ + g_out]; + double scatter_source = 0.0; - for (int e_in = 0; e_in < negroups_; e_in++) { - double scalar_flux = scalar_flux_old_[sr * negroups_ + e_in]; - - double sigma_s = data::mg.macro_xs_[material].get_xs( - MgxsType::NU_SCATTER, e_in, &e_out, nullptr, nullptr, t, a); + for (int g_in = 0; g_in < negroups_; g_in++) { + double scalar_flux = scalar_flux_old_[sr * negroups_ + g_in]; + double sigma_s = + sigma_s_[material * negroups_ * negroups_ + g_out * negroups_ + g_in]; scatter_source += sigma_s * scalar_flux; } - source_[sr * negroups_ + e_out] = scatter_source / sigma_t; + source_[sr * negroups_ + g_out] = scatter_source / sigma_t; } } @@ -167,20 +160,17 @@ void FlatSourceDomain::update_neutron_source(double k_eff) for (int sr = 0; sr < n_source_regions_; sr++) { int material = material_[sr]; - for (int e_out = 0; e_out < negroups_; e_out++) { - double sigma_t = data::mg.macro_xs_[material].get_xs( - MgxsType::TOTAL, e_out, nullptr, nullptr, nullptr, t, a); - double fission_source = 0.0f; + for (int g_out = 0; g_out < negroups_; g_out++) { + double sigma_t = sigma_t_[material * negroups_ + g_out]; + double fission_source = 0.0; - for (int e_in = 0; e_in < negroups_; e_in++) { - double scalar_flux = scalar_flux_old_[sr * negroups_ + e_in]; - double nu_sigma_f = data::mg.macro_xs_[material].get_xs( - MgxsType::NU_FISSION, e_in, nullptr, nullptr, nullptr, t, a); - double chi = data::mg.macro_xs_[material].get_xs( - MgxsType::CHI_PROMPT, e_in, &e_out, nullptr, nullptr, t, a); + for (int g_in = 0; g_in < negroups_; g_in++) { + double scalar_flux = scalar_flux_old_[sr * negroups_ + g_in]; + double nu_sigma_f = nu_sigma_f_[material * negroups_ + g_in]; + double chi = chi_[material * negroups_ + g_out]; fission_source += nu_sigma_f * scalar_flux * chi; } - source_[sr * negroups_ + e_out] += + source_[sr * negroups_ + g_out] += fission_source * inverse_k_eff / sigma_t; } } @@ -188,7 +178,7 @@ void FlatSourceDomain::update_neutron_source(double k_eff) // Add external source if in fixed source mode if (settings::run_mode == RunMode::FIXED_SOURCE) { #pragma omp parallel for - for (int se = 0; se < n_source_elements_; se++) { + for (int64_t se = 0; se < n_source_elements_; se++) { source_[se] += external_source_[se]; } } @@ -206,8 +196,8 @@ void FlatSourceDomain::normalize_scalar_flux_and_volumes( // Normalize scalar flux to total distance travelled by all rays this iteration #pragma omp parallel for - for (int64_t e = 0; e < scalar_flux_new_.size(); e++) { - scalar_flux_new_[e] *= normalization_factor; + for (int64_t se = 0; se < scalar_flux_new_.size(); se++) { + scalar_flux_new_[se] *= normalization_factor; } // Accumulate cell-wise ray length tallies collected this iteration, then @@ -223,16 +213,7 @@ void FlatSourceDomain::normalize_scalar_flux_and_volumes( void FlatSourceDomain::set_flux_to_flux_plus_source( int64_t idx, double volume, int material, int g) { - // Temperature and angle indices, if using multiple temperature - // data sets and/or anisotropic data sets. - // TODO: Currently assumes we are only using single temp/single - // angle data. - const int t = 0; - const int a = 0; - - double sigma_t = data::mg.macro_xs_[material].get_xs( - MgxsType::TOTAL, g, nullptr, nullptr, nullptr, t, a); - + double sigma_t = sigma_t_[material * negroups_ + g]; scalar_flux_new_[idx] /= (sigma_t * volume); scalar_flux_new_[idx] += source_[idx]; } @@ -337,13 +318,6 @@ double FlatSourceDomain::compute_k_eff(double k_eff_old) const double fission_rate_old = 0; double fission_rate_new = 0; - // Temperature and angle indices, if using multiple temperature - // data sets and/or anisotropic data sets. - // TODO: Currently assumes we are only using single temp/single - // angle data. - const int t = 0; - const int a = 0; - // Vector for gathering fission source terms for Shannon entropy calculation vector p(n_source_regions_, 0.0f); @@ -363,8 +337,7 @@ double FlatSourceDomain::compute_k_eff(double k_eff_old) const for (int g = 0; g < negroups_; g++) { int64_t idx = (sr * negroups_) + g; - double nu_sigma_f = data::mg.macro_xs_[material].get_xs( - MgxsType::NU_FISSION, g, nullptr, nullptr, nullptr, t, a); + double nu_sigma_f = nu_sigma_f_[material * negroups_ + g]; sr_fission_source_old += nu_sigma_f * scalar_flux_old_[idx]; sr_fission_source_new += nu_sigma_f * scalar_flux_new_[idx]; } @@ -548,7 +521,7 @@ double FlatSourceDomain::compute_fixed_source_normalization_factor() const { // If we are not in fixed source mode, then there are no external sources // so no normalization is needed. - if (settings::run_mode != RunMode::FIXED_SOURCE) { + if (settings::run_mode != RunMode::FIXED_SOURCE || adjoint_) { return 1.0; } @@ -559,17 +532,10 @@ double FlatSourceDomain::compute_fixed_source_normalization_factor() const for (int sr = 0; sr < n_source_regions_; sr++) { int material = material_[sr]; double volume = volume_[sr] * simulation_volume_; - for (int e = 0; e < negroups_; e++) { - // Temperature and angle indices, if using multiple temperature - // data sets and/or anisotropic data sets. - // TODO: Currently assumes we are only using single temp/single - // angle data. - const int t = 0; - const int a = 0; - double sigma_t = data::mg.macro_xs_[material].get_xs( - MgxsType::TOTAL, e, nullptr, nullptr, nullptr, t, a); + for (int g = 0; g < negroups_; g++) { + double sigma_t = sigma_t_[material * negroups_ + g]; simulation_external_source_strength += - external_source_[sr * negroups_ + e] * sigma_t * volume; + external_source_[sr * negroups_ + g] * sigma_t * volume; } } @@ -603,13 +569,6 @@ void FlatSourceDomain::random_ray_tally() // Reset our tally volumes to zero reset_tally_volumes(); - // Temperature and angle indices, if using multiple temperature - // data sets and/or anisotropic data sets. - // TODO: Currently assumes we are only using single temp/single - // angle data. - const int t = 0; - const int a = 0; - double source_normalization_factor = compute_fixed_source_normalization_factor(); @@ -644,21 +603,15 @@ void FlatSourceDomain::random_ray_tally() break; case SCORE_TOTAL: - score = flux * volume * - data::mg.macro_xs_[material].get_xs( - MgxsType::TOTAL, g, NULL, NULL, NULL, t, a); + score = flux * volume * sigma_t_[material * negroups_ + g]; break; case SCORE_FISSION: - score = flux * volume * - data::mg.macro_xs_[material].get_xs( - MgxsType::FISSION, g, NULL, NULL, NULL, t, a); + score = flux * volume * sigma_f_[material * negroups_ + g]; break; case SCORE_NU_FISSION: - score = flux * volume * - data::mg.macro_xs_[material].get_xs( - MgxsType::NU_FISSION, g, NULL, NULL, NULL, t, a); + score = flux * volume * nu_sigma_f_[material * negroups_ + g]; break; case SCORE_EVENTS: @@ -957,9 +910,8 @@ void FlatSourceDomain::output_to_vtk() const for (int g = 0; g < negroups_; g++) { int64_t source_element = fsr * negroups_ + g; float flux = evaluate_flux_at_point(voxel_positions[i], fsr, g); - float Sigma_f = data::mg.macro_xs_[mat].get_xs( - MgxsType::FISSION, g, nullptr, nullptr, nullptr, 0, 0); - total_fission += Sigma_f * flux; + double sigma_f = sigma_f_[mat * negroups_ + g]; + total_fission += sigma_f * flux; } total_fission = convert_to_big_endian(total_fission); std::fwrite(&total_fission, sizeof(float), 1, plot); @@ -977,10 +929,10 @@ void FlatSourceDomain::apply_external_source_to_source_region( const auto& discrete_energies = discrete->x(); const auto& discrete_probs = discrete->prob(); - for (int e = 0; e < discrete_energies.size(); e++) { - int g = data::mg.get_group_index(discrete_energies[e]); + for (int i = 0; i < discrete_energies.size(); i++) { + int g = data::mg.get_group_index(discrete_energies[i]); external_source_[source_region * negroups_ + g] += - discrete_probs[e] * strength_factor; + discrete_probs[i] * strength_factor; } } @@ -1074,27 +1026,107 @@ void FlatSourceDomain::convert_external_sources() } } // End loop over external sources +// Divide the fixed source term by sigma t (to save time when applying each +// iteration) +#pragma omp parallel for + for (int sr = 0; sr < n_source_regions_; sr++) { + int material = material_[sr]; + for (int g = 0; g < negroups_; g++) { + double sigma_t = sigma_t_[material * negroups_ + g]; + external_source_[sr * negroups_ + g] /= sigma_t; + } + } +} + +void FlatSourceDomain::flux_swap() +{ + scalar_flux_old_.swap(scalar_flux_new_); +} + +void FlatSourceDomain::flatten_xs() +{ // Temperature and angle indices, if using multiple temperature // data sets and/or anisotropic data sets. // TODO: Currently assumes we are only using single temp/single angle data. const int t = 0; const int a = 0; -// Divide the fixed source term by sigma t (to save time when applying each -// iteration) -#pragma omp parallel for - for (int sr = 0; sr < n_source_regions_; sr++) { - int material = material_[sr]; - for (int e = 0; e < negroups_; e++) { - double sigma_t = data::mg.macro_xs_[material].get_xs( - MgxsType::TOTAL, e, nullptr, nullptr, nullptr, t, a); - external_source_[sr * negroups_ + e] /= sigma_t; + n_materials_ = data::mg.macro_xs_.size(); + for (auto& m : data::mg.macro_xs_) { + for (int g_out = 0; g_out < negroups_; g_out++) { + if (m.exists_in_model) { + double sigma_t = + m.get_xs(MgxsType::TOTAL, g_out, NULL, NULL, NULL, t, a); + sigma_t_.push_back(sigma_t); + + double nu_Sigma_f = + m.get_xs(MgxsType::NU_FISSION, g_out, NULL, NULL, NULL, t, a); + nu_sigma_f_.push_back(nu_Sigma_f); + + double sigma_f = + m.get_xs(MgxsType::FISSION, g_out, NULL, NULL, NULL, t, a); + sigma_f_.push_back(sigma_f); + + double chi = + m.get_xs(MgxsType::CHI_PROMPT, g_out, &g_out, NULL, NULL, t, a); + chi_.push_back(chi); + + for (int g_in = 0; g_in < negroups_; g_in++) { + double sigma_s = + m.get_xs(MgxsType::NU_SCATTER, g_in, &g_out, NULL, NULL, t, a); + sigma_s_.push_back(sigma_s); + } + } else { + sigma_t_.push_back(0); + nu_sigma_f_.push_back(0); + sigma_f_.push_back(0); + chi_.push_back(0); + for (int g_in = 0; g_in < negroups_; g_in++) { + sigma_s_.push_back(0); + } + } } } } -void FlatSourceDomain::flux_swap() + +void FlatSourceDomain::set_adjoint_sources(const vector& forward_flux) { - scalar_flux_old_.swap(scalar_flux_new_); + // Set the external source to 1/forward_flux + // The forward flux is given in terms of total for the forward simulation + // so we must convert it to a "per batch" quantity +#pragma omp parallel for + for (int64_t se = 0; se < n_source_elements_; se++) { + external_source_[se] = 1.0 / forward_flux[se]; + } + + // Divide the fixed source term by sigma t (to save time when applying each + // iteration) +#pragma omp parallel for + for (int sr = 0; sr < n_source_regions_; sr++) { + int material = material_[sr]; + for (int g = 0; g < negroups_; g++) { + double sigma_t = sigma_t_[material * negroups_ + g]; + external_source_[sr * negroups_ + g] /= sigma_t; + } + } +} + +void FlatSourceDomain::transpose_scattering_matrix() +{ + // Transpose the inner two dimensions for each material + for (int m = 0; m < n_materials_; ++m) { + int material_offset = m * negroups_ * negroups_; + for (int i = 0; i < negroups_; ++i) { + for (int j = i + 1; j < negroups_; ++j) { + // Calculate indices of the elements to swap + int idx1 = material_offset + i * negroups_ + j; + int idx2 = material_offset + j * negroups_ + i; + + // Swap the elements to transpose the matrix + std::swap(sigma_s_[idx1], sigma_s_[idx2]); + } + } + } } } // namespace openmc diff --git a/src/random_ray/linear_source_domain.cpp b/src/random_ray/linear_source_domain.cpp index 5c3fa91c1..f3f6fa068 100644 --- a/src/random_ray/linear_source_domain.cpp +++ b/src/random_ray/linear_source_domain.cpp @@ -56,40 +56,30 @@ void LinearSourceDomain::update_neutron_source(double k_eff) double inverse_k_eff = 1.0 / k_eff; - // Temperature and angle indices, if using multiple temperature - // data sets and/or anisotropic data sets. - // TODO: Currently assumes we are only using single temp/single - // angle data. - const int t = 0; - const int a = 0; - #pragma omp parallel for for (int sr = 0; sr < n_source_regions_; sr++) { int material = material_[sr]; MomentMatrix invM = mom_matrix_[sr].inverse(); - for (int e_out = 0; e_out < negroups_; e_out++) { - double sigma_t = data::mg.macro_xs_[material].get_xs( - MgxsType::TOTAL, e_out, nullptr, nullptr, nullptr, t, a); + for (int g_out = 0; g_out < negroups_; g_out++) { + double sigma_t = sigma_t_[material * negroups_ + g_out]; double scatter_flat = 0.0f; double fission_flat = 0.0f; MomentArray scatter_linear = {0.0, 0.0, 0.0}; MomentArray fission_linear = {0.0, 0.0, 0.0}; - for (int e_in = 0; e_in < negroups_; e_in++) { + for (int g_in = 0; g_in < negroups_; g_in++) { // Handles for the flat and linear components of the flux - double flux_flat = scalar_flux_old_[sr * negroups_ + e_in]; - MomentArray flux_linear = flux_moments_old_[sr * negroups_ + e_in]; + double flux_flat = scalar_flux_old_[sr * negroups_ + g_in]; + MomentArray flux_linear = flux_moments_old_[sr * negroups_ + g_in]; // Handles for cross sections - double sigma_s = data::mg.macro_xs_[material].get_xs( - MgxsType::NU_SCATTER, e_in, &e_out, nullptr, nullptr, t, a); - double nu_sigma_f = data::mg.macro_xs_[material].get_xs( - MgxsType::NU_FISSION, e_in, nullptr, nullptr, nullptr, t, a); - double chi = data::mg.macro_xs_[material].get_xs( - MgxsType::CHI_PROMPT, e_in, &e_out, nullptr, nullptr, t, a); + double sigma_s = + sigma_s_[material * negroups_ * negroups_ + g_out * negroups_ + g_in]; + double nu_sigma_f = nu_sigma_f_[material * negroups_ + g_in]; + double chi = chi_[material * negroups_ + g_out]; // Compute source terms for flat and linear components of the flux scatter_flat += sigma_s * flux_flat; @@ -99,7 +89,7 @@ void LinearSourceDomain::update_neutron_source(double k_eff) } // Compute the flat source term - source_[sr * negroups_ + e_out] = + source_[sr * negroups_ + g_out] = (scatter_flat + fission_flat * inverse_k_eff) / sigma_t; // Compute the linear source terms @@ -107,7 +97,7 @@ void LinearSourceDomain::update_neutron_source(double k_eff) // are not well known, we will leave the source gradients as zero // so as to avoid causing any numerical instability. if (simulation::current_batch > 10) { - source_gradients_[sr * negroups_ + e_out] = + source_gradients_[sr * negroups_ + g_out] = invM * ((scatter_linear + fission_linear * inverse_k_eff) / sigma_t); } } @@ -116,7 +106,7 @@ void LinearSourceDomain::update_neutron_source(double k_eff) if (settings::run_mode == RunMode::FIXED_SOURCE) { // Add external source to flat source term if in fixed source mode #pragma omp parallel for - for (int se = 0; se < n_source_elements_; se++) { + for (int64_t se = 0; se < n_source_elements_; se++) { source_[se] += external_source_[se]; } } @@ -133,9 +123,9 @@ void LinearSourceDomain::normalize_scalar_flux_and_volumes( // Normalize flux to total distance travelled by all rays this iteration #pragma omp parallel for - for (int64_t e = 0; e < scalar_flux_new_.size(); e++) { - scalar_flux_new_[e] *= normalization_factor; - flux_moments_new_[e] *= normalization_factor; + for (int64_t se = 0; se < scalar_flux_new_.size(); se++) { + scalar_flux_new_[se] *= normalization_factor; + flux_moments_new_[se] *= normalization_factor; } // Accumulate cell-wise ray length tallies collected this iteration, then diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp index eb64cb7d2..7a359f356 100644 --- a/src/random_ray/random_ray.cpp +++ b/src/random_ray/random_ray.cpp @@ -316,17 +316,9 @@ void RandomRay::attenuate_flux_flat_source(double distance, bool is_active) int64_t source_element = source_region * negroups_; int material = this->material(); - // Temperature and angle indices, if using multiple temperature - // data sets and/or anisotropic data sets. - // TODO: Currently assumes we are only using single temp/single - // angle data. - const int t = 0; - const int a = 0; - // MOC incoming flux attenuation + source contribution/attenuation equation for (int g = 0; g < negroups_; g++) { - float sigma_t = data::mg.macro_xs_[material].get_xs( - MgxsType::TOTAL, g, NULL, NULL, NULL, t, a); + float sigma_t = domain_->sigma_t_[material * negroups_ + g]; float tau = sigma_t * distance; float exponential = cjosey_exponential(tau); // exponential = 1 - exp(-tau) float new_delta_psi = @@ -388,13 +380,6 @@ void RandomRay::attenuate_flux_linear_source(double distance, bool is_active) int64_t source_element = source_region * negroups_; int material = this->material(); - // Temperature and angle indices, if using multiple temperature - // data sets and/or anisotropic data sets. - // TODO: Currently assumes we are only using single temp/single - // angle data. - const int t = 0; - const int a = 0; - Position& centroid = domain->centroid_[source_region]; Position midpoint = r() + u() * (distance / 2.0); @@ -422,8 +407,7 @@ void RandomRay::attenuate_flux_linear_source(double distance, bool is_active) for (int g = 0; g < negroups_; g++) { // Compute tau, the optical thickness of the ray segment - float sigma_t = data::mg.macro_xs_[material].get_xs( - MgxsType::TOTAL, g, NULL, NULL, NULL, t, a); + float sigma_t = domain_->sigma_t_[material * negroups_ + g]; float tau = sigma_t * distance; // If tau is very small, set it to zero to avoid numerical issues. diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index 57959e801..a9180c68e 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -23,6 +23,22 @@ namespace openmc { void openmc_run_random_ray() { + ////////////////////////////////////////////////////////// + // Run forward simulation + ////////////////////////////////////////////////////////// + + // Check if adjoint calculation is needed. If it is, we will run the forward + // calculation first and then the adjoint calculation later. + bool adjoint_needed = FlatSourceDomain::adjoint_; + + // Configure the domain for forward simulation + FlatSourceDomain::adjoint_ = false; + + // If we're going to do an adjoint simulation afterwards, report that this is + // the initial forward flux solve. + if (adjoint_needed && mpi::master) + header("FORWARD FLUX SOLVE", 3); + // Initialize OpenMC general data structures openmc_simulation_init(); @@ -30,26 +46,93 @@ void openmc_run_random_ray() if (mpi::master) validate_random_ray_inputs(); - // Initialize Random Ray Simulation Object - RandomRaySimulation sim; + // Declare forward flux so that it can be saved for later adjoint simulation + vector forward_flux; - // Begin main simulation timer - simulation::time_total.start(); + { + // Initialize Random Ray Simulation Object + RandomRaySimulation sim; - // Execute random ray simulation - sim.simulate(); + // Initialize fixed sources, if present + sim.prepare_fixed_sources(); - // End main simulation timer - openmc::simulation::time_total.stop(); + // Begin main simulation timer + simulation::time_total.start(); - // Finalize OpenMC - openmc_simulation_finalize(); + // Execute random ray simulation + sim.simulate(); - // Reduce variables across MPI ranks - sim.reduce_simulation_statistics(); + // End main simulation timer + simulation::time_total.stop(); - // Output all simulation results - sim.output_simulation_results(); + // Normalize and save the final forward flux + forward_flux = sim.domain()->scalar_flux_final_; + + double source_normalization_factor = + sim.domain()->compute_fixed_source_normalization_factor() / + (settings::n_batches - settings::n_inactive); + +#pragma omp parallel for + for (uint64_t i = 0; i < forward_flux.size(); i++) { + forward_flux[i] *= source_normalization_factor; + } + + // Finalize OpenMC + openmc_simulation_finalize(); + + // Reduce variables across MPI ranks + sim.reduce_simulation_statistics(); + + // Output all simulation results + sim.output_simulation_results(); + } + + ////////////////////////////////////////////////////////// + // Run adjoint simulation (if enabled) + ////////////////////////////////////////////////////////// + + if (adjoint_needed) { + reset_timers(); + + // Configure the domain for adjoint simulation + FlatSourceDomain::adjoint_ = true; + + if (mpi::master) + header("ADJOINT FLUX SOLVE", 3); + + // Initialize OpenMC general data structures + openmc_simulation_init(); + + // Initialize Random Ray Simulation Object + RandomRaySimulation adjoint_sim; + + // Initialize adjoint fixed sources, if present + adjoint_sim.prepare_fixed_sources_adjoint(forward_flux); + + // Transpose scattering matrix + adjoint_sim.domain()->transpose_scattering_matrix(); + + // Swap nu_sigma_f and chi + adjoint_sim.domain()->nu_sigma_f_.swap(adjoint_sim.domain()->chi_); + + // Begin main simulation timer + simulation::time_total.start(); + + // Execute random ray simulation + adjoint_sim.simulate(); + + // End main simulation timer + simulation::time_total.stop(); + + // Finalize OpenMC + openmc_simulation_finalize(); + + // Reduce variables across MPI ranks + adjoint_sim.reduce_simulation_statistics(); + + // Output all simulation results + adjoint_sim.output_simulation_results(); + } } // Enforces restrictions on inputs in random ray mode. While there are @@ -254,16 +337,31 @@ RandomRaySimulation::RandomRaySimulation() default: fatal_error("Unknown random ray source shape"); } + + // Convert OpenMC native MGXS into a more efficient format + // internal to the random ray solver + domain_->flatten_xs(); } -void RandomRaySimulation::simulate() +void RandomRaySimulation::prepare_fixed_sources() { if (settings::run_mode == RunMode::FIXED_SOURCE) { // Transfer external source user inputs onto random ray source regions domain_->convert_external_sources(); domain_->count_external_source_regions(); } +} +void RandomRaySimulation::prepare_fixed_sources_adjoint( + vector& forward_flux) +{ + if (settings::run_mode == RunMode::FIXED_SOURCE) { + domain_->set_adjoint_sources(forward_flux); + } +} + +void RandomRaySimulation::simulate() +{ // Random ray power iteration loop while (simulation::current_batch < settings::n_batches) { @@ -314,18 +412,20 @@ void RandomRaySimulation::simulate() } // Execute all tallying tasks, if this is an active batch - if (simulation::current_batch > settings::n_inactive && mpi::master) { - - // Generate mapping between source regions and tallies - if (!domain_->mapped_all_tallies_) { - domain_->convert_source_regions_to_tallies(); - } - - // Use above mapping to contribute FSR flux data to appropriate tallies - domain_->random_ray_tally(); + if (simulation::current_batch > settings::n_inactive) { // Add this iteration's scalar flux estimate to final accumulated estimate domain_->accumulate_iteration_flux(); + + if (mpi::master) { + // Generate mapping between source regions and tallies + if (!domain_->mapped_all_tallies_) { + domain_->convert_source_regions_to_tallies(); + } + + // Use above mapping to contribute FSR flux data to appropriate tallies + domain_->random_ray_tally(); + } } // Set phi_old = phi_new @@ -448,6 +548,9 @@ void RandomRaySimulation::print_results_random_ray( } fmt::print(" Volume Estimator Type = {}\n", estimator); + std::string adjoint_true = (FlatSourceDomain::adjoint_) ? "ON" : "OFF"; + fmt::print(" Adjoint Flux Mode = {}\n", adjoint_true); + header("Timing Statistics", 4); show_time("Total time for initialization", time_initialize.elapsed()); show_time("Reading cross sections", time_read_xs.elapsed(), 1); diff --git a/src/settings.cpp b/src/settings.cpp index 3a905bdf9..d52177ae8 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -301,6 +301,10 @@ void get_run_parameters(pugi::xml_node node_base) FlatSourceDomain::volume_normalized_flux_tallies_ = get_node_value_bool(random_ray_node, "volume_normalized_flux_tallies"); } + if (check_for_node(random_ray_node, "adjoint")) { + FlatSourceDomain::adjoint_ = + get_node_value_bool(random_ray_node, "adjoint"); + } } } diff --git a/tests/regression_tests/random_ray_adjoint_fixed_source/__init__.py b/tests/regression_tests/random_ray_adjoint_fixed_source/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat b/tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat new file mode 100644 index 000000000..686987512 --- /dev/null +++ b/tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat @@ -0,0 +1,245 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + True + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_adjoint_fixed_source/results_true.dat b/tests/regression_tests/random_ray_adjoint_fixed_source/results_true.dat new file mode 100644 index 000000000..a216fa9fc --- /dev/null +++ b/tests/regression_tests/random_ray_adjoint_fixed_source/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +-7.235364E+03 +3.367109E+09 +tally 2: +4.818311E+05 +6.269371E+10 +tally 3: +1.515641E+06 +4.598791E+11 diff --git a/tests/regression_tests/random_ray_adjoint_fixed_source/test.py b/tests/regression_tests/random_ray_adjoint_fixed_source/test.py new file mode 100644 index 000000000..0295e36e9 --- /dev/null +++ b/tests/regression_tests/random_ray_adjoint_fixed_source/test.py @@ -0,0 +1,20 @@ +import os + +from openmc.examples import random_ray_three_region_cube + +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_adjoint_fixed_source(): + model = random_ray_three_region_cube() + model.settings.random_ray['adjoint'] = True + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_adjoint_k_eff/__init__.py b/tests/regression_tests/random_ray_adjoint_k_eff/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_adjoint_k_eff/inputs_true.dat b/tests/regression_tests/random_ray_adjoint_k_eff/inputs_true.dat new file mode 100644 index 000000000..725702a49 --- /dev/null +++ b/tests/regression_tests/random_ray_adjoint_k_eff/inputs_true.dat @@ -0,0 +1,110 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.126 0.126 + 10 10 + -0.63 -0.63 + +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 + + + 1.26 1.26 + 2 2 + -1.26 -1.26 + +2 2 +2 5 + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + multi-group + + 100.0 + 20.0 + + + -1.26 -1.26 -1 1.26 1.26 1 + + + True + True + + + + + 2 2 + -1.26 -1.26 + 1.26 1.26 + + + 1 + + + 1e-05 0.0635 10.0 100.0 1000.0 500000.0 1000000.0 20000000.0 + + + 1 2 + flux fission nu-fission + analog + + + diff --git a/tests/regression_tests/random_ray_adjoint_k_eff/results_true.dat b/tests/regression_tests/random_ray_adjoint_k_eff/results_true.dat new file mode 100644 index 000000000..1690d46e9 --- /dev/null +++ b/tests/regression_tests/random_ray_adjoint_k_eff/results_true.dat @@ -0,0 +1,171 @@ +k-combined: +1.006640E+00 1.812967E-03 +tally 1: +6.684129E+00 +8.939821E+00 +2.685967E+00 +1.443592E+00 +0.000000E+00 +0.000000E+00 +6.358774E+00 +8.091444E+00 +9.687217E-01 +1.878029E-01 +0.000000E+00 +0.000000E+00 +5.963160E+00 +7.117108E+00 +1.932332E-01 +7.473914E-03 +0.000000E+00 +0.000000E+00 +5.137593E+00 +5.283310E+00 +1.714616E-01 +5.884834E-03 +1.086218E-06 +2.361752E-13 +4.857253E+00 +4.719856E+00 +5.689580E-02 +6.476286E-04 +2.989356E-03 +1.787808E-06 +4.830516E+00 +4.666801E+00 +7.203015E-03 +1.037676E-05 +3.620020E+00 +2.620927E+00 +5.161382E+00 +5.328124E+00 +6.786255E-02 +9.210763E-04 +5.531943E+00 +6.120553E+00 +5.414034E+00 +5.864661E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.632338E+00 +6.347626E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.682608E+00 +6.462382E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.310716E+00 +5.645180E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.945409E+00 +4.893171E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.842688E+00 +4.690352E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.117198E+00 +5.237280E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +6.938711E+00 +9.633345E+00 +2.835258E+00 +1.608212E+00 +0.000000E+00 +0.000000E+00 +6.549505E+00 +8.584036E+00 +1.015138E+00 +2.061993E-01 +0.000000E+00 +0.000000E+00 +6.050651E+00 +7.327711E+00 +1.992816E-01 +7.948424E-03 +0.000000E+00 +0.000000E+00 +5.113981E+00 +5.234801E+00 +1.732323E-01 +6.006619E-03 +1.097435E-06 +2.410627E-13 +4.837033E+00 +4.680541E+00 +5.760042E-02 +6.637112E-04 +3.026377E-03 +1.832205E-06 +4.827049E+00 +4.660105E+00 +7.319913E-03 +1.071647E-05 +3.678770E+00 +2.706730E+00 +5.175337E+00 +5.356957E+00 +6.923046E-02 +9.586177E-04 +5.643451E+00 +6.370016E+00 +6.693323E+00 +8.964322E+00 +2.753307E+00 +1.516683E+00 +0.000000E+00 +0.000000E+00 +6.358384E+00 +8.090233E+00 +9.912008E-01 +1.965868E-01 +0.000000E+00 +0.000000E+00 +5.957484E+00 +7.103246E+00 +1.974033E-01 +7.798286E-03 +0.000000E+00 +0.000000E+00 +5.130744E+00 +5.268844E+00 +1.749233E-01 +6.123348E-03 +1.108148E-06 +2.457474E-13 +4.857340E+00 +4.720019E+00 +5.816659E-02 +6.768049E-04 +3.056125E-03 +1.868351E-06 +4.830629E+00 +4.667018E+00 +7.366289E-03 +1.085264E-05 +3.702077E+00 +2.741125E+00 +5.164864E+00 +5.335279E+00 +6.947917E-02 +9.655086E-04 +5.663725E+00 +6.415806E+00 diff --git a/tests/regression_tests/random_ray_adjoint_k_eff/test.py b/tests/regression_tests/random_ray_adjoint_k_eff/test.py new file mode 100644 index 000000000..44cf1182a --- /dev/null +++ b/tests/regression_tests/random_ray_adjoint_k_eff/test.py @@ -0,0 +1,20 @@ +import os + +from openmc.examples import random_ray_lattice + +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_basic(): + model = random_ray_lattice() + model.settings.random_ray['adjoint'] = True + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() From ae37d6c0da33b6941f726e506db78736fd1d51d1 Mon Sep 17 00:00:00 2001 From: Zoe Prieto <101403129+zoeprieto@users.noreply.github.com> Date: Fri, 22 Nov 2024 12:16:49 -0300 Subject: [PATCH 208/671] Statistical weights in IndependentSource (#3195) Co-authored-by: Paul Wilson Co-authored-by: Paul Romano --- docs/source/usersguide/settings.rst | 20 ++++++ include/openmc/settings.h | 47 +++++++------- openmc/settings.py | 25 ++++++++ src/finalize.cpp | 1 + src/settings.cpp | 7 +++ src/source.cpp | 21 +++++-- src/tallies/tally.cpp | 3 +- .../test_uniform_source_sampling.py | 63 +++++++++++++++++++ 8 files changed, 157 insertions(+), 30 deletions(-) create mode 100644 tests/unit_tests/test_uniform_source_sampling.py diff --git a/docs/source/usersguide/settings.rst b/docs/source/usersguide/settings.rst index 349aa34d0..ca11d6487 100644 --- a/docs/source/usersguide/settings.rst +++ b/docs/source/usersguide/settings.rst @@ -183,6 +183,7 @@ source distributions and has four main attributes that one can set: :attr:`IndependentSource.energy`, which defines the energy distribution, and :attr:`IndependentSource.time`, which defines the time distribution. + The spatial distribution can be set equal to a sub-class of :class:`openmc.stats.Spatial`; common choices are :class:`openmc.stats.Point` or :class:`openmc.stats.Box`. To independently specify distributions in the @@ -225,6 +226,7 @@ distribution. This could be a probability mass function (:class:`openmc.stats.Tabular`). By default, if no time distribution is specified, particles are started at :math:`t=0`. + As an example, to create an isotropic, 10 MeV monoenergetic source uniformly distributed over a cube centered at the origin with an edge length of 10 cm, and emitting a pulse of particles from 0 to 10 µs, one @@ -252,6 +254,24 @@ sampled 70% of the time and another that should be sampled 30% of the time:: settings.source = [src1, src2] +When the relative strengths are several orders of magnitude different, it may +happen that not enough statistics are obtained from the lower strength source. +This can be improved by sampling among the sources with equal probability, +applying the source strength as a weight on the sampled source particles. The +:attr:`Settings.uniform_source_sampling` attribute can be used to enable this +option:: + + src1 = openmc.IndependentSource() + src1.strength = 100.0 + ... + + src2 = openmc.IndependentSource() + src2.strength = 1.0 + ... + + settings.source = [src1, src2] + settings.uniform_source_sampling = True + Finally, the :attr:`IndependentSource.particle` attribute can be used to indicate the source should be composed of particles other than neutrons. For example, the following would generate a photon source:: diff --git a/include/openmc/settings.h b/include/openmc/settings.h index 1e44c0880..9a4ce56ec 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -44,29 +44,30 @@ extern "C" bool entropy_on; //!< calculate Shannon entropy? extern "C" bool event_based; //!< use event-based mode (instead of history-based) extern bool legendre_to_tabular; //!< convert Legendre distributions to tabular? -extern bool material_cell_offsets; //!< create material cells offsets? -extern "C" bool output_summary; //!< write summary.h5? -extern bool output_tallies; //!< write tallies.out? -extern bool particle_restart_run; //!< particle restart run? -extern "C" bool photon_transport; //!< photon transport turned on? -extern "C" bool reduce_tallies; //!< reduce tallies at end of batch? -extern bool res_scat_on; //!< use resonance upscattering method? -extern "C" bool restart_run; //!< restart run? -extern "C" bool run_CE; //!< run with continuous-energy data? -extern bool source_latest; //!< write latest source at each batch? -extern bool source_separate; //!< write source to separate file? -extern bool source_write; //!< write source in HDF5 files? -extern bool source_mcpl_write; //!< write source in mcpl files? -extern bool surf_source_write; //!< write surface source file? -extern bool surf_mcpl_write; //!< write surface mcpl file? -extern bool surf_source_read; //!< read surface source file? -extern bool survival_biasing; //!< use survival biasing? -extern bool temperature_multipole; //!< use multipole data? -extern "C" bool trigger_on; //!< tally triggers enabled? -extern bool trigger_predict; //!< predict batches for triggers? -extern bool ufs_on; //!< uniform fission site method on? -extern bool urr_ptables_on; //!< use unresolved resonance prob. tables? -extern "C" bool weight_windows_on; //!< are weight windows are enabled? +extern bool material_cell_offsets; //!< create material cells offsets? +extern "C" bool output_summary; //!< write summary.h5? +extern bool output_tallies; //!< write tallies.out? +extern bool particle_restart_run; //!< particle restart run? +extern "C" bool photon_transport; //!< photon transport turned on? +extern "C" bool reduce_tallies; //!< reduce tallies at end of batch? +extern bool res_scat_on; //!< use resonance upscattering method? +extern "C" bool restart_run; //!< restart run? +extern "C" bool run_CE; //!< run with continuous-energy data? +extern bool source_latest; //!< write latest source at each batch? +extern bool source_separate; //!< write source to separate file? +extern bool source_write; //!< write source in HDF5 files? +extern bool source_mcpl_write; //!< write source in mcpl files? +extern bool surf_source_write; //!< write surface source file? +extern bool surf_mcpl_write; //!< write surface mcpl file? +extern bool surf_source_read; //!< read surface source file? +extern bool survival_biasing; //!< use survival biasing? +extern bool temperature_multipole; //!< use multipole data? +extern "C" bool trigger_on; //!< tally triggers enabled? +extern bool trigger_predict; //!< predict batches for triggers? +extern bool uniform_source_sampling; //!< sample sources uniformly? +extern bool ufs_on; //!< uniform fission site method on? +extern bool urr_ptables_on; //!< use unresolved resonance prob. tables? +extern "C" bool weight_windows_on; //!< are weight windows are enabled? extern bool weight_window_checkpoint_surface; //!< enable weight window check //!< upon surface crossing? extern bool weight_window_checkpoint_collision; //!< enable weight window check diff --git a/openmc/settings.py b/openmc/settings.py index a350de72e..77598b204 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -266,6 +266,9 @@ class Settings: Maximum number of batches simulated. If this is set, the number of batches specified via ``batches`` is interpreted as the minimum number of batches + uniform_source_sampling : bool + Whether to sampling among multiple sources uniformly, applying their + strengths as weights to sampled particles. ufs_mesh : openmc.RegularMesh Mesh to be used for redistributing source sites via the uniform fission site (UFS) method. @@ -328,6 +331,7 @@ class Settings: self._photon_transport = None self._plot_seed = None self._ptables = None + self._uniform_source_sampling = None self._seed = None self._survival_biasing = None @@ -575,6 +579,15 @@ class Settings: cv.check_type('photon transport', photon_transport, bool) self._photon_transport = photon_transport + @property + def uniform_source_sampling(self) -> bool: + return self._uniform_source_sampling + + @uniform_source_sampling.setter + def uniform_source_sampling(self, uniform_source_sampling: bool): + cv.check_type('strength as weights', uniform_source_sampling, bool) + self._uniform_source_sampling = uniform_source_sampling + @property def plot_seed(self): return self._plot_seed @@ -1221,6 +1234,11 @@ class Settings: subelement.text = ' '.join( str(x) for x in self._statepoint['batches']) + def _create_uniform_source_sampling_subelement(self, root): + if self._uniform_source_sampling is not None: + element = ET.SubElement(root, "uniform_source_sampling") + element.text = str(self._uniform_source_sampling).lower() + def _create_sourcepoint_subelement(self, root): if self._sourcepoint: element = ET.SubElement(root, "source_point") @@ -1702,6 +1720,11 @@ class Settings: if text is not None: self.photon_transport = text in ('true', '1') + def _uniform_source_sampling_from_xml_element(self, root): + text = get_text(root, 'uniform_source_sampling') + if text is not None: + self.uniform_source_sampling = text in ('true', '1') + def _plot_seed_from_xml_element(self, root): text = get_text(root, 'plot_seed') if text is not None: @@ -1957,6 +1980,7 @@ class Settings: self._create_energy_mode_subelement(element) self._create_max_order_subelement(element) self._create_photon_transport_subelement(element) + self._create_uniform_source_sampling_subelement(element) self._create_plot_seed_subelement(element) self._create_ptables_subelement(element) self._create_seed_subelement(element) @@ -2063,6 +2087,7 @@ class Settings: settings._energy_mode_from_xml_element(elem) settings._max_order_from_xml_element(elem) settings._photon_transport_from_xml_element(elem) + settings._uniform_source_sampling_from_xml_element(elem) settings._plot_seed_from_xml_element(elem) settings._ptables_from_xml_element(elem) settings._seed_from_xml_element(elem) diff --git a/src/finalize.cpp b/src/finalize.cpp index 08c2fced3..981ec5cba 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -133,6 +133,7 @@ int openmc_finalize() settings::trigger_on = false; settings::trigger_predict = false; settings::trigger_batch_interval = 1; + settings::uniform_source_sampling = false; settings::ufs_on = false; settings::urr_ptables_on = true; settings::verbosity = 7; diff --git a/src/settings.cpp b/src/settings.cpp index d52177ae8..5e11949bb 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -72,6 +72,7 @@ bool survival_biasing {false}; bool temperature_multipole {false}; bool trigger_on {false}; bool trigger_predict {false}; +bool uniform_source_sampling {false}; bool ufs_on {false}; bool urr_ptables_on {true}; bool weight_windows_on {false}; @@ -786,6 +787,12 @@ void read_settings_xml(pugi::xml_node root) sourcepoint_batch = statepoint_batch; } + // Check is the user specified to convert strength to statistical weight + if (check_for_node(root, "uniform_source_sampling")) { + uniform_source_sampling = + get_node_value_bool(root, "uniform_source_sampling"); + } + // Check if the user has specified to write surface source if (check_for_node(root, "surf_source_write")) { surf_source_write = true; diff --git a/src/source.cpp b/src/source.cpp index 15fe8433b..9d3cae6cf 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -616,18 +616,27 @@ SourceSite sample_external_source(uint64_t* seed) // Sample from among multiple source distributions int i = 0; if (model::external_sources.size() > 1) { - double xi = prn(seed) * total_strength; - double c = 0.0; - for (; i < model::external_sources.size(); ++i) { - c += model::external_sources[i]->strength(); - if (xi < c) - break; + if (settings::uniform_source_sampling) { + i = prn(seed) * model::external_sources.size(); + } else { + double xi = prn(seed) * total_strength; + double c = 0.0; + for (; i < model::external_sources.size(); ++i) { + c += model::external_sources[i]->strength(); + if (xi < c) + break; + } } } // Sample source site from i-th source distribution SourceSite site {model::external_sources[i]->sample_with_constraints(seed)}; + // Set particle creation weight + if (settings::uniform_source_sampling) { + site.wgt *= model::external_sources[i]->strength(); + } + // If running in MG, convert site.E to group if (!settings::run_CE) { site.E = lower_bound_index(data::mg.rev_energy_bins_.begin(), diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index ba899611c..4f33abf6b 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -751,7 +751,8 @@ void Tally::accumulate() if (mpi::master || !settings::reduce_tallies) { // Calculate total source strength for normalization double total_source = 0.0; - if (settings::run_mode == RunMode::FIXED_SOURCE) { + if (settings::run_mode == RunMode::FIXED_SOURCE && + !settings::uniform_source_sampling) { for (const auto& s : model::external_sources) { total_source += s->strength(); } diff --git a/tests/unit_tests/test_uniform_source_sampling.py b/tests/unit_tests/test_uniform_source_sampling.py new file mode 100644 index 000000000..ece8afe89 --- /dev/null +++ b/tests/unit_tests/test_uniform_source_sampling.py @@ -0,0 +1,63 @@ +import openmc +import pytest + + +@pytest.fixture +def sphere_model(): + mat = openmc.Material() + mat.add_nuclide('Li6', 1.0) + mat.set_density('g/cm3', 1.0) + sphere = openmc.Sphere(r=1.0, boundary_type='vacuum') + cell = openmc.Cell(region=-sphere, fill=mat) + model = openmc.Model() + model.geometry = openmc.Geometry([cell]) + + model.settings.particles = 100 + model.settings.batches = 1 + model.settings.source = openmc.IndependentSource( + energy=openmc.stats.delta_function(1.0e3), + strength=100.0 + ) + model.settings.run_mode = "fixed source" + model.settings.surf_source_write = { + "max_particles": 100, + } + + tally = openmc.Tally() + tally.scores = ['flux'] + model.tallies = [tally] + return model + + +def test_source_weight(run_in_tmpdir, sphere_model): + # Run OpenMC without uniform source sampling and check that banked particles + # have weight 1 + sphere_model.settings.uniform_source_sampling = False + sphere_model.run() + particles = openmc.ParticleList.from_hdf5('surface_source.h5') + assert set(p.wgt for p in particles) == {1.0} + + # Run with uniform source sampling and check that banked particles have + # weight == strength + sphere_model.settings.uniform_source_sampling = True + sphere_model.run() + particles = openmc.ParticleList.from_hdf5('surface_source.h5') + strength = sphere_model.settings.source[0].strength + assert set(p.wgt for p in particles) == {strength} + + +def test_tally_mean(run_in_tmpdir, sphere_model): + # Run without uniform source sampling + sphere_model.settings.uniform_source_sampling = False + sp_file = sphere_model.run() + with openmc.StatePoint(sp_file) as sp: + reference_mean = sp.tallies[sphere_model.tallies[0].id].mean + + # Run with uniform source sampling + sphere_model.settings.uniform_source_sampling = True + sp_file = sphere_model.run() + with openmc.StatePoint(sp_file) as sp: + mean = sp.tallies[sphere_model.tallies[0].id].mean + + # Check that tally means match + assert mean == pytest.approx(reference_mean) From dd01c40ae1f320dc74a5ec2674b92b09b9d03361 Mon Sep 17 00:00:00 2001 From: Kevin Sawatzky <66632997+nuclearkevin@users.noreply.github.com> Date: Sat, 23 Nov 2024 08:09:47 -0600 Subject: [PATCH 209/671] Enable adaptive mesh support on libMesh tallies (#3185) Co-authored-by: Patrick Shriwise --- include/openmc/mesh.h | 11 +++++- src/mesh.cpp | 86 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 82 insertions(+), 15 deletions(-) diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index f0ad57247..5edb03003 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -948,6 +948,7 @@ public: private: void initialize() override; void set_mesh_pointer_from_filename(const std::string& filename); + void build_eqn_sys(); // Methods @@ -966,7 +967,8 @@ private: vector> pl_; //!< per-thread point locators unique_ptr - equation_systems_; //!< pointer to the equation systems of the mesh + equation_systems_; //!< pointer to the libMesh EquationSystems + //!< instance std::string eq_system_name_; //!< name of the equation system holding OpenMC results std::unordered_map @@ -975,6 +977,13 @@ private: libMesh::BoundingBox bbox_; //!< bounding box of the mesh libMesh::dof_id_type first_element_id_; //!< id of the first element in the mesh + + const bool adaptive_; //!< whether this mesh has adaptivity enabled or not + std::vector + bin_to_elem_map_; //!< mapping bin indices to dof indices for active + //!< elements + std::vector elem_to_bin_map_; //!< mapping dof indices to bin indices for + //!< active elements }; #endif diff --git a/src/mesh.cpp b/src/mesh.cpp index b90fead03..0b051e63f 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -2922,7 +2922,7 @@ void MOABMesh::write(const std::string& base_filename) const const std::string LibMesh::mesh_lib_type = "libmesh"; -LibMesh::LibMesh(pugi::xml_node node) : UnstructuredMesh(node) +LibMesh::LibMesh(pugi::xml_node node) : UnstructuredMesh(node), adaptive_(false) { // filename_ and length_multiplier_ will already be set by the // UnstructuredMesh constructor @@ -2933,6 +2933,7 @@ LibMesh::LibMesh(pugi::xml_node node) : UnstructuredMesh(node) // create the mesh from a pointer to a libMesh Mesh LibMesh::LibMesh(libMesh::MeshBase& input_mesh, double length_multiplier) + : adaptive_(input_mesh.n_active_elem() != input_mesh.n_elem()) { m_ = &input_mesh; set_length_multiplier(length_multiplier); @@ -2941,6 +2942,7 @@ LibMesh::LibMesh(libMesh::MeshBase& input_mesh, double length_multiplier) // create the mesh from an input file LibMesh::LibMesh(const std::string& filename, double length_multiplier) + : adaptive_(false) { set_mesh_pointer_from_filename(filename); set_length_multiplier(length_multiplier); @@ -2955,6 +2957,15 @@ void LibMesh::set_mesh_pointer_from_filename(const std::string& filename) m_->read(filename_); } +// build a libMesh equation system for storing values +void LibMesh::build_eqn_sys() +{ + eq_system_name_ = fmt::format("mesh_{}_system", id_); + equation_systems_ = make_unique(*m_); + libMesh::ExplicitSystem& eq_sys = + equation_systems_->add_system(eq_system_name_); +} + // intialize from mesh file void LibMesh::initialize() { @@ -2982,13 +2993,6 @@ void LibMesh::initialize() filename_)); } - // create an equation system for storing values - eq_system_name_ = fmt::format("mesh_{}_system", id_); - - equation_systems_ = make_unique(*m_); - libMesh::ExplicitSystem& eq_sys = - equation_systems_->add_system(eq_system_name_); - for (int i = 0; i < num_threads(); i++) { pl_.emplace_back(m_->sub_point_locator()); pl_.back()->set_contains_point_tol(FP_COINCIDENT); @@ -2999,6 +3003,21 @@ void LibMesh::initialize() auto first_elem = *m_->elements_begin(); first_element_id_ = first_elem->id(); + // if the mesh is adaptive elements aren't guaranteed by libMesh to be + // contiguous in ID space, so we need to map from bin indices (defined over + // active elements) to global dof ids + if (adaptive_) { + bin_to_elem_map_.reserve(m_->n_active_local_elem()); + elem_to_bin_map_.resize(m_->n_local_elem(), -1); + for (auto it = m_->active_local_elements_begin(); + it != m_->active_local_elements_end(); it++) { + auto elem = *it; + + bin_to_elem_map_.push_back(elem->id()); + elem_to_bin_map_[elem->id()] = bin_to_elem_map_.size() - 1; + } + } + // bounding box for the mesh for quick rejection checks bbox_ = libMesh::MeshTools::create_bounding_box(*m_); libMesh::Point ll = bbox_.min(); @@ -3056,7 +3075,7 @@ std::string LibMesh::library() const int LibMesh::n_bins() const { - return m_->n_elem(); + return m_->n_active_elem(); } int LibMesh::n_surface_bins() const @@ -3079,6 +3098,18 @@ int LibMesh::n_surface_bins() const void LibMesh::add_score(const std::string& var_name) { + if (adaptive_) { + warning(fmt::format( + "Exodus output cannot be provided as unstructured mesh {} is adaptive.", + this->id_)); + + return; + } + + if (!equation_systems_) { + build_eqn_sys(); + } + // check if this is a new variable std::string value_name = var_name + "_mean"; if (!variable_map_.count(value_name)) { @@ -3100,14 +3131,28 @@ void LibMesh::add_score(const std::string& var_name) void LibMesh::remove_scores() { - auto& eqn_sys = equation_systems_->get_system(eq_system_name_); - eqn_sys.clear(); - variable_map_.clear(); + if (equation_systems_) { + auto& eqn_sys = equation_systems_->get_system(eq_system_name_); + eqn_sys.clear(); + variable_map_.clear(); + } } void LibMesh::set_score_data(const std::string& var_name, const vector& values, const vector& std_dev) { + if (adaptive_) { + warning(fmt::format( + "Exodus output cannot be provided as unstructured mesh {} is adaptive.", + this->id_)); + + return; + } + + if (!equation_systems_) { + build_eqn_sys(); + } + auto& eqn_sys = equation_systems_->get_system(eq_system_name_); if (!eqn_sys.is_initialized()) { @@ -3125,6 +3170,10 @@ void LibMesh::set_score_data(const std::string& var_name, for (auto it = m_->local_elements_begin(); it != m_->local_elements_end(); it++) { + if (!(*it)->active()) { + continue; + } + auto bin = get_bin_from_element(*it); // set value @@ -3143,6 +3192,14 @@ void LibMesh::set_score_data(const std::string& var_name, void LibMesh::write(const std::string& filename) const { + if (adaptive_) { + warning(fmt::format( + "Exodus output cannot be provided as unstructured mesh {} is adaptive.", + this->id_)); + + return; + } + write_message(fmt::format( "Writing file: {}.e for unstructured mesh {}", filename, this->id_)); libMesh::ExodusII_IO exo(*m_); @@ -3176,7 +3233,8 @@ int LibMesh::get_bin(Position r) const int LibMesh::get_bin_from_element(const libMesh::Elem* elem) const { - int bin = elem->id() - first_element_id_; + int bin = + adaptive_ ? elem_to_bin_map_[elem->id()] : elem->id() - first_element_id_; if (bin >= n_bins() || bin < 0) { fatal_error(fmt::format("Invalid bin: {}", bin)); } @@ -3191,7 +3249,7 @@ std::pair, vector> LibMesh::plot( const libMesh::Elem& LibMesh::get_element_from_bin(int bin) const { - return m_->elem_ref(bin); + return adaptive_ ? m_->elem_ref(bin_to_elem_map_.at(bin)) : m_->elem_ref(bin); } double LibMesh::volume(int bin) const From 2d988a69a10dec9e2964782d20a08927574d763a Mon Sep 17 00:00:00 2001 From: Ebny Walid Ahammed <69362074+magnoxemo@users.noreply.github.com> Date: Sat, 23 Nov 2024 13:10:39 -0600 Subject: [PATCH 210/671] External sources alias sampler (#3201) Co-authored-by: Paul Romano --- include/openmc/source.h | 3 +++ openmc/model/model.py | 4 ++-- src/settings.cpp | 7 +++++++ src/source.cpp | 18 +++++------------- tests/regression_tests/source/results_true.dat | 2 +- .../unit_tests/test_uniform_source_sampling.py | 12 ++++++++++++ 6 files changed, 30 insertions(+), 16 deletions(-) diff --git a/include/openmc/source.h b/include/openmc/source.h index 5cc0356e3..6733eaeff 100644 --- a/include/openmc/source.h +++ b/include/openmc/source.h @@ -36,6 +36,9 @@ namespace model { extern vector> external_sources; +// Probability distribution for selecting external sources +extern DiscreteIndex external_sources_probability; + } // namespace model //============================================================================== diff --git a/openmc/model/model.py b/openmc/model/model.py index ffad8f503..49cb0a83e 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -885,7 +885,7 @@ class Model: n_samples: int = 1000, prn_seed: int | None = None, **init_kwargs - ) -> list[openmc.SourceParticle]: + ) -> openmc.ParticleList: """Sample external source and return source particles. .. versionadded:: 0.15.1 @@ -902,7 +902,7 @@ class Model: Returns ------- - list of openmc.SourceParticle + openmc.ParticleList List of samples source particles """ import openmc.lib diff --git a/src/settings.cpp b/src/settings.cpp index 5e11949bb..61eda7996 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -563,6 +563,13 @@ void read_settings_xml(pugi::xml_node root) model::external_sources.push_back(make_unique(path)); } + // Build probability mass function for sampling external sources + vector source_strengths; + for (auto& s : model::external_sources) { + source_strengths.push_back(s->strength()); + } + model::external_sources_probability.assign(source_strengths); + // If no source specified, default to isotropic point source at origin with // Watt spectrum. No default source is needed in random ray mode. if (model::external_sources.empty() && diff --git a/src/source.cpp b/src/source.cpp index 9d3cae6cf..2116809f2 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -44,7 +44,10 @@ namespace openmc { namespace model { vector> external_sources; -} + +DiscreteIndex external_sources_probability; + +} // namespace model //============================================================================== // Source implementation @@ -608,24 +611,13 @@ void initialize_source() SourceSite sample_external_source(uint64_t* seed) { - // Determine total source strength - double total_strength = 0.0; - for (auto& s : model::external_sources) - total_strength += s->strength(); - // Sample from among multiple source distributions int i = 0; if (model::external_sources.size() > 1) { if (settings::uniform_source_sampling) { i = prn(seed) * model::external_sources.size(); } else { - double xi = prn(seed) * total_strength; - double c = 0.0; - for (; i < model::external_sources.size(); ++i) { - c += model::external_sources[i]->strength(); - if (xi < c) - break; - } + i = model::external_sources_probability.sample(seed); } } diff --git a/tests/regression_tests/source/results_true.dat b/tests/regression_tests/source/results_true.dat index 94d9ced15..673d27c8a 100644 --- a/tests/regression_tests/source/results_true.dat +++ b/tests/regression_tests/source/results_true.dat @@ -1,2 +1,2 @@ k-combined: -3.054101E-01 1.167865E-03 +2.959436E-01 2.782384E-03 diff --git a/tests/unit_tests/test_uniform_source_sampling.py b/tests/unit_tests/test_uniform_source_sampling.py index ece8afe89..7f805e37d 100644 --- a/tests/unit_tests/test_uniform_source_sampling.py +++ b/tests/unit_tests/test_uniform_source_sampling.py @@ -61,3 +61,15 @@ def test_tally_mean(run_in_tmpdir, sphere_model): # Check that tally means match assert mean == pytest.approx(reference_mean) + + +def test_multiple_sources(sphere_model): + low_strength_src = openmc.IndependentSource( + energy=openmc.stats.delta_function(1.0e6), strength=1e-7) + sphere_model.settings.source.append(low_strength_src) + sphere_model.settings.uniform_source_sampling = True + + # Sample particles from source and make sure 1 MeV shows up despite + # negligible strength + particles = sphere_model.sample_external_source(100) + assert {p.E for p in particles} == {1.0e3, 1.0e6} From a9fe2a05c114796ceb7a640e9d68c823dfa4da16 Mon Sep 17 00:00:00 2001 From: Kevin Sawatzky <66632997+nuclearkevin@users.noreply.github.com> Date: Tue, 26 Nov 2024 21:23:29 -0600 Subject: [PATCH 211/671] Fix bin index to DoF ID mapping bug in adaptive libMesh meshes (#3206) --- src/mesh.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/mesh.cpp b/src/mesh.cpp index 0b051e63f..ca6440210 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -3007,10 +3007,10 @@ void LibMesh::initialize() // contiguous in ID space, so we need to map from bin indices (defined over // active elements) to global dof ids if (adaptive_) { - bin_to_elem_map_.reserve(m_->n_active_local_elem()); - elem_to_bin_map_.resize(m_->n_local_elem(), -1); - for (auto it = m_->active_local_elements_begin(); - it != m_->active_local_elements_end(); it++) { + bin_to_elem_map_.reserve(m_->n_active_elem()); + elem_to_bin_map_.resize(m_->n_elem(), -1); + for (auto it = m_->active_elements_begin(); it != m_->active_elements_end(); + it++) { auto elem = *it; bin_to_elem_map_.push_back(elem->id()); From a940216d3a10800fc7d63f305cb481196c86e3fb Mon Sep 17 00:00:00 2001 From: Kevin Sawatzky <66632997+nuclearkevin@users.noreply.github.com> Date: Mon, 2 Dec 2024 22:41:41 -0600 Subject: [PATCH 212/671] Ensure libMesh::ReplicatedMesh is used for LibMesh tallies (#3208) --- src/mesh.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/mesh.cpp b/src/mesh.cpp index ca6440210..c572d255a 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -2935,6 +2935,11 @@ LibMesh::LibMesh(pugi::xml_node node) : UnstructuredMesh(node), adaptive_(false) LibMesh::LibMesh(libMesh::MeshBase& input_mesh, double length_multiplier) : adaptive_(input_mesh.n_active_elem() != input_mesh.n_elem()) { + if (!dynamic_cast(&input_mesh)) { + fatal_error("At present LibMesh tallies require a replicated mesh. Please " + "ensure 'input_mesh' is a libMesh::ReplicatedMesh."); + } + m_ = &input_mesh; set_length_multiplier(length_multiplier); initialize(); @@ -2952,7 +2957,8 @@ LibMesh::LibMesh(const std::string& filename, double length_multiplier) void LibMesh::set_mesh_pointer_from_filename(const std::string& filename) { filename_ = filename; - unique_m_ = make_unique(*settings::libmesh_comm, n_dimension_); + unique_m_ = + make_unique(*settings::libmesh_comm, n_dimension_); m_ = unique_m_.get(); m_->read(filename_); } From de8132a5a431660f5ff515cc7894ea0f283d3bec Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Tue, 3 Dec 2024 08:22:46 +0100 Subject: [PATCH 213/671] adding unstrucutred mesh file suffix to docstring (#3211) Co-authored-by: Paul Romano --- openmc/mesh.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openmc/mesh.py b/openmc/mesh.py index 6afe5d36e..ef5c6c784 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -2072,7 +2072,9 @@ class UnstructuredMesh(MeshBase): Parameters ---------- filename : path-like - Location of the unstructured mesh file + Location of the unstructured mesh file. Supported files for 'moab' + library are .h5 and .vtk. Supported files for 'libmesh' library are + exodus mesh files .exo. library : {'moab', 'libmesh'} Mesh library used for the unstructured mesh tally mesh_id : int From 775c41512283d178a49c787dc5fdfe617bac4938 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Mon, 16 Dec 2024 08:40:02 -0600 Subject: [PATCH 214/671] Write and read mesh name attribute (#3221) --- docs/source/io_formats/statepoint.rst | 5 +- docs/source/io_formats/tallies.rst | 7 +- include/openmc/mesh.h | 20 ++-- openmc/mesh.py | 92 ++++++++++--------- src/mesh.cpp | 55 +++++------ .../tally_slice_merge/inputs_true.dat | 2 +- .../tally_slice_merge/test.py | 3 + tests/unit_tests/test_mesh.py | 21 +++++ 8 files changed, 126 insertions(+), 79 deletions(-) diff --git a/docs/source/io_formats/statepoint.rst b/docs/source/io_formats/statepoint.rst index f61e967ca..82d20a763 100644 --- a/docs/source/io_formats/statepoint.rst +++ b/docs/source/io_formats/statepoint.rst @@ -72,7 +72,10 @@ The current version of the statepoint file format is 18.1. **/tallies/meshes/mesh /** -:Datasets: - **type** (*char[]*) -- Type of mesh. +:Attributes: - **id** (*int*) -- ID of the mesh + - **type** (*char[]*) -- Type of mesh. + +:Datasets: - **name** (*char[]*) -- Name of the mesh. - **dimension** (*int*) -- Number of mesh cells in each dimension. - **Regular Mesh Only:** - **lower_left** (*double[]*) -- Coordinates of lower-left corner of diff --git a/docs/source/io_formats/tallies.rst b/docs/source/io_formats/tallies.rst index 78101ab66..9f29a949a 100644 --- a/docs/source/io_formats/tallies.rst +++ b/docs/source/io_formats/tallies.rst @@ -109,7 +109,7 @@ The ```` element accepts the following sub-elements: prematurely if there are no hits in any bins at the first evalulation. It is the user's responsibility to specify enough particles per batch to get a nonzero score in at least one bin. - + *Default*: False :scores: @@ -329,6 +329,11 @@ If a mesh is desired as a filter for a tally, it must be specified in a separate element with the tag name ````. This element has the following attributes/sub-elements: + :name: + An optional string name to identify the mesh in output files. + + *Default*: "" + :type: The type of mesh. This can be either "regular", "rectilinear", "cylindrical", "spherical", or "unstructured". diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index 5edb03003..bd86d54fb 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -132,13 +132,18 @@ public: int32_t id() const { return id_; } + const std::string& name() const { return name_; } + //! Set the mesh ID void set_id(int32_t id = -1); + //! Write the mesh data to an HDF5 group + void to_hdf5(hid_t group) const; + //! Write mesh data to an HDF5 group // //! \param[in] group HDF5 group - virtual void to_hdf5(hid_t group) const = 0; + virtual void to_hdf5_inner(hid_t group) const = 0; //! Find the mesh lines that intersect an axis-aligned slice plot // @@ -202,7 +207,8 @@ public: // Data members xt::xtensor lower_left_; //!< Lower-left coordinates of mesh xt::xtensor upper_right_; //!< Upper-right coordinates of mesh - int id_ {-1}; //!< User-specified ID + int id_ {-1}; //!< Mesh ID + std::string name_; //!< User-specified name int n_dimension_ {-1}; //!< Number of dimensions }; @@ -410,7 +416,7 @@ public: std::pair, vector> plot( Position plot_ll, Position plot_ur) const override; - void to_hdf5(hid_t group) const override; + void to_hdf5_inner(hid_t group) const override; //! Get the coordinate for the mesh grid boundary in the positive direction //! @@ -460,7 +466,7 @@ public: std::pair, vector> plot( Position plot_ll, Position plot_ur) const override; - void to_hdf5(hid_t group) const override; + void to_hdf5_inner(hid_t group) const override; //! Get the coordinate for the mesh grid boundary in the positive direction //! @@ -506,7 +512,7 @@ public: std::pair, vector> plot( Position plot_ll, Position plot_ur) const override; - void to_hdf5(hid_t group) const override; + void to_hdf5_inner(hid_t group) const override; double volume(const MeshIndex& ijk) const override; @@ -570,7 +576,7 @@ public: std::pair, vector> plot( Position plot_ll, Position plot_ur) const override; - void to_hdf5(hid_t group) const override; + void to_hdf5_inner(hid_t group) const override; double r(int i) const { return grid_[0][i]; } double theta(int i) const { return grid_[1][i]; } @@ -632,7 +638,7 @@ public: void surface_bins_crossed(Position r0, Position r1, const Direction& u, vector& bins) const override; - void to_hdf5(hid_t group) const override; + void to_hdf5_inner(hid_t group) const override; std::string bin_label(int bin) const override; diff --git a/openmc/mesh.py b/openmc/mesh.py index ef5c6c784..0b6f6b84e 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -99,21 +99,40 @@ class MeshBase(IDManagerMixin, ABC): Instance of a MeshBase subclass """ + mesh_type = 'regular' if 'type' not in group.attrs else group.attrs['type'].decode() + mesh_id = int(group.name.split('/')[-1].lstrip('mesh ')) + mesh_name = '' if not 'name' in group else group['name'][()].decode() - mesh_type = group['type'][()].decode() if mesh_type == 'regular': - return RegularMesh.from_hdf5(group) + return RegularMesh.from_hdf5(group, mesh_id, mesh_name) elif mesh_type == 'rectilinear': - return RectilinearMesh.from_hdf5(group) + return RectilinearMesh.from_hdf5(group, mesh_id, mesh_name) elif mesh_type == 'cylindrical': - return CylindricalMesh.from_hdf5(group) + return CylindricalMesh.from_hdf5(group, mesh_id, mesh_name) elif mesh_type == 'spherical': - return SphericalMesh.from_hdf5(group) + return SphericalMesh.from_hdf5(group, mesh_id, mesh_name) elif mesh_type == 'unstructured': - return UnstructuredMesh.from_hdf5(group) + return UnstructuredMesh.from_hdf5(group, mesh_id, mesh_name) else: raise ValueError('Unrecognized mesh type: "' + mesh_type + '"') + def to_xml_element(self): + """Return XML representation of the mesh + + Returns + ------- + element : lxml.etree._Element + XML element containing mesh data + + """ + elem = ET.Element("mesh") + + elem.set("id", str(self._id)) + if self.name: + elem.set("name", self.name) + + return elem + @classmethod def from_xml_element(cls, elem: ET.Element): """Generates a mesh from an XML element @@ -132,18 +151,21 @@ class MeshBase(IDManagerMixin, ABC): mesh_type = get_text(elem, 'type') if mesh_type == 'regular' or mesh_type is None: - return RegularMesh.from_xml_element(elem) + mesh = RegularMesh.from_xml_element(elem) elif mesh_type == 'rectilinear': - return RectilinearMesh.from_xml_element(elem) + mesh = RectilinearMesh.from_xml_element(elem) elif mesh_type == 'cylindrical': - return CylindricalMesh.from_xml_element(elem) + mesh = CylindricalMesh.from_xml_element(elem) elif mesh_type == 'spherical': - return SphericalMesh.from_xml_element(elem) + mesh = SphericalMesh.from_xml_element(elem) elif mesh_type == 'unstructured': - return UnstructuredMesh.from_xml_element(elem) + mesh = UnstructuredMesh.from_xml_element(elem) else: raise ValueError(f'Unrecognized mesh type "{mesh_type}" found.') + mesh.name = get_text(elem, 'name', default='') + return mesh + def get_homogenized_materials( self, model: openmc.Model, @@ -791,11 +813,9 @@ class RegularMesh(StructuredMesh): return string @classmethod - def from_hdf5(cls, group: h5py.Group): - mesh_id = int(group.name.split('/')[-1].lstrip('mesh ')) - + def from_hdf5(cls, group: h5py.Group, mesh_id: int, name: str): # Read and assign mesh properties - mesh = cls(mesh_id) + mesh = cls(mesh_id=mesh_id, name=name) mesh.dimension = group['dimension'][()] mesh.lower_left = group['lower_left'][()] if 'width' in group: @@ -899,9 +919,7 @@ class RegularMesh(StructuredMesh): XML element containing mesh data """ - - element = ET.Element("mesh") - element.set("id", str(self._id)) + element = super().to_xml_element() if self._dimension is not None: subelement = ET.SubElement(element, "dimension") @@ -937,10 +955,6 @@ class RegularMesh(StructuredMesh): mesh_id = int(get_text(elem, 'id')) mesh = cls(mesh_id=mesh_id) - mesh_type = get_text(elem, 'type') - if mesh_type is not None: - mesh.type = mesh_type - dimension = get_text(elem, 'dimension') if dimension is not None: mesh.dimension = [int(x) for x in dimension.split()] @@ -1235,11 +1249,9 @@ class RectilinearMesh(StructuredMesh): return string @classmethod - def from_hdf5(cls, group: h5py.Group): - mesh_id = int(group.name.split('/')[-1].lstrip('mesh ')) - + def from_hdf5(cls, group: h5py.Group, mesh_id: int, name: str): # Read and assign mesh properties - mesh = cls(mesh_id=mesh_id) + mesh = cls(mesh_id=mesh_id, name=name) mesh.x_grid = group['x_grid'][()] mesh.y_grid = group['y_grid'][()] mesh.z_grid = group['z_grid'][()] @@ -1279,8 +1291,7 @@ class RectilinearMesh(StructuredMesh): """ - element = ET.Element("mesh") - element.set("id", str(self._id)) + element = super().to_xml_element() element.set("type", "rectilinear") subelement = ET.SubElement(element, "x_grid") @@ -1541,12 +1552,11 @@ class CylindricalMesh(StructuredMesh): return (r_index, phi_index, z_index) @classmethod - def from_hdf5(cls, group: h5py.Group): - mesh_id = int(group.name.split('/')[-1].lstrip('mesh ')) - + def from_hdf5(cls, group: h5py.Group, mesh_id: int, name: str): # Read and assign mesh properties mesh = cls( mesh_id=mesh_id, + name=name, r_grid = group['r_grid'][()], phi_grid = group['phi_grid'][()], z_grid = group['z_grid'][()], @@ -1647,8 +1657,7 @@ class CylindricalMesh(StructuredMesh): """ - element = ET.Element("mesh") - element.set("id", str(self._id)) + element = super().to_xml_element() element.set("type", "cylindrical") subelement = ET.SubElement(element, "r_grid") @@ -1926,15 +1935,14 @@ class SphericalMesh(StructuredMesh): return string @classmethod - def from_hdf5(cls, group: h5py.Group): - mesh_id = int(group.name.split('/')[-1].lstrip('mesh ')) - + def from_hdf5(cls, group: h5py.Group, mesh_id: int, name: str): # Read and assign mesh properties mesh = cls( r_grid = group['r_grid'][()], theta_grid = group['theta_grid'][()], phi_grid = group['phi_grid'][()], mesh_id=mesh_id, + name=name ) if 'origin' in group: mesh.origin = group['origin'][()] @@ -1951,8 +1959,7 @@ class SphericalMesh(StructuredMesh): """ - element = ET.Element("mesh") - element.set("id", str(self._id)) + element = super().to_xml_element() element.set("type", "spherical") subelement = ET.SubElement(element, "r_grid") @@ -2444,8 +2451,7 @@ class UnstructuredMesh(MeshBase): writer.Write() @classmethod - def from_hdf5(cls, group: h5py.Group): - mesh_id = int(group.name.split('/')[-1].lstrip('mesh ')) + def from_hdf5(cls, group: h5py.Group, mesh_id: int, name: str): filename = group['filename'][()].decode() library = group['library'][()].decode() if 'options' in group.attrs: @@ -2453,7 +2459,7 @@ class UnstructuredMesh(MeshBase): else: options = None - mesh = cls(filename=filename, library=library, mesh_id=mesh_id, options=options) + mesh = cls(filename=filename, library=library, mesh_id=mesh_id, name=name, options=options) mesh._has_statepoint_data = True vol_data = group['volumes'][()] mesh.volumes = np.reshape(vol_data, (vol_data.shape[0],)) @@ -2480,9 +2486,9 @@ class UnstructuredMesh(MeshBase): """ - element = ET.Element("mesh") - element.set("id", str(self._id)) + element = super().to_xml_element() element.set("type", "unstructured") + element.set("library", self._library) if self.options is not None: element.set('options', self.options) diff --git a/src/mesh.cpp b/src/mesh.cpp index c572d255a..2ee616f28 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -109,6 +109,8 @@ Mesh::Mesh(pugi::xml_node node) { // Read mesh id id_ = std::stoi(get_node_value(node, "id")); + if (check_for_node(node, "name")) + name_ = get_node_value(node, "name"); } void Mesh::set_id(int32_t id) @@ -236,6 +238,28 @@ vector Mesh::material_volumes( return result; } +void Mesh::to_hdf5(hid_t group) const +{ + // Create group for mesh + std::string group_name = fmt::format("mesh {}", id_); + hid_t mesh_group = create_group(group, group_name.c_str()); + + // Write mesh type + write_attribute(mesh_group, "type", this->get_mesh_type()); + + // Write mesh ID + write_attribute(mesh_group, "id", id_); + + // Write mesh name + write_dataset(mesh_group, "name", name_); + + // Write mesh data + this->to_hdf5_inner(mesh_group); + + // Close group + close_group(mesh_group); +} + //============================================================================== // Structured Mesh implementation //============================================================================== @@ -389,11 +413,8 @@ std::string UnstructuredMesh::bin_label(int bin) const return fmt::format("Mesh Index ({})", bin); }; -void UnstructuredMesh::to_hdf5(hid_t group) const +void UnstructuredMesh::to_hdf5_inner(hid_t mesh_group) const { - hid_t mesh_group = create_group(group, fmt::format("mesh {}", id_)); - - write_dataset(mesh_group, "type", mesh_type); write_dataset(mesh_group, "filename", filename_); write_dataset(mesh_group, "library", this->library()); if (!options_.empty()) { @@ -453,8 +474,6 @@ void UnstructuredMesh::to_hdf5(hid_t group) const write_dataset(mesh_group, "volumes", volumes); write_dataset(mesh_group, "connectivity", connectivity); write_dataset(mesh_group, "element_types", elem_types); - - close_group(mesh_group); } void UnstructuredMesh::set_length_multiplier(double length_multiplier) @@ -948,17 +967,13 @@ std::pair, vector> RegularMesh::plot( return {axis_lines[0], axis_lines[1]}; } -void RegularMesh::to_hdf5(hid_t group) const +void RegularMesh::to_hdf5_inner(hid_t mesh_group) const { - hid_t mesh_group = create_group(group, "mesh " + std::to_string(id_)); - write_dataset(mesh_group, "type", "regular"); write_dataset(mesh_group, "dimension", get_x_shape()); write_dataset(mesh_group, "lower_left", lower_left_); write_dataset(mesh_group, "upper_right", upper_right_); write_dataset(mesh_group, "width", width_); - - close_group(mesh_group); } xt::xtensor RegularMesh::count_sites( @@ -1138,16 +1153,12 @@ std::pair, vector> RectilinearMesh::plot( return {axis_lines[0], axis_lines[1]}; } -void RectilinearMesh::to_hdf5(hid_t group) const +void RectilinearMesh::to_hdf5_inner(hid_t mesh_group) const { - hid_t mesh_group = create_group(group, "mesh " + std::to_string(id_)); - write_dataset(mesh_group, "type", "rectilinear"); write_dataset(mesh_group, "x_grid", grid_[0]); write_dataset(mesh_group, "y_grid", grid_[1]); write_dataset(mesh_group, "z_grid", grid_[2]); - - close_group(mesh_group); } double RectilinearMesh::volume(const MeshIndex& ijk) const @@ -1417,17 +1428,13 @@ std::pair, vector> CylindricalMesh::plot( return {axis_lines[0], axis_lines[1]}; } -void CylindricalMesh::to_hdf5(hid_t group) const +void CylindricalMesh::to_hdf5_inner(hid_t mesh_group) const { - hid_t mesh_group = create_group(group, "mesh " + std::to_string(id_)); - write_dataset(mesh_group, "type", "cylindrical"); write_dataset(mesh_group, "r_grid", grid_[0]); write_dataset(mesh_group, "phi_grid", grid_[1]); write_dataset(mesh_group, "z_grid", grid_[2]); write_dataset(mesh_group, "origin", origin_); - - close_group(mesh_group); } double CylindricalMesh::volume(const MeshIndex& ijk) const @@ -1733,17 +1740,13 @@ std::pair, vector> SphericalMesh::plot( return {axis_lines[0], axis_lines[1]}; } -void SphericalMesh::to_hdf5(hid_t group) const +void SphericalMesh::to_hdf5_inner(hid_t mesh_group) const { - hid_t mesh_group = create_group(group, "mesh " + std::to_string(id_)); - write_dataset(mesh_group, "type", SphericalMesh::mesh_type); write_dataset(mesh_group, "r_grid", grid_[0]); write_dataset(mesh_group, "theta_grid", grid_[1]); write_dataset(mesh_group, "phi_grid", grid_[2]); write_dataset(mesh_group, "origin", origin_); - - close_group(mesh_group); } double SphericalMesh::volume(const MeshIndex& ijk) const diff --git a/tests/regression_tests/tally_slice_merge/inputs_true.dat b/tests/regression_tests/tally_slice_merge/inputs_true.dat index 356962f8e..7159d833a 100644 --- a/tests/regression_tests/tally_slice_merge/inputs_true.dat +++ b/tests/regression_tests/tally_slice_merge/inputs_true.dat @@ -308,7 +308,7 @@ - + 2 2 -50.0 -50.0 50.0 50.0 diff --git a/tests/regression_tests/tally_slice_merge/test.py b/tests/regression_tests/tally_slice_merge/test.py index 090e31448..aec73979b 100644 --- a/tests/regression_tests/tally_slice_merge/test.py +++ b/tests/regression_tests/tally_slice_merge/test.py @@ -149,6 +149,9 @@ class TallySliceMergeTestHarness(PyAPITestHarness): sum2 = mesh_tally.summation(filter_type=openmc.MeshFilter, filter_bins=[(2, 1), (2, 2)]) + mesh = mesh_tally.find_filter(openmc.MeshFilter).mesh + assert mesh.name == 'mesh' + # Merge the mesh tally slices merge_tally = sum1.merge(sum2) diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index ed08be816..27322165f 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -357,6 +357,27 @@ def test_CylindricalMesh_get_indices_at_coords(): assert mesh.get_indices_at_coords([102, 199.1, 299]) == (0, 3, 0) # forth angle quadrant +def test_mesh_name_roundtrip(run_in_tmpdir): + + mesh = openmc.RegularMesh() + mesh.name = 'regular-mesh' + mesh.lower_left = (-1, -1, -1) + mesh.width = (1, 1, 1) + mesh.dimension = (1, 1, 1) + + mesh_filter = openmc.MeshFilter(mesh) + tally = openmc.Tally() + tally.filters = [mesh_filter] + tally.scores = ['flux'] + + openmc.Tallies([tally]).export_to_xml() + + xml_tallies = openmc.Tallies.from_xml() + + mesh = xml_tallies[0].find_filter(openmc.MeshFilter).mesh + assert mesh.name == 'regular-mesh' + + def test_umesh_roundtrip(run_in_tmpdir, request): umesh = openmc.UnstructuredMesh(request.path.parent / 'test_mesh_tets.e', 'moab') umesh.output = True From 3a001d3de2130da08d33f995f54b248ff2edcaad Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Mon, 6 Jan 2025 13:40:13 +0100 Subject: [PATCH 215/671] updated link to log mapping technique (#3241) --- docs/source/methods/cross_sections.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/methods/cross_sections.rst b/docs/source/methods/cross_sections.rst index 2cafa8691..21a1ef8df 100644 --- a/docs/source/methods/cross_sections.rst +++ b/docs/source/methods/cross_sections.rst @@ -290,7 +290,7 @@ scattering information in the water while the fuel can be simulated with linear or even isotropic scattering. .. _logarithmic mapping technique: - https://laws.lanl.gov/vhosts/mcnp.lanl.gov/pdf_files/la-ur-14-24530.pdf + https://mcnp.lanl.gov/pdf_files/TechReport_2014_LANL_LA-UR-14-24530_Brown.pdf .. _Hwang: https://doi.org/10.13182/NSE87-A16381 .. _Josey: https://doi.org/10.1016/j.jcp.2015.08.013 .. _WMP Library: https://github.com/mit-crpg/WMP_Library From 393334829db331f1b95d36d0da32a9ae786590ab Mon Sep 17 00:00:00 2001 From: Masoud Date: Tue, 7 Jan 2025 10:02:05 +0330 Subject: [PATCH 216/671] Adding '#define _USE_MATH_DEFINES' to make M_PI declared in Intel and MSVC compilers (#3238) Co-authored-by: Paul Romano --- src/external/quartic_solver.cpp | 1 + src/mesh.cpp | 7 ++++--- src/plot.cpp | 2 ++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/external/quartic_solver.cpp b/src/external/quartic_solver.cpp index 0b280e83c..915020ffa 100644 --- a/src/external/quartic_solver.cpp +++ b/src/external/quartic_solver.cpp @@ -1,4 +1,5 @@ #include +#define _USE_MATH_DEFINES // to make M_PI declared in Intel and MSVC compilers #include #include #include diff --git a/src/mesh.cpp b/src/mesh.cpp index 2ee616f28..4b1878c5a 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -1,7 +1,8 @@ #include "openmc/mesh.h" -#include // for copy, equal, min, min_element -#include // for ceil -#include // for size_t +#include // for copy, equal, min, min_element +#define _USE_MATH_DEFINES // to make M_PI declared in Intel and MSVC compilers +#include // for ceil +#include // for size_t #include #include diff --git a/src/plot.cpp b/src/plot.cpp index 348138570..de7d475e1 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -1,6 +1,8 @@ #include "openmc/plot.h" #include +#define _USE_MATH_DEFINES // to make M_PI declared in Intel and MSVC compilers +#include #include #include #include From 5ad1a4aa1e67ed6a4b88ebe84af0f1b117d14cd5 Mon Sep 17 00:00:00 2001 From: Baptiste Mouginot <15145274+bam241@users.noreply.github.com> Date: Tue, 7 Jan 2025 22:50:02 +0100 Subject: [PATCH 217/671] Differentiate materials in DAGMC universes (#3056) Co-authored-by: Baptiste Mouginot Co-authored-by: Patrick Shriwise Co-authored-by: azimG Co-authored-by: Paul Romano --- docs/source/io_formats/geometry.rst | 34 +- docs/source/pythonapi/capi.rst | 1 + include/openmc/capi.h | 3 + include/openmc/cell.h | 8 +- include/openmc/dagmc.h | 15 + include/openmc/surface.h | 8 +- openmc/__init__.py | 1 + openmc/dagmc.py | 625 ++++++++++++++++++++ openmc/geometry.py | 2 +- openmc/lib/__init__.py | 1 + openmc/lib/dagmc.py | 43 ++ openmc/model/model.py | 119 +++- openmc/universe.py | 837 ++++++++------------------- src/cell.cpp | 4 +- src/dagmc.cpp | 102 +++- src/particle.cpp | 7 +- src/plot.cpp | 2 +- src/surface.cpp | 8 +- tests/unit_tests/dagmc/test_model.py | 256 ++++++++ 19 files changed, 1424 insertions(+), 652 deletions(-) create mode 100644 openmc/dagmc.py create mode 100644 openmc/lib/dagmc.py create mode 100644 tests/unit_tests/dagmc/test_model.py diff --git a/docs/source/io_formats/geometry.rst b/docs/source/io_formats/geometry.rst index ac48e48d2..6d0a37a24 100644 --- a/docs/source/io_formats/geometry.rst +++ b/docs/source/io_formats/geometry.rst @@ -407,13 +407,33 @@ Each ```` element can have the following attributes or sub-eleme *Default*: None + :material_overrides: + This element contains information on material overrides to be applied to the + DAGMC universe. It has the following attributes and sub-elements: - .. note:: A geometry.xml file containing only a DAGMC model for a file named `dagmc.h5m` (no CSG) - looks as follows + :cell: + Material override information for a single cell. It contains the following + attributes and sub-elements: - .. code-block:: xml + :id: + The cell ID in the DAGMC geometry for which the material override will + apply. - - - - + :materials: + A list of material IDs that will apply to instances of the cell. If the + list contains only one ID, it will replace the original material + assignment of all instances of the DAGMC cell. If the list contains more + than one material, each material ID of the list will be assigned to the + various instances of the DAGMC cell. + + *Default*: None + +.. note:: A geometry.xml file containing only a DAGMC model for a file named + `dagmc.h5m` (no CSG) looks as follows: + + .. code-block:: xml + + + + + diff --git a/docs/source/pythonapi/capi.rst b/docs/source/pythonapi/capi.rst index 995ad97fa..9ceff83fd 100644 --- a/docs/source/pythonapi/capi.rst +++ b/docs/source/pythonapi/capi.rst @@ -19,6 +19,7 @@ Functions finalize find_cell find_material + dagmc_universe_cell_ids global_bounding_box global_tallies hard_reset diff --git a/include/openmc/capi.h b/include/openmc/capi.h index 9401156a6..8edd99c07 100644 --- a/include/openmc/capi.h +++ b/include/openmc/capi.h @@ -29,6 +29,9 @@ int openmc_cell_set_temperature( int32_t index, double T, const int32_t* instance, bool set_contained = false); int openmc_cell_set_translation(int32_t index, const double xyz[]); int openmc_cell_set_rotation(int32_t index, const double rot[], size_t rot_len); +int openmc_dagmc_universe_get_cell_ids( + int32_t univ_id, int32_t* ids, size_t* n); +int openmc_dagmc_universe_get_num_cells(int32_t univ_id, size_t* n); int openmc_energy_filter_get_bins( int32_t index, const double** energies, size_t* n); int openmc_energy_filter_set_bins( diff --git a/include/openmc/cell.h b/include/openmc/cell.h index 70843140b..032475ce9 100644 --- a/include/openmc/cell.h +++ b/include/openmc/cell.h @@ -320,7 +320,6 @@ public: int32_t universe_; //!< Universe # this cell is in int32_t fill_; //!< Universe # filling this cell int32_t n_instances_ {0}; //!< Number of instances of this cell - GeometryType geom_type_; //!< Geometric representation type (CSG, DAGMC) //! \brief Index corresponding to this cell in distribcell arrays int distribcell_index_ {C_NONE}; @@ -350,6 +349,13 @@ public: vector rotation_; vector offset_; //!< Distribcell offset table + + // Accessors + const GeometryType& geom_type() const { return geom_type_; } + GeometryType& geom_type() { return geom_type_; } + +private: + GeometryType geom_type_; //!< Geometric representation type (CSG, DAGMC) }; struct CellInstanceItem { diff --git a/include/openmc/dagmc.h b/include/openmc/dagmc.h index 2facf4fc0..47fcfe237 100644 --- a/include/openmc/dagmc.h +++ b/include/openmc/dagmc.h @@ -29,6 +29,12 @@ void check_dagmc_root_univ(); #include "openmc/particle.h" #include "openmc/position.h" #include "openmc/surface.h" +#include "openmc/vector.h" + +#include // for shared_ptr, unique_ptr +#include +#include +#include // for pair class UWUW; @@ -133,6 +139,10 @@ public: void legacy_assign_material( std::string mat_string, std::unique_ptr& c) const; + //! Assign a material overriding normal assignement to a cell + //! \param[in] c The OpenMC cell to which the material is assigned + void override_assign_material(std::unique_ptr& c) const; + //! Return the index into the model cells vector for a given DAGMC volume //! handle in the universe //! \param[in] vol MOAB handle to the DAGMC volume set @@ -184,6 +194,11 @@ private: //!< generate new material IDs for the universe bool has_graveyard_; //!< Indicates if the DAGMC geometry has a "graveyard" //!< volume + std::unordered_map> + material_overrides_; //!< Map of material overrides + //!< keys correspond to the DAGMCCell id + //!< values are a list of material ids used + //!< for the override }; //============================================================================== diff --git a/include/openmc/surface.h b/include/openmc/surface.h index af235301c..498f71d4f 100644 --- a/include/openmc/surface.h +++ b/include/openmc/surface.h @@ -38,7 +38,6 @@ public: int id_; //!< Unique ID std::string name_; //!< User-defined name unique_ptr bc_; //!< Boundary condition - GeometryType geom_type_; //!< Geometry type indicator (CSG or DAGMC) bool surf_source_ {false}; //!< Activate source banking for the surface? explicit Surface(pugi::xml_node surf_node); @@ -91,6 +90,13 @@ public: //! Get the BoundingBox for this surface. virtual BoundingBox bounding_box(bool /*pos_side*/) const { return {}; } + // Accessors + const GeometryType& geom_type() const { return geom_type_; } + GeometryType& geom_type() { return geom_type_; } + +private: + GeometryType geom_type_; //!< Geometry type indicator (CSG or DAGMC) + protected: virtual void to_hdf5_inner(hid_t group_id) const = 0; }; diff --git a/openmc/__init__.py b/openmc/__init__.py index 566d28706..bb972b4e6 100644 --- a/openmc/__init__.py +++ b/openmc/__init__.py @@ -15,6 +15,7 @@ from openmc.volume import * from openmc.weight_windows import * from openmc.surface import * from openmc.universe import * +from openmc.dagmc import * from openmc.source import * from openmc.settings import * from openmc.lattice import * diff --git a/openmc/dagmc.py b/openmc/dagmc.py new file mode 100644 index 000000000..8ab0aaf69 --- /dev/null +++ b/openmc/dagmc.py @@ -0,0 +1,625 @@ +from collections.abc import Iterable, Mapping +from numbers import Integral + +import h5py +import lxml.etree as ET +import numpy as np +import warnings + +import openmc +import openmc.checkvalue as cv +from ._xml import get_text +from .checkvalue import check_type, check_value +from .surface import _BOUNDARY_TYPES +from .bounding_box import BoundingBox +from .utility_funcs import input_path + + +class DAGMCUniverse(openmc.UniverseBase): + """A reference to a DAGMC file to be used in the model. + + .. versionadded:: 0.13.0 + + Parameters + ---------- + filename : str + Path to the DAGMC file used to represent this universe. + universe_id : int, optional + Unique identifier of the universe. If not specified, an identifier will + automatically be assigned. + name : str, optional + Name of the universe. If not specified, the name is the empty string. + auto_geom_ids : bool + Set IDs automatically on initialization (True) or report overlaps in ID + space between CSG and DAGMC (False) + auto_mat_ids : bool + Set IDs automatically on initialization (True) or report overlaps in ID + space between OpenMC and UWUW materials (False) + material_overrides : dict, optional + A dictionary of material overrides. The keys are material name strings + and the values are Iterables of openmc.Material objects. If a material + name is found in the DAGMC file, the material will be replaced with the + openmc.Material object in the value. + + Attributes + ---------- + id : int + Unique identifier of the universe + name : str + Name of the universe + filename : str + Path to the DAGMC file used to represent this universe. + auto_geom_ids : bool + Set IDs automatically on initialization (True) or report overlaps in ID + space between CSG and DAGMC (False) + auto_mat_ids : bool + Set IDs automatically on initialization (True) or report overlaps in ID + space between OpenMC and UWUW materials (False) + bounding_box : openmc.BoundingBox + Lower-left and upper-right coordinates of an axis-aligned bounding box + of the universe. + + .. versionadded:: 0.13.1 + material_names : list of str + Return a sorted list of materials names that are contained within the + DAGMC h5m file. This is useful when naming openmc.Material() objects as + each material name present in the DAGMC h5m file must have a matching + openmc.Material() with the same name. + + .. versionadded:: 0.13.2 + n_cells : int + The number of cells in the DAGMC model. This is the number of cells at + runtime and accounts for the implicit complement whether or not is it + present in the DAGMC file. + + .. versionadded:: 0.13.2 + n_surfaces : int + The number of surfaces in the model. + + .. versionadded:: 0.13.2 + material_overrides : dict + A dictionary of material overrides. Keys are cell IDs; values are + iterables of :class:`openmc.Material` objects. The material assignment + of each DAGMC cell ID key will be replaced with the + :class:`~openmc.Material` object in the value. If the value contains + multiple :class:`~openmc.Material` objects, each Material in the list + will be assigned to the corresponding instance of the cell. + + .. versionadded:: 0.15.1 + """ + + def __init__(self, + filename: cv.PathLike, + universe_id=None, + name='', + auto_geom_ids=False, + auto_mat_ids=False, + material_overrides=None): + super().__init__(universe_id, name) + # Initialize class attributes + self.filename = filename + self.auto_geom_ids = auto_geom_ids + self.auto_mat_ids = auto_mat_ids + self._material_overrides = {} + if material_overrides is not None: + self.material_overrides = material_overrides + + def __repr__(self): + string = super().__repr__() + string += '{: <16}=\t{}\n'.format('\tGeom', 'DAGMC') + string += '{: <16}=\t{}\n'.format('\tFile', self.filename) + return string + + @property + def bounding_box(self): + with h5py.File(self.filename) as dagmc_file: + coords = dagmc_file['tstt']['nodes']['coordinates'][()] + lower_left_corner = coords.min(axis=0) + upper_right_corner = coords.max(axis=0) + return openmc.BoundingBox(lower_left_corner, upper_right_corner) + + @property + def filename(self): + return self._filename + + @filename.setter + def filename(self, val: cv.PathLike): + cv.check_type('DAGMC filename', val, cv.PathLike) + self._filename = input_path(val) + + @property + def material_overrides(self): + return self._material_overrides + + @material_overrides.setter + def material_overrides(self, val): + cv.check_type('material overrides', val, Mapping) + for key, value in val.items(): + self.add_material_override(key, value) + + def replace_material_assignment(self, material_name: str, material: openmc.Material): + """Replace a material assignment within the DAGMC universe. + + Replace the material assignment of all cells filled with a material in + the DAGMC universe. The universe must be synchronized in an initialized + Model (see :meth:`~openmc.DAGMCUniverse.sync_dagmc_cells`) before + calling this method. + + .. versionadded:: 0.15.1 + + Parameters + ---------- + material_name : str + Material name to replace + material : openmc.Material + Material to replace the material_name with + + """ + if material_name not in self.material_names: + raise ValueError( + f"No material with name '{material_name}' found in the DAGMC universe") + + if not self.cells: + raise RuntimeError("This DAGMC universe has not been synchronized " + "on an initialized Model.") + + for cell in self.cells.values(): + if cell.fill is None: + continue + if isinstance(cell.fill, openmc.Iterable): + cell.fill = list(map(lambda x: material if x.name == material_name else x, cell.fill)) + else: + cell.fill = material if cell.fill.name == material_name else cell.fill + + def add_material_override(self, key, overrides=None): + """Add a material override to the universe. + + .. versionadded:: 0.15 + + Parameters + ---------- + key : openmc.DAGMCCell or int + Cell object or ID of the Cell to override + value : openmc.Material or Iterable of openmc.Material + Material(s) to be applied to the Cell passed as the key + + """ + # Ensure that they key is a valid type + if not isinstance(key, (int, openmc.DAGMCCell)): + raise ValueError("Unrecognized key type. " + "Must be an integer or openmc.DAGMCCell object") + + # Ensure that overrides is an iterable of openmc.Material + overrides = overrides if isinstance(overrides, openmc.Iterable) else [overrides] + cv.check_iterable_type('material objects', overrides, (openmc.Material, type(None))) + + # if a DAGMCCell is passed, redcue the key to the ID of the cell + if isinstance(key, openmc.DAGMCCell): + key = key.id + + if key not in self.cells: + raise ValueError(f"Cell ID '{key}' not found in DAGMC universe") + + self._material_overrides[key] = overrides + + @property + def auto_geom_ids(self): + return self._auto_geom_ids + + @auto_geom_ids.setter + def auto_geom_ids(self, val): + cv.check_type('DAGMC automatic geometry ids', val, bool) + self._auto_geom_ids = val + + @property + def auto_mat_ids(self): + return self._auto_mat_ids + + @auto_mat_ids.setter + def auto_mat_ids(self, val): + cv.check_type('DAGMC automatic material ids', val, bool) + self._auto_mat_ids = val + + @property + def material_names(self): + dagmc_file_contents = h5py.File(self.filename) + material_tags_hex = dagmc_file_contents['/tstt/tags/NAME'].get( + 'values') + material_tags_ascii = [] + for tag in material_tags_hex: + candidate_tag = tag.tobytes().decode().replace('\x00', '') + # tags might be for temperature or reflective surfaces + if candidate_tag.startswith('mat:'): + # if name ends with _comp remove it, it is not parsed + if candidate_tag.endswith('_comp'): + candidate_tag = candidate_tag[:-5] + # removes first 4 characters as openmc.Material name should be + # set without the 'mat:' part of the tag + material_tags_ascii.append(candidate_tag[4:]) + + return sorted(set(material_tags_ascii)) + + def _n_geom_elements(self, geom_type): + """ + Helper function for retrieving the number geometric entities in a DAGMC + file + + Parameters + ---------- + geom_type : str + The type of geometric entity to count. One of {'Volume', 'Surface'}. Returns + the runtime number of voumes in the DAGMC model (includes implicit complement). + + Returns + ------- + int + Number of geometry elements of the specified type + """ + cv.check_value('geometry type', geom_type, ('volume', 'surface')) + + def decode_str_tag(tag_val): + return tag_val.tobytes().decode().replace('\x00', '') + + with h5py.File(self.filename) as dagmc_file: + category_data = dagmc_file['tstt/tags/CATEGORY/values'] + category_strs = map(decode_str_tag, category_data) + n = sum([v == geom_type.capitalize() for v in category_strs]) + + # check for presence of an implicit complement in the file and + # increment the number of cells if it doesn't exist + if geom_type == 'volume': + name_data = dagmc_file['tstt/tags/NAME/values'] + name_strs = map(decode_str_tag, name_data) + if not sum(['impl_complement' in n for n in name_strs]): + n += 1 + return n + + @property + def n_cells(self): + return self._n_geom_elements('volume') + + @property + def n_surfaces(self): + return self._n_geom_elements('surface') + + def create_xml_subelement(self, xml_element, memo=None): + if memo is None: + memo = set() + + if self in memo: + return + + memo.add(self) + + # Ensure that the material overrides are up-to-date + for cell in self.cells.values(): + if cell.fill is None: + continue + self.add_material_override(cell, cell.fill) + + # Set xml element values + dagmc_element = ET.Element('dagmc_universe') + dagmc_element.set('id', str(self.id)) + + if self.auto_geom_ids: + dagmc_element.set('auto_geom_ids', 'true') + if self.auto_mat_ids: + dagmc_element.set('auto_mat_ids', 'true') + dagmc_element.set('filename', str(self.filename)) + if self._material_overrides: + mat_element = ET.Element('material_overrides') + for key in self._material_overrides: + cell_overrides = ET.Element('cell_override') + cell_overrides.set("id", str(key)) + material_element = ET.Element('material_ids') + material_element.text = ' '.join( + str(t.id) for t in self._material_overrides[key]) + cell_overrides.append(material_element) + mat_element.append(cell_overrides) + dagmc_element.append(mat_element) + xml_element.append(dagmc_element) + + def bounding_region( + self, + bounded_type: str = 'box', + boundary_type: str = 'vacuum', + starting_id: int = 10000, + padding_distance: float = 0. + ): + """Creates a either a spherical or box shaped bounding region around + the DAGMC geometry. + + .. versionadded:: 0.13.1 + + Parameters + ---------- + bounded_type : str + The type of bounding surface(s) to use when constructing the region. + Options include a single spherical surface (sphere) or a rectangle + made from six planes (box). + boundary_type : str + Boundary condition that defines the behavior for particles hitting + the surface. Defaults to vacuum boundary condition. Passed into the + surface construction. + starting_id : int + Starting ID of the surface(s) used in the region. For bounded_type + 'box', the next 5 IDs will also be used. Defaults to 10000 to reduce + the chance of an overlap of surface IDs with the DAGMC geometry. + padding_distance : float + Distance between the bounding region surfaces and the minimal + bounding box. Allows for the region to be larger than the DAGMC + geometry. + + Returns + ------- + openmc.Region + Region instance + """ + + check_type('boundary type', boundary_type, str) + check_value('boundary type', boundary_type, _BOUNDARY_TYPES) + check_type('starting surface id', starting_id, Integral) + check_type('bounded type', bounded_type, str) + check_value('bounded type', bounded_type, ('box', 'sphere')) + + bbox = self.bounding_box.expand(padding_distance, True) + + if bounded_type == 'sphere': + radius = np.linalg.norm(bbox.upper_right - bbox.center) + bounding_surface = openmc.Sphere( + surface_id=starting_id, + x0=bbox.center[0], + y0=bbox.center[1], + z0=bbox.center[2], + boundary_type=boundary_type, + r=radius, + ) + + return -bounding_surface + + if bounded_type == 'box': + # defines plane surfaces for all six faces of the bounding box + lower_x = openmc.XPlane(bbox[0][0], surface_id=starting_id) + upper_x = openmc.XPlane(bbox[1][0], surface_id=starting_id+1) + lower_y = openmc.YPlane(bbox[0][1], surface_id=starting_id+2) + upper_y = openmc.YPlane(bbox[1][1], surface_id=starting_id+3) + lower_z = openmc.ZPlane(bbox[0][2], surface_id=starting_id+4) + upper_z = openmc.ZPlane(bbox[1][2], surface_id=starting_id+5) + + region = +lower_x & -upper_x & +lower_y & -upper_y & +lower_z & -upper_z + + for surface in region.get_surfaces().values(): + surface.boundary_type = boundary_type + + return region + + def bounded_universe(self, bounding_cell_id=10000, **kwargs): + """Returns an openmc.Universe filled with this DAGMCUniverse and bounded + with a cell. Defaults to a box cell with a vacuum surface however this + can be changed using the kwargs which are passed directly to + DAGMCUniverse.bounding_region(). + + Parameters + ---------- + bounding_cell_id : int + The cell ID number to use for the bounding cell, defaults to 10000 to reduce + the chance of overlapping ID numbers with the DAGMC geometry. + + Returns + ------- + openmc.Universe + Universe instance + """ + bounding_cell = openmc.Cell( + fill=self, cell_id=bounding_cell_id, region=self.bounding_region(**kwargs)) + return openmc.Universe(cells=[bounding_cell]) + + @classmethod + def from_hdf5(cls, group): + """Create DAGMC universe from HDF5 group + + Parameters + ---------- + group : h5py.Group + Group in HDF5 file + + Returns + ------- + openmc.DAGMCUniverse + DAGMCUniverse instance + + """ + id = int(group.name.split('/')[-1].lstrip('universe ')) + fname = group['filename'][()].decode() + name = group['name'][()].decode() if 'name' in group else None + + out = cls(fname, universe_id=id, name=name) + + out.auto_geom_ids = bool(group.attrs['auto_geom_ids']) + out.auto_mat_ids = bool(group.attrs['auto_mat_ids']) + + return out + + @classmethod + def from_xml_element(cls, elem, mats = None): + """Generate DAGMC universe from XML element + + Parameters + ---------- + elem : lxml.etree._Element + `` element + mats : dict + Dictionary mapping material ID strings to :class:`openmc.Material` + instances (defined in :meth:`openmc.Geometry.from_xml`) + + Returns + ------- + openmc.DAGMCUniverse + DAGMCUniverse instance + + """ + id = int(get_text(elem, 'id')) + fname = get_text(elem, 'filename') + + out = cls(fname, universe_id=id) + + name = get_text(elem, 'name') + if name is not None: + out.name = name + + out.auto_geom_ids = bool(elem.get('auto_geom_ids')) + out.auto_mat_ids = bool(elem.get('auto_mat_ids')) + + el_mat_override = elem.find('material_overrides') + if el_mat_override is not None: + if mats is None: + raise ValueError("Material overrides found in DAGMC universe " + "but no materials were provided to populate " + "the mapping.") + out._material_overrides = {} + for elem in el_mat_override.findall('cell_override'): + cell_id = int(get_text(elem, 'id')) + mat_ids = get_text(elem, 'material_ids').split(' ') + mat_objs = [mats[mat_id] for mat_id in mat_ids] + out._material_overrides[cell_id] = mat_objs + + return out + + def _partial_deepcopy(self): + """Clone all of the openmc.DAGMCUniverse object's attributes except for + its cells, as they are copied within the clone function. This should + only to be used within the openmc.UniverseBase.clone() context. + """ + clone = openmc.DAGMCUniverse(name=self.name, filename=self.filename) + clone.volume = self.volume + clone.auto_geom_ids = self.auto_geom_ids + clone.auto_mat_ids = self.auto_mat_ids + return clone + + def add_cell(self, cell): + """Add a cell to the universe. + + Parameters + ---------- + cell : openmc.DAGMCCell + Cell to add + + """ + if not isinstance(cell, openmc.DAGMCCell): + msg = f'Unable to add a DAGMCCell to DAGMCUniverse ' \ + f'ID="{self._id}" since "{cell}" is not a DAGMCCell' + raise TypeError(msg) + + cell_id = cell.id + + if cell_id not in self._cells: + self._cells[cell_id] = cell + + def remove_cell(self, cell): + """Remove a cell from the universe. + + Parameters + ---------- + cell : openmc.Cell + Cell to remove + + """ + + if not isinstance(cell, openmc.DAGMCCell): + msg = f'Unable to remove a Cell from Universe ID="{self._id}" ' \ + f'since "{cell}" is not a Cell' + raise TypeError(msg) + + # If the Cell is in the Universe's list of Cells, delete it + self._cells.pop(cell.id, None) + + def sync_dagmc_cells(self, mats: Iterable[openmc.Material]): + """Synchronize DAGMC cell information between Python and C API + + .. versionadded:: 0.15.1 + + Parameters + ---------- + mats : iterable of openmc.Material + Iterable of materials to assign to the DAGMC cells + + """ + import openmc.lib + if not openmc.lib.is_initialized: + raise RuntimeError("This universe must be part of an openmc.Model " + "initialized via Model.init_lib before calling " + "this method.") + + dagmc_cell_ids = openmc.lib.dagmc.dagmc_universe_cell_ids(self.id) + if len(dagmc_cell_ids) != self.n_cells: + raise ValueError( + f"Number of cells in DAGMC universe {self.id} does not match " + f"the number of cells in the Python universe." + ) + + mats_per_id = {mat.id: mat for mat in mats} + for dag_cell_id in dagmc_cell_ids: + dag_cell = openmc.lib.cells[dag_cell_id] + if isinstance(dag_cell.fill, Iterable): + fill = [mats_per_id[mat.id] for mat in dag_cell.fill if mat] + else: + fill = mats_per_id[dag_cell.fill.id] if dag_cell.fill else None + self.add_cell(openmc.DAGMCCell(cell_id=dag_cell_id, fill=fill)) + + +class DAGMCCell(openmc.Cell): + """A cell class for DAGMC-based geometries. + + .. versionadded:: 0.15.1 + + Parameters + ---------- + cell_id : int or None, optional + Unique identifier for the cell. If None, an identifier will be + automatically assigned. + name : str, optional + Name of the cell. + fill : openmc.Material or None, optional + Material filling the cell. If None, the cell is filled with vacuum. + + Attributes + ---------- + DAG_parent_universe : int + The parent universe of the cell. + + """ + def __init__(self, cell_id=None, name='', fill=None): + super().__init__(cell_id, name, fill, None) + + @property + def DAG_parent_universe(self): + """Get the parent universe of the cell.""" + return self._parent_universe + + @DAG_parent_universe.setter + def DAG_parent_universe(self, universe): + """Set the parent universe of the cell.""" + self._parent_universe = universe.id + + def bounding_box(self): + return BoundingBox.infinite() + + def get_all_cells(self, memo=None): + return {} + + def get_all_universes(self, memo=None): + return {} + + def clone(self, clone_materials=True, clone_regions=True, memo=None): + warnings.warn("clone is not available for cells in a DAGMC universe") + return self + + def plot(self, *args, **kwargs): + raise TypeError("plot is not available for DAGMC cells.") + + def create_xml_subelement(self, xml_element, memo=None): + raise TypeError("create_xml_subelement is not available for DAGMC cells.") + + @classmethod + def from_xml_element(cls, elem, surfaces, materials, get_universe): + raise TypeError("from_xml_element is not available for DAGMC cells.") diff --git a/openmc/geometry.py b/openmc/geometry.py index a52dc4418..28e1e48ec 100644 --- a/openmc/geometry.py +++ b/openmc/geometry.py @@ -217,7 +217,7 @@ class Geometry: # Add any DAGMC universes for e in elem.findall('dagmc_universe'): - dag_univ = openmc.DAGMCUniverse.from_xml_element(e) + dag_univ = openmc.DAGMCUniverse.from_xml_element(e, mats) universes[dag_univ.id] = dag_univ # Dictionary that maps each universe to a list of cells/lattices that diff --git a/openmc/lib/__init__.py b/openmc/lib/__init__.py index 9bb2efb38..5fe35b974 100644 --- a/openmc/lib/__init__.py +++ b/openmc/lib/__init__.py @@ -68,6 +68,7 @@ from .settings import settings from .math import * from .plot import * from .weight_windows import * +from .dagmc import * # Flag to denote whether or not openmc.lib.init has been called # TODO: Establish and use a flag in the C++ code to represent the status of the diff --git a/openmc/lib/dagmc.py b/openmc/lib/dagmc.py new file mode 100644 index 000000000..18ec81a4b --- /dev/null +++ b/openmc/lib/dagmc.py @@ -0,0 +1,43 @@ +from ctypes import c_int, c_int32, POINTER, c_size_t + +import numpy as np + +from . import _dll +from .error import _error_handler + + +__all__ = [ + 'dagmc_universe_cell_ids' +] + +# DAGMC functions +_dll.openmc_dagmc_universe_get_cell_ids.argtypes = [c_int32, POINTER(c_int32), POINTER(c_size_t)] +_dll.openmc_dagmc_universe_get_cell_ids.restype = c_int +_dll.openmc_dagmc_universe_get_cell_ids.errcheck = _error_handler +_dll.openmc_dagmc_universe_get_num_cells.argtypes = [c_int32, POINTER(c_size_t)] +_dll.openmc_dagmc_universe_get_num_cells.restype = c_int +_dll.openmc_dagmc_universe_get_num_cells.errcheck = _error_handler + + +def dagmc_universe_cell_ids(universe_id: int) -> np.ndarray: + """Return an array of cell IDs for a DAGMC universe. + + Parameters + ---------- + dagmc_id : int + ID of the DAGMC universe to get cell IDs from. + + Returns + ------- + numpy.ndarray + DAGMC cell IDs for the universe. + + """ + n = c_size_t() + _dll.openmc_dagmc_universe_get_num_cells(universe_id, n) + cell_ids = np.empty(n.value, dtype=np.int32) + + _dll.openmc_dagmc_universe_get_cell_ids( + universe_id, cell_ids.ctypes.data_as(POINTER(c_int32)), n + ) + return cell_ids diff --git a/openmc/model/model.py b/openmc/model/model.py index 49cb0a83e..5137ea1ab 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1,7 +1,6 @@ from __future__ import annotations from collections.abc import Iterable from functools import lru_cache -import os from pathlib import Path from numbers import Integral from tempfile import NamedTemporaryFile @@ -15,8 +14,9 @@ import openmc import openmc._xml as xml from openmc.dummy_comm import DummyCommunicator from openmc.executor import _process_CLI_arguments -from openmc.checkvalue import check_type, check_value, PathLike +from openmc.checkvalue import check_type, check_value from openmc.exceptions import InvalidIDError +import openmc.lib from openmc.utility_funcs import change_directory @@ -324,6 +324,28 @@ class Model: # communicator openmc.lib.init(args=args, intracomm=intracomm, output=output) + def sync_dagmc_universes(self): + """Synchronize all DAGMC universes in the current geometry. + + This method iterates over all DAGMC universes in the geometry and + synchronizes their cells with the current material assignments. Requires + that the model has been initialized via :meth:`Model.init_lib`. + + .. versionadded:: 0.15.1 + + """ + if self.is_initialized: + if self.materials: + materials = self.materials + else: + materials = list(self.geometry.get_all_materials().values()) + for univ in self.geometry.get_all_universes().values(): + if isinstance(univ, openmc.DAGMCUniverse): + univ.sync_dagmc_cells(materials) + else: + raise ValueError("The model must be initialized before calling " + "this method") + def finalize_lib(self): """Finalize simulation and free memory allocated for the C API @@ -1154,51 +1176,86 @@ class Model: self._change_py_lib_attribs(names_or_ids, volume, 'material', 'volume') - def differentiate_depletable_mats(self, diff_volume_method: str): + def differentiate_depletable_mats(self, diff_volume_method: str = None): """Assign distribmats for each depletable material .. versionadded:: 0.14.0 + .. versionchanged:: 0.15.1 + diff_volume_method default is None, do not set volumes on the new + material ovjects. Is now a convenience method for + differentiate_mats(diff_volume_method, depletable_only=True) + Parameters ---------- diff_volume_method : str Specifies how the volumes of the new materials should be found. - Default is to 'divide equally' which divides the original material - volume equally between the new materials, 'match cell' sets the - volume of the material to volume of the cell they fill. + - None: Do not assign volumes to the new materials (Default) + - 'divide_equally': Divide the original material volume equally between the new materials + - 'match cell': Set the volume of the material to the volume of the cell they fill """ + self.differentiate_mats(diff_volume_method, depletable_only=True) + + def differentiate_mats(self, diff_volume_method: str = None, depletable_only: bool = True): + """Assign distribmats for each material + + .. versionadded:: 0.15.1 + + Parameters + ---------- + diff_volume_method : str + Specifies how the volumes of the new materials should be found. + - None: Do not assign volumes to the new materials (Default) + - 'divide_equally': Divide the original material volume equally between the new materials + - 'match cell': Set the volume of the material to the volume of the cell they fill + depletable_only : bool + Default is True, only depletable materials will be differentiated. If False, all materials will be + differentiated. + """ + check_value('volume differentiation method', diff_volume_method, ("divide equally", "match cell", None)) + # Count the number of instances for each cell and material self.geometry.determine_paths(instances_only=True) - # Extract all depletable materials which have multiple instances - distribmats = set( - [mat for mat in self.materials - if mat.depletable and mat.num_instances > 1]) - - if diff_volume_method == 'divide equally': - for mat in distribmats: - if mat.volume is None: - raise RuntimeError("Volume not specified for depletable " - f"material with ID={mat.id}.") - mat.volume /= mat.num_instances - - if distribmats: - # Assign distribmats to cells - for cell in self.geometry.get_all_material_cells().values(): - if cell.fill in distribmats: - mat = cell.fill - if diff_volume_method == 'divide equally': - cell.fill = [mat.clone() for _ in range(cell.num_instances)] - elif diff_volume_method == 'match cell': - for _ in range(cell.num_instances): - cell.fill = mat.clone() + # Find all or depletable_only materials which have multiple instance + distribmats = set() + for mat in self.materials: + # Differentiate all materials with multiple instances + diff_mat = mat.num_instances > 1 + # If depletable_only is True, differentiate only depletable materials + if depletable_only: + diff_mat = diff_mat and mat.depletable + if diff_mat: + # Assign volumes to the materials according to requirements + if diff_volume_method == "divide equally": + if mat.volume is None: + raise RuntimeError( + "Volume not specified for " + f"material with ID={mat.id}.") + else: + mat.volume /= mat.num_instances + elif diff_volume_method == "match cell": + for cell in self.geometry.get_all_material_cells().values(): + if cell.fill == mat: if not cell.volume: raise ValueError( f"Volume of cell ID={cell.id} not specified. " "Set volumes of cells prior to using " - "diff_volume_method='match cell'." - ) - cell.fill.volume = cell.volume + "diff_volume_method='match cell'.") + distribmats.add(mat) + + if not distribmats: + return + + # Assign distribmats to cells + for cell in self.geometry.get_all_material_cells().values(): + if cell.fill in distribmats: + mat = cell.fill + if diff_volume_method != 'match cell': + cell.fill = [mat.clone() for _ in range(cell.num_instances)] + elif diff_volume_method == 'match cell': + cell.fill = mat.clone() + cell.fill.volume = cell.volume if self.materials is not None: self.materials = openmc.Materials( diff --git a/openmc/universe.py b/openmc/universe.py index 648b773df..85ce6fd96 100644 --- a/openmc/universe.py +++ b/openmc/universe.py @@ -2,22 +2,16 @@ from __future__ import annotations import math from abc import ABC, abstractmethod from collections.abc import Iterable -from numbers import Integral, Real +from numbers import Real from pathlib import Path from tempfile import TemporaryDirectory import warnings -import h5py -import lxml.etree as ET import numpy as np import openmc import openmc.checkvalue as cv -from ._xml import get_text -from .checkvalue import check_type, check_value from .mixin import IDManagerMixin -from .surface import _BOUNDARY_TYPES -from .utility_funcs import input_path class UniverseBase(ABC, IDManagerMixin): @@ -55,6 +49,10 @@ class UniverseBase(ABC, IDManagerMixin): def name(self): return self._name + @property + def cells(self): + return self._cells + @name.setter def name(self, name): if name is not None: @@ -135,6 +133,130 @@ class UniverseBase(ABC, IDManagerMixin): """ + def _determine_paths(self, path='', instances_only=False): + """Count the number of instances for each cell in the universe, and + record the count in the :attr:`Cell.num_instances` properties.""" + + univ_path = path + f'u{self.id}' + + for cell in self.cells.values(): + cell_path = f'{univ_path}->c{cell.id}' + fill = cell._fill + fill_type = cell.fill_type + + # If universe-filled, recursively count cells in filling universe + if fill_type == 'universe': + fill._determine_paths(cell_path + '->', instances_only) + # If lattice-filled, recursively call for all universes in lattice + elif fill_type == 'lattice': + latt = fill + + # Count instances in each universe in the lattice + for index in latt._natural_indices: + latt_path = '{}->l{}({})->'.format( + cell_path, latt.id, ",".join(str(x) for x in index)) + univ = latt.get_universe(index) + univ._determine_paths(latt_path, instances_only) + + else: + if fill_type == 'material': + mat = fill + elif fill_type == 'distribmat': + mat = fill[cell._num_instances] + else: + mat = None + + if mat is not None: + mat._num_instances += 1 + if not instances_only: + mat._paths.append(f'{cell_path}->m{mat.id}') + + # Append current path + cell._num_instances += 1 + if not instances_only: + cell._paths.append(cell_path) + + def add_cells(self, cells): + """Add multiple cells to the universe. + + Parameters + ---------- + cells : Iterable of openmc.Cell + Cells to add + + """ + + if not isinstance(cells, Iterable): + msg = f'Unable to add Cells to Universe ID="{self._id}" since ' \ + f'"{cells}" is not iterable' + raise TypeError(msg) + + for cell in cells: + self.add_cell(cell) + + @abstractmethod + def add_cell(self, cell): + pass + + @abstractmethod + def remove_cell(self, cell): + pass + + def clear_cells(self): + """Remove all cells from the universe.""" + + self._cells.clear() + + def get_all_cells(self, memo=None): + """Return all cells that are contained within the universe + + Returns + ------- + cells : dict + Dictionary whose keys are cell IDs and values are :class:`Cell` + instances + + """ + + if memo is None: + memo = set() + elif self in memo: + return {} + memo.add(self) + + # Add this Universe's cells to the dictionary + cells = {} + cells.update(self._cells) + + # Append all Cells in each Cell in the Universe to the dictionary + for cell in self._cells.values(): + cells.update(cell.get_all_cells(memo)) + + return cells + + def get_all_materials(self, memo=None): + """Return all materials that are contained within the universe + + Returns + ------- + materials : dict + Dictionary whose keys are material IDs and values are + :class:`Material` instances + + """ + + if memo is None: + memo = set() + + materials = {} + + # Append all Cells in each Cell in the Universe to the dictionary + cells = self.get_all_cells(memo) + for cell in cells.values(): + materials.update(cell.get_all_materials(memo)) + + return materials + @abstractmethod def _partial_deepcopy(self): """Deepcopy all parameters of an openmc.UniverseBase object except its cells. @@ -182,93 +304,6 @@ class UniverseBase(ABC, IDManagerMixin): return memo[self] - -class Universe(UniverseBase): - """A collection of cells that can be repeated. - - Parameters - ---------- - universe_id : int, optional - Unique identifier of the universe. If not specified, an identifier will - automatically be assigned - name : str, optional - Name of the universe. If not specified, the name is the empty string. - cells : Iterable of openmc.Cell, optional - Cells to add to the universe. By default no cells are added. - - Attributes - ---------- - id : int - Unique identifier of the universe - name : str - Name of the universe - cells : dict - Dictionary whose keys are cell IDs and values are :class:`Cell` - instances - volume : float - Volume of the universe in cm^3. This can either be set manually or - calculated in a stochastic volume calculation and added via the - :meth:`Universe.add_volume_information` method. - bounding_box : openmc.BoundingBox - Lower-left and upper-right coordinates of an axis-aligned bounding box - of the universe. - - """ - - def __init__(self, universe_id=None, name='', cells=None): - super().__init__(universe_id, name) - - if cells is not None: - self.add_cells(cells) - - def __repr__(self): - string = super().__repr__() - string += '{: <16}=\t{}\n'.format('\tGeom', 'CSG') - string += '{: <16}=\t{}\n'.format('\tCells', list(self._cells.keys())) - return string - - @property - def cells(self): - return self._cells - - @property - def bounding_box(self) -> openmc.BoundingBox: - regions = [c.region for c in self.cells.values() - if c.region is not None] - if regions: - return openmc.Union(regions).bounding_box - else: - return openmc.BoundingBox.infinite() - - @classmethod - def from_hdf5(cls, group, cells): - """Create universe from HDF5 group - - Parameters - ---------- - group : h5py.Group - Group in HDF5 file - cells : dict - Dictionary mapping cell IDs to instances of :class:`openmc.Cell`. - - Returns - ------- - openmc.Universe - Universe instance - - """ - universe_id = int(group.name.split('/')[-1].lstrip('universe ')) - cell_ids = group['cells'][()] - - # Create this Universe - universe = cls(universe_id) - - # Add each Cell to the Universe - for cell_id in cell_ids: - universe.add_cell(cells[cell_id]) - - return universe - def find(self, point): """Find cells/universes/lattices which contain a given point @@ -528,67 +563,6 @@ class Universe(UniverseBase): axes.imshow(img, extent=(x_min, x_max, y_min, y_max), **kwargs) return axes - def add_cell(self, cell): - """Add a cell to the universe. - - Parameters - ---------- - cell : openmc.Cell - Cell to add - - """ - - if not isinstance(cell, openmc.Cell): - msg = f'Unable to add a Cell to Universe ID="{self._id}" since ' \ - f'"{cell}" is not a Cell' - raise TypeError(msg) - - cell_id = cell.id - - if cell_id not in self._cells: - self._cells[cell_id] = cell - - def add_cells(self, cells): - """Add multiple cells to the universe. - - Parameters - ---------- - cells : Iterable of openmc.Cell - Cells to add - - """ - - if not isinstance(cells, Iterable): - msg = f'Unable to add Cells to Universe ID="{self._id}" since ' \ - f'"{cells}" is not iterable' - raise TypeError(msg) - - for cell in cells: - self.add_cell(cell) - - def remove_cell(self, cell): - """Remove a cell from the universe. - - Parameters - ---------- - cell : openmc.Cell - Cell to remove - - """ - - if not isinstance(cell, openmc.Cell): - msg = f'Unable to remove a Cell from Universe ID="{self._id}" ' \ - f'since "{cell}" is not a Cell' - raise TypeError(msg) - - # If the Cell is in the Universe's list of Cells, delete it - self._cells.pop(cell.id, None) - - def clear_cells(self): - """Remove all cells from the universe.""" - - self._cells.clear() - def get_nuclides(self): """Returns all nuclides in the universe @@ -636,55 +610,128 @@ class Universe(UniverseBase): return nuclides - def get_all_cells(self, memo=None): - """Return all cells that are contained within the universe - Returns - ------- + +class Universe(UniverseBase): + """A collection of cells that can be repeated. + + Parameters + ---------- + universe_id : int, optional + Unique identifier of the universe. If not specified, an identifier will + automatically be assigned + name : str, optional + Name of the universe. If not specified, the name is the empty string. + cells : Iterable of openmc.Cell, optional + Cells to add to the universe. By default no cells are added. + + Attributes + ---------- + id : int + Unique identifier of the universe + name : str + Name of the universe + cells : dict + Dictionary whose keys are cell IDs and values are :class:`Cell` + instances + volume : float + Volume of the universe in cm^3. This can either be set manually or + calculated in a stochastic volume calculation and added via the + :meth:`Universe.add_volume_information` method. + bounding_box : openmc.BoundingBox + Lower-left and upper-right coordinates of an axis-aligned bounding box + of the universe. + + """ + + def __init__(self, universe_id=None, name='', cells=None): + super().__init__(universe_id, name) + + if cells is not None: + self.add_cells(cells) + + def __repr__(self): + string = super().__repr__() + string += '{: <16}=\t{}\n'.format('\tGeom', 'CSG') + string += '{: <16}=\t{}\n'.format('\tCells', list(self._cells.keys())) + return string + + @property + def bounding_box(self) -> openmc.BoundingBox: + regions = [c.region for c in self.cells.values() + if c.region is not None] + if regions: + return openmc.Union(regions).bounding_box + else: + return openmc.BoundingBox.infinite() + + @classmethod + def from_hdf5(cls, group, cells): + """Create universe from HDF5 group + + Parameters + ---------- + group : h5py.Group + Group in HDF5 file cells : dict - Dictionary whose keys are cell IDs and values are :class:`Cell` - instances - - """ - - if memo is None: - memo = set() - elif self in memo: - return {} - memo.add(self) - - # Add this Universe's cells to the dictionary - cells = {} - cells.update(self._cells) - - # Append all Cells in each Cell in the Universe to the dictionary - for cell in self._cells.values(): - cells.update(cell.get_all_cells(memo)) - - return cells - - def get_all_materials(self, memo=None): - """Return all materials that are contained within the universe + Dictionary mapping cell IDs to instances of :class:`openmc.Cell`. Returns ------- - materials : dict - Dictionary whose keys are material IDs and values are - :class:`Material` instances + openmc.Universe + Universe instance + + """ + universe_id = int(group.name.split('/')[-1].lstrip('universe ')) + cell_ids = group['cells'][()] + + # Create this Universe + universe = cls(universe_id) + + # Add each Cell to the Universe + for cell_id in cell_ids: + universe.add_cell(cells[cell_id]) + + return universe + + + def add_cell(self, cell): + """Add a cell to the universe. + + Parameters + ---------- + cell : openmc.Cell + Cell to add """ - if memo is None: - memo = set() + if not isinstance(cell, openmc.Cell): + msg = f'Unable to add a Cell to Universe ID="{self._id}" since ' \ + f'"{cell}" is not a Cell' + raise TypeError(msg) - materials = {} + cell_id = cell.id - # Append all Cells in each Cell in the Universe to the dictionary - cells = self.get_all_cells(memo) - for cell in cells.values(): - materials.update(cell.get_all_materials(memo)) + if cell_id not in self._cells: + self._cells[cell_id] = cell - return materials + def remove_cell(self, cell): + """Remove a cell from the universe. + + Parameters + ---------- + cell : openmc.Cell + Cell to remove + + """ + + if not isinstance(cell, openmc.Cell): + msg = f'Unable to remove a Cell from Universe ID="{self._id}" ' \ + f'since "{cell}" is not a Cell' + raise TypeError(msg) + + # If the Cell is in the Universe's list of Cells, delete it + self._cells.pop(cell.id, None) def create_xml_subelement(self, xml_element, memo=None): if memo is None: @@ -706,50 +753,6 @@ class Universe(UniverseBase): cell_element.set("universe", str(self._id)) xml_element.append(cell_element) - def _determine_paths(self, path='', instances_only=False): - """Count the number of instances for each cell in the universe, and - record the count in the :attr:`Cell.num_instances` properties.""" - - univ_path = path + f'u{self.id}' - - for cell in self.cells.values(): - cell_path = f'{univ_path}->c{cell.id}' - fill = cell._fill - fill_type = cell.fill_type - - # If universe-filled, recursively count cells in filling universe - if fill_type == 'universe': - fill._determine_paths(cell_path + '->', instances_only) - - # If lattice-filled, recursively call for all universes in lattice - elif fill_type == 'lattice': - latt = fill - - # Count instances in each universe in the lattice - for index in latt._natural_indices: - latt_path = '{}->l{}({})->'.format( - cell_path, latt.id, ",".join(str(x) for x in index)) - univ = latt.get_universe(index) - univ._determine_paths(latt_path, instances_only) - - else: - if fill_type == 'material': - mat = fill - elif fill_type == 'distribmat': - mat = fill[cell._num_instances] - else: - mat = None - - if mat is not None: - mat._num_instances += 1 - if not instances_only: - mat._paths.append(f'{cell_path}->m{mat.id}') - - # Append current path - cell._num_instances += 1 - if not instances_only: - cell._paths.append(cell_path) - def _partial_deepcopy(self): """Clone all of the openmc.Universe object's attributes except for its cells, as they are copied within the clone function. This should only to be @@ -758,363 +761,3 @@ class Universe(UniverseBase): clone = openmc.Universe(name=self.name) clone.volume = self.volume return clone - - -class DAGMCUniverse(UniverseBase): - """A reference to a DAGMC file to be used in the model. - - .. versionadded:: 0.13.0 - - Parameters - ---------- - filename : path-like - Path to the DAGMC file used to represent this universe. - universe_id : int, optional - Unique identifier of the universe. If not specified, an identifier will - automatically be assigned. - name : str, optional - Name of the universe. If not specified, the name is the empty string. - auto_geom_ids : bool - Set IDs automatically on initialization (True) or report overlaps in ID - space between CSG and DAGMC (False) - auto_mat_ids : bool - Set IDs automatically on initialization (True) or report overlaps in ID - space between OpenMC and UWUW materials (False) - - Attributes - ---------- - id : int - Unique identifier of the universe - name : str - Name of the universe - filename : str - Path to the DAGMC file used to represent this universe. - auto_geom_ids : bool - Set IDs automatically on initialization (True) or report overlaps in ID - space between CSG and DAGMC (False) - auto_mat_ids : bool - Set IDs automatically on initialization (True) or report overlaps in ID - space between OpenMC and UWUW materials (False) - bounding_box : openmc.BoundingBox - Lower-left and upper-right coordinates of an axis-aligned bounding box - of the universe. - - .. versionadded:: 0.13.1 - material_names : list of str - Return a sorted list of materials names that are contained within the - DAGMC h5m file. This is useful when naming openmc.Material() objects - as each material name present in the DAGMC h5m file must have a - matching openmc.Material() with the same name. - - .. versionadded:: 0.13.2 - n_cells : int - The number of cells in the DAGMC model. This is the number of cells at - runtime and accounts for the implicit complement whether or not is it - present in the DAGMC file. - - .. versionadded:: 0.13.2 - n_surfaces : int - The number of surfaces in the model. - - .. versionadded:: 0.13.2 - - """ - - def __init__(self, - filename: cv.PathLike, - universe_id=None, - name='', - auto_geom_ids=False, - auto_mat_ids=False): - super().__init__(universe_id, name) - # Initialize class attributes - self.filename = filename - self.auto_geom_ids = auto_geom_ids - self.auto_mat_ids = auto_mat_ids - - def __repr__(self): - string = super().__repr__() - string += '{: <16}=\t{}\n'.format('\tGeom', 'DAGMC') - string += '{: <16}=\t{}\n'.format('\tFile', self.filename) - return string - - @property - def bounding_box(self): - with h5py.File(self.filename) as dagmc_file: - coords = dagmc_file['tstt']['nodes']['coordinates'][()] - lower_left_corner = coords.min(axis=0) - upper_right_corner = coords.max(axis=0) - return openmc.BoundingBox(lower_left_corner, upper_right_corner) - - @property - def filename(self): - return self._filename - - @filename.setter - def filename(self, val: cv.PathLike): - cv.check_type('DAGMC filename', val, cv.PathLike) - self._filename = input_path(val) - - @property - def auto_geom_ids(self): - return self._auto_geom_ids - - @auto_geom_ids.setter - def auto_geom_ids(self, val): - cv.check_type('DAGMC automatic geometry ids', val, bool) - self._auto_geom_ids = val - - @property - def auto_mat_ids(self): - return self._auto_mat_ids - - @auto_mat_ids.setter - def auto_mat_ids(self, val): - cv.check_type('DAGMC automatic material ids', val, bool) - self._auto_mat_ids = val - - @property - def material_names(self): - dagmc_file_contents = h5py.File(self.filename) - material_tags_hex = dagmc_file_contents['/tstt/tags/NAME'].get( - 'values') - material_tags_ascii = [] - for tag in material_tags_hex: - candidate_tag = tag.tobytes().decode().replace('\x00', '') - # tags might be for temperature or reflective surfaces - if candidate_tag.startswith('mat:'): - # removes first 4 characters as openmc.Material name should be - # set without the 'mat:' part of the tag - material_tags_ascii.append(candidate_tag[4:]) - - return sorted(set(material_tags_ascii)) - - def get_all_cells(self, memo=None): - return {} - - def get_all_materials(self, memo=None): - return {} - - def _n_geom_elements(self, geom_type): - """ - Helper function for retrieving the number geometric entities in a DAGMC - file - - Parameters - ---------- - geom_type : str - The type of geometric entity to count. One of {'Volume', 'Surface'}. Returns - the runtime number of voumes in the DAGMC model (includes implicit complement). - - Returns - ------- - int - Number of geometry elements of the specified type - """ - cv.check_value('geometry type', geom_type, ('volume', 'surface')) - - def decode_str_tag(tag_val): - return tag_val.tobytes().decode().replace('\x00', '') - - with h5py.File(self.filename) as dagmc_file: - category_data = dagmc_file['tstt/tags/CATEGORY/values'] - category_strs = map(decode_str_tag, category_data) - n = sum([v == geom_type.capitalize() for v in category_strs]) - - # check for presence of an implicit complement in the file and - # increment the number of cells if it doesn't exist - if geom_type == 'volume': - name_data = dagmc_file['tstt/tags/NAME/values'] - name_strs = map(decode_str_tag, name_data) - if not sum(['impl_complement' in n for n in name_strs]): - n += 1 - return n - - @property - def n_cells(self): - return self._n_geom_elements('volume') - - @property - def n_surfaces(self): - return self._n_geom_elements('surface') - - def create_xml_subelement(self, xml_element, memo=None): - if memo is None: - memo = set() - - if self in memo: - return - - memo.add(self) - - # Set xml element values - dagmc_element = ET.Element('dagmc_universe') - dagmc_element.set('id', str(self.id)) - - if self.auto_geom_ids: - dagmc_element.set('auto_geom_ids', 'true') - if self.auto_mat_ids: - dagmc_element.set('auto_mat_ids', 'true') - dagmc_element.set('filename', str(self.filename)) - xml_element.append(dagmc_element) - - def bounding_region( - self, - bounded_type: str = 'box', - boundary_type: str = 'vacuum', - starting_id: int = 10000, - padding_distance: float = 0. - ): - """Creates a either a spherical or box shaped bounding region around - the DAGMC geometry. - - .. versionadded:: 0.13.1 - - Parameters - ---------- - bounded_type : str - The type of bounding surface(s) to use when constructing the region. - Options include a single spherical surface (sphere) or a rectangle - made from six planes (box). - boundary_type : str - Boundary condition that defines the behavior for particles hitting - the surface. Defaults to vacuum boundary condition. Passed into the - surface construction. - starting_id : int - Starting ID of the surface(s) used in the region. For bounded_type - 'box', the next 5 IDs will also be used. Defaults to 10000 to reduce - the chance of an overlap of surface IDs with the DAGMC geometry. - padding_distance : float - Distance between the bounding region surfaces and the minimal - bounding box. Allows for the region to be larger than the DAGMC - geometry. - - Returns - ------- - openmc.Region - Region instance - """ - - check_type('boundary type', boundary_type, str) - check_value('boundary type', boundary_type, _BOUNDARY_TYPES) - check_type('starting surface id', starting_id, Integral) - check_type('bounded type', bounded_type, str) - check_value('bounded type', bounded_type, ('box', 'sphere')) - - bbox = self.bounding_box.expand(padding_distance, True) - - if bounded_type == 'sphere': - radius = np.linalg.norm(bbox.upper_right - bbox.center) - bounding_surface = openmc.Sphere( - surface_id=starting_id, - x0=bbox.center[0], - y0=bbox.center[1], - z0=bbox.center[2], - boundary_type=boundary_type, - r=radius, - ) - - return -bounding_surface - - if bounded_type == 'box': - # defines plane surfaces for all six faces of the bounding box - lower_x = openmc.XPlane(bbox[0][0], surface_id=starting_id) - upper_x = openmc.XPlane(bbox[1][0], surface_id=starting_id+1) - lower_y = openmc.YPlane(bbox[0][1], surface_id=starting_id+2) - upper_y = openmc.YPlane(bbox[1][1], surface_id=starting_id+3) - lower_z = openmc.ZPlane(bbox[0][2], surface_id=starting_id+4) - upper_z = openmc.ZPlane(bbox[1][2], surface_id=starting_id+5) - - region = +lower_x & -upper_x & +lower_y & -upper_y & +lower_z & -upper_z - - for surface in region.get_surfaces().values(): - surface.boundary_type = boundary_type - - return region - - def bounded_universe(self, bounding_cell_id=10000, **kwargs): - """Returns an openmc.Universe filled with this DAGMCUniverse and bounded - with a cell. Defaults to a box cell with a vacuum surface however this - can be changed using the kwargs which are passed directly to - DAGMCUniverse.bounding_region(). - - Parameters - ---------- - bounding_cell_id : int - The cell ID number to use for the bounding cell, defaults to 10000 to reduce - the chance of overlapping ID numbers with the DAGMC geometry. - - Returns - ------- - openmc.Universe - Universe instance - """ - bounding_cell = openmc.Cell( - fill=self, cell_id=bounding_cell_id, region=self.bounding_region(**kwargs)) - return openmc.Universe(cells=[bounding_cell]) - - @classmethod - def from_hdf5(cls, group): - """Create DAGMC universe from HDF5 group - - Parameters - ---------- - group : h5py.Group - Group in HDF5 file - - Returns - ------- - openmc.DAGMCUniverse - DAGMCUniverse instance - - """ - id = int(group.name.split('/')[-1].lstrip('universe ')) - fname = group['filename'][()].decode() - name = group['name'][()].decode() if 'name' in group else None - - out = cls(fname, universe_id=id, name=name) - - out.auto_geom_ids = bool(group.attrs['auto_geom_ids']) - out.auto_mat_ids = bool(group.attrs['auto_mat_ids']) - - return out - - @classmethod - def from_xml_element(cls, elem): - """Generate DAGMC universe from XML element - - Parameters - ---------- - elem : lxml.etree._Element - `` element - - Returns - ------- - openmc.DAGMCUniverse - DAGMCUniverse instance - - """ - id = int(get_text(elem, 'id')) - fname = get_text(elem, 'filename') - - out = cls(fname, universe_id=id) - - name = get_text(elem, 'name') - if name is not None: - out.name = name - - out.auto_geom_ids = bool(elem.get('auto_geom_ids')) - out.auto_mat_ids = bool(elem.get('auto_mat_ids')) - - return out - - def _partial_deepcopy(self): - """Clone all of the openmc.DAGMCUniverse object's attributes except for - its cells, as they are copied within the clone function. This should - only to be used within the openmc.UniverseBase.clone() context. - """ - clone = openmc.DAGMCUniverse(name=self.name, filename=self.filename) - clone.volume = self.volume - clone.auto_geom_ids = self.auto_geom_ids - clone.auto_mat_ids = self.auto_mat_ids - return clone diff --git a/src/cell.cpp b/src/cell.cpp index 888766787..d4d28fb70 100644 --- a/src/cell.cpp +++ b/src/cell.cpp @@ -252,12 +252,12 @@ void Cell::to_hdf5(hid_t cell_group) const // default constructor CSGCell::CSGCell() { - geom_type_ = GeometryType::CSG; + geom_type() = GeometryType::CSG; } CSGCell::CSGCell(pugi::xml_node cell_node) { - geom_type_ = GeometryType::CSG; + geom_type() = GeometryType::CSG; if (check_for_node(cell_node, "id")) { id_ = std::stoi(get_node_value(cell_node, "id")); diff --git a/src/dagmc.cpp b/src/dagmc.cpp index b79676c36..134e31ecf 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -72,6 +72,23 @@ DAGUniverse::DAGUniverse(pugi::xml_node node) adjust_material_ids_ = get_node_value_bool(node, "auto_mat_ids"); } + // get material assignment overloading + if (check_for_node(node, "material_overrides")) { + auto mat_node = node.child("material_overrides"); + // loop over all subelements (each subelement corresponds to a material) + for (pugi::xml_node cell_node : mat_node.children("cell_override")) { + // Store assignment reference name + int32_t ref_assignment = std::stoi(get_node_value(cell_node, "id")); + + // Get mat name for each assignement instances + vector instance_mats = + get_node_array(cell_node, "material_ids"); + + // Store mat name for each instances + material_overrides_.emplace(ref_assignment, instance_mats); + } + } + initialize(); } @@ -211,12 +228,13 @@ void DAGUniverse::init_geometry() if (mat_str == "graveyard") { graveyard = vol_handle; } - // material void checks if (mat_str == "void" || mat_str == "vacuum" || mat_str == "graveyard") { c->material_.push_back(MATERIAL_VOID); } else { - if (uses_uwuw()) { + if (material_overrides_.count(c->id_)) { + override_assign_material(c); + } else if (uses_uwuw()) { uwuw_assign_material(vol_handle, c); } else { legacy_assign_material(mat_str, c); @@ -609,6 +627,33 @@ void DAGUniverse::uwuw_assign_material( fatal_error("DAGMC was not configured with UWUW."); #endif // OPENMC_UWUW } + +void DAGUniverse::override_assign_material(std::unique_ptr& c) const +{ + // if Cell ID matches an override key, use it to override the material + // assignment else if UWUW is used, get the material assignment from the DAGMC + // metadata + // Notify User that an override is being applied on a DAGMCCell + write_message(fmt::format("Applying override for DAGMCCell {}", c->id_), 8); + + if (settings::verbosity >= 10) { + auto msg = fmt::format("Assigning DAGMC cell {} material(s) based on " + "override information (see input XML).", + c->id_); + write_message(msg, 10); + } + + // Override the material assignment for each cell instance using the legacy + // assignement + for (auto mat_id : material_overrides_.at(c->id_)) { + if (model::material_map.find(mat_id) == model::material_map.end()) { + fatal_error(fmt::format( + "Material with ID '{}' not found for DAGMC cell {}", mat_id, c->id_)); + } + c->material_.push_back(mat_id); + } +} + //============================================================================== // DAGMC Cell implementation //============================================================================== @@ -616,7 +661,7 @@ void DAGUniverse::uwuw_assign_material( DAGCell::DAGCell(std::shared_ptr dag_ptr, int32_t dag_idx) : Cell {}, dagmc_ptr_(dag_ptr), dag_index_(dag_idx) { - geom_type_ = GeometryType::DAG; + geom_type() = GeometryType::DAG; }; std::pair DAGCell::distance( @@ -719,7 +764,7 @@ BoundingBox DAGCell::bounding_box() const DAGSurface::DAGSurface(std::shared_ptr dag_ptr, int32_t dag_idx) : Surface {}, dagmc_ptr_(dag_ptr), dag_index_(dag_idx) { - geom_type_ = GeometryType::DAG; + geom_type() = GeometryType::DAG; } // empty constructor moab::EntityHandle DAGSurface::mesh_handle() const @@ -818,12 +863,61 @@ int32_t next_cell(int32_t surf, int32_t curr_cell, int32_t univ) return univp->cell_index(new_vol); } +extern "C" int openmc_dagmc_universe_get_cell_ids( + int32_t univ_id, int32_t* ids, size_t* n) +{ + // make sure the universe id is a DAGMC Universe + const auto& univ = model::universes[model::universe_map[univ_id]]; + if (univ->geom_type() != GeometryType::DAG) { + set_errmsg(fmt::format("Universe {} is not a DAGMC Universe", univ_id)); + return OPENMC_E_INVALID_TYPE; + } + + std::vector dag_cell_ids; + for (const auto& cell_index : univ->cells_) { + const auto& cell = model::cells[cell_index]; + if (cell->geom_type() == GeometryType::CSG) { + set_errmsg(fmt::format("Cell {} is not a DAGMC Cell", cell->id_)); + return OPENMC_E_INVALID_TYPE; + } + dag_cell_ids.push_back(cell->id_); + } + std::copy(dag_cell_ids.begin(), dag_cell_ids.end(), ids); + *n = dag_cell_ids.size(); + return 0; +} + +extern "C" int openmc_dagmc_universe_get_num_cells(int32_t univ_id, size_t* n) +{ + // make sure the universe id is a DAGMC Universe + const auto& univ = model::universes[model::universe_map[univ_id]]; + if (univ->geom_type() != GeometryType::DAG) { + set_errmsg(fmt::format("Universe {} is not a DAGMC universe", univ_id)); + return OPENMC_E_INVALID_TYPE; + } + *n = univ->cells_.size(); + return 0; +} + } // namespace openmc #else namespace openmc { +extern "C" int openmc_dagmc_universe_get_cell_ids( + int32_t univ_id, int32_t* ids, size_t* n) +{ + set_errmsg("OpenMC was not configured with DAGMC"); + return OPENMC_E_UNASSIGNED; +}; + +extern "C" int openmc_dagmc_universe_get_num_cells(int32_t univ_id, size_t* n) +{ + set_errmsg("OpenMC was not configured with DAGMC"); + return OPENMC_E_UNASSIGNED; +}; + void read_dagmc_universes(pugi::xml_node node) { if (check_for_node(node, "dagmc_universe")) { diff --git a/src/particle.cpp b/src/particle.cpp index 64c50c943..0ea865014 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -533,7 +533,7 @@ void Particle::cross_surface(const Surface& surf) // if we're crossing a CSG surface, make sure the DAG history is reset #ifdef DAGMC - if (surf.geom_type_ == GeometryType::CSG) + if (surf.geom_type() == GeometryType::CSG) history().reset(); #endif @@ -548,7 +548,7 @@ void Particle::cross_surface(const Surface& surf) #ifdef DAGMC // in DAGMC, we know what the next cell should be - if (surf.geom_type_ == GeometryType::DAG) { + if (surf.geom_type() == GeometryType::DAG) { int32_t i_cell = next_cell(std::abs(surface()), cell_last(n_coord() - 1), lowest_coord().universe) - 1; @@ -668,7 +668,8 @@ void Particle::cross_reflective_bc(const Surface& surf, Direction new_u) // the lower universes. // (unless we're using a dagmc model, which has exactly one universe) n_coord() = 1; - if (surf.geom_type_ != GeometryType::DAG && !neighbor_list_find_cell(*this)) { + if (surf.geom_type() != GeometryType::DAG && + !neighbor_list_find_cell(*this)) { mark_as_lost("Couldn't find particle after reflecting from surface " + std::to_string(surf.id_) + "."); return; diff --git a/src/plot.cpp b/src/plot.cpp index de7d475e1..85131d67a 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -1303,7 +1303,7 @@ void ProjectionPlot::create_output() const int32_t i_surface = std::abs(p.surface()) - 1; if (i_surface > 0 && - model::surfaces[i_surface]->geom_type_ == GeometryType::DAG) { + model::surfaces[i_surface]->geom_type() == GeometryType::DAG) { #ifdef DAGMC int32_t i_cell = next_cell(i_surface, p.cell_last(p.n_coord() - 1), p.lowest_coord().universe); diff --git a/src/surface.cpp b/src/surface.cpp index 50ef2a128..dbcaf8498 100644 --- a/src/surface.cpp +++ b/src/surface.cpp @@ -165,9 +165,9 @@ void Surface::to_hdf5(hid_t group_id) const { hid_t surf_group = create_group(group_id, fmt::format("surface {}", id_)); - if (geom_type_ == GeometryType::DAG) { + if (geom_type() == GeometryType::DAG) { write_string(surf_group, "geom_type", "dagmc", false); - } else if (geom_type_ == GeometryType::CSG) { + } else if (geom_type() == GeometryType::CSG) { write_string(surf_group, "geom_type", "csg", false); if (bc_) { @@ -189,11 +189,11 @@ void Surface::to_hdf5(hid_t group_id) const CSGSurface::CSGSurface() : Surface {} { - geom_type_ = GeometryType::CSG; + geom_type() = GeometryType::CSG; }; CSGSurface::CSGSurface(pugi::xml_node surf_node) : Surface {surf_node} { - geom_type_ = GeometryType::CSG; + geom_type() = GeometryType::CSG; }; //============================================================================== diff --git a/tests/unit_tests/dagmc/test_model.py b/tests/unit_tests/dagmc/test_model.py new file mode 100644 index 000000000..9fdfbcebc --- /dev/null +++ b/tests/unit_tests/dagmc/test_model.py @@ -0,0 +1,256 @@ +from pathlib import Path + +import lxml.etree as ET +import numpy as np +import pytest +import openmc +from openmc.utility_funcs import change_directory + +pytestmark = pytest.mark.skipif( + not openmc.lib._dagmc_enabled(), + reason="DAGMC CAD geometry is not enabled.") + + +@pytest.fixture() +def model(request): + pitch = 1.26 + + mats = {} + mats["no-void fuel"] = openmc.Material(1, name="no-void fuel") + mats["no-void fuel"].add_nuclide("U235", 0.03) + mats["no-void fuel"].add_nuclide("U238", 0.97) + mats["no-void fuel"].add_nuclide("O16", 2.0) + mats["no-void fuel"].set_density("g/cm3", 10.0) + + mats["41"] = openmc.Material(name="41") + mats["41"].add_nuclide("H1", 2.0) + mats["41"].add_element("O", 1.0) + mats["41"].set_density("g/cm3", 1.0) + mats["41"].add_s_alpha_beta("c_H_in_H2O") + + p = Path(request.fspath).parent / "dagmc.h5m" + + daguniv = openmc.DAGMCUniverse(p, auto_geom_ids=True) + + lattice = openmc.RectLattice() + lattice.dimension = [2, 2] + lattice.lower_left = [-pitch, -pitch] + lattice.pitch = [pitch, pitch] + lattice.universes = [ + [daguniv, daguniv], + [daguniv, daguniv]] + + box = openmc.model.RectangularParallelepiped(-pitch, pitch, -pitch, pitch, -5, 5) + + root = openmc.Universe(cells=[openmc.Cell(region=-box, fill=lattice)]) + + settings = openmc.Settings() + settings.batches = 100 + settings.inactive = 10 + settings.particles = 1000 + + ll, ur = root.bounding_box + mat_vol = openmc.VolumeCalculation([mats["no-void fuel"]], 1000000, ll, ur) + cell_vol = openmc.VolumeCalculation(list(root.cells.values()), 1000000, ll, ur) + settings.volume_calculations = [mat_vol, cell_vol] + + model = openmc.Model() + model.materials = openmc.Materials(mats.values()) + model.geometry = openmc.Geometry(root=root) + model.settings = settings + + with change_directory(tmpdir=True): + try: + model.init_lib() + model.sync_dagmc_universes() + yield model + finally: + model.finalize_lib() + openmc.reset_auto_ids() + + +def test_dagmc_replace_material_assignment(model): + mats = {} + + mats["foo"] = openmc.Material(name="foo") + mats["foo"].add_nuclide("H1", 2.0) + mats["foo"].add_element("O", 1.0) + mats["foo"].set_density("g/cm3", 1.0) + mats["foo"].add_s_alpha_beta("c_H_in_H2O") + + for univ in model.geometry.get_all_universes().values(): + if not isinstance(univ, openmc.DAGMCUniverse): + break + + cells_with_41 = [] + for cell in univ.cells.values(): + if cell.fill is None: + continue + if cell.fill.name == "41": + cells_with_41.append(cell.id) + univ.replace_material_assignment("41", mats["foo"]) + for cell_id in cells_with_41: + assert univ.cells[cell_id] == mats["foo"] + + +def test_dagmc_add_material_override_with_id(model): + mats = {} + mats["foo"] = openmc.Material(name="foo") + mats["foo"].add_nuclide("H1", 2.0) + mats["foo"].add_element("O", 1.0) + mats["foo"].set_density("g/cm3", 1.0) + mats["foo"].add_s_alpha_beta("c_H_in_H2O") + + for univ in model.geometry.get_all_universes().values(): + if not isinstance(univ, openmc.DAGMCUniverse): + break + + cells_with_41 = [] + for cell in univ.cells.values(): + if cell.fill is None: + continue + if cell.fill.name == "41": + cells_with_41.append(cell.id) + univ.add_material_override(cell.id, mats["foo"]) + for cell_id in cells_with_41: + assert univ.cells[cell_id] == mats["foo"] + + +def test_dagmc_add_material_override_with_cell(model): + mats = {} + mats["foo"] = openmc.Material(name="foo") + mats["foo"].add_nuclide("H1", 2.0) + mats["foo"].add_element("O", 1.0) + mats["foo"].set_density("g/cm3", 1.0) + mats["foo"].add_s_alpha_beta("c_H_in_H2O") + + for univ in model.geometry.get_all_universes().values(): + if not isinstance(univ, openmc.DAGMCUniverse): + break + + cells_with_41 = [] + for cell in univ.cells.values(): + if cell.fill is None: + continue + if cell.fill.name == "41": + cells_with_41.append(cell.id) + univ.add_material_override(cell, mats["foo"]) + for cell_id in cells_with_41: + assert univ.cells[cell_id] == mats["foo"] + + +def test_model_differentiate_depletable_with_dagmc(model, run_in_tmpdir): + model.calculate_volumes() + + # Get the volume of the no-void fuel material before differentiation + volume_before = np.sum([m.volume for m in model.materials if m.name == "no-void fuel"]) + + # Differentiate the depletable materials + model.differentiate_depletable_mats(diff_volume_method="divide equally") + # Get the volume of the no-void fuel material after differentiation + volume_after = np.sum([m.volume for m in model.materials if "fuel" in m.name]) + assert np.isclose(volume_before, volume_after) + assert len(model.materials) == 4*2 +1 + + +def test_model_differentiate_with_dagmc(model): + root = model.geometry.root_universe + ll, ur = root.bounding_box + model.calculate_volumes() + # Get the volume of the no-void fuel material before differentiation + volume_before = np.sum([m.volume for m in model.materials if m.name == "no-void fuel"]) + + # Differentiate all the materials + model.differentiate_mats(depletable_only=False) + + # Get the volume of the no-void fuel material after differentiation + mat_vol = openmc.VolumeCalculation(model.materials, 1000000, ll, ur) + model.settings.volume_calculations = [mat_vol] + model.init_lib() # need to reinitialize the lib after differentiating the materials + model.calculate_volumes() + volume_after = np.sum([m.volume for m in model.materials if "fuel" in m.name]) + assert np.isclose(volume_before, volume_after) + assert len(model.materials) == 4*2 + 4 + + +def test_bad_override_cell_id(model): + for univ in model.geometry.get_all_universes().values(): + if isinstance(univ, openmc.DAGMCUniverse): + break + with pytest.raises(ValueError, match="Cell ID '1' not found in DAGMC universe"): + univ.material_overrides = {1: model.materials[0]} + + +def test_bad_override_type(model): + not_a_dag_cell = openmc.Cell() + for univ in model.geometry.get_all_universes().values(): + if isinstance(univ, openmc.DAGMCUniverse): + break + with pytest.raises(ValueError, match="Unrecognized key type. Must be an integer or openmc.DAGMCCell object"): + univ.material_overrides = {not_a_dag_cell: model.materials[0]} + + +def test_bad_replacement_mat_name(model): + for univ in model.geometry.get_all_universes().values(): + if isinstance(univ, openmc.DAGMCUniverse): + break + with pytest.raises(ValueError, match="No material with name 'not_a_mat' found in the DAGMC universe"): + univ.replace_material_assignment("not_a_mat", model.materials[0]) + + +def test_dagmc_xml(model): + # Set the environment + mats = {} + mats["no-void fuel"] = openmc.Material(1, name="no-void fuel") + mats["no-void fuel"].add_nuclide("U235", 0.03) + mats["no-void fuel"].add_nuclide("U238", 0.97) + mats["no-void fuel"].add_nuclide("O16", 2.0) + mats["no-void fuel"].set_density("g/cm3", 10.0) + + mats[5] = openmc.Material(name="41") + mats[5].add_nuclide("H1", 2.0) + mats[5].add_element("O", 1.0) + mats[5].set_density("g/cm3", 1.0) + mats[5].add_s_alpha_beta("c_H_in_H2O") + + for univ in model.geometry.get_all_universes().values(): + if isinstance(univ, openmc.DAGMCUniverse): + dag_univ = univ + break + + for k, v in mats.items(): + if isinstance(k, int): + dag_univ.add_material_override(k, v) + model.materials.append(v) + elif isinstance(k, str): + dag_univ.replace_material_assignment(k, v) + + # Tesing the XML subelement generation + root = ET.Element('dagmc_universe') + dag_univ.create_xml_subelement(root) + dagmc_ele = root.find('dagmc_universe') + + assert dagmc_ele.get('id') == str(dag_univ.id) + assert dagmc_ele.get('filename') == str(dag_univ.filename) + assert dagmc_ele.get('auto_geom_ids') == str(dag_univ.auto_geom_ids).lower() + + override_eles = dagmc_ele.find('material_overrides').findall('cell_override') + assert len(override_eles) == 4 + + for i, override_ele in enumerate(override_eles): + cell_id = override_ele.get('id') + assert dag_univ.material_overrides[int(cell_id)][0].id == int(override_ele.find('material_ids').text) + + model.export_to_model_xml() + + xml_model = openmc.Model.from_model_xml() + + for univ in xml_model.geometry.get_all_universes().values(): + if isinstance(univ, openmc.DAGMCUniverse): + xml_dagmc_univ = univ + break + + assert xml_dagmc_univ._material_overrides.keys() == dag_univ._material_overrides.keys() + + for xml_mats, model_mats in zip(xml_dagmc_univ._material_overrides.values(), dag_univ._material_overrides.values()): + assert all([xml_mat.id == orig_mat.id for xml_mat, orig_mat in zip(xml_mats, model_mats)]) From 8c7200fad3e37f6274fe7ce292acdf77d8f9fafe Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Tue, 7 Jan 2025 17:31:25 -0600 Subject: [PATCH 218/671] Enable UWUW library when building with DAGMC in CI (#3246) --- tools/ci/gha-install.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/ci/gha-install.py b/tools/ci/gha-install.py index 282389a8f..d52c4c825 100644 --- a/tools/ci/gha-install.py +++ b/tools/ci/gha-install.py @@ -31,6 +31,7 @@ def install(omp=False, mpi=False, phdf5=False, dagmc=False, libmesh=False, ncrys if dagmc: cmake_cmd.append('-DOPENMC_USE_DAGMC=ON') + cmake_cmd.append('-DOPENMC_USE_UWUW=ON') dagmc_path = os.environ.get('HOME') + '/DAGMC' cmake_cmd.append('-DCMAKE_PREFIX_PATH=' + dagmc_path) From 6a5d80efd6a41a968c69702d4b0239f8fc6c6b2b Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Wed, 8 Jan 2025 17:27:09 +0100 Subject: [PATCH 219/671] updated docker file to latest DAGMC (#3251) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4a94e3c0b..583682ef5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,7 +48,7 @@ ENV DD_REPO='https://github.com/pshriwise/double-down' ENV DD_INSTALL_DIR=$HOME/Double_down # DAGMC variables -ENV DAGMC_BRANCH='v3.2.3' +ENV DAGMC_BRANCH='v3.2.4' ENV DAGMC_REPO='https://github.com/svalinn/DAGMC' ENV DAGMC_INSTALL_DIR=$HOME/DAGMC/ From 10a63bbd27c114386fc1085ee57a36681dd60aba Mon Sep 17 00:00:00 2001 From: Micah Gale Date: Wed, 8 Jan 2025 11:47:25 -0600 Subject: [PATCH 220/671] Move to support python 3.13 (#3165) --- .github/workflows/ci.yml | 20 ++++++++++---------- .readthedocs.yaml | 2 +- pyproject.toml | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5973c3cd4..c5051b8d0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - python-version: ["3.10"] + python-version: ["3.11"] mpi: [n, y] omp: [n, y] dagmc: [n] @@ -35,34 +35,34 @@ jobs: vectfit: [n] include: - - python-version: "3.11" - omp: n - mpi: n - python-version: "3.12" omp: n mpi: n + - python-version: "3.13" + omp: n + mpi: n - dagmc: y - python-version: "3.10" + python-version: "3.11" mpi: y omp: y - ncrystal: y - python-version: "3.10" + python-version: "3.11" mpi: n omp: n - libmesh: y - python-version: "3.10" + python-version: "3.11" mpi: y omp: y - libmesh: y - python-version: "3.10" + python-version: "3.11" mpi: n omp: y - event: y - python-version: "3.10" + python-version: "3.11" omp: y mpi: n - vectfit: y - python-version: "3.10" + python-version: "3.11" omp: n mpi: y name: "Python ${{ matrix.python-version }} (omp=${{ matrix.omp }}, diff --git a/.readthedocs.yaml b/.readthedocs.yaml index b7b69f9fe..7b69b64b6 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -3,7 +3,7 @@ version: 2 build: os: "ubuntu-20.04" tools: - python: "3.10" + python: "3.12" sphinx: configuration: docs/source/conf.py diff --git a/pyproject.toml b/pyproject.toml index d0419d550..e4e8472f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ authors = [ ] description = "OpenMC" version = "0.15.1-dev" -requires-python = ">=3.10" +requires-python = ">=3.11" 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.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] dependencies = [ "numpy", From 4492f9db10f8c8189f72795c14e36e8f32b153b0 Mon Sep 17 00:00:00 2001 From: Andrew Davis Date: Wed, 8 Jan 2025 18:16:01 +0000 Subject: [PATCH 221/671] Fix type comparison (#3244) --- src/weight_windows.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index 68f7550ae..e798a2f7a 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -737,7 +737,7 @@ WeightWindowsGenerator::WeightWindowsGenerator(pugi::xml_node node) int32_t mesh_idx = model::mesh_map[mesh_id]; max_realizations_ = std::stoi(get_node_value(node, "max_realizations")); - int active_batches = settings::n_batches - settings::n_inactive; + int32_t active_batches = settings::n_batches - settings::n_inactive; if (max_realizations_ > active_batches) { auto msg = fmt::format("The maximum number of specified tally realizations ({}) is " From 8ba66f9fe30072b3935b8d1b5282354509031f3e Mon Sep 17 00:00:00 2001 From: Adam Nelson <1037107+nelsonag@users.noreply.github.com> Date: Wed, 8 Jan 2025 13:58:59 -0600 Subject: [PATCH 222/671] Fix for erroneously non-zero tally results of photon threshold reactions (#3242) Co-authored-by: Paul Romano --- src/photon.cpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/photon.cpp b/src/photon.cpp index 08062a2e9..4b2fb4197 100644 --- a/src/photon.cpp +++ b/src/photon.cpp @@ -328,15 +328,19 @@ PhotonInteraction::PhotonInteraction(hid_t group) } // Take logarithm of energies and cross sections since they are log-log - // interpolated + // interpolated. Note that cross section libraries converted from ACE files + // represent zero as exp(-500) to avoid log-log interpolation errors. For + // 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_ > 0.0, xt::log(coherent_), -500.0); - incoherent_ = xt::where(incoherent_ > 0.0, xt::log(incoherent_), -500.0); + 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_ > 0.0, xt::log(photoelectric_total_), -500.0); + photoelectric_total_ > limit, xt::log(photoelectric_total_), -900.0); pair_production_total_ = xt::where( - pair_production_total_ > 0.0, xt::log(pair_production_total_), -500.0); - heating_ = xt::where(heating_ > 0.0, xt::log(heating_), -500.0); + pair_production_total_ > limit, xt::log(pair_production_total_), -900.0); + heating_ = xt::where(heating_ > limit, xt::log(heating_), -900.0); } PhotonInteraction::~PhotonInteraction() From 0d4a85d3a81b74c013fc30435870ea3f7ed4eb41 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 8 Jan 2025 14:00:57 -0600 Subject: [PATCH 223/671] Remove top-level import of openmc.lib (#3250) --- openmc/model/model.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index 5137ea1ab..402d6c061 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -16,7 +16,6 @@ from openmc.dummy_comm import DummyCommunicator from openmc.executor import _process_CLI_arguments from openmc.checkvalue import check_type, check_value from openmc.exceptions import InvalidIDError -import openmc.lib from openmc.utility_funcs import change_directory From 1eca46f536382c79884c8069676ef15d356e1e6c Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Wed, 8 Jan 2025 22:15:00 -0600 Subject: [PATCH 224/671] Write mesh type as a dataset always (#3253) --- docs/source/io_formats/statepoint.rst | 2 +- openmc/mesh.py | 2 +- src/mesh.cpp | 7 +------ 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/docs/source/io_formats/statepoint.rst b/docs/source/io_formats/statepoint.rst index 82d20a763..fd00a3e77 100644 --- a/docs/source/io_formats/statepoint.rst +++ b/docs/source/io_formats/statepoint.rst @@ -73,9 +73,9 @@ The current version of the statepoint file format is 18.1. **/tallies/meshes/mesh /** :Attributes: - **id** (*int*) -- ID of the mesh - - **type** (*char[]*) -- Type of mesh. :Datasets: - **name** (*char[]*) -- Name of the mesh. + - **type** (*char[]*) -- Type of mesh. - **dimension** (*int*) -- Number of mesh cells in each dimension. - **Regular Mesh Only:** - **lower_left** (*double[]*) -- Coordinates of lower-left corner of diff --git a/openmc/mesh.py b/openmc/mesh.py index 0b6f6b84e..e4d83d81d 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -99,7 +99,7 @@ class MeshBase(IDManagerMixin, ABC): Instance of a MeshBase subclass """ - mesh_type = 'regular' if 'type' not in group.attrs else group.attrs['type'].decode() + mesh_type = 'regular' if 'type' not in group.keys() else group['type'][()].decode() mesh_id = int(group.name.split('/')[-1].lstrip('mesh ')) mesh_name = '' if not 'name' in group else group['name'][()].decode() diff --git a/src/mesh.cpp b/src/mesh.cpp index 4b1878c5a..97bf710ca 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -246,7 +246,7 @@ void Mesh::to_hdf5(hid_t group) const hid_t mesh_group = create_group(group, group_name.c_str()); // Write mesh type - write_attribute(mesh_group, "type", this->get_mesh_type()); + write_dataset(mesh_group, "type", this->get_mesh_type()); // Write mesh ID write_attribute(mesh_group, "id", id_); @@ -308,7 +308,6 @@ Position StructuredMesh::sample_element( UnstructuredMesh::UnstructuredMesh(pugi::xml_node node) : Mesh(node) { - // check the mesh type if (check_for_node(node, "type")) { auto temp = get_node_value(node, "type", true, true); @@ -970,7 +969,6 @@ std::pair, vector> RegularMesh::plot( void RegularMesh::to_hdf5_inner(hid_t mesh_group) const { - write_dataset(mesh_group, "type", "regular"); write_dataset(mesh_group, "dimension", get_x_shape()); write_dataset(mesh_group, "lower_left", lower_left_); write_dataset(mesh_group, "upper_right", upper_right_); @@ -1156,7 +1154,6 @@ std::pair, vector> RectilinearMesh::plot( void RectilinearMesh::to_hdf5_inner(hid_t mesh_group) const { - write_dataset(mesh_group, "type", "rectilinear"); write_dataset(mesh_group, "x_grid", grid_[0]); write_dataset(mesh_group, "y_grid", grid_[1]); write_dataset(mesh_group, "z_grid", grid_[2]); @@ -1431,7 +1428,6 @@ std::pair, vector> CylindricalMesh::plot( void CylindricalMesh::to_hdf5_inner(hid_t mesh_group) const { - write_dataset(mesh_group, "type", "cylindrical"); write_dataset(mesh_group, "r_grid", grid_[0]); write_dataset(mesh_group, "phi_grid", grid_[1]); write_dataset(mesh_group, "z_grid", grid_[2]); @@ -1743,7 +1739,6 @@ std::pair, vector> SphericalMesh::plot( void SphericalMesh::to_hdf5_inner(hid_t mesh_group) const { - write_dataset(mesh_group, "type", SphericalMesh::mesh_type); write_dataset(mesh_group, "r_grid", grid_[0]); write_dataset(mesh_group, "theta_grid", grid_[1]); write_dataset(mesh_group, "phi_grid", grid_[2]); From 7c142a361fc91a8f2fc69a163b314afc92d4452c Mon Sep 17 00:00:00 2001 From: "Joseph F. Specht IV" <102691489+jspecht3@users.noreply.github.com> Date: Fri, 10 Jan 2025 13:18:03 -0600 Subject: [PATCH 225/671] Change `Zernike` documentation in polynomial.py (#3258) --- openmc/polynomial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmc/polynomial.py b/openmc/polynomial.py index 1259c61ca..341cdff47 100644 --- a/openmc/polynomial.py +++ b/openmc/polynomial.py @@ -92,7 +92,7 @@ class Zernike(Polynomial): Parameters ---------- coef : Iterable of float - A list of coefficients of each term in radial only Zernike polynomials + A list of coefficients of each term in Zernike polynomials radius : float Domain of Zernike polynomials to be applied on. Default is 1. From 51f0e6f35039a61925477033a8eea4b2c871a728 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 10 Jan 2025 15:11:32 -0600 Subject: [PATCH 226/671] Add Patrick Shriwise to technical committee (#3255) --- docs/source/devguide/contributing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/devguide/contributing.rst b/docs/source/devguide/contributing.rst index c08b1a362..cda031389 100644 --- a/docs/source/devguide/contributing.rst +++ b/docs/source/devguide/contributing.rst @@ -109,7 +109,7 @@ Leadership Team The TC consists of the following individuals: - `Paul Romano `_ -- `Sterling Harper `_ +- `Patrick Shriwise `_ - `Adam Nelson `_ - `Benoit Forget `_ From c226c783c4e7d6c63a1c84ed5f1ee4a9beaead1c Mon Sep 17 00:00:00 2001 From: Ethan Peterson Date: Fri, 10 Jan 2025 17:20:00 -0500 Subject: [PATCH 227/671] Bug fix for Polygon 'yz' basis (#3259) --- openmc/model/surface_composite.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index 1f25b9ca6..c7655694d 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -1252,11 +1252,11 @@ class Polygon(CompositeSurface): else: op = operator.neg if basis == 'xy': - surf = openmc.Plane(a=dx, b=dy, d=-c) + surf = openmc.Plane(a=dx, b=dy, c=0.0, d=-c) elif basis == 'yz': - surf = openmc.Plane(b=dx, c=dy, d=-c) + surf = openmc.Plane(a=0.0, b=dx, c=dy, d=-c) elif basis == 'xz': - surf = openmc.Plane(a=dx, c=dy, d=-c) + surf = openmc.Plane(a=dx, b=0.0, c=dy, d=-c) else: y0 = -c/dy r2 = dy**2 / dx**2 From d2edf0ce4e8992155010dd2a2578a0ebbd0a1f90 Mon Sep 17 00:00:00 2001 From: Jan Malec Date: Sat, 11 Jan 2025 02:23:35 +0100 Subject: [PATCH 228/671] Fix path handling for thermal ACE generation (#3171) Co-authored-by: Paul Romano --- openmc/data/njoy.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openmc/data/njoy.py b/openmc/data/njoy.py index 1bf44891e..f5ef836f9 100644 --- a/openmc/data/njoy.py +++ b/openmc/data/njoy.py @@ -426,7 +426,7 @@ def make_ace(filename, temperatures=None, acer=True, xsdir=None, def make_ace_thermal(filename, filename_thermal, temperatures=None, - ace='ace', xsdir=None, output_dir=None, error=0.001, + ace=None, xsdir=None, output_dir=None, error=0.001, iwt=2, evaluation=None, evaluation_thermal=None, table_name=None, zaids=None, nmix=None, **kwargs): """Generate thermal scattering ACE file from ENDF files @@ -441,7 +441,7 @@ def make_ace_thermal(filename, filename_thermal, temperatures=None, Temperatures in Kelvin to produce data at. If omitted, data is produced at all temperatures given in the ENDF thermal scattering sublibrary. ace : str, optional - Path of ACE file to write + Path of ACE file to write. Default to ``"ace"``. xsdir : str, optional Path of xsdir file to write. Defaults to ``"xsdir"`` in the same directory as ``ace`` @@ -589,7 +589,7 @@ def make_ace_thermal(filename, filename_thermal, temperatures=None, commands += 'stop\n' run(commands, tapein, tapeout, **kwargs) - ace = output_dir / ace + ace = (output_dir / "ace") if ace is None else Path(ace) xsdir = (ace.parent / "xsdir") if xsdir is None else Path(xsdir) with ace.open('w') as ace_file, xsdir.open('w') as xsdir_file: # Concatenate ACE and xsdir files together From cf3f0201a0f8de3eb61d3193f6f6a1995a493dfc Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Sun, 12 Jan 2025 17:50:48 -0600 Subject: [PATCH 229/671] Update to a consistent definition of the r2 parameter for cones (#3254) Co-authored-by: Matthew Nyberg Co-authored-by: Paul Romano --- openmc/model/surface_composite.py | 45 +++++++++++--------- openmc/surface.py | 71 ++++++++++++++++--------------- 2 files changed, 60 insertions(+), 56 deletions(-) diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index c7655694d..88433118b 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -729,7 +729,7 @@ class OrthogonalBox(CompositeSurface): class XConeOneSided(CompositeSurface): - """One-sided cone parallel the x-axis + r"""One-sided cone parallel the x-axis A one-sided cone is composed of a normal cone surface and a "disambiguation" surface that eliminates the ambiguity as to which region of space is @@ -742,15 +742,16 @@ class XConeOneSided(CompositeSurface): Parameters ---------- x0 : float, optional - x-coordinate of the apex. Defaults to 0. + x-coordinate of the apex in [cm]. y0 : float, optional - y-coordinate of the apex. Defaults to 0. + y-coordinate of the apex in [cm]. z0 : float, optional - z-coordinate of the apex. Defaults to 0. + z-coordinate of the apex in [cm]. r2 : float, optional - Parameter related to the aperture [:math:`\\rm cm^2`]. - It can be interpreted as the increase in the radius squared per cm along - the cone's axis of revolution. + The square of the slope of the cone. It is defined as + :math:`\left(\frac{r}{h}\right)^2` for a radius, :math:`r` and an axial + distance :math:`h` from the apex. An easy way to define this quantity is + to take the square of the radius of the cone (in cm) 1 cm from the apex. up : bool Whether to select the side of the cone that extends to infinity in the positive direction of the coordinate axis (the positive half-space of @@ -783,7 +784,7 @@ class XConeOneSided(CompositeSurface): class YConeOneSided(CompositeSurface): - """One-sided cone parallel the y-axis + r"""One-sided cone parallel the y-axis A one-sided cone is composed of a normal cone surface and a "disambiguation" surface that eliminates the ambiguity as to which region of space is @@ -796,15 +797,16 @@ class YConeOneSided(CompositeSurface): Parameters ---------- x0 : float, optional - x-coordinate of the apex. Defaults to 0. + x-coordinate of the apex in [cm]. y0 : float, optional - y-coordinate of the apex. Defaults to 0. + y-coordinate of the apex in [cm]. z0 : float, optional - z-coordinate of the apex. Defaults to 0. + z-coordinate of the apex in [cm]. r2 : float, optional - Parameter related to the aperture [:math:`\\rm cm^2`]. - It can be interpreted as the increase in the radius squared per cm along - the cone's axis of revolution. + The square of the slope of the cone. It is defined as + :math:`\left(\frac{r}{h}\right)^2` for a radius, :math:`r` and an axial + distance :math:`h` from the apex. An easy way to define this quantity is + to take the square of the radius of the cone (in cm) 1 cm from the apex. up : bool Whether to select the side of the cone that extends to infinity in the positive direction of the coordinate axis (the positive half-space of @@ -836,7 +838,7 @@ class YConeOneSided(CompositeSurface): class ZConeOneSided(CompositeSurface): - """One-sided cone parallel the z-axis + r"""One-sided cone parallel the z-axis A one-sided cone is composed of a normal cone surface and a "disambiguation" surface that eliminates the ambiguity as to which region of space is @@ -849,15 +851,16 @@ class ZConeOneSided(CompositeSurface): Parameters ---------- x0 : float, optional - x-coordinate of the apex. Defaults to 0. + x-coordinate of the apex in [cm]. y0 : float, optional - y-coordinate of the apex. Defaults to 0. + y-coordinate of the apex in [cm]. z0 : float, optional - z-coordinate of the apex. Defaults to 0. + z-coordinate of the apex in [cm]. r2 : float, optional - Parameter related to the aperture [:math:`\\rm cm^2`]. - It can be interpreted as the increase in the radius squared per cm along - the cone's axis of revolution. + The square of the slope of the cone. It is defined as + :math:`\left(\frac{r}{h}\right)^2` for a radius, :math:`r` and an axial + distance :math:`h` from the apex. An easy way to define this quantity is + to take the square of the radius of the cone (in cm) 1 cm from the apex. up : bool Whether to select the side of the cone that extends to infinity in the positive direction of the coordinate axis (the positive half-space of diff --git a/openmc/surface.py b/openmc/surface.py index c95025f94..6919952ec 100644 --- a/openmc/surface.py +++ b/openmc/surface.py @@ -1753,7 +1753,7 @@ class Sphere(QuadricMixin, Surface): class Cone(QuadricMixin, Surface): - """A conical surface parallel to the x-, y-, or z-axis. + r"""A conical surface parallel to the x-, y-, or z-axis. .. Note:: This creates a double cone, which is two one-sided cones that meet at their apex. @@ -1763,24 +1763,22 @@ class Cone(QuadricMixin, Surface): Parameters ---------- x0 : float, optional - x-coordinate of the apex in [cm]. Defaults to 0. + x-coordinate of the apex in [cm]. y0 : float, optional - y-coordinate of the apex in [cm]. Defaults to 0. + y-coordinate of the apex in [cm]. z0 : float, optional - z-coordinate of the apex in [cm]. Defaults to 0. + z-coordinate of the apex in [cm]. r2 : float, optional - Parameter related to the aperture [:math:`\\rm cm^2`]. - It can be interpreted as the increase in the radius squared per cm along - the cone's axis of revolution. + The square of the slope of the cone. It is defined as + :math:`\left(\frac{r}{h}\right)^2` for a radius, :math:`r` and an axial + distance :math:`h` from the apex. An easy way to define this quantity is + to take the square of the radius of the cone (in cm) 1 cm from the apex. dx : float, optional x-component of the vector representing the axis of the cone. - Defaults to 0. dy : float, optional y-component of the vector representing the axis of the cone. - Defaults to 0. dz : float, optional z-component of the vector representing the axis of the cone. - Defaults to 1. surface_id : int, optional Unique identifier for the surface. If not specified, an identifier will automatically be assigned. @@ -1805,7 +1803,7 @@ class Cone(QuadricMixin, Surface): z0 : float z-coordinate of the apex in [cm] r2 : float - Parameter related to the aperature [cm^2] + Parameter related to the aperture dx : float x-component of the vector representing the axis of the cone. dy : float @@ -1911,7 +1909,7 @@ class Cone(QuadricMixin, Surface): class XCone(QuadricMixin, Surface): - """A cone parallel to the x-axis of the form :math:`(y - y_0)^2 + (z - z_0)^2 = + r"""A cone parallel to the x-axis of the form :math:`(y - y_0)^2 + (z - z_0)^2 = r^2 (x - x_0)^2`. .. Note:: @@ -1921,15 +1919,16 @@ class XCone(QuadricMixin, Surface): Parameters ---------- x0 : float, optional - x-coordinate of the apex in [cm]. Defaults to 0. + x-coordinate of the apex in [cm]. y0 : float, optional - y-coordinate of the apex in [cm]. Defaults to 0. + y-coordinate of the apex in [cm]. z0 : float, optional - z-coordinate of the apex in [cm]. Defaults to 0. + z-coordinate of the apex in [cm]. r2 : float, optional - Parameter related to the aperture [:math:`\\rm cm^2`]. - It can be interpreted as the increase in the radius squared per cm along - the cone's axis of revolution. + The square of the slope of the cone. It is defined as + :math:`\left(\frac{r}{h}\right)^2` for a radius, :math:`r` and an axial + distance :math:`h` from the apex. An easy way to define this quantity is + to take the square of the radius of the cone (in cm) 1 cm from the apex. boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles @@ -1953,7 +1952,7 @@ class XCone(QuadricMixin, Surface): z0 : float z-coordinate of the apex in [cm] r2 : float - Parameter related to the aperature + Parameter related to the aperture boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. @@ -2012,7 +2011,7 @@ class XCone(QuadricMixin, Surface): class YCone(QuadricMixin, Surface): - """A cone parallel to the y-axis of the form :math:`(x - x_0)^2 + (z - z_0)^2 = + r"""A cone parallel to the y-axis of the form :math:`(x - x_0)^2 + (z - z_0)^2 = r^2 (y - y_0)^2`. .. Note:: @@ -2022,15 +2021,16 @@ class YCone(QuadricMixin, Surface): Parameters ---------- x0 : float, optional - x-coordinate of the apex in [cm]. Defaults to 0. + x-coordinate of the apex in [cm]. y0 : float, optional - y-coordinate of the apex in [cm]. Defaults to 0. + y-coordinate of the apex in [cm]. z0 : float, optional - z-coordinate of the apex in [cm]. Defaults to 0. + z-coordinate of the apex in [cm]. r2 : float, optional - Parameter related to the aperture [:math:`\\rm cm^2`]. - It can be interpreted as the increase in the radius squared per cm along - the cone's axis of revolution. + The square of the slope of the cone. It is defined as + :math:`\left(\frac{r}{h}\right)^2` for a radius, :math:`r` and an axial + distance :math:`h` from the apex. An easy way to define this quantity is + to take the square of the radius of the cone (in cm) 1 cm from the apex. boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles @@ -2054,7 +2054,7 @@ class YCone(QuadricMixin, Surface): z0 : float z-coordinate of the apex in [cm] r2 : float - Parameter related to the aperature + Parameter related to the aperture boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. @@ -2113,7 +2113,7 @@ class YCone(QuadricMixin, Surface): class ZCone(QuadricMixin, Surface): - """A cone parallel to the z-axis of the form :math:`(x - x_0)^2 + (y - y_0)^2 = + r"""A cone parallel to the z-axis of the form :math:`(x - x_0)^2 + (y - y_0)^2 = r^2 (z - z_0)^2`. .. Note:: @@ -2123,15 +2123,16 @@ class ZCone(QuadricMixin, Surface): Parameters ---------- x0 : float, optional - x-coordinate of the apex in [cm]. Defaults to 0. + x-coordinate of the apex in [cm]. y0 : float, optional - y-coordinate of the apex in [cm]. Defaults to 0. + y-coordinate of the apex in [cm]. z0 : float, optional - z-coordinate of the apex in [cm]. Defaults to 0. + z-coordinate of the apex in [cm]. r2 : float, optional - Parameter related to the aperature [cm^2]. - This is the square of the radius of the cone 1 cm from. - This can also be treated as the square of the slope of the cone relative to its axis. + The square of the slope of the cone. It is defined as + :math:`\left(\frac{r}{h}\right)^2` for a radius, :math:`r` and an axial + distance :math:`h` from the apex. An easy way to define this quantity is + to take the square of the radius of the cone (in cm) 1 cm from the apex. boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles @@ -2155,7 +2156,7 @@ class ZCone(QuadricMixin, Surface): z0 : float z-coordinate of the apex in [cm] r2 : float - Parameter related to the aperature + Parameter related to the aperture. boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. From d39a4140114704f064cb994467049e8b1e9bf203 Mon Sep 17 00:00:00 2001 From: azimG Date: Mon, 13 Jan 2025 07:19:07 +0100 Subject: [PATCH 230/671] Set Model attributes only if needed (#3209) Co-authored-by: Paul Romano --- openmc/model/model.py | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index 402d6c061..1c261061a 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -64,22 +64,11 @@ class Model: def __init__(self, geometry=None, materials=None, settings=None, tallies=None, plots=None): - self.geometry = openmc.Geometry() - self.materials = openmc.Materials() - self.settings = openmc.Settings() - self.tallies = openmc.Tallies() - self.plots = openmc.Plots() - - if geometry is not None: - self.geometry = geometry - if materials is not None: - self.materials = materials - if settings is not None: - self.settings = settings - if tallies is not None: - self.tallies = tallies - if plots is not None: - self.plots = plots + self.geometry = openmc.Geometry() if geometry is None else geometry + self.materials = openmc.Materials() if materials is None else materials + self.settings = openmc.Settings() if settings is None else settings + self.tallies = openmc.Tallies() if tallies is None else tallies + self.plots = openmc.Plots() if plots is None else plots @property def geometry(self) -> openmc.Geometry | None: From 549cc0973c9002da86c91564b2951a1c3f5c2d4b Mon Sep 17 00:00:00 2001 From: Adam Nelson <1037107+nelsonag@users.noreply.github.com> Date: Tue, 14 Jan 2025 09:51:47 -0600 Subject: [PATCH 231/671] Enable the LegendreFilter filter to be used in photon tallies for orders greater than P0. (#3245) Co-authored-by: Paul Romano --- src/physics.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/physics.cpp b/src/physics.cpp index 69e74f2ea..34dc4ce1c 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -293,8 +293,8 @@ void sample_photon_reaction(Particle& p) // Coherent (Rayleigh) scattering prob += micro.coherent; if (prob > cutoff) { - double mu = element.rayleigh_scatter(alpha, p.current_seed()); - p.u() = rotate_angle(p.u(), mu, nullptr, p.current_seed()); + p.mu() = element.rayleigh_scatter(alpha, p.current_seed()); + p.u() = rotate_angle(p.u(), p.mu(), nullptr, p.current_seed()); p.event() = TallyEvent::SCATTER; p.event_mt() = COHERENT; return; @@ -303,10 +303,10 @@ void sample_photon_reaction(Particle& p) // Incoherent (Compton) scattering prob += micro.incoherent; if (prob > cutoff) { - double alpha_out, mu; + double alpha_out; int i_shell; element.compton_scatter( - alpha, true, &alpha_out, &mu, &i_shell, p.current_seed()); + alpha, true, &alpha_out, &p.mu(), &i_shell, p.current_seed()); // Determine binding energy of shell. The binding energy is 0.0 if // doppler broadening is not used. @@ -322,9 +322,9 @@ void sample_photon_reaction(Particle& p) double E_electron = (alpha - alpha_out) * MASS_ELECTRON_EV - e_b; int electron = static_cast(ParticleType::electron); if (E_electron >= settings::energy_cutoff[electron]) { - double mu_electron = (alpha - alpha_out * mu) / + double mu_electron = (alpha - alpha_out * p.mu()) / std::sqrt(alpha * alpha + alpha_out * alpha_out - - 2.0 * alpha * alpha_out * mu); + 2.0 * alpha * alpha_out * p.mu()); Direction u = rotate_angle(p.u(), mu_electron, &phi, p.current_seed()); p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron); } @@ -338,7 +338,7 @@ void sample_photon_reaction(Particle& p) phi += PI; p.E() = alpha_out * MASS_ELECTRON_EV; - p.u() = rotate_angle(p.u(), mu, &phi, p.current_seed()); + p.u() = rotate_angle(p.u(), p.mu(), &phi, p.current_seed()); p.event() = TallyEvent::SCATTER; p.event_mt() = INCOHERENT; return; From 6d731934f4e0b62c3be6094c283a19730d8ce08f Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 15 Jan 2025 06:35:37 -0600 Subject: [PATCH 232/671] Fix bug in WeightWindowGenerator for empty energy bounds (#3263) --- openmc/weight_windows.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openmc/weight_windows.py b/openmc/weight_windows.py index 5df9f71dc..1ddbfe10f 100644 --- a/openmc/weight_windows.py +++ b/openmc/weight_windows.py @@ -897,7 +897,10 @@ class WeightWindowGenerator: mesh_id = int(get_text(elem, 'mesh')) mesh = meshes[mesh_id] - energy_bounds = [float(x) for x in get_text(elem, 'energy_bounds').split()] + if (energy_bounds := get_text(elem, 'energy_bounds')) is not None: + energy_bounds = [float(x) for x in energy_bounds.split()] + else: + energy_bounds = None particle_type = get_text(elem, 'particle_type') wwg = cls(mesh, energy_bounds, particle_type) From 32662b409a7780a9cf75732acf4e049a6ed6954b Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Thu, 16 Jan 2025 09:36:13 -0600 Subject: [PATCH 233/671] Add constant for invalid surface tokens. (#3260) Co-authored-by: Paul Romano --- include/openmc/cell.h | 1 - include/openmc/constants.h | 4 ++++ include/openmc/particle_data.h | 22 +++++++++++++++++----- src/boundary_condition.cpp | 6 ++---- src/dagmc.cpp | 2 +- src/geometry.cpp | 8 ++++---- src/output.cpp | 4 ++-- src/particle.cpp | 12 ++++++------ src/plot.cpp | 6 +++--- src/tallies/filter_musurface.cpp | 2 +- src/tallies/filter_surface.cpp | 2 +- 11 files changed, 41 insertions(+), 28 deletions(-) diff --git a/include/openmc/cell.h b/include/openmc/cell.h index 032475ce9..d01020f8e 100644 --- a/include/openmc/cell.h +++ b/include/openmc/cell.h @@ -29,7 +29,6 @@ namespace openmc { enum class Fill { MATERIAL, UNIVERSE, LATTICE }; -// TODO: Convert to enum constexpr int32_t OP_LEFT_PAREN {std::numeric_limits::max()}; constexpr int32_t OP_RIGHT_PAREN {std::numeric_limits::max() - 1}; constexpr int32_t OP_COMPLEMENT {std::numeric_limits::max() - 2}; diff --git a/include/openmc/constants.h b/include/openmc/constants.h index 605ae1839..a83a4a07d 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -350,6 +350,10 @@ enum class RandomRaySourceShape { FLAT, LINEAR, LINEAR_XY }; enum class GeometryType { CSG, DAG }; +// a surface token cannot be zero due to the unsigned nature of zero for integer +// representations. This value represents no surface. +constexpr int32_t SURFACE_NONE {0}; + } // namespace openmc #endif // OPENMC_CONSTANTS_H diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index 164148cce..66137c5f6 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -185,10 +185,14 @@ struct CacheDataMG { struct BoundaryInfo { double distance {INFINITY}; //!< distance to nearest boundary - int surface_index {0}; //!< if boundary is surface, index in surfaces vector - int coord_level; //!< coordinate level after crossing boundary + int surface { + SURFACE_NONE}; //!< surface token, non-zero if boundary is surface + int coord_level; //!< coordinate level after crossing boundary array lattice_translation {}; //!< which way lattice indices will change + + // TODO: off-by-one + int surface_index() const { return std::abs(surface) - 1; } }; /* @@ -226,7 +230,7 @@ public: void init_from_r_u(Position r_a, Direction u_a) { clear(); - surface() = 0; + surface() = SURFACE_NONE; material() = C_NONE; r() = r_a; u() = u_a; @@ -296,10 +300,17 @@ public: Direction& u_local() { return coord_[n_coord_ - 1].u; } const Direction& u_local() const { return coord_[n_coord_ - 1].u; } - // Surface that the particle is on + // Surface token for the surface that the particle is currently on int& surface() { return surface_; } const int& surface() const { return surface_; } + // Surface index based on the current value of the surface_ attribute + int surface_index() const + { + // TODO: off-by-one + return std::abs(surface_) - 1; + } + // Boundary information BoundaryInfo& boundary() { return boundary_; } @@ -337,7 +348,8 @@ private: Position r_last_; //!< previous coordinates Direction u_last_; //!< previous direction coordinates - int surface_ {0}; //!< index for surface particle is on + int surface_ { + SURFACE_NONE}; //!< surface token for surface the particle is currently on BoundaryInfo boundary_; //!< Info about the next intersection diff --git a/src/boundary_condition.cpp b/src/boundary_condition.cpp index b58054dce..7216ac896 100644 --- a/src/boundary_condition.cpp +++ b/src/boundary_condition.cpp @@ -129,8 +129,7 @@ TranslationalPeriodicBC::TranslationalPeriodicBC(int i_surf, int j_surf) void TranslationalPeriodicBC::handle_particle( Particle& p, const Surface& surf) const { - // TODO: off-by-one on surface indices throughout this function. - int i_particle_surf = std::abs(p.surface()) - 1; + int i_particle_surf = p.surface_index(); // Figure out which of the two BC surfaces were struck then find the // particle's new location and surface. @@ -255,8 +254,7 @@ RotationalPeriodicBC::RotationalPeriodicBC(int i_surf, int j_surf) void RotationalPeriodicBC::handle_particle( Particle& p, const Surface& surf) const { - // TODO: off-by-one on surface indices throughout this function. - int i_particle_surf = std::abs(p.surface()) - 1; + int i_particle_surf = p.surface_index(); // Figure out which of the two BC surfaces were struck to figure out if a // forward or backward rotation is required. Specify the other surface as diff --git a/src/dagmc.cpp b/src/dagmc.cpp index 134e31ecf..94c220850 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -847,7 +847,7 @@ void check_dagmc_root_univ() int32_t next_cell(int32_t surf, int32_t curr_cell, int32_t univ) { - auto surfp = dynamic_cast(model::surfaces[surf - 1].get()); + auto surfp = dynamic_cast(model::surfaces[surf].get()); auto cellp = dynamic_cast(model::cells[curr_cell].get()); auto univp = static_cast(model::universes[univ].get()); diff --git a/src/geometry.cpp b/src/geometry.cpp index 445b19faa..9e975c00d 100644 --- a/src/geometry.cpp +++ b/src/geometry.cpp @@ -421,15 +421,15 @@ BoundaryInfo distance_to_boundary(GeometryState& p) // have to explicitly check which half-space the particle would be // traveling into if the surface is crossed if (c.is_simple() || d == INFTY) { - info.surface_index = level_surf_cross; + info.surface = level_surf_cross; } else { Position r_hit = r + d_surf * u; Surface& surf {*model::surfaces[std::abs(level_surf_cross) - 1]}; Direction norm = surf.normal(r_hit); if (u.dot(norm) > 0) { - info.surface_index = std::abs(level_surf_cross); + info.surface = std::abs(level_surf_cross); } else { - info.surface_index = -std::abs(level_surf_cross); + info.surface = -std::abs(level_surf_cross); } } @@ -441,7 +441,7 @@ BoundaryInfo distance_to_boundary(GeometryState& p) } else { if (d == INFINITY || (d - d_lat) / d >= FP_REL_PRECISION) { d = d_lat; - info.surface_index = 0; + info.surface = SURFACE_NONE; info.lattice_translation = level_lat_trans; info.coord_level = i + 1; } diff --git a/src/output.cpp b/src/output.cpp index a430fe9a6..8494450c4 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -200,9 +200,9 @@ void print_particle(Particle& p) } // Display miscellaneous info. - if (p.surface() != 0) { + if (p.surface() != SURFACE_NONE) { // Surfaces identifiers are >= 1, but indices are >= 0 so we need -1 - const Surface& surf {*model::surfaces[std::abs(p.surface()) - 1]}; + const Surface& surf {*model::surfaces[p.surface_index()]}; fmt::print(" Surface = {}\n", (p.surface() > 0) ? surf.id_ : -surf.id_); } fmt::print(" Weight = {}\n", p.wgt()); diff --git a/src/particle.cpp b/src/particle.cpp index 0ea865014..65e8065fd 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -108,7 +108,7 @@ void Particle::from_source(const SourceSite* src) { // Reset some attributes clear(); - surface() = 0; + surface() = SURFACE_NONE; cell_born() = C_NONE; material() = C_NONE; n_collision() = 0; @@ -277,7 +277,7 @@ void Particle::event_cross_surface() n_coord_last() = n_coord(); // Set surface that particle is on and adjust coordinate levels - surface() = boundary().surface_index; + surface() = boundary().surface; n_coord() = boundary().coord_level; if (boundary().lattice_translation[0] != 0 || @@ -291,7 +291,7 @@ void Particle::event_cross_surface() } else { // Particle crosses surface // TODO: off-by-one - const auto& surf {model::surfaces[std::abs(surface()) - 1].get()}; + 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); @@ -328,7 +328,7 @@ void Particle::event_collide() score_surface_tally(*this, model::active_meshsurf_tallies); // Clear surface component - surface() = 0; + surface() = SURFACE_NONE; if (settings::run_CE) { collision(*this); @@ -549,7 +549,7 @@ void Particle::cross_surface(const Surface& surf) #ifdef DAGMC // in DAGMC, we know what the next cell should be if (surf.geom_type() == GeometryType::DAG) { - int32_t i_cell = next_cell(std::abs(surface()), cell_last(n_coord() - 1), + int32_t i_cell = next_cell(surface_index(), cell_last(n_coord() - 1), lowest_coord().universe) - 1; // save material and temp @@ -587,7 +587,7 @@ void Particle::cross_surface(const Surface& surf) // the particle is really traveling tangent to a surface, if we move it // forward a tiny bit it should fix the problem. - surface() = 0; + surface() = SURFACE_NONE; n_coord() = 1; r() += TINY_BIT * u(); diff --git a/src/plot.cpp b/src/plot.cpp index 85131d67a..b36ed6f5d 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -1301,7 +1301,7 @@ void ProjectionPlot::create_output() const while (intersection_found) { bool inside_cell = false; - int32_t i_surface = std::abs(p.surface()) - 1; + int32_t i_surface = p.surface_index(); if (i_surface > 0 && model::surfaces[i_surface]->geom_type() == GeometryType::DAG) { #ifdef DAGMC @@ -1334,13 +1334,13 @@ void ProjectionPlot::create_output() const this_line_segments[tid][horiz].emplace_back( color_by_ == PlotColorBy::mats ? p.material() : p.lowest_coord().cell, - dist.distance, std::abs(dist.surface_index)); + dist.distance, std::abs(dist.surface)); // Advance particle for (int lev = 0; lev < p.n_coord(); ++lev) { p.coord(lev).r += dist.distance * p.coord(lev).u; } - p.surface() = dist.surface_index; + p.surface() = dist.surface; p.n_coord_last() = p.n_coord(); p.n_coord() = dist.coord_level; if (dist.lattice_translation[0] != 0 || diff --git a/src/tallies/filter_musurface.cpp b/src/tallies/filter_musurface.cpp index 58fe39b91..340149d4c 100644 --- a/src/tallies/filter_musurface.cpp +++ b/src/tallies/filter_musurface.cpp @@ -12,7 +12,7 @@ void MuSurfaceFilter::get_all_bins( const Particle& p, TallyEstimator estimator, FilterMatch& match) const { // Get surface normal (and make sure it is a unit vector) - const auto surf {model::surfaces[std::abs(p.surface()) - 1].get()}; + const auto surf {model::surfaces[p.surface_index()].get()}; auto n = surf->normal(p.r()); n /= n.norm(); diff --git a/src/tallies/filter_surface.cpp b/src/tallies/filter_surface.cpp index a84d17728..1fbf8d44e 100644 --- a/src/tallies/filter_surface.cpp +++ b/src/tallies/filter_surface.cpp @@ -47,7 +47,7 @@ void SurfaceFilter::set_surfaces(gsl::span surfaces) void SurfaceFilter::get_all_bins( const Particle& p, TallyEstimator estimator, FilterMatch& match) const { - auto search = map_.find(std::abs(p.surface()) - 1); + auto search = map_.find(p.surface_index()); if (search != map_.end()) { match.bins_.push_back(search->second); if (p.surface() < 0) { From bd874f1b3ebf0e75851da750dd179593a0f7e7f0 Mon Sep 17 00:00:00 2001 From: Joshua Einstein-Curtis <33426885+jeinstei@users.noreply.github.com> Date: Thu, 16 Jan 2025 09:38:19 -0600 Subject: [PATCH 234/671] Update plots.py for PathLike to string handling error (#3261) Co-authored-by: Paul Romano --- openmc/plots.py | 3 ++- tests/unit_tests/test_plots.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openmc/plots.py b/openmc/plots.py index 7532d9d5c..2bb3ef65f 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -250,9 +250,10 @@ def voxel_to_vtk(voxel_file: PathLike, output: PathLike = 'plot.vti'): writer.SetInputData(grid) else: writer.SetInput(grid) + output = str(output) if not output.endswith(".vti"): output += ".vti" - writer.SetFileName(str(output)) + writer.SetFileName(output) writer.Write() return output diff --git a/tests/unit_tests/test_plots.py b/tests/unit_tests/test_plots.py index a1e15016a..4efc12c93 100644 --- a/tests/unit_tests/test_plots.py +++ b/tests/unit_tests/test_plots.py @@ -90,7 +90,7 @@ def test_voxel_plot(run_in_tmpdir): assert Path('test_voxel_plot.vti').is_file() vox_plot.filename = 'h5_voxel_plot' - vox_plot.to_vtk('another_test_voxel_plot.vti') + vox_plot.to_vtk(Path('another_test_voxel_plot.vti')) assert Path('h5_voxel_plot.h5').is_file() assert Path('another_test_voxel_plot.vti').is_file() From 3bf1486f4980dd520810ad33e0aeea5d14dd6a87 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 20 Jan 2025 07:18:50 -0600 Subject: [PATCH 235/671] Fix bug in Surface.normalize (#3270) --- openmc/surface.py | 2 +- tests/unit_tests/test_surface.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/openmc/surface.py b/openmc/surface.py index 6919952ec..840c3125d 100644 --- a/openmc/surface.py +++ b/openmc/surface.py @@ -309,7 +309,7 @@ class Surface(IDManagerMixin, ABC): coeffs = self._get_base_coeffs() coeffs = np.asarray(coeffs) nonzeros = ~np.isclose(coeffs, 0., rtol=0., atol=self._atol) - norm_factor = np.abs(coeffs[nonzeros][0]) + norm_factor = coeffs[nonzeros][0] return tuple([c/norm_factor for c in coeffs]) def is_equal(self, other): diff --git a/tests/unit_tests/test_surface.py b/tests/unit_tests/test_surface.py index 12cd8c9d2..e9560223d 100644 --- a/tests/unit_tests/test_surface.py +++ b/tests/unit_tests/test_surface.py @@ -753,3 +753,16 @@ def test_ztorus(): assert isinstance(sr, openmc.YTorus) sr = s.rotate((0., 90., 0.)) assert isinstance(sr, openmc.XTorus) + + +def test_normalize(): + """Test that equivalent planes give same normalized coefficients""" + p1 = openmc.Plane(a=0.0, b=1.0, c=0.0, d=1.0) + p2 = openmc.Plane(a=0.0, b=2.0, c=0.0, d=2.0) + assert p1.normalize() == p2.normalize() + + p2 = openmc.Plane(a=0.0, b=-1.0, c=0.0, d=-1.0) + assert p1.normalize() == p2.normalize() + + p2 = openmc.YPlane(1.0) + assert p1.normalize() == p2.normalize() From 9170bf34ad4b0bdc93c433a8b77263c52515b663 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 20 Jan 2025 07:51:13 -0600 Subject: [PATCH 236/671] Update recognized thermal scattering materials for ENDF/B-VIII.1 (#3267) --- openmc/data/njoy.py | 67 +++++++++++++++++++++++++++++++ openmc/data/thermal.py | 89 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 145 insertions(+), 11 deletions(-) diff --git a/openmc/data/njoy.py b/openmc/data/njoy.py index f5ef836f9..e025cff0d 100644 --- a/openmc/data/njoy.py +++ b/openmc/data/njoy.py @@ -18,21 +18,45 @@ _THERMAL_DATA = { 'c_Al27': ThermalTuple('al27', [13027], 1), 'c_Al_in_Al2O3': ThermalTuple('asap00', [13027], 1), 'c_Be': ThermalTuple('be', [4009], 1), + 'c_Be_distinct': ThermalTuple('besd', [4009], 1), 'c_Be_in_BeO': ThermalTuple('bebeo', [4009], 1), 'c_Be_in_Be2C': ThermalTuple('bebe2c', [4009], 1), + 'c_Be_in_BeF2': ThermalTuple('bebef2', [4009], 1), 'c_Be_in_FLiBe': ThermalTuple('beflib', [4009], 1), 'c_C6H6': ThermalTuple('benz', [1001, 6000, 6012], 2), + 'c_C_in_Be2C': ThermalTuple('cbe2c', [6000, 6012, 6013], 1), + 'c_C_in_C5O2H8': ThermalTuple('clucit', [6000, 6012, 6013], 1), + 'c_C_in_C8H8': ThermalTuple('cc8h8', [6000, 6012, 6013], 1), + 'c_C_in_CF2': ThermalTuple('ccf2', [6000, 6012, 6013], 1), 'c_C_in_SiC': ThermalTuple('csic', [6000, 6012, 6013], 1), + 'c_C_in_UC_100p': ThermalTuple('cuc100', [6000, 6012, 6013], 1), + 'c_C_in_UC_10p': ThermalTuple('cuc10', [6000, 6012, 6013], 1), + 'c_C_in_UC_5p': ThermalTuple('cuc5', [6000, 6012, 6013], 1), + 'c_C_in_UC': ThermalTuple('cinuc', [6000, 6012, 6013], 1), + 'c_C_in_UC_HALEU': ThermalTuple('cuchal', [6000, 6012, 6013], 1), + 'c_C_in_UC_HEU': ThermalTuple('cucheu', [6000, 6012, 6013], 1), + 'c_C_in_ZrC': ThermalTuple('czrc', [6000, 6012, 6013], 1), 'c_Ca_in_CaH2': ThermalTuple('cacah2', [20040, 20042, 20043, 20044, 20046, 20048], 1), + 'c_D_in_7LiD': ThermalTuple('dlid', [1002], 1), 'c_D_in_D2O': ThermalTuple('dd2o', [1002], 1), 'c_D_in_D2O_solid': ThermalTuple('dice', [1002], 1), + 'c_F_in_Be2': ThermalTuple('fbef2', [9019], 1), + 'c_F_in_CF2': ThermalTuple('fcf2', [9019], 1), 'c_F_in_FLiBe': ThermalTuple('fflibe', [9019], 1), + 'c_F_in_HF': ThermalTuple('f_hf', [9019], 1), + 'c_F_in_MgF2': ThermalTuple('fmgf2', [9019], 1), 'c_Fe56': ThermalTuple('fe56', [26056], 1), 'c_Graphite': ThermalTuple('graph', [6000, 6012, 6013], 1), 'c_Graphite_10p': ThermalTuple('grph10', [6000, 6012, 6013], 1), + 'c_Graphite_20p': ThermalTuple('grph20', [6000, 6012, 6013], 1), 'c_Graphite_30p': ThermalTuple('grph30', [6000, 6012, 6013], 1), + 'c_Graphite_distinct': ThermalTuple('grphsd', [6000, 6012, 6013], 1), + 'c_H_in_7LiH': ThermalTuple('hlih', [1001], 1), 'c_H_in_C5O2H8': ThermalTuple('lucite', [1001], 1), + 'c_H_in_C8H8': ThermalTuple('hc8h8', [1001], 1), 'c_H_in_CaH2': ThermalTuple('hcah2', [1001], 1), + 'c_H1_in_CaH2': ThermalTuple('h1cah2', [1001], 1), + 'c_H2_in_CaH2': ThermalTuple('h2cah2', [1001], 1), 'c_H_in_CH2': ThermalTuple('hch2', [1001], 1), 'c_H_in_CH4_liquid': ThermalTuple('lch4', [1001], 1), 'c_H_in_CH4_solid': ThermalTuple('sch4', [1001], 1), @@ -49,24 +73,67 @@ _THERMAL_DATA = { 'c_H_in_ZrH2': ThermalTuple('hzrh2', [1001], 1), 'c_H_in_ZrHx': ThermalTuple('hzrhx', [1001], 1), 'c_Li_in_FLiBe': ThermalTuple('liflib', [3006, 3007], 1), + 'c_Li_in_7LiD': ThermalTuple('lilid', [3007], 1), + 'c_Li_in_7LiH': ThermalTuple('lilih', [3007], 1), 'c_Mg24': ThermalTuple('mg24', [12024], 1), + 'c_Mg_in_MgF2': ThermalTuple('mgmgf2', [12024, 12025, 12026], 1), + 'c_Mg_in_MgO': ThermalTuple('mgmgo', [12024, 12025, 12026], 1), + 'c_N_in_UN_100p': ThermalTuple('nun100', [7014, 7015], 1), + 'c_N_in_UN_10p': ThermalTuple('nun10', [7014, 7015], 1), + 'c_N_in_UN_5p': ThermalTuple('nun5', [7014, 7015], 1), 'c_N_in_UN': ThermalTuple('n-un', [7014, 7015], 1), + 'c_N_in_UN_HALEU': ThermalTuple('nunhal', [7014, 7015], 1), + 'c_N_in_UN_HEU': ThermalTuple('nunheu', [7014, 7015], 1), 'c_O_in_Al2O3': ThermalTuple('osap00', [8016, 8017, 8018], 1), 'c_O_in_BeO': ThermalTuple('obeo', [8016, 8017, 8018], 1), + 'c_O_in_C5O2H8': ThermalTuple('olucit', [8016, 8017, 8018], 1), 'c_O_in_D2O': ThermalTuple('od2o', [8016, 8017, 8018], 1), 'c_O_in_H2O_solid': ThermalTuple('oice', [8016, 8017, 8018], 1), + 'c_O_in_MgO': ThermalTuple('omgo', [8016, 8017, 8018], 1), + 'c_O_in_PuO2': ThermalTuple('opuo2', [8016, 8017, 8018], 1), + 'c_O_in_SiO2_alpha': ThermalTuple('osio2a', [8016, 8017, 8018], 1), + 'c_O_in_UO2_100p': ThermalTuple('ouo200', [8016, 8017, 8018], 1), + 'c_O_in_UO2_10p': ThermalTuple('ouo210', [8016, 8017, 8018], 1), + 'c_O_in_UO2_5p': ThermalTuple('ouo25', [8016, 8017, 8018], 1), 'c_O_in_UO2': ThermalTuple('ouo2', [8016, 8017, 8018], 1), + 'c_O_in_UO2_HALEU': ThermalTuple('ouo2hl', [8016, 8017, 8018], 1), + 'c_O_in_UO2_HEU': ThermalTuple('ouo2he', [8016, 8017, 8018], 1), 'c_ortho_D': ThermalTuple('orthod', [1002], 1), 'c_ortho_H': ThermalTuple('orthoh', [1001], 1), 'c_para_D': ThermalTuple('parad', [1002], 1), 'c_para_H': ThermalTuple('parah', [1001], 1), + 'c_Pu_in_PuO2': ThermalTuple('puo2', [94239, 94240, 94241, 94242, 94243], 1), 'c_Si28': ThermalTuple('si00', [14028], 1), 'c_Si_in_SiC': ThermalTuple('sisic', [14028, 14029, 14030], 1), + 'c_Si_in_SiO2_alpha': ThermalTuple('si_o2a', [14028, 14029, 14030], 1), 'c_SiO2_alpha': ThermalTuple('sio2-a', [8016, 8017, 8018, 14028, 14029, 14030], 3), 'c_SiO2_beta': ThermalTuple('sio2-b', [8016, 8017, 8018, 14028, 14029, 14030], 3), + 'c_U_metal_100p': ThermalTuple('u-100p', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_metal_10p': ThermalTuple('u-10p', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_metal_5p': ThermalTuple('u-5p', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_metal': ThermalTuple('umetal', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_metal_HALEU': ThermalTuple('uhaleu', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_metal_HEU': ThermalTuple('u-heu', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UC_100p': ThermalTuple('uc-100', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UC_10p': ThermalTuple('uc-10', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UC_5p': ThermalTuple('uc-5', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UC': ThermalTuple('uc-nat', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UC_HALEU': ThermalTuple('uc-hal', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UC_HEU': ThermalTuple('uc-heu', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UN_100p': ThermalTuple('un-100', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UN_10p': ThermalTuple('un-10', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UN_5p': ThermalTuple('un-5', [92233, 92234, 92235, 92236, 92238], 1), 'c_U_in_UN': ThermalTuple('u-un', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UN_HALEU': ThermalTuple('un-hal', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UN_HEU': ThermalTuple('un-heu', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UO2_100p': ThermalTuple('uo2100', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UO2_10p': ThermalTuple('uo2-10', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UO2_5p': ThermalTuple('uo2-5', [92233, 92234, 92235, 92236, 92238], 1), 'c_U_in_UO2': ThermalTuple('uuo2', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UO2_HALEU': ThermalTuple('uo2hal', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UO2_HEU': ThermalTuple('uo2heu', [92233, 92234, 92235, 92236, 92238], 1), 'c_Y_in_YH2': ThermalTuple('yyh2', [39089], 1), + 'c_Zr_in_ZrC': ThermalTuple('zrzrc', [40000, 40090, 40091, 40092, 40094, 40096], 1), 'c_Zr_in_ZrH': ThermalTuple('zrzrh', [40000, 40090, 40091, 40092, 40094, 40096], 1), 'c_Zr_in_ZrH2': ThermalTuple('zrzrh2', [40000, 40090, 40091, 40092, 40094, 40096], 1), 'c_Zr_in_ZrHx': ThermalTuple('zrzrhx', [40000, 40090, 40091, 40092, 40094, 40096], 1), diff --git a/openmc/data/thermal.py b/openmc/data/thermal.py index 6df171d3a..df4dc1503 100644 --- a/openmc/data/thermal.py +++ b/openmc/data/thermal.py @@ -32,30 +32,54 @@ _THERMAL_NAMES = { 'c_Al_in_Al2O3': ('asap00', 'asap', 'al(al2o3)'), 'c_Be': ('be', 'be-metal', 'be-met', 'be00', 'be-metal', 'be metal', '4-be'), 'c_BeO': ('beo',), - 'c_Be_in_BeO': ('bebeo', 'be-beo', 'be-o', 'be/o', 'bbeo00', 'be(beo)'), - 'c_Be_in_Be2C': ('bebe2c',), + 'c_Be_distinct': ('besd', 'be+sd'), + 'c_Be_in_BeO': ('bebeo', 'be-beo', 'be-o', 'be/o', 'bbeo00', 'be(beo)', 'be_beo'), + 'c_Be_in_Be2C': ('bebe2c', 'be(be2c)'), + 'c_Be_in_BeF2': ('bebef2', 'be in bef2'), 'c_Be_in_FLiBe': ('beflib', 'be(flibe)'), 'c_C6H6': ('benz', 'c6h6', 'benzine'), - 'c_C_in_SiC': ('csic', 'c-sic', 'c(3c-sic)'), - 'c_Ca_in_CaH2': ('cah', 'cah00', 'cacah2', 'ca(cah2)'), + 'c_C_in_Be2C': ('cbe2c', 'c(be2c)'), + 'c_C_in_C5O2H8': ('clucit', 'c(lucite)'), + 'c_C_in_C8H8': ('cc8h8', 'c(polystyr'), + 'c_C_in_CF2': ('ccf2', 'c(teflon)'), + 'c_C_in_SiC': ('csic', 'c-sic', 'c(3c-sic)', 'c_sic'), + 'c_C_in_UC_100p': ('cuc100', 'cinuc_100p'), + 'c_C_in_UC_10p': ('cuc10', 'cinuc_10p'), + 'c_C_in_UC_5p': ('cuc5', 'cinuc_5p'), + 'c_C_in_UC': ('cinuc', 'cinuc_nat'), + 'c_C_in_UC_HALEU': ('cuchal', 'cinuc_haleu'), + 'c_C_in_UC_HEU': ('cucheu', 'cinuc_heu'), + 'c_C_in_ZrC': ('czrc', 'c(zrc)'), + 'c_Ca_in_CaH2': ('cah', 'cah00', 'cacah2', 'ca(cah2)', 'ca_cah2'), + 'c_D_in_7LiD': ('dlid', 'd(7lid)'), 'c_D_in_D2O': ('dd2o', 'd-d2o', 'hwtr', 'hw', 'dhw00', 'd(d2o)'), - 'c_D_in_D2O_ice': ('dice',), + 'c_D_in_D2O_solid': ('dice',), + 'c_F_in_Be2': ('fbef2', 'f in bef2'), + 'c_F_in_CF2': ('fcf2', 'f(teflon)'), 'c_F_in_FLiBe': ('fflibe', 'f(flibe)'), + 'c_F_in_HF': ('f_hf',), + 'c_F_in_MgF2': ('fmgf2', 'f in mgf2'), 'c_Fe56': ('fe', 'fe56', 'fe-56', '26-fe- 56'), 'c_Graphite': ('graph', 'grph', 'gr', 'gr00', 'graphite'), 'c_Graphite_10p': ('grph10', '10p graphit'), + 'c_Graphite_20p': ('grph20', '20 graphite'), 'c_Graphite_30p': ('grph30', '30p graphit'), + 'c_Graphite_distinct': ('grphsd', 'grph+sd'), + 'c_H_in_7LiH': ('hlih', 'h(7lih)'), 'c_H_in_C5O2H8': ('lucite', 'c5o2h8', 'h-luci', 'h(lucite)'), + 'c_H_in_C8H8': ('hc8h8', 'h(polystyr'), 'c_H_in_CaH2': ('hcah2', 'hca00', 'h(cah2)'), + 'c_H1_in_CaH2': ('h1cah2', 'h1_cah2'), + 'c_H2_in_CaH2': ('h2cah2', 'h2_cah2'), 'c_H_in_CH2': ('hch2', 'poly', 'pol', 'h-poly', 'pol00', 'h(ch2)'), 'c_H_in_CH4_liquid': ('lch4', 'lmeth', 'l-ch4'), 'c_H_in_CH4_solid': ('sch4', 'smeth', 's-ch4'), 'c_H_in_CH4_solid_phase_II': ('sch4p2',), 'c_H_in_H2O': ('hh2o', 'h-h2o', 'lwtr', 'lw', 'lw00', 'h(h2o)'), 'c_H_in_H2O_solid': ('hice', 'h-ice', 'ice00', 'h(ice-ih)', 'h(ice)'), - 'c_H_in_HF': ('hhf', 'h(hf)'), + 'c_H_in_HF': ('hhf', 'h(hf)', 'h_hf'), 'c_H_in_Mesitylene': ('mesi00', 'mesi', 'mesi-phii'), - 'c_H_in_ParaffinicOil': ('hparaf', 'h(paraffin'), + 'c_H_in_ParaffinicOil': ('hparaf', 'h(paraffin', 'h(paraffini'), 'c_H_in_Toluene': ('tol00', 'tol', 'tolue-phii'), 'c_H_in_UH3': ('huh3', 'h(uh3)'), 'c_H_in_YH2': ('hyh2', 'h-yh2', 'h(yh2)'), @@ -63,24 +87,67 @@ _THERMAL_NAMES = { 'c_H_in_ZrH2': ('hzrh2', 'h(zrh2)'), 'c_H_in_ZrHx': ('hzrhx', 'h(zrhx)'), 'c_Li_in_FLiBe': ('liflib', 'li(flibe)'), + 'c_Li_in_7LiD': ('lilid', '7li(7lid)'), + 'c_Li_in_7LiH': ('lilih', '7li(7lih)'), 'c_Mg24': ('mg', 'mg24', 'mg00', '24-mg'), - 'c_N_in_UN': ('n-un', 'n(un)', 'n(un) l'), + 'c_Mg_in_MgF2': ('mgmgf2', 'mg in mgf2'), + 'c_Mg_in_MgO': ('mgmgo', 'mg in mgo'), + 'c_N_in_UN_100p': ('nun100', 'n-un-100p'), + 'c_N_in_UN_10p': ('nun10', 'n-un-10p'), + 'c_N_in_UN_5p': ('nun5', 'n-un-5p'), + 'c_N_in_UN': ('n-un', 'n(un)', 'n(un) l', 'ninun'), + 'c_N_in_UN_HALEU': ('nunhal', 'n-un-haleu'), + 'c_N_in_UN_HEU': ('nunheu', 'n-un-heu'), 'c_O_in_Al2O3': ('osap00', 'osap', 'o(al2o3)'), - 'c_O_in_BeO': ('obeo', 'o-beo', 'o-be', 'o/be', 'obeo00', 'o(beo)'), + 'c_O_in_BeO': ('obeo', 'o-beo', 'o-be', 'o/be', 'obeo00', 'o(beo)', 'o_beo'), + 'c_O_in_C5O2H8': ('olucit', 'o(lucite)'), 'c_O_in_D2O': ('od2o', 'o-d2o', 'ohw00', 'o(d2o)'), 'c_O_in_H2O_solid': ('oice', 'o-ice', 'o(ice-ih)'), + 'c_O_in_MgO': ('omgo', 'o in mgo'), + 'c_O_in_PuO2': ('opuo2', 'o in puo2'), + 'c_O_in_SiO2_alpha': ('osio2a', 'o_sio2a'), + 'c_O_in_UO2_100p': ('ouo200', 'o-uo2-100p'), + 'c_O_in_UO2_10p': ('ouo210', 'oinuo2-10p'), + 'c_O_in_UO2_5p': ('ouo25', 'oinuo2-5p'), 'c_O_in_UO2': ('ouo2', 'o-uo2', 'o2-u', 'o2/u', 'ouo200', 'o(uo2)'), + 'c_O_in_UO2_HALEU': ('ouo2hl', 'ouo2-haleu'), + 'c_O_in_UO2_HEU': ('ouo2he', 'o_uo2-heu'), 'c_ortho_D': ('orthod', 'orthoD', 'dortho', 'od200', 'ortod', 'ortho-d'), 'c_ortho_H': ('orthoh', 'orthoH', 'hortho', 'oh200', 'ortoh', 'ortho-h'), 'c_para_D': ('parad', 'paraD', 'dpara', 'pd200', 'para-d'), 'c_para_H': ('parah', 'paraH', 'hpara', 'ph200', 'para-h'), + 'c_Pu_in_PuO2': ('puo2', 'pu in puo2'), 'c_Si28': ('si00', 'sili', 'si'), - 'c_Si_in_SiC': ('sisic', 'si-sic', 'si(3c-sic)'), + 'c_Si_in_SiC': ('sisic', 'si-sic', 'si(3c-sic)', 'si_sic'), + 'c_Si_in_SiO2_alpha': ('si_o2a', 'si_sio2a'), 'c_SiO2_alpha': ('sio2', 'sio2a', 'sio2alpha'), 'c_SiO2_beta': ('sio2b', 'sio2beta'), - 'c_U_in_UN': ('u-un', 'u(un)', 'u(un) l'), + 'c_U_metal_100p': ('u-100p',), + 'c_U_metal_10p': ('u-10p',), + 'c_U_metal_5p': ('u-5p',), + 'c_U_metal': ('umetal', 'u-metal'), + 'c_U_metal_HALEU': ('uhaleu', 'u-haleu'), + 'c_U_metal_HEU': ('u-heu',), + 'c_U_in_UC_100p': ('uc-100', 'uinuc_100p'), + 'c_U_in_UC_10p': ('uc-10', 'uinuc_10p'), + 'c_U_in_UC_5p': ('uc-5', 'uinuc_5p'), + 'c_U_in_UC': ('uc-nat', 'uinuc_nat'), + 'c_U_in_UC_HALEU': ('uc-hal', 'uinuc_haleu'), + 'c_U_in_UC_HEU': ('uc-heu', 'uinuc_heu'), + 'c_U_in_UN_100p': ('un-100', 'u-un-100p'), + 'c_U_in_UN_10p': ('un-10', 'u-un-10p'), + 'c_U_in_UN_5p': ('un-5', 'u-un-5p'), + 'c_U_in_UN': ('u-un', 'u(un)', 'u(un) l', 'uinun'), + 'c_U_in_UN_HALEU': ('un-hal', 'u-un-haleu'), + 'c_U_in_UN_HEU': ('un-heu', 'u-un-heu'), + 'c_U_in_UO2_100p': ('uo2100', 'uuo2-100p'), + 'c_U_in_UO2_10p': ('uo2-10', 'uuo2-10p'), + 'c_U_in_UO2_5p': ('uo2-5', 'uuo2-5p'), 'c_U_in_UO2': ('uuo2', 'u-uo2', 'u-o2', 'u/o2', 'uuo200', 'u(uo2)'), + 'c_U_in_UO2_HALEU': ('uo2hal', 'uuo2-haleu'), + 'c_U_in_UO2_HEU': ('uo2heu', 'u_uo2-heu'), 'c_Y_in_YH2': ('yyh2', 'y-yh2', 'y(yh2)'), + 'c_Zr_in_ZrC': ('zrzrc', 'zr(zrc)'), 'c_Zr_in_ZrH': ('zrzrh', 'zr-zrh', 'zr-h', 'zr/h', 'zr(zrh)'), 'c_Zr_in_ZrH2': ('zrzrh2', 'zr(zrh2)'), 'c_Zr_in_ZrHx': ('zrzrhx', 'zr(zrhx)'), From 560bd22bce0b3d21d6a5962b4a5da917d4ef6be1 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Tue, 21 Jan 2025 16:27:01 -0600 Subject: [PATCH 237/671] Tweak To Sphinx Install Documentation (#3271) --- docs/source/devguide/docbuild.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/devguide/docbuild.rst b/docs/source/devguide/docbuild.rst index 389a423f3..f723db06e 100644 --- a/docs/source/devguide/docbuild.rst +++ b/docs/source/devguide/docbuild.rst @@ -12,7 +12,7 @@ Python API. That is, from the root directory of the OpenMC repository: .. code-block:: sh - python -m pip install .[docs] + python -m pip install ".[docs]" ----------------------------------- Building Documentation as a Webpage From 70897800264098b2b3c68c993b89b5582e42f387 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Fri, 24 Jan 2025 14:29:00 -0600 Subject: [PATCH 238/671] Add test for flux bias with weight windows in multigroup mode (#3202) Co-authored-by: Paul Romano --- src/physics_mg.cpp | 6 +-- tests/unit_tests/weightwindows/test_ww_mg.py | 51 ++++++++++++++++++++ tests/unit_tests/weightwindows/ww_mg.txt | 27 +++++++++++ 3 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 tests/unit_tests/weightwindows/test_ww_mg.py create mode 100644 tests/unit_tests/weightwindows/ww_mg.txt diff --git a/src/physics_mg.cpp b/src/physics_mg.cpp index 3cc0532d2..e967afd79 100644 --- a/src/physics_mg.cpp +++ b/src/physics_mg.cpp @@ -28,12 +28,12 @@ void collision_mg(Particle& p) // Add to the collision counter for the particle p.n_collision()++; - if (settings::weight_window_checkpoint_collision) - apply_weight_windows(p); - // Sample the reaction type sample_reaction(p); + if (settings::weight_window_checkpoint_collision) + apply_weight_windows(p); + // Display information about collision if ((settings::verbosity >= 10) || p.trace()) { write_message(fmt::format(" Energy Group = {}", p.g()), 1); diff --git a/tests/unit_tests/weightwindows/test_ww_mg.py b/tests/unit_tests/weightwindows/test_ww_mg.py new file mode 100644 index 000000000..23e09ea0c --- /dev/null +++ b/tests/unit_tests/weightwindows/test_ww_mg.py @@ -0,0 +1,51 @@ +import numpy as np +import openmc + + +def test_weight_windows_mg(request, run_in_tmpdir): + # import basic random ray model + model = openmc.examples.random_ray_three_region_cube() + + # create a mesh tally + mesh = openmc.RegularMesh.from_domain(model.geometry, (3, 3, 3)) + mesh_tally = openmc.Tally() + mesh_tally.filters = [openmc.MeshFilter(mesh)] + mesh_tally.scores = ['flux'] + model.tallies = [mesh_tally] + + # replace random ray settings with fixed source settings + settings = openmc.Settings() + settings.particles = 5000 + settings.batches = 10 + settings.energy_mode = 'multi-group' + settings.run_mode = 'fixed source' + space = openmc.stats.Point((1, 1, 1)) + energy = openmc.stats.delta_function(1e6) + settings.source = openmc.IndependentSource(space=space, energy=energy) + model.settings = settings + + # perform analog simulation + statepoint = model.run() + + # extract flux from analog simulation + with openmc.StatePoint(statepoint) as sp: + tally_out = sp.get_tally(id=mesh_tally.id) + flux_analog = tally_out.mean + + # load the weight windows for this problem and apply them + ww_lower_bnds = np.loadtxt(request.path.parent / 'ww_mg.txt') + weight_windows = openmc.WeightWindows(mesh, lower_ww_bounds=ww_lower_bnds, upper_bound_ratio=5.0) + model.settings.weight_windows = weight_windows + model.settings.weight_windows_on = True + + # re-run with weight windows + statepoint = model.run() + with openmc.StatePoint(statepoint) as sp: + tally_out = sp.get_tally(id=mesh_tally.id) + flux_ww = tally_out.mean + + # the sum of the fluxes should approach the same value (no bias introduced) + analog_sum = flux_analog.sum() + ww_sum = flux_ww.sum() + assert np.allclose(analog_sum, ww_sum, rtol=1e-2) + diff --git a/tests/unit_tests/weightwindows/ww_mg.txt b/tests/unit_tests/weightwindows/ww_mg.txt new file mode 100644 index 000000000..7bcc59d97 --- /dev/null +++ b/tests/unit_tests/weightwindows/ww_mg.txt @@ -0,0 +1,27 @@ +5.000000000000000278e-02 +1.023184121346435924e-02 +3.006624096325660397e-03 +1.030178532774538719e-02 +6.058877789444589400e-03 +2.216914234856166583e-03 +3.061186967456217597e-03 +2.148267952185671601e-03 +1.026171712186230980e-03 +1.040022203443005666e-02 +6.040633813799485378e-03 +2.364143118752318716e-03 +6.119726639841410569e-03 +4.329097093078606955e-03 +1.873104469085542763e-03 +2.246957229279350661e-03 +1.851165248260521617e-03 +7.825824911598703530e-04 +3.021300894848398706e-03 +2.286420236345795311e-03 +9.318583473482396160e-04 +2.234702678114806364e-03 +1.813664566119888152e-03 +7.969287848384389462e-04 +1.017895970086981662e-03 +7.707144532950136471e-04 +3.386087166633241791e-04 From f207d4220afa0582942b2b8f5281e3151ebac429 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Fri, 24 Jan 2025 16:49:58 -0600 Subject: [PATCH 239/671] Adding methods to automatically apply results to existing Tally objects. (#2671) Co-authored-by: Paul Romano --- openmc/arithmetic.py | 52 +++---------- openmc/cell.py | 3 +- openmc/mgxs/mgxs.py | 2 +- openmc/model/model.py | 24 +++++- openmc/statepoint.py | 40 ++++++---- openmc/tallies.py | 104 ++++++++++++++++++++------ openmc/universe.py | 3 +- tests/regression_tests/source/test.py | 2 +- tests/unit_tests/test_tallies.py | 77 ++++++++++++++++++- 9 files changed, 223 insertions(+), 84 deletions(-) diff --git a/openmc/arithmetic.py b/openmc/arithmetic.py index 821014e36..92c42284c 100644 --- a/openmc/arithmetic.py +++ b/openmc/arithmetic.py @@ -93,9 +93,9 @@ class CrossNuclide: Parameters ---------- - left_nuclide : openmc.Nuclide or CrossNuclide + left_nuclide : str or CrossNuclide The left nuclide in the outer product - right_nuclide : openmc.Nuclide or CrossNuclide + right_nuclide : str or CrossNuclide The right nuclide in the outer product binary_op : str The tally arithmetic binary operator (e.g., '+', '-', etc.) used to @@ -103,9 +103,9 @@ class CrossNuclide: Attributes ---------- - left_nuclide : openmc.Nuclide or CrossNuclide + left_nuclide : str or CrossNuclide The left nuclide in the outer product - right_nuclide : openmc.Nuclide or CrossNuclide + right_nuclide : str or CrossNuclide The right nuclide in the outer product binary_op : str The tally arithmetic binary operator (e.g., '+', '-', etc.) used to @@ -134,7 +134,7 @@ class CrossNuclide: @left_nuclide.setter def left_nuclide(self, left_nuclide): cv.check_type('left_nuclide', left_nuclide, - (openmc.Nuclide, CrossNuclide, AggregateNuclide)) + (str, CrossNuclide, AggregateNuclide)) self._left_nuclide = left_nuclide @property @@ -144,7 +144,7 @@ class CrossNuclide: @right_nuclide.setter def right_nuclide(self, right_nuclide): cv.check_type('right_nuclide', right_nuclide, - (openmc.Nuclide, CrossNuclide, AggregateNuclide)) + (str, CrossNuclide, AggregateNuclide)) self._right_nuclide = right_nuclide @property @@ -159,26 +159,7 @@ class CrossNuclide: @property def name(self): - - string = '' - - # If the Summary was linked, the left nuclide is a Nuclide object - if isinstance(self.left_nuclide, openmc.Nuclide): - string += '(' + self.left_nuclide.name - # If the Summary was not linked, the left nuclide is the ZAID - else: - string += '(' + str(self.left_nuclide) - - string += ' ' + self.binary_op + ' ' - - # If the Summary was linked, the right nuclide is a Nuclide object - if isinstance(self.right_nuclide, openmc.Nuclide): - string += self.right_nuclide.name + ')' - # If the Summary was not linked, the right nuclide is the ZAID - else: - string += str(self.right_nuclide) + ')' - - return string + return f'({self.left_nuclide} {self.binary_op} {self.right_nuclide})' class CrossFilter: @@ -440,7 +421,7 @@ class AggregateNuclide: Parameters ---------- - nuclides : Iterable of str or openmc.Nuclide or CrossNuclide + nuclides : Iterable of str or CrossNuclide The nuclides included in the aggregation aggregate_op : str The tally aggregation operator (e.g., 'sum', 'avg', etc.) used @@ -448,7 +429,7 @@ class AggregateNuclide: Attributes ---------- - nuclides : Iterable of str or openmc.Nuclide or CrossNuclide + nuclides : Iterable of str or CrossNuclide The nuclides included in the aggregation aggregate_op : str The tally aggregation operator (e.g., 'sum', 'avg', etc.) used @@ -473,13 +454,7 @@ class AggregateNuclide: return str(other) == str(self) def __repr__(self): - - # Append each nuclide in the aggregate to the string - string = f'{self.aggregate_op}(' - names = [nuclide.name if isinstance(nuclide, openmc.Nuclide) - else str(nuclide) for nuclide in self.nuclides] - string += ', '.join(map(str, names)) + ')' - return string + return f'{self.aggregate_op}{self.name}' @property def nuclides(self): @@ -502,12 +477,9 @@ class AggregateNuclide: @property def name(self): - # Append each nuclide in the aggregate to the string - names = [nuclide.name if isinstance(nuclide, openmc.Nuclide) - else str(nuclide) for nuclide in self.nuclides] - string = '(' + ', '.join(map(str, names)) + ')' - return string + names = [str(nuclide) for nuclide in self.nuclides] + return '(' + ', '.join(map(str, names)) + ')' class AggregateFilter: diff --git a/openmc/cell.py b/openmc/cell.py index fe3939bbe..2e0f2ff13 100644 --- a/openmc/cell.py +++ b/openmc/cell.py @@ -403,9 +403,8 @@ class Cell(IDManagerMixin): if self._atoms is not None: volume = self.volume for name, atoms in self._atoms.items(): - nuclide = openmc.Nuclide(name) density = 1.0e-24 * atoms.n/volume # density in atoms/b-cm - nuclides[name] = (nuclide, density) + nuclides[name] = (name, density) else: raise RuntimeError( 'Volume information is needed to calculate microscopic ' diff --git a/openmc/mgxs/mgxs.py b/openmc/mgxs/mgxs.py index 588b6f64e..1bc7f9387 100644 --- a/openmc/mgxs/mgxs.py +++ b/openmc/mgxs/mgxs.py @@ -956,7 +956,7 @@ class MGXS: self.xs_tally._nuclides = [] nuclides = self.get_nuclides() for nuclide in nuclides: - self.xs_tally.nuclides.append(openmc.Nuclide(nuclide)) + self.xs_tally.nuclides.append(nuclide) # Remove NaNs which may have resulted from divide-by-zero operations self.xs_tally._mean = np.nan_to_num(self.xs_tally.mean) diff --git a/openmc/model/model.py b/openmc/model/model.py index 1c261061a..80859c55d 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -14,7 +14,7 @@ import openmc import openmc._xml as xml from openmc.dummy_comm import DummyCommunicator from openmc.executor import _process_CLI_arguments -from openmc.checkvalue import check_type, check_value +from openmc.checkvalue import check_type, check_value, PathLike from openmc.exceptions import InvalidIDError from openmc.utility_funcs import change_directory @@ -604,7 +604,8 @@ class Model: def run(self, particles=None, threads=None, geometry_debug=False, restart_file=None, tracks=False, output=True, cwd='.', openmc_exec='openmc', mpi_args=None, event_based=None, - export_model_xml=True, **export_kwargs): + export_model_xml=True, apply_tally_results=False, + **export_kwargs): """Run OpenMC If the C API has been initialized, then the C API is used, otherwise, @@ -654,6 +655,11 @@ class Model: to True. .. versionadded:: 0.13.3 + apply_tally_results : bool + Whether to apply results of the final statepoint file to the + model's tally objects. + + .. versionadded:: 0.15.1 **export_kwargs Keyword arguments passed to either :meth:`Model.export_to_model_xml` or :meth:`Model.export_to_xml`. @@ -725,6 +731,10 @@ class Model: if mtime >= tstart: # >= allows for poor clock resolution tstart = mtime last_statepoint = sp + + if apply_tally_results: + self.apply_tally_results(last_statepoint) + return last_statepoint def calculate_volumes(self, threads=None, output=True, cwd='.', @@ -932,6 +942,16 @@ class Model: n_samples=n_samples, prn_seed=prn_seed ) + def apply_tally_results(self, statepoint: PathLike | openmc.StatePoint): + """Apply results from a statepoint to tally objects on the Model + + Parameters + ---------- + statepoint : PathLike or openmc.StatePoint + Statepoint file used to update tally results + """ + self.tallies.add_results(statepoint) + def plot_geometry(self, output=True, cwd='.', openmc_exec='openmc', export_model_xml=True, **export_kwargs): """Creates plot images as specified by the Model.plots attribute diff --git a/openmc/statepoint.py b/openmc/statepoint.py index 13aac0caf..5720f8cd0 100644 --- a/openmc/statepoint.py +++ b/openmc/statepoint.py @@ -448,14 +448,11 @@ class StatePoint: nuclide_names = group['nuclides'][()] # Add all nuclides to the Tally - for name in nuclide_names: - nuclide = openmc.Nuclide(name.decode().strip()) - tally.nuclides.append(nuclide) + tally.nuclides = [name.decode().strip() for name in nuclide_names] # Add the scores to the Tally scores = group['score_bins'][()] - for score in scores: - tally.scores.append(score.decode()) + tally.scores = [score.decode() for score in scores] # Add Tally to the global dictionary of all Tallies tally.sparse = self.sparse @@ -526,15 +523,16 @@ class StatePoint: def get_tally(self, scores=[], filters=[], nuclides=[], name=None, id=None, estimator=None, exact_filters=False, - exact_nuclides=False, exact_scores=False): + exact_nuclides=False, exact_scores=False, + multiply_density=None, derivative=None): """Finds and returns a Tally object with certain properties. This routine searches the list of Tallies and returns the first Tally found which satisfies all of the input parameters. NOTE: If any of the "exact" parameters are False (default), the input - parameters do not need to match the complete Tally specification and - may only represent a subset of the Tally's properties. If an "exact" + parameters do not need to match the complete Tally specification and may + only represent a subset of the Tally's properties. If an "exact" parameter is True then number of scores, filters, or nuclides in the parameters must precisely match those of any matching Tally. @@ -561,9 +559,15 @@ class StatePoint: to those in the matching Tally. If False (default), the nuclides in the parameters may be a subset of those in the matching Tally. exact_scores : bool - If True, the number of scores in the parameters must be identical - to those in the matching Tally. If False (default), the scores - in the parameters may be a subset of those in the matching Tally. + If True, the number of scores in the parameters must be identical to + those in the matching Tally. If False (default), the scores in the + parameters may be a subset of those in the matching Tally. Default + is None (no check). + multiply_density : bool, optional + If not None, the Tally must have the multiply_density attribute set + to the same value as this parameter. + derivative : openmc.TallyDerivative, optional + TallyDerivative object to match. Returns ------- @@ -591,17 +595,25 @@ class StatePoint: if id and id != test_tally.id: continue - # Determine if Tally has queried estimator - if estimator and estimator != test_tally.estimator: + # Determine if Tally has queried estimator, only move on to next tally + # if the estimator is both specified and the tally estimtor does not + # match + if estimator is not None and estimator != test_tally.estimator: continue # The number of filters, nuclides and scores must exactly match if exact_scores and len(scores) != test_tally.num_scores: continue - if exact_nuclides and len(nuclides) != test_tally.num_nuclides: + if exact_nuclides and nuclides and len(nuclides) != test_tally.num_nuclides: + continue + if exact_nuclides and not nuclides and test_tally.nuclides != ['total']: continue if exact_filters and len(filters) != test_tally.num_filters: continue + if derivative is not None and derivative != test_tally.derivative: + continue + if multiply_density is not None and multiply_density != test_tally.multiply_density: + continue # Determine if Tally has the queried score(s) if scores: diff --git a/openmc/tallies.py b/openmc/tallies.py index f39ffa078..b0d78d357 100644 --- a/openmc/tallies.py +++ b/openmc/tallies.py @@ -1,3 +1,4 @@ +from __future__ import annotations from collections.abc import Iterable, MutableSequence import copy from functools import partial, reduce @@ -33,7 +34,7 @@ _NUCLIDE_CLASSES = (str, openmc.CrossNuclide, openmc.AggregateNuclide) _FILTER_CLASSES = (openmc.Filter, openmc.CrossFilter, openmc.AggregateFilter) # Valid types of estimators -ESTIMATOR_TYPES = ['tracklength', 'collision', 'analog'] +ESTIMATOR_TYPES = {'tracklength', 'collision', 'analog'} class Tally(IDManagerMixin): @@ -65,7 +66,9 @@ class Tally(IDManagerMixin): scores : list of str List of defined scores, e.g. 'flux', 'fission', etc. estimator : {'analog', 'tracklength', 'collision'} - Type of estimator for the tally + Type of estimator for the tally. If unset (None), OpenMC will automatically + select an appropriate estimator based on the tally filters and scores + with a preference for 'tracklength'. triggers : list of openmc.Trigger List of tally triggers num_scores : int @@ -131,6 +134,36 @@ class Tally(IDManagerMixin): self._sp_filename = None self._results_read = False + def __eq__(self, other): + if other.id != self.id: + return False + if other.name != self.name: + return False + # estimators are automatically set based on the tally filters and scores + # during OpenMC initialization if this value is None, so it is not + # considered a requirement for equivalence if it is unset on either + # tally as it implies that the user is allowing OpenMC to select an + # appropriate estimator. If the value is explicitly set on both tallies, + # then the values must match for the tallies to be considered equivalent. + if self.estimator is not None and other.estimator is not None and other.estimator != self.estimator: + return False + if other.filters != self.filters: + return False + # for tallies are loaded from statpoint files + # an empty nuclide list is equivalent to a list with 'total' + other_nuclides = other.nuclides.copy() + self_nuclides = self.nuclides.copy() + if 'total' in other_nuclides: + other_nuclides.remove('total') + if 'total' in self_nuclides: + self_nuclides.remove('total') + if other_nuclides != self_nuclides: + return False + for attr in {'scores', 'triggers', 'derivative', 'multiply_density'}: + if getattr(other, attr) != getattr(self, attr): + return False + return True + def __repr__(self): parts = ['Tally'] parts.append('{: <15}=\t{}'.format('ID', self.id)) @@ -266,7 +299,8 @@ class Tally(IDManagerMixin): @estimator.setter def estimator(self, estimator): - cv.check_value('estimator', estimator, ESTIMATOR_TYPES) + # allow the estimator to be set to None (let OpenMC choose the estimator at runtime) + cv.check_value('estimator', estimator, ESTIMATOR_TYPES | {None}) self._estimator = estimator @property @@ -304,8 +338,16 @@ class Tally(IDManagerMixin): # Open the HDF5 statepoint file with h5py.File(self._sp_filename, 'r') as f: + # Set number of realizations + group = f[f'tallies/tally {self.id}'] + self.num_realizations = int(group['n_realizations'][()]) + + # Update nuclides + nuclide_names = group['nuclides'][()] + self.nuclides = [name.decode().strip() for name in nuclide_names] + # Extract Tally data from the file - data = f[f'tallies/tally {self.id}/results'] + data = group['results'] sum_ = data[:, :, 0] sum_sq = data[:, :, 1] @@ -511,7 +553,7 @@ class Tally(IDManagerMixin): Parameters ---------- - nuclide : openmc.Nuclide + nuclide : str Nuclide to remove """ @@ -883,6 +925,21 @@ class Tally(IDManagerMixin): return element + def add_results(self, statepoint: cv.PathLike | openmc.StatePoint): + """Add results from the provided statepoint file to this tally instance + + .. versionadded:: 0.15.1 + + Parameters + ---------- + statepoint : openmc.PathLike or openmc.StatePoint + Statepoint used to update tally results + """ + if isinstance(statepoint, openmc.StatePoint): + self._sp_filename = statepoint._f.filename + else: + self._sp_filename = str(statepoint) + @classmethod def from_xml_element(cls, elem, **kwargs): """Generate tally object from an XML element @@ -1020,17 +1077,10 @@ class Tally(IDManagerMixin): in the Tally. """ - # Look for the user-requested nuclide in all of the Tally's Nuclides + # Look for the user-requested nuclide in all of the Tally's nuclides for i, test_nuclide in enumerate(self.nuclides): - # If the Summary was linked, then values are Nuclide objects - if isinstance(test_nuclide, openmc.Nuclide): - if test_nuclide.name == nuclide: - return i - - # If the Summary has not been linked, then values are ZAIDs - else: - if test_nuclide == nuclide: - return i + if test_nuclide == nuclide: + return i msg = (f'Unable to get the nuclide index for Tally since "{nuclide}" ' 'is not one of the nuclides') @@ -1357,9 +1407,7 @@ class Tally(IDManagerMixin): column_name = 'nuclide' for nuclide in self.nuclides: - if isinstance(nuclide, openmc.Nuclide): - nuclides.append(nuclide.name) - elif isinstance(nuclide, openmc.AggregateNuclide): + if isinstance(nuclide, openmc.AggregateNuclide): nuclides.append(nuclide.name) column_name = f'{nuclide.aggregate_op}(nuclide)' else: @@ -2350,7 +2398,8 @@ class Tally(IDManagerMixin): new_tally.name = self.name new_tally._mean = self.mean / other new_tally._std_dev = self.std_dev * np.abs(1. / other) - new_tally.estimator = self.estimator + if self.estimator is not None: + new_tally.estimator = self.estimator new_tally.with_summary = self.with_summary new_tally.num_realizations = self.num_realizations @@ -2643,8 +2692,8 @@ class Tally(IDManagerMixin): # Determine the nuclide indices from any of the requested nuclides for nuclide in self.nuclides: - if nuclide.name not in nuclides: - nuclide_index = self.get_nuclide_index(nuclide.name) + if nuclide not in nuclides: + nuclide_index = self.get_nuclide_index(nuclide) nuclide_indices.append(nuclide_index) # Loop over indices in reverse to remove excluded Nuclides @@ -3166,6 +3215,19 @@ class Tallies(cv.CheckedList): # Continue iterating from the first loop break + def add_results(self, statepoint: cv.PathLike | openmc.StatePoint): + """Add results from the provided statepoint file + + .. versionadded:: 0.15.1 + + Parameters + ---------- + statepoint : openmc.PathLike or openmc.StatePoint + Statepoint used to update tally results + """ + for tally in self: + tally.add_results(statepoint) + def _create_tally_subelements(self, root_element): for tally in self: root_element.append(tally.to_xml_element()) diff --git a/openmc/universe.py b/openmc/universe.py index 85ce6fd96..71ffa726b 100644 --- a/openmc/universe.py +++ b/openmc/universe.py @@ -598,9 +598,8 @@ class UniverseBase(ABC, IDManagerMixin): if self._atoms: volume = self.volume for name, atoms in self._atoms.items(): - nuclide = openmc.Nuclide(name) density = 1.0e-24 * atoms.n/volume # density in atoms/b-cm - nuclides[name] = (nuclide, density) + nuclides[name] = (name, density) else: raise RuntimeError( 'Volume information is needed to calculate microscopic cross ' diff --git a/tests/regression_tests/source/test.py b/tests/regression_tests/source/test.py index c1f4dccff..96efa77eb 100644 --- a/tests/regression_tests/source/test.py +++ b/tests/regression_tests/source/test.py @@ -11,7 +11,7 @@ class SourceTestHarness(PyAPITestHarness): super().__init__(*args, **kwargs) mat1 = openmc.Material(material_id=1, temperature=294) mat1.set_density('g/cm3', 4.5) - mat1.add_nuclide(openmc.Nuclide('U235'), 1.0) + mat1.add_nuclide('U235', 1.0) self._model.materials = openmc.Materials([mat1]) sphere = openmc.Sphere(surface_id=1, r=10.0, boundary_type='vacuum') diff --git a/tests/unit_tests/test_tallies.py b/tests/unit_tests/test_tallies.py index 544443312..f044df5d0 100644 --- a/tests/unit_tests/test_tallies.py +++ b/tests/unit_tests/test_tallies.py @@ -41,4 +41,79 @@ def test_xml_roundtrip(run_in_tmpdir): assert len(new_tally.triggers) == 1 assert new_tally.triggers[0].trigger_type == tally.triggers[0].trigger_type assert new_tally.triggers[0].threshold == tally.triggers[0].threshold - assert new_tally.triggers[0].scores == tally.triggers[0].scores \ No newline at end of file + assert new_tally.triggers[0].scores == tally.triggers[0].scores + assert new_tally.multiply_density == tally.multiply_density + + +def test_tally_equivalence(): + tally_a = openmc.Tally() + tally_b = openmc.Tally(tally_id=tally_a.id) + + tally_a.name = 'new name' + assert tally_a != tally_b + tally_b.name = tally_a.name + assert tally_a == tally_b + + assert tally_a == tally_b + ef_a = openmc.EnergyFilter([0.0, 0.1, 1.0, 10.0e6]) + ef_b = openmc.EnergyFilter([0.0, 0.1, 1.0, 10.0e6]) + + tally_a.filters = [ef_a] + assert tally_a != tally_b + tally_b.filters = [ef_b] + assert tally_a == tally_b + + tally_a.scores = ['flux', 'absorption', 'fission', 'scatter'] + assert tally_a != tally_b + tally_b.scores = ['flux', 'absorption', 'fission', 'scatter'] + assert tally_a == tally_b + + tally_a.nuclides = [] + tally_b.nuclides = [] + assert tally_a == tally_b + + tally_a.nuclides = ['total'] + assert tally_a == tally_b + + # a tally with an estimator set to None is equal to + # a tally with an estimator specified + tally_a.estimator = 'collision' + assert tally_a == tally_b + tally_b.estimator = 'collision' + assert tally_a == tally_b + + tally_a.multiply_density = False + assert tally_a != tally_b + tally_b.multiply_density = False + assert tally_a == tally_b + + trigger_a = openmc.Trigger('rel_err', 0.025) + trigger_b = openmc.Trigger('rel_err', 0.025) + + tally_a.triggers = [trigger_a] + assert tally_a != tally_b + tally_b.triggers = [trigger_b] + assert tally_a == tally_b + + +def test_tally_application(sphere_model, run_in_tmpdir): + # Create a tally with most possible gizmos + tally = openmc.Tally(name='test tally') + ef = openmc.EnergyFilter([0.0, 0.1, 1.0, 10.0e6]) + mesh = openmc.RegularMesh.from_domain(sphere_model.geometry, (2, 2, 2)) + mf = openmc.MeshFilter(mesh) + tally.filters = [ef, mf] + tally.scores = ['flux', 'absorption', 'fission', 'scatter'] + sphere_model.tallies = [tally] + + # run the simulation and apply results + sp_file = sphere_model.run(apply_tally_results=True) + with openmc.StatePoint(sp_file) as sp: + assert tally in sp.tallies.values() + sp_tally = sp.tallies[tally.id] + + # at this point the tally information regarding results should be the same + assert (sp_tally.mean == tally.mean).all() + assert (sp_tally.std_dev == tally.std_dev).all() + assert sp_tally.nuclides == tally.nuclides + From 7a18108724edbdc0adf004308c3955824d9b102c Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Fri, 24 Jan 2025 18:06:15 -0600 Subject: [PATCH 240/671] Adjust for secondary particle energy directly in heating scores (#3227) Co-authored-by: Paul Romano --- include/openmc/particle.h | 6 ++++ include/openmc/particle_data.h | 12 ++++--- src/particle.cpp | 19 ++++++++--- src/physics.cpp | 9 +++-- src/physics_mg.cpp | 9 +++-- src/tallies/tally_scoring.cpp | 13 ++------ src/track_output.cpp | 4 +-- src/weight_windows.cpp | 2 +- tests/unit_tests/weightwindows/test.py | 46 ++++++++++++++++++++++++-- 9 files changed, 83 insertions(+), 37 deletions(-) diff --git a/include/openmc/particle.h b/include/openmc/particle.h index 6a2e67049..b1f9fabd1 100644 --- a/include/openmc/particle.h +++ b/include/openmc/particle.h @@ -53,6 +53,12 @@ public: //! \param type Particle type void create_secondary(double wgt, Direction u, double E, ParticleType type); + //! split a particle + // + //! creates a new particle with weight wgt + //! \param wgt Weight of the new particle + void split(double wgt); + //! initialize from a source site // //! initializes a particle from data stored in a source site. The source diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index 66137c5f6..2255d1a7d 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -430,7 +430,7 @@ private: int delayed_group_ {0}; int n_bank_ {0}; - int n_bank_second_ {0}; + double bank_second_E_ {0.0}; double wgt_bank_ {0.0}; int n_delayed_bank_[MAX_DELAYED_GROUPS]; @@ -544,11 +544,13 @@ public: int& delayed_group() { return delayed_group_; } // delayed group // Post-collision data - int& n_bank() { return n_bank_; } // number of banked fission sites - int& n_bank_second() + double& bank_second_E() { - return n_bank_second_; - } // number of secondaries banked + return bank_second_E_; + } // energy of last reaction secondaries + const double& bank_second_E() const { return bank_second_E_; } + + int& n_bank() { return n_bank_; } // number of banked fission sites double& wgt_bank() { return wgt_bank_; } // weight of banked fission sites int* n_delayed_bank() { diff --git a/src/particle.cpp b/src/particle.cpp index 65e8065fd..4dbab3213 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -91,17 +91,25 @@ void Particle::create_secondary( return; } - secondary_bank().emplace_back(); - - auto& bank {secondary_bank().back()}; + auto& bank = secondary_bank().emplace_back(); bank.particle = type; bank.wgt = wgt; bank.r = r(); bank.u = u; bank.E = settings::run_CE ? E : g(); bank.time = time(); + bank_second_E() += bank.E; +} - n_bank_second() += 1; +void Particle::split(double wgt) +{ + auto& bank = secondary_bank().emplace_back(); + bank.particle = type(); + bank.wgt = wgt; + bank.r = r(); + bank.u = u(); + bank.E = settings::run_CE ? E() : g(); + bank.time = time(); } void Particle::from_source(const SourceSite* src) @@ -356,7 +364,7 @@ void Particle::event_collide() // Reset banked weight during collision n_bank() = 0; - n_bank_second() = 0; + bank_second_E() = 0.0; wgt_bank() = 0.0; zero_delayed_bank(); @@ -417,6 +425,7 @@ void Particle::event_revive_from_secondary() from_source(&secondary_bank().back()); secondary_bank().pop_back(); n_event() = 0; + bank_second_E() = 0.0; // Subtract secondary particle energy from interim pulse-height results if (!model::active_pulse_height_tallies.empty() && diff --git a/src/physics.cpp b/src/physics.cpp index 34dc4ce1c..c324ce6b9 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -239,11 +239,10 @@ void create_fission_sites(Particle& p, int i_nuclide, const Reaction& rx) } // Write fission particles to nuBank - p.nu_bank().emplace_back(); - NuBank* nu_bank_entry = &p.nu_bank().back(); - nu_bank_entry->wgt = site.wgt; - nu_bank_entry->E = site.E; - nu_bank_entry->delayed_group = site.delayed_group; + NuBank& nu_bank_entry = p.nu_bank().emplace_back(); + nu_bank_entry.wgt = site.wgt; + nu_bank_entry.E = site.E; + nu_bank_entry.delayed_group = site.delayed_group; } // If shared fission bank was full, and no fissions could be added, diff --git a/src/physics_mg.cpp b/src/physics_mg.cpp index e967afd79..e381b27c9 100644 --- a/src/physics_mg.cpp +++ b/src/physics_mg.cpp @@ -188,11 +188,10 @@ void create_fission_sites(Particle& p) } // Write fission particles to nuBank - p.nu_bank().emplace_back(); - NuBank* nu_bank_entry = &p.nu_bank().back(); - nu_bank_entry->wgt = site.wgt; - nu_bank_entry->E = site.E; - nu_bank_entry->delayed_group = site.delayed_group; + NuBank& nu_bank_entry = p.nu_bank().emplace_back(); + nu_bank_entry.wgt = site.wgt; + nu_bank_entry.E = site.E; + nu_bank_entry.delayed_group = site.delayed_group; } // If shared fission bank was full, and no fissions could be added, diff --git a/src/tallies/tally_scoring.cpp b/src/tallies/tally_scoring.cpp index e798161ec..02cb48567 100644 --- a/src/tallies/tally_scoring.cpp +++ b/src/tallies/tally_scoring.cpp @@ -964,14 +964,9 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, // The energy deposited is the difference between the pre-collision // and post-collision energy... score = E - p.E(); - // ...less the energy of any secondary particles since they will be // transported individually later - const auto& bank = p.secondary_bank(); - for (auto it = bank.end() - p.n_bank_second(); it < bank.end(); - ++it) { - score -= it->E; - } + score -= p.bank_second_E(); score *= p.wgt_last(); } else { @@ -1500,13 +1495,9 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index, // The energy deposited is the difference between the pre-collision and // post-collision energy... score = E - p.E(); - // ...less the energy of any secondary particles since they will be // transported individually later - const auto& bank = p.secondary_bank(); - for (auto it = bank.end() - p.n_bank_second(); it < bank.end(); ++it) { - score -= it->E; - } + score -= p.bank_second_E(); score *= p.wgt_last(); } diff --git a/src/track_output.cpp b/src/track_output.cpp index 5c1436de7..f4344d50f 100644 --- a/src/track_output.cpp +++ b/src/track_output.cpp @@ -31,8 +31,8 @@ int n_tracks_written; //! Number of tracks written void add_particle_track(Particle& p) { - p.tracks().emplace_back(); - p.tracks().back().particle = p.type(); + auto& track = p.tracks().emplace_back(); + track.particle = p.type(); } void write_particle_track(Particle& p) diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index e798a2f7a..9579f71c0 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -114,7 +114,7 @@ void apply_weight_windows(Particle& p) // 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.create_secondary(weight / n_split, p.u(), p.E(), p.type()); + p.split(weight / n_split); } // remaining weight is applied to current particle p.wgt() = weight / n_split; diff --git a/tests/unit_tests/weightwindows/test.py b/tests/unit_tests/weightwindows/test.py index 79aadbef0..d6e509522 100644 --- a/tests/unit_tests/weightwindows/test.py +++ b/tests/unit_tests/weightwindows/test.py @@ -4,7 +4,6 @@ from pathlib import Path import numpy as np import pytest from uncertainties import ufloat - import openmc import openmc.lib from openmc.stats import Discrete, Point @@ -224,6 +223,47 @@ def test_lower_ww_bounds_shape(): assert ww.lower_ww_bounds.shape == (2, 3, 4, 1) +def test_photon_heating(run_in_tmpdir): + water = openmc.Material() + water.add_nuclide('H1', 1.0) + water.add_nuclide('O16', 2.0) + water.set_density('g/cm3', 1.0) + + box = openmc.model.RectangularParallelepiped( + -300, 300, -300, 300, -300, 300, boundary_type='reflective') + cell = openmc.Cell(region=-box, fill=water) + model = openmc.Model() + model.geometry = openmc.Geometry([cell]) + + mesh = openmc.RegularMesh.from_domain(model.geometry, dimension=(5, 5, 5)) + wwg = openmc.WeightWindowGenerator(mesh, particle_type='photon') + model.settings.weight_window_generators = [wwg] + + space = openmc.stats.Point((0, 0, 0)) + energy = openmc.stats.delta_function(5e6) + model.settings.source = openmc.IndependentSource( + space=space, energy=energy, particle='photon') + + model.settings.run_mode = 'fixed source' + model.settings.batches = 5 + model.settings.particles = 100 + + tally = openmc.Tally() + tally.scores = ['heating'] + tally.filters = [ + openmc.ParticleFilter(['photon']), + openmc.MeshFilter(mesh) + ] + model.tallies = [tally] + + sp_file = model.run() + with openmc.StatePoint(sp_file) as sp: + tally_mean = sp.tallies[tally.id].mean + + # these values should be nearly identical + assert np.all(tally_mean >= 0) + + def test_roundtrip(run_in_tmpdir, model, wws): model.settings.weight_windows = wws @@ -249,11 +289,11 @@ def test_ww_attrs_python(model): # is successful wws = openmc.WeightWindows(mesh, lower_bounds, upper_bound_ratio=10.0) - assert wws.energy_bounds == None + assert wws.energy_bounds is None wwg = openmc.WeightWindowGenerator(mesh) - assert wwg.energy_bounds == None + assert wwg.energy_bounds is None def test_ww_attrs_capi(run_in_tmpdir, model): model.export_to_xml() From 2bea7f338b63c59e9a49f27989c6e9e71299a584 Mon Sep 17 00:00:00 2001 From: Andrew Davis Date: Sat, 25 Jan 2025 01:59:52 +0000 Subject: [PATCH 241/671] Added missing documentation (#3275) Co-authored-by: Jonathan Shimwell Co-authored-by: Paul Romano --- docs/source/io_formats/settings.rst | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 34b73fc55..06149318e 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -252,11 +252,29 @@ to false. *Default*: true +-------------------------------- +```` Element +-------------------------------- + +This element indicates the maximum number of lost particles. + + *Default*: 10 + +------------------------------------ +```` Element +------------------------------------ + + +This element indicates the maximum number of lost particles, relative to the +total number of particles. + + *Default*: 1.0e-6 + ------------------------------------- ```` Element ------------------------------------- -This element indicates the number of neutrons to run in flight concurrently +This element indicates the number of particles to run in flight concurrently when using event-based parallelism. A higher value uses more memory, but may be more efficient computationally. From a8768b78450d3f2d480062d441c10e0c30eec254 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Mon, 27 Jan 2025 22:54:32 -0600 Subject: [PATCH 242/671] FW-CADIS Weight Window Generation with Random Ray (#3273) Co-authored-by: Olek <45364492+yardasol@users.noreply.github.com> Co-authored-by: Paul Romano --- docs/source/io_formats/settings.rst | 2 +- docs/source/methods/index.rst | 1 + docs/source/methods/random_ray.rst | 2 + docs/source/methods/variance_reduction.rst | 134 ++++++ docs/source/usersguide/index.rst | 1 + docs/source/usersguide/random_ray.rst | 191 +++++++- docs/source/usersguide/variance_reduction.rst | 162 +++++++ include/openmc/weight_windows.h | 20 +- openmc/weight_windows.py | 16 +- src/random_ray/random_ray_simulation.cpp | 1 + src/weight_windows.cpp | 121 +++-- .../weightwindows_fw_cadis/__init__.py | 0 .../weightwindows_fw_cadis/inputs_true.dat | 260 +++++++++++ .../weightwindows_fw_cadis/results_true.dat | 442 ++++++++++++++++++ .../weightwindows_fw_cadis/test.py | 33 ++ tests/testing_harness.py | 34 ++ 16 files changed, 1351 insertions(+), 69 deletions(-) create mode 100644 docs/source/methods/variance_reduction.rst create mode 100644 docs/source/usersguide/variance_reduction.rst create mode 100644 tests/regression_tests/weightwindows_fw_cadis/__init__.py create mode 100644 tests/regression_tests/weightwindows_fw_cadis/inputs_true.dat create mode 100644 tests/regression_tests/weightwindows_fw_cadis/results_true.dat create mode 100644 tests/regression_tests/weightwindows_fw_cadis/test.py diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 06149318e..4da57fe2a 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -1398,7 +1398,7 @@ mesh-based weight windows. *Default*: true :method: - Method used to update weight window values (currently only 'magic' is supported) + Method used to update weight window values (one of 'magic' or 'fw_cadis') *Default*: magic diff --git a/docs/source/methods/index.rst b/docs/source/methods/index.rst index 08aa7d053..75c421c87 100644 --- a/docs/source/methods/index.rst +++ b/docs/source/methods/index.rst @@ -20,4 +20,5 @@ Theory and Methodology energy_deposition parallelization cmfd + variance_reduction random_ray \ No newline at end of file diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index c827c351d..eb22b0544 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -1060,6 +1060,8 @@ random ray and Monte Carlo, however. develop the scattering source by way of inactive batches before beginning active batches. +.. _adjoint: + ------------------------ Adjoint Flux Solver Mode ------------------------ diff --git a/docs/source/methods/variance_reduction.rst b/docs/source/methods/variance_reduction.rst new file mode 100644 index 000000000..353ae5077 --- /dev/null +++ b/docs/source/methods/variance_reduction.rst @@ -0,0 +1,134 @@ +.. _methods_variance_reduction: + +================== +Variance Reduction +================== + +.. _methods_variance_reduction_intro: + +------------ +Introduction +------------ + +Transport problems can sometimes involve a significant degree of attenuation +between the source and a detector (tally) region, which can result in a flux +differential of ten orders of magnitude (or more) throughout the simulation +domain. As Monte Carlo uncertainties tend to be inversely proportional to the +physical flux density, it can be extremely difficult to accurately resolve +tallies in locations that are optically far from the source. This issue is +particularly common in fixed source simulations, where some tally locations may +not experience a single scoring event, even after billions of analog histories. + +Variance reduction techniques aim to either flatten the global uncertainty +distribution, such that all regions of phase space have a fairly similar +uncertainty, or to reduce the uncertainty in specific locations (such as a +detector). There are two strategies available in OpenMC for variance reduction: +the Monte Carlo MAGIC method and the FW-CADIS method. Both strategies work by +developing a weight window mesh that can be utilized by subsequent Monte Carlo +solves to split particles heading towards areas of lower flux densities while +terminating particles in higher flux regions---all while maintaining a fair +game. + +------------ +MAGIC Method +------------ + +The Method of Automatic Generation of Importances by Calculation, or `MAGIC +method `_, is an iterative +technique that uses spatial flux information :math:`\phi(r)` obtained from a +normal Monte Carlo solve to produce weight windows :math:`w(r)` that can be +utilized by a subsequent iteration of Monte Carlo. While the first generation of +weight windows produced may only help to reduce variance slightly, use of these +weights to generate another set of weight windows results in a progressively +improving iterative scheme. + +Equation :eq:`magic` defines how the lower bound of weight windows +:math:`w_{\ell}(r)` are generated with MAGIC using forward flux information. +Here, we can see that the flux at location :math:`r` is normalized by the +maximum flux in any group at that location. We can also see that the weights are +divided by a factor of two, which accounts for the typical :math:`5\times` +factor separating the lower and upper weight window bounds in OpenMC. + +.. math:: + :label: magic + + w_{\ell}(r) = \frac{\phi(r)}{2\,\text{max}(\phi(r))} + +A major advantage of this technique is that it does not require any special +transport machinery; it simply uses multiple Monte Carlo simulations to +iteratively improve a set of weight windows (which are typically defined on a +mesh covering the simulation domain). The downside to this method is that as the +flux differential increases between areas near and far from the source, it +requires more outer Monte Carlo iterations, each of which can be expensive in +itself. Additionally, computation of weight windows based on regular (forward) +neutron flux tally information does not produce the most numerically effective +set of weight windows. Nonetheless, MAGIC remains a simple and effective +technique for generating weight windows. + +-------- +FW-CADIS +-------- + +As discussed in the previous section, computation of weight windows based on +regular (forward) neutron flux tally information does not produce the most +numerically efficient set of weight windows. It is highly preferable to generate +weight windows based on spatial adjoint flux :math:`\phi^{\dag}(r)` +information. The adjoint flux is essentially the "reverse" simulation problem, +where we sample a random point and assume this is where a particle was absorbed, +and then trace it backwards (upscattering in energy), until we sample the point +where it was born from. + +The Forward-Weighted Consistent Adjoint Driven Importance Sampling method, or +`FW-CADIS method `_, produces weight windows +for global variance reduction given adjoint flux information throughout the +entire domain. The weight window lower bound is defined in Equation +:eq:`fw_cadis`, and also involves a normalization step not shown here. + +.. math:: + :label: fw_cadis + + w_{\ell}(r) = \frac{1}{2\phi^{\dag}(r)} + +While the algorithm itself is quite simple, it requires estimates of the global +adjoint flux distribution, which is difficult to generate directly with Monte +Carlo transport. Thus, FW-CADIS typically uses an alternative solver (often +deterministic) that can be more readily adapted for generating adjoint flux +information, and which is often much cheaper than Monte Carlo given that a rough +solution is often sufficient for weight window generation. + +The FW-CADIS implementation in OpenMC utilizes its own internal random ray +multigroup transport solver to generate the adjoint source distribution. No +coupling to any external transport is solver is necessary. The random ray solver +operates on the same geometry as the Monte Carlo solver, so no redefinition of +the simulation geometry is required. More details on how the adjoint flux is +computed are given in the :ref:`adjoint methods section `. + +More information on the workflow is available in the :ref:`user guide +`, but generally production of weight windows with FW-CADIS +involves several stages (some of which are highly automated). These tasks +include generation of approximate multigroup cross section data for use by the +random ray solver, running of the random ray solver in normal (forward flux) +mode to generate a source for the adjoint solver, running of the random ray +solver in adjoint mode to generate adjoint flux tallies, and finally the +production of weight windows via the FW-CADIS method. As is discussed in the +user guide, most of these steps are automated together, making the additional +burden on the user fairly small. + +The major advantage of this technique is that it typically produces much more +numerically efficient weight windows as compared to those generated with MAGIC, +sometimes with an order-of-magnitude improvement in the figure of merit +(Equation :eq:`variance_fom`), which accounts for both the variance and the +execution time. Another major advantage is that the cost of the random ray +solver is typically negligible compared to the cost of the subsequent Monte +Carlo solve itself, making it a very cheap method to deploy. The downside to +this method is that it introduces a second transport method into the mix (random +ray), such that there are more free input parameters for the user to know about +and adjust, potentially making the method more complex to use. However, as many +of the parameters have natural choices, much of this parameterization can be +handled automatically behind the scenes without the need for the user to be +aware of this. + +.. math:: + :label: variance_fom + + \text{FOM} = \frac{1}{\text{Time} \times \sigma^2} diff --git a/docs/source/usersguide/index.rst b/docs/source/usersguide/index.rst index 03a77e870..74651c011 100644 --- a/docs/source/usersguide/index.rst +++ b/docs/source/usersguide/index.rst @@ -25,6 +25,7 @@ essential aspects of using OpenMC to perform simulations. processing parallel volume + variance_reduction random_ray troubleshoot \ No newline at end of file diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index 2b9cf6724..b6852b7ca 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -435,10 +435,11 @@ Inputting Multigroup Cross Sections (MGXS) Multigroup cross sections for use with OpenMC's random ray solver are input the same way as with OpenMC's traditional multigroup Monte Carlo mode. There is more information on generating multigroup cross sections via OpenMC in the -:ref:`multigroup materials ` user guide. You may also wish to -use an existing multigroup library. An example of using OpenMC's Python -interface to generate a correctly formatted ``mgxs.h5`` input file is given -in the `OpenMC Jupyter notebook collection +:ref:`multigroup materials ` user guide. You may also wish to use +an existing ``mgxs.h5`` MGXS library file, or define your own given a known set +of cross section data values (e.g., as taken from a benchmark specification). An +example of using OpenMC's Python interface to generate a correctly formatted +``mgxs.h5`` input file is given in the `OpenMC Jupyter notebook collection `_. .. note:: @@ -447,6 +448,184 @@ in the `OpenMC Jupyter notebook collection separate materials can be defined each with a separate multigroup dataset corresponding to a given temperature. +.. _mgxs_gen: + +------------------------------------------- +Generating Multigroup Cross Sections (MGXS) +------------------------------------------- + +OpenMC is capable of generating multigroup cross sections by way of flux +collapsing data based on flux solutions obtained from a continuous energy Monte +Carlo solve. While it is a circular excercise in some respects to use continuous +energy Monte Carlo to generate cross sections to be used by a reduced-fidelity +multigroup transport solver, there are many use cases where this is nonetheless +highly desirable. For instance, generation of a multigroup library may enable +the same set of approximate multigroup cross section data to be used across a +variety of problem types (or through a multidimensional parameter sweep of +design variables) with only modest errors and at greatly reduced cost as +compared to using only continuous energy Monte Carlo. + +We give here a quick summary of how to produce a multigroup cross section data +file (``mgxs.h5``) from a starting point of a typical continuous energy Monte +Carlo input file. Notably, continuous energy input files define materials as a +mixture of nuclides with different densities, whereas multigroup materials are +simply defined by which name they correspond to in a ``mgxs.h5`` library file. + +To generate the cross section data, we begin with a continuous energy Monte +Carlo input deck and add in the required tallies that will be needed to generate +our library. In this example, we will specify material-wise cross sections and a +two group energy decomposition:: + + # Define geometry + ... + ... + geometry = openmc.Geometry() + ... + ... + + # Initialize MGXS library with a finished OpenMC geometry object + mgxs_lib = openmc.mgxs.Library(geometry) + + # Pick energy group structure + groups = mgxs.EnergyGroups(mgxs.GROUP_STRUCTURES['CASMO-2']) + mgxs_lib.energy_groups = groups + + # Disable transport correction + mgxs_lib.correction = None + + # Specify needed cross sections for random ray + mgxs_lib.mgxs_types = ['total', 'absorption', 'nu-fission', 'fission', + 'nu-scatter matrix', 'multiplicity matrix', 'chi'] + + # Specify a "cell" domain type for the cross section tally filters + mgxs_lib.domain_type = "material" + + # Specify the cell domains over which to compute multi-group cross sections + mgxs_lib.domains = geom.get_all_materials().values() + + # Do not compute cross sections on a nuclide-by-nuclide basis + mgxs_lib.by_nuclide = False + + # Check the library - if no errors are raised, then the library is satisfactory. + mgxs_lib.check_library_for_openmc_mgxs() + + # Construct all tallies needed for the multi-group cross section library + mgxs_lib.build_library() + + # Create a "tallies.xml" file for the MGXS Library + tallies = openmc.Tallies() + mgxs_lib.add_to_tallies_file(tallies, merge=True) + + # Export + tallies.export_to_xml() + + ... + +When selecting an energy decomposition, you can manually define group boundaries +or pick out a group structure already known to OpenMC (a list of which can be +found at :class:`openmc.mgxs.GROUP_STRUCTURES`). Once the above input deck has +been run, the resulting statepoint file will contain the needed flux and +reaction rate tally data so that a MGXS library file can be generated. Below is +the postprocessing script needed to generate the ``mgxs.h5`` library file given +a statepoint file (e.g., ``statepoint.100.h5``) file and summary file (e.g., +``summary.h5``) that resulted from running our previous example:: + + import openmc + import openmc.mgxs as mgxs + + summary = openmc.Summary('summary.h5') + geom = summary.geometry + mats = summary.materials + + statepoint_filename = 'statepoint.100.h5' + sp = openmc.StatePoint(statepoint_filename) + + groups = mgxs.EnergyGroups(mgxs.GROUP_STRUCTURES['CASMO-2']) + mgxs_lib = openmc.mgxs.Library(geom) + mgxs_lib.energy_groups = groups + mgxs_lib.correction = None + mgxs_lib.mgxs_types = ['total', 'absorption', 'nu-fission', 'fission', + 'nu-scatter matrix', 'multiplicity matrix', 'chi'] + + # Specify a "cell" domain type for the cross section tally filters + mgxs_lib.domain_type = "material" + + # Specify the cell domains over which to compute multi-group cross sections + mgxs_lib.domains = geom.get_all_materials().values() + + # Do not compute cross sections on a nuclide-by-nuclide basis + mgxs_lib.by_nuclide = False + + # Check the library - if no errors are raised, then the library is satisfactory. + mgxs_lib.check_library_for_openmc_mgxs() + + # Construct all tallies needed for the multi-group cross section library + mgxs_lib.build_library() + + mgxs_lib.load_from_statepoint(sp) + + names = [] + for mat in mgxs_lib.domains: names.append(mat.name) + + # Create a MGXS File which can then be written to disk + mgxs_file = mgxs_lib.create_mg_library(xs_type='macro', xsdata_names=names) + + # Write the file to disk using the default filename of "mgxs.h5" + mgxs_file.export_to_hdf5("mgxs.h5") + +Notably, the postprocessing script needs to match the same +:class:`openmc.mgxs.Library` settings that were used to generate the tallies, +but otherwise is able to discern the rest of the simulation details from the +statepoint and summary files. Once the postprocessing script is successfully +run, the ``mgxs.h5`` file can be loaded by subsequent runs of OpenMC. + +If you want to convert continuous energy material objects in an OpenMC input +deck to multigroup ones from a ``mgxs.h5`` library, you can follow the below +example. Here we begin with the original continuous energy materials we used to +generate our MGXS library:: + + fuel = openmc.Material(name='UO2 (2.4%)') + fuel.set_density('g/cm3', 10.29769) + fuel.add_nuclide('U234', 4.4843e-6) + fuel.add_nuclide('U235', 5.5815e-4) + fuel.add_nuclide('U238', 2.2408e-2) + fuel.add_nuclide('O16', 4.5829e-2) + + water = openmc.Material(name='Hot borated water') + water.set_density('g/cm3', 0.740582) + water.add_nuclide('H1', 4.9457e-2) + water.add_nuclide('O16', 2.4672e-2) + water.add_nuclide('B10', 8.0042e-6) + water.add_nuclide('B11', 3.2218e-5) + water.add_s_alpha_beta('c_H_in_H2O') + + materials = openmc.Materials([fuel, water]) + +Once the ``mgxs.h5`` library file has been generated, we can then manually make +the necessary edits to the material definitions so that they load from the +multigroup library instead of defining their isotopic contents, as:: + + # Instantiate some Macroscopic Data + fuel_data = openmc.Macroscopic('UO2 (2.4%)') + water_data = openmc.Macroscopic('Hot borated water') + + # Instantiate some Materials and register the appropriate Macroscopic objects + fuel= openmc.Material(name='UO2 (2.4%)') + fuel.set_density('macro', 1.0) + fuel.add_macroscopic(fuel_data) + + water= openmc.Material(name='Hot borated water') + water.set_density('macro', 1.0) + water.add_macroscopic(water_data) + + # Instantiate a Materials collection and export to XML + materials = openmc.Materials([fuel, water]) + materials.cross_sections = "mgxs.h5" + +In the above example, our ``fuel`` and ``water`` materials will now load MGXS +data from the ``mgxs.h5`` file instead of loading continuous energy isotopic +cross section data. + -------------- Linear Sources -------------- @@ -597,9 +776,7 @@ estimator, the following code would be used: Adjoint Flux Mode ----------------- -The adjoint flux random ray solver mode can be enabled as: -entire -:: +The adjoint flux random ray solver mode can be enabled as:: settings.random_ray['adjoint'] = True diff --git a/docs/source/usersguide/variance_reduction.rst b/docs/source/usersguide/variance_reduction.rst new file mode 100644 index 000000000..124f34203 --- /dev/null +++ b/docs/source/usersguide/variance_reduction.rst @@ -0,0 +1,162 @@ +.. _variance_reduction: + +================== +Variance Reduction +================== + +Global variance reduction in OpenMC is accomplished by weight windowing +techniques. OpenMC is capable of generating weight windows using either the +MAGIC or FW-CADIS methods. Both techniques will produce a ``weight_windows.h5`` +file that can be loaded and used later on. In this section, we break down the +steps required to both generate and then apply weight windows. + +.. _ww_generator: + +------------------------------------ +Generating Weight Windows with MAGIC +------------------------------------ + +As discussed in the :ref:`methods section `, MAGIC +is an iterative method that uses flux tally information from a Monte Carlo +simulation to produce weight windows for a user-defined mesh. While generating +the weight windows, OpenMC is capable of applying the weight windows generated +from a previous batch while processing the next batch, allowing for progressive +improvement in the weight window quality across iterations. + +The typical way of generating weight windows is to define a mesh and then add an +:class:`openmc.WeightWindowGenerator` object to an :attr:`openmc.Settings` +instance, as follows:: + + # Define weight window spatial mesh + ww_mesh = openmc.RegularMesh() + ww_mesh.dimension = (10, 10, 10) + ww_mesh.lower_left = (0.0, 0.0, 0.0) + ww_mesh.upper_right = (100.0, 100.0, 100.0) + + # Create weight window object and adjust parameters + wwg = openmc.WeightWindowGenerator( + method='magic', + mesh=ww_mesh, + max_realizations=settings.batches + ) + + # Add generator to Settings instance + settings.weight_window_generators = wwg + +Notably, the :attr:`max_realizations` attribute is adjusted to the number of +batches, such that all iterations are used to refine the weight window +parameters. + +With the :class:`~openmc.WeightWindowGenerator` instance added to the +:attr:`~openmc.Settings`, the rest of the problem can be defined as normal. When +running, note that the second iteration and beyond may be several orders of +magnitude slower than the first. As the weight windows are applied in each +iteration, particles may be agressively split, resulting in a large number of +secondary (split) particles being generated per initial source particle. This is +not necessarily a bad thing, as the split particles are much more efficient at +exploring low flux regions of phase space as compared to initial particles. +Thus, even though the reported "particles/second" metric of OpenMC may be much +lower when generating (or just applying) weight windows as compared to analog +MC, it typically leads to an overall improvement in the figure of merit +accounting for the reduction in the variance. + +.. warning:: + The number of particles per batch may need to be adjusted downward + significantly to result in reasonable runtimes when weight windows are being + generated or used. + +At the end of the simulation, a ``weight_windows.h5`` file will be saved to disk +for later use. Loading it in another subsequent simulation will be discussed in +the "Using Weight Windows" section below. + +------------------------------------------------------ +Generating Weight Windows with FW-CADIS and Random Ray +------------------------------------------------------ + +Weight window generation with FW-CADIS and random ray in OpenMC uses the same +exact strategy as with MAGIC. An :class:`openmc.WeightWindowGenerator` object is +added to the :attr:`openmc.Settings` object, and a ``weight_windows.h5`` will be +generated at the end of the simulation. The only difference is that the code +must be run in random ray mode. A full description of how to enable and setup +random ray mode can be found in the :ref:`Random Ray User Guide `. + +.. note:: + It is a long term goal for OpenMC to be able to generate FW-CADIS weight + windows with only a few tweaks to an existing continuous energy Monte Carlo + input deck. However, at the present time, the workflow requires several + steps to generate multigroup cross section data and to configure the random + ray solver. A high level overview of the current workflow for generation of + weight windows with FW-CADIS using random ray is given below. + +1. Produce approximate multigroup cross section data (stored in a ``mgxs.h5`` + library). There is more information on generating multigroup cross sections + via OpenMC in the :ref:`multigroup materials ` user guide, and a + specific example of generating cross section data for use with random ray in + the :ref:`random ray MGXS guide `. + +2. Make a copy of your continuous energy Python input file. You'll edit the new + file to work in multigroup mode with random ray for producing weight windows. + +3. Adjust the material definitions in your new multigroup Python file to utilize + the multigroup cross sections instead of nuclide-wise continuous energy data. + There is a specific example of making this conversion in the :ref:`random ray + MGXS guide `. + +4. Configure OpenMC to run in random ray mode (by adding several standard random + ray input flags and settings to the :attr:`openmc.Settings.random_ray` + dictionary). More information can be found in the :ref:`Random Ray User + Guide `. + +5. Add in a :class:`~openmc.WeightWindowGenerator` in a similar manner as for + MAGIC generation with Monte Carlo and set the :attr:`method` attribute set to + ``"fw_cadis"``:: + + # Define weight window spatial mesh + ww_mesh = openmc.RegularMesh() + ww_mesh.dimension = (10, 10, 10) + ww_mesh.lower_left = (0.0, 0.0, 0.0) + ww_mesh.upper_right = (100.0, 100.0, 100.0) + + # Create weight window object and adjust parameters + wwg = openmc.WeightWindowGenerator( + method='fw_cadis', + mesh=ww_mesh, + max_realizations=settings.batches + ) + + # Add generator to openmc.settings object + settings.weight_window_generators = wwg + + +.. warning:: + If using FW-CADIS weight window generation, ensure that the selected weight + window mesh does not subdivide any cells in the problem. In the future, this + restriction is intended to be relaxed, but for now subdivision of cells by a + mesh tally will result in undefined behavior. + +6. When running your multigroup random ray input deck, OpenMC will automatically + run a forward solve followed by an adjoint solve, with a + ``weight_windows.h5`` file generated at the end. The ``weight_windows.h5`` + file will contain FW-CADIS generated weight windows. This file can be used in + identical manner as one generated with MAGIC, as described below. + +-------------------- +Using Weight Windows +-------------------- + +To use a ``weight_windows.h5`` weight window file with OpenMC's Monte Carlo +solver, the Python input just needs to load the h5 file:: + + settings.weight_window_checkpoints = {'collision': True, 'surface': True} + settings.survival_biasing = False + settings.weight_windows = openmc.hdf5_to_wws('weight_windows.h5') + settings.weight_windows_on = True + +The :class:`~openmc.WeightWindowGenerator` instance is not needed to load an +existing ``weight_windows.h5`` file. Inclusion of a +:class:`~openmc.WeightWindowGenerator` instance will cause OpenMC to generate +*new* weight windows and thus overwrite the existing ``weight_windows.h5`` file. +Weight window mesh information is embedded into the weight window file, so the +mesh does not need to be redefined. Monte Carlo solves that load a weight window +file as above will utilize weight windows to reduce the variance of the +simulation. diff --git a/include/openmc/weight_windows.h b/include/openmc/weight_windows.h index 9852fe679..2d4d55694 100644 --- a/include/openmc/weight_windows.h +++ b/include/openmc/weight_windows.h @@ -17,9 +17,7 @@ namespace openmc { -enum class WeightWindowUpdateMethod { - MAGIC, -}; +enum class WeightWindowUpdateMethod { MAGIC, FW_CADIS }; //============================================================================== // Constants @@ -127,8 +125,9 @@ public: //! \param[in] threshold Relative error threshold. Results over this //! threshold will be ignored \param[in] ratio Ratio of upper to lower //! weight window bounds - void update_magic(const Tally* tally, const std::string& value = "mean", - double threshold = 1.0, double ratio = 5.0); + void update_weights(const Tally* tally, const std::string& value = "mean", + double threshold = 1.0, double ratio = 5.0, + WeightWindowUpdateMethod method = WeightWindowUpdateMethod::MAGIC); // NOTE: This is unused for now but may be used in the future //! Write weight window settings to an HDF5 file @@ -221,12 +220,11 @@ public: void create_tally(); // Data members - int32_t tally_idx_; //!< Index of the tally used to update the weight windows - int32_t ww_idx_; //!< Index of the weight windows object being generated - std::string method_; //!< Method used to update weight window. Only "magic" - //!< is valid for now. - int32_t max_realizations_; //!< Maximum number of tally realizations - int32_t update_interval_; //!< Determines how often updates occur + int32_t tally_idx_; //!< Index of the tally used to update the weight windows + int32_t ww_idx_; //!< Index of the weight windows object being generated + WeightWindowUpdateMethod method_; //!< Method used to update weight window. + int32_t max_realizations_; //!< Maximum number of tally realizations + int32_t update_interval_; //!< Determines how often updates occur bool on_the_fly_; //!< Whether or not to keep tally results between batches or //!< realizations diff --git a/openmc/weight_windows.py b/openmc/weight_windows.py index 1ddbfe10f..0502d309f 100644 --- a/openmc/weight_windows.py +++ b/openmc/weight_windows.py @@ -660,9 +660,8 @@ class WeightWindowGenerator: maximum and minimum energy for the data available at runtime. particle_type : {'neutron', 'photon'} Particle type the weight windows apply to - method : {'magic'} - The weight window generation methodology applied during an update. Only - 'magic' is currently supported. + method : {'magic', 'fw_cadis'} + The weight window generation methodology applied during an update. max_realizations : int The upper limit for number of tally realizations when generating weight windows. @@ -680,9 +679,8 @@ class WeightWindowGenerator: energies in [eV] for a single bin particle_type : {'neutron', 'photon'} Particle type the weight windows apply to - method : {'magic'} - The weight window generation methodology applied during an update. Only - 'magic' is currently supported. + method : {'magic', 'fw_cadis'} + The weight window generation methodology applied during an update. max_realizations : int The upper limit for number of tally realizations when generating weight windows. @@ -767,7 +765,7 @@ class WeightWindowGenerator: @method.setter def method(self, m: str): cv.check_type('generation method', m, str) - cv.check_value('generation method', m, {'magic'}) + cv.check_value('generation method', m, ('magic', 'fw_cadis')) self._method = m if self._update_parameters is not None: try: @@ -800,7 +798,7 @@ class WeightWindowGenerator: return self._update_parameters def _check_update_parameters(self, params: dict): - if self.method == 'magic': + if self.method == 'magic' or self.method == 'fw_cadis': check_params = self._MAGIC_PARAMS for key, val in params.items(): @@ -843,7 +841,7 @@ class WeightWindowGenerator: update_parameters : dict The update parameters as-read from the XML node (keys: str, values: str) """ - if method == 'magic': + if method == 'magic' or method == 'fw_cadis': check_params = cls._MAGIC_PARAMS for param, param_type in check_params.items(): diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index a9180c68e..a8e6d1b82 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -173,6 +173,7 @@ void validate_random_ray_inputs() case FilterType::MATERIAL: case FilterType::MESH: case FilterType::UNIVERSE: + case FilterType::PARTICLE: break; default: fatal_error("Invalid filter specified. Only cell, cell_instance, " diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index 9579f71c0..19ae7c077 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -22,6 +22,7 @@ #include "openmc/particle.h" #include "openmc/particle_data.h" #include "openmc/physics_common.h" +#include "openmc/random_ray/flat_source_domain.h" #include "openmc/search.h" #include "openmc/settings.h" #include "openmc/tallies/filter_energy.h" @@ -482,8 +483,8 @@ void WeightWindows::set_bounds( upper_ww_ *= ratio; } -void WeightWindows::update_magic( - const Tally* tally, const std::string& value, double threshold, double ratio) +void WeightWindows::update_weights(const Tally* tally, const std::string& value, + double threshold, double ratio, WeightWindowUpdateMethod method) { /////////////////////////// // Setup and checks @@ -624,20 +625,44 @@ void WeightWindows::update_magic( auto mesh_vols = this->mesh()->volumes(); int e_bins = new_bounds.shape()[0]; - for (int e = 0; e < e_bins; e++) { - // select all - auto group_view = xt::view(new_bounds, e); - // divide by volume of mesh elements - for (int i = 0; i < group_view.size(); i++) { - group_view[i] /= mesh_vols[i]; + if (method == WeightWindowUpdateMethod::MAGIC) { + // If we are computing weight windows with forward fluxes derived from a + // Monte Carlo or forward random ray solve, we use the MAGIC algorithm. + for (int e = 0; e < e_bins; e++) { + // select all + auto group_view = xt::view(new_bounds, e); + + // divide by volume of mesh elements + for (int i = 0; i < group_view.size(); i++) { + group_view[i] /= mesh_vols[i]; + } + + double group_max = + *std::max_element(group_view.begin(), group_view.end()); + // normalize values in this energy group by the maximum value for this + // group + if (group_max > 0.0) + group_view /= 2.0 * group_max; + } + } else { + // If we are computing weight windows with adjoint fluxes derived from an + // adjoint random ray solve, we use the FW-CADIS algorithm. + for (int e = 0; e < e_bins; e++) { + // select all + auto group_view = xt::view(new_bounds, e); + + // divide by volume of mesh elements + for (int i = 0; i < group_view.size(); i++) { + group_view[i] /= mesh_vols[i]; + } } - double group_max = *std::max_element(group_view.begin(), group_view.end()); - // normalize values in this energy group by the maximum value for this - // group - if (group_max > 0.0) - group_view /= 2.0 * group_max; + xt::noalias(new_bounds) = 1.0 / new_bounds; + + auto max_val = xt::amax(new_bounds)(); + + xt::noalias(new_bounds) = new_bounds / (2.0 * max_val); } // make sure that values where the mean is zero are set s.t. the weight window @@ -760,38 +785,52 @@ WeightWindowsGenerator::WeightWindowsGenerator(pugi::xml_node node) e_bounds.push_back(data::energy_max[p_type]); } - // set method and parameters for updates - method_ = get_node_value(node, "method"); - if (method_ == "magic") { - // parse non-default update parameters if specified - if (check_for_node(node, "update_parameters")) { - pugi::xml_node params_node = node.child("update_parameters"); - if (check_for_node(params_node, "value")) - tally_value_ = get_node_value(params_node, "value"); - if (check_for_node(params_node, "threshold")) - threshold_ = std::stod(get_node_value(params_node, "threshold")); - if (check_for_node(params_node, "ratio")) { - ratio_ = std::stod(get_node_value(params_node, "ratio")); - } + // set method + std::string method_string = get_node_value(node, "method"); + if (method_string == "magic") { + method_ = WeightWindowUpdateMethod::MAGIC; + if (settings::solver_type == SolverType::RANDOM_RAY && + FlatSourceDomain::adjoint_) { + fatal_error("Random ray weight window generation with MAGIC cannot be " + "done in adjoint mode."); } - // check update parameter values - if (tally_value_ != "mean" && tally_value_ != "rel_err") { - fatal_error(fmt::format("Unsupported tally value '{}' specified for " - "weight window generation.", - tally_value_)); + } else if (method_string == "fw_cadis") { + method_ = WeightWindowUpdateMethod::FW_CADIS; + if (settings::solver_type != SolverType::RANDOM_RAY) { + fatal_error("FW-CADIS can only be run in random ray solver mode."); } - if (threshold_ <= 0.0) - fatal_error(fmt::format("Invalid relative error threshold '{}' (<= 0.0) " - "specified for weight window generation", - ratio_)); - if (ratio_ <= 1.0) - fatal_error(fmt::format("Invalid weight window ratio '{}' (<= 1.0) " - "specified for weight window generation")); + FlatSourceDomain::adjoint_ = true; } else { fatal_error(fmt::format( - "Unknown weight window update method '{}' specified", method_)); + "Unknown weight window update method '{}' specified", method_string)); } + // parse non-default update parameters if specified + if (check_for_node(node, "update_parameters")) { + pugi::xml_node params_node = node.child("update_parameters"); + if (check_for_node(params_node, "value")) + tally_value_ = get_node_value(params_node, "value"); + if (check_for_node(params_node, "threshold")) + threshold_ = std::stod(get_node_value(params_node, "threshold")); + if (check_for_node(params_node, "ratio")) { + ratio_ = std::stod(get_node_value(params_node, "ratio")); + } + } + + // check update parameter values + if (tally_value_ != "mean" && tally_value_ != "rel_err") { + fatal_error(fmt::format("Unsupported tally value '{}' specified for " + "weight window generation.", + tally_value_)); + } + if (threshold_ <= 0.0) + fatal_error(fmt::format("Invalid relative error threshold '{}' (<= 0.0) " + "specified for weight window generation", + ratio_)); + if (ratio_ <= 1.0) + fatal_error(fmt::format("Invalid weight window ratio '{}' (<= 1.0) " + "specified for weight window generation")); + // create a matching weight windows object auto wws = WeightWindows::create(); ww_idx_ = wws->index(); @@ -860,7 +899,7 @@ void WeightWindowsGenerator::update() const tally->n_realizations_ % update_interval_ != 0) return; - wws->update_magic(tally, tally_value_, threshold_, ratio_); + wws->update_weights(tally, tally_value_, threshold_, ratio_, method_); // if we're not doing on the fly generation, reset the tally results once // we're done with the update @@ -944,7 +983,7 @@ extern "C" int openmc_weight_windows_update_magic(int32_t ww_idx, // get the WeightWindows object const auto& wws = variance_reduction::weight_windows.at(ww_idx); - wws->update_magic(tally, value, threshold, ratio); + wws->update_weights(tally, value, threshold, ratio); return 0; } diff --git a/tests/regression_tests/weightwindows_fw_cadis/__init__.py b/tests/regression_tests/weightwindows_fw_cadis/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/weightwindows_fw_cadis/inputs_true.dat b/tests/regression_tests/weightwindows_fw_cadis/inputs_true.dat new file mode 100644 index 000000000..586bbb120 --- /dev/null +++ b/tests/regression_tests/weightwindows_fw_cadis/inputs_true.dat @@ -0,0 +1,260 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + + 1 + neutron + 10 + 1 + true + fw_cadis + + + + 6 6 6 + 0.0 0.0 0.0 + 30.0 30.0 30.0 + + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + naive + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/weightwindows_fw_cadis/results_true.dat b/tests/regression_tests/weightwindows_fw_cadis/results_true.dat new file mode 100644 index 000000000..e39b2e239 --- /dev/null +++ b/tests/regression_tests/weightwindows_fw_cadis/results_true.dat @@ -0,0 +1,442 @@ +RegularMesh + ID = 1 + Name = + Dimensions = 3 + Voxels = [6 6 6] + Lower left = [0. 0. 0.] + Upper Right = [np.float64(30.0), np.float64(30.0), np.float64(30.0)] + Width = [5. 5. 5.] +Lower Bounds +1.50e-01 +1.54e-01 +1.43e-01 +1.30e-01 +1.30e-01 +1.97e-01 +1.46e-01 +1.25e-01 +1.34e-01 +1.32e-01 +1.27e-01 +3.40e-01 +1.36e-01 +1.13e-01 +1.09e-01 +1.19e-01 +1.25e-01 +3.08e-01 +1.38e-01 +1.18e-01 +9.49e-02 +1.19e-01 +1.35e-01 +1.96e-01 +1.12e-01 +9.60e-02 +9.33e-02 +9.74e-02 +1.37e-01 +2.76e-01 +4.53e-02 +1.31e-01 +2.47e-01 +7.36e-02 +1.72e-01 +1.38e-01 +1.55e-01 +1.54e-01 +1.48e-01 +1.36e-01 +1.29e-01 +1.88e-01 +1.40e-01 +1.51e-01 +1.28e-01 +1.18e-01 +1.29e-01 +2.86e-01 +1.42e-01 +1.26e-01 +1.28e-01 +1.16e-01 +1.22e-01 +2.81e-01 +1.38e-01 +1.22e-01 +1.04e-01 +9.81e-02 +1.08e-01 +1.97e-01 +1.34e-01 +1.32e-01 +1.10e-01 +8.69e-02 +1.03e-01 +1.33e-01 +2.51e-01 +3.39e-01 +1.13e-01 +6.92e-02 +1.54e-01 +5.71e-02 +1.60e-01 +1.52e-01 +1.34e-01 +1.32e-01 +1.28e-01 +2.53e-01 +1.33e-01 +1.35e-01 +1.15e-01 +1.32e-01 +1.37e-01 +3.13e-01 +1.27e-01 +1.36e-01 +1.35e-01 +1.10e-01 +1.08e-01 +8.81e-02 +1.51e-01 +1.32e-01 +1.13e-01 +9.89e-02 +9.25e-02 +5.04e-02 +1.40e-01 +1.56e-01 +1.40e-01 +9.19e-02 +6.54e-02 +2.05e-02 +3.35e-01 +1.88e-01 +3.16e-01 +1.80e-01 +5.78e-02 +5.36e-03 +1.43e-01 +1.35e-01 +1.47e-01 +1.33e-01 +1.47e-01 +4.95e-01 +1.42e-01 +1.45e-01 +1.27e-01 +1.31e-01 +1.26e-01 +1.43e-01 +1.49e-01 +1.23e-01 +1.24e-01 +1.13e-01 +9.69e-02 +1.07e-01 +1.65e-01 +1.50e-01 +1.24e-01 +1.07e-01 +8.90e-02 +8.81e-02 +1.48e-01 +1.58e-01 +1.20e-01 +7.93e-02 +7.19e-02 +3.94e-02 +2.29e-01 +1.95e-01 +2.66e-01 +1.51e-01 +3.52e-02 +3.68e-03 +1.48e-01 +1.18e-01 +1.47e-01 +1.41e-01 +1.37e-01 +2.71e-01 +1.70e-01 +1.70e-01 +1.29e-01 +1.33e-01 +1.28e-01 +2.79e-01 +1.84e-01 +1.42e-01 +1.18e-01 +1.25e-01 +1.26e-01 +1.64e-01 +1.73e-01 +1.37e-01 +1.12e-01 +9.01e-02 +1.13e-01 +1.68e-01 +1.60e-01 +1.52e-01 +1.30e-01 +8.71e-02 +7.72e-02 +3.76e-02 +1.57e-01 +9.31e-02 +1.63e-01 +6.80e-02 +6.22e-02 +3.37e-03 +2.47e-01 +2.17e-01 +1.48e-01 +2.01e-01 +1.54e-01 +5.69e-02 +2.89e-01 +3.59e-01 +1.17e-01 +8.64e-02 +3.34e-01 +1.50e-01 +5.00e-01 +2.16e-01 +1.22e-01 +1.66e-01 +1.44e-01 +4.98e-02 +4.28e-01 +1.17e-01 +8.73e-02 +1.11e-01 +1.09e-01 +9.69e-02 +3.26e-01 +2.28e-01 +2.49e-01 +3.55e-02 +1.69e-02 +3.95e-03 +1.03e-01 +4.89e-02 +1.90e-01 +4.61e-02 +9.75e-03 +3.00e-04 +Upper Bounds +7.49e-01 +7.71e-01 +7.13e-01 +6.51e-01 +6.48e-01 +9.85e-01 +7.31e-01 +6.26e-01 +6.69e-01 +6.59e-01 +6.33e-01 +1.70e+00 +6.82e-01 +5.67e-01 +5.45e-01 +5.94e-01 +6.24e-01 +1.54e+00 +6.92e-01 +5.91e-01 +4.75e-01 +5.96e-01 +6.77e-01 +9.82e-01 +5.59e-01 +4.80e-01 +4.67e-01 +4.87e-01 +6.84e-01 +1.38e+00 +2.26e-01 +6.55e-01 +1.24e+00 +3.68e-01 +8.59e-01 +6.88e-01 +7.73e-01 +7.70e-01 +7.42e-01 +6.78e-01 +6.46e-01 +9.40e-01 +7.00e-01 +7.57e-01 +6.39e-01 +5.92e-01 +6.43e-01 +1.43e+00 +7.12e-01 +6.29e-01 +6.40e-01 +5.78e-01 +6.09e-01 +1.40e+00 +6.90e-01 +6.10e-01 +5.21e-01 +4.91e-01 +5.42e-01 +9.87e-01 +6.71e-01 +6.60e-01 +5.52e-01 +4.35e-01 +5.14e-01 +6.65e-01 +1.26e+00 +1.70e+00 +5.63e-01 +3.46e-01 +7.72e-01 +2.86e-01 +8.00e-01 +7.58e-01 +6.70e-01 +6.59e-01 +6.41e-01 +1.26e+00 +6.65e-01 +6.76e-01 +5.76e-01 +6.60e-01 +6.86e-01 +1.57e+00 +6.35e-01 +6.81e-01 +6.75e-01 +5.51e-01 +5.39e-01 +4.40e-01 +7.56e-01 +6.62e-01 +5.67e-01 +4.95e-01 +4.63e-01 +2.52e-01 +6.98e-01 +7.78e-01 +6.99e-01 +4.59e-01 +3.27e-01 +1.02e-01 +1.67e+00 +9.38e-01 +1.58e+00 +9.00e-01 +2.89e-01 +2.68e-02 +7.16e-01 +6.75e-01 +7.35e-01 +6.63e-01 +7.34e-01 +2.47e+00 +7.11e-01 +7.23e-01 +6.34e-01 +6.54e-01 +6.32e-01 +7.14e-01 +7.45e-01 +6.15e-01 +6.19e-01 +5.64e-01 +4.85e-01 +5.37e-01 +8.26e-01 +7.51e-01 +6.21e-01 +5.36e-01 +4.45e-01 +4.40e-01 +7.40e-01 +7.92e-01 +5.99e-01 +3.96e-01 +3.60e-01 +1.97e-01 +1.14e+00 +9.76e-01 +1.33e+00 +7.55e-01 +1.76e-01 +1.84e-02 +7.42e-01 +5.90e-01 +7.37e-01 +7.07e-01 +6.84e-01 +1.36e+00 +8.52e-01 +8.49e-01 +6.46e-01 +6.63e-01 +6.42e-01 +1.40e+00 +9.19e-01 +7.08e-01 +5.89e-01 +6.24e-01 +6.28e-01 +8.21e-01 +8.64e-01 +6.87e-01 +5.58e-01 +4.50e-01 +5.64e-01 +8.40e-01 +7.99e-01 +7.62e-01 +6.51e-01 +4.36e-01 +3.86e-01 +1.88e-01 +7.85e-01 +4.66e-01 +8.13e-01 +3.40e-01 +3.11e-01 +1.68e-02 +1.24e+00 +1.09e+00 +7.40e-01 +1.00e+00 +7.72e-01 +2.85e-01 +1.44e+00 +1.79e+00 +5.85e-01 +4.32e-01 +1.67e+00 +7.50e-01 +2.50e+00 +1.08e+00 +6.10e-01 +8.32e-01 +7.19e-01 +2.49e-01 +2.14e+00 +5.87e-01 +4.36e-01 +5.57e-01 +5.47e-01 +4.84e-01 +1.63e+00 +1.14e+00 +1.25e+00 +1.78e-01 +8.43e-02 +1.97e-02 +5.17e-01 +2.44e-01 +9.49e-01 +2.31e-01 +4.87e-02 +1.50e-03 \ No newline at end of file diff --git a/tests/regression_tests/weightwindows_fw_cadis/test.py b/tests/regression_tests/weightwindows_fw_cadis/test.py new file mode 100644 index 000000000..7a4718ed5 --- /dev/null +++ b/tests/regression_tests/weightwindows_fw_cadis/test.py @@ -0,0 +1,33 @@ +import os + +import openmc +from openmc.examples import random_ray_three_region_cube + +from tests.testing_harness import WeightWindowPyAPITestHarness + + +class MGXSTestHarness(WeightWindowPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + + +def test_random_ray_adjoint_fixed_source(): + model = random_ray_three_region_cube() + + ww_mesh = openmc.RegularMesh() + n = 6 + width = 30.0 + ww_mesh.dimension = (n, n, n) + ww_mesh.lower_left = (0.0, 0.0, 0.0) + ww_mesh.upper_right = (width, width, width) + + wwg = openmc.WeightWindowGenerator( + method="fw_cadis", mesh=ww_mesh, max_realizations=model.settings.batches) + model.settings.weight_window_generators = wwg + model.settings.random_ray['volume_estimator'] = 'naive' + + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/testing_harness.py b/tests/testing_harness.py index 81527452b..ddae89e02 100644 --- a/tests/testing_harness.py +++ b/tests/testing_harness.py @@ -441,6 +441,40 @@ class TolerantPyAPITestHarness(PyAPITestHarness): assert compare, 'Results do not agree' +class WeightWindowPyAPITestHarness(PyAPITestHarness): + def _get_results(self): + """Digest info in the weight window file and return as a string.""" + ww = openmc.hdf5_to_wws()[0] + + # Access the weight window bounds + lower_bound = ww.lower_ww_bounds + upper_bound = ww.upper_ww_bounds + + # Flatten both arrays + flattened_lower_bound = lower_bound.flatten() + flattened_upper_bound = upper_bound.flatten() + + # Convert each element to a string in scientific notation with 2 decimal places + formatted_lower_bound = [f'{x:.2e}' for x in flattened_lower_bound] + formatted_upper_bound = [f'{x:.2e}' for x in flattened_upper_bound] + + # Concatenate the formatted arrays + concatenated_strings = ["Lower Bounds"] + formatted_lower_bound + \ + ["Upper Bounds"] + formatted_upper_bound + + # Join the concatenated strings into a single string with newline characters + final_string = '\n'.join(concatenated_strings) + + # Prepend the mesh text description and return final string + return str(ww.mesh) + final_string + + def _cleanup(self): + super()._cleanup() + f = 'weight_windows.h5' + if os.path.exists(f): + os.remove(f) + + class PlotTestHarness(TestHarness): """Specialized TestHarness for running OpenMC plotting tests.""" def __init__(self, plot_names, voxel_convert_checks=[]): From 8626ce5c4352634d18d62ab328448abfa99bdc02 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Mon, 27 Jan 2025 23:31:59 -0800 Subject: [PATCH 243/671] Rely on std::filesystem for file_utils (#3042) Co-authored-by: Andrew Johnson Co-authored-by: Paul Romano --- include/openmc/file_utils.h | 8 +++- src/cross_sections.cpp | 25 +++++----- src/dagmc.cpp | 4 +- src/file_utils.cpp | 61 ++++++++---------------- src/simulation.cpp | 3 +- src/state_point.cpp | 10 ++-- tests/cpp_unit_tests/test_file_utils.cpp | 9 ++++ 7 files changed, 53 insertions(+), 67 deletions(-) diff --git a/include/openmc/file_utils.h b/include/openmc/file_utils.h index 6e9812d5e..db6564034 100644 --- a/include/openmc/file_utils.h +++ b/include/openmc/file_utils.h @@ -5,7 +5,10 @@ namespace openmc { -// TODO: replace with std::filesystem when switch to C++17 is made +// NOTE: This is a thin wrapper over std::filesystem because we +// pass strings around a lot. Objects like settings::path_input +// are extern std::string to play with other libraries and languages + //! Determine if a path is a directory //! \param[in] path Path to check //! \return Whether the path is a directory @@ -18,7 +21,8 @@ bool file_exists(const std::string& filename); //! Determine directory containing given file //! \param[in] filename Path to file -//! \return Name of directory containing file +//! \return Name of directory containing file excluding the final directory +//! separator std::string dir_name(const std::string& filename); // Gets the file extension of whatever string is passed in. This is defined as diff --git a/src/cross_sections.cpp b/src/cross_sections.cpp index 248ae9019..b1bfde03d 100644 --- a/src/cross_sections.cpp +++ b/src/cross_sections.cpp @@ -23,6 +23,7 @@ #include "pugixml.hpp" #include // for getenv +#include #include namespace openmc { @@ -282,15 +283,16 @@ void read_ce_cross_sections(const vector>& nuc_temps, void read_ce_cross_sections_xml() { // Check if cross_sections.xml exists - const auto& filename = settings::path_cross_sections; - if (dir_exists(filename)) { + std::filesystem::path filename(settings::path_cross_sections); + if (!std::filesystem::exists(filename)) { + fatal_error( + "Cross sections XML file '" + filename.string() + "' does not exist."); + } + + if (std::filesystem::is_directory(filename)) { fatal_error("OPENMC_CROSS_SECTIONS is set to a directory. " "It should be set to an XML file."); } - if (!file_exists(filename)) { - // Could not find cross_sections.xml file - fatal_error("Cross sections XML file '" + filename + "' does not exist."); - } write_message("Reading cross sections XML file...", 5); @@ -309,15 +311,10 @@ void read_ce_cross_sections_xml() } else { // If no directory is listed in cross_sections.xml, by default select the // directory in which the cross_sections.xml file resides - - // TODO: Use std::filesystem functionality when C++17 is adopted - auto pos = filename.rfind("/"); - if (pos == std::string::npos) { - // No '\\' found, so the file must be in the same directory as - // materials.xml - directory = settings::path_input; + if (filename.has_parent_path()) { + directory = filename.parent_path().string(); } else { - directory = filename.substr(0, pos); + directory = settings::path_input; } } diff --git a/src/dagmc.cpp b/src/dagmc.cpp index 94c220850..52c45f21c 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -56,7 +57,8 @@ DAGUniverse::DAGUniverse(pugi::xml_node node) if (check_for_node(node, "filename")) { filename_ = get_node_value(node, "filename"); if (!starts_with(filename_, "/")) { - filename_ = dir_name(settings::path_input) + filename_; + std::filesystem::path d(dir_name(settings::path_input)); + filename_ = (d / filename_).string(); } } else { fatal_error("Must specify a file for the DAGMC universe"); diff --git a/src/file_utils.cpp b/src/file_utils.cpp index 197c9767c..517f82d15 100644 --- a/src/file_utils.cpp +++ b/src/file_utils.cpp @@ -1,65 +1,42 @@ #include "openmc/file_utils.h" -#include // any_of -#include // for isalpha -#include // for ifstream -#include +#include namespace openmc { bool dir_exists(const std::string& path) { - struct stat s; - if (stat(path.c_str(), &s) != 0) - return false; - - return s.st_mode & S_IFDIR; + std::filesystem::path d(path); + return std::filesystem::is_directory(d); } bool file_exists(const std::string& filename) { - // rule out file being a path to a directory - if (dir_exists(filename)) + std::filesystem::path p(filename); + if (!std::filesystem::exists(p)) { return false; - - std::ifstream s {filename}; - return s.good(); + } + if (std::filesystem::is_directory(p)) { + return false; + } + return true; } std::string dir_name(const std::string& filename) { - size_t pos = filename.find_last_of("\\/"); - return (std::string::npos == pos) ? "" : filename.substr(0, pos + 1); + std::filesystem::path p(filename); + return (p.parent_path()).string(); } std::string get_file_extension(const std::string& filename) { - // try our best to work on windows... -#if defined(_WIN32) || defined(_WIN64) - const char sep_char = '\\'; -#else - const char sep_char = '/'; -#endif - - // check that at least one letter is present - const auto last_period_pos = filename.find_last_of('.'); - const auto last_sep_pos = filename.find_last_of(sep_char); - - // no file extension. In the first case, we are only given - // a file name. In the second, we have been given a file path. - // If that's the case, periods are allowed in directory names, - // but have the interpretation as preceding a file extension - // after the last separator. - if (last_period_pos == std::string::npos || - (last_sep_pos < std::string::npos && last_period_pos < last_sep_pos)) - return ""; - - const std::string ending = filename.substr(last_period_pos + 1); - - // check that at least one character is present. - const bool has_alpha = std::any_of(ending.begin(), ending.end(), - [](char x) { return static_cast(std::isalpha(x)); }); - return has_alpha ? ending : ""; + std::filesystem::path p(filename); + auto ext = p.extension(); + if (!ext.empty()) { + // path::extension includes the period + return ext.string().substr(1); + } + return ""; } } // namespace openmc diff --git a/src/simulation.cpp b/src/simulation.cpp index 09b376473..030e447f1 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -446,10 +446,9 @@ void finalize_batch() // Write a continously-overwritten source point if requested. if (settings::source_latest) { - // note: correct file extension appended automatically auto filename = settings::path_output + "source"; gsl::span bankspan(simulation::source_bank); - write_source_point(filename.c_str(), bankspan, simulation::work_index, + write_source_point(filename, bankspan, simulation::work_index, settings::source_mcpl_write); } } diff --git a/src/state_point.cpp b/src/state_point.cpp index adc026fa3..fcc389df1 100644 --- a/src/state_point.cpp +++ b/src/state_point.cpp @@ -52,9 +52,7 @@ extern "C" int openmc_statepoint_write(const char* filename, bool* write_source) // If a file name was specified, ensure it has .h5 file extension const auto extension = get_file_extension(filename_); - if (extension == "") { - filename_.append(".h5"); - } else if (extension != "h5") { + if (extension != "h5") { warning("openmc_statepoint_write was passed a file extension differing " "from .h5, but an hdf5 file will be written."); } @@ -578,8 +576,10 @@ void write_source_point(std::string filename, gsl::span source_bank, // Dispatch to appropriate function based on file type if (use_mcpl) { + filename.append(".mcpl"); write_mcpl_source_point(filename.c_str(), source_bank, bank_index); } else { + filename.append(".h5"); write_h5_source_point(filename.c_str(), source_bank, bank_index); } } @@ -602,9 +602,7 @@ void write_h5_source_point(const char* filename, std::string filename_(filename); const auto extension = get_file_extension(filename_); - if (extension == "") { - filename_.append(".h5"); - } else if (extension != "h5") { + if (extension != "h5") { warning("write_source_point was passed a file extension differing " "from .h5, but an hdf5 file will be written."); } diff --git a/tests/cpp_unit_tests/test_file_utils.cpp b/tests/cpp_unit_tests/test_file_utils.cpp index 3b7a74346..8b0d99d76 100644 --- a/tests/cpp_unit_tests/test_file_utils.cpp +++ b/tests/cpp_unit_tests/test_file_utils.cpp @@ -30,3 +30,12 @@ TEST_CASE("Test file_exists") // Note: not clear how to portably test where a file should exist. REQUIRE(!file_exists("./should_not_exist/really_do_not_make_this_please")); } + +TEST_CASE("Test dir_name") +{ + REQUIRE(dir_name("") == ""); + REQUIRE(dir_name("/") == "/"); + REQUIRE(dir_name("hello") == ""); + REQUIRE(dir_name("hello/world") == "hello"); + REQUIRE(dir_name("/path/to/dir/") == "/path/to/dir"); +} From 27f3afefa472567304588edfb6fe18ab8362aa9e Mon Sep 17 00:00:00 2001 From: Skywalker <49872736+cn-skywalker@users.noreply.github.com> Date: Tue, 28 Jan 2025 15:57:27 +0800 Subject: [PATCH 244/671] Fix the bug in the Material.from_xml_element function (#3278) Co-authored-by: Paul Romano --- openmc/material.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openmc/material.py b/openmc/material.py index a6401216a..343b2fff2 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -1582,7 +1582,6 @@ class Material(IDManagerMixin): if 'volume' in elem.attrib: mat.volume = float(elem.get('volume')) - mat.depletable = bool(elem.get('depletable')) # Get each nuclide for nuclide in elem.findall('nuclide'): @@ -1592,6 +1591,9 @@ class Material(IDManagerMixin): elif 'wo' in nuclide.attrib: mat.add_nuclide(name, float(nuclide.attrib['wo']), 'wo') + # Get depletable attribute + mat.depletable = elem.get('depletable') in ('true', '1') + # Get each S(a,b) table for sab in elem.findall('sab'): fraction = float(sab.get('fraction', 1.0)) From 860d739f4c30c0a59885caad0534d4fee293f53b Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Tue, 28 Jan 2025 17:44:32 +0100 Subject: [PATCH 245/671] Doc typo fix for rand ray mgxs (#3280) Co-authored-by: Jon Shimwell --- docs/source/usersguide/random_ray.rst | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index b6852b7ca..b797a7216 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -111,7 +111,7 @@ detector from the core. In this case, rays sampled in the moderator region and heading toward the detector will begin life with a highly scattered thermal spectrum and will have an inaccurate fast spectrum. If the dead zone length is only 20 cm, we might imagine such rays writing to the detector tally within -their active lengths, despite their innaccurate estimate of the uncollided fast +their active lengths, despite their inaccurate estimate of the uncollided fast angular flux. Thus, an inactive length of 100--200 cm would ensure that any such rays would still be within their inactive regions, and only rays that have actually traversed through the core (and thus have an accurate representation of @@ -487,7 +487,7 @@ two group energy decomposition:: mgxs_lib = openmc.mgxs.Library(geometry) # Pick energy group structure - groups = mgxs.EnergyGroups(mgxs.GROUP_STRUCTURES['CASMO-2']) + groups = openmc.mgxs.EnergyGroups(openmc.mgxs.GROUP_STRUCTURES['CASMO-2']) mgxs_lib.energy_groups = groups # Disable transport correction @@ -501,7 +501,7 @@ two group energy decomposition:: mgxs_lib.domain_type = "material" # Specify the cell domains over which to compute multi-group cross sections - mgxs_lib.domains = geom.get_all_materials().values() + mgxs_lib.domains = geometry.get_all_materials().values() # Do not compute cross sections on a nuclide-by-nuclide basis mgxs_lib.by_nuclide = False @@ -531,7 +531,6 @@ a statepoint file (e.g., ``statepoint.100.h5``) file and summary file (e.g., ``summary.h5``) that resulted from running our previous example:: import openmc - import openmc.mgxs as mgxs summary = openmc.Summary('summary.h5') geom = summary.geometry @@ -540,7 +539,7 @@ a statepoint file (e.g., ``statepoint.100.h5``) file and summary file (e.g., statepoint_filename = 'statepoint.100.h5' sp = openmc.StatePoint(statepoint_filename) - groups = mgxs.EnergyGroups(mgxs.GROUP_STRUCTURES['CASMO-2']) + groups = openmc.mgxs.EnergyGroups(openmc.mgxs.GROUP_STRUCTURES['CASMO-2']) mgxs_lib = openmc.mgxs.Library(geom) mgxs_lib.energy_groups = groups mgxs_lib.correction = None From d9c8e594c76f7f99b6acf5027de93ccfd26173e9 Mon Sep 17 00:00:00 2001 From: Skywalker <49872736+cn-skywalker@users.noreply.github.com> Date: Wed, 29 Jan 2025 03:44:04 +0800 Subject: [PATCH 246/671] fix the bug in function differentiate_mats() (#3277) Co-authored-by: Paul Romano --- openmc/model/model.py | 27 ++++++++++++++----- .../test_deplete_coupled_operator.py | 4 +-- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index 80859c55d..c8329c2fb 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1199,7 +1199,7 @@ class Model: diff_volume_method : str Specifies how the volumes of the new materials should be found. - None: Do not assign volumes to the new materials (Default) - - 'divide_equally': Divide the original material volume equally between the new materials + - 'divide equally': Divide the original material volume equally between the new materials - 'match cell': Set the volume of the material to the volume of the cell they fill """ self.differentiate_mats(diff_volume_method, depletable_only=True) @@ -1214,7 +1214,7 @@ class Model: diff_volume_method : str Specifies how the volumes of the new materials should be found. - None: Do not assign volumes to the new materials (Default) - - 'divide_equally': Divide the original material volume equally between the new materials + - 'divide equally': Divide the original material volume equally between the new materials - 'match cell': Set the volume of the material to the volume of the cell they fill depletable_only : bool Default is True, only depletable materials will be differentiated. If False, all materials will be @@ -1225,9 +1225,15 @@ class Model: # Count the number of instances for each cell and material self.geometry.determine_paths(instances_only=True) + # Get list of materials + if self.materials: + materials = self.materials + else: + materials = list(self.geometry.get_all_materials().values()) + # Find all or depletable_only materials which have multiple instance distribmats = set() - for mat in self.materials: + for mat in materials: # Differentiate all materials with multiple instances diff_mat = mat.num_instances > 1 # If depletable_only is True, differentiate only depletable materials @@ -1259,11 +1265,20 @@ class Model: for cell in self.geometry.get_all_material_cells().values(): if cell.fill in distribmats: mat = cell.fill - if diff_volume_method != 'match cell': + + # Clone materials + if cell.num_instances > 1: cell.fill = [mat.clone() for _ in range(cell.num_instances)] - elif diff_volume_method == 'match cell': + else: cell.fill = mat.clone() - cell.fill.volume = cell.volume + + # For 'match cell', assign volumes based on the cells + if diff_volume_method == 'match cell': + if cell.fill_type == 'distribmat': + for clone_mat in cell.fill: + clone_mat.volume = cell.volume + else: + cell.fill.volume = cell.volume if self.materials is not None: self.materials = openmc.Materials( diff --git a/tests/unit_tests/test_deplete_coupled_operator.py b/tests/unit_tests/test_deplete_coupled_operator.py index fe79d621b..ba291e07d 100644 --- a/tests/unit_tests/test_deplete_coupled_operator.py +++ b/tests/unit_tests/test_deplete_coupled_operator.py @@ -114,7 +114,7 @@ def test_diff_volume_method_divide_equally(model_with_volumes): ) all_cells = list(operator.model.geometry.get_all_cells().values()) - assert all_cells[0].fill[0].volume == 51 - assert all_cells[1].fill[0].volume == 51 + assert all_cells[0].fill.volume == 51 + assert all_cells[1].fill.volume == 51 # mat2 is not depletable assert all_cells[2].fill.volume is None From 59c398be84fa5e88279dc5b980f4a79c6f57a951 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 31 Jan 2025 10:12:24 -0600 Subject: [PATCH 247/671] Consolidate plotting capabilities in Model.plot (#3282) Co-authored-by: Jonathan Shimwell --- openmc/cell.py | 56 +--------- openmc/geometry.py | 55 +--------- openmc/model/model.py | 237 ++++++++++++++++++++++++++++++++--------- openmc/plots.py | 85 +++++++++++++++ openmc/region.py | 39 +------ openmc/universe.py | 240 +++--------------------------------------- 6 files changed, 289 insertions(+), 423 deletions(-) diff --git a/openmc/cell.py b/openmc/cell.py index 2e0f2ff13..cd0573e8b 100644 --- a/openmc/cell.py +++ b/openmc/cell.py @@ -10,6 +10,7 @@ import openmc import openmc.checkvalue as cv from ._xml import get_text from .mixin import IDManagerMixin +from .plots import add_plot_params from .region import Region, Complement from .surface import Halfspace from .bounding_box import BoundingBox @@ -559,64 +560,11 @@ class Cell(IDManagerMixin): return memo[self] + @add_plot_params def plot(self, *args, **kwargs): """Display a slice plot of the cell. .. versionadded:: 0.14.0 - - Parameters - ---------- - origin : iterable of float - Coordinates at the origin of the plot. If left as None then the - bounding box center will be used to attempt to ascertain the origin. - Defaults to (0, 0, 0) if the bounding box is not finite - width : iterable of float - Width of the plot in each basis direction. If left as none then the - bounding box width will be used to attempt to ascertain the plot - width. Defaults to (10, 10) if the bounding box is not finite - pixels : Iterable of int or int - If iterable of ints provided, then this directly sets the number of - pixels to use in each basis direction. If int provided, then this - sets the total number of pixels in the plot and the number of pixels - in each basis direction is calculated from this total and the image - aspect ratio. - basis : {'xy', 'xz', 'yz'} - The basis directions for the plot - color_by : {'cell', 'material'} - Indicate whether the plot should be colored by cell or by material - colors : dict - Assigns colors to specific materials or cells. Keys are instances of - :class:`Cell` or :class:`Material` and values are RGB 3-tuples, RGBA - 4-tuples, or strings indicating SVG color names. Red, green, blue, - and alpha should all be floats in the range [0.0, 1.0], for example: - - .. code-block:: python - - # Make water blue - water = openmc.Cell(fill=h2o) - water.plot(colors={water: (0., 0., 1.)}) - seed : int - Seed for the random number generator - openmc_exec : str - Path to OpenMC executable. - axes : matplotlib.Axes - Axes to draw to - legend : bool - Whether a legend showing material or cell names should be drawn - legend_kwargs : dict - Keyword arguments passed to :func:`matplotlib.pyplot.legend`. - outline : bool - Whether outlines between color boundaries should be drawn - axis_units : {'km', 'm', 'cm', 'mm'} - Units used on the plot axis - **kwargs - Keyword arguments passed to :func:`matplotlib.pyplot.imshow` - - Returns - ------- - matplotlib.axes.Axes - Axes containing resulting image - """ # Create dummy universe but preserve used_ids next_id = openmc.Universe.next_id diff --git a/openmc/geometry.py b/openmc/geometry.py index 28e1e48ec..c069d5796 100644 --- a/openmc/geometry.py +++ b/openmc/geometry.py @@ -9,6 +9,7 @@ import lxml.etree as ET import openmc import openmc._xml as xml +from .plots import add_plot_params from .checkvalue import check_type, check_less_than, check_greater_than, PathLike @@ -747,62 +748,10 @@ class Geometry: clone.root_universe = self.root_universe.clone() return clone + @add_plot_params def plot(self, *args, **kwargs): """Display a slice plot of the geometry. .. versionadded:: 0.14.0 - - Parameters - ---------- - origin : iterable of float - Coordinates at the origin of the plot. If left as None then the - bounding box center will be used to attempt to ascertain the origin. - Defaults to (0, 0, 0) if the bounding box is not finite - width : iterable of float - Width of the plot in each basis direction. If left as none then the - bounding box width will be used to attempt to ascertain the plot - width. Defaults to (10, 10) if the bounding box is not finite - pixels : Iterable of int or int - If iterable of ints provided, then this directly sets the number of - pixels to use in each basis direction. If int provided, then this - sets the total number of pixels in the plot and the number of pixels - in each basis direction is calculated from this total and the image - aspect ratio. - basis : {'xy', 'xz', 'yz'} - The basis directions for the plot - color_by : {'cell', 'material'} - Indicate whether the plot should be colored by cell or by material - colors : dict - Assigns colors to specific materials or cells. Keys are instances of - :class:`Cell` or :class:`Material` and values are RGB 3-tuples, RGBA - 4-tuples, or strings indicating SVG color names. Red, green, blue, - and alpha should all be floats in the range [0.0, 1.0], for - example:: - - # Make water blue - water = openmc.Cell(fill=h2o) - universe.plot(..., colors={water: (0., 0., 1.)) - seed : int - Seed for the random number generator - openmc_exec : str - Path to OpenMC executable. - axes : matplotlib.Axes - Axes to draw to - legend : bool - Whether a legend showing material or cell names should be drawn - legend_kwargs : dict - Keyword arguments passed to :func:`matplotlib.pyplot.legend`. - outline : bool - Whether outlines between color boundaries should be drawn - axis_units : {'km', 'm', 'cm', 'mm'} - Units used on the plot axis - **kwargs - Keyword arguments passed to :func:`matplotlib.pyplot.imshow` - - Returns - ------- - matplotlib.axes.Axes - Axes containing resulting image """ - return self.root_universe.plot(*args, **kwargs) diff --git a/openmc/model/model.py b/openmc/model/model.py index c8329c2fb..8dd13ef6b 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1,9 +1,10 @@ from __future__ import annotations -from collections.abc import Iterable +from collections.abc import Iterable, Sequence from functools import lru_cache from pathlib import Path -from numbers import Integral -from tempfile import NamedTemporaryFile +import math +from numbers import Integral, Real +from tempfile import NamedTemporaryFile, TemporaryDirectory import warnings import h5py @@ -16,6 +17,7 @@ from openmc.dummy_comm import DummyCommunicator from openmc.executor import _process_CLI_arguments from openmc.checkvalue import check_type, check_value, PathLike from openmc.exceptions import InvalidIDError +from openmc.plots import add_plot_params from openmc.utility_funcs import change_directory @@ -130,6 +132,10 @@ class Model: for plot in plots: self._plots.append(plot) + @property + def bounding_box(self) -> openmc.BoundingBox: + return self.geometry.bounding_box + @property def is_initialized(self) -> bool: try: @@ -827,78 +833,207 @@ class Model: openmc.lib.materials[domain_id].volume = \ vol_calc.volumes[domain_id].n + @add_plot_params def plot( self, + origin: Sequence[float] | None = None, + width: Sequence[float] | None = None, + pixels: int | Sequence[int] = 40000, + basis: str = 'xy', + color_by: str = 'cell', + colors: dict | None = None, + seed: int | None = None, + openmc_exec: PathLike = 'openmc', + axes=None, + legend: bool = False, + axis_units: str = 'cm', + outline: bool | str = False, n_samples: int | None = None, plane_tolerance: float = 1., + legend_kwargs: dict | None = None, source_kwargs: dict | None = None, + contour_kwargs: dict | None = None, **kwargs, ): - """Display a slice plot of the geometry. + """Display a slice plot of the model. .. versionadded:: 0.15.1 - - Parameters - ---------- - n_samples : int, optional - The number of source particles to sample and add to plot. Defaults - to None which doesn't plot any particles on the plot. - plane_tolerance: float - When plotting a plane the source locations within the plane +/- - the plane_tolerance will be included and those outside of the - plane_tolerance will not be shown - source_kwargs : dict, optional - Keyword arguments passed to :func:`matplotlib.pyplot.scatter`. - **kwargs - Keyword arguments passed to :func:`openmc.Universe.plot` - - Returns - ------- - matplotlib.axes.Axes - Axes containing resulting image """ + import matplotlib.image as mpimg + import matplotlib.patches as mpatches + import matplotlib.pyplot as plt check_type('n_samples', n_samples, int | None) - check_type('plane_tolerance', plane_tolerance, float) + check_type('plane_tolerance', plane_tolerance, Real) + if legend_kwargs is None: + legend_kwargs = {} + legend_kwargs.setdefault('bbox_to_anchor', (1.05, 1)) + legend_kwargs.setdefault('loc', 2) + legend_kwargs.setdefault('borderaxespad', 0.0) if source_kwargs is None: source_kwargs = {} source_kwargs.setdefault('marker', 'x') - ax = self.geometry.plot(**kwargs) + # Determine extents of plot + if basis == 'xy': + x, y, z = 0, 1, 2 + xlabel, ylabel = f'x [{axis_units}]', f'y [{axis_units}]' + elif basis == 'yz': + x, y, z = 1, 2, 0 + xlabel, ylabel = f'y [{axis_units}]', f'z [{axis_units}]' + elif basis == 'xz': + x, y, z = 0, 2, 1 + xlabel, ylabel = f'x [{axis_units}]', f'z [{axis_units}]' + + bb = self.bounding_box + # checks to see if bounding box contains -inf or inf values + if np.isinf(bb.extent[basis]).any(): + if origin is None: + origin = (0, 0, 0) + if width is None: + width = (10, 10) + else: + if origin is None: + # if nan values in the bb.center they get replaced with 0.0 + # this happens when the bounding_box contains inf values + with warnings.catch_warnings(): + warnings.simplefilter("ignore", RuntimeWarning) + origin = np.nan_to_num(bb.center) + if width is None: + bb_width = bb.width + width = (bb_width[x], bb_width[y]) + + if isinstance(pixels, int): + aspect_ratio = width[0] / width[1] + pixels_y = math.sqrt(pixels / aspect_ratio) + pixels = (int(pixels / pixels_y), int(pixels_y)) + + axis_scaling_factor = {'km': 0.00001, 'm': 0.01, 'cm': 1, 'mm': 10} + + x_min = (origin[x] - 0.5*width[0]) * axis_scaling_factor[axis_units] + x_max = (origin[x] + 0.5*width[0]) * axis_scaling_factor[axis_units] + y_min = (origin[y] - 0.5*width[1]) * axis_scaling_factor[axis_units] + y_max = (origin[y] + 0.5*width[1]) * axis_scaling_factor[axis_units] + + with TemporaryDirectory() as tmpdir: + if seed is not None: + self.settings.plot_seed = seed + + # Create plot object matching passed arguments + plot = openmc.Plot() + plot.origin = origin + plot.width = width + plot.pixels = pixels + plot.basis = basis + plot.color_by = color_by + if colors is not None: + plot.colors = colors + self.plots.append(plot) + + # Run OpenMC in geometry plotting mode + self.plot_geometry(False, cwd=tmpdir, openmc_exec=openmc_exec) + + # Read image from file + img_path = Path(tmpdir) / f'plot_{plot.id}.png' + if not img_path.is_file(): + img_path = img_path.with_suffix('.ppm') + img = mpimg.imread(str(img_path)) + + # Create a figure sized such that the size of the axes within + # exactly matches the number of pixels specified + if axes is None: + px = 1/plt.rcParams['figure.dpi'] + fig, axes = plt.subplots() + axes.set_xlabel(xlabel) + axes.set_ylabel(ylabel) + params = fig.subplotpars + width = pixels[0]*px/(params.right - params.left) + height = pixels[1]*px/(params.top - params.bottom) + fig.set_size_inches(width, height) + + if outline: + # Combine R, G, B values into a single int + rgb = (img * 256).astype(int) + image_value = (rgb[..., 0] << 16) + \ + (rgb[..., 1] << 8) + (rgb[..., 2]) + + # Set default arguments for contour() + if contour_kwargs is None: + contour_kwargs = {} + contour_kwargs.setdefault('colors', 'k') + contour_kwargs.setdefault('linestyles', 'solid') + contour_kwargs.setdefault('algorithm', 'serial') + + axes.contour( + image_value, + origin="upper", + levels=np.unique(image_value), + extent=(x_min, x_max, y_min, y_max), + **contour_kwargs + ) + + # add legend showing which colors represent which material + # or cell if that was requested + if legend: + if plot.colors == {}: + raise ValueError("Must pass 'colors' dictionary if you " + "are adding a legend via legend=True.") + + if color_by == "cell": + expected_key_type = openmc.Cell + else: + expected_key_type = openmc.Material + + patches = [] + for key, color in plot.colors.items(): + + if isinstance(key, int): + raise TypeError( + "Cannot use IDs in colors dict for auto legend.") + elif not isinstance(key, expected_key_type): + raise TypeError( + "Color dict key type does not match color_by") + + # this works whether we're doing cells or materials + label = key.name if key.name != '' else key.id + + # matplotlib takes RGB on 0-1 scale rather than 0-255. at + # this point PlotBase has already checked that 3-tuple + # based colors are already valid, so if the length is three + # then we know it just needs to be converted to the 0-1 + # format. + if len(color) == 3 and not isinstance(color, str): + scaled_color = ( + color[0]/255, color[1]/255, color[2]/255) + else: + scaled_color = color + + key_patch = mpatches.Patch(color=scaled_color, label=label) + patches.append(key_patch) + + axes.legend(handles=patches, **legend_kwargs) + + # Plot image and return the axes + if outline != 'only': + axes.imshow(img, extent=(x_min, x_max, y_min, y_max), **kwargs) + + if n_samples: # Sample external source particles particles = self.sample_external_source(n_samples) - # Determine plotting parameters and bounding box of geometry - bbox = self.geometry.bounding_box - origin = kwargs.get('origin', None) - basis = kwargs.get('basis', 'xy') - indices = {'xy': (0, 1, 2), 'xz': (0, 2, 1), 'yz': (1, 2, 0)}[basis] - - # Infer origin if not provided - if np.isinf(bbox.extent[basis]).any(): - if origin is None: - origin = (0, 0, 0) - else: - if origin is None: - # if nan values in the bbox.center they get replaced with 0.0 - # this happens when the bounding_box contains inf values - with warnings.catch_warnings(): - warnings.simplefilter("ignore", RuntimeWarning) - origin = np.nan_to_num(bbox.center) - - slice_index = indices[2] - slice_value = origin[slice_index] - + # Get points within tolerance of the slice plane + slice_value = origin[z] xs = [] ys = [] tol = plane_tolerance for particle in particles: - if (slice_value - tol < particle.r[slice_index] < slice_value + tol): - xs.append(particle.r[indices[0]]) - ys.append(particle.r[indices[1]]) - ax.scatter(xs, ys, **source_kwargs) - return ax + if (slice_value - tol < particle.r[z] < slice_value + tol): + xs.append(particle.r[x]) + ys.append(particle.r[y]) + axes.scatter(xs, ys, **source_kwargs) + + return axes def sample_external_source( self, diff --git a/openmc/plots.py b/openmc/plots.py index 2bb3ef65f..a59a7f0eb 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -165,6 +165,91 @@ _SVG_COLORS = { 'yellowgreen': (154, 205, 50) } +_PLOT_PARAMS = """ + Parameters + ---------- + origin : iterable of float + Coordinates at the origin of the plot. If left as None, + the center of the bounding box will be used to attempt to ascertain + the origin with infinite values being replaced by 0. + width : iterable of float + Width of the plot in each basis direction. If left as none then the + width of the bounding box will be used to attempt to + ascertain the plot width. Defaults to (10, 10) if the bounding box + contains inf values. + pixels : Iterable of int or int + If iterable of ints provided then this directly sets the number of + pixels to use in each basis direction. If int provided then this + sets the total number of pixels in the plot and the number of + pixels in each basis direction is calculated from this total and + the image aspect ratio. + basis : {'xy', 'xz', 'yz'} + The basis directions for the plot + color_by : {'cell', 'material'} + Indicate whether the plot should be colored by cell or by material + colors : dict + Assigns colors to specific materials or cells. Keys are instances of + :class:`Cell` or :class:`Material` and values are RGB 3-tuples, RGBA + 4-tuples, or strings indicating SVG color names. Red, green, blue, + and alpha should all be floats in the range [0.0, 1.0], for example: + + .. code-block:: python + + # Make water blue + water = openmc.Cell(fill=h2o) + universe.plot(..., colors={water: (0., 0., 1.)) + seed : int + Seed for the random number generator + openmc_exec : str + Path to OpenMC executable. + axes : matplotlib.Axes + Axes to draw to + + .. versionadded:: 0.13.1 + legend : bool + Whether a legend showing material or cell names should be drawn + + .. versionadded:: 0.14.0 + outline : bool or str + Whether outlines between color boundaries should be drawn. If set to + 'only', only outlines will be drawn. + + .. versionadded:: 0.14.0 + axis_units : {'km', 'm', 'cm', 'mm'} + Units used on the plot axis + + .. versionadded:: 0.14.0 + n_samples : int, optional + The number of source particles to sample and add to plot. Defaults + to None which doesn't plot any particles on the plot. + plane_tolerance: float + When plotting a plane the source locations within the plane +/- + the plane_tolerance will be included and those outside of the + plane_tolerance will not be shown + legend_kwargs : dict + Keyword arguments passed to :func:`matplotlib.pyplot.legend`. + + .. versionadded:: 0.14.0 + source_kwargs : dict, optional + Keyword arguments passed to :func:`matplotlib.pyplot.scatter`. + contour_kwargs : dict, optional + Keyword arguments passed to :func:`matplotlib.pyplot.contour`. + **kwargs + Keyword arguments passed to :func:`matplotlib.pyplot.imshow`. + + Returns + ------- + matplotlib.axes.Axes + Axes containing resulting image +""" + + +# Decorator for consistently adding plot parameters to docstrings (Model.plot, +# Geometry.plot, Universe.plot, etc.) +def add_plot_params(func): + func.__doc__ += _PLOT_PARAMS + return func + def _get_plot_image(plot, cwd): from IPython.display import Image diff --git a/openmc/region.py b/openmc/region.py index e1cb83475..cb9f3abd2 100644 --- a/openmc/region.py +++ b/openmc/region.py @@ -8,6 +8,7 @@ import numpy as np import openmc from .bounding_box import BoundingBox +from .plots import add_plot_params class Region(ABC): @@ -343,47 +344,11 @@ class Region(ABC): return type(self)(n.rotate(rotation, pivot=pivot, order=order, inplace=inplace, memo=memo) for n in self) + @add_plot_params def plot(self, *args, **kwargs): """Display a slice plot of the region. .. versionadded:: 0.15.0 - - Parameters - ---------- - origin : iterable of float - Coordinates at the origin of the plot. If left as None then the - bounding box center will be used to attempt to ascertain the origin. - Defaults to (0, 0, 0) if the bounding box is not finite - width : iterable of float - Width of the plot in each basis direction. If left as none then the - bounding box width will be used to attempt to ascertain the plot - width. Defaults to (10, 10) if the bounding box is not finite - pixels : Iterable of int or int - If iterable of ints provided, then this directly sets the number of - pixels to use in each basis direction. If int provided, then this - sets the total number of pixels in the plot and the number of pixels - in each basis direction is calculated from this total and the image - aspect ratio. - basis : {'xy', 'xz', 'yz'} - The basis directions for the plot - seed : int - Seed for the random number generator - openmc_exec : str - Path to OpenMC executable. - axes : matplotlib.Axes - Axes to draw to - outline : bool - Whether outlines between color boundaries should be drawn - axis_units : {'km', 'm', 'cm', 'mm'} - Units used on the plot axis - **kwargs - Keyword arguments passed to :func:`matplotlib.pyplot.imshow` - - Returns - ------- - matplotlib.axes.Axes - Axes containing resulting image - """ for key in ('color_by', 'colors', 'legend', 'legend_kwargs'): if key in kwargs: diff --git a/openmc/universe.py b/openmc/universe.py index 71ffa726b..02b794dee 100644 --- a/openmc/universe.py +++ b/openmc/universe.py @@ -1,17 +1,14 @@ from __future__ import annotations -import math from abc import ABC, abstractmethod from collections.abc import Iterable from numbers import Real -from pathlib import Path -from tempfile import TemporaryDirectory -import warnings import numpy as np import openmc import openmc.checkvalue as cv from .mixin import IDManagerMixin +from .plots import add_plot_params class UniverseBase(ABC, IDManagerMixin): @@ -334,234 +331,21 @@ class UniverseBase(ABC, IDManagerMixin): return [self, cell] + cell.fill.find(p) return [] - # default kwargs that are passed to plt.legend in the plot method below. - _default_legend_kwargs = {'bbox_to_anchor': ( - 1.05, 1), 'loc': 2, 'borderaxespad': 0.0} - - def plot(self, origin=None, width=None, pixels=40000, - basis='xy', color_by='cell', colors=None, seed=None, - openmc_exec='openmc', axes=None, legend=False, axis_units='cm', - legend_kwargs=_default_legend_kwargs, outline=False, - **kwargs): + @add_plot_params + def plot(self, *args, **kwargs): """Display a slice plot of the universe. - - Parameters - ---------- - origin : iterable of float - Coordinates at the origin of the plot. If left as None, - universe.bounding_box.center will be used to attempt to ascertain - the origin with infinite values being replaced by 0. - width : iterable of float - Width of the plot in each basis direction. If left as none then the - universe.bounding_box.width() will be used to attempt to - ascertain the plot width. Defaults to (10, 10) if the bounding_box - contains inf values - pixels : Iterable of int or int - If iterable of ints provided then this directly sets the number of - pixels to use in each basis direction. If int provided then this - sets the total number of pixels in the plot and the number of - pixels in each basis direction is calculated from this total and - the image aspect ratio. - basis : {'xy', 'xz', 'yz'} - The basis directions for the plot - color_by : {'cell', 'material'} - Indicate whether the plot should be colored by cell or by material - colors : dict - Assigns colors to specific materials or cells. Keys are instances of - :class:`Cell` or :class:`Material` and values are RGB 3-tuples, RGBA - 4-tuples, or strings indicating SVG color names. Red, green, blue, - and alpha should all be floats in the range [0.0, 1.0], for example: - - .. code-block:: python - - # Make water blue - water = openmc.Cell(fill=h2o) - universe.plot(..., colors={water: (0., 0., 1.)) - seed : int - Seed for the random number generator - openmc_exec : str - Path to OpenMC executable. - axes : matplotlib.Axes - Axes to draw to - - .. versionadded:: 0.13.1 - legend : bool - Whether a legend showing material or cell names should be drawn - - .. versionadded:: 0.14.0 - legend_kwargs : dict - Keyword arguments passed to :func:`matplotlib.pyplot.legend`. - - .. versionadded:: 0.14.0 - outline : bool - Whether outlines between color boundaries should be drawn - - .. versionadded:: 0.14.0 - axis_units : {'km', 'm', 'cm', 'mm'} - Units used on the plot axis - - .. versionadded:: 0.14.0 - **kwargs - Keyword arguments passed to :func:`matplotlib.pyplot.imshow` - - Returns - ------- - matplotlib.axes.Axes - Axes containing resulting image - """ - import matplotlib.image as mpimg - import matplotlib.patches as mpatches - import matplotlib.pyplot as plt + model = openmc.Model() + model.geometry = openmc.Geometry(self) - # Determine extents of plot - if basis == 'xy': - x, y = 0, 1 - xlabel, ylabel = f'x [{axis_units}]', f'y [{axis_units}]' - elif basis == 'yz': - x, y = 1, 2 - xlabel, ylabel = f'y [{axis_units}]', f'z [{axis_units}]' - elif basis == 'xz': - x, y = 0, 2 - xlabel, ylabel = f'x [{axis_units}]', f'z [{axis_units}]' + # Determine whether any materials contains macroscopic data and if + # so, set energy mode accordingly + for mat in self.get_all_materials().values(): + if mat._macroscopic is not None: + model.settings.energy_mode = 'multi-group' + break - bb = self.bounding_box - # checks to see if bounding box contains -inf or inf values - if np.isinf(bb.extent[basis]).any(): - if origin is None: - origin = (0, 0, 0) - if width is None: - width = (10, 10) - else: - if origin is None: - # if nan values in the bb.center they get replaced with 0.0 - # this happens when the bounding_box contains inf values - with warnings.catch_warnings(): - warnings.simplefilter("ignore", RuntimeWarning) - origin = np.nan_to_num(bb.center) - if width is None: - bb_width = bb.width - x_width = bb_width['xyz'.index(basis[0])] - y_width = bb_width['xyz'.index(basis[1])] - width = (x_width, y_width) - - if isinstance(pixels, int): - aspect_ratio = width[0] / width[1] - pixels_y = math.sqrt(pixels / aspect_ratio) - pixels = (int(pixels / pixels_y), int(pixels_y)) - - axis_scaling_factor = {'km': 0.00001, 'm': 0.01, 'cm': 1, 'mm': 10} - - x_min = (origin[x] - 0.5*width[0]) * axis_scaling_factor[axis_units] - x_max = (origin[x] + 0.5*width[0]) * axis_scaling_factor[axis_units] - y_min = (origin[y] - 0.5*width[1]) * axis_scaling_factor[axis_units] - y_max = (origin[y] + 0.5*width[1]) * axis_scaling_factor[axis_units] - - with TemporaryDirectory() as tmpdir: - model = openmc.Model() - model.geometry = openmc.Geometry(self) - if seed is not None: - model.settings.plot_seed = seed - - # Determine whether any materials contains macroscopic data and if - # so, set energy mode accordingly - for mat in self.get_all_materials().values(): - if mat._macroscopic is not None: - model.settings.energy_mode = 'multi-group' - break - - # Create plot object matching passed arguments - plot = openmc.Plot() - plot.origin = origin - plot.width = width - plot.pixels = pixels - plot.basis = basis - plot.color_by = color_by - if colors is not None: - plot.colors = colors - model.plots.append(plot) - - # Run OpenMC in geometry plotting mode - model.plot_geometry(False, cwd=tmpdir, openmc_exec=openmc_exec) - - # Read image from file - img_path = Path(tmpdir) / f'plot_{plot.id}.png' - if not img_path.is_file(): - img_path = img_path.with_suffix('.ppm') - img = mpimg.imread(str(img_path)) - - # Create a figure sized such that the size of the axes within - # exactly matches the number of pixels specified - if axes is None: - px = 1/plt.rcParams['figure.dpi'] - fig, axes = plt.subplots() - axes.set_xlabel(xlabel) - axes.set_ylabel(ylabel) - params = fig.subplotpars - width = pixels[0]*px/(params.right - params.left) - height = pixels[1]*px/(params.top - params.bottom) - fig.set_size_inches(width, height) - - if outline: - # Combine R, G, B values into a single int - rgb = (img * 256).astype(int) - image_value = (rgb[..., 0] << 16) + \ - (rgb[..., 1] << 8) + (rgb[..., 2]) - - axes.contour( - image_value, - origin="upper", - colors="k", - linestyles="solid", - linewidths=1, - levels=np.unique(image_value), - extent=(x_min, x_max, y_min, y_max), - ) - - # add legend showing which colors represent which material - # or cell if that was requested - if legend: - if plot.colors == {}: - raise ValueError("Must pass 'colors' dictionary if you " - "are adding a legend via legend=True.") - - if color_by == "cell": - expected_key_type = openmc.Cell - else: - expected_key_type = openmc.Material - - patches = [] - for key, color in plot.colors.items(): - - if isinstance(key, int): - raise TypeError( - "Cannot use IDs in colors dict for auto legend.") - elif not isinstance(key, expected_key_type): - raise TypeError( - "Color dict key type does not match color_by") - - # this works whether we're doing cells or materials - label = key.name if key.name != '' else key.id - - # matplotlib takes RGB on 0-1 scale rather than 0-255. at - # this point PlotBase has already checked that 3-tuple - # based colors are already valid, so if the length is three - # then we know it just needs to be converted to the 0-1 - # format. - if len(color) == 3 and not isinstance(color, str): - scaled_color = ( - color[0]/255, color[1]/255, color[2]/255) - else: - scaled_color = color - - key_patch = mpatches.Patch(color=scaled_color, label=label) - patches.append(key_patch) - - axes.legend(handles=patches, **legend_kwargs) - - # Plot image and return the axes - axes.imshow(img, extent=(x_min, x_max, y_min, y_max), **kwargs) - return axes + return model.plot(*args, **kwargs) def get_nuclides(self): """Returns all nuclides in the universe From 6e0f156d33527c5bdd693021bf419ae40c6cd649 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 4 Feb 2025 10:38:37 -0600 Subject: [PATCH 248/671] Fix Tabular.from_xml_element for histogram case (#3287) --- openmc/stats/univariate.py | 5 +++-- src/distribution.cpp | 6 +++++- tests/unit_tests/test_stats.py | 33 +++++++++++++++++++++++---------- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index 0dc6f3856..e0475bf78 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -1116,8 +1116,9 @@ class Tabular(Univariate): """ interpolation = get_text(elem, 'interpolation') params = [float(x) for x in get_text(elem, 'parameters').split()] - x = params[:len(params)//2] - p = params[len(params)//2:] + m = (len(params) + 1)//2 # +1 for when len(params) is odd + x = params[:m] + p = params[m:] return cls(x, p, interpolation) def integral(self): diff --git a/src/distribution.cpp b/src/distribution.cpp index a6b4acd58..8c700460f 100644 --- a/src/distribution.cpp +++ b/src/distribution.cpp @@ -258,8 +258,12 @@ Tabular::Tabular(pugi::xml_node node) interp_ = Interpolation::histogram; } - // Read and initialize tabular distribution + // Read and initialize tabular distribution. If number of parameters is odd, + // add an extra zero for the 'p' array. auto params = get_node_array(node, "parameters"); + if (params.size() % 2 != 0) { + params.push_back(0.0); + } std::size_t n = params.size() / 2; const double* x = params.data(); const double* p = x + n; diff --git a/tests/unit_tests/test_stats.py b/tests/unit_tests/test_stats.py index 643e11556..386181f34 100644 --- a/tests/unit_tests/test_stats.py +++ b/tests/unit_tests/test_stats.py @@ -195,19 +195,10 @@ def test_watt(): def test_tabular(): + # test linear-linear sampling x = np.array([0.0, 5.0, 7.0, 10.0]) p = np.array([10.0, 20.0, 5.0, 6.0]) d = openmc.stats.Tabular(x, p, 'linear-linear') - elem = d.to_xml_element('distribution') - - d = openmc.stats.Tabular.from_xml_element(elem) - assert all(d.x == x) - assert all(d.p == p) - assert d.interpolation == 'linear-linear' - assert len(d) == len(x) - - # test linear-linear sampling - d = openmc.stats.Tabular(x, p) n_samples = 100_000 samples = d.sample(n_samples) assert_sample_mean(samples, d.mean()) @@ -242,6 +233,28 @@ def test_tabular(): d.cdf() +def test_tabular_from_xml(): + x = np.array([0.0, 5.0, 7.0, 10.0]) + p = np.array([10.0, 20.0, 5.0, 6.0]) + d = openmc.stats.Tabular(x, p, 'linear-linear') + elem = d.to_xml_element('distribution') + + d = openmc.stats.Tabular.from_xml_element(elem) + assert all(d.x == x) + assert all(d.p == p) + assert d.interpolation == 'linear-linear' + assert len(d) == len(x) + + # Make sure XML roundtrip works with len(x) == len(p) + 1 + x = np.array([0.0, 5.0, 7.0, 10.0]) + p = np.array([10.0, 20.0, 5.0]) + d = openmc.stats.Tabular(x, p, 'histogram') + elem = d.to_xml_element('distribution') + d = openmc.stats.Tabular.from_xml_element(elem) + assert all(d.x == x) + assert all(d.p == p) + + def test_legendre(): # Pu239 elastic scattering at 100 keV coeffs = [1.000e+0, 1.536e-1, 1.772e-2, 5.945e-4, 3.497e-5, 1.881e-5] From 7e033b25ada8f8582f1a95fe74d0f6e11bbfd16b Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Tue, 11 Feb 2025 09:15:43 +0100 Subject: [PATCH 249/671] added terminal output showing compile options selected (#3291) Co-authored-by: Jon Shimwell --- CMakeLists.txt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 575e45373..e15de1b6f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,17 @@ option(OPENMC_USE_MCPL "Enable MCPL" option(OPENMC_USE_NCRYSTAL "Enable support for NCrystal scattering" OFF) option(OPENMC_USE_UWUW "Enable UWUW" OFF) +message(STATUS "OPENMC_USE_OPENMP ${OPENMC_USE_OPENMP}") +message(STATUS "OPENMC_BUILD_TESTS ${OPENMC_BUILD_TESTS}") +message(STATUS "OPENMC_ENABLE_PROFILE ${OPENMC_ENABLE_PROFILE}") +message(STATUS "OPENMC_ENABLE_COVERAGE ${OPENMC_ENABLE_COVERAGE}") +message(STATUS "OPENMC_USE_DAGMC ${OPENMC_USE_DAGMC}") +message(STATUS "OPENMC_USE_LIBMESH ${OPENMC_USE_LIBMESH}") +message(STATUS "OPENMC_USE_MPI ${OPENMC_USE_MPI}") +message(STATUS "OPENMC_USE_MCPL ${OPENMC_USE_MCPL}") +message(STATUS "OPENMC_USE_NCRYSTAL ${OPENMC_USE_NCRYSTAL}") +message(STATUS "OPENMC_USE_UWUW ${OPENMC_USE_UWUW}") + # Warnings for deprecated options foreach(OLD_OPT IN ITEMS "openmp" "profile" "coverage" "dagmc" "libmesh") if(DEFINED ${OLD_OPT}) From 27ce2ceee3e666d211cdd5d9db25300dada67fa1 Mon Sep 17 00:00:00 2001 From: Thomas Kittelmann Date: Tue, 11 Feb 2025 15:18:27 +0100 Subject: [PATCH 250/671] Updates for building with NCrystal support (and fix CI) (#3274) --- CMakeLists.txt | 10 +++- cmake/OpenMCConfig.cmake.in | 10 +++- docs/source/usersguide/install.rst | 5 +- include/openmc/ncrystal_interface.h | 16 ++----- .../ncrystal/results_true.dat | 14 +++--- tools/ci/gha-install-ncrystal.sh | 46 ------------------- tools/ci/gha-install.py | 2 - tools/ci/gha-install.sh | 4 +- tools/ci/gha-script.sh | 2 - 9 files changed, 33 insertions(+), 76 deletions(-) delete mode 100755 tools/ci/gha-install-ncrystal.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index e15de1b6f..f7079aebc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -121,7 +121,15 @@ endmacro() #=============================================================================== if(OPENMC_USE_NCRYSTAL) - find_package(NCrystal REQUIRED) + if(NOT DEFINED "NCrystal_DIR") + #Invocation of "ncrystal-config --show cmakedir" is needed to find NCrystal + #when it is installed from Python wheels: + execute_process( + COMMAND "ncrystal-config" "--show" "cmakedir" + OUTPUT_VARIABLE "NCrystal_DIR" OUTPUT_STRIP_TRAILING_WHITESPACE + ) + endif() + find_package(NCrystal 3.8.0 REQUIRED) message(STATUS "Found NCrystal: ${NCrystal_DIR} (version ${NCrystal_VERSION})") endif() diff --git a/cmake/OpenMCConfig.cmake.in b/cmake/OpenMCConfig.cmake.in index b3b901de4..b02bbffe5 100644 --- a/cmake/OpenMCConfig.cmake.in +++ b/cmake/OpenMCConfig.cmake.in @@ -10,6 +10,14 @@ if(@OPENMC_USE_DAGMC@) endif() if(@OPENMC_USE_NCRYSTAL@) + if(NOT DEFINED "NCrystal_DIR") + #Invocation of "ncrystal-config --show cmakedir" is needed to find NCrystal + #when it is installed from Python wheels: + execute_process( + COMMAND "ncrystal-config" "--show" "cmakedir" + OUTPUT_VARIABLE "NCrystal_DIR" OUTPUT_STRIP_TRAILING_WHITESPACE + ) + endif() find_package(NCrystal REQUIRED) message(STATUS "Found NCrystal: ${NCrystal_DIR} (version ${NCrystal_VERSION})") endif() @@ -41,4 +49,4 @@ endif() if(@OPENMC_USE_UWUW@ AND NOT ${DAGMC_BUILD_UWUW}) message(FATAL_ERROR "UWUW is enabled in OpenMC but the DAGMC installation discovered was not configured with UWUW.") -endif() \ No newline at end of file +endif() diff --git a/docs/source/usersguide/install.rst b/docs/source/usersguide/install.rst index 1c0b7fa5b..b86f5fee0 100644 --- a/docs/source/usersguide/install.rst +++ b/docs/source/usersguide/install.rst @@ -287,9 +287,8 @@ Prerequisites Adding this option allows the creation of materials from NCrystal, which replaces the scattering kernel treatment of ACE files with a modular, on-the-fly approach. To use it `install - `_ and `initialize - `_ - NCrystal and turn on the option in the CMake configuration step:: + `_ NCrystal and + turn on the option in the CMake configuration step:: cmake -DOPENMC_USE_NCRYSTAL=on .. diff --git a/include/openmc/ncrystal_interface.h b/include/openmc/ncrystal_interface.h index 5a3882df9..22422e378 100644 --- a/include/openmc/ncrystal_interface.h +++ b/include/openmc/ncrystal_interface.h @@ -2,7 +2,6 @@ #define OPENMC_NCRYSTAL_INTERFACE_H #ifdef NCRYSTAL -#include "NCrystal/NCRNG.hh" #include "NCrystal/NCrystal.hh" #endif @@ -58,19 +57,10 @@ public: //---------------------------------------------------------------------------- // Trivial methods when compiling without NCRYSTAL - std::string cfg() const - { - return ""; - } - double xs(const Particle& p) const - { - return -1.0; - } + std::string cfg() const { return ""; } + double xs(const Particle& p) const { return -1.0; } void scatter(Particle& p) const {} - operator bool() const - { - return false; - } + operator bool() const { return false; } #endif private: diff --git a/tests/regression_tests/ncrystal/results_true.dat b/tests/regression_tests/ncrystal/results_true.dat index 3ef0306fd..28c02c51c 100644 --- a/tests/regression_tests/ncrystal/results_true.dat +++ b/tests/regression_tests/ncrystal/results_true.dat @@ -106,8 +106,8 @@ 104 1 1.82e+00 1.83e+00 1 total current 1.10e-05 4.07e-06 105 1 1.83e+00 1.85e+00 1 total current 1.50e-05 4.01e-06 106 1 1.85e+00 1.87e+00 1 total current 1.90e-05 4.82e-06 -107 1 1.87e+00 1.88e+00 1 total current 2.20e-05 3.89e-06 -108 1 1.88e+00 1.90e+00 1 total current 2.10e-05 3.79e-06 +107 1 1.87e+00 1.88e+00 1 total current 2.30e-05 3.96e-06 +108 1 1.88e+00 1.90e+00 1 total current 2.00e-05 3.94e-06 109 1 1.90e+00 1.92e+00 1 total current 1.50e-05 3.42e-06 110 1 1.92e+00 1.94e+00 1 total current 2.20e-05 5.12e-06 111 1 1.94e+00 1.95e+00 1 total current 2.10e-05 5.86e-06 @@ -118,12 +118,12 @@ 116 1 2.02e+00 2.04e+00 1 total current 1.30e-05 3.35e-06 117 1 2.04e+00 2.06e+00 1 total current 1.90e-05 4.07e-06 118 1 2.06e+00 2.08e+00 1 total current 1.30e-05 3.00e-06 -119 1 2.08e+00 2.09e+00 1 total current 1.40e-05 4.00e-06 -120 1 2.09e+00 2.11e+00 1 total current 3.10e-05 5.47e-06 +119 1 2.08e+00 2.09e+00 1 total current 1.50e-05 4.28e-06 +120 1 2.09e+00 2.11e+00 1 total current 3.00e-05 4.94e-06 121 1 2.11e+00 2.13e+00 1 total current 2.30e-05 5.39e-06 122 1 2.13e+00 2.15e+00 1 total current 2.20e-05 4.67e-06 -123 1 2.15e+00 2.16e+00 1 total current 1.70e-05 4.48e-06 -124 1 2.16e+00 2.18e+00 1 total current 1.60e-05 4.00e-06 +123 1 2.15e+00 2.16e+00 1 total current 1.80e-05 4.67e-06 +124 1 2.16e+00 2.18e+00 1 total current 1.50e-05 4.01e-06 125 1 2.18e+00 2.20e+00 1 total current 1.80e-05 4.16e-06 126 1 2.20e+00 2.22e+00 1 total current 1.80e-05 4.42e-06 127 1 2.22e+00 2.23e+00 1 total current 1.80e-05 5.54e-06 @@ -135,7 +135,7 @@ 133 1 2.32e+00 2.34e+00 1 total current 2.30e-05 3.96e-06 134 1 2.34e+00 2.36e+00 1 total current 1.90e-05 3.48e-06 135 1 2.36e+00 2.37e+00 1 total current 1.60e-05 3.71e-06 -136 1 2.37e+00 2.39e+00 1 total current 1.60e-05 4.27e-06 +136 1 2.37e+00 2.39e+00 1 total current 1.70e-05 4.48e-06 137 1 2.39e+00 2.41e+00 1 total current 2.30e-05 4.48e-06 138 1 2.41e+00 2.43e+00 1 total current 2.10e-05 3.48e-06 139 1 2.43e+00 2.44e+00 1 total current 1.60e-05 2.21e-06 diff --git a/tools/ci/gha-install-ncrystal.sh b/tools/ci/gha-install-ncrystal.sh deleted file mode 100755 index 16f77e13e..000000000 --- a/tools/ci/gha-install-ncrystal.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash -set -ex -cd $HOME - -#Use the NCrystal develop branch (in the near future we can move this to master): -git clone https://github.com/mctools/ncrystal --branch develop --single-branch --depth 1 ncrystal_src - -SRC_DIR="$PWD/ncrystal_src" -BLD_DIR="$PWD/ncrystal_bld" -INST_DIR="$PWD/ncrystal_inst" -PYTHON=$(which python3) - -CPU_COUNT=1 - -mkdir "$BLD_DIR" -cd ncrystal_bld - -cmake \ - "${SRC_DIR}" \ - -DBUILD_SHARED_LIBS=ON \ - -DNCRYSTAL_NOTOUCH_CMAKE_BUILD_TYPE=ON \ - -DNCRYSTAL_MODIFY_RPATH=OFF \ - -DCMAKE_BUILD_TYPE=Release \ - -DNCRYSTAL_ENABLE_EXAMPLES=OFF \ - -DNCRYSTAL_ENABLE_SETUPSH=OFF \ - -DNCRYSTAL_ENABLE_DATA=EMBED \ - -DCMAKE_INSTALL_PREFIX="${INST_DIR}" \ - -DPython3_EXECUTABLE="$PYTHON" - -make -j${CPU_COUNT:-1} -make install - -#Note: There is no "make test" or "make ctest" functionality for NCrystal -# yet. If it appears in the future, we should add it here. - -# Output the configuration to the log - -"${INST_DIR}/bin/ncrystal-config" --setup - -# Change environmental variables - -eval $( "${INST_DIR}/bin/ncrystal-config" --setup ) - -# Check installation worked - -nctool --test diff --git a/tools/ci/gha-install.py b/tools/ci/gha-install.py index d52c4c825..1eb9a55b4 100644 --- a/tools/ci/gha-install.py +++ b/tools/ci/gha-install.py @@ -42,8 +42,6 @@ def install(omp=False, mpi=False, phdf5=False, dagmc=False, libmesh=False, ncrys if ncrystal: cmake_cmd.append('-DOPENMC_USE_NCRYSTAL=ON') - ncrystal_path = os.environ.get('HOME') + '/ncrystal_inst' - cmake_cmd.append(f'-DCMAKE_PREFIX_PATH={ncrystal_path}') # Build in coverage mode for coverage testing cmake_cmd.append('-DOPENMC_ENABLE_COVERAGE=on') diff --git a/tools/ci/gha-install.sh b/tools/ci/gha-install.sh index cff7dc834..50f110ed4 100755 --- a/tools/ci/gha-install.sh +++ b/tools/ci/gha-install.sh @@ -16,7 +16,9 @@ fi # Install NCrystal if needed if [[ $NCRYSTAL = 'y' ]]; then - ./tools/ci/gha-install-ncrystal.sh + pip install 'ncrystal>=4.0.0' + #Basic quick verification: + nctool --test fi # Install vectfit for WMP generation if needed diff --git a/tools/ci/gha-script.sh b/tools/ci/gha-script.sh index 9de84f3e5..4733907eb 100755 --- a/tools/ci/gha-script.sh +++ b/tools/ci/gha-script.sh @@ -16,8 +16,6 @@ fi # Check NCrystal installation if [[ $NCRYSTAL = 'y' ]]; then - # Change environmental variables - eval $( "${HOME}/ncrystal_inst/bin/ncrystal-config" --setup ) nctool --test fi From 04393200cc79defe5cbfc5484376ea5d753f067c Mon Sep 17 00:00:00 2001 From: John Tramm Date: Tue, 11 Feb 2025 11:23:26 -0600 Subject: [PATCH 251/671] Random Ray Source Region Refactor (#3288) --- CMakeLists.txt | 1 + include/openmc/openmp_interface.h | 12 +- .../openmc/random_ray/flat_source_domain.h | 132 +----- .../openmc/random_ray/linear_source_domain.h | 30 +- include/openmc/random_ray/source_region.h | 400 ++++++++++++++++++ src/random_ray/flat_source_domain.cpp | 290 +++++-------- src/random_ray/linear_source_domain.cpp | 129 ++---- src/random_ray/random_ray.cpp | 60 ++- src/random_ray/random_ray_simulation.cpp | 2 +- src/random_ray/source_region.cpp | 236 +++++++++++ 10 files changed, 832 insertions(+), 460 deletions(-) create mode 100644 include/openmc/random_ray/source_region.h create mode 100644 src/random_ray/source_region.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index f7079aebc..94caec757 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -401,6 +401,7 @@ list(APPEND libopenmc_SOURCES src/random_ray/flat_source_domain.cpp src/random_ray/linear_source_domain.cpp src/random_ray/moment_matrix.cpp + src/random_ray/source_region.cpp src/reaction.cpp src/reaction_product.cpp src/scattdata.cpp diff --git a/include/openmc/openmp_interface.h b/include/openmc/openmp_interface.h index fe517415b..30bd6f008 100644 --- a/include/openmc/openmp_interface.h +++ b/include/openmc/openmp_interface.h @@ -36,13 +36,15 @@ inline int thread_num() class OpenMPMutex { public: - OpenMPMutex() + void init() { #ifdef _OPENMP omp_init_lock(&mutex_); #endif } + OpenMPMutex() { init(); } + ~OpenMPMutex() { #ifdef _OPENMP @@ -62,14 +64,10 @@ public: // rather, it produces two different mutexes. // Copy constructor - OpenMPMutex(const OpenMPMutex& other) { OpenMPMutex(); } + OpenMPMutex(const OpenMPMutex& other) { init(); } // Copy assignment operator - OpenMPMutex& operator=(const OpenMPMutex& other) - { - OpenMPMutex(); - return *this; - } + OpenMPMutex& operator=(const OpenMPMutex& other) { return *this; } //! Lock the mutex. // diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index b5b5db706..011ff7ccd 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -4,83 +4,12 @@ #include "openmc/constants.h" #include "openmc/openmp_interface.h" #include "openmc/position.h" +#include "openmc/random_ray/source_region.h" #include "openmc/source.h" +#include namespace openmc { -//---------------------------------------------------------------------------- -// Helper Functions - -// The hash_combine function is the standard hash combine function from boost -// that is typically used for combining multiple hash values into a single hash -// as is needed for larger objects being stored in a hash map. The function is -// taken from: -// https://www.boost.org/doc/libs/1_55_0/doc/html/hash/reference.html#boost.hash_combine -// which carries the following license: -// -// Boost Software License - Version 1.0 - August 17th, 2003 -// Permission is hereby granted, free of charge, to any person or organization -// obtaining a copy of the software and accompanying documentation covered by -// this license (the "Software") to use, reproduce, display, distribute, -// execute, and transmit the Software, and to prepare derivative works of the -// Software, and to permit third-parties to whom the Software is furnished to -// do so, all subject to the following: -// The copyright notices in the Software and this entire statement, including -// the above license grant, this restriction and the following disclaimer, -// must be included in all copies of the Software, in whole or in part, and -// all derivative works of the Software, unless such copies or derivative -// works are solely in the form of machine-executable object code generated by -// a source language processor. -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT -// SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE -// FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, -// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. -inline void hash_combine(size_t& seed, const size_t v) -{ - seed ^= (v + 0x9e3779b9 + (seed << 6) + (seed >> 2)); -} - -//---------------------------------------------------------------------------- -// Helper Structs - -// A mapping object that is used to map between a specific random ray -// source region and an OpenMC native tally bin that it should score to -// every iteration. -struct TallyTask { - int tally_idx; - int filter_idx; - int score_idx; - int score_type; - TallyTask(int tally_idx, int filter_idx, int score_idx, int score_type) - : tally_idx(tally_idx), filter_idx(filter_idx), score_idx(score_idx), - score_type(score_type) - {} - TallyTask() = default; - - // Comparison and Hash operators are defined to allow usage of the - // TallyTask struct as a key in an unordered_set - bool operator==(const TallyTask& other) const - { - return tally_idx == other.tally_idx && filter_idx == other.filter_idx && - score_idx == other.score_idx && score_type == other.score_type; - } - - struct HashFunctor { - size_t operator()(const TallyTask& task) const - { - size_t seed = 0; - hash_combine(seed, task.tally_idx); - hash_combine(seed, task.filter_idx); - hash_combine(seed, task.score_idx); - hash_combine(seed, task.score_type); - return seed; - } - }; -}; - /* * The FlatSourceDomain class encompasses data and methods for storing * scalar flux and source region for all flat source regions in a @@ -108,15 +37,16 @@ public: void random_ray_tally(); virtual void accumulate_iteration_flux(); void output_to_vtk() const; - virtual void all_reduce_replicated_source_regions(); + void all_reduce_replicated_source_regions(); void convert_external_sources(); void count_external_source_regions(); void set_adjoint_sources(const vector& forward_flux); - virtual void flux_swap(); + void flux_swap(); virtual double evaluate_flux_at_point(Position r, int64_t sr, int g) const; double compute_fixed_source_normalization_factor() const; void flatten_xs(); void transpose_scattering_matrix(); + void serialize_final_fluxes(vector& flux); //---------------------------------------------------------------------------- // Static Data members @@ -129,7 +59,6 @@ public: //---------------------------------------------------------------------------- // Public Data members - bool mapped_all_tallies_ {false}; // If all source regions have been visited int64_t n_source_regions_ {0}; // Total number of source regions in the model @@ -140,22 +69,6 @@ public: // in model::cells vector source_region_offsets_; - // 1D arrays representing values for all source regions - vector lock_; - vector volume_; - vector volume_t_; - vector position_recorded_; - vector position_; - - // 2D arrays stored in 1D representing values for all source regions x energy - // groups - vector scalar_flux_old_; - vector scalar_flux_new_; - vector source_; - vector external_source_; - vector external_source_present_; - vector scalar_flux_final_; - // 2D arrays stored in 1D representing values for all materials x energy // groups int n_materials_; @@ -168,20 +81,22 @@ public: // groups x energy groups vector sigma_s_; + // The abstract container holding all source region-specific data + SourceRegionContainer source_regions_; + protected: //---------------------------------------------------------------------------- // Methods void apply_external_source_to_source_region( - Discrete* discrete, double strength_factor, int64_t source_region); + Discrete* discrete, double strength_factor, int64_t sr); void apply_external_source_to_cell_instances(int32_t i_cell, Discrete* discrete, double strength_factor, int target_material_id, const vector& instances); void apply_external_source_to_cell_and_children(int32_t i_cell, Discrete* discrete, double strength_factor, int32_t target_material_id); - virtual void set_flux_to_flux_plus_source( - int64_t idx, double volume, int material, int g); - void set_flux_to_source(int64_t idx); - virtual void set_flux_to_old_flux(int64_t idx); + virtual void set_flux_to_flux_plus_source(int64_t sr, double volume, int g); + void set_flux_to_source(int64_t sr, int g); + virtual void set_flux_to_old_flux(int64_t sr, int g); //---------------------------------------------------------------------------- // Private data members @@ -193,20 +108,6 @@ protected: simulation_volume_; // Total physical volume of the simulation domain, as // defined by the 3D box of the random ray source - // 2D array representing values for all source elements x tally - // tasks - vector> tally_task_; - - // 1D array representing values for all source regions, with each region - // containing a set of volume tally tasks. This more complicated data - // structure is convenient for ensuring that volumes are only tallied once per - // source region, regardless of how many energy groups are used for tallying. - vector> volume_task_; - - // 1D arrays representing values for all source regions - vector material_; - vector volume_naive_; - // 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 @@ -250,15 +151,6 @@ T convert_to_big_endian(T in) return out; } -template -void parallel_fill(vector& arr, T value) -{ -#pragma omp parallel for schedule(static) - for (int i = 0; i < arr.size(); i++) { - arr[i] = value; - } -} - } // namespace openmc #endif // OPENMC_RANDOM_RAY_FLAT_SOURCE_DOMAIN_H diff --git a/include/openmc/random_ray/linear_source_domain.h b/include/openmc/random_ray/linear_source_domain.h index 4812d1433..67fdd99f8 100644 --- a/include/openmc/random_ray/linear_source_domain.h +++ b/include/openmc/random_ray/linear_source_domain.h @@ -18,48 +18,22 @@ namespace openmc { class LinearSourceDomain : public FlatSourceDomain { public: - //---------------------------------------------------------------------------- - // Constructors - LinearSourceDomain(); - //---------------------------------------------------------------------------- // Methods void update_neutron_source(double k_eff) override; - double compute_k_eff(double k_eff_old) const; void normalize_scalar_flux_and_volumes( double total_active_distance_per_iteration) override; void batch_reset() override; - void convert_source_regions_to_tallies(); - void reset_tally_volumes(); - void random_ray_tally(); void accumulate_iteration_flux() override; void output_to_vtk() const; - void all_reduce_replicated_source_regions() override; - void convert_external_sources(); - void count_external_source_regions(); - void flux_swap() override; double evaluate_flux_at_point(Position r, int64_t sr, int g) const override; - //---------------------------------------------------------------------------- - // Public Data members - - vector source_gradients_; - vector flux_moments_old_; - vector flux_moments_new_; - vector flux_moments_t_; - vector centroid_; - vector centroid_iteration_; - vector centroid_t_; - vector mom_matrix_; - vector mom_matrix_t_; - protected: //---------------------------------------------------------------------------- // Methods - void set_flux_to_flux_plus_source( - int64_t idx, double volume, int material, int g) override; - void set_flux_to_old_flux(int64_t idx) override; + void set_flux_to_flux_plus_source(int64_t sr, double volume, int g) override; + void set_flux_to_old_flux(int64_t sr, int g) override; }; // class LinearSourceDomain diff --git a/include/openmc/random_ray/source_region.h b/include/openmc/random_ray/source_region.h new file mode 100644 index 000000000..0dc617a64 --- /dev/null +++ b/include/openmc/random_ray/source_region.h @@ -0,0 +1,400 @@ +#ifndef OPENMC_RANDOM_RAY_SOURCE_REGION_H +#define OPENMC_RANDOM_RAY_SOURCE_REGION_H + +#include "openmc/openmp_interface.h" +#include "openmc/position.h" +#include "openmc/random_ray/moment_matrix.h" +#include "openmc/settings.h" + +namespace openmc { + +//---------------------------------------------------------------------------- +// Helper Functions + +// The hash_combine function is the standard hash combine function from boost +// that is typically used for combining multiple hash values into a single hash +// as is needed for larger objects being stored in a hash map. The function is +// taken from: +// https://www.boost.org/doc/libs/1_55_0/doc/html/hash/reference.html#boost.hash_combine +// which carries the following license: +// +// Boost Software License - Version 1.0 - August 17th, 2003 +// Permission is hereby granted, free of charge, to any person or organization +// obtaining a copy of the software and accompanying documentation covered by +// this license (the "Software") to use, reproduce, display, distribute, +// execute, and transmit the Software, and to prepare derivative works of the +// Software, and to permit third-parties to whom the Software is furnished to +// do so, all subject to the following: +// The copyright notices in the Software and this entire statement, including +// the above license grant, this restriction and the following disclaimer, +// must be included in all copies of the Software, in whole or in part, and +// all derivative works of the Software, unless such copies or derivative +// works are solely in the form of machine-executable object code generated by +// a source language processor. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +// SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +// FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +inline void hash_combine(size_t& seed, const size_t v) +{ + seed ^= (v + 0x9e3779b9 + (seed << 6) + (seed >> 2)); +} + +//---------------------------------------------------------------------------- +// Helper Structs + +// A mapping object that is used to map between a specific random ray +// source region and an OpenMC native tally bin that it should score to +// every iteration. +struct TallyTask { + int tally_idx; + int filter_idx; + int score_idx; + int score_type; + TallyTask(int tally_idx, int filter_idx, int score_idx, int score_type) + : tally_idx(tally_idx), filter_idx(filter_idx), score_idx(score_idx), + score_type(score_type) + {} + TallyTask() = default; + + // Comparison and Hash operators are defined to allow usage of the + // TallyTask struct as a key in an unordered_set + bool operator==(const TallyTask& other) const + { + return tally_idx == other.tally_idx && filter_idx == other.filter_idx && + score_idx == other.score_idx && score_type == other.score_type; + } + + struct HashFunctor { + size_t operator()(const TallyTask& task) const + { + size_t seed = 0; + hash_combine(seed, task.tally_idx); + hash_combine(seed, task.filter_idx); + hash_combine(seed, task.score_idx); + hash_combine(seed, task.score_type); + return seed; + } + }; +}; + +class SourceRegion { +public: + //---------------------------------------------------------------------------- + // Constructors + SourceRegion(int negroups, bool is_linear); + SourceRegion() = default; + + //---------------------------------------------------------------------------- + // Public Data members + + // Scalar fields + int material_ {0}; + OpenMPMutex lock_; + double volume_ {0.0}; + double volume_t_ {0.0}; + double volume_naive_ {0.0}; + int position_recorded_ {0}; + int external_source_present_ {0}; + Position position_ {0.0, 0.0, 0.0}; + Position centroid_ {0.0, 0.0, 0.0}; + Position centroid_iteration_ {0.0, 0.0, 0.0}; + Position centroid_t_ {0.0, 0.0, 0.0}; + MomentMatrix mom_matrix_ {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; + MomentMatrix mom_matrix_t_ {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; + // A set of volume tally tasks. This more complicated data structure is + // convenient for ensuring that volumes are only tallied once per source + // region, regardless of how many energy groups are used for tallying. + std::unordered_set volume_task_; + + // Energy group-wise 1D arrays + vector scalar_flux_old_; + vector scalar_flux_new_; + vector source_; + vector external_source_; + vector scalar_flux_final_; + + vector source_gradients_; + vector flux_moments_old_; + vector flux_moments_new_; + vector flux_moments_t_; + + // 2D array representing values for all energy groups x tally + // tasks. Each group may have a different number of tally tasks + // associated with it, necessitating the use of a jagged array. + vector> tally_task_; + +}; // class SourceRegion + +class SourceRegionContainer { +public: + //---------------------------------------------------------------------------- + // Constructors + SourceRegionContainer(int negroups, bool is_linear) + : negroups_(negroups), is_linear_(is_linear) + {} + SourceRegionContainer() = default; + + //---------------------------------------------------------------------------- + // Public Accessors + int& material(int64_t sr) { return material_[sr]; } + const int& material(int64_t sr) const { return material_[sr]; } + + OpenMPMutex& lock(int64_t sr) { return lock_[sr]; } + const OpenMPMutex& lock(int64_t sr) const { return lock_[sr]; } + + double& volume(int64_t sr) { return volume_[sr]; } + const double& volume(int64_t sr) const { return volume_[sr]; } + + double& volume_t(int64_t sr) { return volume_t_[sr]; } + const double& volume_t(int64_t sr) const { return volume_t_[sr]; } + + double& volume_naive(int64_t sr) { return volume_naive_[sr]; } + const double& volume_naive(int64_t sr) const { return volume_naive_[sr]; } + + int& position_recorded(int64_t sr) { return position_recorded_[sr]; } + const int& position_recorded(int64_t sr) const + { + return position_recorded_[sr]; + } + + int& external_source_present(int64_t sr) + { + return external_source_present_[sr]; + } + const int& external_source_present(int64_t sr) const + { + return external_source_present_[sr]; + } + + Position& position(int64_t sr) { return position_[sr]; } + const Position& position(int64_t sr) const { return position_[sr]; } + + Position& centroid(int64_t sr) { return centroid_[sr]; } + const Position& centroid(int64_t sr) const { return centroid_[sr]; } + + Position& centroid_iteration(int64_t sr) { return centroid_iteration_[sr]; } + const Position& centroid_iteration(int64_t sr) const + { + return centroid_iteration_[sr]; + } + + Position& centroid_t(int64_t sr) { return centroid_t_[sr]; } + const Position& centroid_t(int64_t sr) const { return centroid_t_[sr]; } + + MomentMatrix& mom_matrix(int64_t sr) { return mom_matrix_[sr]; } + const MomentMatrix& mom_matrix(int64_t sr) const { return mom_matrix_[sr]; } + + MomentMatrix& mom_matrix_t(int64_t sr) { return mom_matrix_t_[sr]; } + const MomentMatrix& mom_matrix_t(int64_t sr) const + { + return mom_matrix_t_[sr]; + } + + MomentArray& source_gradients(int64_t sr, int g) + { + return source_gradients_[index(sr, g)]; + } + const MomentArray& source_gradients(int64_t sr, int g) const + { + return source_gradients_[index(sr, g)]; + } + MomentArray& source_gradients(int64_t se) { return source_gradients_[se]; } + const MomentArray& source_gradients(int64_t se) const + { + return source_gradients_[se]; + } + + MomentArray& flux_moments_old(int64_t sr, int g) + { + return flux_moments_old_[index(sr, g)]; + } + const MomentArray& flux_moments_old(int64_t sr, int g) const + { + return flux_moments_old_[index(sr, g)]; + } + MomentArray& flux_moments_old(int64_t se) { return flux_moments_old_[se]; } + const MomentArray& flux_moments_old(int64_t se) const + { + return flux_moments_old_[se]; + } + + MomentArray& flux_moments_new(int64_t sr, int g) + { + return flux_moments_new_[index(sr, g)]; + } + const MomentArray& flux_moments_new(int64_t sr, int g) const + { + return flux_moments_new_[index(sr, g)]; + } + MomentArray& flux_moments_new(int64_t se) { return flux_moments_new_[se]; } + const MomentArray& flux_moments_new(int64_t se) const + { + return flux_moments_new_[se]; + } + + MomentArray& flux_moments_t(int64_t sr, int g) + { + return flux_moments_t_[index(sr, g)]; + } + const MomentArray& flux_moments_t(int64_t sr, int g) const + { + return flux_moments_t_[index(sr, g)]; + } + MomentArray& flux_moments_t(int64_t se) { return flux_moments_t_[se]; } + const MomentArray& flux_moments_t(int64_t se) const + { + return flux_moments_t_[se]; + } + + double& scalar_flux_old(int64_t sr, int g) + { + return scalar_flux_old_[index(sr, g)]; + } + const double& scalar_flux_old(int64_t sr, int g) const + { + return scalar_flux_old_[index(sr, g)]; + } + double& scalar_flux_old(int64_t se) { return scalar_flux_old_[se]; } + const double& scalar_flux_old(int64_t se) const + { + return scalar_flux_old_[se]; + } + + double& scalar_flux_new(int64_t sr, int g) + { + return scalar_flux_new_[index(sr, g)]; + } + const double& scalar_flux_new(int64_t sr, int g) const + { + return scalar_flux_new_[index(sr, g)]; + } + double& scalar_flux_new(int64_t se) { return scalar_flux_new_[se]; } + const double& scalar_flux_new(int64_t se) const + { + return scalar_flux_new_[se]; + } + + double& scalar_flux_final(int64_t sr, int g) + { + return scalar_flux_final_[index(sr, g)]; + } + const double& scalar_flux_final(int64_t sr, int g) const + { + return scalar_flux_final_[index(sr, g)]; + } + double& scalar_flux_final(int64_t se) { return scalar_flux_final_[se]; } + const double& scalar_flux_final(int64_t se) const + { + return scalar_flux_final_[se]; + } + + float& source(int64_t sr, int g) { return source_[index(sr, g)]; } + const float& source(int64_t sr, int g) const { return source_[index(sr, g)]; } + float& source(int64_t se) { return source_[se]; } + const float& source(int64_t se) const { return source_[se]; } + + float& external_source(int64_t sr, int g) + { + return external_source_[index(sr, g)]; + } + const float& external_source(int64_t sr, int g) const + { + return external_source_[index(sr, g)]; + } + float& external_source(int64_t se) { return external_source_[se]; } + const float& external_source(int64_t se) const + { + return external_source_[se]; + } + + vector& tally_task(int64_t sr, int g) + { + return tally_task_[index(sr, g)]; + } + const vector& tally_task(int64_t sr, int g) const + { + return tally_task_[index(sr, g)]; + } + vector& tally_task(int64_t se) { return tally_task_[se]; } + const vector& tally_task(int64_t se) const + { + return tally_task_[se]; + } + + std::unordered_set& volume_task(int64_t sr) + { + return volume_task_[sr]; + } + const std::unordered_set& volume_task( + int64_t sr) const + { + return volume_task_[sr]; + } + + //---------------------------------------------------------------------------- + // Public Methods + + void push_back(const SourceRegion& sr); + void assign(int n_source_regions, const SourceRegion& source_region); + void flux_swap(); + void mpi_sync_ranks(bool reduce_position); + +private: + //---------------------------------------------------------------------------- + // Private Data Members + int64_t n_source_regions_ {0}; + int negroups_ {0}; + bool is_linear_ {false}; + + // SoA storage for scalar fields (one item per source region) + vector material_; + vector lock_; + vector volume_; + vector volume_t_; + vector volume_naive_; + vector position_recorded_; + vector external_source_present_; + vector position_; + vector centroid_; + vector centroid_iteration_; + vector centroid_t_; + vector mom_matrix_; + vector mom_matrix_t_; + // A set of volume tally tasks. This more complicated data structure is + // convenient for ensuring that volumes are only tallied once per source + // region, regardless of how many energy groups are used for tallying. + vector> volume_task_; + + // SoA energy group-wise 2D arrays flattened to 1D + vector scalar_flux_old_; + vector scalar_flux_new_; + vector scalar_flux_final_; + vector source_; + vector external_source_; + + vector source_gradients_; + vector flux_moments_old_; + vector flux_moments_new_; + vector flux_moments_t_; + + // SoA 3D array representing values for all source regions x energy groups x + // tally tasks. The outer two dimensions (source regions and energy groups) + // are flattened to 1D. Each group may have a different number of tally tasks + // associated with it, necessitating the use of a jagged array for the inner + // dimension. + vector> tally_task_; + + //---------------------------------------------------------------------------- + // Private Methods + + // Helper function for indexing + inline int index(int64_t sr, int g) const { return sr * negroups_ + g; } +}; + +} // namespace openmc + +#endif // OPENMC_RANDOM_RAY_SOURCE_REGION_H \ No newline at end of file diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 19913c03c..39a50ace6 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -46,40 +46,17 @@ FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) } // Initialize cell-wise arrays - lock_.resize(n_source_regions_); - material_.resize(n_source_regions_); - position_recorded_.assign(n_source_regions_, 0); - position_.resize(n_source_regions_); - volume_.assign(n_source_regions_, 0.0); - volume_t_.assign(n_source_regions_, 0.0); - volume_naive_.assign(n_source_regions_, 0.0); + bool is_linear = RandomRay::source_shape_ != RandomRaySourceShape::FLAT; + source_regions_ = SourceRegionContainer(negroups_, is_linear); + source_regions_.assign(n_source_regions_, SourceRegion(negroups_, is_linear)); - // Initialize element-wise arrays - scalar_flux_new_.assign(n_source_elements_, 0.0); - scalar_flux_final_.assign(n_source_elements_, 0.0); - source_.resize(n_source_elements_); - - tally_task_.resize(n_source_elements_); - volume_task_.resize(n_source_regions_); - - if (settings::run_mode == RunMode::EIGENVALUE) { - // If in eigenvalue mode, set starting flux to guess of unity - scalar_flux_old_.assign(n_source_elements_, 1.0); - } else { - // If in fixed source mode, set starting flux to guess of zero - // and initialize external source arrays - scalar_flux_old_.assign(n_source_elements_, 0.0); - external_source_.assign(n_source_elements_, 0.0); - external_source_present_.assign(n_source_regions_, false); - } - - // Initialize material array + // Initialize materials int64_t source_region_id = 0; for (int i = 0; i < model::cells.size(); i++) { Cell& cell = *model::cells[i]; if (cell.type_ == Fill::MATERIAL) { for (int j = 0; j < cell.n_instances_; j++) { - material_[source_region_id++] = cell.material(j); + source_regions_.material(source_region_id++) = cell.material(j); } } } @@ -113,17 +90,23 @@ FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) void FlatSourceDomain::batch_reset() { - // Reset scalar fluxes, iteration volume tallies, and region hit flags to - // zero - parallel_fill(scalar_flux_new_, 0.0); - parallel_fill(volume_, 0.0); +// Reset scalar fluxes and iteration volume tallies to zero +#pragma omp parallel for + for (int64_t sr = 0; sr < n_source_regions_; sr++) { + source_regions_.volume(sr) = 0.0; + } +#pragma omp parallel for + for (int64_t se = 0; se < n_source_elements_; se++) { + source_regions_.scalar_flux_new(se) = 0.0; + } } void FlatSourceDomain::accumulate_iteration_flux() { #pragma omp parallel for for (int64_t se = 0; se < n_source_elements_; se++) { - scalar_flux_final_[se] += scalar_flux_new_[se]; + source_regions_.scalar_flux_final(se) += + source_regions_.scalar_flux_new(se); } } @@ -137,40 +120,40 @@ void FlatSourceDomain::update_neutron_source(double k_eff) // Add scattering source #pragma omp parallel for - for (int sr = 0; sr < n_source_regions_; sr++) { - int material = material_[sr]; + for (int64_t sr = 0; sr < n_source_regions_; sr++) { + int material = source_regions_.material(sr); for (int g_out = 0; g_out < negroups_; g_out++) { double sigma_t = sigma_t_[material * negroups_ + g_out]; double scatter_source = 0.0; for (int g_in = 0; g_in < negroups_; g_in++) { - double scalar_flux = scalar_flux_old_[sr * negroups_ + g_in]; + double scalar_flux = source_regions_.scalar_flux_old(sr, g_in); double sigma_s = sigma_s_[material * negroups_ * negroups_ + g_out * negroups_ + g_in]; scatter_source += sigma_s * scalar_flux; } - source_[sr * negroups_ + g_out] = scatter_source / sigma_t; + source_regions_.source(sr, g_out) = scatter_source / sigma_t; } } // Add fission source #pragma omp parallel for - for (int sr = 0; sr < n_source_regions_; sr++) { - int material = material_[sr]; + for (int64_t sr = 0; sr < n_source_regions_; sr++) { + int material = source_regions_.material(sr); for (int g_out = 0; g_out < negroups_; g_out++) { double sigma_t = sigma_t_[material * negroups_ + g_out]; double fission_source = 0.0; for (int g_in = 0; g_in < negroups_; g_in++) { - double scalar_flux = scalar_flux_old_[sr * negroups_ + g_in]; + double scalar_flux = source_regions_.scalar_flux_old(sr, g_in); double nu_sigma_f = nu_sigma_f_[material * negroups_ + g_in]; double chi = chi_[material * negroups_ + g_out]; fission_source += nu_sigma_f * scalar_flux * chi; } - source_[sr * negroups_ + g_out] += + source_regions_.source(sr, g_out) += fission_source * inverse_k_eff / sigma_t; } } @@ -179,7 +162,7 @@ void FlatSourceDomain::update_neutron_source(double k_eff) if (settings::run_mode == RunMode::FIXED_SOURCE) { #pragma omp parallel for for (int64_t se = 0; se < n_source_elements_; se++) { - source_[se] += external_source_[se]; + source_regions_.source(se) += source_regions_.external_source(se); } } @@ -194,38 +177,42 @@ void FlatSourceDomain::normalize_scalar_flux_and_volumes( double volume_normalization_factor = 1.0 / (total_active_distance_per_iteration * simulation::current_batch); -// Normalize scalar flux to total distance travelled by all rays this iteration +// Normalize scalar flux to total distance travelled by all rays this +// iteration #pragma omp parallel for - for (int64_t se = 0; se < scalar_flux_new_.size(); se++) { - scalar_flux_new_[se] *= normalization_factor; + for (int64_t se = 0; se < n_source_elements_; se++) { + source_regions_.scalar_flux_new(se) *= normalization_factor; } // Accumulate cell-wise ray length tallies collected this iteration, then // update the simulation-averaged cell-wise volume estimates #pragma omp parallel for for (int64_t sr = 0; sr < n_source_regions_; sr++) { - volume_t_[sr] += volume_[sr]; - volume_naive_[sr] = volume_[sr] * normalization_factor; - volume_[sr] = volume_t_[sr] * volume_normalization_factor; + source_regions_.volume_t(sr) += source_regions_.volume(sr); + source_regions_.volume_naive(sr) = + source_regions_.volume(sr) * normalization_factor; + source_regions_.volume(sr) = + source_regions_.volume_t(sr) * volume_normalization_factor; } } void FlatSourceDomain::set_flux_to_flux_plus_source( - int64_t idx, double volume, int material, int g) + int64_t sr, double volume, int g) { - double sigma_t = sigma_t_[material * negroups_ + g]; - scalar_flux_new_[idx] /= (sigma_t * volume); - scalar_flux_new_[idx] += source_[idx]; + double sigma_t = sigma_t_[source_regions_.material(sr) * negroups_ + g]; + source_regions_.scalar_flux_new(sr, g) /= (sigma_t * volume); + source_regions_.scalar_flux_new(sr, g) += source_regions_.source(sr, g); } -void FlatSourceDomain::set_flux_to_old_flux(int64_t idx) +void FlatSourceDomain::set_flux_to_old_flux(int64_t sr, int g) { - scalar_flux_new_[idx] = scalar_flux_old_[idx]; + source_regions_.scalar_flux_new(sr, g) = + source_regions_.scalar_flux_old(sr, g); } -void FlatSourceDomain::set_flux_to_source(int64_t idx) +void FlatSourceDomain::set_flux_to_source(int64_t sr, int g) { - scalar_flux_new_[idx] = source_[idx]; + source_regions_.scalar_flux_new(sr, g) = source_regions_.source(sr, g); } // Combine transport flux contributions and flat source contributions from the @@ -235,20 +222,16 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() int64_t n_hits = 0; #pragma omp parallel for reduction(+ : n_hits) - for (int sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions_; sr++) { - double volume_simulation_avg = volume_[sr]; - double volume_iteration = volume_naive_[sr]; + double volume_simulation_avg = source_regions_.volume(sr); + double volume_iteration = source_regions_.volume_naive(sr); // Increment the number of hits if cell was hit this iteration if (volume_iteration) { n_hits++; } - // Check if an external source is present in this source region - bool external_source_present = - external_source_present_.size() && external_source_present_[sr]; - // The volume treatment depends on the volume estimator type // and whether or not an external source is present in the cell. double volume; @@ -260,7 +243,7 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() volume = volume_simulation_avg; break; case RandomRayVolumeEstimator::HYBRID: - if (external_source_present) { + if (source_regions_.external_source_present(sr)) { volume = volume_iteration; } else { volume = volume_simulation_avg; @@ -270,17 +253,14 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() fatal_error("Invalid volume estimator type"); } - int material = material_[sr]; for (int g = 0; g < negroups_; g++) { - int64_t idx = (sr * negroups_) + g; - // There are three scenarios we need to consider: if (volume_iteration > 0.0) { // 1. If the FSR was hit this iteration, then the new flux is equal to // the flat source from the previous iteration plus the contributions // from rays passing through the source region (computed during the // transport sweep) - set_flux_to_flux_plus_source(idx, volume, material, g); + set_flux_to_flux_plus_source(sr, volume, g); } else if (volume_simulation_avg > 0.0) { // 2. If the FSR was not hit this iteration, but has been hit some // previous iteration, then we need to make a choice about what @@ -294,10 +274,10 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() // in the cell we will use the previous iteration's flux estimate. This // injects a small degree of correlation into the simulation, but this // is going to be trivial when the miss rate is a few percent or less. - if (external_source_present) { - set_flux_to_old_flux(idx); + if (source_regions_.external_source_present(sr)) { + set_flux_to_old_flux(sr, g); } else { - set_flux_to_source(idx); + set_flux_to_source(sr, g); } } // If the FSR was not hit this iteration, and it has never been hit in @@ -322,24 +302,25 @@ double FlatSourceDomain::compute_k_eff(double k_eff_old) const vector p(n_source_regions_, 0.0f); #pragma omp parallel for reduction(+ : fission_rate_old, fission_rate_new) - for (int sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions_; sr++) { // If simulation averaged volume is zero, don't include this cell - double volume = volume_[sr]; + double volume = source_regions_.volume(sr); if (volume == 0.0) { continue; } - int material = material_[sr]; + int material = source_regions_.material(sr); double sr_fission_source_old = 0; double sr_fission_source_new = 0; for (int g = 0; g < negroups_; g++) { - int64_t idx = (sr * negroups_) + g; double nu_sigma_f = nu_sigma_f_[material * negroups_ + g]; - sr_fission_source_old += nu_sigma_f * scalar_flux_old_[idx]; - sr_fission_source_new += nu_sigma_f * scalar_flux_new_[idx]; + sr_fission_source_old += + nu_sigma_f * source_regions_.scalar_flux_old(sr, g); + sr_fission_source_new += + nu_sigma_f * source_regions_.scalar_flux_new(sr, g); } // Compute total fission rates in FSR @@ -361,7 +342,7 @@ double FlatSourceDomain::compute_k_eff(double k_eff_old) const double inverse_sum = 1 / fission_rate_new; #pragma omp parallel for reduction(+ : H) - for (int sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions_; sr++) { // Only if FSR has non-negative and non-zero fission source if (p[sr] > 0.0f) { // Normalize to total weight of bank sites. p_i for better performance @@ -419,11 +400,11 @@ void FlatSourceDomain::convert_source_regions_to_tallies() // Attempt to generate mapping for all source regions #pragma omp parallel for - for (int sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions_; sr++) { // If this source region has not been hit by a ray yet, then // we aren't going to be able to map it, so skip it. - if (!position_recorded_[sr]) { + if (!source_regions_.position_recorded(sr)) { all_source_regions_mapped = false; continue; } @@ -432,8 +413,8 @@ void FlatSourceDomain::convert_source_regions_to_tallies() // crossing through this source region is used to estabilish // the spatial location of the source region Particle p; - p.r() = position_[sr]; - p.r_last() = position_[sr]; + p.r() = source_regions_.position(sr); + p.r_last() = source_regions_.position(sr); bool found = exhaustive_find_cell(p); // Loop over energy groups (so as to support energy filters) @@ -449,7 +430,7 @@ void FlatSourceDomain::convert_source_regions_to_tallies() // If this task has already been populated, we don't need to do // it again. - if (tally_task_[source_element].size() > 0) { + if (source_regions_.tally_task(sr, g).size() > 0) { continue; } @@ -479,11 +460,11 @@ void FlatSourceDomain::convert_source_regions_to_tallies() // If a valid tally, filter, and score combination has been found, // then add it to the list of tally tasks for this source element. TallyTask task(i_tally, filter_index, score_index, score_bin); - tally_task_[source_element].push_back(task); + source_regions_.tally_task(sr, g).push_back(task); // Also add this task to the list of volume tasks for this source // region. - volume_task_[sr].insert(task); + source_regions_.volume_task(sr).insert(task); } } } @@ -529,13 +510,13 @@ double FlatSourceDomain::compute_fixed_source_normalization_factor() const // total external source strength in the simulation. double simulation_external_source_strength = 0.0; #pragma omp parallel for reduction(+ : simulation_external_source_strength) - for (int sr = 0; sr < n_source_regions_; sr++) { - int material = material_[sr]; - double volume = volume_[sr] * simulation_volume_; + for (int64_t sr = 0; sr < n_source_regions_; sr++) { + int material = source_regions_.material(sr); + double volume = source_regions_.volume(sr) * simulation_volume_; for (int g = 0; g < negroups_; g++) { double sigma_t = sigma_t_[material * negroups_ + g]; simulation_external_source_strength += - external_source_[sr * negroups_ + g] * sigma_t * volume; + source_regions_.external_source(sr, g) * sigma_t * volume; } } @@ -576,7 +557,7 @@ void FlatSourceDomain::random_ray_tally() // element, we check if there are any scores needed and apply // them. #pragma omp parallel for - for (int sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions_; sr++) { // The fsr.volume_ is the unitless fractional simulation averaged volume // (i.e., it is the FSR's fraction of the overall simulation volume). The // simulation_volume_ is the total 3D physical volume in cm^3 of the @@ -586,15 +567,15 @@ void FlatSourceDomain::random_ray_tally() // eigenvalue solves, but useful in fixed source solves for returning the // flux shape with a magnitude that makes sense relative to the fixed // source strength. - double volume = volume_[sr] * simulation_volume_; + double volume = source_regions_.volume(sr) * simulation_volume_; - double material = material_[sr]; + double material = source_regions_.material(sr); for (int g = 0; g < negroups_; g++) { - int idx = sr * negroups_ + g; - double flux = scalar_flux_new_[idx] * source_normalization_factor; + double flux = + source_regions_.scalar_flux_new(sr, g) * source_normalization_factor; // Determine numerical score value - for (auto& task : tally_task_[idx]) { + for (auto& task : source_regions_.tally_task(sr, g)) { double score; switch (task.score_type) { @@ -637,7 +618,7 @@ void FlatSourceDomain::random_ray_tally() // for normalizing the flux. We store this volume in a separate tensor. // We only contribute to each volume tally bin once per FSR. if (volume_normalized_flux_tallies_) { - for (const auto& task : volume_task_[sr]) { + for (const auto& task : source_regions_.volume_task(sr)) { if (task.score_type == SCORE_FLUX) { #pragma omp atomic tally_volumes_[task.tally_idx](task.filter_idx, task.score_idx) += @@ -676,7 +657,6 @@ void FlatSourceDomain::random_ray_tally() void FlatSourceDomain::all_reduce_replicated_source_regions() { #ifdef OPENMC_MPI - // If we only have 1 MPI rank, no need // to reduce anything. if (mpi::n_procs <= 1) @@ -684,80 +664,15 @@ void FlatSourceDomain::all_reduce_replicated_source_regions() simulation::time_bank_sendrecv.start(); - // The "position_recorded" variable needs to be allreduced (and maxed), - // as whether or not a cell was hit will affect some decisions in how the - // source is calculated in the next iteration so as to avoid dividing - // by zero. We take the max rather than the sum as the hit values are - // expected to be zero or 1. - MPI_Allreduce(MPI_IN_PLACE, position_recorded_.data(), n_source_regions_, - MPI_INT, MPI_MAX, mpi::intracomm); - - // The position variable is more complicated to reduce than the others, - // as we do not want the sum of all positions in each cell, rather, we - // want to just pick any single valid position. Thus, we perform a gather - // and then pick the first valid position we find for all source regions - // that have had a position recorded. This operation does not need to - // be broadcast back to other ranks, as this value is only used for the - // tally conversion operation, which is only performed on the master rank. - // While this is expensive, it only needs to be done for active batches, - // and only if we have not mapped all the tallies yet. Once tallies are - // fully mapped, then the position vector is fully populated, so this - // operation can be skipped. - // First, we broadcast the fully mapped tally status variable so that // all ranks are on the same page int mapped_all_tallies_i = static_cast(mapped_all_tallies_); MPI_Bcast(&mapped_all_tallies_i, 1, MPI_INT, 0, mpi::intracomm); - // Then, we perform the gather of position data, if needed - if (simulation::current_batch > settings::n_inactive && - !mapped_all_tallies_i) { + bool reduce_position = + simulation::current_batch > settings::n_inactive && !mapped_all_tallies_i; - // Master rank will gather results and pick valid positions - if (mpi::master) { - // Initialize temporary vector for receiving positions - vector> all_position; - all_position.resize(mpi::n_procs); - for (int i = 0; i < mpi::n_procs; i++) { - all_position[i].resize(n_source_regions_); - } - - // Copy master rank data into gathered vector for convenience - all_position[0] = position_; - - // Receive all data into gather vector - for (int i = 1; i < mpi::n_procs; i++) { - MPI_Recv(all_position[i].data(), n_source_regions_ * 3, MPI_DOUBLE, i, - 0, mpi::intracomm, MPI_STATUS_IGNORE); - } - - // Scan through gathered data and pick first valid cell posiiton - for (int sr = 0; sr < n_source_regions_; sr++) { - if (position_recorded_[sr] == 1) { - for (int i = 0; i < mpi::n_procs; i++) { - if (all_position[i][sr].x != 0.0 || all_position[i][sr].y != 0.0 || - all_position[i][sr].z != 0.0) { - position_[sr] = all_position[i][sr]; - break; - } - } - } - } - } else { - // Other ranks just send in their data - MPI_Send(position_.data(), n_source_regions_ * 3, MPI_DOUBLE, 0, 0, - mpi::intracomm); - } - } - - // For the rest of the source region data, we simply perform an all reduce, - // as these values will be needed on all ranks for transport during the - // next iteration. - MPI_Allreduce(MPI_IN_PLACE, volume_.data(), n_source_regions_, MPI_DOUBLE, - MPI_SUM, mpi::intracomm); - - MPI_Allreduce(MPI_IN_PLACE, scalar_flux_new_.data(), n_source_elements_, - MPI_DOUBLE, MPI_SUM, mpi::intracomm); + source_regions_.mpi_sync_ranks(reduce_position); simulation::time_bank_sendrecv.stop(); #endif @@ -766,7 +681,7 @@ void FlatSourceDomain::all_reduce_replicated_source_regions() double FlatSourceDomain::evaluate_flux_at_point( Position r, int64_t sr, int g) const { - return scalar_flux_final_[sr * negroups_ + g] / + return source_regions_.scalar_flux_final(sr, g) / (settings::n_batches - settings::n_inactive); } @@ -894,7 +809,7 @@ void FlatSourceDomain::output_to_vtk() const std::fprintf(plot, "SCALARS Materials int\n"); std::fprintf(plot, "LOOKUP_TABLE default\n"); for (int fsr : voxel_indices) { - int mat = material_[fsr]; + int mat = source_regions_.material(fsr); mat = convert_to_big_endian(mat); std::fwrite(&mat, sizeof(int), 1, plot); } @@ -906,7 +821,7 @@ void FlatSourceDomain::output_to_vtk() const int64_t fsr = voxel_indices[i]; float total_fission = 0.0; - int mat = material_[fsr]; + int mat = source_regions_.material(fsr); for (int g = 0; g < negroups_; g++) { int64_t source_element = fsr * negroups_ + g; float flux = evaluate_flux_at_point(voxel_positions[i], fsr, g); @@ -922,16 +837,16 @@ void FlatSourceDomain::output_to_vtk() const } void FlatSourceDomain::apply_external_source_to_source_region( - Discrete* discrete, double strength_factor, int64_t source_region) + Discrete* discrete, double strength_factor, int64_t sr) { - external_source_present_[source_region] = true; + source_regions_.external_source_present(sr) = 1; const auto& discrete_energies = discrete->x(); const auto& discrete_probs = discrete->prob(); for (int i = 0; i < discrete_energies.size(); i++) { int g = data::mg.get_group_index(discrete_energies[i]); - external_source_[source_region * negroups_ + g] += + source_regions_.external_source(sr, g) += discrete_probs[i] * strength_factor; } } @@ -983,8 +898,8 @@ void FlatSourceDomain::count_external_source_regions() { n_external_source_regions_ = 0; #pragma omp parallel for reduction(+ : n_external_source_regions_) - for (int sr = 0; sr < n_source_regions_; sr++) { - if (external_source_present_[sr]) { + for (int64_t sr = 0; sr < n_source_regions_; sr++) { + if (source_regions_.external_source_present(sr)) { n_external_source_regions_++; } } @@ -1029,18 +944,17 @@ void FlatSourceDomain::convert_external_sources() // Divide the fixed source term by sigma t (to save time when applying each // iteration) #pragma omp parallel for - for (int sr = 0; sr < n_source_regions_; sr++) { - int material = material_[sr]; + for (int64_t sr = 0; sr < n_source_regions_; sr++) { for (int g = 0; g < negroups_; g++) { - double sigma_t = sigma_t_[material * negroups_ + g]; - external_source_[sr * negroups_ + g] /= sigma_t; + double sigma_t = sigma_t_[source_regions_.material(sr) * negroups_ + g]; + source_regions_.external_source(sr, g) /= sigma_t; } } } void FlatSourceDomain::flux_swap() { - scalar_flux_old_.swap(scalar_flux_new_); + source_regions_.flux_swap(); } void FlatSourceDomain::flatten_xs() @@ -1096,17 +1010,16 @@ void FlatSourceDomain::set_adjoint_sources(const vector& forward_flux) // so we must convert it to a "per batch" quantity #pragma omp parallel for for (int64_t se = 0; se < n_source_elements_; se++) { - external_source_[se] = 1.0 / forward_flux[se]; + source_regions_.external_source(se) = 1.0 / forward_flux[se]; } // Divide the fixed source term by sigma t (to save time when applying each // iteration) #pragma omp parallel for - for (int sr = 0; sr < n_source_regions_; sr++) { - int material = material_[sr]; + for (int64_t sr = 0; sr < n_source_regions_; sr++) { for (int g = 0; g < negroups_; g++) { - double sigma_t = sigma_t_[material * negroups_ + g]; - external_source_[sr * negroups_ + g] /= sigma_t; + double sigma_t = sigma_t_[source_regions_.material(sr) * negroups_ + g]; + source_regions_.external_source(sr, g) /= sigma_t; } } } @@ -1129,4 +1042,15 @@ void FlatSourceDomain::transpose_scattering_matrix() } } +void FlatSourceDomain::serialize_final_fluxes(vector& flux) +{ + // Ensure array is correct size + flux.resize(n_source_regions_ * negroups_); +// Serialize the final fluxes for output +#pragma omp parallel for + for (int64_t se = 0; se < n_source_elements_; se++) { + flux[se] = source_regions_.scalar_flux_final(se); + } +} + } // namespace openmc diff --git a/src/random_ray/linear_source_domain.cpp b/src/random_ray/linear_source_domain.cpp index f3f6fa068..fb6502a3e 100644 --- a/src/random_ray/linear_source_domain.cpp +++ b/src/random_ray/linear_source_domain.cpp @@ -20,33 +20,17 @@ namespace openmc { // LinearSourceDomain implementation //============================================================================== -LinearSourceDomain::LinearSourceDomain() : FlatSourceDomain() -{ - // First order spatial moment of the scalar flux - flux_moments_old_.assign(n_source_elements_, {0.0, 0.0, 0.0}); - flux_moments_new_.assign(n_source_elements_, {0.0, 0.0, 0.0}); - flux_moments_t_.assign(n_source_elements_, {0.0, 0.0, 0.0}); - // Source gradients given by M inverse multiplied by source moments - source_gradients_.assign(n_source_elements_, {0.0, 0.0, 0.0}); - - centroid_.assign(n_source_regions_, {0.0, 0.0, 0.0}); - centroid_iteration_.assign(n_source_regions_, {0.0, 0.0, 0.0}); - centroid_t_.assign(n_source_regions_, {0.0, 0.0, 0.0}); - mom_matrix_.assign(n_source_regions_, {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}); - mom_matrix_t_.assign(n_source_regions_, {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}); -} - void LinearSourceDomain::batch_reset() { FlatSourceDomain::batch_reset(); #pragma omp parallel for - for (int64_t se = 0; se < n_source_elements_; se++) { - flux_moments_new_[se] = {0.0, 0.0, 0.0}; + for (int64_t sr = 0; sr < n_source_regions_; sr++) { + source_regions_.centroid_iteration(sr) = {0.0, 0.0, 0.0}; + source_regions_.mom_matrix(sr) = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; } #pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions_; sr++) { - centroid_iteration_[sr] = {0.0, 0.0, 0.0}; - mom_matrix_[sr] = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; + for (int64_t se = 0; se < n_source_elements_; se++) { + source_regions_.flux_moments_new(se) = {0.0, 0.0, 0.0}; } } @@ -57,10 +41,9 @@ void LinearSourceDomain::update_neutron_source(double k_eff) double inverse_k_eff = 1.0 / k_eff; #pragma omp parallel for - for (int sr = 0; sr < n_source_regions_; sr++) { - - int material = material_[sr]; - MomentMatrix invM = mom_matrix_[sr].inverse(); + for (int64_t sr = 0; sr < n_source_regions_; sr++) { + int material = source_regions_.material(sr); + MomentMatrix invM = source_regions_.mom_matrix(sr).inverse(); for (int g_out = 0; g_out < negroups_; g_out++) { double sigma_t = sigma_t_[material * negroups_ + g_out]; @@ -72,8 +55,8 @@ void LinearSourceDomain::update_neutron_source(double k_eff) for (int g_in = 0; g_in < negroups_; g_in++) { // Handles for the flat and linear components of the flux - double flux_flat = scalar_flux_old_[sr * negroups_ + g_in]; - MomentArray flux_linear = flux_moments_old_[sr * negroups_ + g_in]; + double flux_flat = source_regions_.scalar_flux_old(sr, g_in); + MomentArray flux_linear = source_regions_.flux_moments_old(sr, g_in); // Handles for cross sections double sigma_s = @@ -89,7 +72,7 @@ void LinearSourceDomain::update_neutron_source(double k_eff) } // Compute the flat source term - source_[sr * negroups_ + g_out] = + source_regions_.source(sr, g_out) = (scatter_flat + fission_flat * inverse_k_eff) / sigma_t; // Compute the linear source terms @@ -97,7 +80,7 @@ void LinearSourceDomain::update_neutron_source(double k_eff) // are not well known, we will leave the source gradients as zero // so as to avoid causing any numerical instability. if (simulation::current_batch > 10) { - source_gradients_[sr * negroups_ + g_out] = + source_regions_.source_gradients(sr, g_out) = invM * ((scatter_linear + fission_linear * inverse_k_eff) / sigma_t); } } @@ -107,7 +90,7 @@ void LinearSourceDomain::update_neutron_source(double k_eff) // Add external source to flat source term if in fixed source mode #pragma omp parallel for for (int64_t se = 0; se < n_source_elements_; se++) { - source_[se] += external_source_[se]; + source_regions_.source(se) += source_regions_.external_source(se); } } @@ -123,48 +106,44 @@ void LinearSourceDomain::normalize_scalar_flux_and_volumes( // Normalize flux to total distance travelled by all rays this iteration #pragma omp parallel for - for (int64_t se = 0; se < scalar_flux_new_.size(); se++) { - scalar_flux_new_[se] *= normalization_factor; - flux_moments_new_[se] *= normalization_factor; + for (int64_t se = 0; se < n_source_elements_; se++) { + source_regions_.scalar_flux_new(se) *= normalization_factor; + source_regions_.flux_moments_new(se) *= normalization_factor; } // Accumulate cell-wise ray length tallies collected this iteration, then // update the simulation-averaged cell-wise volume estimates #pragma omp parallel for for (int64_t sr = 0; sr < n_source_regions_; sr++) { - centroid_t_[sr] += centroid_iteration_[sr]; - mom_matrix_t_[sr] += mom_matrix_[sr]; - volume_t_[sr] += volume_[sr]; - volume_naive_[sr] = volume_[sr] * normalization_factor; - volume_[sr] = volume_t_[sr] * volume_normalization_factor; - if (volume_t_[sr] > 0.0) { - double inv_volume = 1.0 / volume_t_[sr]; - centroid_[sr] = centroid_t_[sr]; - centroid_[sr] *= inv_volume; - mom_matrix_[sr] = mom_matrix_t_[sr]; - mom_matrix_[sr] *= inv_volume; + source_regions_.centroid_t(sr) += source_regions_.centroid_iteration(sr); + source_regions_.mom_matrix_t(sr) += source_regions_.mom_matrix(sr); + source_regions_.volume_t(sr) += source_regions_.volume(sr); + source_regions_.volume_naive(sr) = + source_regions_.volume(sr) * normalization_factor; + source_regions_.volume(sr) = + source_regions_.volume_t(sr) * volume_normalization_factor; + if (source_regions_.volume_t(sr) > 0.0) { + double inv_volume = 1.0 / source_regions_.volume_t(sr); + source_regions_.centroid(sr) = source_regions_.centroid_t(sr); + source_regions_.centroid(sr) *= inv_volume; + source_regions_.mom_matrix(sr) = source_regions_.mom_matrix_t(sr); + source_regions_.mom_matrix(sr) *= inv_volume; } } } void LinearSourceDomain::set_flux_to_flux_plus_source( - int64_t idx, double volume, int material, int g) + int64_t sr, double volume, int g) { - scalar_flux_new_[idx] /= volume; - scalar_flux_new_[idx] += source_[idx]; - flux_moments_new_[idx] *= (1.0 / volume); + source_regions_.scalar_flux_new(sr, g) /= volume; + source_regions_.scalar_flux_new(sr, g) += source_regions_.source(sr, g); + source_regions_.flux_moments_new(sr, g) *= (1.0 / volume); } -void LinearSourceDomain::set_flux_to_old_flux(int64_t idx) +void LinearSourceDomain::set_flux_to_old_flux(int64_t sr, int g) { - scalar_flux_new_[idx] = scalar_flux_old_[idx]; - flux_moments_new_[idx] = flux_moments_old_[idx]; -} - -void LinearSourceDomain::flux_swap() -{ - FlatSourceDomain::flux_swap(); - flux_moments_old_.swap(flux_moments_new_); + source_regions_.scalar_flux_new(g) = source_regions_.scalar_flux_old(g); + source_regions_.flux_moments_new(g) = source_regions_.flux_moments_old(g); } void LinearSourceDomain::accumulate_iteration_flux() @@ -175,48 +154,20 @@ void LinearSourceDomain::accumulate_iteration_flux() // Accumulate scalar flux moments #pragma omp parallel for for (int64_t se = 0; se < n_source_elements_; se++) { - flux_moments_t_[se] += flux_moments_new_[se]; + source_regions_.flux_moments_t(se) += source_regions_.flux_moments_new(se); } } -void LinearSourceDomain::all_reduce_replicated_source_regions() -{ -#ifdef OPENMC_MPI - FlatSourceDomain::all_reduce_replicated_source_regions(); - simulation::time_bank_sendrecv.start(); - - // We are going to assume we can safely cast Position, MomentArray, - // and MomentMatrix to contiguous arrays of doubles for the MPI - // allreduce operation. This is a safe assumption as typically - // compilers will at most pad to 8 byte boundaries. If a new FP32 MomentArray - // type is introduced, then there will likely be padding, in which case this - // function will need to become more complex. - if (sizeof(MomentArray) != 3 * sizeof(double) || - sizeof(MomentMatrix) != 6 * sizeof(double)) { - fatal_error("Unexpected buffer padding in linear source domain reduction."); - } - - MPI_Allreduce(MPI_IN_PLACE, static_cast(flux_moments_new_.data()), - n_source_elements_ * 3, MPI_DOUBLE, MPI_SUM, mpi::intracomm); - MPI_Allreduce(MPI_IN_PLACE, static_cast(mom_matrix_.data()), - n_source_regions_ * 6, MPI_DOUBLE, MPI_SUM, mpi::intracomm); - MPI_Allreduce(MPI_IN_PLACE, static_cast(centroid_iteration_.data()), - n_source_regions_ * 3, MPI_DOUBLE, MPI_SUM, mpi::intracomm); - - simulation::time_bank_sendrecv.stop(); -#endif -} - double LinearSourceDomain::evaluate_flux_at_point( Position r, int64_t sr, int g) const { double phi_flat = FlatSourceDomain::evaluate_flux_at_point(r, sr, g); - Position local_r = r - centroid_[sr]; - MomentArray phi_linear = flux_moments_t_[sr * negroups_ + g]; + Position local_r = r - source_regions_.centroid(sr); + MomentArray phi_linear = source_regions_.flux_moments_t(sr, g); phi_linear *= 1.0 / (settings::n_batches - settings::n_inactive); - MomentMatrix invM = mom_matrix_[sr].inverse(); + MomentMatrix invM = source_regions_.mom_matrix(sr).inverse(); MomentArray phi_solved = invM * phi_linear; return phi_flat + phi_solved.dot(local_r); diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp index 7a359f356..9c2a890ec 100644 --- a/src/random_ray/random_ray.cpp +++ b/src/random_ray/random_ray.cpp @@ -309,11 +309,9 @@ void RandomRay::attenuate_flux_flat_source(double distance, bool is_active) int i_cell = lowest_coord().cell; // The source region is the spatial region index - int64_t source_region = - domain_->source_region_offsets_[i_cell] + cell_instance(); + int64_t sr = domain_->source_region_offsets_[i_cell] + cell_instance(); // The source element is the energy-specific region index - int64_t source_element = source_region * negroups_; int material = this->material(); // MOC incoming flux attenuation + source contribution/attenuation equation @@ -322,7 +320,7 @@ void RandomRay::attenuate_flux_flat_source(double distance, bool is_active) float tau = sigma_t * distance; float exponential = cjosey_exponential(tau); // exponential = 1 - exp(-tau) float new_delta_psi = - (angular_flux_[g] - domain_->source_[source_element + g]) * exponential; + (angular_flux_[g] - domain_->source_regions_.source(sr, g)) * exponential; delta_psi_[g] = new_delta_psi; angular_flux_[g] -= new_delta_psi; } @@ -332,28 +330,28 @@ void RandomRay::attenuate_flux_flat_source(double distance, bool is_active) if (is_active) { // Aquire lock for source region - domain_->lock_[source_region].lock(); + domain_->source_regions_.lock(sr).lock(); // Accumulate delta psi into new estimate of source region flux for // this iteration for (int g = 0; g < negroups_; g++) { - domain_->scalar_flux_new_[source_element + g] += delta_psi_[g]; + domain_->source_regions_.scalar_flux_new(sr, g) += delta_psi_[g]; } // Accomulate volume (ray distance) into this iteration's estimate // of the source region's volume - domain_->volume_[source_region] += distance; + domain_->source_regions_.volume(sr) += distance; // Tally valid position inside the source region (e.g., midpoint of // the ray) if not done already - if (!domain_->position_recorded_[source_region]) { + if (!domain_->source_regions_.position_recorded(sr)) { Position midpoint = r() + u() * (distance / 2.0); - domain_->position_[source_region] = midpoint; - domain_->position_recorded_[source_region] = 1; + domain_->source_regions_.position(sr) = midpoint; + domain_->source_regions_.position_recorded(sr) = 1; } // Release lock - domain_->lock_[source_region].unlock(); + domain_->source_regions_.lock(sr).unlock(); } } @@ -373,14 +371,12 @@ void RandomRay::attenuate_flux_linear_source(double distance, bool is_active) int i_cell = lowest_coord().cell; // The source region is the spatial region index - int64_t source_region = - domain_->source_region_offsets_[i_cell] + cell_instance(); + int64_t sr = domain_->source_region_offsets_[i_cell] + cell_instance(); // The source element is the energy-specific region index - int64_t source_element = source_region * negroups_; int material = this->material(); - Position& centroid = domain->centroid_[source_region]; + Position& centroid = domain_->source_regions_.centroid(sr); Position midpoint = r() + u() * (distance / 2.0); // Determine the local position of the midpoint and the ray origin @@ -393,7 +389,7 @@ void RandomRay::attenuate_flux_linear_source(double distance, bool is_active) // be no estimate of its centroid. We detect this by checking if it has // any accumulated volume. If its volume is zero, just use the midpoint // of the ray as the region's centroid. - if (domain->volume_t_[source_region]) { + if (domain_->source_regions_.volume_t(sr)) { rm_local = midpoint - centroid; r0_local = r() - centroid; } else { @@ -420,9 +416,10 @@ void RandomRay::attenuate_flux_linear_source(double distance, bool is_active) // calculated from the source gradients dot product with local centroid // and direction, respectively. float spatial_source = - domain_->source_[source_element + g] + - rm_local.dot(domain->source_gradients_[source_element + g]); - float dir_source = u().dot(domain->source_gradients_[source_element + g]); + domain_->source_regions_.source(sr, g) + + rm_local.dot(domain_->source_regions_.source_gradients(sr, g)); + float dir_source = + u().dot(domain_->source_regions_.source_gradients(sr, g)); float gn = exponentialG(tau); float f1 = 1.0f - tau * gn; @@ -465,33 +462,33 @@ void RandomRay::attenuate_flux_linear_source(double distance, bool is_active) rm_local, u(), distance); // Aquire lock for source region - domain_->lock_[source_region].lock(); + domain_->source_regions_.lock(sr).lock(); // Accumulate deltas into the new estimate of source region flux for this // iteration for (int g = 0; g < negroups_; g++) { - domain_->scalar_flux_new_[source_element + g] += delta_psi_[g]; - domain->flux_moments_new_[source_element + g] += delta_moments_[g]; + domain_->source_regions_.scalar_flux_new(sr, g) += delta_psi_[g]; + domain_->source_regions_.flux_moments_new(sr, g) += delta_moments_[g]; } // Accumulate the volume (ray segment distance), centroid, and spatial // momement estimates into the running totals for the iteration for this // source region. The centroid and spatial momements estimates are scaled by // the ray segment length as part of length averaging of the estimates. - domain_->volume_[source_region] += distance; - domain->centroid_iteration_[source_region] += midpoint * distance; + domain_->source_regions_.volume(sr) += distance; + domain_->source_regions_.centroid_iteration(sr) += midpoint * distance; moment_matrix_estimate *= distance; - domain->mom_matrix_[source_region] += moment_matrix_estimate; + domain_->source_regions_.mom_matrix(sr) += moment_matrix_estimate; // Tally valid position inside the source region (e.g., midpoint of // the ray) if not done already - if (!domain_->position_recorded_[source_region]) { - domain_->position_[source_region] = midpoint; - domain_->position_recorded_[source_region] = 1; + if (!domain_->source_regions_.position_recorded(sr)) { + domain_->source_regions_.position(sr) = midpoint; + domain_->source_regions_.position_recorded(sr) = 1; } // Release lock - domain_->lock_[source_region].unlock(); + domain_->source_regions_.lock(sr).unlock(); } } @@ -537,11 +534,10 @@ void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain) // Initialize ray's starting angular flux to starting location's isotropic // source int i_cell = lowest_coord().cell; - int64_t source_region_idx = - domain_->source_region_offsets_[i_cell] + cell_instance(); + int64_t sr = domain_->source_region_offsets_[i_cell] + cell_instance(); for (int g = 0; g < negroups_; g++) { - angular_flux_[g] = domain_->source_[source_region_idx * negroups_ + g]; + angular_flux_[g] = domain_->source_regions_.source(sr, g); } } diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index a8e6d1b82..c4f719b23 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -66,7 +66,7 @@ void openmc_run_random_ray() simulation::time_total.stop(); // Normalize and save the final forward flux - forward_flux = sim.domain()->scalar_flux_final_; + sim.domain()->serialize_final_fluxes(forward_flux); double source_normalization_factor = sim.domain()->compute_fixed_source_normalization_factor() / diff --git a/src/random_ray/source_region.cpp b/src/random_ray/source_region.cpp new file mode 100644 index 000000000..0a20af8a9 --- /dev/null +++ b/src/random_ray/source_region.cpp @@ -0,0 +1,236 @@ +#include "openmc/random_ray/source_region.h" + +#include "openmc/error.h" +#include "openmc/message_passing.h" + +namespace openmc { + +//============================================================================== +// SourceRegion implementation +//============================================================================== +SourceRegion::SourceRegion(int negroups, bool is_linear) +{ + if (settings::run_mode == RunMode::EIGENVALUE) { + // If in eigenvalue mode, set starting flux to guess of 1 + scalar_flux_old_.assign(negroups, 1.0); + } else { + // If in fixed source mode, set starting flux to guess of zero + // and initialize external source arrays + scalar_flux_old_.assign(negroups, 0.0); + external_source_.assign(negroups, 0.0); + } + + scalar_flux_new_.assign(negroups, 0.0); + source_.resize(negroups); + scalar_flux_final_.assign(negroups, 0.0); + + tally_task_.resize(negroups); + + if (is_linear) { + source_gradients_.resize(negroups); + flux_moments_old_.resize(negroups); + flux_moments_new_.resize(negroups); + flux_moments_t_.resize(negroups); + } +} + +//============================================================================== +// SourceRegionContainer implementation +//============================================================================== +void SourceRegionContainer::push_back(const SourceRegion& sr) +{ + n_source_regions_++; + + // Scalar fields + material_.push_back(sr.material_); + lock_.push_back(sr.lock_); + volume_.push_back(sr.volume_); + volume_t_.push_back(sr.volume_t_); + volume_naive_.push_back(sr.volume_naive_); + position_recorded_.push_back(sr.position_recorded_); + external_source_present_.push_back(sr.external_source_present_); + position_.push_back(sr.position_); + volume_task_.push_back(sr.volume_task_); + + // Only store these fields if is_linear_ is true + if (is_linear_) { + centroid_.push_back(sr.centroid_); + centroid_iteration_.push_back(sr.centroid_iteration_); + centroid_t_.push_back(sr.centroid_t_); + mom_matrix_.push_back(sr.mom_matrix_); + mom_matrix_t_.push_back(sr.mom_matrix_t_); + } + + // Energy-dependent fields + for (int g = 0; g < negroups_; ++g) { + scalar_flux_old_.push_back(sr.scalar_flux_old_[g]); + scalar_flux_new_.push_back(sr.scalar_flux_new_[g]); + scalar_flux_final_.push_back(sr.scalar_flux_final_[g]); + source_.push_back(sr.source_[g]); + if (settings::run_mode == RunMode::FIXED_SOURCE) { + external_source_.push_back(sr.external_source_[g]); + } + + // Only store these fields if is_linear_ is true + if (is_linear_) { + source_gradients_.push_back(sr.source_gradients_[g]); + flux_moments_old_.push_back(sr.flux_moments_old_[g]); + flux_moments_new_.push_back(sr.flux_moments_new_[g]); + flux_moments_t_.push_back(sr.flux_moments_t_[g]); + } + + // Tally tasks + tally_task_.emplace_back(sr.tally_task_[g]); + } +} + +void SourceRegionContainer::assign( + int n_source_regions, const SourceRegion& source_region) +{ + // Clear existing data + n_source_regions_ = 0; + material_.clear(); + lock_.clear(); + volume_.clear(); + volume_t_.clear(); + volume_naive_.clear(); + position_recorded_.clear(); + external_source_present_.clear(); + position_.clear(); + + if (is_linear_) { + centroid_.clear(); + centroid_iteration_.clear(); + centroid_t_.clear(); + mom_matrix_.clear(); + mom_matrix_t_.clear(); + } + + scalar_flux_old_.clear(); + scalar_flux_new_.clear(); + scalar_flux_final_.clear(); + source_.clear(); + external_source_.clear(); + + if (is_linear_) { + source_gradients_.clear(); + flux_moments_old_.clear(); + flux_moments_new_.clear(); + flux_moments_t_.clear(); + } + + tally_task_.clear(); + volume_task_.clear(); + + // Fill with copies of source_region + for (int i = 0; i < n_source_regions; ++i) { + push_back(source_region); + } +} + +void SourceRegionContainer::flux_swap() +{ + scalar_flux_old_.swap(scalar_flux_new_); + if (is_linear_) { + flux_moments_old_.swap(flux_moments_new_); + } +} + +void SourceRegionContainer::mpi_sync_ranks(bool reduce_position) +{ +#ifdef OPENMC_MPI + + // The "position_recorded" variable needs to be allreduced (and maxed), + // as whether or not a cell was hit will affect some decisions in how the + // source is calculated in the next iteration so as to avoid dividing + // by zero. We take the max rather than the sum as the hit values are + // expected to be zero or 1. + MPI_Allreduce(MPI_IN_PLACE, position_recorded_.data(), 1, MPI_INT, MPI_MAX, + mpi::intracomm); + + // The position variable is more complicated to reduce than the others, + // as we do not want the sum of all positions in each cell, rather, we + // want to just pick any single valid position. Thus, we perform a gather + // and then pick the first valid position we find for all source regions + // that have had a position recorded. This operation does not need to + // be broadcast back to other ranks, as this value is only used for the + // tally conversion operation, which is only performed on the master rank. + // While this is expensive, it only needs to be done for active batches, + // and only if we have not mapped all the tallies yet. Once tallies are + // fully mapped, then the position vector is fully populated, so this + // operation can be skipped. + + // Then, we perform the gather of position data, if needed + if (reduce_position) { + + // Master rank will gather results and pick valid positions + if (mpi::master) { + // Initialize temporary vector for receiving positions + vector> all_position; + all_position.resize(mpi::n_procs); + for (int i = 0; i < mpi::n_procs; i++) { + all_position[i].resize(n_source_regions_); + } + + // Copy master rank data into gathered vector for convenience + all_position[0] = position_; + + // Receive all data into gather vector + for (int i = 1; i < mpi::n_procs; i++) { + MPI_Recv(all_position[i].data(), n_source_regions_ * 3, MPI_DOUBLE, i, + 0, mpi::intracomm, MPI_STATUS_IGNORE); + } + + // Scan through gathered data and pick first valid cell posiiton + for (int64_t sr = 0; sr < n_source_regions_; sr++) { + if (position_recorded_[sr] == 1) { + for (int i = 0; i < mpi::n_procs; i++) { + if (all_position[i][sr].x != 0.0 || all_position[i][sr].y != 0.0 || + all_position[i][sr].z != 0.0) { + position_[sr] = all_position[i][sr]; + break; + } + } + } + } + } else { + // Other ranks just send in their data + MPI_Send(position_.data(), n_source_regions_ * 3, MPI_DOUBLE, 0, 0, + mpi::intracomm); + } + } + + // For the rest of the source region data, we simply perform an all reduce, + // as these values will be needed on all ranks for transport during the + // next iteration. + MPI_Allreduce(MPI_IN_PLACE, volume_.data(), n_source_regions_, MPI_DOUBLE, + MPI_SUM, mpi::intracomm); + + MPI_Allreduce(MPI_IN_PLACE, scalar_flux_new_.data(), + n_source_regions_ * negroups_, MPI_DOUBLE, MPI_SUM, mpi::intracomm); + + if (is_linear_) { + // We are going to assume we can safely cast Position, MomentArray, + // and MomentMatrix to contiguous arrays of doubles for the MPI + // allreduce operation. This is a safe assumption as typically + // compilers will at most pad to 8 byte boundaries. If a new FP32 + // MomentArray type is introduced, then there will likely be padding, in + // which case this function will need to become more complex. + if (sizeof(MomentArray) != 3 * sizeof(double) || + sizeof(MomentMatrix) != 6 * sizeof(double)) { + fatal_error( + "Unexpected buffer padding in linear source domain reduction."); + } + + MPI_Allreduce(MPI_IN_PLACE, static_cast(flux_moments_new_.data()), + n_source_regions_ * negroups_ * 3, MPI_DOUBLE, MPI_SUM, mpi::intracomm); + MPI_Allreduce(MPI_IN_PLACE, static_cast(mom_matrix_.data()), + n_source_regions_ * 6, MPI_DOUBLE, MPI_SUM, mpi::intracomm); + MPI_Allreduce(MPI_IN_PLACE, static_cast(centroid_iteration_.data()), + n_source_regions_ * 3, MPI_DOUBLE, MPI_SUM, mpi::intracomm); + } + +#endif +} + +} // namespace openmc \ No newline at end of file From e9ddf885e79aed1ade482d2d0fc5a170cfb30a22 Mon Sep 17 00:00:00 2001 From: Jose Ignacio Marquez Damian <22483345+marquezj@users.noreply.github.com> Date: Wed, 12 Feb 2025 01:12:52 +0100 Subject: [PATCH 252/671] Correct normalization of thermal elastic in non standard ENDF-6 files (#3234) Co-authored-by: Paul Romano --- openmc/data/thermal.py | 137 +++++++++++++++----------- tests/unit_tests/test_data_thermal.py | 6 +- 2 files changed, 85 insertions(+), 58 deletions(-) diff --git a/openmc/data/thermal.py b/openmc/data/thermal.py index df4dc1503..4ecc7c040 100644 --- a/openmc/data/thermal.py +++ b/openmc/data/thermal.py @@ -5,7 +5,6 @@ from numbers import Real from io import StringIO import itertools import os -import re import tempfile from warnings import warn @@ -871,7 +870,7 @@ class ThermalScattering(EqualityMixin): @classmethod def from_njoy(cls, filename, filename_thermal, temperatures=None, evaluation=None, evaluation_thermal=None, - use_endf_data=True, **kwargs): + use_endf_data=True, divide_incoherent_elastic=False, **kwargs): """Generate thermal scattering data by running NJOY. Parameters @@ -894,8 +893,13 @@ class ThermalScattering(EqualityMixin): use_endf_data : bool If the material has incoherent elastic scattering, the ENDF data will be used rather than the ACE data. + divide_incoherent_elastic : bool + Divide incoherent elastic cross section by number of principal + atoms. This is not part of the ENDF-6 standard but it is how it is + processed by NJOY. **kwargs - Keyword arguments passed to :func:`openmc.data.njoy.make_ace_thermal` + Keyword arguments passed to + :func:`openmc.data.njoy.make_ace_thermal` Returns ------- @@ -920,7 +924,7 @@ class ThermalScattering(EqualityMixin): # Load ENDF data to replace incoherent elastic if use_endf_data: - data_endf = cls.from_endf(filename_thermal) + data_endf = cls.from_endf(filename_thermal, divide_incoherent_elastic) if data_endf.elastic is not None: # Get appropriate temperatures if temperatures is None: @@ -938,7 +942,7 @@ class ThermalScattering(EqualityMixin): return data @classmethod - def from_endf(cls, ev_or_filename): + def from_endf(cls, ev_or_filename, divide_incoherent_elastic=False): """Generate thermal scattering data from an ENDF file Parameters @@ -946,6 +950,10 @@ class ThermalScattering(EqualityMixin): ev_or_filename : openmc.data.endf.Evaluation or str ENDF evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. + divide_incoherent_elastic : bool + Divide incoherent elastic cross section by number of principal + atoms. This is not part of the ENDF-6 standard but it is how it is + processed by NJOY. Returns ------- @@ -958,56 +966,6 @@ class ThermalScattering(EqualityMixin): else: ev = endf.Evaluation(ev_or_filename) - # Read coherent/incoherent elastic data - elastic = None - if (7, 2) in ev.section: - # Define helper functions to avoid duplication - def get_coherent_elastic(file_obj): - # Get structure factor at first temperature - params, S = endf.get_tab1_record(file_obj) - strT = _temperature_str(params[0]) - n_temps = params[2] - bragg_edges = S.x - xs = {strT: CoherentElastic(bragg_edges, S.y)} - distribution = {strT: CoherentElasticAE(xs[strT])} - - # Get structure factor for subsequent temperatures - for _ in range(n_temps): - params, S = endf.get_list_record(file_obj) - strT = _temperature_str(params[0]) - xs[strT] = CoherentElastic(bragg_edges, S) - distribution[strT] = CoherentElasticAE(xs[strT]) - return xs, distribution - - def get_incoherent_elastic(file_obj): - params, W = endf.get_tab1_record(file_obj) - bound_xs = params[0] - xs = {} - distribution = {} - for T, debye_waller in zip(W.x, W.y): - strT = _temperature_str(T) - xs[strT] = IncoherentElastic(bound_xs, debye_waller) - distribution[strT] = IncoherentElasticAE(debye_waller) - return xs, distribution - - file_obj = StringIO(ev.section[7, 2]) - lhtr = endf.get_head_record(file_obj)[2] - if lhtr == 1: - # coherent elastic - xs, distribution = get_coherent_elastic(file_obj) - elif lhtr == 2: - # incoherent elastic - xs, distribution = get_incoherent_elastic(file_obj) - elif lhtr == 3: - # mixed coherent / incoherent elastic - xs_c, dist_c = get_coherent_elastic(file_obj) - xs_i, dist_i = get_incoherent_elastic(file_obj) - assert sorted(xs_c) == sorted(xs_i) - xs = {T: Sum([xs_c[T], xs_i[T]]) for T in xs_c} - distribution = {T: MixedElasticAE(dist_c[T], dist_i[T]) for T in dist_c} - - elastic = ThermalScatteringReaction(xs, distribution) - # Read incoherent inelastic data assert (7, 4) in ev.section, 'No MF=7, MT=4 found in thermal scattering' file_obj = StringIO(ev.section[7, 4]) @@ -1022,6 +980,7 @@ class ThermalScattering(EqualityMixin): data['A0'] = awr = B[2] data['e_max'] = energy_max = B[3] data['M0'] = B[5] + free_xs = data['free_atom_xs'] / data['M0'] # Get information about non-principal atoms n_non_principal = params[5] @@ -1066,6 +1025,74 @@ class ThermalScattering(EqualityMixin): _, Teff = endf.get_tab1_record(file_obj) data['effective_temperature'].append(Teff) + # Read coherent/incoherent elastic data + elastic = None + if (7, 2) in ev.section: + # Define helper functions to avoid duplication + def get_coherent_elastic(file_obj): + # Get structure factor at first temperature + params, S = endf.get_tab1_record(file_obj) + strT = _temperature_str(params[0]) + n_temps = params[2] + bragg_edges = S.x + xs = {strT: CoherentElastic(bragg_edges, S.y)} + distribution = {strT: CoherentElasticAE(xs[strT])} + + # Get structure factor for subsequent temperatures + for _ in range(n_temps): + params, S = endf.get_list_record(file_obj) + strT = _temperature_str(params[0]) + xs[strT] = CoherentElastic(bragg_edges, S) + distribution[strT] = CoherentElasticAE(xs[strT]) + return xs, distribution + + def get_incoherent_elastic(file_obj, natom): + params, W = endf.get_tab1_record(file_obj) + bound_xs = params[0]/natom + + # Check whether divide_incoherent_elastic was applied correctly + if abs(free_xs - bound_xs/(1 + 1/data['A0'])**2) > 0.5: + if divide_incoherent_elastic: + msg = ( + 'Thermal scattering evaluation follows ENDF-6 ' + 'definition of bound cross section but ' + 'divide_incoherent_elastic=True.' + ) + else: + msg = ( + 'Thermal scattering evaluation follows NJOY ' + 'definition of bound cross section but ' + 'divide_incoherent_elastic=False.' + ) + warn(msg) + + xs = {} + distribution = {} + for T, debye_waller in zip(W.x, W.y): + strT = _temperature_str(T) + xs[strT] = IncoherentElastic(bound_xs, debye_waller) + distribution[strT] = IncoherentElasticAE(debye_waller) + return xs, distribution + + file_obj = StringIO(ev.section[7, 2]) + lhtr = endf.get_head_record(file_obj)[2] + natom = data['M0'] if divide_incoherent_elastic else 1 + if lhtr == 1: + # coherent elastic + xs, distribution = get_coherent_elastic(file_obj) + elif lhtr == 2: + # incoherent elastic + xs, distribution = get_incoherent_elastic(file_obj, natom) + elif lhtr == 3: + # mixed coherent / incoherent elastic + xs_c, dist_c = get_coherent_elastic(file_obj) + xs_i, dist_i = get_incoherent_elastic(file_obj, natom) + assert sorted(xs_c) == sorted(xs_i) + xs = {T: Sum([xs_c[T], xs_i[T]]) for T in xs_c} + distribution = {T: MixedElasticAE(dist_c[T], dist_i[T]) for T in dist_c} + + elastic = ThermalScatteringReaction(xs, distribution) + name = ev.target['zsymam'].strip() instance = cls(name, awr, energy_max, kTs) if elastic is not None: diff --git a/tests/unit_tests/test_data_thermal.py b/tests/unit_tests/test_data_thermal.py index 9f5a8cb40..c9fd9045b 100644 --- a/tests/unit_tests/test_data_thermal.py +++ b/tests/unit_tests/test_data_thermal.py @@ -41,7 +41,7 @@ def hzrh(): """H in ZrH thermal scattering data.""" endf_data = os.environ['OPENMC_ENDF_DATA'] filename = os.path.join(endf_data, 'thermal_scatt', 'tsl-HinZrH.endf') - return openmc.data.ThermalScattering.from_endf(filename) + return openmc.data.ThermalScattering.from_endf(filename, divide_incoherent_elastic=True) @pytest.fixture(scope='module') @@ -64,7 +64,7 @@ def sio2(): """SiO2 thermal scattering data.""" endf_data = os.environ['OPENMC_ENDF_DATA'] filename = os.path.join(endf_data, 'thermal_scatt', 'tsl-SiO2.endf') - return openmc.data.ThermalScattering.from_endf(filename) + return openmc.data.ThermalScattering.from_endf(filename, divide_incoherent_elastic=True) def test_h2o_attributes(h2o): @@ -144,7 +144,7 @@ def test_continuous_dist(h2o_njoy): def test_h2o_endf(): endf_data = os.environ['OPENMC_ENDF_DATA'] filename = os.path.join(endf_data, 'thermal_scatt', 'tsl-HinH2O.endf') - h2o = openmc.data.ThermalScattering.from_endf(filename) + h2o = openmc.data.ThermalScattering.from_endf(filename, divide_incoherent_elastic=True) assert not h2o.elastic assert h2o.atomic_weight_ratio == pytest.approx(0.99917) assert h2o.energy_max == pytest.approx(3.99993) From 18c3112415aa19947737ffcd70641a194faa98dd Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Wed, 12 Feb 2025 07:03:17 -0600 Subject: [PATCH 253/671] Adding tmate action to CI for debugging (#3138) --- .github/workflows/ci.yml | 20 +++++++++++++++++++- docs/source/devguide/tests.rst | 24 ++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5051b8d0..e69a13db9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,6 +88,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + fetch-depth: 2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 @@ -98,7 +100,17 @@ jobs: run: | echo "OPENMC_CROSS_SECTIONS=$HOME/nndc_hdf5/cross_sections.xml" >> $GITHUB_ENV echo "OPENMC_ENDF_DATA=$HOME/endf-b-vii.1" >> $GITHUB_ENV - + # get the sha of the last branch commit + # for push and workflow_dispatch events, use the current reference head + BRANCH_SHA=HEAD + # for a pull_request event, use the last reference of the parents of the merge commit + if [ "${{ github.event_name }}" == "pull_request" ]; then + BRANCH_SHA=$(git rev-list --parents -n 1 HEAD | rev | cut -d" " -f 1 | rev) + fi + COMMIT_MESSAGE=$(git log $BRANCH_SHA -1 --pretty=%B) + echo ${COMMIT_MESSAGE} + echo "COMMIT_MESSAGE=${COMMIT_MESSAGE}" >> $GITHUB_ENV + - name: Apt dependencies shell: bash run: | @@ -153,6 +165,12 @@ jobs: CTEST_OUTPUT_ON_FAILURE=1 make test -C $GITHUB_WORKSPACE/build/ $GITHUB_WORKSPACE/tools/ci/gha-script.sh + - name: Setup tmate debug session + continue-on-error: true + if: ${{ contains(env.COMMIT_MESSAGE, '[gha-debug]') }} + uses: mxschmitt/action-tmate@v3 + timeout-minutes: 10 + - name: after_success shell: bash run: | diff --git a/docs/source/devguide/tests.rst b/docs/source/devguide/tests.rst index d8fc53b3a..3665ad9d4 100644 --- a/docs/source/devguide/tests.rst +++ b/docs/source/devguide/tests.rst @@ -84,6 +84,30 @@ that, consider the following: limit the number of threads that OpenBLAS uses internally; this can be done by setting the :envvar:`OPENBLAS_NUM_THREADS` environment variable to 1. +Debugging Tests in CI +--------------------- + +.. _tmate: `_ + +Tests can be debugged in CI using a feature called `tmate`_. CI debugging can be +enabled by including "[gha-debug]" in the commit message. When the test fails, a +link similar to the one shown below will be provided in the GitHub Actions +output after failure occurs. Logging into the provided link will allow you to +debug the test in the CI environment. The following is an example of the output +shown in the CI log that provides the link to the tmate session: + +.. code-block:: text + :linenos: + + Created new session successfully + ssh 2VcykjU7vNdvAzEjQcc839GM2@nyc1.tmate.io + https://tmate.io/t/2VcykjU7vNdvAzEjQcc839GM2 + Entering main loop + Web shell: https://tmate.io/t/2VcykjU7vNdvAzEjQcc839GM2 + SSH: ssh 2VcykjU7vNdvAzEjQcc839GM2@nyc1.tmate.io + ... + + Generating XML Inputs --------------------- From 78bf4cf145d2477fd0efed51c6506d3dda940d43 Mon Sep 17 00:00:00 2001 From: rherrero-pf <156206440+rherrero-pf@users.noreply.github.com> Date: Thu, 13 Feb 2025 22:26:36 +0100 Subject: [PATCH 254/671] Add VTU export for Unstructured meshes (#3290) Co-authored-by: Paul Romano --- openmc/mesh.py | 85 ++++++++++++++++++++++------------- tests/unit_tests/test_mesh.py | 82 +++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 31 deletions(-) diff --git a/openmc/mesh.py b/openmc/mesh.py index e4d83d81d..4c594c9c6 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -9,6 +9,7 @@ from numbers import Integral, Real import h5py import lxml.etree as ET import numpy as np +from pathlib import Path import openmc import openmc.checkvalue as cv @@ -2358,55 +2359,71 @@ class UnstructuredMesh(MeshBase): self.write_data_to_vtk(**kwargs) def write_data_to_vtk( - self, - filename: PathLike | None = None, - datasets: dict | None = None, - volume_normalization: bool = True + self, + filename: PathLike | None = None, + datasets: dict | None = None, + volume_normalization: bool = True, ): """Map data to unstructured VTK mesh elements. + If filename is None, then a filename will be generated based on the mesh + ID, and exported to VTK format. + Parameters ---------- filename : str or pathlib.Path - Name of the VTK file to write + Name of the VTK file to write. If the filename ends in '.vtu' then a + binary VTU format file will be written, if the filename ends in + '.vtk' then a legacy VTK file will be written. datasets : dict - Dictionary whose keys are the data labels - and values are numpy appropriately sized arrays - of the data + Dictionary whose keys are the data labels and values are numpy + appropriately sized arrays of the data volume_normalization : bool - Whether or not to normalize the data by the - volume of the mesh elements + Whether or not to normalize the data by the volume of the mesh + elements """ - import vtk - from vtk.util import numpy_support as nps + from vtkmodules.util import numpy_support + from vtkmodules import vtkCommonCore + from vtkmodules import vtkCommonDataModel + from vtkmodules import vtkIOLegacy + from vtkmodules import vtkIOXML if self.connectivity is None or self.vertices is None: - raise RuntimeError('This mesh has not been ' - 'loaded from a statepoint file.') + raise RuntimeError( + "This mesh has not been loaded from a statepoint file." + ) if filename is None: - filename = f'mesh_{self.id}.vtk' + filename = f"mesh_{self.id}.vtk" - writer = vtk.vtkUnstructuredGridWriter() + if Path(filename).suffix == ".vtk": + writer = vtkIOLegacy.vtkUnstructuredGridWriter() + + elif Path(filename).suffix == ".vtu": + writer = vtkIOXML.vtkXMLUnstructuredGridWriter() + writer.SetCompressorTypeToZLib() + writer.SetDataModeToBinary() writer.SetFileName(str(filename)) - grid = vtk.vtkUnstructuredGrid() + grid = vtkCommonDataModel.vtkUnstructuredGrid() - vtk_pnts = vtk.vtkPoints() - vtk_pnts.SetData(nps.numpy_to_vtk(self.vertices)) - grid.SetPoints(vtk_pnts) + points = vtkCommonCore.vtkPoints() + points.SetData(numpy_support.numpy_to_vtk(self.vertices)) + grid.SetPoints(points) n_skipped = 0 for elem_type, conn in zip(self.element_types, self.connectivity): if elem_type == self._LINEAR_TET: - elem = vtk.vtkTetra() + elem = vtkCommonDataModel.vtkTetra() elif elem_type == self._LINEAR_HEX: - elem = vtk.vtkHexahedron() + elem = vtkCommonDataModel.vtkHexahedron() elif elem_type == self._UNSUPPORTED_ELEM: n_skipped += 1 + continue else: - raise RuntimeError(f'Invalid element type {elem_type} found') + raise RuntimeError(f"Invalid element type {elem_type} found") + for i, c in enumerate(conn): if c == -1: break @@ -2415,30 +2432,36 @@ class UnstructuredMesh(MeshBase): grid.InsertNextCell(elem.GetCellType(), elem.GetPointIds()) if n_skipped > 0: - warnings.warn(f'{n_skipped} elements were not written because ' - 'they are not of type linear tet/hex') + warnings.warn( + f"{n_skipped} elements were not written because " + "they are not of type linear tet/hex" + ) # check that datasets are the correct size datasets_out = [] if datasets is not None: for name, data in datasets.items(): if data.shape != self.dimension: - raise ValueError(f'Cannot apply dataset "{name}" with ' - f'shape {data.shape} to mesh {self.id} ' - f'with dimensions {self.dimension}') + raise ValueError( + f'Cannot apply dataset "{name}" with ' + f"shape {data.shape} to mesh {self.id} " + f"with dimensions {self.dimension}" + ) if volume_normalization: for name, data in datasets.items(): if np.issubdtype(data.dtype, np.integer): - warnings.warn(f'Integer data set "{name}" will ' - 'not be volume-normalized.') + warnings.warn( + f'Integer data set "{name}" will ' + "not be volume-normalized." + ) continue data /= self.volumes # add data to the mesh for name, data in datasets.items(): datasets_out.append(data) - arr = vtk.vtkDoubleArray() + arr = vtkCommonCore.vtkDoubleArray() arr.SetName(name) arr.SetNumberOfTuples(data.size) diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index 27322165f..3f307dda8 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -1,8 +1,11 @@ from math import pi +from pathlib import Path import numpy as np import pytest import openmc +import openmc.lib +from openmc.utility_funcs import change_directory @pytest.mark.parametrize("val_left,val_right", [(0, 0), (-1., -1.), (2.0, 2)]) @@ -398,6 +401,85 @@ def test_umesh_roundtrip(run_in_tmpdir, request): assert umesh.id == xml_mesh.id +@pytest.fixture(scope='module') +def simple_umesh(request): + """Fixture returning UnstructuredMesh with all attributes""" + surf1 = openmc.Sphere(r=20.0, boundary_type="vacuum") + material1 = openmc.Material() + material1.add_element("H", 1.0) + material1.set_density('g/cm3', 1.0) + + materials = openmc.Materials([material1]) + cell1 = openmc.Cell(region=-surf1, fill=material1) + geometry = openmc.Geometry([cell1]) + + umesh = openmc.UnstructuredMesh( + filename=request.path.parent.parent + / "regression_tests/external_moab/test_mesh_tets.h5m", + library="moab", + mesh_id=1 + ) + # setting ID to make it easier to get the mesh from the statepoint later + mesh_filter = openmc.MeshFilter(umesh) + + # Create flux mesh tally to score alpha production + mesh_tally = openmc.Tally(name="test_tally") + mesh_tally.filters = [mesh_filter] + mesh_tally.scores = ["total"] + + tallies = openmc.Tallies([mesh_tally]) + + settings = openmc.Settings() + settings.run_mode = "fixed source" + settings.batches = 2 + settings.particles = 100 + settings.source = openmc.IndependentSource( + space=openmc.stats.Point((0.1, 0.1, 0.1)) + ) + + model = openmc.Model( + materials=materials, geometry=geometry, settings=settings, tallies=tallies + ) + + with change_directory(tmpdir=True): + statepoint_file = model.run() + with openmc.StatePoint(statepoint_file) as sp: + return sp.meshes[1] + + +@pytest.mark.skipif(not openmc.lib._dagmc_enabled(), reason="DAGMC not enabled.") +@pytest.mark.parametrize('export_type', ('.vtk', '.vtu')) +def test_umesh(run_in_tmpdir, simple_umesh, export_type): + """Performs a minimal UnstructuredMesh simulation, reads in the resulting + statepoint file and writes the mesh data to vtk and vtkhdf files. It is + necessary to read in the unstructured mesh from a statepoint file to ensure + it has all the required attributes + """ + # Get VTK modules + vtkIOLegacy = pytest.importorskip("vtkmodules.vtkIOLegacy") + vtkIOXML = pytest.importorskip("vtkmodules.vtkIOXML") + + # Sample some random data and write to VTK + rng = np.random.default_rng() + ref_data = rng.random(simple_umesh.dimension) + filename = f"test_mesh{export_type}" + simple_umesh.write_data_to_vtk(datasets={"mean": ref_data}, filename=filename) + + assert Path(filename).exists() + + if export_type == ".vtk": + reader = vtkIOLegacy.vtkGenericDataObjectReader() + elif export_type == ".vtu": + reader = vtkIOXML.vtkXMLGenericDataObjectReader() + reader.SetFileName(str(filename)) + reader.Update() + + # Get mean from file and make sure it matches original data + arr = reader.GetOutput().GetCellData().GetArray("mean") + mean = np.array([arr.GetTuple1(i) for i in range(ref_data.size)]) + np.testing.assert_almost_equal(mean, ref_data) + + def test_mesh_get_homogenized_materials(): """Test the get_homogenized_materials method""" # Simple model with 1 cm of Fe56 next to 1 cm of H1 From 0ceb49bc5cb9c7fa8853db6822f82ff73a909a7e Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 13 Feb 2025 18:04:04 -0600 Subject: [PATCH 255/671] Avoid error in CI from newlines in commit message (#3302) --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e69a13db9..de99bbbe2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,10 +107,10 @@ jobs: if [ "${{ github.event_name }}" == "pull_request" ]; then BRANCH_SHA=$(git rev-list --parents -n 1 HEAD | rev | cut -d" " -f 1 | rev) fi - COMMIT_MESSAGE=$(git log $BRANCH_SHA -1 --pretty=%B) + COMMIT_MESSAGE=$(git log $BRANCH_SHA -1 --pretty=%B | tr '\n' ' ') echo ${COMMIT_MESSAGE} echo "COMMIT_MESSAGE=${COMMIT_MESSAGE}" >> $GITHUB_ENV - + - name: Apt dependencies shell: bash run: | @@ -170,7 +170,7 @@ jobs: if: ${{ contains(env.COMMIT_MESSAGE, '[gha-debug]') }} uses: mxschmitt/action-tmate@v3 timeout-minutes: 10 - + - name: after_success shell: bash run: | From 02e225b8f66d7f22b80e4f0c8cf78090178638e6 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Fri, 14 Feb 2025 05:03:19 +0100 Subject: [PATCH 256/671] Avoid end of life ubuntu 20.04 in ReadTheDocs runner (#3301) Co-authored-by: Jon Shimwell --- .readthedocs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 7b69b64b6..8bbc7d01f 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,7 +1,7 @@ version: 2 build: - os: "ubuntu-20.04" + os: "ubuntu-24.04" tools: python: "3.12" From be4396c12bd09ad0ea81eadc90e16888bab93dff Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Fri, 14 Feb 2025 17:39:09 +0100 Subject: [PATCH 257/671] adding non elastic MT number (#3285) Co-authored-by: Paul Romano --- openmc/data/reaction.py | 10 +++++----- src/reaction.cpp | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/openmc/data/reaction.py b/openmc/data/reaction.py index 8f90d2de4..65b59582c 100644 --- a/openmc/data/reaction.py +++ b/openmc/data/reaction.py @@ -25,11 +25,11 @@ from .product import Product from .uncorrelated import UncorrelatedAngleEnergy -REACTION_NAME = {1: '(n,total)', 2: '(n,elastic)', 4: '(n,level)', - 5: '(n,misc)', 11: '(n,2nd)', 16: '(n,2n)', 17: '(n,3n)', - 18: '(n,fission)', 19: '(n,f)', 20: '(n,nf)', 21: '(n,2nf)', - 22: '(n,na)', 23: '(n,n3a)', 24: '(n,2na)', 25: '(n,3na)', - 27: '(n,absorption)', 28: '(n,np)', 29: '(n,n2a)', +REACTION_NAME = {1: '(n,total)', 2: '(n,elastic)', 3: "(n,nonelastic)", + 4: '(n,level)', 5: '(n,misc)', 11: '(n,2nd)', 16: '(n,2n)', + 17: '(n,3n)', 18: '(n,fission)', 19: '(n,f)', 20: '(n,nf)', + 21: '(n,2nf)', 22: '(n,na)', 23: '(n,n3a)', 24: '(n,2na)', + 25: '(n,3na)', 27: '(n,absorption)', 28: '(n,np)', 29: '(n,n2a)', 30: '(n,2n2a)', 32: '(n,nd)', 33: '(n,nt)', 34: '(n,n3He)', 35: '(n,nd2a)', 36: '(n,nt2a)', 37: '(n,4n)', 38: '(n,3nf)', 41: '(n,2np)', 42: '(n,3np)', 44: '(n,n2p)', 45: '(n,npa)', diff --git a/src/reaction.cpp b/src/reaction.cpp index 0643cb08b..3bdc83746 100644 --- a/src/reaction.cpp +++ b/src/reaction.cpp @@ -175,6 +175,7 @@ std::unordered_map REACTION_NAME_MAP { // Normal ENDF-based reactions {TOTAL_XS, "(n,total)"}, {ELASTIC, "(n,elastic)"}, + {N_NONELASTIC, "(n,nonelastic)"}, {N_LEVEL, "(n,level)"}, {N_2ND, "(n,2nd)"}, {N_2N, "(n,2n)"}, From 11587786e06641851a2c28a6b753105b02575541 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Sat, 15 Feb 2025 05:58:30 +0100 Subject: [PATCH 258/671] removed old command line scripts (#3300) Co-authored-by: Paul Romano --- docs/source/_images/plotmeshtally.png | Bin 72643 -> 0 bytes docs/source/io_formats/plots.rst | 14 +- docs/source/usersguide/plots.rst | 10 +- docs/source/usersguide/processing.rst | 11 +- docs/source/usersguide/scripts.rst | 139 -------- docs/source/usersguide/settings.rst | 7 +- pyproject.toml | 11 +- scripts/openmc-ace-to-hdf5 | 156 --------- scripts/openmc-plot-mesh-tally | 344 -------------------- scripts/openmc-track-combine | 24 -- scripts/openmc-track-to-vtk | 41 --- scripts/openmc-update-inputs | 307 ----------------- scripts/openmc-update-mgxs | 212 ------------ scripts/openmc-voxel-to-vtk | 22 -- tests/regression_tests/track_output/test.py | 5 +- tests/testing_harness.py | 4 +- 16 files changed, 23 insertions(+), 1284 deletions(-) delete mode 100644 docs/source/_images/plotmeshtally.png delete mode 100755 scripts/openmc-ace-to-hdf5 delete mode 100755 scripts/openmc-plot-mesh-tally delete mode 100755 scripts/openmc-track-combine delete mode 100755 scripts/openmc-track-to-vtk delete mode 100755 scripts/openmc-update-inputs delete mode 100755 scripts/openmc-update-mgxs delete mode 100755 scripts/openmc-voxel-to-vtk diff --git a/docs/source/_images/plotmeshtally.png b/docs/source/_images/plotmeshtally.png deleted file mode 100644 index d874b4906ab84bcf11617f116a4ecd7360aa2ce2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 72643 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�+IC#>D)3I^1hid;L>$ap*P%UA~gYNtAFR)_j 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 diff --git a/docs/source/io_formats/plots.rst b/docs/source/io_formats/plots.rst index 04d5720c8..c1fa78330 100644 --- a/docs/source/io_formats/plots.rst +++ b/docs/source/io_formats/plots.rst @@ -71,9 +71,9 @@ sub-elements: the PNG file format. The "voxel" plot type produces a binary datafile containing voxel grid positioning and the cell or material (specified by the ``color`` tag) at the center of each voxel. Voxel plot files can be - processed into VTK files using the :ref:`scripts_voxel` script provided with - OpenMC and subsequently viewed with a 3D viewer such as VISIT or Paraview. - See the :ref:`io_voxel` for information about the datafile structure. + processed into VTK files using the :func:`openmc.voxel_to_vtk` function and + subsequently viewed with a 3D viewer such as VISIT or Paraview. See the + :ref:`io_voxel` for information about the datafile structure. .. note:: High-resolution voxel files produced by OpenMC can be quite large, but the equivalent VTK files will be significantly smaller. @@ -152,17 +152,17 @@ attributes or sub-elements. These are not used in "voxel" plots: *Default*: 255 255 255 (white) :show_overlaps: - Indicates whether overlapping regions of different cells are shown. + Indicates whether overlapping regions of different cells are shown. *Default*: None :overlap_color: - Specifies the RGB color of overlapping regions of different cells. Does not - do anything if ``show_overlaps`` is "false" or not specified. Should be 3 + Specifies the RGB color of overlapping regions of different cells. Does not + do anything if ``show_overlaps`` is "false" or not specified. Should be 3 integers separated by spaces. *Default*: 255 0 0 (red) - + :meshlines: The ``meshlines`` sub-element allows for plotting the boundaries of a regular mesh on top of a plot. Only one ``meshlines`` element is allowed per diff --git a/docs/source/usersguide/plots.rst b/docs/source/usersguide/plots.rst index 2d588519c..d453ea453 100644 --- a/docs/source/usersguide/plots.rst +++ b/docs/source/usersguide/plots.rst @@ -111,11 +111,11 @@ The voxel plot data is written to an :ref:`HDF5 file `. The voxel file can subsequently be converted into a standard mesh format that can be viewed in `ParaView `_, `VisIt `_, etc. This typically -will compress the size of the file significantly. The provided -:ref:`scripts_voxel` script can convert the HDF5 voxel file to VTK formats. Once -processed into a standard 3D file format, colors and masks can be defined using -the stored ID numbers to better explore the geometry. The process for doing this -will depend on the 3D viewer, but should be straightforward. +will compress the size of the file significantly. The +:func:`openmc.voxel_to_vtk` function can convert the HDF5 voxel file to VTK +formats. Once processed into a standard 3D file format, colors and masks can be +defined using the stored ID numbers to better explore the geometry. The process +for doing this will depend on the 3D viewer, but should be straightforward. .. note:: 3D voxel plotting can be very computer intensive for the viewing program (Visit, ParaView, etc.) if the number of voxels is large (>10 diff --git a/docs/source/usersguide/processing.rst b/docs/source/usersguide/processing.rst index 3103b7b7c..10944b5e2 100644 --- a/docs/source/usersguide/processing.rst +++ b/docs/source/usersguide/processing.rst @@ -41,12 +41,9 @@ Plotting in 2D -------------- The `example notebook`_ also demonstrates how to plot a structured mesh tally in -two dimensions using the Python API. One can also use the :ref:`scripts_plot` -script which provides an interactive GUI to explore and plot structured mesh -tallies for any scores and filter bins. - -.. image:: ../_images/plotmeshtally.png - :width: 400px +two dimensions using the Python API. One can also use the `openmc-plotter +`_ application that provides an +interactive GUI to explore and plot a much wider variety of tallies. .. _usersguide_track: @@ -81,7 +78,7 @@ of three, e.g., if we wanted particles 3 and 4 from batch 1 and generation 2:: After running OpenMC, the working directory will contain a file of the form "track_(batch #)_(generation #)_(particle #).h5" for each particle tracked. These track files can be converted into VTK poly data files with the -:ref:`scripts_track` script. +:class:`openmc.Tracks` class. ---------------------- Source Site Processing diff --git a/docs/source/usersguide/scripts.rst b/docs/source/usersguide/scripts.rst index 78ee6f775..0879d63ef 100644 --- a/docs/source/usersguide/scripts.rst +++ b/docs/source/usersguide/scripts.rst @@ -53,142 +53,3 @@ flags: .. note:: If you're using the Python API, :func:`openmc.run` is equivalent to running ``openmc`` from the command line. - -.. _scripts_ace: - ----------------------- -``openmc-ace-to-hdf5`` ----------------------- - -This script can be used to create HDF5 nuclear data libraries used by OpenMC if -you have existing ACE files. There are four different ways you can specify ACE -libraries that are to be converted: - -1. List each ACE library as a positional argument. This is very useful in - conjunction with the usual shell utilities (``ls``, ``find``, etc.). -2. Use the ``--xml`` option to specify a pre-v0.9 cross_sections.xml file. -3. Use the ``--xsdir`` option to specify a MCNP xsdir file. -4. Use the ``--xsdata`` option to specify a Serpent xsdata file. - -The script does not use any extra information from cross_sections.xml/ xsdir/ -xsdata files to determine whether the nuclide is metastable. Instead, the -``--metastable`` argument can be used to specify whether the ZAID naming convention -follows the NNDC data convention (1000*Z + A + 300 + 100*m), or the MCNP data -convention (essentially the same as NNDC, except that the first metastable state -of Am242 is 95242 and the ground state is 95642). - -The optional ``--fission_energy_release`` argument will accept an HDF5 file -containing a library of fission energy release (ENDF MF=1 MT=458) data. A -library built from ENDF/B-VII.1 data is released with OpenMC and can be found at -openmc/data/fission_Q_data_endb71.h5. This data is necessary for -'fission-q-prompt' and 'fission-q-recoverable' tallies, but is not needed -otherwise. - --h, --help show help message and exit - --d DESTINATION, --destination DESTINATION - Directory to create new library in - --m META, --metastable META - How to interpret ZAIDs for metastable nuclides. META - can be either 'nndc' or 'mcnp'. (default: nndc) - ---xml XML Old-style cross_sections.xml that lists ACE libraries - ---xsdir XSDIR MCNP xsdir file that lists ACE libraries - ---xsdata XSDATA Serpent xsdata file that lists ACE libraries - ---fission_energy_release FISSION_ENERGY_RELEASE - HDF5 file containing fission energy release data - -.. _scripts_plot: - --------------------------- -``openmc-plot-mesh-tally`` --------------------------- - -``openmc-plot-mesh-tally`` provides a graphical user interface for plotting mesh -tallies. The path to the statepoint file can be provided as an optional arugment -(if omitted, a file dialog will be presented). - -.. _scripts_track_combine: - ------------------------- -``openmc-track-combine`` ------------------------- - -This script combines multiple HDF5 :ref:`particle track files -` into a single HDF5 particle track file. The filenames of the -particle track files should be given as posititional arguments. The output -filename can also be changed with the ``-o`` flag: - --o OUT, --out OUT Output HDF5 particle track file - -.. _scripts_track: - ------------------------ -``openmc-track-to-vtk`` ------------------------ - -This script converts HDF5 :ref:`particle track files ` to VTK -poly data that can be viewed with ParaView or VisIt. The filenames of the -particle track files should be given as posititional arguments. The output -filename can also be changed with the ``-o`` flag: - --o OUT, --out OUT Output VTK poly filename - ------------------------- -``openmc-update-inputs`` ------------------------- - -If you have existing XML files that worked in a previous version of OpenMC that -no longer work with the current version, you can try to update these files using -``openmc-update-inputs``. If any of the given files do not match the most -up-to-date formatting, then they will be automatically rewritten. The old -out-of-date files will not be deleted; they will be moved to a new file with -'.original' appended to their name. - -Formatting changes that will be made: - -geometry.xml - Lattices containing 'outside' attributes/tags will be replaced with lattices - containing 'outer' attributes, and the appropriate cells/universes will be - added. Any 'surfaces' attributes/elements on a cell will be renamed 'region'. - -materials.xml - Nuclide names will be changed from ACE aliases (e.g., Am-242m) to HDF5/GNDS - names (e.g., Am242_m1). Thermal scattering table names will be changed from - ACE aliases (e.g., HH2O) to HDF5/GNDS names (e.g., c_H_in_H2O). - ----------------------- -``openmc-update-mgxs`` ----------------------- - -This script updates OpenMC's deprecated multi-group cross section XML files to -the latest HDF5-based format. - --i IN, --input IN Input XML file --o OUT, --output OUT Output file in HDF5 format - -.. _scripts_voxel: - ---------------------------- -``openmc-voxel-to-vtk`` ---------------------------- - -When OpenMC generates :ref:`voxel plots `, they are in an -:ref:`HDF5 format ` that is not terribly useful by itself. The -``openmc-voxel-to-vtk`` script converts a voxel HDF5 file to a `VTK -`_ file. To run this script, you will need to have the VTK -Python bindings installed. To convert a voxel file, simply provide the path to -the file: - -.. code-block:: sh - - openmc-voxel-to-vtk voxel_1.h5 - -The ``openmc-voxel-to-vtk`` script also takes the following optional -command-line arguments: - --o, --output Path to output VTK file diff --git a/docs/source/usersguide/settings.rst b/docs/source/usersguide/settings.rst index ca11d6487..1b2d4bc1a 100644 --- a/docs/source/usersguide/settings.rst +++ b/docs/source/usersguide/settings.rst @@ -751,11 +751,10 @@ instance, whereas the :meth:`openmc.Track.filter` method returns a new with more than one process, a separate track file will be written for each MPI process with the filename ``tracks_p#.h5`` where # is the rank of the corresponding process. Multiple track files can be - combined with the :ref:`scripts_track_combine` script: + combined with the :meth:`openmc.Tracks.combine` method:: - .. code-block:: sh - - openmc-track-combine tracks_p*.h5 --out tracks.h5 + track_files = [f"tracks_p{rank}.h5" for rank in range(32)] + openmc.Tracks.combine(track_files, "tracks.h5") ----------------------- Restarting a Simulation diff --git a/pyproject.toml b/pyproject.toml index e4e8472f1..1f8de10e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,19 +59,10 @@ Repository = "https://github.com/openmc-dev/openmc" Issues = "https://github.com/openmc-dev/openmc/issues" [tool.setuptools.packages.find] -include = ['openmc*', 'scripts*'] +include = ['openmc*'] exclude = ['tests*'] [tool.setuptools.package-data] "openmc.data.effective_dose" = ["**/*.txt"] "openmc.data" = ["*.txt", "*.DAT", "*.json", "*.h5"] "openmc.lib" = ["libopenmc.dylib", "libopenmc.so"] - -[project.scripts] -openmc-ace-to-hdf5 = "scripts.openmc_ace_to_hdf5:main" -openmc-plot-mesh-tally = "scripts.openmc_plot_mesh_tally:main" -openmc-track-combine = "scripts.openmc_track_combine:main" -openmc-track-to-vtk = "scripts.openmc_track_to_vtk:main" -openmc-update-inputs = "scripts.openmc_update_inputs:main" -openmc-update-mgxs = "scripts.openmc_update_mgxs:main" -openmc-voxel-to-vtk = "scripts.openmc_voxel_to_vtk:main" diff --git a/scripts/openmc-ace-to-hdf5 b/scripts/openmc-ace-to-hdf5 deleted file mode 100755 index 66ae7e2a9..000000000 --- a/scripts/openmc-ace-to-hdf5 +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env python3 - -"""This script can be used to create HDF5 nuclear data libraries used by -OpenMC. There are four different ways you can specify ACE libraries that are to -be converted: - -1. List each ACE library as a positional argument. This is very useful in - conjunction with the usual shell utilities (ls, find, etc.). -2. Use the --xsdir option to specify a MCNP xsdir file. -3. Use the --xsdata option to specify a Serpent xsdata file. - -The script does not use any extra information from xsdir/xsdata files to -determine whether the nuclide is metastable. Instead, the --metastable argument -can be used to specify whether the ZAID naming convention follows the NNDC data -convention (1000*Z + A + 300 + 100*m), or the MCNP data convention (essentially -the same as NNDC, except that the first metastable state of Am242 is 95242 and -the ground state is 95642). - -""" - -import argparse -from functools import partial -import os -from pathlib import Path -import warnings - -import openmc.data -from openmc.data.ace import TableType - - -def ace_to_hdf5(destination, xsdir, xsdata, libraries, metastable, libver): - - if not destination.is_dir(): - destination.mkdir(parents=True, exist_ok=True) - - ace_libraries = [] - if xsdir is not None: - ace_libraries.extend(openmc.data.ace.get_libraries_from_xsdir(xsdir)) - elif xsdata is not None: - ace_libraries.extend(openmc.data.ace.get_libraries_from_xsdata(xsdata)) - else: - ace_libraries = [Path(lib) for lib in libraries] - - converted = {} - library = openmc.data.DataLibrary() - - for path in ace_libraries: - # Check that ACE library exists - if not os.path.exists(path): - warnings.warn(f"ACE library '{path}' does not exist.") - continue - - lib = openmc.data.ace.Library(path) - for table in lib.tables: - # Check type of the ACE table and determine appropriate class / - # conversion function - if table.data_type == TableType.NEUTRON_CONTINUOUS: - name = table.zaid - cls = openmc.data.IncidentNeutron - converter = partial(cls.from_ace, metastable_scheme=metastable) - elif table.data_type == TableType.THERMAL_SCATTERING: - # Adjust name to be the new thermal scattering name - name = openmc.data.get_thermal_name(table.zaid) - cls = openmc.data.ThermalScattering - converter = cls.from_ace - else: - print(f"Can't convert ACE table {table.name}") - continue - - if name not in converted: - try: - data = converter(table) - except Exception as e: - print(f"Failed to convert {table.name}: {e}") - continue - - print(f"Converting {table.name} (ACE) to {data.name} (HDF5)") - - # Determine output filename - outfile = destination / (data.name.replace(".", "_") + ".h5") - data.export_to_hdf5(outfile, "w", libver=libver) - - # Register with library - library.register_file(outfile) - - # Add nuclide to list - converted[name] = outfile - else: - # Read existing HDF5 file - data = cls.from_hdf5(converted[name]) - - # Add data for new temperature - try: - print(f"Converting {table.name} (ACE) to {data.name} (HDF5)") - if table.data_type == TableType.NEUTRON_CONTINUOUS: - data.add_temperature_from_ace(table, metastable) - else: - data.add_temperature_from_ace(table) - except Exception as e: - print(f"Failed to convert {table.name}: {e}") - continue - - # Re-export - data.export_to_hdf5(converted[name], "w", libver=libver) - - # Write cross_sections.xml - library.export_to_xml(destination / "cross_sections.xml") - - -if __name__ == "__main__": - - class CustomFormatter( - argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter - ): - pass - - parser = argparse.ArgumentParser( - description=__doc__, formatter_class=CustomFormatter - ) - parser.add_argument("libraries", nargs="*", help="ACE libraries to convert to HDF5") - parser.add_argument( - "-d", - "--destination", - type=Path, - default=Path.cwd(), - help="Directory to create new library in", - ) - parser.add_argument( - "-m", - "--metastable", - choices=["mcnp", "nndc"], - default="nndc", - help="How to interpret ZAIDs for metastable nuclides", - ) - parser.add_argument("--xsdir", help="MCNP xsdir file that lists " "ACE libraries") - parser.add_argument( - "--xsdata", help="Serpent xsdata file that lists " "ACE libraries" - ) - parser.add_argument( - "--libver", - choices=["earliest", "latest"], - default="earliest", - help="Output HDF5 versioning. Use " - "'earliest' for backwards compatibility or 'latest' " - "for performance", - ) - args = parser.parse_args() - - ace_to_hdf5( - destination=args.destination, - xsdir=args.xsdir, - xsdata=args.xsdata, - libraries=args.libraries, - metastable=args.metastable, - libver=args.libver, - ) diff --git a/scripts/openmc-plot-mesh-tally b/scripts/openmc-plot-mesh-tally deleted file mode 100755 index 2a4fcd743..000000000 --- a/scripts/openmc-plot-mesh-tally +++ /dev/null @@ -1,344 +0,0 @@ -#!/usr/bin/env python3 - -"""Python script to plot tally data generated by OpenMC.""" - - -import os -import sys -import argparse -import tkinter as tk -import tkinter.filedialog as filedialog -import tkinter.font as font -import tkinter.messagebox as messagebox -import tkinter.ttk as ttk - -import matplotlib -matplotlib.use("TkAgg") -from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg -from matplotlib.backends.backend_tkagg import NavigationToolbar2Tk -from matplotlib.figure import Figure -import matplotlib.pyplot as plt -import numpy as np - -from openmc import StatePoint, MeshFilter, UnstructuredMesh - -_COMBOBOX_SELECTED = '<>' - - -def mesh_filter_check(filter): - """ - Check that the filter is a usable mesh filter - """ - return isinstance(filter, MeshFilter) and not isinstance(filter.mesh, UnstructuredMesh) - - -class MeshPlotter(tk.Frame): - def __init__(self, parent, filename): - super().__init__(parent) - - self.labels = { - 'Cell': 'Cell:', - 'Cellborn': 'Cell born:', - 'Surface': 'Surface:', - 'Material': 'Material:', - 'Universe': 'Universe:', - 'Energy': 'Energy in:', - 'Energyout': 'Energy out:' - } - - self.filterBoxes = {} - - # Read data from source or leakage fraction file - self.get_file_data(filename) - - # Set up top-level window - top = self.winfo_toplevel() - top.title('Mesh Tally Plotter: ' + filename) - top.rowconfigure(0, weight=1) - top.columnconfigure(0, weight=1) - self.grid(sticky=tk.W+tk.N) - - # Create widgets and draw to screen - self.create_widgets() - self.update() - - def create_widgets(self): - figureFrame = tk.Frame(self) - figureFrame.grid(row=0, column=0) - - # Create the Figure and Canvas - self.dpi = 100 - self.fig = Figure((5.0, 5.0), dpi=self.dpi) - self.canvas = FigureCanvasTkAgg(self.fig, master=figureFrame) - self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1) - - # Create the navigation toolbar, tied to the canvas - self.mpl_toolbar = NavigationToolbar2Tk(self.canvas, figureFrame) - self.mpl_toolbar.update() - self.canvas._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=1) - - # Create frame for comboboxes - self.selectFrame = tk.Frame(self) - self.selectFrame.grid(row=1, column=0, sticky=tk.W+tk.E) - - # Tally selection - labelTally = tk.Label(self.selectFrame, text='Tally:') - labelTally.grid(row=0, column=0, sticky=tk.W) - self.tallyBox = ttk.Combobox(self.selectFrame, state='readonly') - self.tallyBox['values'] = [self.datafile.tallies[i].id - for i in self.meshTallies] - self.tallyBox.current(0) - self.tallyBox.grid(row=0, column=1, sticky=tk.W+tk.E) - self.tallyBox.bind(_COMBOBOX_SELECTED, self.update) - - # Planar basis selection - labelBasis = tk.Label(self.selectFrame, text='Basis:') - labelBasis.grid(row=1, column=0, sticky=tk.W) - self.basisBox = ttk.Combobox(self.selectFrame, state='readonly') - self.basisBox['values'] = ('xy', 'yz', 'xz') - self.basisBox.current(0) - self.basisBox.grid(row=1, column=1, sticky=tk.W+tk.E) - self.basisBox.bind(_COMBOBOX_SELECTED, self.update) - - # Axial level - labelAxial = tk.Label(self.selectFrame, text='Axial level:') - labelAxial.grid(row=2, column=0, sticky=tk.W) - self.axialBox = ttk.Combobox(self.selectFrame, state='readonly') - self.axialBox.grid(row=2, column=1, sticky=tk.W+tk.E) - self.axialBox.bind(_COMBOBOX_SELECTED, self.redraw) - - # Option for mean/uncertainty - labelMean = tk.Label(self.selectFrame, text='Mean/Uncertainty:') - labelMean.grid(row=3, column=0, sticky=tk.W) - self.meanBox = ttk.Combobox(self.selectFrame, state='readonly') - self.meanBox['values'] = ('Mean', 'Absolute uncertainty', - 'Relative uncertainty') - self.meanBox.current(0) - self.meanBox.grid(row=3, column=1, sticky=tk.W+tk.E) - self.meanBox.bind(_COMBOBOX_SELECTED, self.update) - - # Scores - labelScore = tk.Label(self.selectFrame, text='Score:') - labelScore.grid(row=4, column=0, sticky=tk.W) - self.scoreBox = ttk.Combobox(self.selectFrame, state='readonly') - self.scoreBox.grid(row=4, column=1, sticky=tk.W+tk.E) - self.scoreBox.bind(_COMBOBOX_SELECTED, self.redraw) - - # Filter label - boldfont = font.Font(weight='bold') - labelFilters = tk.Label(self.selectFrame, text='Filters:', - font=boldfont) - labelFilters.grid(row=5, column=0, sticky=tk.W) - - def update(self, event=None): - widget = event.widget if event else None - - tally_id = self.meshTallies[self.tallyBox.current()] - selectedTally = self.datafile.tallies[tally_id] - - # Get mesh for selected tally - self.mesh = selectedTally.find_filter(MeshFilter).mesh - - # Get mesh dimensions - if len(self.mesh.dimension) == 2: - self.nx, self.ny = self.mesh.dimension - self.nz = 1 - else: - self.nx, self.ny, self.nz = self.mesh.dimension - - # Repopulate comboboxes baesd on current basis selection - text = self.basisBox.get() - if text == 'xy': - self.axialBox['values'] = [str(i+1) for i in range(self.nz)] - elif text == 'yz': - self.axialBox['values'] = [str(i+1) for i in range(self.nx)] - else: - self.axialBox['values'] = [str(i+1) for i in range(self.ny)] - self.axialBox.current(0) - - # If update() was called by a change in the basis combobox, we don't - # need to repopulate the filters - if widget == self.basisBox: - self.redraw() - return - - # Update scores - self.scoreBox['values'] = selectedTally.scores - self.scoreBox.current(0) - - # Remove any filter labels/comboboxes that exist - for row in range(6, self.selectFrame.grid_size()[1]): - for w in self.selectFrame.grid_slaves(row=row): - w.grid_forget() - w.destroy() - - # create a label/combobox for each filter in selected tally - count = 0 - for f in selectedTally.filters: - filterType = f.short_name - if filterType == 'Mesh': - continue - count += 1 - - # Create label and combobox for this filter - label = tk.Label(self.selectFrame, text=self.labels[filterType]) - label.grid(row=count+6, column=0, sticky=tk.W) - combobox = ttk.Combobox(self.selectFrame, state='readonly') - self.filterBoxes[filterType] = combobox - - # Set combobox items - if filterType in ['Energy', 'Energyout']: - combobox['values'] = ['{} to {}'.format(*ebin) - for ebin in f.bins] - else: - combobox['values'] = [str(i) for i in f.bins] - - combobox.current(0) - combobox.grid(row=count+6, column=1, sticky=tk.W+tk.E) - combobox.bind(_COMBOBOX_SELECTED, self.redraw) - - # If There are no filters, leave a 'None available' message - if count == 0: - count += 1 - label = tk.Label(self.selectFrame, text="None Available") - label.grid(row=count+6, column=0, sticky=tk.W) - - self.redraw() - - def redraw(self, event=None): - basis = self.basisBox.current() + 1 - axial_level = self.axialBox.current() + 1 - mbvalue = self.meanBox.get() - - # Get selected tally - tally_id = self.meshTallies[self.tallyBox.current()] - selectedTally = self.datafile.tallies[tally_id] - - # Create spec_list - spec_list = [] - for f in selectedTally.filters: - if f.short_name == 'Mesh': - mesh_filter = f - continue - elif f.short_name in ['Energy', 'Energyout']: - index = self.filterBoxes[f.short_name].current() - ebin = f.bins[index] - spec_list.append((type(f), (ebin,))) - else: - index = self.filterBoxes[f.short_name].current() - spec_list.append((type(f), (index,))) - - dims = (self.nx, self.ny, self.nz) - - text = self.basisBox.get() - if text == 'xy': - h_ind = 0 - v_ind = 1 - elif text == 'yz': - h_ind = 1 - v_ind = 2 - else: - h_ind = 0 - v_ind = 2 - - axial_ind = 3 - (h_ind + v_ind) - dims = (dims[h_ind], dims[v_ind]) - - mesh_dim = len(self.mesh.dimension) - if mesh_dim == 3: - mesh_indices = [0,0,0] - else: - mesh_indices = [0,0] - - matrix = np.zeros(dims) - for i in range(dims[0]): - for j in range(dims[1]): - if mesh_dim == 3: - mesh_indices[h_ind] = i + 1 - mesh_indices[v_ind] = j + 1 - mesh_indices[axial_ind] = axial_level - else: - mesh_indices[0] = i + 1 - mesh_indices[1] = j + 1 - filters, filter_bins = zip(*spec_list + [ - (type(mesh_filter), (tuple(mesh_indices),))]) - mean = selectedTally.get_values( - [self.scoreBox.get()], filters, filter_bins) - stdev = selectedTally.get_values( - [self.scoreBox.get()], filters, filter_bins, - value='std_dev') - if mbvalue == 'Mean': - matrix[i, j] = mean - elif mbvalue == 'Absolute uncertainty': - matrix[i, j] = stdev - else: - if mean > 0.: - matrix[i, j] = stdev/mean - else: - matrix[i, j] = 0. - - # Clear the figure - self.fig.clear() - - # Make figure, set up color bar - self.axes = self.fig.add_subplot(111) - cax = self.axes.imshow(matrix.transpose(), vmin=0.0, vmax=matrix.max(), - interpolation='none', origin='lower') - self.fig.colorbar(cax) - - self.axes.set_xticks([]) - self.axes.set_yticks([]) - self.axes.set_aspect('equal') - - # Draw canvas - self.canvas.draw() - - def get_file_data(self, filename): - # Create StatePoint object and read in data - self.datafile = StatePoint(filename) - - meshes = self.datafile.meshes - if any(isinstance(m, UnstructuredMesh) for m in meshes.values()): - warn_msg = "Unstructured meshes are present in the" \ - " statepoint file but are not currently" \ - " supported by this script" - messagebox.showwarning("Unstructured Meshes", message=warn_msg) - - # Find which tallies are mesh tallies - self.meshTallies = [] - for itally, tally in self.datafile.tallies.items(): - if any([mesh_filter_check(f) for f in tally.filters]): - self.meshTallies.append(itally) - - if not self.meshTallies: - messagebox.showerror("Invalid StatePoint File", - "File does not contain mesh tallies!") - sys.exit(1) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument('statepoint', nargs='?', help='Statepoint file') - args = parser.parse_args() - - # Hide root window - root = tk.Tk() - root.withdraw() - - # If no filename given as command-line argument, open file dialog - if args.statepoint is None: - filename = filedialog.askopenfilename(title='Select statepoint file', - initialdir='.') - else: - filename = args.statepoint - - if filename: - # Check to make sure file exists - if not os.path.isfile(filename): - messagebox.showerror("File not found", - "Could not find regular file: " + filename) - sys.exit(1) - - app = MeshPlotter(root, filename) - root.deiconify() - root.mainloop() diff --git a/scripts/openmc-track-combine b/scripts/openmc-track-combine deleted file mode 100755 index f69f2151c..000000000 --- a/scripts/openmc-track-combine +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python3 - -"""Combine multiple HDF5 particle track files.""" - -import argparse - -import openmc - - -def main(): - # Parse command-line arguments - parser = argparse.ArgumentParser( - description='Combine particle track files into a single .h5 file.') - parser.add_argument('input', metavar='IN', nargs='+', - help='Input HDF5 particle track filename(s).') - parser.add_argument('-o', '--out', metavar='OUT', default='tracks.h5', - help='Output HDF5 particle track file.') - - args = parser.parse_args() - openmc.Tracks.combine(args.input, args.out) - - -if __name__ == '__main__': - main() diff --git a/scripts/openmc-track-to-vtk b/scripts/openmc-track-to-vtk deleted file mode 100755 index 82439bea7..000000000 --- a/scripts/openmc-track-to-vtk +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python3 - -"""Convert HDF5 particle track to VTK poly data. - -""" - -import argparse - -import openmc -import vtk - - -def _parse_args(): - # Create argument parser. - parser = argparse.ArgumentParser( - description='Convert particle track file(s) to a .pvtp file.') - parser.add_argument('input', metavar='IN', type=str, - help='Input particle track data filename.') - parser.add_argument('-o', '--out', metavar='OUT', type=str, dest='out', - help='Output VTK poly data filename.') - - # Parse and return commandline arguments. - return parser.parse_args() - - -def main(): - # Parse commandline arguments. - args = _parse_args() - - # Make sure that the output filename ends with '.pvtp'. - if not args.out: - args.out = 'tracks.pvtp' - elif not args.out.endswith('.pvtp'): - args.out += '.pvtp' - - # Write coordinate values to points array. - track_file = openmc.Tracks(args.input) - track_file.write_to_vtk(args.out) - -if __name__ == '__main__': - main() diff --git a/scripts/openmc-update-inputs b/scripts/openmc-update-inputs deleted file mode 100755 index 5ee1f1ca0..000000000 --- a/scripts/openmc-update-inputs +++ /dev/null @@ -1,307 +0,0 @@ -#!/usr/bin/env python3 -"""Update OpenMC's input XML files to the latest format.""" - -import argparse -from itertools import chain -from random import randint -from shutil import move -import xml.etree.ElementTree as ET - -import openmc.data - - -description = "Update OpenMC's input XML files to the latest format." -epilog = """\ -If any of the given files do not match the most up-to-date formatting, then they -will be automatically rewritten. The old out-of-date files will not be deleted; -they will be moved to a new file with '.original' appended to their name. - -Formatting changes that will be made: - -geometry.xml: Lattices containing 'outside' attributes/tags will be replaced - with lattices containing 'outer' attributes, and the appropriate - cells/universes will be added. Any 'surfaces' attributes/elements on a cell - will be renamed 'region'. - -materials.xml: Nuclide names will be changed from ACE aliases (e.g., Am-242m) to - HDF5/GNDS names (e.g., Am242_m1). Thermal scattering table names will be - changed from ACE aliases (e.g., HH2O) to HDF5/GNDS names (e.g., c_H_in_H2O). - -""" - - -def parse_args(): - """Read the input files from the commandline.""" - # Create argument parser. - parser = argparse.ArgumentParser( - description=description, - epilog=epilog, - formatter_class=argparse.RawTextHelpFormatter) - parser.add_argument('input', metavar='IN', type=str, nargs='+', - help='Input XML file(s).') - - # Parse and return commandline arguments. - return parser.parse_args() - - -def get_universe_ids(geometry_root): - """Return a set of universe id numbers.""" - root = geometry_root - out = set() - - # Get the ids of universes defined by cells. - for cell in root.iter('cell'): - # Get universe attributes/elements - if 'universe' in cell.attrib: - uid = cell.attrib['universe'] - out.add(int(uid)) - elif cell.find('universe') is not None: - elem = cell.find('universe') - uid = elem.text - out.add(int(uid)) - else: - # Default to universe 0 - out.add(0) - - # Get the ids of universes defined by lattices. - for lat in root.iter('lattice'): - # Get id attributes. - if 'id' in lat.attrib: - uid = lat.attrib['id'] - out.add(int(uid)) - - # Get id elements. - elif lat.find('id') is not None: - elem = lat.find('id') - uid = elem.text - out.add(int(uid)) - - return out - - -def get_cell_ids(geometry_root): - """Return a set of cell id numbers.""" - root = geometry_root - out = set() - - # Get the ids of universes defined by cells. - for cell in root.iter('cell'): - # Get id attributes. - if 'id' in cell.attrib: - cid = cell.attrib['id'] - out.add(int(cid)) - - # Get id elements. - elif cell.find('id') is not None: - elem = cell.find('id') - cid = elem.text - out.add(int(cid)) - - return out - - -def find_new_id(current_ids, preferred=None): - """Return a new id that is not already present in current_ids.""" - distance_from_preferred = 21 - max_random_attempts = 10000 - - # First, try to find an id near the preferred number. - if preferred is not None: - assert isinstance(preferred, int) - for i in range(1, distance_from_preferred): - if (preferred - i not in current_ids) and (preferred - i > 0): - return preferred - i - if (preferred + i not in current_ids) and (preferred + i > 0): - return preferred + i - - # If that was unsuccessful, attempt to randomly guess a new id number. - for i in range(max_random_attempts): - num = randint(1, 2147483647) - if num not in current_inds: - return num - - # Raise an error if an id was not found. - raise RuntimeError('Could not find a unique id number for a new universe.') - - -def get_lat_id(lattice_element): - """Return the id integer of the lattice_element.""" - assert isinstance(lattice_element, ET.Element) - if 'id' in lattice_element.attrib: - return int(lattice_element.attrib['id'].strip()) - elif any([child.tag == 'id' for child in lattice_element]): - elem = lattice_element.find('id') - return int(elem.text.strip()) - else: - raise RuntimeError('Could not find the id for a lattice.') - - -def pop_lat_outside(lattice_element): - """Return lattice's outside material and remove from attributes/elements.""" - assert isinstance(lattice_element, ET.Element) - - # Check attributes. - if 'outside' in lattice_element.attrib: - material = lattice_element.attrib['outside'].strip() - del lattice_element.attrib['outside'] - - # Check subelements. - elif any([child.tag == 'outside' for child in lattice_element]): - elem = lattice_element.find('outside') - material = elem.text.strip() - lattice_element.remove(elem) - - # No 'outside' specified. This means the outside is a void. - else: - material = 'void' - - return material - - -def update_geometry(geometry_root): - """Update the given XML geometry tree. Return True if changes were made.""" - root = geometry_root - was_updated = False - - # Get a set of already-used universe and cell ids. - uids = get_universe_ids(root) - cids = get_cell_ids(root) - taken_ids = uids.union(cids) - - # Replace 'outside' with 'outer' in lattices. - for lat in chain(root.iter('lattice'), root.iter('hex_lattice')): - # Get the lattice's id. - lat_id = get_lat_id(lat) - - # Ignore lattices that have 'outer' specified. - if any([child.tag == 'outer' for child in lat]): continue - if 'outer' in lat.attrib: continue - - # Pop the 'outside' material. - material = pop_lat_outside(lat) - - # Get an id number for a new outer universe. Ideally, the id should - # be close to the lattice's id. - new_uid = find_new_id(taken_ids, preferred=lat_id) - assert new_uid not in taken_ids - - # Add the new universe filled with the old 'outside' material to the - # geometry. - new_cell = ET.Element('cell') - new_cell.attrib['id'] = str(new_uid) - new_cell.attrib['universe'] = str(new_uid) - new_cell.attrib['material'] = material - root.append(new_cell) - taken_ids.add(new_uid) - - # Add the new universe to the lattice's 'outer' attribute. - lat.attrib['outer'] = str(new_uid) - - was_updated = True - - for surface in root.findall('surface'): - for attribute in ['type', 'coeffs', 'boundary']: - if surface.find(attribute) is not None: - value = surface.find(attribute).text - surface.attrib[attribute] = value - - child_element = surface.find(attribute) - surface.remove(child_element) - was_updated = True - - # Remove 'type' from lattice definitions. - for lat in root.iter('lattice'): - elem = lat.find('type') - if elem is not None: - lat.remove(elem) - was_updated = True - if 'type' in lat.attrib: - del lat.attrib['type'] - was_updated = True - - # Change 'width' to 'pitch' in lattice definitions. - for lat in root.iter('lattice'): - elem = lat.find('width') - if elem is not None: - elem.tag = 'pitch' - was_updated = True - if 'width' in lat.attrib: - lat.attrib['pitch'] = lat.attrib['width'] - del lat.attrib['width'] - was_updated = True - - # Change 'surfaces' to 'region' in cell definitions - for cell in root.iter('cell'): - elem = cell.find('surfaces') - if elem is not None: - elem.tag = 'region' - was_updated = True - if 'surfaces' in cell.attrib: - cell.attrib['region'] = cell.attrib['surfaces'] - del cell.attrib['surfaces'] - was_updated = True - - return was_updated - -def update_materials(root): - """Update the given XML materials tree. Return True if changes were made.""" - was_updated = False - - for material in root.findall('material'): - for nuclide in material.findall('nuclide'): - if 'name' in nuclide.attrib: - nucname = nuclide.attrib['name'].replace('-', '') - # If a nuclide name is in the ZAID notation (e.g., a number), - # convert it to the proper nuclide name. - if nucname.strip().isnumeric(): - nucname = openmc.data.ace.get_metadata(int(nucname))[0] - nucname = nucname.replace('Nat', '0') - if nucname.endswith('m'): - nucname = nucname[:-1] + '_m1' - nuclide.set('name', nucname) - was_updated = True - - elif nuclide.find('name') is not None: - name_elem = nuclide.find('name') - nucname = name_elem.text - nucname = nucname.replace('-', '') - nucname = nucname.replace('Nat', '0') - if nucname.endswith('m'): - nucname = nucname[:-1] + '_m1' - name_elem.text = nucname - was_updated = True - - for sab in material.findall('sab'): - if 'name' in sab.attrib: - sabname = sab.attrib['name'] - sab.set('name', openmc.data.get_thermal_name(sabname)) - was_updated = True - - elif sab.find('name') is not None: - name_elem = sab.find('name') - sabname = name_elem.text - name_elem.text = openmc.data.get_thermal_name(sabname) - was_updated = True - - return was_updated - - -if __name__ == '__main__': - args = parse_args() - for fname in args.input: - # Parse the XML data. - tree = ET.parse(fname) - root = tree.getroot() - was_updated = False - - if root.tag == 'geometry': - was_updated = update_geometry(root) - elif root.tag == 'materials': - was_updated = update_materials(root) - - if was_updated: - # Move the original geometry file to preserve it. - move(fname, fname + '.original') - - # Write a new geometry file. - tree.write(fname, xml_declaration=True) \ No newline at end of file diff --git a/scripts/openmc-update-mgxs b/scripts/openmc-update-mgxs deleted file mode 100755 index aac6959b7..000000000 --- a/scripts/openmc-update-mgxs +++ /dev/null @@ -1,212 +0,0 @@ -#!/usr/bin/env python3 -"""Update OpenMC's deprecated multi-group cross section XML files to the latest -HDF5-based format. - -""" - -import os -import warnings -import xml.etree.ElementTree as ET - -import argparse -import numpy as np - -import openmc.mgxs_library - - -def parse_args(): - """Read the input files from the commandline.""" - # Create argument parser - parser = argparse.ArgumentParser(description=__doc__, - formatter_class=argparse.RawTextHelpFormatter) - parser.add_argument('-i', '--input', type=argparse.FileType('r'), - help='input XML file') - parser.add_argument('-o', '--output', nargs='?', default='', - help='output file, in HDF5 format') - args = vars(parser.parse_args()) - - if args['output'] == '': - filename = args['input'].name - extension = os.path.splitext(filename) - if extension == '.xml': - filename = filename[:filename.rfind('.')] + '.h5' - args['output'] = filename - - # Parse and return commandline arguments. - return args - - -def get_data(element, entry): - value = element.find(entry) - if value is not None: - value = value.text.strip() - elif entry in element.attrib: - value = element.attrib[entry].strip() - else: - value = None - - return value - - -def main(): - args = parse_args() - - # Parse the XML data. - tree = ET.parse(args['input']) - root = tree.getroot() - - # Get old metadata - group_structure = tree.find('group_structure').text.strip() - group_structure = np.array(group_structure.split(), dtype=float) - # Convert from MeV to eV - group_structure *= 1.e6 - energy_groups = openmc.mgxs.EnergyGroups(group_structure) - - inverse_velocity = tree.find('inverse-velocity') - if inverse_velocity is not None: - inverse_velocity = inverse_velocity.text.split() - inverse_velocity = np.array(inverse_velocity, dtype=float) - else: - inverse_velocity = None - - xsd = [] - names = [] - - # Now move on to the cross section data itself - for xsdata_elem in root.iter('xsdata'): - name = get_data(xsdata_elem, 'name') - - temperature = get_data(xsdata_elem, 'kT') - if temperature is not None: - temperature = float(temperature) / openmc.data.K_BOLTZMANN * 1.E6 - else: - temperature = 294. - temperatures = [temperature] - - awr = get_data(xsdata_elem, 'awr') - if awr is not None: - awr = float(awr) - - representation = get_data(xsdata_elem, 'representation') - if representation is None: - representation = 'isotropic' - if representation == 'angle': - n_azi = int(get_data(xsdata_elem, 'num_azimuthal')) - n_pol = int(get_data(xsdata_elem, 'num_polar')) - - scatter_format = get_data(xsdata_elem, 'scatt_type') - if scatter_format is None: - scatter_format = 'legendre' - - order = int(get_data(xsdata_elem, 'order')) - - tab_leg = get_data(xsdata_elem, 'tabular_legendre') - if tab_leg is not None: - warnings.warn('The tabular_legendre option has moved to the ' - 'settings.xml file and must be added manually') - - # Either add the data to a previously existing xsdata (if it is - # for the same 'name' but a different temperature), or create a - # new one. - try: - # It is in our list, so store that entry - i = names.index(name) - except ValueError: - # It is not in our list, so add it - i = -1 - xsd.append(openmc.XSdata(name, energy_groups, - temperatures=temperatures, - representation=representation)) - if awr is not None: - xsd[-1].atomic_weight_ratio = awr - if representation == 'angle': - xsd[-1].num_azimuthal = n_azi - xsd[-1].num_polar = n_pol - xsd[-1].scatter_format = scatter_format - xsd[-1].order = order - names.append(name) - - if scatter_format == 'legendre': - order_dim = order + 1 - else: - order_dim = order - - if i != -1: - xsd[i].add_temperature(temperature) - - total = get_data(xsdata_elem, 'total') - if total is not None: - total = np.array(total.split(), dtype=float) - total.shape = xsd[i].xs_shapes['[G]'] - xsd[i].set_total(total, temperature) - - if inverse_velocity is not None: - xsd[i].set_inverse_velocity(inverse_velocity, temperature) - - absorption = get_data(xsdata_elem, 'absorption') - absorption = np.array(absorption.split(), dtype=float) - absorption.shape = xsd[i].xs_shapes['[G]'] - xsd[i].set_absorption(absorption, temperature) - - scatter = get_data(xsdata_elem, 'scatter') - scatter = np.array(scatter.split(), dtype=float) - # This is now a flattened-array of something that started with a - # shape of [Order][G][G']; we need to unflatten and then switch the - # ordering - in_shape = (order_dim, energy_groups.num_groups, - energy_groups.num_groups) - if representation == 'angle': - in_shape = (n_pol, n_azi) + in_shape - scatter.shape = in_shape - scatter = np.swapaxes(scatter, 2, 3) - scatter = np.swapaxes(scatter, 3, 4) - else: - scatter.shape = in_shape - scatter = np.swapaxes(scatter, 0, 1) - scatter = np.swapaxes(scatter, 1, 2) - - xsd[i].set_scatter_matrix(scatter, temperature) - - multiplicity = get_data(xsdata_elem, 'multiplicity') - if multiplicity is not None: - multiplicity = np.array(multiplicity.split(), dtype=float) - multiplicity.shape = xsd[i].xs_shapes["[G][G']"] - xsd[i].set_multiplicity_matrix(multiplicity, temperature) - - fission = get_data(xsdata_elem, 'fission') - if fission is not None: - fission = np.array(fission.split(), dtype=float) - fission.shape = xsd[i].xs_shapes['[G]'] - xsd[i].set_fission(fission, temperature) - - kappa_fission = get_data(xsdata_elem, 'kappa_fission') - if kappa_fission is not None: - kappa_fission = np.array(kappa_fission.split(), dtype=float) - kappa_fission.shape = xsd[i].xs_shapes['[G]'] - xsd[i].set_kappa_fission(kappa_fission, temperature) - - chi = get_data(xsdata_elem, 'chi') - if chi is not None: - chi = np.array(chi.split(), dtype=float) - chi.shape = xsd[i].xs_shapes['[G]'] - xsd[i].set_chi(chi, temperature) - else: - chi = None - - nu_fission = get_data(xsdata_elem, 'nu_fission') - if nu_fission is not None: - nu_fission = np.array(nu_fission.split(), dtype=float) - if chi is not None: - nu_fission.shape = xsd[i].xs_shapes['[G]'] - else: - nu_fission.shape = xsd[i].xs_shapes["[G][G']"] - xsd[i].set_nu_fission(nu_fission, temperature) - - # Build library as we go, but first we have enough to initialize it - lib = openmc.MGXSLibrary(energy_groups) - lib.add_xsdatas(xsd) - lib.export_to_hdf5(args['output']) - - -if __name__ == '__main__': - main() diff --git a/scripts/openmc-voxel-to-vtk b/scripts/openmc-voxel-to-vtk deleted file mode 100755 index 3f59ffd42..000000000 --- a/scripts/openmc-voxel-to-vtk +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python3 - -from argparse import ArgumentParser - -import openmc - - -if __name__ == "__main__": - # Process command line arguments - parser = ArgumentParser('Converts a voxel HDF5 file to a VTK file') - parser.add_argument("voxel_file", help="Path to voxel h5 file") - parser.add_argument( - "-o", - "--output", - action="store", - default="plot", - help="Path to output VTK file.", - ) - args = parser.parse_args() - print("Reading and translating data...") - openmc.voxel_to_vtk(args.voxel_file, args.output) - print(f"Written VTK file {args.output}...") diff --git a/tests/regression_tests/track_output/test.py b/tests/regression_tests/track_output/test.py index 62526117d..299e1aa34 100644 --- a/tests/regression_tests/track_output/test.py +++ b/tests/regression_tests/track_output/test.py @@ -1,7 +1,6 @@ import glob import os from pathlib import Path -from subprocess import call import numpy as np import openmc @@ -26,8 +25,8 @@ class TrackTestHarness(TestHarness): # For MPI mode, combine track files if config['mpi']: - call(['../../../scripts/openmc-track-combine', '-o', 'tracks.h5'] + - glob.glob('tracks_p*.h5')) + track_files = list(glob.glob('tracks_p*.h5')) + openmc.Tracks.combine(track_files, 'tracks.h5') # Get string of track file information outstr = '' diff --git a/tests/testing_harness.py b/tests/testing_harness.py index ddae89e02..6de475e6e 100644 --- a/tests/testing_harness.py +++ b/tests/testing_harness.py @@ -1,5 +1,4 @@ from difflib import unified_diff -from subprocess import check_call import filecmp import glob import h5py @@ -487,8 +486,7 @@ class PlotTestHarness(TestHarness): # Check that voxel h5 can be converted to vtk for voxel_h5_filename in self._voxel_convert_checks: - check_call(['../../../scripts/openmc-voxel-to-vtk'] + - glob.glob(voxel_h5_filename)) + openmc.voxel_to_vtk(voxel_h5_filename) def _test_output_created(self): """Make sure *.png has been created.""" From a5b26de041003d30ee097e83c5540f72f912bb67 Mon Sep 17 00:00:00 2001 From: Olek <45364492+yardasol@users.noreply.github.com> Date: Mon, 17 Feb 2025 11:02:51 -0600 Subject: [PATCH 259/671] Random ray consistency changes (#3298) --- openmc/settings.py | 20 +++++++------- src/random_ray/flat_source_domain.cpp | 27 +++++-------------- .../random_ray_adjoint_k_eff/results_true.dat | 2 +- 3 files changed, 17 insertions(+), 32 deletions(-) diff --git a/openmc/settings.py b/openmc/settings.py index 77598b204..b29393d9a 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -1110,27 +1110,27 @@ class Settings: if not isinstance(random_ray, Mapping): raise ValueError(f'Unable to set random_ray from "{random_ray}" ' 'which is not a dict.') - for key in random_ray: + for key, value in random_ray.items(): if key == 'distance_active': - cv.check_type('active ray length', random_ray[key], Real) - cv.check_greater_than('active ray length', random_ray[key], 0.0) + cv.check_type('active ray length', value, Real) + cv.check_greater_than('active ray length', value, 0.0) elif key == 'distance_inactive': - cv.check_type('inactive ray length', random_ray[key], Real) + cv.check_type('inactive ray length', value, Real) cv.check_greater_than('inactive ray length', - random_ray[key], 0.0, True) + value, 0.0, True) elif key == 'ray_source': - cv.check_type('random ray source', random_ray[key], SourceBase) + cv.check_type('random ray source', value, SourceBase) elif key == 'volume_estimator': - cv.check_value('volume estimator', random_ray[key], + cv.check_value('volume estimator', value, ('naive', 'simulation_averaged', 'hybrid')) elif key == 'source_shape': - cv.check_value('source shape', random_ray[key], + cv.check_value('source shape', value, ('flat', 'linear', 'linear_xy')) elif key == 'volume_normalized_flux_tallies': - cv.check_type('volume normalized flux tallies', random_ray[key], bool) + cv.check_type('volume normalized flux tallies', value, bool) elif key == 'adjoint': - cv.check_type('adjoint', random_ray[key], bool) + cv.check_type('adjoint', value, bool) else: raise ValueError(f'Unable to set random ray to "{key}" which is ' 'unsupported by OpenMC') diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 39a50ace6..098fd17b0 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -118,7 +118,7 @@ void FlatSourceDomain::update_neutron_source(double k_eff) double inverse_k_eff = 1.0 / k_eff; - // Add scattering source + // Add scattering + fission source #pragma omp parallel for for (int64_t sr = 0; sr < n_source_regions_; sr++) { int material = source_regions_.material(sr); @@ -126,35 +126,20 @@ void FlatSourceDomain::update_neutron_source(double k_eff) for (int g_out = 0; g_out < negroups_; g_out++) { double sigma_t = sigma_t_[material * negroups_ + g_out]; double scatter_source = 0.0; + double fission_source = 0.0; for (int g_in = 0; g_in < negroups_; g_in++) { double scalar_flux = source_regions_.scalar_flux_old(sr, g_in); double sigma_s = sigma_s_[material * negroups_ * negroups_ + g_out * negroups_ + g_in]; - scatter_source += sigma_s * scalar_flux; - } - - source_regions_.source(sr, g_out) = scatter_source / sigma_t; - } - } - - // Add fission source -#pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions_; sr++) { - int material = source_regions_.material(sr); - - for (int g_out = 0; g_out < negroups_; g_out++) { - double sigma_t = sigma_t_[material * negroups_ + g_out]; - double fission_source = 0.0; - - for (int g_in = 0; g_in < negroups_; g_in++) { - double scalar_flux = source_regions_.scalar_flux_old(sr, g_in); double nu_sigma_f = nu_sigma_f_[material * negroups_ + g_in]; double chi = chi_[material * negroups_ + g_out]; + + scatter_source += sigma_s * scalar_flux; fission_source += nu_sigma_f * scalar_flux * chi; } - source_regions_.source(sr, g_out) += - fission_source * inverse_k_eff / sigma_t; + source_regions_.source(sr, g_out) = + (scatter_source + fission_source * inverse_k_eff) / sigma_t; } } diff --git a/tests/regression_tests/random_ray_adjoint_k_eff/results_true.dat b/tests/regression_tests/random_ray_adjoint_k_eff/results_true.dat index 1690d46e9..657c841b5 100644 --- a/tests/regression_tests/random_ray_adjoint_k_eff/results_true.dat +++ b/tests/regression_tests/random_ray_adjoint_k_eff/results_true.dat @@ -1,5 +1,5 @@ k-combined: -1.006640E+00 1.812967E-03 +1.006640E+00 1.812969E-03 tally 1: 6.684129E+00 8.939821E+00 From 81b7388624f70a54b7d0555acde9045c6f6b9935 Mon Sep 17 00:00:00 2001 From: Gavin Ridley Date: Mon, 17 Feb 2025 21:11:54 -0600 Subject: [PATCH 260/671] Raytrace plots (#2655) Co-authored-by: Patrick Shriwise Co-authored-by: Paul Romano --- docs/source/_images/phong_triso.png | Bin 0 -> 170621 bytes docs/source/io_formats/plots.rst | 110 ++- docs/source/pythonapi/base.rst | 3 +- docs/source/releasenotes/0.14.0.rst | 2 +- docs/source/usersguide/plots.rst | 136 +++- include/openmc/cell.h | 2 +- include/openmc/dagmc.h | 3 +- include/openmc/particle.h | 4 - include/openmc/particle_data.h | 14 +- include/openmc/plot.h | 267 ++++++- include/openmc/position.h | 20 +- include/openmc/surface.h | 2 +- openmc/_xml.py | 4 +- openmc/model/model.py | 23 +- openmc/plots.py | 512 +++++++++---- src/dagmc.cpp | 9 +- src/particle.cpp | 7 - src/particle_data.cpp | 44 +- src/plot.cpp | 674 +++++++++++++----- src/position.cpp | 7 - src/surface.cpp | 2 +- .../plot_projections/plots.xml | 45 +- .../plot_projections/results_true.dat | 2 +- .../regression_tests/plot_projections/test.py | 5 +- tests/unit_tests/test_plots.py | 66 +- 25 files changed, 1487 insertions(+), 476 deletions(-) create mode 100644 docs/source/_images/phong_triso.png diff --git a/docs/source/_images/phong_triso.png b/docs/source/_images/phong_triso.png new file mode 100644 index 0000000000000000000000000000000000000000..7f1ac61cab667c9c8d05ed3943d8e85afa967d1d GIT binary patch literal 170621 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�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
  • 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

    N_{XJ7X8HT)gSf#KU2mfqx? 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 literal 0 HcmV?d00001 diff --git a/docs/source/io_formats/plots.rst b/docs/source/io_formats/plots.rst index c1fa78330..1a42a4281 100644 --- a/docs/source/io_formats/plots.rst +++ b/docs/source/io_formats/plots.rst @@ -7,13 +7,18 @@ Geometry Plotting Specification -- plots.xml Basic plotting capabilities are available in OpenMC by creating a plots.xml file and subsequently running with the ``--plot`` command-line flag. The root element of the plots.xml is simply ```` and any number output plots can be -defined with ```` sub-elements. Two plot types are currently implemented +defined with ```` sub-elements. Four plot types are currently implemented in openMC: * ``slice`` 2D pixel plot along one of the major axes. Produces a PNG image file. * ``voxel`` 3D voxel data dump. Produces an HDF5 file containing voxel xyz position and cell or material id. +* ``wireframe_raytrace`` 2D pixel plot of a three-dimensional view of a + geometry using wireframes around cells or materials and coloring by depth + through each material. +* ``solid_raytrace`` 2D pixel plot of a three-dimensional view of a geometry + with solid colored surfaces of a set of cells or materials. ------------------ @@ -66,21 +71,22 @@ sub-elements: *Default*: None - Required entry :type: - Keyword for type of plot to be produced. Currently only "slice" and "voxel" - plots are implemented. The "slice" plot type creates 2D pixel maps saved in - the PNG file format. The "voxel" plot type produces a binary datafile - containing voxel grid positioning and the cell or material (specified by the - ``color`` tag) at the center of each voxel. Voxel plot files can be - processed into VTK files using the :func:`openmc.voxel_to_vtk` function and - subsequently viewed with a 3D viewer such as VISIT or Paraview. See the - :ref:`io_voxel` for information about the datafile structure. + Keyword for type of plot to be produced. Currently "slice", "voxel", + "wireframe_raytrace", and "solid_raytrace" plots are implemented. The + "slice" plot type creates 2D pixel maps saved in the PNG file format. The + "voxel" plot type produces a binary datafile containing voxel grid + positioning and the cell or material (specified by the ``color`` tag) at the + center of each voxel. Voxel plot files can be processed into VTK files using + the :func:`openmc.voxel_to_vtk` function and subsequently viewed with a 3D + viewer such as VISIT or Paraview. See :ref:`io_voxel` for information about + the datafile structure. .. note:: High-resolution voxel files produced by OpenMC can be quite large, but the equivalent VTK files will be significantly smaller. *Default*: "slice" -```` elements of ``type`` "slice" and "voxel" must contain the ``pixels`` +All ```` elements must contain the ``pixels`` attribute or sub-element: :pixels: @@ -96,7 +102,7 @@ attribute or sub-element: ``width``/``pixels`` along that basis direction may not appear in the plot. - *Default*: None - Required entry for "slice" and "voxel" plots + *Default*: None - Required entry for all plots ```` elements of ``type`` "slice" can also contain the following attributes or sub-elements. These are not used in "voxel" plots: @@ -125,6 +131,11 @@ attributes or sub-elements. These are not used in "voxel" plots: Specifies the custom color for the cell or material. Should be 3 integers separated by spaces. + :xs: + The attenuation coefficient for volume rendering of color in units of + inverse centimeters. Zero corresponds to transparency. Only for plot type + "wireframe_raytrace". + As an example, if your plot is colored by material and you want material 23 to be blue, the corresponding ``color`` element would look like: @@ -191,3 +202,80 @@ attributes or sub-elements. These are not used in "voxel" plots: *Default*: 0 0 0 (black) *Default*: None + +```` elements of ``type`` "wireframe_raytrace" or "solid_raytrace" can contain the +following attributes or sub-elements. + + :camera_position: + Location in 3D Cartesian space the camera is at. + + + *Default*: None - Required for all ``wireframe_raytrace`` or + ``solid_raytrace`` plots + + :look_at: + Location in 3D Cartesian space the camera is looking at. + + + *Default*: None - Required for all ``wireframe_raytrace`` or + ``solid_raytrace`` plots + + :field_of_view: + The horizontal field of view in degrees. Defaults to roughly the same value + as for the human eye. + + *Default*: 70 + + :orthographic_width: + If set to a nonzero value, an orthographic rather than perspective + projection for the camera is employed. An orthographic projection puts out + parallel rays from the camera of a width prescribed here in the horizontal + direction, with the width in the vertical direction decided by the pixel + aspect ratio. + + *Default*: 0 + +```` elements of ``type`` "solid_raytrace" can contain the following attributes or +sub-elements. + + :opaque_ids: + List of integer IDs of cells or materials to be treated as visible in the + plot. Whether the integers are interpreted as cell or material IDs depends + on ``color_by``. + + *Default*: None - Required for all phong plots + + :light_position: + Location in 3D Cartesian space of the light. + + + *Default*: Same location as ``camera_position`` + + :diffuse_fraction: + Fraction of light originating from non-directional sources. If set to one, + the coloring is not influenced by surface curvature, and no shadows appear. + If set to zero, only regions illuminated by the light are not black. + + + *Default*: 0.1 + +```` elements of ``type`` "wireframe_raytrace" can contain the following +attributes or sub-elements. + + :wireframe_color: + RGB value of the wireframe's color + + *Default*: 0, 0, 0 (black) + + :wireframe_thickness: + Integer number of pixels that the wireframe takes up. The value is a radius + of the wireframe. Setting to zero removes any wireframing. + + *Default*: 0 + + :wireframe_ids: + Integer IDs of cells or materials of regions to draw wireframes around. + Whether the integers are interpreted as cell or material IDs depends on + ``color_by``. + + *Default*: None diff --git a/docs/source/pythonapi/base.rst b/docs/source/pythonapi/base.rst index 23df02f2e..d6d04444f 100644 --- a/docs/source/pythonapi/base.rst +++ b/docs/source/pythonapi/base.rst @@ -165,7 +165,8 @@ Geometry Plotting :template: myclass.rst openmc.Plot - openmc.ProjectionPlot + openmc.WireframeRayTracePlot + openmc.SolidRayTracePlot openmc.Plots Running OpenMC diff --git a/docs/source/releasenotes/0.14.0.rst b/docs/source/releasenotes/0.14.0.rst index 445edfa07..f82aa3085 100644 --- a/docs/source/releasenotes/0.14.0.rst +++ b/docs/source/releasenotes/0.14.0.rst @@ -52,7 +52,7 @@ Compatibility Notes and Deprecations New Features ------------ -- A new :class:`openmc.ProjectionPlot` class enables the generation of orthographic or +- A new :class:`openmc.WireframeRayTracePlot` class enables the generation of orthographic or perspective projection plots. (`#1926 `_) - The :class:`openmc.model.RightCircularCylinder` class now supports optional diff --git a/docs/source/usersguide/plots.rst b/docs/source/usersguide/plots.rst index d453ea453..da0c69bdd 100644 --- a/docs/source/usersguide/plots.rst +++ b/docs/source/usersguide/plots.rst @@ -122,27 +122,82 @@ for doing this will depend on the 3D viewer, but should be straightforward. million or so). Thus if you want an accurate picture that renders smoothly, consider using only one voxel in a certain direction. ----------------- -Projection Plots ----------------- +---------------------- +Solid Ray-traced Plots +---------------------- + +.. image:: ../_images/phong_triso.png + :width: 300px + +The :class:`openmc.SolidRayTracePlot` class allows three dimensional +visualization of detailed geometric features without voxelization. The plot +above visualizes a geometry created by :class:`openmc.TRISO`, with the materials +in the fuel kernel distinguished by color. It was enclosed in a bounding box +such that some kernels are cut off, revealing the inner structure of the kernel. + +The `Phong reflection model +`_ approximates how light +reflects off of a surface. On a diffusely light-scattering material, the Phong +model prescribes the amount of light reflected from a surface as proportional to +the dot product between the normal vector of the surface and the vector between +that point on the surface and the light. With this assumption, visually +appealing plots of simulation geometries can be created. + +Solid ray-traced plots use the same ray tracing functions that neutrons and +photons do in OpenMC, so any input that does not leak particles can be +visualized in 3D using a solid ray-traced plot. That being said, these plots are +not useful for detecting overlap or undefined regions, so it is recommended to +use the slice plot approach for geometry debugging. + +Only a few inputs are required for a solid ray-traced plot. The camera location, +where the camera is looking, and a set of opaque material or cell IDs are +required. The colors of materials or cells are prescribed in the same way as +slice plots. The set of IDs that are opaque in the plot must correspond to +materials if coloring by material, or cells if coloring by cell. + +A minimal solid ray-traced plot input could be:: + + plot = openmc.SolidRayTracePlot() + plot.pixels = (600, 600) + plot.camera_position = (10.0, 20.0, -30.0) + plot.look_at = (4.0, 5.0, 1.0) + plot.color_by = 'cell' + + # optional. defaults to camera_position + plot.light_position = (10, 20, 30) + + # controls ambient lighting. Defaults to 10% + plot.diffuse_fraction = 0.1 + plot.opaque_domains = [cell2, cell3] + +These plots are then stored into a :class:`openmc.Plots` instance, just like the +slice plots. + +--------------- +Wireframe Plots +--------------- .. only:: html .. image:: ../_images/hexlat_anim.gif :width: 200px -The :class:`openmc.ProjectionPlot` class presents an alternative method of -producing 3D visualizations of OpenMC geometries. It was developed to overcome -the primary shortcoming of voxel plots, that an enormous number of voxels must -be employed to capture detailed geometric features. Projection plots perform -volume rendering on material or cell volumes, with colors specified in the same -manner as slice plots. This is done using the native ray tracing capabilities -within OpenMC, so any geometry in which particles successfully run without -overlaps or leaks will work with projection plots. +The :class:`openmc.WireframeRayTracePlot` class also produces 3D visualizations +of OpenMC geometries without voxelization but is intended to show the inside of +a model using wireframing of cell or material boundaries in addition to cell +coloring based on the path length of camera rays through the model. The coloring +in these plots is a bit like turning the model into partially transparent +colored glass that can be seen through, without any refractive effects. This is +called volume rendering. The colors are specified in exactly the same interface +employed by slice plots. -One drawback of projection plots is that particle tracks cannot be overlaid on +Similar to solid ray-traced plots, these use the native ray tracing capabilities +within OpenMC, so any geometry in which particles successfully run without +overlaps or leaks will work with wireframe plots. + +One drawback of wireframe plots is that particle tracks cannot be overlaid on them at present. Moreover, checking for overlap regions is not currently -possible with projection plots. The image heading this section can be created by +possible with wireframe plots. The image heading this section can be created by adding the following code to the hexagonal lattice example packaged with OpenMC, before exporting to plots.xml. @@ -152,7 +207,7 @@ before exporting to plots.xml. import numpy as np for i in range(100): phi = 2 * np.pi * i/100 - thisp = openmc.ProjectionPlot(plot_id = 4 + i) + thisp = openmc.WireframeRayTracePlot(plot_id = 4 + i) thisp.filename = 'frame%s'%(str(i).zfill(3)) thisp.look_at = [0, 0, 0] thisp.camera_position = [r * np.cos(phi), r * np.sin(phi), 6 * np.sin(phi)] @@ -167,42 +222,45 @@ before exporting to plots.xml. plot_file.append(thisp) -This generates a sequence of png files which can be joined to form a gif. Each +This generates a sequence of png files that can be joined to form a gif. Each image specifies a different camera position using some simple periodic functions -to create a perfectly looped gif. :attr:`ProjectionPlot.look_at` defines where -the camera's centerline should point at. :attr:`ProjectionPlot.camera_position` -similarly defines where the camera is situated in the universe level we seek to -plot. The other settings resemble those employed by :class:`openmc.Plot`, with -the exception of the :class:`ProjectionPlot.set_transparent` method and -:attr:`ProjectionPlot.xs` dictionary. These are used to control volume rendering -of material volumes. "xs" here stands for cross section, and it defines material -opacities in units of inverse centimeters. Setting this value to a large number -would make a material or cell opaque, and setting it to zero makes a material -transparent. Thus, the :class:`ProjectionPlot.set_transparent` can be used to -make all materials in the geometry transparent. From there, individual material -or cell opacities can be tuned to produce the desired result. +to create a perfectly looped gif. :attr:`~WireframeRayTracePlot.look_at` defines +where the camera's centerline should point at. +:attr:`~WireframeRayTracePlot.camera_position` similarly defines where the +camera is situated in the universe level we seek to plot. The other settings +resemble those employed by :class:`openmc.Plot`, with the exception of the +:meth:`~WireframeRayTracePlot.set_transparent` method and +:attr:`~WireframeRayTracePlot.xs` dictionary. These are used to control volume +rendering of material volumes. "xs" here stands for cross section, and it +defines material opacities in units of inverse centimeters. Setting this value +to a large number would make a material or cell opaque, and setting it to zero +makes a material transparent. Thus, the +:meth:`~WireframeRayTracePlot.set_transparent` method can be used to make all +materials in the geometry transparent. From there, individual material or cell +opacities can be tuned to produce the desired result. Two camera projections are available when using these plots, perspective and orthographic. The default, perspective projection, is a cone of rays passing through each pixel which radiate from the camera position and span the field of view in the x and y positions. The horizontal field of view can be set with the -:attr: `ProjectionPlot.horizontal_field_of_view` attribute, which is to be -specified in units of degrees. The field of view only influences behavior in +:attr:`~WireframeRayTracePlot.horizontal_field_of_view` attribute, which is to +be specified in units of degrees. The field of view only influences behavior in perspective projection mode. In the orthographic projection, rays follow the same angle but originate from different points. The horizontal width of this plane of ray starting points may -be set with the :attr: `ProjectionPlot.orthographic_width` element. If this -element is nonzero, the orthographic projection is employed. Left to its default -value of zero, the perspective projection is employed. +be set with the :attr:`~WireframeRayTracePlot.orthographic_width` attribute. If +this element is nonzero, the orthographic projection is employed. Left to its +default value of zero, the perspective projection is employed. -Lastly, projection plots come packaged with wireframe generation that can target -either all surface/cell/material boundaries in the geometry, or only wireframing -around specific regions. In the above example, we have set only the fuel region -from the hexagonal lattice example to have a wireframe drawn around it. This is -accomplished by setting the :attr: `ProjectionPlot.wireframe_domains`, which may -be set to either material IDs or cell IDs. The -:attr:`ProjectionPlot.wireframe_thickness` attribute sets the wireframe +Most importantly, wireframe plots come packaged with wireframe generation that +can target either all surface/cell/material boundaries in the geometry, or only +wireframing around specific regions. In the above example, we have set only the +fuel region from the hexagonal lattice example to have a wireframe drawn around +it. This is accomplished by setting the +:attr:`~WireframeRayTracePlot.wireframe_domains` attribute, which may be set to +either material IDs or cell IDs. The +:attr:`~WireframeRayTracePlot.wireframe_thickness` attribute sets the wireframe thickness in units of pixels. .. note:: When setting specific material or cell regions to have wireframes diff --git a/include/openmc/cell.h b/include/openmc/cell.h index d01020f8e..d7a3cad18 100644 --- a/include/openmc/cell.h +++ b/include/openmc/cell.h @@ -115,7 +115,7 @@ private: //! //! Uses the comobination of half-spaces and binary operators to determine //! if short circuiting can be used. Short cicuiting uses the relative and - //! absolute depth of parenthases in the expression. + //! absolute depth of parentheses in the expression. bool contains_complex(Position r, Direction u, int32_t on_surface) const; //! BoundingBox if the paritcle is in a simple cell. diff --git a/include/openmc/dagmc.h b/include/openmc/dagmc.h index 47fcfe237..9c4e47fdf 100644 --- a/include/openmc/dagmc.h +++ b/include/openmc/dagmc.h @@ -49,7 +49,8 @@ public: double evaluate(Position r) const override; double distance(Position r, Direction u, bool coincident) const override; Direction normal(Position r) const override; - Direction reflect(Position r, Direction u, GeometryState* p) const override; + Direction reflect( + Position r, Direction u, GeometryState* p = nullptr) const override; inline void to_hdf5_inner(hid_t group_id) const override {}; diff --git a/include/openmc/particle.h b/include/openmc/particle.h index b1f9fabd1..aaac864e1 100644 --- a/include/openmc/particle.h +++ b/include/openmc/particle.h @@ -39,10 +39,6 @@ public: double speed() const; - //! moves the particle by the distance length to its next location - //! \param length the distance the particle is moved - void move_distance(double length); - //! create a secondary particle // //! stores the current phase space attributes of the particle in the diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index 2255d1a7d..e2fac79e3 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -191,6 +191,13 @@ struct BoundaryInfo { array lattice_translation {}; //!< which way lattice indices will change + void reset() + { + distance = INFINITY; + surface = SURFACE_NONE; + coord_level = 0; + lattice_translation = {0, 0, 0}; + } // TODO: off-by-one int surface_index() const { return std::abs(surface) - 1; } }; @@ -226,6 +233,12 @@ public: n_coord_last_ = 1; } + //! moves the particle by the specified distance to its next location + //! \param distance the distance the particle is moved + void move_distance(double distance); + + void advance_to_boundary_from_void(); + // Initialize all internal state from position and direction void init_from_r_u(Position r_a, Direction u_a) { @@ -565,7 +578,6 @@ public: int& cell_born() { return cell_born_; } const int& cell_born() const { return cell_born_; } - // index of the current and last material // Total number of collisions suffered by particle int& n_collision() { return n_collision_; } const int& n_collision() const { return n_collision_; } diff --git a/include/openmc/plot.h b/include/openmc/plot.h index ee6c3fbec..69e801c5f 100644 --- a/include/openmc/plot.h +++ b/include/openmc/plot.h @@ -4,6 +4,7 @@ #include #include #include +#include #include "pugixml.hpp" #include "xtensor/xarray.hpp" @@ -61,6 +62,14 @@ struct RGBColor { return red == other.red && green == other.green && blue == other.blue; } + RGBColor& operator*=(const double x) + { + red *= x; + green *= x; + blue *= x; + return *this; + } + // Members uint8_t red, green, blue; }; @@ -70,9 +79,14 @@ const RGBColor WHITE {255, 255, 255}; const RGBColor RED {255, 0, 0}; const RGBColor BLACK {0, 0, 0}; -/* - * PlottableInterface classes just have to have a unique ID in the plots.xml - * file, and guarantee being able to create output in some way. +/** + * \class PlottableInterface + * \brief Interface for plottable objects. + * + * PlottableInterface classes must have a unique ID in the plots.xml file. + * They guarantee the ability to create output in some form. This interface + * is designed to be implemented by classes that produce plot-relevant data + * which can be visualized. */ class PlottableInterface { private: @@ -232,8 +246,8 @@ T SlicePlotBase::get_map() const data.set_overlap(y, x); } } // inner for - } // outer for - } // omp parallel + } + } return data; } @@ -268,32 +282,110 @@ public: RGBColor meshlines_color_; //!< Color of meshlines on the plot }; -class ProjectionPlot : public PlottableInterface { +/** + * \class RaytracePlot + * \brief Base class for plots that generate images through ray tracing. + * + * This class serves as a base for plots that create their visuals by tracing + * rays from a camera through the problem geometry. It inherits from + * PlottableInterface, ensuring that it provides an implementation for + * generating output specific to ray-traced visualization. WireframeRayTracePlot + * and SolidRayTracePlot provide concrete implementations of this class. + */ +class RayTracePlot : public PlottableInterface { +public: + RayTracePlot(pugi::xml_node plot); + + // Standard getters. No setting since it's done from XML. + const Position& camera_position() const { return camera_position_; } + const Position& look_at() const { return look_at_; } + const double& horizontal_field_of_view() const + { + return horizontal_field_of_view_; + } + + virtual void print_info() const; + +protected: + Direction camera_x_axis() const + { + return {camera_to_model_[0], camera_to_model_[3], camera_to_model_[6]}; + } + + Direction camera_y_axis() const + { + return {camera_to_model_[1], camera_to_model_[4], camera_to_model_[7]}; + } + + Direction camera_z_axis() const + { + return {camera_to_model_[2], camera_to_model_[5], camera_to_model_[8]}; + } + + void set_output_path(pugi::xml_node plot_node); + + /* + * Gets the starting position and direction for the pixel corresponding + * to this horizontal and vertical position. + */ + std::pair get_pixel_ray(int horiz, int vert) const; + + std::array pixels_; // pixel dimension of resulting image + +private: + void set_look_at(pugi::xml_node node); + void set_camera_position(pugi::xml_node node); + void set_field_of_view(pugi::xml_node node); + void set_pixels(pugi::xml_node node); + void set_orthographic_width(pugi::xml_node node); + + double horizontal_field_of_view_ {70.0}; // horiz. f.o.v. in degrees + Position camera_position_; // where camera is + Position look_at_; // point camera is centered looking at + + Direction up_ {0.0, 0.0, 1.0}; // which way is up + + /* The horizontal thickness, if using an orthographic projection. + * If set to zero, we assume using a perspective projection. + */ + double orthographic_width_ {C_NONE}; + + /* + * Cached camera-to-model matrix with column vectors of axes. The x-axis is + * the vector between the camera_position_ and look_at_; the y-axis is the + * cross product of the x-axis with the up_ vector, and the z-axis is the + * cross product of the x and y axes. + */ + std::array camera_to_model_; +}; + +class ProjectionRay; + +/** + * \class WireframeRayTracePlot + * \brief Creates plots that are like colorful x-ray imaging + * + * WireframeRayTracePlot is a specialized form of RayTracePlot designed for + * creating projection plots. This involves tracing rays from a camera through + * the problem geometry and rendering the results based on depth of penetration + * through materials or cells and their colors. + */ +class WireframeRayTracePlot : public RayTracePlot { + + friend class ProjectionRay; public: - ProjectionPlot(pugi::xml_node plot); + WireframeRayTracePlot(pugi::xml_node plot); virtual void create_output() const; virtual void print_info() const; private: - void set_output_path(pugi::xml_node plot_node); - void set_look_at(pugi::xml_node node); - void set_camera_position(pugi::xml_node node); - void set_field_of_view(pugi::xml_node node); - void set_pixels(pugi::xml_node node); void set_opacities(pugi::xml_node node); - void set_orthographic_width(pugi::xml_node node); void set_wireframe_thickness(pugi::xml_node node); void set_wireframe_ids(pugi::xml_node node); void set_wireframe_color(pugi::xml_node node); - /* If starting the particle from outside the geometry, we have to - * find a distance to the boundary in a non-standard surface intersection - * check. It's an exhaustive search over surfaces in the top-level universe. - */ - static int advance_to_boundary_from_void(GeometryState& p); - /* Checks if a vector of two TrackSegments is equivalent. We define this * to mean not having matching intersection lengths, but rather having * a matching sequence of surface/cell/material intersections. @@ -314,30 +406,15 @@ private: * if two surfaces bound a single cell, it allows drawing that sharp edge * where the surfaces intersect. */ - int surface; // last surface ID intersected in this segment + int surface_index {-1}; // last surface index intersected in this segment TrackSegment(int id_a, double length_a, int surface_a) - : id(id_a), length(length_a), surface(surface_a) + : id(id_a), length(length_a), surface_index(surface_a) {} }; - // Max intersections before we assume ray tracing is caught in an infinite - // loop: - static const int MAX_INTERSECTIONS = 1000000; - - std::array pixels_; // pixel dimension of resulting image - double horizontal_field_of_view_ {70.0}; // horiz. f.o.v. in degrees - Position camera_position_; // where camera is - Position look_at_; // point camera is centered looking at - Direction up_ {0.0, 0.0, 1.0}; // which way is up - // which color IDs should be wireframed. If empty, all cells are wireframed. vector wireframe_ids_; - /* The horizontal thickness, if using an orthographic projection. - * If set to zero, we assume using a perspective projection. - */ - double orthographic_width_ {0.0}; - // Thickness of the wireframe lines. Can set to zero for no wireframe. int wireframe_thickness_ {1}; @@ -345,6 +422,124 @@ private: vector xs_; // macro cross section values for cell volume rendering }; +/** + * \class SolidRayTracePlot + * \brief Plots 3D objects as the eye might see them. + * + * Plots a geometry with single-scattered Phong lighting plus a diffuse lighting + * contribution. The result is a physically reasonable, aesthetic 3D view of a + * geometry. + */ +class SolidRayTracePlot : public RayTracePlot { + friend class PhongRay; + +public: + SolidRayTracePlot(pugi::xml_node plot); + + virtual void create_output() const; + virtual void print_info() const; + +private: + void set_opaque_ids(pugi::xml_node node); + void set_light_position(pugi::xml_node node); + void set_diffuse_fraction(pugi::xml_node node); + + std::unordered_set opaque_ids_; + + double diffuse_fraction_ {0.1}; + + // By default, the light is at the camera unless otherwise specified. + 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: + Ray(Position r, Direction u) { init_from_r_u(r, u); } + + // 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, + vector& line_segments) + : Ray(r, u), plot_(plot), line_segments_(line_segments) + {} + + virtual void on_intersection() override; + +private: + /* Store a reference to the plot object which is running this ray, in order + * to access some of the plot settings which influence the behavior where + * intersections are. + */ + const WireframeRayTracePlot& plot_; + + /* The ray runs through the geometry, and records the lengths of ray segments + * and cells they lie in along the way. + */ + vector& line_segments_; +}; + +class PhongRay : public Ray { +public: + PhongRay(Position r, Direction u, const SolidRayTracePlot& plot) + : Ray(r, u), plot_(plot) + { + result_color_ = plot_.not_found_; + } + + virtual void on_intersection() override; + + const RGBColor& result_color() { return result_color_; } + +private: + const SolidRayTracePlot& plot_; + + /* After the ray is reflected, it is moving towards the + * camera. It does that in order to see if the exposed surface + * is shadowed by something else. + */ + bool reflected_ {false}; + + // Have to record the first hit ID, so that if the region + // does get shadowed, we recall what its color should be + // when tracing from the surface to the light. + int orig_hit_id_ {-1}; + + RGBColor result_color_; +}; + //=============================================================================== // Non-member functions //=============================================================================== diff --git a/include/openmc/position.h b/include/openmc/position.h index dc60ab35a..5d291d26b 100644 --- a/include/openmc/position.h +++ b/include/openmc/position.h @@ -94,8 +94,24 @@ struct Position { //! \result Reflected vector Position reflect(Position n) const; - //! Rotate the position based on a rotation matrix - Position rotate(const vector& rotation) const; + //! Rotate the position by applying a rotation matrix + template + Position rotate(const T& rotation) const + { + return {x * rotation[0] + y * rotation[1] + z * rotation[2], + x * rotation[3] + y * rotation[4] + z * rotation[5], + x * rotation[6] + y * rotation[7] + z * rotation[8]}; + } + + //! Rotate the position by applying the inverse of a rotation matrix + //! using the fact that rotation matrices are orthonormal. + template + Position inverse_rotate(const T& rotation) const + { + return {x * rotation[0] + y * rotation[3] + z * rotation[6], + x * rotation[1] + y * rotation[4] + z * rotation[7], + x * rotation[2] + y * rotation[5] + z * rotation[8]}; + } // Data members double x = 0.; diff --git a/include/openmc/surface.h b/include/openmc/surface.h index 498f71d4f..2397a64cc 100644 --- a/include/openmc/surface.h +++ b/include/openmc/surface.h @@ -62,7 +62,7 @@ public: Position r, Direction u, GeometryState* p = nullptr) const; virtual Direction diffuse_reflect( - Position r, Direction u, uint64_t* seed, GeometryState* p = nullptr) const; + Position r, Direction u, uint64_t* seed) const; //! Evaluate the equation describing the surface. //! diff --git a/openmc/_xml.py b/openmc/_xml.py index b40ecb258..17389a82f 100644 --- a/openmc/_xml.py +++ b/openmc/_xml.py @@ -82,7 +82,7 @@ def reorder_attributes(root): def get_elem_tuple(elem, name, dtype=int): - '''Helper function to get a tuple of values from an elem + """Helper function to get a tuple of values from an elem Parameters ---------- @@ -97,7 +97,7 @@ def get_elem_tuple(elem, name, dtype=int): ------- tuple of dtype Data read from the tuple - ''' + """ subelem = elem.find(name) if subelem is not None: return tuple([dtype(x) for x in subelem.text.split()]) diff --git a/openmc/model/model.py b/openmc/model/model.py index 8dd13ef6b..5e93d9804 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -124,7 +124,7 @@ class Model: @plots.setter def plots(self, plots): - check_type('plots', plots, Iterable, openmc.Plot) + check_type('plots', plots, Iterable, openmc.PlotBase) if isinstance(plots, openmc.Plots): self._plots = plots else: @@ -220,7 +220,8 @@ class Model: materials = openmc.Materials.from_xml(materials) geometry = openmc.Geometry.from_xml(geometry, materials) settings = openmc.Settings.from_xml(settings) - tallies = openmc.Tallies.from_xml(tallies) if Path(tallies).exists() else None + tallies = openmc.Tallies.from_xml( + tallies) if Path(tallies).exists() else None plots = openmc.Plots.from_xml(plots) if Path(plots).exists() else None return cls(geometry, materials, settings, tallies, plots) @@ -242,12 +243,16 @@ class Model: model = cls() meshes = {} - model.settings = openmc.Settings.from_xml_element(root.find('settings'), meshes) - model.materials = openmc.Materials.from_xml_element(root.find('materials')) - model.geometry = openmc.Geometry.from_xml_element(root.find('geometry'), model.materials) + model.settings = openmc.Settings.from_xml_element( + root.find('settings'), meshes) + model.materials = openmc.Materials.from_xml_element( + root.find('materials')) + model.geometry = openmc.Geometry.from_xml_element( + root.find('geometry'), model.materials) if root.find('tallies') is not None: - model.tallies = openmc.Tallies.from_xml_element(root.find('tallies'), meshes) + model.tallies = openmc.Tallies.from_xml_element( + root.find('tallies'), meshes) if root.find('plots') is not None: model.plots = openmc.Plots.from_xml_element(root.find('plots')) @@ -538,11 +543,13 @@ class Model: if self.tallies: tallies_element = self.tallies.to_xml_element(mesh_memo) - xml.clean_indentation(tallies_element, level=1, trailing_indent=self.plots) + xml.clean_indentation( + tallies_element, level=1, trailing_indent=self.plots) fh.write(ET.tostring(tallies_element, encoding="unicode")) if self.plots: plots_element = self.plots.to_xml_element() - xml.clean_indentation(plots_element, level=1, trailing_indent=False) + xml.clean_indentation( + plots_element, level=1, trailing_indent=False) fh.write(ET.tostring(plots_element, encoding="unicode")) fh.write("\n") diff --git a/openmc/plots.py b/openmc/plots.py index a59a7f0eb..246076598 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -555,13 +555,21 @@ class PlotBase(IDManagerMixin): else: domains = geometry.get_all_cells().values() - # Set the seed for the random number generator rng = np.random.RandomState(seed) # Generate random colors for each feature for domain in domains: self.colors[domain] = rng.randint(0, 256, (3,)) + def _colors_to_xml(self, element): + for domain, color in sorted(self._colors.items(), + key=lambda x: self._get_id(x[0])): + subelement = ET.SubElement(element, "color") + subelement.set("id", str(self._get_id(domain))) + if isinstance(color, str): + color = _SVG_COLORS[color.lower()] + subelement.set("rgb", ' '.join(str(x) for x in color)) + def to_xml_element(self): """Save common plot attributes to XML element @@ -887,13 +895,7 @@ class Plot(PlotBase): subelement.text = ' '.join(map(str, self._width)) if self._colors: - for domain, color in sorted(self._colors.items(), - key=lambda x: PlotBase._get_id(x[0])): - subelement = ET.SubElement(element, "color") - subelement.set("id", str(PlotBase._get_id(domain))) - if isinstance(color, str): - color = _SVG_COLORS[color.lower()] - subelement.set("rgb", ' '.join(str(x) for x in color)) + self._colors_to_xml(element) if self._show_overlaps: subelement = ET.SubElement(element, "show_overlaps") @@ -1051,7 +1053,8 @@ class Plot(PlotBase): """ if self.type != 'voxel': - raise ValueError('Generating a VTK file only works for voxel plots') + raise ValueError( + 'Generating a VTK file only works for voxel plots') # Create plots.xml Plots([self]).export_to_xml(cwd) @@ -1072,20 +1075,17 @@ class Plot(PlotBase): return voxel_to_vtk(h5_voxel_file, output) -class ProjectionPlot(PlotBase): +class RayTracePlot(PlotBase): """Definition of a camera's view of OpenMC geometry - Colors are defined in the same manner as the Plot class, but with the addition - of a coloring parameter resembling a macroscopic cross section in units of inverse - centimeters. The volume rendering technique is used to color regions of the model. - An infinite cross section denotes a fully opaque region, and zero represents a - transparent region which will expose the color of the regions behind it. - The camera projection may either by orthographic or perspective. Perspective - projections are more similar to a pinhole camera, and orthographic projections - preserve parallel lines and distances. + projections are more similar to a pinhole camera, and orthographic + projections preserve parallel lines and distances. - .. versionadded:: 0.14.0 + This is an abstract base class that :class:`WireframeRayTracePlot` and + :class:`SolidRayTracePlot` finish the implementation of. + + .. versionadded:: 0.15.1 Parameters ---------- @@ -1113,21 +1113,6 @@ class ProjectionPlot(PlotBase): unlike with the default perspective projection. The height of the array is deduced from the ratio of pixel dimensions for the image. Defaults to zero, i.e. using perspective projection. - wireframe_thickness : int - Line thickness employed for drawing wireframes around cells or - material regions. Can be set to zero for no wireframes at all. - Defaults to one pixel. - wireframe_color : tuple of ints - RGB color of the wireframe lines. Defaults to black. - wireframe_domains : iterable of either Material or Cells - If provided, the wireframe is only drawn around these. - If color_by is by material, it must be a list of materials, else cells. - xs : dict - A mapping from cell/material IDs to floats. The floating point values - are macroscopic cross sections influencing the volume rendering opacity - of each geometric region. Zero corresponds to perfect transparency, and - infinity equivalent to opaque. These must be set by the user, but default - values can be obtained using the set_transparent method. """ def __init__(self, plot_id=None, name=''): @@ -1138,10 +1123,6 @@ class ProjectionPlot(PlotBase): self._look_at = (0.0, 0.0, 0.0) self._up = (0.0, 0.0, 1.0) self._orthographic_width = 0.0 - self._wireframe_thickness = 1 - self._wireframe_color = _SVG_COLORS['black'] - self._wireframe_domains = [] - self._xs = {} @property def horizontal_field_of_view(self): @@ -1195,6 +1176,161 @@ class ProjectionPlot(PlotBase): assert orthographic_width >= 0.0 self._orthographic_width = orthographic_width + def _check_domains_consistent_with_color_by(self, domains): + """Check domains are the same as the type we are coloring by""" + for region in domains: + # if an integer is passed, we have to assume it was a valid ID + if isinstance(region, int): + continue + + if self._color_by == 'material': + if not isinstance(region, openmc.Material): + raise Exception('Domain list must be materials if ' + 'color_by=material') + else: + if not isinstance(region, openmc.Cell): + raise Exception('Domain list must be cells if ' + 'color_by=cell') + + def to_xml_element(self): + """Return XML representation of the ray trace plot + + Returns + ------- + element : lxml.etree._Element + XML element containing plot data + + """ + + element = super().to_xml_element() + element.set("id", str(self._id)) + + subelement = ET.SubElement(element, "camera_position") + subelement.text = ' '.join(map(str, self._camera_position)) + + subelement = ET.SubElement(element, "look_at") + subelement.text = ' '.join(map(str, self._look_at)) + + subelement = ET.SubElement(element, "horizontal_field_of_view") + subelement.text = str(self._horizontal_field_of_view) + + # do not need to write if orthographic_width == 0.0 + if self._orthographic_width > 0.0: + subelement = ET.SubElement(element, "orthographic_width") + subelement.text = str(self._orthographic_width) + + return element + + def __repr__(self): + string = '' + string += '{: <16}=\t{}\n'.format('\tID', self._id) + string += '{: <16}=\t{}\n'.format('\tName', self._name) + string += '{: <16}=\t{}\n'.format('\tFilename', self._filename) + string += '{: <16}=\t{}\n'.format('\tHorizontal FOV', + self._horizontal_field_of_view) + string += '{: <16}=\t{}\n'.format('\tOrthographic width', + self._orthographic_width) + string += '{: <16}=\t{}\n'.format('\tCamera position', + self._camera_position) + string += '{: <16}=\t{}\n'.format('\tLook at', self._look_at) + string += '{: <16}=\t{}\n'.format('\tUp', self._up) + string += '{: <16}=\t{}\n'.format('\tPixels', self._pixels) + string += '{: <16}=\t{}\n'.format('\tColor by', self._color_by) + string += '{: <16}=\t{}\n'.format('\tBackground', self._background) + string += '{: <16}=\t{}\n'.format('\tColors', self._colors) + string += '{: <16}=\t{}\n'.format('\tLevel', self._level) + return string + + def _read_xml_attributes(self, elem): + """Helper function called by from_xml_element + of child classes. These are common vaues to be + read by any ray traced plot. + + Returns + ------- + None + """ + + if "filename" in elem.keys(): + self.filename = elem.get("filename") + self.color_by = elem.get("color_by") + + horizontal_fov = elem.find("horizontal_field_of_view") + if horizontal_fov is not None: + self.horizontal_field_of_view = float(horizontal_fov.text) + + if (tmp := elem.find("orthographic_width")) is not None: + self.orthographic_width = float(tmp) + + self.pixels = get_elem_tuple(elem, "pixels") + self.camera_position = get_elem_tuple(elem, "camera_position", float) + self.look_at = get_elem_tuple(elem, "look_at", float) + + if elem.find("background") is not None: + self.background = get_elem_tuple(elem, "background") + + # Set masking information + if (mask_elem := elem.find("mask")) is not None: + mask_components = [int(x) + for x in mask_elem.get("components").split()] + # TODO: set mask components(needs geometry information) + background = mask_elem.get("background") + if background is not None: + self.mask_background = tuple( + [int(x) for x in background.split()]) + + # Set universe level + level = elem.find("level") + if level is not None: + self.level = int(level.text) + + +class WireframeRayTracePlot(RayTracePlot): + """Plots wireframes of geometry with volume rendered colors + + Colors are defined in the same manner as the Plot class, but with the + addition of a coloring parameter resembling a macroscopic cross section in + units of inverse centimeters. The volume rendering technique is used to + color regions of the model. An infinite cross section denotes a fully opaque + region, and zero represents a transparent region which will expose the color + of the regions behind it. + + .. versionchanged:: 0.15.1 + Renamed from ProjectionPlot to WireframeRayTracePlot + + Parameters + ---------- + plot_id : int + Unique identifier for the plot + name : str + Name of the plot + + Attributes + ---------- + wireframe_thickness : int + Line thickness employed for drawing wireframes around cells or material + regions. Can be set to zero for no wireframes at all. Defaults to one + pixel. + wireframe_color : tuple of ints + RGB color of the wireframe lines. Defaults to black. + wireframe_domains : iterable of either Material or Cells + If provided, the wireframe is only drawn around these. If color_by is by + material, it must be a list of materials, else cells. + xs : dict + A mapping from cell/material IDs to floats. The floating point values + are macroscopic cross sections influencing the volume rendering opacity + of each geometric region. Zero corresponds to perfect transparency, and + infinity equivalent to opaque. These must be set by the user, but + default values can be obtained using the :meth:`set_transparent` method. + """ + + def __init__(self, plot_id=None, name=''): + super().__init__(plot_id, name) + self._wireframe_thickness = 1 + self._wireframe_color = _SVG_COLORS['black'] + self._wireframe_domains = [] + self._xs = {} + @property def wireframe_thickness(self): return self._wireframe_thickness @@ -1221,15 +1357,6 @@ class ProjectionPlot(PlotBase): @wireframe_domains.setter def wireframe_domains(self, wireframe_domains): - for region in wireframe_domains: - if self._color_by == 'material': - if not isinstance(region, openmc.Material): - raise Exception('Must provide a list of materials for \ - wireframe_region if color_by=Material') - else: - if not isinstance(region, openmc.Cell): - raise Exception('Must provide a list of cells for \ - wireframe_region if color_by=cell') self._wireframe_domains = wireframe_domains @property @@ -1266,6 +1393,18 @@ class ProjectionPlot(PlotBase): for domain in domains: self.xs[domain] = 0.0 + def __repr__(self): + string = 'Wireframe Ray-traced Plot\n' + string += super().__repr__() + string += '{: <16}=\t{}\n'.format('\tWireframe thickness', + self._wireframe_thickness) + string += '{: <16}=\t{}\n'.format('\tWireframe color', + self._wireframe_color) + string += '{: <16}=\t{}\n'.format('\tWireframe domains', + self._wireframe_domains) + string += '{: <16}=\t{}\n'.format('\tTransparencies', self._xs) + return string + def to_xml_element(self): """Return XML representation of the projection plot @@ -1275,15 +1414,8 @@ class ProjectionPlot(PlotBase): XML element containing plot data """ - element = super().to_xml_element() - element.set("type", "projection") - - subelement = ET.SubElement(element, "camera_position") - subelement.text = ' '.join(map(str, self._camera_position)) - - subelement = ET.SubElement(element, "look_at") - subelement.text = ' '.join(map(str, self._look_at)) + element.set("type", "wireframe_raytrace") subelement = ET.SubElement(element, "wireframe_thickness") subelement.text = str(self._wireframe_thickness) @@ -1294,6 +1426,8 @@ class ProjectionPlot(PlotBase): color = _SVG_COLORS[color.lower()] subelement.text = ' '.join(str(x) for x in color) + self._check_domains_consistent_with_color_by(self.wireframe_domains) + if self._wireframe_domains: id_list = [x.id for x in self._wireframe_domains] subelement = ET.SubElement(element, "wireframe_ids") @@ -1311,43 +1445,8 @@ class ProjectionPlot(PlotBase): subelement.set("rgb", ' '.join(str(x) for x in color)) subelement.set("xs", str(self._xs[domain])) - subelement = ET.SubElement(element, "horizontal_field_of_view") - subelement.text = str(self._horizontal_field_of_view) - - # do not need to write if orthographic_width == 0.0 - if self._orthographic_width > 0.0: - subelement = ET.SubElement(element, "orthographic_width") - subelement.text = str(self._orthographic_width) - return element - def __repr__(self): - string = 'Projection Plot\n' - string += '{: <16}=\t{}\n'.format('\tID', self._id) - string += '{: <16}=\t{}\n'.format('\tName', self._name) - string += '{: <16}=\t{}\n'.format('\tFilename', self._filename) - string += '{: <16}=\t{}\n'.format('\tHorizontal FOV', - self._horizontal_field_of_view) - string += '{: <16}=\t{}\n'.format('\tOrthographic width', - self._orthographic_width) - string += '{: <16}=\t{}\n'.format('\tWireframe thickness', - self._wireframe_thickness) - string += '{: <16}=\t{}\n'.format('\tWireframe color', - self._wireframe_color) - string += '{: <16}=\t{}\n'.format('\tWireframe domains', - self._wireframe_domains) - string += '{: <16}=\t{}\n'.format('\tCamera position', - self._camera_position) - string += '{: <16}=\t{}\n'.format('\tLook at', self._look_at) - string += '{: <16}=\t{}\n'.format('\tUp', self._up) - string += '{: <16}=\t{}\n'.format('\tPixels', self._pixels) - string += '{: <16}=\t{}\n'.format('\tColor by', self._color_by) - string += '{: <16}=\t{}\n'.format('\tBackground', self._background) - string += '{: <16}=\t{}\n'.format('\tColors', self._colors) - string += '{: <16}=\t{}\n'.format('\tTransparencies', self._xs) - string += '{: <16}=\t{}\n'.format('\tLevel', self._level) - return string - @classmethod def from_xml_element(cls, elem): """Generate plot object from an XML element @@ -1359,60 +1458,193 @@ class ProjectionPlot(PlotBase): Returns ------- - openmc.ProjectionPlot - ProjectionPlot object + openmc.WireframeRayTracePlot + WireframeRayTracePlot object """ + plot_id = int(elem.get("id")) - plot = cls(plot_id) - if "filename" in elem.keys(): - plot.filename = elem.get("filename") - plot.color_by = elem.get("color_by") - plot.type = "projection" + plot_name = get_text(elem, 'name', '') + plot = cls(plot_id, plot_name) + plot.type = "wireframe_raytrace" - horizontal_fov = elem.find("horizontal_field_of_view") - if horizontal_fov is not None: - plot.horizontal_field_of_view = float(horizontal_fov.text) + plot._read_xml_attributes(elem) - tmp = elem.find("orthographic_width") - if tmp is not None: - plot.orthographic_width = float(tmp) - - plot.pixels = get_elem_tuple(elem, "pixels") - plot.camera_position = get_elem_tuple(elem, "camera_position", float) - plot.look_at = get_elem_tuple(elem, "look_at", float) - - # Attempt to get wireframe thickness. May not be present - wireframe_thickness = elem.get("wireframe_thickness") - if wireframe_thickness: - plot.wireframe_thickness = int(wireframe_thickness) + # Attempt to get wireframe thickness.May not be present + wireframe_thickness = elem.find("wireframe_thickness") + if wireframe_thickness is not None: + plot.wireframe_thickness = int(wireframe_thickness.text) wireframe_color = elem.get("wireframe_color") if wireframe_color: plot.wireframe_color = [int(item) for item in wireframe_color] # Set plot colors - colors = {} - xs = {} + for color_elem in elem.findall("color"): + uid = int(color_elem.get("id")) + plot.colors[uid] = tuple(int(i) + for i in get_text(color_elem, 'rgb').split()) + plot.xs[uid] = float(color_elem.get("xs")) + + return plot + + +class SolidRayTracePlot(RayTracePlot): + """Phong shading-based rendering of an OpenMC geometry + + This class defines a plot that uses Phong shading to enhance the + visualization of an OpenMC geometry by incorporating diffuse lighting and + configurable opacity for certain regions. It extends :class:`RayTracePlot` + by adding parameters related to lighting and transparency. + + .. versionadded:: 0.15.1 + + Parameters + ---------- + plot_id : int, optional + Unique identifier for the plot + name : str, optional + Name of the plot + + Attributes + ---------- + light_position : tuple or list of float + Position of the light source in 3D space. Defaults to None, which places + the light at the camera position. + diffuse_fraction : float + Fraction of lighting that is diffuse (non-directional). Defaults to 0.1. + Must be between 0 and 1. + opaque_domains : list + List of domains (e.g., cells or materials) that should be rendered as + opaque rather than allowing transparency. + """ + + def __init__(self, plot_id=None, name=''): + super().__init__(plot_id, name) + self._light_position = None + self._diffuse_fraction = 0.1 + self._opaque_domains = [] + + @property + def light_position(self): + return self._light_position + + @light_position.setter + def light_position(self, x): + cv.check_type('plot light position', x, Iterable, Real) + cv.check_length('plot light position', x, 3) + self._light_position = x + + @property + def diffuse_fraction(self): + return self._diffuse_fraction + + @diffuse_fraction.setter + def diffuse_fraction(self, x): + cv.check_type('diffuse fraction', x, Real) + cv.check_greater_than('diffuse fraction', x, 0.0, equality=True) + cv.check_less_than('diffuse fraction', x, 1.0, equality=True) + self._diffuse_fraction = x + + @property + def opaque_domains(self): + return self._opaque_domains + + @opaque_domains.setter + def opaque_domains(self, x): + # Note that _check_domains_consistent_with_color_by checks + # the types within later. This is because we don't necessarily + # know what types are acceptable until the user has set the + # color_by attribute, too. + cv.check_type('opaque domains', x, Iterable) + self._opaque_domains = x + + def __repr__(self): + string = 'Solid Ray-traced Plot\n' + string += super().__repr__() + string += '{: <16}=\t{}\n'.format('\tDiffuse Fraction', + self._diffuse_fraction) + string += '{: <16}=\t{}\n'.format('\tLight position', + self._light_position) + string += '{: <16}=\t{}\n'.format('\tOpaque domains', + self._opaque_domains) + return string + + def to_xml_element(self): + """Return XML representation of the solid ray-traced plot + + Returns + ------- + element : lxml.etree._Element + XML element containing plot data + + """ + element = super().to_xml_element() + element.set("type", "solid_raytrace") + + # no light position means put it at the camera + if self._light_position: + subelement = ET.SubElement(element, "light_position") + subelement.text = ' '.join(map(str, self._light_position)) + + # no diffuse fraction defaults to 0.1 + if self._diffuse_fraction: + subelement = ET.SubElement(element, "diffuse_fraction") + subelement.text = str(self._diffuse_fraction) + + self._check_domains_consistent_with_color_by(self.opaque_domains) + subelement = ET.SubElement(element, "opaque_ids") + + # Extract all IDs, or use the integer value passed in + # explicitly if that was given + subelement.text = ' '.join( + [str(domain) if isinstance(domain, int) else + str(domain.id) for domain in self._opaque_domains]) + + if self._colors: + self._colors_to_xml(element) + + return element + + def _read_phong_attributes(self, elem): + """Read attributes specific to the Phong plot from an XML element""" + if elem.find('light_position') is not None: + self.light_position = get_elem_tuple(elem, 'light_position', float) + + diffuse_fraction = elem.find('diffuse_fraction') + if diffuse_fraction is not None: + self.diffuse_fraction = float(diffuse_fraction.text) + + if elem.find('opaque_ids') is not None: + self.opaque_domains = list(get_elem_tuple(elem, 'opaque_ids', int)) + + @classmethod + def from_xml_element(cls, elem): + """Generate plot object from an XML element + + Parameters + ---------- + elem : lxml.etree._Element + XML element + + Returns + ------- + openmc.WireframeRayTracePlot + WireframeRayTracePlot object + + """ + + plot_id = int(elem.get("id")) + plot_name = get_text(elem, 'name', '') + plot = cls(plot_id, plot_name) + plot.type = "solid_raytrace" + + plot._read_xml_attributes(elem) + plot._read_phong_attributes(elem) + + # Set plot colors for color_elem in elem.findall("color"): uid = color_elem.get("id") - colors[uid] = get_elem_tuple(color_elem, "rgb") - xs[uid] = float(color_elem.get("xs")) - - # Set masking information - mask_elem = elem.find("mask") - if mask_elem is not None: - mask_components = [int(x) - for x in mask_elem.get("components").split()] - # TODO: set mask components (needs geometry information) - background = mask_elem.get("background") - if background is not None: - plot.mask_background = tuple( - [int(x) for x in background.split()]) - - # Set universe level - level = elem.find("level") - if level is not None: - plot.level = int(level.text) + plot.colors[uid] = get_elem_tuple(color_elem, "rgb") return plot @@ -1434,13 +1666,13 @@ class Plots(cv.CheckedList): Parameters ---------- - plots : Iterable of openmc.Plot or openmc.ProjectionPlot + plots : Iterable of openmc.PlotBase plots to add to the collection """ def __init__(self, plots=None): - super().__init__((Plot, ProjectionPlot), 'plots collection') + super().__init__(PlotBase, 'plots collection') self._plots_file = ET.Element("plots") if plots is not None: self += plots @@ -1450,7 +1682,7 @@ class Plots(cv.CheckedList): Parameters ---------- - plot : openmc.Plot or openmc.ProjectionPlot + plot : openmc.PlotBase Plot to append """ @@ -1582,10 +1814,14 @@ class Plots(cv.CheckedList): plots = cls() for e in elem.findall('plot'): plot_type = e.get('type') - if plot_type == 'projection': - plots.append(ProjectionPlot.from_xml_element(e)) - else: + if plot_type == 'wireframe_raytrace': + plots.append(WireframeRayTracePlot.from_xml_element(e)) + elif plot_type == 'solid_raytrace': + plots.append(SolidRayTracePlot.from_xml_element(e)) + elif plot_type in ('slice', 'voxel'): plots.append(Plot.from_xml_element(e)) + else: + raise ValueError("Unknown plot type: {}".format(plot_type)) return plots @classmethod diff --git a/src/dagmc.cpp b/src/dagmc.cpp index 52c45f21c..e24b3fbf6 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -808,15 +808,12 @@ Direction DAGSurface::normal(Position r) const Direction DAGSurface::reflect(Position r, Direction u, GeometryState* p) const { Expects(p); - p->history().reset_to_last_intersection(); - moab::ErrorCode rval; - moab::EntityHandle surf = dagmc_ptr_->entity_by_index(2, dag_index_); double pnt[3] = {r.x, r.y, r.z}; double dir[3]; - rval = dagmc_ptr_->get_angle(surf, pnt, dir, &p->history()); + moab::ErrorCode rval = + dagmc_ptr_->get_angle(mesh_handle(), pnt, dir, &p->history()); MB_CHK_ERR_CONT(rval); - p->last_dir() = u.reflect(dir); - return p->last_dir(); + return u.reflect(dir); } //============================================================================== diff --git a/src/particle.cpp b/src/particle.cpp index 4dbab3213..5b46e2e63 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -75,13 +75,6 @@ double Particle::speed() const } } -void Particle::move_distance(double length) -{ - for (int j = 0; j < n_coord(); ++j) { - coord(j).r += length * coord(j).u; - } -} - void Particle::create_secondary( double wgt, Direction u, double E, ParticleType type) { diff --git a/src/particle_data.cpp b/src/particle_data.cpp index d8a5bb9d9..fef8359a3 100644 --- a/src/particle_data.cpp +++ b/src/particle_data.cpp @@ -15,6 +15,11 @@ namespace openmc { +void GeometryState::mark_as_lost(const char* message) +{ + fatal_error(message); +} + void GeometryState::mark_as_lost(const std::string& message) { mark_as_lost(message.c_str()); @@ -25,11 +30,6 @@ void GeometryState::mark_as_lost(const std::stringstream& message) mark_as_lost(message.str()); } -void GeometryState::mark_as_lost(const char* message) -{ - fatal_error(message); -} - void LocalCoord::rotate(const vector& rotation) { r = r.rotate(rotation); @@ -56,6 +56,40 @@ GeometryState::GeometryState() clear(); } +void GeometryState::advance_to_boundary_from_void() +{ + auto root_coord = this->coord(0); + const auto& root_universe = model::universes[model::root_universe]; + boundary().reset(); + + for (auto c_i : root_universe->cells_) { + auto dist = + model::cells.at(c_i)->distance(root_coord.r, root_coord.u, 0, this); + if (dist.first < boundary().distance) { + boundary().distance = dist.first; + boundary().surface = dist.second; + } + } + + // if no intersection or near-infinite intersection, reset + // boundary information + if (boundary().distance > 1e300) { + boundary().distance = INFTY; + boundary().surface = SURFACE_NONE; + return; + } + + // move the particle up to (and just past) the boundary + move_distance(boundary().distance + TINY_BIT); +} + +void GeometryState::move_distance(double length) +{ + for (int j = 0; j < n_coord(); ++j) { + coord(j).r += length * coord(j).u; + } +} + ParticleData::ParticleData() { zero_delayed_bank(); diff --git a/src/plot.cpp b/src/plot.cpp index b36ed6f5d..093f89f8c 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -204,18 +204,21 @@ void read_plots_xml(pugi::xml_node root) int id = std::stoi(id_string); if (check_for_node(node, "type")) { std::string type_str = get_node_value(node, "type", true); - if (type_str == "slice") + if (type_str == "slice") { model::plots.emplace_back( std::make_unique(node, Plot::PlotType::slice)); - else if (type_str == "voxel") + } else if (type_str == "voxel") { model::plots.emplace_back( std::make_unique(node, Plot::PlotType::voxel)); - else if (type_str == "projection") - model::plots.emplace_back(std::make_unique(node)); - else + } else if (type_str == "wireframe_raytrace") { + model::plots.emplace_back( + std::make_unique(node)); + } else if (type_str == "solid_raytrace") { + model::plots.emplace_back(std::make_unique(node)); + } else { fatal_error( fmt::format("Unsupported plot type '{}' in plot {}", type_str, id)); - + } model::plot_map[model::plots.back()->id()] = model::plots.size() - 1; } else { fatal_error(fmt::format("Must specify plot type in plot {}", id)); @@ -264,8 +267,8 @@ void Plot::create_image() const } data(x, y) = colors_[model::material_map[id]]; } // color_by if-else - } // x for loop - } // y for loop + } + } // draw mesh lines if present if (index_meshlines_mesh_ >= 0) { @@ -1036,26 +1039,49 @@ RGBColor random_color(void) int(prn(&model::plotter_seed) * 255), int(prn(&model::plotter_seed) * 255)}; } -ProjectionPlot::ProjectionPlot(pugi::xml_node node) : PlottableInterface(node) +RayTracePlot::RayTracePlot(pugi::xml_node node) : PlottableInterface(node) { - set_output_path(node); set_look_at(node); set_camera_position(node); set_field_of_view(node); set_pixels(node); - set_opacities(node); set_orthographic_width(node); - set_wireframe_thickness(node); - set_wireframe_ids(node); - set_wireframe_color(node); + set_output_path(node); if (check_for_node(node, "orthographic_width") && check_for_node(node, "field_of_view")) fatal_error("orthographic_width and field_of_view are mutually exclusive " "parameters."); + + // Get centerline vector for camera-to-model. We create vectors around this + // that form a pixel array, and then trace rays along that. + auto up = up_ / up_.norm(); + Direction looking_direction = look_at_ - camera_position_; + looking_direction /= looking_direction.norm(); + if (std::abs(std::abs(looking_direction.dot(up)) - 1.0) < 1e-9) + fatal_error("Up vector cannot align with vector between camera position " + "and look_at!"); + Direction cam_yaxis = looking_direction.cross(up); + cam_yaxis /= cam_yaxis.norm(); + Direction cam_zaxis = cam_yaxis.cross(looking_direction); + cam_zaxis /= cam_zaxis.norm(); + + // Cache the camera-to-model matrix + camera_to_model_ = {looking_direction.x, cam_yaxis.x, cam_zaxis.x, + looking_direction.y, cam_yaxis.y, cam_zaxis.y, looking_direction.z, + cam_yaxis.z, cam_zaxis.z}; } -void ProjectionPlot::set_wireframe_color(pugi::xml_node plot_node) +WireframeRayTracePlot::WireframeRayTracePlot(pugi::xml_node node) + : RayTracePlot(node) +{ + set_opacities(node); + set_wireframe_thickness(node); + set_wireframe_ids(node); + set_wireframe_color(node); +} + +void WireframeRayTracePlot::set_wireframe_color(pugi::xml_node plot_node) { // Copy plot background color if (check_for_node(plot_node, "wireframe_color")) { @@ -1068,7 +1094,7 @@ void ProjectionPlot::set_wireframe_color(pugi::xml_node plot_node) } } -void ProjectionPlot::set_output_path(pugi::xml_node node) +void RayTracePlot::set_output_path(pugi::xml_node node) { // Set output file path std::string filename; @@ -1089,33 +1115,7 @@ void ProjectionPlot::set_output_path(pugi::xml_node node) path_plot_ = filename; } -// Advances to the next boundary from outside the geometry -// Returns -1 if no intersection found, and the surface index -// if an intersection was found. -int ProjectionPlot::advance_to_boundary_from_void(GeometryState& p) -{ - constexpr double scoot = 1e-5; - double min_dist = {INFINITY}; - auto coord = p.coord(0); - Universe* uni = model::universes[model::root_universe].get(); - int intersected_surface = -1; - for (auto c_i : uni->cells_) { - auto dist = model::cells.at(c_i)->distance(coord.r, coord.u, 0, &p); - if (dist.first < min_dist) { - min_dist = dist.first; - intersected_surface = dist.second; - } - } - if (min_dist > 1e300) - return -1; - else { // advance the particle - for (int j = 0; j < p.n_coord(); ++j) - p.coord(j).r += (min_dist + scoot) * p.coord(j).u; - return std::abs(intersected_surface); - } -} - -bool ProjectionPlot::trackstack_equivalent( +bool WireframeRayTracePlot::trackstack_equivalent( const std::vector& track1, const std::vector& track2) const { @@ -1125,7 +1125,7 @@ bool ProjectionPlot::trackstack_equivalent( return false; for (int i = 0; i < track1.size(); ++i) { if (track1[i].id != track2[i].id || - track1[i].surface != track2[i].surface) { + track1[i].surface_index != track2[i].surface_index) { return false; } } @@ -1152,7 +1152,7 @@ bool ProjectionPlot::trackstack_equivalent( if (t1_i == track1.size() && t2_i == track2.size()) break; // Check if surface different - if (track1[t1_i].surface != track2[t2_i].surface) + if (track1[t1_i].surface_index != track2[t2_i].surface_index) return false; // Pretty sure this should not be used: @@ -1160,7 +1160,7 @@ bool ProjectionPlot::trackstack_equivalent( // t1_i != track1.size() - 1 && // track1[t1_i+1].id != track2[t2_i+1].id) return false; if (t2_i != 0 && t1_i != 0 && - track1[t1_i - 1].surface != track2[t2_i - 1].surface) + track1[t1_i - 1].surface_index != track2[t2_i - 1].surface_index) return false; // Check if neighboring cells are different @@ -1176,44 +1176,58 @@ bool ProjectionPlot::trackstack_equivalent( } } -void ProjectionPlot::create_output() const +std::pair RayTracePlot::get_pixel_ray( + int horiz, int vert) const { - // Get centerline vector for camera-to-model. We create vectors around this - // that form a pixel array, and then trace rays along that. - auto up = up_ / up_.norm(); - Direction looking_direction = look_at_ - camera_position_; - looking_direction /= looking_direction.norm(); - if (std::abs(std::abs(looking_direction.dot(up)) - 1.0) < 1e-9) - fatal_error("Up vector cannot align with vector between camera position " - "and look_at!"); - Direction cam_yaxis = looking_direction.cross(up); - cam_yaxis /= cam_yaxis.norm(); - Direction cam_zaxis = cam_yaxis.cross(looking_direction); - cam_zaxis /= cam_zaxis.norm(); - - // Transformation matrix for directions - std::vector camera_to_model = {looking_direction.x, cam_yaxis.x, - cam_zaxis.x, looking_direction.y, cam_yaxis.y, cam_zaxis.y, - looking_direction.z, cam_yaxis.z, cam_zaxis.z}; - - // Now we convert to the polar coordinate system with the polar angle - // measuring the angle from the vector up_. Phi is the rotation about up_. For - // now, up_ is hard-coded to be +z. + // Compute field of view in radians constexpr double DEGREE_TO_RADIAN = M_PI / 180.0; double horiz_fov_radians = horizontal_field_of_view_ * DEGREE_TO_RADIAN; double p0 = static_cast(pixels_[0]); double p1 = static_cast(pixels_[1]); double vert_fov_radians = horiz_fov_radians * p1 / p0; - double dphi = horiz_fov_radians / p0; - double dmu = vert_fov_radians / p1; + // focal_plane_dist can be changed to alter the perspective distortion + // effect. This is in units of cm. This seems to look good most of the + // time. TODO let this variable be set through XML. + constexpr double focal_plane_dist = 10.0; + const double dx = 2.0 * focal_plane_dist * std::tan(0.5 * horiz_fov_radians); + const double dy = p1 / p0 * dx; + + std::pair result; + + // Generate the starting position/direction of the ray + if (orthographic_width_ == C_NONE) { // perspective projection + Direction camera_local_vec; + camera_local_vec.x = focal_plane_dist; + camera_local_vec.y = -0.5 * dx + horiz * dx / p0; + camera_local_vec.z = 0.5 * dy - vert * dy / p1; + camera_local_vec /= camera_local_vec.norm(); + + result.first = camera_position_; + result.second = camera_local_vec.rotate(camera_to_model_); + } else { // orthographic projection + + double x_pix_coord = (static_cast(horiz) - p0 / 2.0) / p0; + double y_pix_coord = (static_cast(vert) - p1 / 2.0) / p1; + + result.first = camera_position_ + + camera_y_axis() * x_pix_coord * orthographic_width_ + + camera_z_axis() * y_pix_coord * orthographic_width_; + result.second = camera_x_axis(); + } + + return result; +} + +void WireframeRayTracePlot::create_output() const +{ size_t width = pixels_[0]; size_t height = pixels_[1]; ImageData data({width, height}, not_found_); - // 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. + // 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); /* Holds all of the track segments for the current rendered line of pixels. @@ -1242,16 +1256,13 @@ void ProjectionPlot::create_output() const const int n_threads = num_threads(); const int tid = thread_num(); - GeometryState p; - p.u() = {1.0, 0.0, 0.0}; - int vert = tid; for (int iter = 0; iter <= pixels_[1] / n_threads; iter++) { - // Save bottom line of current work chunk to compare against later - // I used to have this inside the below if block, but it causes a - // spurious line to be drawn at the bottom of the image. Not sure - // why, but moving it here fixes things. + // Save bottom line of current work chunk to compare against later. This + // used to be inside the below if block, but it causes a spurious line to + // be drawn at the bottom of the image. Not sure why, but moving it here + // fixes things. if (tid == n_threads - 1) old_segments = this_line_segments[n_threads - 1]; @@ -1259,126 +1270,48 @@ void ProjectionPlot::create_output() const for (int horiz = 0; horiz < pixels_[0]; ++horiz) { - // Projection mode below decides ray starting conditions - Position init_r; - Direction init_u; - - // Generate the starting position/direction of the ray - if (orthographic_width_ == 0.0) { // perspective projection - double this_phi = - -horiz_fov_radians / 2.0 + dphi * horiz + 0.5 * dphi; - double this_mu = - -vert_fov_radians / 2.0 + dmu * vert + M_PI / 2.0 + 0.5 * dmu; - Direction camera_local_vec; - camera_local_vec.x = std::cos(this_phi) * std::sin(this_mu); - camera_local_vec.y = std::sin(this_phi) * std::sin(this_mu); - camera_local_vec.z = std::cos(this_mu); - init_u = camera_local_vec.rotate(camera_to_model); - init_r = camera_position_; - } else { // orthographic projection - init_u = looking_direction; - - double x_pix_coord = (static_cast(horiz) - p0 / 2.0) / p0; - double y_pix_coord = (static_cast(vert) - p1 / 2.0) / p0; - - init_r = camera_position_; - init_r += cam_yaxis * x_pix_coord * orthographic_width_; - init_r += cam_zaxis * y_pix_coord * orthographic_width_; - } - - // Resets internal geometry state of particle - p.init_from_r_u(init_r, init_u); - - bool hitsomething = false; - bool intersection_found = true; - int loop_counter = 0; + // RayTracePlot implements camera ray generation + std::pair ru = get_pixel_ray(horiz, vert); this_line_segments[tid][horiz].clear(); + ProjectionRay ray( + ru.first, ru.second, *this, this_line_segments[tid][horiz]); - int first_surface = - -1; // surface first passed when entering the model - bool first_inside_model = true; // false after entering the model - while (intersection_found) { - bool inside_cell = false; - - int32_t i_surface = p.surface_index(); - if (i_surface > 0 && - model::surfaces[i_surface]->geom_type() == GeometryType::DAG) { -#ifdef DAGMC - int32_t i_cell = next_cell(i_surface, - p.cell_last(p.n_coord() - 1), p.lowest_coord().universe); - inside_cell = i_cell >= 0; -#else - fatal_error( - "Not compiled for DAGMC, but somehow you have a DAGCell!"); -#endif - } else { - inside_cell = exhaustive_find_cell(p); - } - - if (inside_cell) { - - // This allows drawing wireframes with surface intersection - // edges on the model boundary for the same cell. - if (first_inside_model) { - this_line_segments[tid][horiz].emplace_back( - color_by_ == PlotColorBy::mats ? p.material() - : p.lowest_coord().cell, - 0.0, first_surface); - first_inside_model = false; - } - - hitsomething = true; - intersection_found = true; - auto dist = distance_to_boundary(p); - this_line_segments[tid][horiz].emplace_back( - color_by_ == PlotColorBy::mats ? p.material() - : p.lowest_coord().cell, - dist.distance, std::abs(dist.surface)); - - // Advance particle - for (int lev = 0; lev < p.n_coord(); ++lev) { - p.coord(lev).r += dist.distance * p.coord(lev).u; - } - p.surface() = dist.surface; - p.n_coord_last() = p.n_coord(); - p.n_coord() = dist.coord_level; - if (dist.lattice_translation[0] != 0 || - dist.lattice_translation[1] != 0 || - dist.lattice_translation[2] != 0) { - cross_lattice(p, dist); - } - - } else { - first_surface = advance_to_boundary_from_void(p); - intersection_found = - first_surface != -1; // -1 if no surface found - } - loop_counter++; - if (loop_counter > MAX_INTERSECTIONS) - fatal_error("Infinite loop in projection plot"); - } + ray.trace(); // Now color the pixel based on what we have intersected... // Loops backwards over intersections. Position current_color( not_found_.red, not_found_.green, not_found_.blue); const auto& segments = this_line_segments[tid][horiz]; - for (unsigned i = segments.size(); i-- > 0;) { + + // There must be at least two cell intersections to color, front and + // back of the cell. Maybe an infinitely thick cell could be present + // with no back, but why would you want to color that? It's easier to + // just skip that edge case and not even color it. + if (segments.size() <= 1) + continue; + + for (int i = segments.size() - 2; i >= 0; --i) { int colormap_idx = segments[i].id; RGBColor seg_color = colors_[colormap_idx]; Position seg_color_vec( seg_color.red, seg_color.green, seg_color.blue); - double mixing = std::exp(-xs_[colormap_idx] * segments[i].length); + double mixing = + std::exp(-xs_[colormap_idx] * + (segments[i + 1].length - segments[i].length)); current_color = current_color * mixing + (1.0 - mixing) * seg_color_vec; - RGBColor result; - result.red = static_cast(current_color.x); - result.green = static_cast(current_color.y); - result.blue = static_cast(current_color.z); - data(horiz, vert) = result; } + // save result converting from double-precision color coordinates to + // byte-sized + RGBColor result; + result.red = static_cast(current_color.x); + result.green = static_cast(current_color.y); + result.blue = static_cast(current_color.z); + data(horiz, vert) = result; + // Check to draw wireframe in horizontal direction. No inter-thread // comm. if (horiz > 0) { @@ -1451,9 +1384,8 @@ void ProjectionPlot::create_output() const #endif } -void ProjectionPlot::print_info() const +void RayTracePlot::print_info() const { - fmt::print("Plot Type: Projection\n"); fmt::print("Camera position: {} {} {}\n", camera_position_.x, camera_position_.y, camera_position_.z); fmt::print("Look at: {} {} {}\n", look_at_.x, look_at_.y, look_at_.z); @@ -1462,7 +1394,13 @@ void ProjectionPlot::print_info() const fmt::print("Pixels: {} {}\n", pixels_[0], pixels_[1]); } -void ProjectionPlot::set_opacities(pugi::xml_node node) +void WireframeRayTracePlot::print_info() const +{ + fmt::print("Plot Type: Wireframe ray-traced\n"); + RayTracePlot::print_info(); +} + +void WireframeRayTracePlot::set_opacities(pugi::xml_node node) { xs_.resize(colors_.size(), 1e6); // set to large value for opaque by default @@ -1492,7 +1430,7 @@ void ProjectionPlot::set_opacities(pugi::xml_node node) } } -void ProjectionPlot::set_orthographic_width(pugi::xml_node node) +void RayTracePlot::set_orthographic_width(pugi::xml_node node) { if (check_for_node(node, "orthographic_width")) { double orthographic_width = @@ -1503,7 +1441,7 @@ void ProjectionPlot::set_orthographic_width(pugi::xml_node node) } } -void ProjectionPlot::set_wireframe_thickness(pugi::xml_node node) +void WireframeRayTracePlot::set_wireframe_thickness(pugi::xml_node node) { if (check_for_node(node, "wireframe_thickness")) { int wireframe_thickness = @@ -1514,7 +1452,7 @@ void ProjectionPlot::set_wireframe_thickness(pugi::xml_node node) } } -void ProjectionPlot::set_wireframe_ids(pugi::xml_node node) +void WireframeRayTracePlot::set_wireframe_ids(pugi::xml_node node) { if (check_for_node(node, "wireframe_ids")) { wireframe_ids_ = get_node_array(node, "wireframe_ids"); @@ -1529,7 +1467,7 @@ void ProjectionPlot::set_wireframe_ids(pugi::xml_node node) std::sort(wireframe_ids_.begin(), wireframe_ids_.end()); } -void ProjectionPlot::set_pixels(pugi::xml_node node) +void RayTracePlot::set_pixels(pugi::xml_node node) { vector pxls = get_node_array(node, "pixels"); if (pxls.size() != 2) @@ -1539,19 +1477,19 @@ void ProjectionPlot::set_pixels(pugi::xml_node node) pixels_[1] = pxls[1]; } -void ProjectionPlot::set_camera_position(pugi::xml_node node) +void RayTracePlot::set_camera_position(pugi::xml_node node) { vector camera_pos = get_node_array(node, "camera_position"); if (camera_pos.size() != 3) { - fatal_error( - fmt::format("look_at element must have three floating point values")); + fatal_error(fmt::format( + "camera_position element must have three floating point values")); } camera_position_.x = camera_pos[0]; camera_position_.y = camera_pos[1]; camera_position_.z = camera_pos[2]; } -void ProjectionPlot::set_look_at(pugi::xml_node node) +void RayTracePlot::set_look_at(pugi::xml_node node) { vector look_at = get_node_array(node, "look_at"); if (look_at.size() != 3) { @@ -1562,7 +1500,7 @@ void ProjectionPlot::set_look_at(pugi::xml_node node) look_at_.z = look_at[2]; } -void ProjectionPlot::set_field_of_view(pugi::xml_node node) +void RayTracePlot::set_field_of_view(pugi::xml_node node) { // Defaults to 70 degree horizontal field of view (see .h file) if (check_for_node(node, "field_of_view")) { @@ -1576,6 +1514,352 @@ void ProjectionPlot::set_field_of_view(pugi::xml_node node) } } +SolidRayTracePlot::SolidRayTracePlot(pugi::xml_node node) : RayTracePlot(node) +{ + set_opaque_ids(node); + set_diffuse_fraction(node); + set_light_position(node); +} + +void SolidRayTracePlot::print_info() const +{ + fmt::print("Plot Type: Solid ray-traced\n"); + RayTracePlot::print_info(); +} + +void SolidRayTracePlot::create_output() const +{ + size_t width = pixels_[0]; + size_t height = pixels_[1]; + ImageData data({width, height}, not_found_); + +#pragma omp parallel for schedule(dynamic) collapse(2) + for (int horiz = 0; horiz < pixels_[0]; ++horiz) { + for (int vert = 0; vert < pixels_[1]; ++vert) { + // RayTracePlot implements camera ray generation + std::pair ru = get_pixel_ray(horiz, vert); + PhongRay ray(ru.first, ru.second, *this); + ray.trace(); + data(horiz, vert) = ray.result_color(); + } + } + +#ifdef USE_LIBPNG + output_png(path_plot(), data); +#else + output_ppm(path_plot(), data); +#endif +} + +void SolidRayTracePlot::set_opaque_ids(pugi::xml_node node) +{ + if (check_for_node(node, "opaque_ids")) { + auto opaque_ids_tmp = get_node_array(node, "opaque_ids"); + + // It is read in as actual ID values, but we have to convert to indices in + // mat/cell array + for (auto& x : opaque_ids_tmp) + x = color_by_ == PlotColorBy::mats ? model::material_map[x] + : model::cell_map[x]; + + opaque_ids_.insert(opaque_ids_tmp.begin(), opaque_ids_tmp.end()); + } +} + +void SolidRayTracePlot::set_light_position(pugi::xml_node node) +{ + if (check_for_node(node, "light_position")) { + auto light_pos_tmp = get_node_array(node, "light_position"); + + if (light_pos_tmp.size() != 3) + fatal_error("Light position must be given as 3D coordinates"); + + light_location_.x = light_pos_tmp[0]; + light_location_.y = light_pos_tmp[1]; + light_location_.z = light_pos_tmp[2]; + } else { + light_location_ = camera_position(); + } +} + +void SolidRayTracePlot::set_diffuse_fraction(pugi::xml_node node) +{ + if (check_for_node(node, "diffuse_fraction")) { + diffuse_fraction_ = std::stod(get_node_value(node, "diffuse_fraction")); + if (diffuse_fraction_ < 0.0 || diffuse_fraction_ > 1.0) { + fatal_error("Must have 0 <= diffuse fraction <= 1"); + } + } +} + +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. + + // 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); + + // 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; + 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 + // + // 1) ID (material or cell depending on color_by_) + // 2) Distance traveled by the ray through that ID + // 3) Index of the intersected surface (starting from 1) + + line_segments_.emplace_back( + plot_.color_by_ == PlottableInterface::PlotColorBy::mats + ? material() + : lowest_coord().cell, + traversal_distance_, boundary().surface_index()); +} + +void PhongRay::on_intersection() +{ + // Check if we hit an opaque material or cell + int hit_id = plot_.color_by_ == PlottableInterface::PlotColorBy::mats + ? material() + : lowest_coord().cell; + + // If we are reflected and have advanced beyond the camera, + // the ray is done. This is checked here because we should + // kill the ray even if the material is not opaque. + if (reflected_ && (r() - plot_.camera_position()).dot(u()) >= 0.0) { + stop(); + return; + } + + // Anything that's not opaque has zero impact on the plot. + if (plot_.opaque_ids_.find(hit_id) == plot_.opaque_ids_.end()) + return; + + if (!reflected_) { + // reflect the particle and set the color to be colored by + // the normal or the diffuse lighting contribution + reflected_ = true; + result_color_ = plot_.colors_[hit_id]; + Direction to_light = plot_.light_location_ - r(); + to_light /= to_light.norm(); + + // TODO + // Not sure what can cause a surface token to be invalid here, although it + // sometimes happens for a few pixels. It's very very rare, so proceed by + // coloring the pixel with the overlap color. It seems to happen only for a + // few pixels on the outer boundary of a hex lattice. + // + // We cannot detect it in the outer loop, and it only matters here, so + // that's why the error handling is a little different than for a lost + // ray. + if (surface() == 0) { + result_color_ = plot_.overlap_color_; + stop(); + return; + } + + // Get surface pointer + const auto& surf = model::surfaces.at(surface_index()); + + Direction normal = surf->normal(r_local()); + normal /= normal.norm(); + + // Need to apply translations to find the normal vector in + // the base level universe's coordinate system. + for (int lev = n_coord() - 2; lev >= 0; --lev) { + if (coord(lev + 1).rotated) { + const Cell& c {*model::cells[coord(lev).cell]}; + normal = normal.inverse_rotate(c.rotation_); + } + } + + // use the normal opposed to the ray direction + if (normal.dot(u()) > 0.0) { + normal *= -1.0; + } + + // Facing away from the light means no lighting + double dotprod = normal.dot(to_light); + dotprod = std::max(0.0, dotprod); + + double modulation = + plot_.diffuse_fraction_ + (1.0 - plot_.diffuse_fraction_) * dotprod; + result_color_ *= modulation; + + // Now point the particle to the camera. We now begin + // checking to see if it's occluded by another surface + u() = to_light; + + orig_hit_id_ = hit_id; + + // OpenMC native CSG and DAGMC surfaces have some slight differences + // in how they interpret particles that are sitting on a surface. + // I don't know exactly why, but this makes everything work beautifully. + if (surf->geom_type() == GeometryType::DAG) { + surface() = 0; + } else { + surface() = -surface(); // go to other side + } + + // Must fully restart coordinate search. Why? Not sure. + clear(); + + // Note this could likely be faster if we cached the previous + // cell we were in before the reflection. This is the easiest + // way to fully initialize all the sub-universe coordinates and + // directions though. + bool found = exhaustive_find_cell(*this); + if (!found) { + fatal_error("Lost particle after reflection."); + } + + // Must recalculate distance to boundary due to the + // direction change + compute_distance(); + + } else { + // If it's not facing the light, we color with the diffuse contribution, so + // next we check if we're going to occlude the last reflected surface. if + // so, color by the diffuse contribution instead + + if (orig_hit_id_ == -1) + fatal_error("somehow a ray got reflected but not original ID set?"); + + result_color_ = plot_.colors_[orig_hit_id_]; + result_color_ *= plot_.diffuse_fraction_; + stop(); + } +} + extern "C" int openmc_id_map(const void* plot, int32_t* data_out) { diff --git a/src/position.cpp b/src/position.cpp index 5b3613b28..0361e99a0 100644 --- a/src/position.cpp +++ b/src/position.cpp @@ -75,13 +75,6 @@ Position Position::operator-() const return {-x, -y, -z}; } -Position Position::rotate(const vector& rotation) const -{ - return {x * rotation[0] + y * rotation[1] + z * rotation[2], - x * rotation[3] + y * rotation[4] + z * rotation[5], - x * rotation[6] + y * rotation[7] + z * rotation[8]}; -} - std::ostream& operator<<(std::ostream& os, Position r) { os << "(" << r.x << ", " << r.y << ", " << r.z << ")"; diff --git a/src/surface.cpp b/src/surface.cpp index dbcaf8498..30a8d1f21 100644 --- a/src/surface.cpp +++ b/src/surface.cpp @@ -141,7 +141,7 @@ Direction Surface::reflect(Position r, Direction u, GeometryState* p) const } Direction Surface::diffuse_reflect( - Position r, Direction u, uint64_t* seed, GeometryState* p) const + Position r, Direction u, uint64_t* seed) const { // Diffuse reflect direction according to the normal. // cosine distribution diff --git a/tests/regression_tests/plot_projections/plots.xml b/tests/regression_tests/plot_projections/plots.xml index 85689da14..50d129cd9 100644 --- a/tests/regression_tests/plot_projections/plots.xml +++ b/tests/regression_tests/plot_projections/plots.xml @@ -1,7 +1,7 @@ - + 0. 0. 0. 20. 20. 20. 200 200 @@ -11,7 +11,7 @@ 70 - + 0. 0. 0. 10. 10. 0. 25 25 @@ -22,7 +22,7 @@ example1 - + 0. 0. 0. 20. 20. 20. 200 200 @@ -31,7 +31,7 @@ 2 - + 0. 0. 0. 0. 10.0 20. 200 200 @@ -39,7 +39,7 @@ example3.png - + 0. 0. 0. 10. 10. 10. 25 25 @@ -52,4 +52,39 @@ + + 0. 0. 0. + 10. 10. 10. + 200 200 + phong.png + 1 3 + + + + + + + 0. 0. 0. + 10. 10. 10. + 0.5 + 200 200 + phong_diffuse.png + 1 3 + + + + + + + 0. 0. 0. + 10. 10. 10. + 0. 10. 10. + 200 200 + phong_move_light.png + 1 3 + + + + + diff --git a/tests/regression_tests/plot_projections/results_true.dat b/tests/regression_tests/plot_projections/results_true.dat index 1a67da300..672280fdd 100644 --- a/tests/regression_tests/plot_projections/results_true.dat +++ b/tests/regression_tests/plot_projections/results_true.dat @@ -1 +1 @@ -24fb0f41ee018ea086962dbd6bcd0b536d11d4b34644bfef4f0e74f8b462fe41a84af39c7ff79046d5d7cfe209084eac54712fa0ec01038e97eb43df1abd0334 \ No newline at end of file +025804f1522eafd6e0e9566ce6b9b5603962f278de222c842fe3e06471290bb575676255bcd55e4f084bdcca4ee56d3c219827cb1ef2b5c3a90f7666986b55e9 \ No newline at end of file diff --git a/tests/regression_tests/plot_projections/test.py b/tests/regression_tests/plot_projections/test.py index cf18503f4..37a6ecf28 100644 --- a/tests/regression_tests/plot_projections/test.py +++ b/tests/regression_tests/plot_projections/test.py @@ -2,5 +2,8 @@ from tests.testing_harness import PlotTestHarness from tests.regression_tests import config def test_plot(): - harness = PlotTestHarness(('plot_1.png', 'example1.png', 'example2.png', 'example3.png', 'orthographic_example1.png')) + harness = PlotTestHarness(('plot_1.png', 'example1.png', 'example2.png', + 'example3.png', 'orthographic_example1.png', + 'phong.png', 'phong_diffuse.png', + 'phong_move_light.png')) harness.main() diff --git a/tests/unit_tests/test_plots.py b/tests/unit_tests/test_plots.py index 4efc12c93..fad574ee6 100644 --- a/tests/unit_tests/test_plots.py +++ b/tests/unit_tests/test_plots.py @@ -4,6 +4,8 @@ import openmc import openmc.examples import pytest +from openmc.plots import _SVG_COLORS + @pytest.fixture(scope='module') def myplot(): @@ -42,7 +44,7 @@ def myplot(): @pytest.fixture(scope='module') def myprojectionplot(): - plot = openmc.ProjectionPlot(name='myprojectionplot') + plot = openmc.WireframeRayTracePlot(name='myprojectionplot') plot.look_at = (0.0, 0.0, 0.0) plot.camera_position = (4.0, 3.0, 0.0) plot.pixels = (500, 500) @@ -118,6 +120,31 @@ def test_repr_proj(myprojectionplot): assert isinstance(r, str) +def test_projection_plot_roundtrip(myprojectionplot): + + elem = myprojectionplot.to_xml_element() + + xml_plot = openmc.WireframeRayTracePlot.from_xml_element(elem) + + svg_colors = _SVG_COLORS + + assert xml_plot.name == myprojectionplot.name + assert xml_plot.look_at == myprojectionplot.look_at + assert xml_plot.camera_position == myprojectionplot.camera_position + assert xml_plot.pixels == myprojectionplot.pixels + assert xml_plot.filename == myprojectionplot.filename + assert xml_plot.background == svg_colors[myprojectionplot.background] + assert xml_plot.color_by == myprojectionplot.color_by + expected_colors = {m.id: svg_colors[c] for m, c in myprojectionplot.colors.items()} + assert xml_plot.colors == expected_colors + # TODO: needs geometry information + # assert xml_plot.mask_components == myprojectionplot.mask_components + assert xml_plot.mask_background == svg_colors[myprojectionplot.mask_background] + # assert xml_plot.overlap_color == svg_colors[myprojectionplot.overlap_color] + assert xml_plot.wireframe_thickness == myprojectionplot.wireframe_thickness + assert xml_plot.level == myprojectionplot.level + + def test_from_geometry(): width = 25. s = openmc.Sphere(r=width/2, boundary_type='vacuum') @@ -182,7 +209,7 @@ def test_plots(run_in_tmpdir): plots = openmc.Plots([p1, p2]) assert len(plots) == 2 - p3 = openmc.ProjectionPlot(name='plot3') + p3 = openmc.WireframeRayTracePlot(name='plot3') plots = openmc.Plots([p1, p2, p3]) assert len(plots) == 3 @@ -223,6 +250,41 @@ def test_voxel_plot_roundtrip(): assert new_plot.color_by == plot.color_by +def test_phong_plot_roundtrip(): + plot = openmc.SolidRayTracePlot(name='my phong plot') + plot.id = 2300 + plot.filename = 'phong1' + plot.pixels = (50, 50) + plot.look_at = (11., 12., 13.) + plot.camera_position = (22., 23., 24.) + plot.diffuse_fraction = 0.5 + plot.horizontal_field_of_view = 90.0 + plot.color_by = 'material' + plot.light_position = (8., 9., 10.) + plot.opaque_domains = [6, 7, 8] + + elem = plot.to_xml_element() + + repr(plot) + + new_plot = openmc.SolidRayTracePlot.from_xml_element(elem) + + assert new_plot.name == plot.name + assert new_plot.id == plot.id + assert new_plot.filename == plot.filename + assert new_plot.pixels == plot.pixels + assert new_plot.look_at == plot.look_at + assert new_plot.camera_position == plot.camera_position + assert new_plot.diffuse_fraction == plot.diffuse_fraction + assert new_plot.horizontal_field_of_view == plot.horizontal_field_of_view + assert new_plot.color_by == plot.color_by + assert new_plot.light_position == plot.light_position + assert new_plot.opaque_domains == plot.opaque_domains + + # ensure the new object is valid to re-write to XML + new_elem = new_plot.to_xml_element() + + def test_plot_directory(run_in_tmpdir): pwr_pin = openmc.examples.pwr_pin_cell() From 3011a14a1339299526c7adb6343854c911c577ed Mon Sep 17 00:00:00 2001 From: John Tramm Date: Tue, 18 Feb 2025 17:38:59 -0600 Subject: [PATCH 261/671] Random Ray Explicit Void Treatment (#3299) Co-authored-by: Paul Romano --- include/openmc/random_ray/random_ray.h | 2 + include/openmc/random_ray/source_region.h | 76 ++++-- src/random_ray/flat_source_domain.cpp | 120 +++++++-- src/random_ray/linear_source_domain.cpp | 16 +- src/random_ray/random_ray.cpp | 182 ++++++++++++- src/random_ray/random_ray_simulation.cpp | 17 ++ src/random_ray/source_region.cpp | 6 + .../random_ray_void/__init__.py | 0 .../random_ray_void/flat/inputs_true.dat | 245 ++++++++++++++++++ .../random_ray_void/flat/results_true.dat | 9 + .../random_ray_void/linear/inputs_true.dat | 245 ++++++++++++++++++ .../random_ray_void/linear/results_true.dat | 9 + .../regression_tests/random_ray_void/test.py | 72 +++++ 13 files changed, 947 insertions(+), 52 deletions(-) create mode 100644 tests/regression_tests/random_ray_void/__init__.py create mode 100644 tests/regression_tests/random_ray_void/flat/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_void/flat/results_true.dat create mode 100644 tests/regression_tests/random_ray_void/linear/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_void/linear/results_true.dat create mode 100644 tests/regression_tests/random_ray_void/test.py diff --git a/include/openmc/random_ray/random_ray.h b/include/openmc/random_ray/random_ray.h index 96c38da7b..4325a6420 100644 --- a/include/openmc/random_ray/random_ray.h +++ b/include/openmc/random_ray/random_ray.h @@ -27,7 +27,9 @@ public: void event_advance_ray(); void attenuate_flux(double distance, bool is_active); void attenuate_flux_flat_source(double distance, bool is_active); + void attenuate_flux_flat_source_void(double distance, bool is_active); void attenuate_flux_linear_source(double distance, bool is_active); + void attenuate_flux_linear_source_void(double distance, bool is_active); void initialize_ray(uint64_t ray_id, FlatSourceDomain* domain); uint64_t transport_history_based_single_ray(); diff --git a/include/openmc/random_ray/source_region.h b/include/openmc/random_ray/source_region.h index 0dc617a64..e41d6b824 100644 --- a/include/openmc/random_ray/source_region.h +++ b/include/openmc/random_ray/source_region.h @@ -91,37 +91,61 @@ public: //---------------------------------------------------------------------------- // Public Data members + //--------------------------------------- // Scalar fields - int material_ {0}; + + int material_ {0}; //!< Index in openmc::model::materials array OpenMPMutex lock_; - double volume_ {0.0}; - double volume_t_ {0.0}; - double volume_naive_ {0.0}; - int position_recorded_ {0}; - int external_source_present_ {0}; - Position position_ {0.0, 0.0, 0.0}; - Position centroid_ {0.0, 0.0, 0.0}; - Position centroid_iteration_ {0.0, 0.0, 0.0}; - Position centroid_t_ {0.0, 0.0, 0.0}; - MomentMatrix mom_matrix_ {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; - MomentMatrix mom_matrix_t_ {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; + double volume_ { + 0.0}; //!< Volume (computed from the sum of ray crossing lengths) + double volume_t_ {0.0}; //!< Volume totaled over all iterations + double volume_sq_ {0.0}; //!< Volume squared + double volume_sq_t_ {0.0}; //!< Volume squared totaled over all iterations + double volume_naive_ {0.0}; //!< Volume as integrated from this iteration only + int position_recorded_ {0}; //!< Has the position been recorded yet? + int external_source_present_ { + 0}; //!< Is an external source present in this region? + Position position_ { + 0.0, 0.0, 0.0}; //!< A position somewhere inside the region + Position centroid_ {0.0, 0.0, 0.0}; //!< The centroid + Position centroid_iteration_ { + 0.0, 0.0, 0.0}; //!< The centroid integrated from this iteration only + Position centroid_t_ { + 0.0, 0.0, 0.0}; //!< The centroid accumulated over all iterations + MomentMatrix mom_matrix_ { + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; //!< The spatial moment matrix + MomentMatrix mom_matrix_t_ {0.0, 0.0, 0.0, 0.0, 0.0, + 0.0}; //!< The spatial moment matrix accumulated over all iterations + // A set of volume tally tasks. This more complicated data structure is // convenient for ensuring that volumes are only tallied once per source // region, regardless of how many energy groups are used for tallying. std::unordered_set volume_task_; + //--------------------------------------- // Energy group-wise 1D arrays - vector scalar_flux_old_; - vector scalar_flux_new_; - vector source_; - vector external_source_; - vector scalar_flux_final_; - vector source_gradients_; - vector flux_moments_old_; - vector flux_moments_new_; - vector flux_moments_t_; + vector + scalar_flux_old_; //!< The scalar flux from the previous iteration + vector + scalar_flux_new_; //!< The scalar flux from the current iteration + vector + source_; //!< The total source term (fission + scattering + external) + vector external_source_; //!< The external source term + vector scalar_flux_final_; //!< The scalar flux accumulated over all + //!< active iterations (used for plotting, + //!< or computing adjoint sources) + vector source_gradients_; //!< The linear source gradients + vector + flux_moments_old_; //!< The linear flux moments from the previous iteration + vector + flux_moments_new_; //!< The linear flux moments from the current iteration + vector + flux_moments_t_; //!< The linear flux moments accumulated over all active + //!< iterations (used for plotting) + + //--------------------------------------- // 2D array representing values for all energy groups x tally // tasks. Each group may have a different number of tally tasks // associated with it, necessitating the use of a jagged array. @@ -152,6 +176,12 @@ public: double& volume_t(int64_t sr) { return volume_t_[sr]; } const double& volume_t(int64_t sr) const { return volume_t_[sr]; } + double& volume_sq(int64_t sr) { return volume_sq_[sr]; } + const double& volume_sq(int64_t sr) const { return volume_sq_[sr]; } + + double& volume_sq_t(int64_t sr) { return volume_sq_t_[sr]; } + const double& volume_sq_t(int64_t sr) const { return volume_sq_t_[sr]; } + double& volume_naive(int64_t sr) { return volume_naive_[sr]; } const double& volume_naive(int64_t sr) const { return volume_naive_[sr]; } @@ -355,6 +385,8 @@ private: vector lock_; vector volume_; vector volume_t_; + vector volume_sq_; + vector volume_sq_t_; vector volume_naive_; vector position_recorded_; vector external_source_present_; @@ -397,4 +429,4 @@ private: } // namespace openmc -#endif // OPENMC_RANDOM_RAY_SOURCE_REGION_H \ No newline at end of file +#endif // OPENMC_RANDOM_RAY_SOURCE_REGION_H diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 098fd17b0..cfe747eec 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -94,6 +94,7 @@ void FlatSourceDomain::batch_reset() #pragma omp parallel for for (int64_t sr = 0; sr < n_source_regions_; sr++) { source_regions_.volume(sr) = 0.0; + source_regions_.volume_sq(sr) = 0.0; } #pragma omp parallel for for (int64_t se = 0; se < n_source_elements_; se++) { @@ -118,11 +119,19 @@ void FlatSourceDomain::update_neutron_source(double k_eff) double inverse_k_eff = 1.0 / k_eff; +// Reset all source regions to zero (important for void regions) +#pragma omp parallel for + for (int64_t se = 0; se < n_source_elements_; se++) { + source_regions_.source(se) = 0.0; + } + // Add scattering + fission source #pragma omp parallel for for (int64_t sr = 0; sr < n_source_regions_; sr++) { int material = source_regions_.material(sr); - + if (material == MATERIAL_VOID) { + continue; + } for (int g_out = 0; g_out < negroups_; g_out++) { double sigma_t = sigma_t_[material * negroups_ + g_out]; double scatter_source = 0.0; @@ -174,8 +183,12 @@ void FlatSourceDomain::normalize_scalar_flux_and_volumes( #pragma omp parallel for for (int64_t sr = 0; sr < n_source_regions_; sr++) { source_regions_.volume_t(sr) += source_regions_.volume(sr); + source_regions_.volume_sq_t(sr) += source_regions_.volume_sq(sr); source_regions_.volume_naive(sr) = source_regions_.volume(sr) * normalization_factor; + source_regions_.volume_sq(sr) = + (source_regions_.volume_sq_t(sr) / source_regions_.volume_t(sr)) * + volume_normalization_factor; source_regions_.volume(sr) = source_regions_.volume_t(sr) * volume_normalization_factor; } @@ -184,9 +197,17 @@ void FlatSourceDomain::normalize_scalar_flux_and_volumes( void FlatSourceDomain::set_flux_to_flux_plus_source( int64_t sr, double volume, int g) { - double sigma_t = sigma_t_[source_regions_.material(sr) * negroups_ + g]; - source_regions_.scalar_flux_new(sr, g) /= (sigma_t * volume); - source_regions_.scalar_flux_new(sr, g) += source_regions_.source(sr, g); + int material = source_regions_.material(sr); + if (material == MATERIAL_VOID) { + source_regions_.scalar_flux_new(sr, g) /= volume; + source_regions_.scalar_flux_new(sr, g) += + 0.5f * source_regions_.external_source(sr, g) * + source_regions_.volume_sq(sr); + } else { + double sigma_t = sigma_t_[source_regions_.material(sr) * negroups_ + g]; + source_regions_.scalar_flux_new(sr, g) /= (sigma_t * volume); + source_regions_.scalar_flux_new(sr, g) += source_regions_.source(sr, g); + } } void FlatSourceDomain::set_flux_to_old_flux(int64_t sr, int g) @@ -296,6 +317,9 @@ double FlatSourceDomain::compute_k_eff(double k_eff_old) const } int material = source_regions_.material(sr); + if (material == MATERIAL_VOID) { + continue; + } double sr_fission_source_old = 0; double sr_fission_source_new = 0; @@ -499,7 +523,13 @@ double FlatSourceDomain::compute_fixed_source_normalization_factor() const int material = source_regions_.material(sr); double volume = source_regions_.volume(sr) * simulation_volume_; for (int g = 0; g < negroups_; g++) { - double sigma_t = sigma_t_[material * negroups_ + g]; + // For non-void regions, we store the external source pre-divided by + // sigma_t. We need to multiply non-void regions back up by sigma_t + // to get the total source strength in the expected units. + double sigma_t = 1.0; + if (material != MATERIAL_VOID) { + sigma_t = sigma_t_[material * negroups_ + g]; + } simulation_external_source_strength += source_regions_.external_source(sr, g) * sigma_t * volume; } @@ -561,7 +591,7 @@ void FlatSourceDomain::random_ray_tally() // Determine numerical score value for (auto& task : source_regions_.tally_task(sr, g)) { - double score; + double score = 0.0; switch (task.score_type) { case SCORE_FLUX: @@ -569,15 +599,21 @@ void FlatSourceDomain::random_ray_tally() break; case SCORE_TOTAL: - score = flux * volume * sigma_t_[material * negroups_ + g]; + if (material != MATERIAL_VOID) { + score = flux * volume * sigma_t_[material * negroups_ + g]; + } break; case SCORE_FISSION: - score = flux * volume * sigma_f_[material * negroups_ + g]; + if (material != MATERIAL_VOID) { + score = flux * volume * sigma_f_[material * negroups_ + g]; + } break; case SCORE_NU_FISSION: - score = flux * volume * nu_sigma_f_[material * negroups_ + g]; + if (material != MATERIAL_VOID) { + score = flux * volume * nu_sigma_f_[material * negroups_ + g]; + } break; case SCORE_EVENTS: @@ -800,21 +836,37 @@ void FlatSourceDomain::output_to_vtk() const } // Plot fission source - std::fprintf(plot, "SCALARS total_fission_source float\n"); - std::fprintf(plot, "LOOKUP_TABLE default\n"); - for (int i = 0; i < Nx * Ny * Nz; i++) { - int64_t fsr = voxel_indices[i]; + if (settings::run_mode == RunMode::EIGENVALUE) { + std::fprintf(plot, "SCALARS total_fission_source float\n"); + std::fprintf(plot, "LOOKUP_TABLE default\n"); + for (int i = 0; i < Nx * Ny * Nz; i++) { + int64_t fsr = voxel_indices[i]; - float total_fission = 0.0; - int mat = source_regions_.material(fsr); - for (int g = 0; g < negroups_; g++) { - int64_t source_element = fsr * negroups_ + g; - float flux = evaluate_flux_at_point(voxel_positions[i], fsr, g); - double sigma_f = sigma_f_[mat * negroups_ + g]; - total_fission += sigma_f * flux; + float total_fission = 0.0; + int mat = source_regions_.material(fsr); + if (mat != MATERIAL_VOID) { + for (int g = 0; g < negroups_; g++) { + int64_t source_element = fsr * negroups_ + g; + float flux = evaluate_flux_at_point(voxel_positions[i], fsr, g); + double sigma_f = sigma_f_[mat * negroups_ + g]; + total_fission += sigma_f * flux; + } + } + total_fission = convert_to_big_endian(total_fission); + std::fwrite(&total_fission, sizeof(float), 1, plot); + } + } else { + std::fprintf(plot, "SCALARS external_source float\n"); + std::fprintf(plot, "LOOKUP_TABLE default\n"); + for (int i = 0; i < Nx * Ny * Nz; i++) { + int64_t fsr = voxel_indices[i]; + float total_external = 0.0f; + for (int g = 0; g < negroups_; g++) { + total_external += source_regions_.external_source(fsr, g); + } + total_external = convert_to_big_endian(total_external); + std::fwrite(&total_external, sizeof(float), 1, plot); } - total_fission = convert_to_big_endian(total_fission); - std::fwrite(&total_fission, sizeof(float), 1, plot); } std::fclose(plot); @@ -847,7 +899,12 @@ void FlatSourceDomain::apply_external_source_to_cell_instances(int32_t i_cell, for (int j : instances) { int cell_material_idx = cell.material(j); - int cell_material_id = model::materials[cell_material_idx]->id(); + int cell_material_id; + if (cell_material_idx == MATERIAL_VOID) { + cell_material_id = MATERIAL_VOID; + } else { + cell_material_id = model::materials[cell_material_idx]->id(); + } if (target_material_id == C_NONE || cell_material_id == target_material_id) { int64_t source_region = source_region_offsets_[i_cell] + j; @@ -930,8 +987,12 @@ void FlatSourceDomain::convert_external_sources() // iteration) #pragma omp parallel for for (int64_t sr = 0; sr < n_source_regions_; sr++) { + int material = source_regions_.material(sr); + if (material == MATERIAL_VOID) { + continue; + } for (int g = 0; g < negroups_; g++) { - double sigma_t = sigma_t_[source_regions_.material(sr) * negroups_ + g]; + double sigma_t = sigma_t_[material * negroups_ + g]; source_regions_.external_source(sr, g) /= sigma_t; } } @@ -994,8 +1055,11 @@ void FlatSourceDomain::set_adjoint_sources(const vector& forward_flux) // The forward flux is given in terms of total for the forward simulation // so we must convert it to a "per batch" quantity #pragma omp parallel for - for (int64_t se = 0; se < n_source_elements_; se++) { - source_regions_.external_source(se) = 1.0 / forward_flux[se]; + for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int g = 0; g < negroups_; g++) { + source_regions_.external_source(sr, g) = + 1.0 / forward_flux[sr * negroups_ + g]; + } } // Divide the fixed source term by sigma t (to save time when applying each @@ -1003,6 +1067,10 @@ void FlatSourceDomain::set_adjoint_sources(const vector& forward_flux) #pragma omp parallel for for (int64_t sr = 0; sr < n_source_regions_; sr++) { for (int g = 0; g < negroups_; g++) { + int material = source_regions_.material(sr); + if (material == MATERIAL_VOID) { + continue; + } double sigma_t = sigma_t_[source_regions_.material(sr) * negroups_ + g]; source_regions_.external_source(sr, g) /= sigma_t; } diff --git a/src/random_ray/linear_source_domain.cpp b/src/random_ray/linear_source_domain.cpp index fb6502a3e..75d078d1b 100644 --- a/src/random_ray/linear_source_domain.cpp +++ b/src/random_ray/linear_source_domain.cpp @@ -43,6 +43,9 @@ void LinearSourceDomain::update_neutron_source(double k_eff) #pragma omp parallel for for (int64_t sr = 0; sr < n_source_regions_; sr++) { int material = source_regions_.material(sr); + if (material == MATERIAL_VOID) { + continue; + } MomentMatrix invM = source_regions_.mom_matrix(sr).inverse(); for (int g_out = 0; g_out < negroups_; g_out++) { @@ -118,10 +121,14 @@ void LinearSourceDomain::normalize_scalar_flux_and_volumes( source_regions_.centroid_t(sr) += source_regions_.centroid_iteration(sr); source_regions_.mom_matrix_t(sr) += source_regions_.mom_matrix(sr); source_regions_.volume_t(sr) += source_regions_.volume(sr); + source_regions_.volume_sq_t(sr) += source_regions_.volume_sq(sr); source_regions_.volume_naive(sr) = source_regions_.volume(sr) * normalization_factor; source_regions_.volume(sr) = source_regions_.volume_t(sr) * volume_normalization_factor; + source_regions_.volume_sq(sr) = + (source_regions_.volume_sq_t(sr) / source_regions_.volume_t(sr)) * + volume_normalization_factor; if (source_regions_.volume_t(sr) > 0.0) { double inv_volume = 1.0 / source_regions_.volume_t(sr); source_regions_.centroid(sr) = source_regions_.centroid_t(sr); @@ -135,8 +142,13 @@ void LinearSourceDomain::normalize_scalar_flux_and_volumes( void LinearSourceDomain::set_flux_to_flux_plus_source( int64_t sr, double volume, int g) { - source_regions_.scalar_flux_new(sr, g) /= volume; - source_regions_.scalar_flux_new(sr, g) += source_regions_.source(sr, g); + int material = source_regions_.material(sr); + if (material == MATERIAL_VOID) { + FlatSourceDomain::set_flux_to_flux_plus_source(sr, volume, g); + } else { + source_regions_.scalar_flux_new(sr, g) /= volume; + source_regions_.scalar_flux_new(sr, g) += source_regions_.source(sr, g); + } source_regions_.flux_moments_new(sr, g) *= (1.0 / volume); } diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp index 9c2a890ec..a8a827775 100644 --- a/src/random_ray/random_ray.cpp +++ b/src/random_ray/random_ray.cpp @@ -274,13 +274,22 @@ void RandomRay::event_advance_ray() void RandomRay::attenuate_flux(double distance, bool is_active) { + switch (source_shape_) { case RandomRaySourceShape::FLAT: - attenuate_flux_flat_source(distance, is_active); + if (this->material() == MATERIAL_VOID) { + attenuate_flux_flat_source_void(distance, is_active); + } else { + attenuate_flux_flat_source(distance, is_active); + } break; case RandomRaySourceShape::LINEAR: case RandomRaySourceShape::LINEAR_XY: - attenuate_flux_linear_source(distance, is_active); + if (this->material() == MATERIAL_VOID) { + attenuate_flux_linear_source_void(distance, is_active); + } else { + attenuate_flux_linear_source(distance, is_active); + } break; default: fatal_error("Unknown source shape for random ray transport."); @@ -355,6 +364,56 @@ void RandomRay::attenuate_flux_flat_source(double distance, bool is_active) } } +// Alternative flux attenuation function for true void regions. +void RandomRay::attenuate_flux_flat_source_void(double distance, bool is_active) +{ + // The number of geometric intersections is counted for reporting purposes + n_event()++; + + // Determine source region index etc. + int i_cell = lowest_coord().cell; + + // The source region is the spatial region index + int64_t sr = domain_->source_region_offsets_[i_cell] + cell_instance(); + + // If ray is in the active phase (not in dead zone), make contributions to + // source region bookkeeping + if (is_active) { + + // Aquire lock for source region + domain_->source_regions_.lock(sr).lock(); + + // Accumulate delta psi into new estimate of source region flux for + // this iteration + for (int g = 0; g < negroups_; g++) { + domain_->source_regions_.scalar_flux_new(sr, g) += + angular_flux_[g] * distance; + } + + // Accomulate volume (ray distance) into this iteration's estimate + // of the source region's volume + domain_->source_regions_.volume(sr) += distance; + domain_->source_regions_.volume_sq(sr) += distance * distance; + + // Tally valid position inside the source region (e.g., midpoint of + // the ray) if not done already + if (!domain_->source_regions_.position_recorded(sr)) { + Position midpoint = r() + u() * (distance / 2.0); + domain_->source_regions_.position(sr) = midpoint; + domain_->source_regions_.position_recorded(sr) = 1; + } + + // Release lock + domain_->source_regions_.lock(sr).unlock(); + } + + // Add source to incoming angular flux, assuming void region + for (int g = 0; g < negroups_; g++) { + angular_flux_[g] += + domain_->source_regions_.external_source(sr, g) * distance; + } +} + void RandomRay::attenuate_flux_linear_source(double distance, bool is_active) { // Cast domain to LinearSourceDomain @@ -492,6 +551,125 @@ void RandomRay::attenuate_flux_linear_source(double distance, bool is_active) } } +// If traveling through a void region, the source term is either zero +// or an external source. As all external sources are currently assumed +// to be flat, we don't really need this function and could instead just call +// the "attenuate_flux_flat_source_void" function and get the same numerical and +// tally results. However, computation of the flux moments in void regions is +// nonetheless useful as this information is still used by the plotter when +// estimating the flux at specific pixel coordinates. Thus, plots will look +// nicer/more accurate if we record flux moments, so this function is useful. +void RandomRay::attenuate_flux_linear_source_void( + double distance, bool is_active) +{ + // Cast domain to LinearSourceDomain + LinearSourceDomain* domain = dynamic_cast(domain_); + if (!domain) { + fatal_error("RandomRay::attenuate_flux_linear_source() called with " + "non-LinearSourceDomain domain."); + } + + // The number of geometric intersections is counted for reporting purposes + n_event()++; + + // Determine source region index etc. + int i_cell = lowest_coord().cell; + + // The source region is the spatial region index + int64_t sr = domain_->source_region_offsets_[i_cell] + cell_instance(); + + Position& centroid = domain_->source_regions_.centroid(sr); + Position midpoint = r() + u() * (distance / 2.0); + + // Determine the local position of the midpoint and the ray origin + // relative to the source region's centroid + Position rm_local; + Position r0_local; + + // In the first few iterations of the simulation, the source region + // may not yet have had any ray crossings, in which case there will + // be no estimate of its centroid. We detect this by checking if it has + // any accumulated volume. If its volume is zero, just use the midpoint + // of the ray as the region's centroid. + if (domain_->source_regions_.volume_t(sr)) { + rm_local = midpoint - centroid; + r0_local = r() - centroid; + } else { + rm_local = {0.0, 0.0, 0.0}; + r0_local = -u() * 0.5 * distance; + } + double distance_2 = distance * distance; + + // Compared to linear flux attenuation through solid regions, + // transport through a void region is greatly simplified. Here we + // compute the updated flux moments. + for (int g = 0; g < negroups_; g++) { + float spatial_source = domain_->source_regions_.source(sr, g); + float new_delta_psi = (angular_flux_[g] - spatial_source) * distance; + float h1 = 0.5f; + h1 = h1 * angular_flux_[g]; + h1 = h1 * distance_2; + spatial_source = spatial_source * distance + new_delta_psi; + + // Store contributions for this group into arrays, so that they can + // be accumulated into the source region's estimates inside of the locked + // region. + delta_moments_[g] = r0_local * spatial_source + u() * h1; + + // If 2D mode is enabled, the z-component of the flux moments is forced + // to zero + if (source_shape_ == RandomRaySourceShape::LINEAR_XY) { + delta_moments_[g].z = 0.0; + } + } + + // If ray is in the active phase (not in dead zone), make contributions to + // source region bookkeeping + if (is_active) { + // Compute an estimate of the spatial moments matrix for the source + // region based on parameters from this ray's crossing + MomentMatrix moment_matrix_estimate; + moment_matrix_estimate.compute_spatial_moments_matrix( + rm_local, u(), distance); + + // Aquire lock for source region + domain_->source_regions_.lock(sr).lock(); + + // Accumulate delta psi into new estimate of source region flux for + // this iteration, and update flux momements + for (int g = 0; g < negroups_; g++) { + domain_->source_regions_.scalar_flux_new(sr, g) += + angular_flux_[g] * distance; + domain_->source_regions_.flux_moments_new(sr, g) += delta_moments_[g]; + } + + // Accumulate the volume (ray segment distance), centroid, and spatial + // momement estimates into the running totals for the iteration for this + // source region. The centroid and spatial momements estimates are scaled by + // the ray segment length as part of length averaging of the estimates. + domain_->source_regions_.volume(sr) += distance; + domain_->source_regions_.centroid_iteration(sr) += midpoint * distance; + moment_matrix_estimate *= distance; + domain_->source_regions_.mom_matrix(sr) += moment_matrix_estimate; + + // Tally valid position inside the source region (e.g., midpoint of + // the ray) if not done already + if (!domain_->source_regions_.position_recorded(sr)) { + domain_->source_regions_.position(sr) = midpoint; + domain_->source_regions_.position_recorded(sr) = 1; + } + + // Release lock + domain_->source_regions_.lock(sr).unlock(); + } + + // Add source to incoming angular flux, assuming void region + for (int g = 0; g < negroups_; g++) { + angular_flux_[g] += + domain_->source_regions_.external_source(sr, g) * distance; + } +} + void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain) { domain_ = domain; diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index c4f719b23..f1477c3fe 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -194,6 +194,23 @@ void validate_random_ray_inputs() fatal_error("Non-isothermal MGXS detected. Only isothermal XS data sets " "supported in random ray mode."); } + for (int g = 0; g < data::mg.num_energy_groups_; g++) { + if (material.exists_in_model) { + // Temperature and angle indices, if using multiple temperature + // data sets and/or anisotropic data sets. + // TODO: Currently assumes we are only using single temp/single angle + // data. + const int t = 0; + const int a = 0; + double sigma_t = + material.get_xs(MgxsType::TOTAL, g, NULL, NULL, NULL, t, a); + if (sigma_t <= 0.0) { + fatal_error("No zero or negative total macroscopic cross sections " + "allowed in random ray mode. If the intention is to make " + "a void material, use a cell fill of 'None' instead."); + } + } + } } // Validate ray source diff --git a/src/random_ray/source_region.cpp b/src/random_ray/source_region.cpp index 0a20af8a9..4ac86ea8c 100644 --- a/src/random_ray/source_region.cpp +++ b/src/random_ray/source_region.cpp @@ -46,6 +46,8 @@ void SourceRegionContainer::push_back(const SourceRegion& sr) lock_.push_back(sr.lock_); volume_.push_back(sr.volume_); volume_t_.push_back(sr.volume_t_); + volume_sq_.push_back(sr.volume_sq_); + volume_sq_t_.push_back(sr.volume_sq_t_); volume_naive_.push_back(sr.volume_naive_); position_recorded_.push_back(sr.position_recorded_); external_source_present_.push_back(sr.external_source_present_); @@ -93,6 +95,8 @@ void SourceRegionContainer::assign( lock_.clear(); volume_.clear(); volume_t_.clear(); + volume_sq_.clear(); + volume_sq_t_.clear(); volume_naive_.clear(); position_recorded_.clear(); external_source_present_.clear(); @@ -205,6 +209,8 @@ void SourceRegionContainer::mpi_sync_ranks(bool reduce_position) // next iteration. MPI_Allreduce(MPI_IN_PLACE, volume_.data(), n_source_regions_, MPI_DOUBLE, MPI_SUM, mpi::intracomm); + MPI_Allreduce(MPI_IN_PLACE, volume_sq_.data(), n_source_regions_, MPI_DOUBLE, + MPI_SUM, mpi::intracomm); MPI_Allreduce(MPI_IN_PLACE, scalar_flux_new_.data(), n_source_regions_ * negroups_, MPI_DOUBLE, MPI_SUM, mpi::intracomm); diff --git a/tests/regression_tests/random_ray_void/__init__.py b/tests/regression_tests/random_ray_void/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_void/flat/inputs_true.dat b/tests/regression_tests/random_ray_void/flat/inputs_true.dat new file mode 100644 index 000000000..617a66053 --- /dev/null +++ b/tests/regression_tests/random_ray_void/flat/inputs_true.dat @@ -0,0 +1,245 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 40 + 20 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + flat + + + + + 1 + + + 2 + + + 3 + + + 6 + flux + tracklength + + + 5 + flux + tracklength + + + 4 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_void/flat/results_true.dat b/tests/regression_tests/random_ray_void/flat/results_true.dat new file mode 100644 index 000000000..bf395f25f --- /dev/null +++ b/tests/regression_tests/random_ray_void/flat/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +1.760401E+00 +1.554914E-01 +tally 2: +1.056204E-01 +5.741779E-04 +tally 3: +7.286803E-03 +2.706427E-06 diff --git a/tests/regression_tests/random_ray_void/linear/inputs_true.dat b/tests/regression_tests/random_ray_void/linear/inputs_true.dat new file mode 100644 index 000000000..4b6a682d9 --- /dev/null +++ b/tests/regression_tests/random_ray_void/linear/inputs_true.dat @@ -0,0 +1,245 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 40 + 20 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + linear + + + + + 1 + + + 2 + + + 3 + + + 6 + flux + tracklength + + + 5 + flux + tracklength + + + 4 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_void/linear/results_true.dat b/tests/regression_tests/random_ray_void/linear/results_true.dat new file mode 100644 index 000000000..77fade829 --- /dev/null +++ b/tests/regression_tests/random_ray_void/linear/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +1.762592E+00 +1.558698E-01 +tally 2: +1.082935E-01 +6.029854E-04 +tally 3: +7.301975E-03 +2.717553E-06 diff --git a/tests/regression_tests/random_ray_void/test.py b/tests/regression_tests/random_ray_void/test.py new file mode 100644 index 000000000..b48a7794d --- /dev/null +++ b/tests/regression_tests/random_ray_void/test.py @@ -0,0 +1,72 @@ +import os + +import openmc +from openmc.utility_funcs import change_directory +from openmc.examples import random_ray_three_region_cube +import pytest + +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) + + +@pytest.mark.parametrize("shape", ["flat", "linear"]) +def test_random_ray_void(shape): + with change_directory(shape): + openmc.reset_auto_ids() + model = random_ray_three_region_cube() + + # There is some different logic for void depending on linear + # vs. flat, so we test both + model.settings.random_ray['source_shape'] = shape + + # As we are testing linear sources, need to have more than + # 10 inactive batches so the moments start getting computed + model.settings.inactive = 20 + model.settings.batches = 40 + + # Begin by getting handles to the cells, and setting the + # source and void areas to have no fill. We leave the absorber + # as solid. + absorber_cell = model.geometry.get_cells_by_name( + 'infinite absorber region', matching=True)[0] + void_cell = model.geometry.get_cells_by_name( + 'infinite void region', matching=True)[0] + source_cell = model.geometry.get_cells_by_name( + 'infinite source region', matching=True)[0] + + void_cell.fill = None + source_cell.fill = None + + # We also need to redefine all three tallies to use cell + # filters instead of material ones + estimator = 'tracklength' + absorber_filter = openmc.CellFilter(absorber_cell) + absorber_tally = openmc.Tally(name="Absorber Tally") + absorber_tally.filters = [absorber_filter] + absorber_tally.scores = ['flux'] + absorber_tally.estimator = estimator + + void_filter = openmc.CellFilter(void_cell) + void_tally = openmc.Tally(name="Void Tally") + void_tally.filters = [void_filter] + void_tally.scores = ['flux'] + void_tally.estimator = estimator + + source_filter = openmc.CellFilter(source_cell) + source_tally = openmc.Tally(name="Source Tally") + source_tally.filters = [source_filter] + source_tally.scores = ['flux'] + source_tally.estimator = estimator + + tallies = openmc.Tallies([source_tally, void_tally, absorber_tally]) + model.tallies = tallies + + harness = MGXSTestHarness('statepoint.40.h5', model) + harness.main() From d96e6860e627a1eb164184b153d41346785982ad Mon Sep 17 00:00:00 2001 From: Sam Pasmann <46267220+spasmann@users.noreply.github.com> Date: Tue, 18 Feb 2025 22:44:15 -0500 Subject: [PATCH 262/671] Randomized Quasi-Monte Carlo Sampling in The Random Ray Method (#3268) Co-authored-by: John Tramm --- docs/source/io_formats/settings.rst | 6 + docs/source/usersguide/random_ray.rst | 18 ++ include/openmc/constants.h | 1 + include/openmc/random_dist.h | 11 ++ include/openmc/random_ray/random_ray.h | 3 + openmc/settings.py | 8 + src/random_dist.cpp | 5 + src/random_ray/random_ray.cpp | 124 ++++++++++++- src/settings.cpp | 11 ++ .../random_ray_halton_samples/__init__.py | 0 .../random_ray_halton_samples/inputs_true.dat | 110 +++++++++++ .../results_true.dat | 171 ++++++++++++++++++ .../random_ray_halton_samples/test.py | 19 ++ 13 files changed, 480 insertions(+), 7 deletions(-) create mode 100644 tests/regression_tests/random_ray_halton_samples/__init__.py create mode 100644 tests/regression_tests/random_ray_halton_samples/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_halton_samples/results_true.dat create mode 100644 tests/regression_tests/random_ray_halton_samples/test.py diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 4da57fe2a..3ddba1d5e 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -457,6 +457,12 @@ found in the :ref:`random ray user guide `. *Default*: None + :sample_method: + Specifies the method for sampling the starting ray distribution. This + element can be set to "prng" or "halton". + + *Default*: prng + ---------------------------------- ```` Element ---------------------------------- diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index b797a7216..eff1764d4 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -299,6 +299,24 @@ acceptable ray source for a two-dimensional 2x2 lattice would look like: provide physical particle fixed sources in addition to the random ray source. +-------------------------- +Quasi-Monte Carlo Sampling +-------------------------- + +By default OpenMC will use a pseudorandom number generator (PRNG) to sample ray +starting locations from a uniform distribution in space and angle. +Alternatively, a randomized Halton sequence may be sampled from, which is a form +of Randomized Qusi-Monte Carlo (RQMC) sampling. RQMC sampling with random ray +has been shown to offer reduced variance as compared to regular PRNG sampling, +as the Halton sequence offers a more uniform distribution of sampled points. +Randomized Halton sampling can be enabled as:: + + settings.random_ray['sample_method'] = 'halton' + +Default behavior using OpenMC's native PRNG can be manually specified as:: + + settings.random_ray['sample_method'] = 'prng' + .. _subdivision_fsr: ---------------------------------- diff --git a/include/openmc/constants.h b/include/openmc/constants.h index a83a4a07d..cfcacbabf 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -344,6 +344,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 }; //============================================================================== // Geometry Constants diff --git a/include/openmc/random_dist.h b/include/openmc/random_dist.h index 0fb186edc..11e88ab8c 100644 --- a/include/openmc/random_dist.h +++ b/include/openmc/random_dist.h @@ -16,6 +16,17 @@ namespace openmc { double uniform_distribution(double a, double b, uint64_t* seed); +//============================================================================== +//! Sample an integer from uniform distribution [a, b] +// +//! \param a Lower bound of uniform distribution +//! \param b Upper bound of uniform distribtion +//! \param seed A pointer to the pseudorandom seed +//! \return Sampled variate +//============================================================================== + +int64_t uniform_int_distribution(int64_t a, int64_t b, uint64_t* seed); + //============================================================================== //! Samples an energy from the Maxwell fission distribution based on a direct //! sampling scheme. diff --git a/include/openmc/random_ray/random_ray.h b/include/openmc/random_ray/random_ray.h index 4325a6420..df1ad70bc 100644 --- a/include/openmc/random_ray/random_ray.h +++ b/include/openmc/random_ray/random_ray.h @@ -33,6 +33,8 @@ public: void initialize_ray(uint64_t ray_id, FlatSourceDomain* domain); uint64_t transport_history_based_single_ray(); + SourceSite sample_prng(); + SourceSite sample_halton(); //---------------------------------------------------------------------------- // Static data members @@ -40,6 +42,7 @@ public: static double distance_active_; // Active ray length static unique_ptr ray_source_; // Starting source for ray sampling static RandomRaySourceShape source_shape_; // Flag for linear source + static RandomRaySampleMethod sample_method_; // Flag for sampling method //---------------------------------------------------------------------------- // Public data members diff --git a/openmc/settings.py b/openmc/settings.py index b29393d9a..110d57c19 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -173,6 +173,9 @@ class Settings: :adjoint: Whether to run the random ray solver in adjoint mode (bool). The default is 'False'. + :sample_method: + Sampling method for the ray starting location and direction of travel. + Options are `prng` (default) or 'halton`. .. versionadded:: 0.15.0 resonance_scattering : dict @@ -1131,6 +1134,9 @@ class Settings: cv.check_type('volume normalized flux tallies', value, bool) elif key == 'adjoint': cv.check_type('adjoint', value, bool) + elif key == 'sample_method': + cv.check_value('sample method', value, + ('prng', 'halton')) else: raise ValueError(f'Unable to set random ray to "{key}" which is ' 'unsupported by OpenMC') @@ -1948,6 +1954,8 @@ class Settings: self.random_ray['adjoint'] = ( child.text in ('true', '1') ) + elif child.tag == 'sample_method': + self.random_ray['sample_method'] = child.text def to_xml_element(self, mesh_memo=None): """Create a 'settings' element to be written to an XML file. diff --git a/src/random_dist.cpp b/src/random_dist.cpp index 1aa35a689..b05b76f99 100644 --- a/src/random_dist.cpp +++ b/src/random_dist.cpp @@ -12,6 +12,11 @@ double uniform_distribution(double a, double b, uint64_t* seed) return a + (b - a) * prn(seed); } +int64_t uniform_int_distribution(int64_t a, int64_t b, uint64_t* seed) +{ + return a + static_cast(prn(seed) * (b - a + 1)); +} + double maxwell_spectrum(double T, uint64_t* seed) { // Set the random numbers diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp index a8a827775..9917cf0c5 100644 --- a/src/random_ray/random_ray.cpp +++ b/src/random_ray/random_ray.cpp @@ -9,6 +9,9 @@ #include "openmc/search.h" #include "openmc/settings.h" #include "openmc/simulation.h" + +#include "openmc/distribution_spatial.h" +#include "openmc/random_dist.h" #include "openmc/source.h" namespace openmc { @@ -174,6 +177,57 @@ float exponentialG2(float tau) return num / den; } +// Implementation of the Fisher-Yates shuffle algorithm. +// Algorithm adapted from: +// https://en.cppreference.com/w/cpp/algorithm/random_shuffle#Version_3 +void fisher_yates_shuffle(vector& arr, uint64_t* seed) +{ + // Loop over the array from the last element down to the second + for (int i = arr.size() - 1; i > 0; --i) { + // Generate a random index in the range [0, i] + int j = uniform_int_distribution(0, i, seed); + std::swap(arr[i], arr[j]); + } +} + +// Function to generate randomized Halton sequence samples +// +// Algorithm adapted from: +// A. B. Owen. A randomized halton algorithm in r. Arxiv, 6 2017. +// URL https://arxiv.org/abs/1706.02808 +vector rhalton(int dim, uint64_t* seed, int64_t skip = 0) +{ + if (dim > 10) { + fatal_error("Halton sampling dimension too large"); + } + int64_t b, res, dig; + double b2r, ans; + const std::array primes = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29}; + vector halton(dim, 0.0); + + vector perm; + for (int D = 0; D < dim; ++D) { + b = primes[D]; + perm.resize(b); + b2r = 1.0 / b; + res = skip; + ans = 0.0; + + while ((1.0 - b2r) < 1.0) { + std::iota(perm.begin(), perm.end(), 0); + fisher_yates_shuffle(perm, seed); + dig = res % b; + ans += perm[dig] * b2r; + res = (res - dig) / b; + b2r /= b; + } + + halton[D] = ans; + } + + return halton; +} + //============================================================================== // RandomRay implementation //============================================================================== @@ -183,6 +237,7 @@ double RandomRay::distance_inactive_; double RandomRay::distance_active_; unique_ptr RandomRay::ray_source_; RandomRaySourceShape RandomRay::source_shape_ {RandomRaySourceShape::FLAT}; +RandomRaySampleMethod RandomRay::sample_method_ {RandomRaySampleMethod::PRNG}; RandomRay::RandomRay() : angular_flux_(data::mg.num_energy_groups_), @@ -684,14 +739,19 @@ void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain) // set identifier for particle id() = simulation::work_index[mpi::rank] + ray_id; - // 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; + // generate source site using sample method + SourceSite site; + switch (sample_method_) { + case RandomRaySampleMethod::PRNG: + site = sample_prng(); + break; + case RandomRaySampleMethod::HALTON: + site = sample_halton(); + break; + default: + fatal_error("Unknown sample method for random ray transport."); + } - // Sample from ray source distribution - SourceSite site {ray_source_->sample(current_seed())}; site.E = lower_bound_index( data::mg.rev_energy_bins_.begin(), data::mg.rev_energy_bins_.end(), site.E); site.E = negroups_ - site.E - 1.; @@ -719,4 +779,54 @@ void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain) } } +SourceSite RandomRay::sample_prng() +{ + // 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; + + // Sample from ray source distribution + SourceSite site {ray_source_->sample(current_seed())}; + + return site; +} + +SourceSite RandomRay::sample_halton() +{ + SourceSite site; + + // Set random number seed + int64_t batch_seed = (simulation::current_batch - 1) * settings::n_particles; + int64_t skip = id(); + init_particle_seeds(batch_seed, seeds()); + stream() = STREAM_TRACKING; + + // Calculate next samples in LDS across 5 dimensions + vector samples = rhalton(5, current_seed(), skip = skip); + + // Get spatial box of ray_source_ + SpatialBox* sb = dynamic_cast( + dynamic_cast(RandomRay::ray_source_.get())->space()); + + // Sample spatial distribution + Position xi {samples[0], samples[1], samples[2]}; + // make a small shift in position to avoid geometry floating point issues + Position shift {FP_COINCIDENT, FP_COINCIDENT, FP_COINCIDENT}; + site.r = (sb->lower_left() + shift) + + xi * ((sb->upper_right() - shift) - (sb->lower_left() + shift)); + + // Sample Polar cosine and azimuthal angles + double mu = 2.0 * samples[3] - 1.0; + double azi = 2.0 * PI * samples[4]; + // Convert to Cartesian coordinates + double c = std::sqrt(1.0 - mu * mu); + site.u.x = mu; + site.u.y = std::cos(azi) * c; + site.u.z = std::sin(azi) * c; + + return site; +} + } // namespace openmc diff --git a/src/settings.cpp b/src/settings.cpp index 61eda7996..60263c846 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -306,6 +306,17 @@ void get_run_parameters(pugi::xml_node node_base) FlatSourceDomain::adjoint_ = get_node_value_bool(random_ray_node, "adjoint"); } + if (check_for_node(random_ray_node, "sample_method")) { + std::string temp_str = + get_node_value(random_ray_node, "sample_method", true, true); + if (temp_str == "prng") { + RandomRay::sample_method_ = RandomRaySampleMethod::PRNG; + } else if (temp_str == "halton") { + RandomRay::sample_method_ = RandomRaySampleMethod::HALTON; + } else { + fatal_error("Unrecognized sample method: " + temp_str); + } + } } } diff --git a/tests/regression_tests/random_ray_halton_samples/__init__.py b/tests/regression_tests/random_ray_halton_samples/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_halton_samples/inputs_true.dat b/tests/regression_tests/random_ray_halton_samples/inputs_true.dat new file mode 100644 index 000000000..624ab495f --- /dev/null +++ b/tests/regression_tests/random_ray_halton_samples/inputs_true.dat @@ -0,0 +1,110 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.126 0.126 + 10 10 + -0.63 -0.63 + +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 + + + 1.26 1.26 + 2 2 + -1.26 -1.26 + +2 2 +2 5 + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + multi-group + + 100.0 + 20.0 + + + -1.26 -1.26 -1 1.26 1.26 1 + + + True + halton + + + + + 2 2 + -1.26 -1.26 + 1.26 1.26 + + + 1 + + + 1e-05 0.0635 10.0 100.0 1000.0 500000.0 1000000.0 20000000.0 + + + 1 2 + flux fission nu-fission + analog + + + diff --git a/tests/regression_tests/random_ray_halton_samples/results_true.dat b/tests/regression_tests/random_ray_halton_samples/results_true.dat new file mode 100644 index 000000000..7eb307da0 --- /dev/null +++ b/tests/regression_tests/random_ray_halton_samples/results_true.dat @@ -0,0 +1,171 @@ +k-combined: +8.388051E-01 7.383265E-03 +tally 1: +5.033308E+00 +5.072162E+00 +1.917335E+00 +7.360725E-01 +4.666410E+00 +4.360038E+00 +2.851812E+00 +1.629362E+00 +4.365590E-01 +3.818884E-02 +1.062497E+00 +2.262071E-01 +1.697621E+00 +5.829333E-01 +5.639912E-02 +6.427568E-04 +1.372642E-01 +3.807294E-03 +2.376683E+00 +1.151027E+00 +8.060902E-02 +1.323179E-03 +1.961862E-01 +7.837693E-03 +7.145452E+00 +1.037540E+01 +8.551803E-02 +1.486269E-03 +2.081363E-01 +8.803955E-03 +2.053205E+01 +8.469498E+01 +3.235618E-02 +2.102891E-04 +8.006311E-02 +1.287559E-03 +1.326545E+01 +3.519484E+01 +1.867471E-01 +6.975133E-03 +5.194275E-01 +5.396284E-02 +7.558115E+00 +1.142535E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.386211E+00 +2.294414E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.827274E+00 +6.782305E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.702858E+00 +1.489752E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +7.475537E+00 +1.133971E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.828685E+01 +6.719606E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.143600E+01 +2.615734E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.590713E+00 +4.224107E+00 +1.705847E+00 +5.831967E-01 +4.151691E+00 +3.454497E+00 +2.730853E+00 +1.495413E+00 +4.072633E-01 +3.325944E-02 +9.911973E-01 +1.970084E-01 +1.664732E+00 +5.598668E-01 +5.385645E-02 +5.858353E-04 +1.310758E-01 +3.470126E-03 +2.312239E+00 +1.088485E+00 +7.662778E-02 +1.195422E-03 +1.864967E-01 +7.080943E-03 +7.105765E+00 +1.025960E+01 +8.287512E-02 +1.396474E-03 +2.017039E-01 +8.272053E-03 +2.099024E+01 +8.854251E+01 +3.191885E-02 +2.048368E-04 +7.898095E-02 +1.254175E-03 +1.355820E+01 +3.676862E+01 +1.815102E-01 +6.590727E-03 +5.048614E-01 +5.098890E-02 +5.093659E+00 +5.192360E+00 +1.874632E+00 +7.031793E-01 +4.562478E+00 +4.165199E+00 +2.870213E+00 +1.650126E+00 +4.244068E-01 +3.608745E-02 +1.032921E+00 +2.137598E-01 +1.703400E+00 +5.873029E-01 +5.464557E-02 +6.042855E-04 +1.329964E-01 +3.579413E-03 +2.389118E+00 +1.163674E+00 +7.832237E-02 +1.251254E-03 +1.906210E-01 +7.411654E-03 +7.162706E+00 +1.042515E+01 +8.273831E-02 +1.391799E-03 +2.013709E-01 +8.244359E-03 +2.043145E+01 +8.383557E+01 +3.096158E-02 +1.924116E-04 +7.661226E-02 +1.178098E-03 +1.314148E+01 +3.454143E+01 +1.771732E-01 +6.279887E-03 +4.927984E-01 +4.858410E-02 diff --git a/tests/regression_tests/random_ray_halton_samples/test.py b/tests/regression_tests/random_ray_halton_samples/test.py new file mode 100644 index 000000000..478b65026 --- /dev/null +++ b/tests/regression_tests/random_ray_halton_samples/test.py @@ -0,0 +1,19 @@ +import os + +from openmc.examples import random_ray_lattice + +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_halton_samples(): + model = random_ray_lattice() + model.settings.random_ray['sample_method'] = 'halton' + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() From 06a88526cfb75786bf69eb453bd6dda01ffad864 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 19 Feb 2025 07:48:39 -0600 Subject: [PATCH 263/671] Relax requirement on polar/azimuthal axis for wwinp conversion (#3307) --- openmc/weight_windows.py | 16 +++++++++++++--- .../regression_tests/weightwindows/ww_n_cyl.txt | 2 +- .../regression_tests/weightwindows/ww_n_sph.txt | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/openmc/weight_windows.py b/openmc/weight_windows.py index 0502d309f..d52b81925 100644 --- a/openmc/weight_windows.py +++ b/openmc/weight_windows.py @@ -513,9 +513,19 @@ def wwinp_to_wws(path: PathLike) -> list[WeightWindows]: line_arr = np.fromstring(wwinp.readline(), sep=' ') xyz2 = line_arr[:3] - # oriented polar and azimuthal vectors aren't yet supported - if np.count_nonzero(xyz1) or np.count_nonzero(xyz2): - raise NotImplementedError('Custom sphere/cylinder orientations are not supported') + # Get polar and azimuthal axes + polar_axis = xyz1 - xyz0 + azimuthal_axis = xyz2 - xyz0 + + # Check for polar axis other than (0, 0, 1) + norm = np.linalg.norm(polar_axis) + if not np.isclose(polar_axis[2]/norm, 1.0): + raise NotImplementedError('Polar axis not aligned to z-axis not supported') + + # Check for azimuthal axis other than (1, 0, 0) + norm = np.linalg.norm(azimuthal_axis) + if not np.isclose(azimuthal_axis[0]/norm, 1.0): + raise NotImplementedError('Azimuthal axis not aligned to x-axis not supported') # read geometry type nwg = int(line_arr[-1]) diff --git a/tests/regression_tests/weightwindows/ww_n_cyl.txt b/tests/regression_tests/weightwindows/ww_n_cyl.txt index 89232628f..6f6144ca3 100644 --- a/tests/regression_tests/weightwindows/ww_n_cyl.txt +++ b/tests/regression_tests/weightwindows/ww_n_cyl.txt @@ -2,7 +2,7 @@ 1 8.0000 7.0000 8.0000 0.0000 0.0000 -9.0001 2.0000 2.0000 4.0000 0.0000 0.0000 0.0000 -0.0000 0.0000 0.0000 2.0000 +1.0000 0.0000 -9.0001 2.0000 0.0000 3.0000 3.0200 1.0000 5.0000 6.0001 1.0000 0.0000 4.0000 8.0080 1.0000 3.0000 14.002 diff --git a/tests/regression_tests/weightwindows/ww_n_sph.txt b/tests/regression_tests/weightwindows/ww_n_sph.txt index 3f9b8dab6..a4418ceeb 100644 --- a/tests/regression_tests/weightwindows/ww_n_sph.txt +++ b/tests/regression_tests/weightwindows/ww_n_sph.txt @@ -2,7 +2,7 @@ 1 8.0000 7.0000 8.0000 0.0000 0.0000 -9.0001 2.0000 4.0000 4.0000 0.0000 0.0000 0.0000 -0.0000 0.0000 0.0000 3.0000 +1.0000 0.0000 -9.0001 3.0000 0.0000 3.0000 3.0200 1.0000 5.0000 6.0001 1.0000 0.0000 2.0000 0.25000 1.0000 1.0000 0.50000 From bcc2a4c5f0260d010ae7f5989c453e7799d7378b Mon Sep 17 00:00:00 2001 From: Gavin Ridley Date: Wed, 19 Feb 2025 15:58:14 -0600 Subject: [PATCH 264/671] simplify mechanism to detect if geometry entity is DAG (#3269) --- include/openmc/cell.h | 12 ++++------ include/openmc/dagmc.h | 6 +++++ include/openmc/surface.h | 50 ++++++++++++++++++--------------------- include/openmc/universe.h | 10 ++++---- src/cell.cpp | 8 ------- src/dagmc.cpp | 11 ++------- src/surface.cpp | 39 ++++++++++++------------------ 7 files changed, 56 insertions(+), 80 deletions(-) diff --git a/include/openmc/cell.h b/include/openmc/cell.h index d7a3cad18..05abbfdfa 100644 --- a/include/openmc/cell.h +++ b/include/openmc/cell.h @@ -349,12 +349,8 @@ public: vector offset_; //!< Distribcell offset table - // Accessors - const GeometryType& geom_type() const { return geom_type_; } - GeometryType& geom_type() { return geom_type_; } - -private: - GeometryType geom_type_; //!< Geometric representation type (CSG, DAGMC) + // Right now, either CSG or DAGMC cells are used. + virtual GeometryType geom_type() const = 0; }; struct CellInstanceItem { @@ -368,7 +364,7 @@ class CSGCell : public Cell { public: //---------------------------------------------------------------------------- // Constructors - CSGCell(); + CSGCell() = default; explicit CSGCell(pugi::xml_node cell_node); //---------------------------------------------------------------------------- @@ -395,6 +391,8 @@ public: bool is_simple() const override { return region_.is_simple(); } + virtual GeometryType geom_type() const override { return GeometryType::CSG; } + protected: //! Returns the beginning position of a parenthesis block (immediately before //! two surface tokens) in the RPN given a starting position at the end of diff --git a/include/openmc/dagmc.h b/include/openmc/dagmc.h index 9c4e47fdf..82ef1b644 100644 --- a/include/openmc/dagmc.h +++ b/include/openmc/dagmc.h @@ -54,6 +54,8 @@ public: inline void to_hdf5_inner(hid_t group_id) const override {}; + virtual GeometryType geom_type() const override { return GeometryType::DAG; } + // Accessor methods moab::DagMC* dagmc_ptr() const { return dagmc_ptr_.get(); } int32_t dag_index() const { return dag_index_; } @@ -78,6 +80,8 @@ public: void to_hdf5_inner(hid_t group_id) const override; + virtual GeometryType geom_type() const override { return GeometryType::DAG; } + // Accessor methods moab::DagMC* dagmc_ptr() const { return dagmc_ptr_.get(); } int32_t dag_index() const { return dag_index_; } @@ -165,6 +169,8 @@ public: void to_hdf5(hid_t universes_group) const override; + virtual GeometryType geom_type() const override { return GeometryType::DAG; } + // Data Members std::shared_ptr dagmc_instance_; //!< DAGMC Instance for this universe diff --git a/include/openmc/surface.h b/include/openmc/surface.h index 2397a64cc..549e8f6ab 100644 --- a/include/openmc/surface.h +++ b/include/openmc/surface.h @@ -90,30 +90,26 @@ public: //! Get the BoundingBox for this surface. virtual BoundingBox bounding_box(bool /*pos_side*/) const { return {}; } - // Accessors - const GeometryType& geom_type() const { return geom_type_; } - GeometryType& geom_type() { return geom_type_; } - -private: - GeometryType geom_type_; //!< Geometry type indicator (CSG or DAGMC) + /* Must specify if this is a CSG or DAGMC-type surface. Only + * the DAGMC surface should return the DAG type geometry, so + * by default, this returns the CSG. The main difference is that + * if the geom_type is found to be DAG in the geometry handling code, + * some DAGMC-specific operations get carried out like resetting + * the particle's intersection history when necessary. + */ + virtual GeometryType geom_type() const { return GeometryType::CSG; } protected: virtual void to_hdf5_inner(hid_t group_id) const = 0; }; -class CSGSurface : public Surface { -public: - explicit CSGSurface(pugi::xml_node surf_node); - CSGSurface(); -}; - //============================================================================== //! A plane perpendicular to the x-axis. // //! The plane is described by the equation \f$x - x_0 = 0\f$ //============================================================================== -class SurfaceXPlane : public CSGSurface { +class SurfaceXPlane : public Surface { public: explicit SurfaceXPlane(pugi::xml_node surf_node); double evaluate(Position r) const override; @@ -131,7 +127,7 @@ public: //! The plane is described by the equation \f$y - y_0 = 0\f$ //============================================================================== -class SurfaceYPlane : public CSGSurface { +class SurfaceYPlane : public Surface { public: explicit SurfaceYPlane(pugi::xml_node surf_node); double evaluate(Position r) const override; @@ -149,7 +145,7 @@ public: //! The plane is described by the equation \f$z - z_0 = 0\f$ //============================================================================== -class SurfaceZPlane : public CSGSurface { +class SurfaceZPlane : public Surface { public: explicit SurfaceZPlane(pugi::xml_node surf_node); double evaluate(Position r) const override; @@ -167,7 +163,7 @@ public: //! The plane is described by the equation \f$A x + B y + C z - D = 0\f$ //============================================================================== -class SurfacePlane : public CSGSurface { +class SurfacePlane : public Surface { public: explicit SurfacePlane(pugi::xml_node surf_node); double evaluate(Position r) const override; @@ -185,7 +181,7 @@ public: //! \f$(y - y_0)^2 + (z - z_0)^2 - R^2 = 0\f$ //============================================================================== -class SurfaceXCylinder : public CSGSurface { +class SurfaceXCylinder : public Surface { public: explicit SurfaceXCylinder(pugi::xml_node surf_node); double evaluate(Position r) const override; @@ -204,7 +200,7 @@ public: //! \f$(x - x_0)^2 + (z - z_0)^2 - R^2 = 0\f$ //============================================================================== -class SurfaceYCylinder : public CSGSurface { +class SurfaceYCylinder : public Surface { public: explicit SurfaceYCylinder(pugi::xml_node surf_node); double evaluate(Position r) const override; @@ -223,7 +219,7 @@ public: //! \f$(x - x_0)^2 + (y - y_0)^2 - R^2 = 0\f$ //============================================================================== -class SurfaceZCylinder : public CSGSurface { +class SurfaceZCylinder : public Surface { public: explicit SurfaceZCylinder(pugi::xml_node surf_node); double evaluate(Position r) const override; @@ -242,7 +238,7 @@ public: //! \f$(x - x_0)^2 + (y - y_0)^2 + (z - z_0)^2 - R^2 = 0\f$ //============================================================================== -class SurfaceSphere : public CSGSurface { +class SurfaceSphere : public Surface { public: explicit SurfaceSphere(pugi::xml_node surf_node); double evaluate(Position r) const override; @@ -261,7 +257,7 @@ public: //! \f$(y - y_0)^2 + (z - z_0)^2 - R^2 (x - x_0)^2 = 0\f$ //============================================================================== -class SurfaceXCone : public CSGSurface { +class SurfaceXCone : public Surface { public: explicit SurfaceXCone(pugi::xml_node surf_node); double evaluate(Position r) const override; @@ -279,7 +275,7 @@ public: //! \f$(x - x_0)^2 + (z - z_0)^2 - R^2 (y - y_0)^2 = 0\f$ //============================================================================== -class SurfaceYCone : public CSGSurface { +class SurfaceYCone : public Surface { public: explicit SurfaceYCone(pugi::xml_node surf_node); double evaluate(Position r) const override; @@ -297,7 +293,7 @@ public: //! \f$(x - x_0)^2 + (y - y_0)^2 - R^2 (z - z_0)^2 = 0\f$ //============================================================================== -class SurfaceZCone : public CSGSurface { +class SurfaceZCone : public Surface { public: explicit SurfaceZCone(pugi::xml_node surf_node); double evaluate(Position r) const override; @@ -315,7 +311,7 @@ public: //! 0\f$ //============================================================================== -class SurfaceQuadric : public CSGSurface { +class SurfaceQuadric : public Surface { public: explicit SurfaceQuadric(pugi::xml_node surf_node); double evaluate(Position r) const override; @@ -333,7 +329,7 @@ public: //! \f$(x-x_0)^2/B^2 + (\sqrt{(y-y_0)^2 + (z-z_0)^2} - A)^2/C^2 -1 \f$ //============================================================================== -class SurfaceXTorus : public CSGSurface { +class SurfaceXTorus : public Surface { public: explicit SurfaceXTorus(pugi::xml_node surf_node); double evaluate(Position r) const override; @@ -350,7 +346,7 @@ public: //! \f$(y-y_0)^2/B^2 + (\sqrt{(x-x_0)^2 + (z-z_0)^2} - A)^2/C^2 -1 \f$ //============================================================================== -class SurfaceYTorus : public CSGSurface { +class SurfaceYTorus : public Surface { public: explicit SurfaceYTorus(pugi::xml_node surf_node); double evaluate(Position r) const override; @@ -367,7 +363,7 @@ public: //! \f$(z-z_0)^2/B^2 + (\sqrt{(x-x_0)^2 + (y-y_0)^2} - A)^2/C^2 -1 \f$ //============================================================================== -class SurfaceZTorus : public CSGSurface { +class SurfaceZTorus : public Surface { public: explicit SurfaceZTorus(pugi::xml_node surf_node); double evaluate(Position r) const override; diff --git a/include/openmc/universe.h b/include/openmc/universe.h index 9fea06bcc..e8fbacfdc 100644 --- a/include/openmc/universe.h +++ b/include/openmc/universe.h @@ -38,13 +38,13 @@ public: BoundingBox bounding_box() const; - const GeometryType& geom_type() const { return geom_type_; } - GeometryType& geom_type() { return geom_type_; } + /* By default, universes are CSG universes. The DAGMC + * universe overrides standard behaviors, and in the future, + * other things might too. + */ + virtual GeometryType geom_type() const { return GeometryType::CSG; } unique_ptr partitioner_; - -private: - GeometryType geom_type_ = GeometryType::CSG; }; //============================================================================== diff --git a/src/cell.cpp b/src/cell.cpp index d4d28fb70..fa8be4496 100644 --- a/src/cell.cpp +++ b/src/cell.cpp @@ -249,16 +249,8 @@ void Cell::to_hdf5(hid_t cell_group) const // CSGCell implementation //============================================================================== -// default constructor -CSGCell::CSGCell() -{ - geom_type() = GeometryType::CSG; -} - CSGCell::CSGCell(pugi::xml_node cell_node) { - geom_type() = GeometryType::CSG; - if (check_for_node(cell_node, "id")) { id_ = std::stoi(get_node_value(cell_node, "id")); } else { diff --git a/src/dagmc.cpp b/src/dagmc.cpp index e24b3fbf6..ea5be04ba 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -129,8 +129,6 @@ void DAGUniverse::set_id() void DAGUniverse::initialize() { - geom_type() = GeometryType::DAG; - #ifdef OPENMC_UWUW // read uwuw materials from the .h5m file if present read_uwuw_materials(); @@ -661,10 +659,7 @@ void DAGUniverse::override_assign_material(std::unique_ptr& c) const //============================================================================== DAGCell::DAGCell(std::shared_ptr dag_ptr, int32_t dag_idx) - : Cell {}, dagmc_ptr_(dag_ptr), dag_index_(dag_idx) -{ - geom_type() = GeometryType::DAG; -}; + : Cell {}, dagmc_ptr_(dag_ptr), dag_index_(dag_idx) {}; std::pair DAGCell::distance( Position r, Direction u, int32_t on_surface, GeometryState* p) const @@ -765,9 +760,7 @@ BoundingBox DAGCell::bounding_box() const DAGSurface::DAGSurface(std::shared_ptr dag_ptr, int32_t dag_idx) : Surface {}, dagmc_ptr_(dag_ptr), dag_index_(dag_idx) -{ - geom_type() = GeometryType::DAG; -} // empty constructor +{} // empty constructor moab::EntityHandle DAGSurface::mesh_handle() const { diff --git a/src/surface.cpp b/src/surface.cpp index 30a8d1f21..3658b3fa1 100644 --- a/src/surface.cpp +++ b/src/surface.cpp @@ -187,15 +187,6 @@ void Surface::to_hdf5(hid_t group_id) const close_group(surf_group); } -CSGSurface::CSGSurface() : Surface {} -{ - geom_type() = GeometryType::CSG; -}; -CSGSurface::CSGSurface(pugi::xml_node surf_node) : Surface {surf_node} -{ - geom_type() = GeometryType::CSG; -}; - //============================================================================== // Generic functions for x-, y-, and z-, planes. //============================================================================== @@ -218,7 +209,7 @@ double axis_aligned_plane_distance( // SurfaceXPlane implementation //============================================================================== -SurfaceXPlane::SurfaceXPlane(pugi::xml_node surf_node) : CSGSurface(surf_node) +SurfaceXPlane::SurfaceXPlane(pugi::xml_node surf_node) : Surface(surf_node) { read_coeffs(surf_node, id_, {&x0_}); } @@ -258,7 +249,7 @@ BoundingBox SurfaceXPlane::bounding_box(bool pos_side) const // SurfaceYPlane implementation //============================================================================== -SurfaceYPlane::SurfaceYPlane(pugi::xml_node surf_node) : CSGSurface(surf_node) +SurfaceYPlane::SurfaceYPlane(pugi::xml_node surf_node) : Surface(surf_node) { read_coeffs(surf_node, id_, {&y0_}); } @@ -298,7 +289,7 @@ BoundingBox SurfaceYPlane::bounding_box(bool pos_side) const // SurfaceZPlane implementation //============================================================================== -SurfaceZPlane::SurfaceZPlane(pugi::xml_node surf_node) : CSGSurface(surf_node) +SurfaceZPlane::SurfaceZPlane(pugi::xml_node surf_node) : Surface(surf_node) { read_coeffs(surf_node, id_, {&z0_}); } @@ -338,7 +329,7 @@ BoundingBox SurfaceZPlane::bounding_box(bool pos_side) const // SurfacePlane implementation //============================================================================== -SurfacePlane::SurfacePlane(pugi::xml_node surf_node) : CSGSurface(surf_node) +SurfacePlane::SurfacePlane(pugi::xml_node surf_node) : Surface(surf_node) { read_coeffs(surf_node, id_, {&A_, &B_, &C_, &D_}); } @@ -457,7 +448,7 @@ Direction axis_aligned_cylinder_normal( //============================================================================== SurfaceXCylinder::SurfaceXCylinder(pugi::xml_node surf_node) - : CSGSurface(surf_node) + : Surface(surf_node) { read_coeffs(surf_node, id_, {&y0_, &z0_, &radius_}); } @@ -500,7 +491,7 @@ BoundingBox SurfaceXCylinder::bounding_box(bool pos_side) const //============================================================================== SurfaceYCylinder::SurfaceYCylinder(pugi::xml_node surf_node) - : CSGSurface(surf_node) + : Surface(surf_node) { read_coeffs(surf_node, id_, {&x0_, &z0_, &radius_}); } @@ -544,7 +535,7 @@ BoundingBox SurfaceYCylinder::bounding_box(bool pos_side) const //============================================================================== SurfaceZCylinder::SurfaceZCylinder(pugi::xml_node surf_node) - : CSGSurface(surf_node) + : Surface(surf_node) { read_coeffs(surf_node, id_, {&x0_, &y0_, &radius_}); } @@ -587,7 +578,7 @@ BoundingBox SurfaceZCylinder::bounding_box(bool pos_side) const // SurfaceSphere implementation //============================================================================== -SurfaceSphere::SurfaceSphere(pugi::xml_node surf_node) : CSGSurface(surf_node) +SurfaceSphere::SurfaceSphere(pugi::xml_node surf_node) : Surface(surf_node) { read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &radius_}); } @@ -753,7 +744,7 @@ Direction axis_aligned_cone_normal( // SurfaceXCone implementation //============================================================================== -SurfaceXCone::SurfaceXCone(pugi::xml_node surf_node) : CSGSurface(surf_node) +SurfaceXCone::SurfaceXCone(pugi::xml_node surf_node) : Surface(surf_node) { read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &radius_sq_}); } @@ -785,7 +776,7 @@ void SurfaceXCone::to_hdf5_inner(hid_t group_id) const // SurfaceYCone implementation //============================================================================== -SurfaceYCone::SurfaceYCone(pugi::xml_node surf_node) : CSGSurface(surf_node) +SurfaceYCone::SurfaceYCone(pugi::xml_node surf_node) : Surface(surf_node) { read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &radius_sq_}); } @@ -817,7 +808,7 @@ void SurfaceYCone::to_hdf5_inner(hid_t group_id) const // SurfaceZCone implementation //============================================================================== -SurfaceZCone::SurfaceZCone(pugi::xml_node surf_node) : CSGSurface(surf_node) +SurfaceZCone::SurfaceZCone(pugi::xml_node surf_node) : Surface(surf_node) { read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &radius_sq_}); } @@ -849,7 +840,7 @@ void SurfaceZCone::to_hdf5_inner(hid_t group_id) const // SurfaceQuadric implementation //============================================================================== -SurfaceQuadric::SurfaceQuadric(pugi::xml_node surf_node) : CSGSurface(surf_node) +SurfaceQuadric::SurfaceQuadric(pugi::xml_node surf_node) : Surface(surf_node) { read_coeffs( surf_node, id_, {&A_, &B_, &C_, &D_, &E_, &F_, &G_, &H_, &J_, &K_}); @@ -1009,7 +1000,7 @@ double torus_distance(double x1, double x2, double x3, double u1, double u2, // SurfaceXTorus implementation //============================================================================== -SurfaceXTorus::SurfaceXTorus(pugi::xml_node surf_node) : CSGSurface(surf_node) +SurfaceXTorus::SurfaceXTorus(pugi::xml_node surf_node) : Surface(surf_node) { read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &A_, &B_, &C_}); } @@ -1062,7 +1053,7 @@ Direction SurfaceXTorus::normal(Position r) const // SurfaceYTorus implementation //============================================================================== -SurfaceYTorus::SurfaceYTorus(pugi::xml_node surf_node) : CSGSurface(surf_node) +SurfaceYTorus::SurfaceYTorus(pugi::xml_node surf_node) : Surface(surf_node) { read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &A_, &B_, &C_}); } @@ -1115,7 +1106,7 @@ Direction SurfaceYTorus::normal(Position r) const // SurfaceZTorus implementation //============================================================================== -SurfaceZTorus::SurfaceZTorus(pugi::xml_node surf_node) : CSGSurface(surf_node) +SurfaceZTorus::SurfaceZTorus(pugi::xml_node surf_node) : Surface(surf_node) { read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &A_, &B_, &C_}); } From aa4de82258424ae9f1dbbb7650ae0ba6b1b6c8b5 Mon Sep 17 00:00:00 2001 From: Gavin Ridley Date: Wed, 19 Feb 2025 19:03:20 -0600 Subject: [PATCH 265/671] remove gsl-lite dependency (#3225) Co-authored-by: Paul Romano --- .gitmodules | 3 - CMakeLists.txt | 15 +- MANIFEST.in | 1 - cmake/OpenMCConfig.cmake.in | 1 - include/openmc/cell.h | 7 +- include/openmc/distribution.h | 6 +- include/openmc/distribution_spatial.h | 5 +- include/openmc/interpolate.h | 12 +- include/openmc/material.h | 10 +- include/openmc/mcpl_interface.h | 10 +- include/openmc/mesh.h | 6 +- include/openmc/nuclide.h | 10 +- include/openmc/photon.h | 3 +- include/openmc/reaction.h | 8 +- include/openmc/span.h | 237 ++++++++++++++++++ include/openmc/state_point.h | 14 +- include/openmc/tallies/filter.h | 5 +- include/openmc/tallies/filter_azimuthal.h | 5 +- include/openmc/tallies/filter_cell.h | 5 +- include/openmc/tallies/filter_cell_instance.h | 9 +- include/openmc/tallies/filter_collision.h | 4 +- include/openmc/tallies/filter_delayedgroup.h | 5 +- include/openmc/tallies/filter_energy.h | 5 +- include/openmc/tallies/filter_energyfunc.h | 3 +- include/openmc/tallies/filter_material.h | 5 +- include/openmc/tallies/filter_mu.h | 5 +- include/openmc/tallies/filter_musurface.h | 2 - include/openmc/tallies/filter_particle.h | 3 +- include/openmc/tallies/filter_polar.h | 5 +- include/openmc/tallies/filter_sph_harm.h | 4 +- include/openmc/tallies/filter_surface.h | 5 +- include/openmc/tallies/filter_time.h | 5 +- include/openmc/tallies/filter_universe.h | 5 +- include/openmc/tallies/tally.h | 6 +- include/openmc/volume_calc.h | 1 - include/openmc/weight_windows.h | 12 +- src/cell.cpp | 16 +- src/dagmc.cpp | 4 +- src/distribution.cpp | 8 +- src/distribution_spatial.cpp | 4 +- src/geometry_aux.cpp | 2 +- src/material.cpp | 15 +- src/mcpl_interface.cpp | 6 +- src/mesh.cpp | 12 +- src/nuclide.cpp | 25 +- src/reaction.cpp | 8 +- src/secondary_thermal.cpp | 5 +- src/simulation.cpp | 6 +- src/state_point.cpp | 8 +- src/surface.cpp | 1 - src/tallies/filter.cpp | 5 +- src/tallies/filter_azimuthal.cpp | 4 +- src/tallies/filter_cell.cpp | 8 +- src/tallies/filter_cell_instance.cpp | 27 +- src/tallies/filter_collision.cpp | 4 +- src/tallies/filter_delayedgroup.cpp | 2 +- src/tallies/filter_distribcell.cpp | 6 +- src/tallies/filter_energy.cpp | 6 +- src/tallies/filter_energyfunc.cpp | 4 +- src/tallies/filter_material.cpp | 8 +- src/tallies/filter_mesh.cpp | 1 - src/tallies/filter_mu.cpp | 4 +- src/tallies/filter_particle.cpp | 2 +- src/tallies/filter_polar.cpp | 4 +- src/tallies/filter_sph_harm.cpp | 8 +- src/tallies/filter_surface.cpp | 8 +- src/tallies/filter_time.cpp | 2 +- src/tallies/filter_universe.cpp | 8 +- src/tallies/filter_zernike.cpp | 4 +- src/tallies/tally.cpp | 11 +- src/weight_windows.cpp | 11 +- vendor/gsl-lite | 1 - 72 files changed, 458 insertions(+), 247 deletions(-) create mode 100644 include/openmc/span.h delete mode 160000 vendor/gsl-lite diff --git a/.gitmodules b/.gitmodules index 4dde5fd5e..f84d09bb1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,6 @@ [submodule "vendor/pugixml"] path = vendor/pugixml url = https://github.com/zeux/pugixml.git -[submodule "vendor/gsl-lite"] - path = vendor/gsl-lite - url = https://github.com/martinmoene/gsl-lite.git [submodule "vendor/xtensor"] path = vendor/xtensor url = https://github.com/xtensor-stack/xtensor.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 94caec757..35694a14b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -293,19 +293,6 @@ if (NOT xtensor_FOUND) add_subdirectory(vendor/xtensor) endif() -#=============================================================================== -# GSL header-only library -#=============================================================================== - -find_package_write_status(gsl-lite) -if (NOT gsl-lite_FOUND) - add_subdirectory(vendor/gsl-lite) - - # Make sure contract violations throw exceptions - target_compile_definitions(gsl-lite-v1 INTERFACE GSL_THROW_ON_CONTRACT_VIOLATION) - target_compile_definitions(gsl-lite-v1 INTERFACE gsl_CONFIG_ALLOWS_NONSTRICT_SPAN_COMPARISON=1) -endif() - #=============================================================================== # Catch2 library #=============================================================================== @@ -519,7 +506,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 gsl::gsl-lite-v1 fmt::fmt ${CMAKE_DL_LIBS}) + xtensor fmt::fmt ${CMAKE_DL_LIBS}) if(TARGET pugixml::pugixml) target_link_libraries(libopenmc pugixml::pugixml) diff --git a/MANIFEST.in b/MANIFEST.in index b73218af0..cdc7e2abc 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -43,6 +43,5 @@ recursive-include vendor *.hh recursive-include vendor *.hpp recursive-include vendor *.pc.in recursive-include vendor *.natvis -include vendor/gsl-lite/include/gsl/gsl prune docs/build prune docs/source/pythonapi/generated/ diff --git a/cmake/OpenMCConfig.cmake.in b/cmake/OpenMCConfig.cmake.in index b02bbffe5..21c552303 100644 --- a/cmake/OpenMCConfig.cmake.in +++ b/cmake/OpenMCConfig.cmake.in @@ -1,7 +1,6 @@ get_filename_component(OpenMC_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) find_package(fmt REQUIRED HINTS ${OpenMC_CMAKE_DIR}/../fmt) -find_package(gsl-lite REQUIRED HINTS ${OpenMC_CMAKE_DIR}/../gsl-lite) find_package(pugixml REQUIRED HINTS ${OpenMC_CMAKE_DIR}/../pugixml) find_package(xtl REQUIRED HINTS ${OpenMC_CMAKE_DIR}/../xtl) find_package(xtensor REQUIRED HINTS ${OpenMC_CMAKE_DIR}/../xtensor) diff --git a/include/openmc/cell.h b/include/openmc/cell.h index 05abbfdfa..98709fb33 100644 --- a/include/openmc/cell.h +++ b/include/openmc/cell.h @@ -10,7 +10,6 @@ #include "hdf5.h" #include "pugixml.hpp" -#include #include "openmc/bounding_box.h" #include "openmc/constants.h" @@ -128,7 +127,7 @@ private: void add_precedence(); //! Add parenthesis to enforce precedence - gsl::index add_parentheses(gsl::index start); + int64_t add_parentheses(int64_t start); //! Remove complement operators from the expression void remove_complement_ops(); @@ -418,8 +417,8 @@ struct CellInstance { return index_cell == other.index_cell && instance == other.instance; } - gsl::index index_cell; - gsl::index instance; + int64_t index_cell; + int64_t instance; }; //! Structure necessary for inserting CellInstance into hashed STL data diff --git a/include/openmc/distribution.h b/include/openmc/distribution.h index e70c803de..854cf7d77 100644 --- a/include/openmc/distribution.h +++ b/include/openmc/distribution.h @@ -7,10 +7,10 @@ #include // for size_t #include "pugixml.hpp" -#include #include "openmc/constants.h" #include "openmc/memory.h" // for unique_ptr +#include "openmc/span.h" #include "openmc/vector.h" // for vector namespace openmc { @@ -44,9 +44,9 @@ class DiscreteIndex { public: DiscreteIndex() {}; DiscreteIndex(pugi::xml_node node); - DiscreteIndex(gsl::span p); + DiscreteIndex(span p); - void assign(gsl::span p); + void assign(span p); //! Sample a value from the distribution //! \param seed Pseudorandom number seed pointer diff --git a/include/openmc/distribution_spatial.h b/include/openmc/distribution_spatial.h index 8ff766d13..9c3bc743f 100644 --- a/include/openmc/distribution_spatial.h +++ b/include/openmc/distribution_spatial.h @@ -6,6 +6,7 @@ #include "openmc/distribution.h" #include "openmc/mesh.h" #include "openmc/position.h" +#include "openmc/span.h" namespace openmc { @@ -104,7 +105,7 @@ private: class MeshSpatial : public SpatialDistribution { public: explicit MeshSpatial(pugi::xml_node node); - explicit MeshSpatial(int32_t mesh_id, gsl::span strengths); + explicit MeshSpatial(int32_t mesh_id, span strengths); //! Sample a position from the distribution //! \param seed Pseudorandom number seed pointer @@ -144,7 +145,7 @@ class PointCloud : public SpatialDistribution { public: explicit PointCloud(pugi::xml_node node); explicit PointCloud( - std::vector point_cloud, gsl::span strengths); + std::vector point_cloud, span strengths); //! Sample a position from the distribution //! \param seed Pseudorandom number seed pointer diff --git a/include/openmc/interpolate.h b/include/openmc/interpolate.h index db501f71b..31ae8b0e3 100644 --- a/include/openmc/interpolate.h +++ b/include/openmc/interpolate.h @@ -4,10 +4,9 @@ #include #include -#include - #include "openmc/error.h" #include "openmc/search.h" +#include "openmc/span.h" namespace openmc { @@ -36,8 +35,8 @@ inline double interpolate_log_log( return y0 * std::exp(f * std::log(y1 / y0)); } -inline double interpolate_lagrangian(gsl::span xs, - gsl::span ys, int idx, double x, int order) +inline double interpolate_lagrangian( + span xs, span ys, int idx, double x, int order) { double output {0.0}; @@ -56,9 +55,8 @@ inline double interpolate_lagrangian(gsl::span xs, return output; } -inline double interpolate(gsl::span xs, - gsl::span ys, double x, - Interpolation i = Interpolation::lin_lin) +inline double interpolate(span xs, span ys, + double x, Interpolation i = Interpolation::lin_lin) { int idx = lower_bound_index(xs.begin(), xs.end(), x); diff --git a/include/openmc/material.h b/include/openmc/material.h index 9235be356..31fab2ae2 100644 --- a/include/openmc/material.h +++ b/include/openmc/material.h @@ -4,9 +4,9 @@ #include #include +#include "openmc/span.h" #include "pugixml.hpp" #include "xtensor/xtensor.hpp" -#include #include #include "openmc/bremsstrahlung.h" @@ -118,21 +118,21 @@ public: // //! \param[in] density Density value //! \param[in] units Units of density - void set_density(double density, gsl::cstring_span units); + void set_density(double density, const std::string& units); //! Set temperature of the material void set_temperature(double temperature) { temperature_ = temperature; }; //! Get nuclides in material //! \return Indices into the global nuclides vector - gsl::span nuclides() const + span nuclides() const { return {nuclide_.data(), nuclide_.size()}; } //! Get densities of each nuclide in material //! \return Densities in [atom/b-cm] - gsl::span densities() const + span densities() const { return {atom_density_.data(), atom_density_.size()}; } @@ -210,7 +210,7 @@ private: //---------------------------------------------------------------------------- // Private data members - gsl::index index_; + int64_t index_; bool depletable_ {false}; //!< Is the material depletable? bool fissionable_ { diff --git a/include/openmc/mcpl_interface.h b/include/openmc/mcpl_interface.h index 1f0c94d6d..e5c182280 100644 --- a/include/openmc/mcpl_interface.h +++ b/include/openmc/mcpl_interface.h @@ -2,10 +2,9 @@ #define OPENMC_MCPL_INTERFACE_H #include "openmc/particle_data.h" +#include "openmc/span.h" #include "openmc/vector.h" -#include - #include namespace openmc { @@ -30,13 +29,14 @@ vector mcpl_source_sites(std::string path); // //! \param[in] filename Path to MCPL file //! \param[in] source_bank Vector of SourceSites to write to file for this -//! MPI rank +//! MPI rank. Note that this can't be const due to +//! it being used as work space by MPI. //! \param[in] bank_indx Pointer to vector of site index ranges over all //! MPI ranks. This can be computed by calling //! calculate_parallel_index_vector on //! source_bank.size(). -void write_mcpl_source_point(const char* filename, - gsl::span source_bank, vector const& bank_index); +void write_mcpl_source_point(const char* filename, span source_bank, + const vector& bank_index); } // namespace openmc #endif // OPENMC_MCPL_INTERFACE_H diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index bd86d54fb..3aef49c7e 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -9,13 +9,13 @@ #include "hdf5.h" #include "pugixml.hpp" #include "xtensor/xtensor.hpp" -#include #include "openmc/bounding_box.h" #include "openmc/error.h" #include "openmc/memory.h" // for unique_ptr #include "openmc/particle.h" #include "openmc/position.h" +#include "openmc/span.h" #include "openmc/vector.h" #include "openmc/xml_interface.h" @@ -179,8 +179,8 @@ public: //! \param[out] Array of (material index, volume) for desired element //! \param[inout] seed Pseudorandom number seed //! \return Number of materials within element - int material_volumes(int n_sample, int bin, gsl::span volumes, - uint64_t* seed) const; + int material_volumes( + int n_sample, int bin, span volumes, uint64_t* seed) const; //! Determine volume of materials within a single mesh elemenet // diff --git a/include/openmc/nuclide.h b/include/openmc/nuclide.h index 1cc9d2972..329c776d0 100644 --- a/include/openmc/nuclide.h +++ b/include/openmc/nuclide.h @@ -7,7 +7,6 @@ #include #include // for pair -#include #include #include "openmc/array.h" @@ -17,6 +16,7 @@ #include "openmc/particle.h" #include "openmc/reaction.h" #include "openmc/reaction_product.h" +#include "openmc/span.h" #include "openmc/urr.h" #include "openmc/vector.h" #include "openmc/wmp.h" @@ -81,8 +81,8 @@ public: //! \param[in] energy Energy group boundaries in [eV] //! \param[in] flux Flux in each energy group (not normalized per eV) //! \return Reaction rate - double collapse_rate(int MT, double temperature, - gsl::span energy, gsl::span flux) const; + double collapse_rate(int MT, double temperature, span energy, + span flux) const; //============================================================================ // Data members @@ -91,7 +91,7 @@ public: int A_; //!< Mass number int metastable_; //!< Metastable state double awr_; //!< Atomic weight ratio - gsl::index index_; //!< Index in the nuclides array + int64_t index_; //!< Index in the nuclides array // Temperature dependent cross section data vector kTs_; //!< temperatures in eV (k*T) @@ -138,7 +138,7 @@ private: // //! \param[in] T Temperature in [K] //! \return Temperature index and interpolation factor - std::pair find_temperature(double T) const; + std::pair find_temperature(double T) const; static int XS_TOTAL; static int XS_ABSORPTION; diff --git a/include/openmc/photon.h b/include/openmc/photon.h index 9901c8c6d..1fee6c9f5 100644 --- a/include/openmc/photon.h +++ b/include/openmc/photon.h @@ -7,7 +7,6 @@ #include "openmc/vector.h" #include "xtensor/xtensor.hpp" -#include #include #include @@ -61,7 +60,7 @@ public: // Data members std::string name_; //!< Name of element, e.g. "Zr" int Z_; //!< Atomic number - gsl::index index_; //!< Index in global elements vector + int64_t index_; //!< Index in global elements vector // Microscopic cross sections xt::xtensor energy_; diff --git a/include/openmc/reaction.h b/include/openmc/reaction.h index 93987d9d7..d5f04d136 100644 --- a/include/openmc/reaction.h +++ b/include/openmc/reaction.h @@ -7,10 +7,10 @@ #include #include "hdf5.h" -#include #include "openmc/particle_data.h" #include "openmc/reaction_product.h" +#include "openmc/span.h" #include "openmc/vector.h" namespace openmc { @@ -33,7 +33,7 @@ public: //! \param[in] i_temp Temperature index //! \param[in] i_grid Energy grid index //! \param[in] interp_factor Interpolation factor between grid points - double xs(gsl::index i_temp, gsl::index i_grid, double interp_factor) const; + double xs(int64_t i_temp, int64_t i_grid, double interp_factor) const; //! Calculate cross section // @@ -47,8 +47,8 @@ public: //! \param[in] flux Flux in each energy group (not normalized per eV) //! \param[in] grid Nuclide energy grid //! \return Reaction rate - double collapse_rate(gsl::index i_temp, gsl::span energy, - gsl::span flux, const vector& grid) const; + double collapse_rate(int64_t i_temp, span energy, + span flux, const vector& grid) const; //! Cross section at a single temperature struct TemperatureXS { diff --git a/include/openmc/span.h b/include/openmc/span.h new file mode 100644 index 000000000..723bccd76 --- /dev/null +++ b/include/openmc/span.h @@ -0,0 +1,237 @@ +#ifndef OPENMC_SPAN_H +#define OPENMC_SPAN_H +#include // for std::size_t, std::ptrdiff_t +#include // for std::begin, std::end +#include // for std::out_of_range +#include + +#include "openmc/vector.h" + +namespace openmc { + +template +class span { +public: + using value_type = T; + using pointer = T*; + using const_pointer = const T*; + using reference = T&; + using const_reference = const T&; + using iterator = T*; + using const_iterator = const T*; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + + /** + * @brief Default constructor for an empty span. + */ + span() noexcept : data_(nullptr), size_(0) {} + + /** + * @brief Constructs a span from a pointer and size. + * @param ptr Pointer to the first element. + * @param count Number of elements in the span. + */ + span(pointer ptr, size_type count) : data_(ptr), size_(count) {} + + /** + * @brief Constructs a span from two pointers marking the span range. + * @param first Pointer to the first element. + * @param last Pointer past the last element. + * @throws std::out_of_range if last < first. + */ + span(pointer first, pointer last) : data_(first), size_(last - first) + { + if (last < first) { + throw std::out_of_range("span: last pointer is before first pointer"); + } + } + + /** + * @brief Constructs a span from a non-const std::vector. + * @param vec Reference to the vector to create a span from. + */ + span(std::vector& vec) : data_(vec.data()), size_(vec.size()) {} + + /** + * @brief Constructs a span from a const std::vector. + * + * This is handling the semantics that a span is used + * for read-only access into a vector. + * @param vec Reference to the const vector to create a span from. + */ + template::value>> + span(const std::vector>& vec) + : data_(vec.data()), size_(vec.size()) + {} + + /** + * @brief Constructs a read-only span from a non-const span. + */ + template::value>> + span(const span>& other) noexcept + : data_(other.data()), size_(other.size()) + {} + + /** + * @brief Access an element without bounds checking. + * @param index Index of the element to access. + * @return Reference to the accessed element. + */ + reference operator[](size_type index) { return data_[index]; } + + /** + * @brief Access an element without bounds checking (const version). + * @param index Index of the element to access. + * @return Const reference to the accessed element. + */ + const_reference operator[](size_type index) const { return data_[index]; } + + /** + * @brief Access an element with bounds checking. + * @param index Index of the element to access. + * @return Reference to the accessed element. + * @throws std::out_of_range if index is out of range. + */ + reference at(size_type index) + { + if (index >= size_) { + throw std::out_of_range("span: index out of range"); + } + return data_[index]; + } + + /** + * @brief Access an element with bounds checking (const version). + * @param index Index of the element to access. + * @return Const reference to the accessed element. + * @throws std::out_of_range if index is out of range. + */ + const_reference at(size_type index) const + { + if (index >= size_) { + throw std::out_of_range("span: index out of range"); + } + return data_[index]; + } + + /** + * @brief Get a pointer to the underlying data. + * @return Pointer to the data, or nullptr if the span is empty. + */ + pointer data() noexcept { return data_; } + + /** + * @brief Get a const pointer to the underlying data. + * @return Const pointer to the data, or nullptr if the span is empty. + */ + const_pointer data() const noexcept { return data_; } + + /** + * @brief Get the number of elements in the span. + * @return The size of the span. + */ + size_type size() const noexcept { return size_; } + + /** + * @brief Check if the span is empty. + * @return True if the span is empty, false otherwise. + */ + bool empty() const noexcept { return size_ == 0; } + + /** + * @brief Get an iterator to the beginning of the span. + * @return Iterator pointing to the first element. + */ + iterator begin() noexcept { return data_; } + + /** + * @brief Get a const iterator to the beginning of the span. + * @return Const iterator pointing to the first element. + */ + const_iterator begin() const noexcept { return data_; } + + /** + * @brief Get a const iterator to the beginning of the span. + * @return Const iterator pointing to the first element. + */ + const_iterator cbegin() const noexcept { return data_; } + + /** + * @brief Get an iterator to the end of the span. + * @return Iterator pointing past the last element. + */ + iterator end() noexcept { return data_ + size_; } + + /** + * @brief Get a const iterator to the end of the span. + * @return Const iterator pointing past the last element. + */ + const_iterator end() const noexcept { return data_ + size_; } + + /** + * @brief Get a const iterator to the end of the span. + * @return Const iterator pointing past the last element. + */ + const_iterator cend() const noexcept { return data_ + size_; } + + /** + * @brief Access the first element. + * @return Reference to the first element. + * @throws std::out_of_range if the span is empty. + */ + reference front() + { + if (empty()) { + throw std::out_of_range("span::front(): span is empty"); + } + return data_[0]; + } + + /** + * @brief Access the first element (const version). + * @return Const reference to the first element. + * @throws std::out_of_range if the span is empty. + */ + const_reference front() const + { + if (empty()) { + throw std::out_of_range("span::front(): span is empty"); + } + return data_[0]; + } + + /** + * @brief Access the last element. + * @return Reference to the last element. + * @throws std::out_of_range if the span is empty. + */ + reference back() + { + if (empty()) { + throw std::out_of_range("span::back(): span is empty"); + } + return data_[size_ - 1]; + } + + /** + * @brief Access the last element (const version). + * @return Const reference to the last element. + * @throws std::out_of_range if the span is empty. + */ + const_reference back() const + { + if (empty()) { + throw std::out_of_range("span::back(): span is empty"); + } + return data_[size_ - 1]; + } + +private: + pointer data_; + size_type size_; +}; + +} // namespace openmc +#endif // OPENMC_SPAN_H diff --git a/include/openmc/state_point.h b/include/openmc/state_point.h index f0d41e169..fb1aaf7b9 100644 --- a/include/openmc/state_point.h +++ b/include/openmc/state_point.h @@ -4,13 +4,12 @@ #include #include -#include - #include "hdf5.h" #include "openmc/capi.h" #include "openmc/particle.h" #include "openmc/shared_array.h" +#include "openmc/span.h" #include "openmc/vector.h" namespace openmc { @@ -34,15 +33,18 @@ void load_state_point(); // values on each rank, used to create global indexing. This vector // can be created by calling calculate_parallel_index_vector on // source_bank.size() if such a vector is not already available. -void write_h5_source_point(const char* filename, - gsl::span source_bank, const vector& bank_index); +// +// The source_bank variable is used as work space if MPI is used, +// so it cannot be given as a const span. +void write_h5_source_point(const char* filename, span source_bank, + const vector& bank_index); -void write_source_point(std::string, gsl::span source_bank, +void write_source_point(std::string, span source_bank, const vector& bank_index, bool use_mcpl); // This appends a source bank specification to an HDF5 file // that's already open. It is used internally by write_source_point. -void write_source_bank(hid_t group_id, gsl::span source_bank, +void write_source_bank(hid_t group_id, span source_bank, const vector& bank_index); void read_source_bank( diff --git a/include/openmc/tallies/filter.h b/include/openmc/tallies/filter.h index 210ab284b..31dd609ed 100644 --- a/include/openmc/tallies/filter.h +++ b/include/openmc/tallies/filter.h @@ -6,7 +6,6 @@ #include #include "pugixml.hpp" -#include #include "openmc/constants.h" #include "openmc/hdf5_interface.h" @@ -130,7 +129,7 @@ public: //! \return Number of bins int n_bins() const { return n_bins_; } - gsl::index index() const { return index_; } + int64_t index() const { return index_; } //---------------------------------------------------------------------------- // Data members @@ -140,7 +139,7 @@ protected: private: int32_t id_ {C_NONE}; - gsl::index index_; + int64_t index_; }; //============================================================================== diff --git a/include/openmc/tallies/filter_azimuthal.h b/include/openmc/tallies/filter_azimuthal.h index 37e1ef073..4853c5545 100644 --- a/include/openmc/tallies/filter_azimuthal.h +++ b/include/openmc/tallies/filter_azimuthal.h @@ -4,8 +4,7 @@ #include "openmc/vector.h" #include -#include - +#include "openmc/span.h" #include "openmc/tallies/filter.h" namespace openmc { @@ -39,7 +38,7 @@ public: //---------------------------------------------------------------------------- // Accessors - void set_bins(gsl::span bins); + void set_bins(span bins); private: //---------------------------------------------------------------------------- diff --git a/include/openmc/tallies/filter_cell.h b/include/openmc/tallies/filter_cell.h index b7ea01ce6..ac6539466 100644 --- a/include/openmc/tallies/filter_cell.h +++ b/include/openmc/tallies/filter_cell.h @@ -4,8 +4,7 @@ #include #include -#include - +#include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/vector.h" @@ -42,7 +41,7 @@ public: const vector& cells() const { return cells_; } - void set_cells(gsl::span cells); + void set_cells(span cells); protected: //---------------------------------------------------------------------------- diff --git a/include/openmc/tallies/filter_cell_instance.h b/include/openmc/tallies/filter_cell_instance.h index f500f4889..69499765b 100644 --- a/include/openmc/tallies/filter_cell_instance.h +++ b/include/openmc/tallies/filter_cell_instance.h @@ -4,9 +4,8 @@ #include #include -#include - #include "openmc/cell.h" +#include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/vector.h" @@ -22,7 +21,7 @@ public: // Constructors, destructors CellInstanceFilter() = default; - CellInstanceFilter(gsl::span instances); + CellInstanceFilter(span instances); ~CellInstanceFilter() = default; //---------------------------------------------------------------------------- @@ -47,7 +46,7 @@ public: const std::unordered_set& cells() const { return cells_; } - void set_cell_instances(gsl::span instances); + void set_cell_instances(span instances); private: //---------------------------------------------------------------------------- @@ -60,7 +59,7 @@ private: std::unordered_set cells_; //! A map from cell/instance indices to filter bin indices. - std::unordered_map map_; + std::unordered_map map_; //! Indicates if filter uses only material-filled cells bool material_cells_only_; diff --git a/include/openmc/tallies/filter_collision.h b/include/openmc/tallies/filter_collision.h index d2dab70ca..7d42a5ddd 100644 --- a/include/openmc/tallies/filter_collision.h +++ b/include/openmc/tallies/filter_collision.h @@ -1,9 +1,9 @@ #ifndef OPENMC_TALLIES_FILTER_COLLISIONS_H #define OPENMC_TALLIES_FILTER_COLLISIONS_H -#include #include +#include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/vector.h" @@ -39,7 +39,7 @@ public: // Accessors const vector& bins() const { return bins_; } - void set_bins(gsl::span bins); + void set_bins(span bins); protected: //---------------------------------------------------------------------------- diff --git a/include/openmc/tallies/filter_delayedgroup.h b/include/openmc/tallies/filter_delayedgroup.h index 71919b2ec..7d11447ab 100644 --- a/include/openmc/tallies/filter_delayedgroup.h +++ b/include/openmc/tallies/filter_delayedgroup.h @@ -1,8 +1,7 @@ #ifndef OPENMC_TALLIES_FILTER_DELAYEDGROUP_H #define OPENMC_TALLIES_FILTER_DELAYEDGROUP_H -#include - +#include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/vector.h" @@ -42,7 +41,7 @@ public: const vector& groups() const { return groups_; } - void set_groups(gsl::span groups); + void set_groups(span groups); private: //---------------------------------------------------------------------------- diff --git a/include/openmc/tallies/filter_energy.h b/include/openmc/tallies/filter_energy.h index e35e01a6d..cf8a8aa0f 100644 --- a/include/openmc/tallies/filter_energy.h +++ b/include/openmc/tallies/filter_energy.h @@ -1,8 +1,7 @@ #ifndef OPENMC_TALLIES_FILTER_ENERGY_H #define OPENMC_TALLIES_FILTER_ENERGY_H -#include - +#include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/vector.h" @@ -38,7 +37,7 @@ public: // Accessors const vector& bins() const { return bins_; } - void set_bins(gsl::span bins); + void set_bins(span bins); bool matches_transport_groups() const { return matches_transport_groups_; } diff --git a/include/openmc/tallies/filter_energyfunc.h b/include/openmc/tallies/filter_energyfunc.h index d77ef0fa8..e03c23dda 100644 --- a/include/openmc/tallies/filter_energyfunc.h +++ b/include/openmc/tallies/filter_energyfunc.h @@ -2,6 +2,7 @@ #define OPENMC_TALLIES_FILTER_ENERGYFUNC_H #include "openmc/constants.h" +#include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/vector.h" @@ -42,7 +43,7 @@ public: const vector& energy() const { return energy_; } const vector& y() const { return y_; } Interpolation interpolation() const { return interpolation_; } - void set_data(gsl::span energy, gsl::span y); + void set_data(span energy, span y); void set_interpolation(const std::string& interpolation); private: diff --git a/include/openmc/tallies/filter_material.h b/include/openmc/tallies/filter_material.h index aa5df5b3a..ccfe5403d 100644 --- a/include/openmc/tallies/filter_material.h +++ b/include/openmc/tallies/filter_material.h @@ -4,8 +4,7 @@ #include #include -#include - +#include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/vector.h" @@ -44,7 +43,7 @@ public: const vector& materials() const { return materials_; } - void set_materials(gsl::span materials); + void set_materials(span materials); protected: //---------------------------------------------------------------------------- diff --git a/include/openmc/tallies/filter_mu.h b/include/openmc/tallies/filter_mu.h index b0ee40eac..d6e7f1798 100644 --- a/include/openmc/tallies/filter_mu.h +++ b/include/openmc/tallies/filter_mu.h @@ -1,8 +1,7 @@ #ifndef OPENMC_TALLIES_FILTER_MU_H #define OPENMC_TALLIES_FILTER_MU_H -#include - +#include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/vector.h" @@ -38,7 +37,7 @@ public: //---------------------------------------------------------------------------- // Accessors - void set_bins(gsl::span bins); + void set_bins(span bins); protected: //---------------------------------------------------------------------------- diff --git a/include/openmc/tallies/filter_musurface.h b/include/openmc/tallies/filter_musurface.h index 2ca19e3a2..fa6816836 100644 --- a/include/openmc/tallies/filter_musurface.h +++ b/include/openmc/tallies/filter_musurface.h @@ -1,8 +1,6 @@ #ifndef OPENMC_TALLIES_FILTER_MU_SURFACE_H #define OPENMC_TALLIES_FILTER_MU_SURFACE_H -#include - #include "openmc/tallies/filter_mu.h" #include "openmc/vector.h" diff --git a/include/openmc/tallies/filter_particle.h b/include/openmc/tallies/filter_particle.h index a181d5cee..863a6d282 100644 --- a/include/openmc/tallies/filter_particle.h +++ b/include/openmc/tallies/filter_particle.h @@ -2,6 +2,7 @@ #define OPENMC_TALLIES_FILTER_PARTICLE_H #include "openmc/particle.h" +#include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/vector.h" @@ -38,7 +39,7 @@ public: const vector& particles() const { return particles_; } - void set_particles(gsl::span particles); + void set_particles(span particles); private: //---------------------------------------------------------------------------- diff --git a/include/openmc/tallies/filter_polar.h b/include/openmc/tallies/filter_polar.h index 78bb25aa4..c7c73c89f 100644 --- a/include/openmc/tallies/filter_polar.h +++ b/include/openmc/tallies/filter_polar.h @@ -3,8 +3,7 @@ #include -#include - +#include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/vector.h" @@ -39,7 +38,7 @@ public: //---------------------------------------------------------------------------- // Accessors - void set_bins(gsl::span bins); + void set_bins(span bins); private: //---------------------------------------------------------------------------- diff --git a/include/openmc/tallies/filter_sph_harm.h b/include/openmc/tallies/filter_sph_harm.h index 5d4a4bd99..a6904c301 100644 --- a/include/openmc/tallies/filter_sph_harm.h +++ b/include/openmc/tallies/filter_sph_harm.h @@ -3,8 +3,6 @@ #include -#include - #include "openmc/tallies/filter.h" namespace openmc { @@ -46,7 +44,7 @@ public: SphericalHarmonicsCosine cosine() const { return cosine_; } - void set_cosine(gsl::cstring_span cosine); + void set_cosine(const std::string& cosine); private: //---------------------------------------------------------------------------- diff --git a/include/openmc/tallies/filter_surface.h b/include/openmc/tallies/filter_surface.h index 3537f1cc7..e78243f5f 100644 --- a/include/openmc/tallies/filter_surface.h +++ b/include/openmc/tallies/filter_surface.h @@ -4,8 +4,7 @@ #include #include -#include - +#include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/vector.h" @@ -40,7 +39,7 @@ public: //---------------------------------------------------------------------------- // Accessors - void set_surfaces(gsl::span surfaces); + void set_surfaces(span surfaces); private: //---------------------------------------------------------------------------- diff --git a/include/openmc/tallies/filter_time.h b/include/openmc/tallies/filter_time.h index 105ef9880..3ce557abd 100644 --- a/include/openmc/tallies/filter_time.h +++ b/include/openmc/tallies/filter_time.h @@ -1,8 +1,7 @@ #ifndef OPENMC_TALLIES_FILTER_TIME_H #define OPENMC_TALLIES_FILTER_TIME_H -#include - +#include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/vector.h" @@ -38,7 +37,7 @@ public: // Accessors const vector& bins() const { return bins_; } - void set_bins(gsl::span bins); + void set_bins(span bins); protected: //---------------------------------------------------------------------------- diff --git a/include/openmc/tallies/filter_universe.h b/include/openmc/tallies/filter_universe.h index d4894353b..461434ec4 100644 --- a/include/openmc/tallies/filter_universe.h +++ b/include/openmc/tallies/filter_universe.h @@ -4,8 +4,7 @@ #include #include -#include - +#include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/vector.h" @@ -40,7 +39,7 @@ public: //---------------------------------------------------------------------------- // Accessors - void set_universes(gsl::span universes); + void set_universes(span universes); private: //---------------------------------------------------------------------------- diff --git a/include/openmc/tallies/tally.h b/include/openmc/tallies/tally.h index 2f0cddcf0..48f678ced 100644 --- a/include/openmc/tallies/tally.h +++ b/include/openmc/tallies/tally.h @@ -3,6 +3,7 @@ #include "openmc/constants.h" #include "openmc/memory.h" // for unique_ptr +#include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/tallies/trigger.h" #include "openmc/vector.h" @@ -10,7 +11,6 @@ #include "pugixml.hpp" #include "xtensor/xfixed.hpp" #include "xtensor/xtensor.hpp" -#include #include #include @@ -93,7 +93,7 @@ public: //! \brief Check if this tally has a specified type of filter bool has_filter(FilterType filter_type) const; - void set_filters(gsl::span filters); + void set_filters(span filters); //! Given already-set filters, set the stride lengths void set_strides(); @@ -192,7 +192,7 @@ private: //! Whether to multiply by atom density for reaction rates bool multiply_density_ {true}; - gsl::index index_; + int64_t index_; }; //============================================================================== diff --git a/include/openmc/volume_calc.h b/include/openmc/volume_calc.h index 376b8c707..fa8d3d65e 100644 --- a/include/openmc/volume_calc.h +++ b/include/openmc/volume_calc.h @@ -14,7 +14,6 @@ #include "pugixml.hpp" #include "xtensor/xtensor.hpp" -#include #ifdef _OPENMP #include #endif diff --git a/include/openmc/weight_windows.h b/include/openmc/weight_windows.h index 2d4d55694..6f2ef0707 100644 --- a/include/openmc/weight_windows.h +++ b/include/openmc/weight_windows.h @@ -4,7 +4,6 @@ #include #include -#include #include #include @@ -12,6 +11,7 @@ #include "openmc/memory.h" #include "openmc/mesh.h" #include "openmc/particle.h" +#include "openmc/span.h" #include "openmc/tallies/tally.h" #include "openmc/vector.h" @@ -104,7 +104,7 @@ public: //! Set the weight window ID void set_id(int32_t id = -1); - void set_energy_bounds(gsl::span bounds); + void set_energy_bounds(span bounds); void set_mesh(const std::unique_ptr& mesh); @@ -148,9 +148,9 @@ public: void set_bounds(const xt::xtensor& lower_bounds, double ratio); void set_bounds( - gsl::span lower_bounds, gsl::span upper_bounds); + span lower_bounds, span upper_bounds); - void set_bounds(gsl::span lower_bounds, double ratio); + void set_bounds(span lower_bounds, double ratio); void set_particle_type(ParticleType p_type); @@ -192,8 +192,8 @@ public: private: //---------------------------------------------------------------------------- // Data members - int32_t id_; //!< Unique ID - gsl::index index_; //!< Index into weight windows vector + int32_t id_; //!< Unique ID + int64_t index_; //!< Index into weight windows vector ParticleType particle_type_ { ParticleType::neutron}; //!< Particle type to apply weight windows to vector energy_bounds_; //!< Energy boundaries [eV] diff --git a/src/cell.cpp b/src/cell.cpp index fa8be4496..4b2699229 100644 --- a/src/cell.cpp +++ b/src/cell.cpp @@ -2,6 +2,7 @@ #include "openmc/cell.h" #include +#include #include #include #include @@ -10,7 +11,6 @@ #include #include -#include #include "openmc/capi.h" #include "openmc/constants.h" @@ -137,7 +137,7 @@ void Cell::set_temperature(double T, int32_t instance, bool set_contained) auto contained_cells = this->get_contained_cells(instance); for (const auto& entry : contained_cells) { auto& cell = model::cells[entry.first]; - Expects(cell->type_ == Fill::MATERIAL); + assert(cell->type_ == Fill::MATERIAL); auto& instances = entry.second; for (auto instance : instances) { cell->set_temperature(T, instance); @@ -179,7 +179,7 @@ void Cell::import_properties_hdf5(hid_t group) // Modify temperatures for the cell sqrtkT_.clear(); sqrtkT_.resize(temps.size()); - for (gsl::index i = 0; i < temps.size(); ++i) { + for (int64_t i = 0; i < temps.size(); ++i) { this->set_temperature(temps[i], i); } @@ -570,7 +570,7 @@ void Region::apply_demorgan( //! precedence than unions using parentheses. //============================================================================== -gsl::index Region::add_parentheses(gsl::index start) +int64_t Region::add_parentheses(int64_t start) { int32_t start_token = expression_[start]; // Add left parenthesis and set new position to be after parenthesis @@ -642,7 +642,7 @@ void Region::add_precedence() int32_t current_op = 0; std::size_t current_dist = 0; - for (gsl::index i = 0; i < expression_.size(); i++) { + for (int64_t i = 0; i < expression_.size(); i++) { int32_t token = expression_[i]; if (token == OP_UNION || token == OP_INTERSECTION) { @@ -938,7 +938,7 @@ BoundingBox Region::bounding_box_complex(vector postfix) const } } - Ensures(i_stack == 0); + assert(i_stack == 0); return stack.front(); } @@ -1210,8 +1210,8 @@ struct ParentCell { lattice_index < other.lattice_index); } - gsl::index cell_index; - gsl::index lattice_index; + int64_t cell_index; + int64_t lattice_index; }; //! Structure used to insert ParentCell into hashed STL data structures diff --git a/src/dagmc.cpp b/src/dagmc.cpp index ea5be04ba..134360886 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -1,5 +1,7 @@ #include "openmc/dagmc.h" +#include + #include "openmc/constants.h" #include "openmc/container_util.h" #include "openmc/error.h" @@ -800,7 +802,7 @@ Direction DAGSurface::normal(Position r) const Direction DAGSurface::reflect(Position r, Direction u, GeometryState* p) const { - Expects(p); + assert(p); double pnt[3] = {r.x, r.y, r.z}; double dir[3]; moab::ErrorCode rval = diff --git a/src/distribution.cpp b/src/distribution.cpp index 8c700460f..ca00cbde4 100644 --- a/src/distribution.cpp +++ b/src/distribution.cpp @@ -8,8 +8,6 @@ #include // for runtime_error #include // for string, stod -#include - #include "openmc/error.h" #include "openmc/math_functions.h" #include "openmc/random_dist.h" @@ -30,12 +28,12 @@ DiscreteIndex::DiscreteIndex(pugi::xml_node node) assign({params.data() + n, n}); } -DiscreteIndex::DiscreteIndex(gsl::span p) +DiscreteIndex::DiscreteIndex(span p) { assign(p); } -void DiscreteIndex::assign(gsl::span p) +void DiscreteIndex::assign(span p) { prob_.assign(p.begin(), p.end()); @@ -417,7 +415,7 @@ double Mixture::sample(uint64_t* seed) const p, [](const DistPair& pair, double p) { return pair.first < p; }); // This should not happen. Catch it - Ensures(it != distribution_.cend()); + assert(it != distribution_.cend()); // Sample the chosen distribution return it->second->sample(seed); diff --git a/src/distribution_spatial.cpp b/src/distribution_spatial.cpp index 5c193a95d..d2b0f413b 100644 --- a/src/distribution_spatial.cpp +++ b/src/distribution_spatial.cpp @@ -262,7 +262,7 @@ MeshSpatial::MeshSpatial(pugi::xml_node node) elem_idx_dist_.assign(strengths); } -MeshSpatial::MeshSpatial(int32_t mesh_idx, gsl::span strengths) +MeshSpatial::MeshSpatial(int32_t mesh_idx, span strengths) : mesh_idx_(mesh_idx) { check_element_types(); @@ -331,7 +331,7 @@ PointCloud::PointCloud(pugi::xml_node node) } PointCloud::PointCloud( - std::vector point_cloud, gsl::span strengths) + std::vector point_cloud, span strengths) { point_cloud_.assign(point_cloud.begin(), point_cloud.end()); point_idx_dist_.assign(strengths); diff --git a/src/geometry_aux.cpp b/src/geometry_aux.cpp index 050d4db96..2f8a55743 100644 --- a/src/geometry_aux.cpp +++ b/src/geometry_aux.cpp @@ -344,7 +344,7 @@ void prepare_distribcell(const std::vector* user_distribcells) // By default, add material cells to the list of distributed cells if (settings::material_cell_offsets) { - for (gsl::index i = 0; i < model::cells.size(); ++i) { + for (int64_t i = 0; i < model::cells.size(); ++i) { if (model::cells[i]->type_ == Fill::MATERIAL) distribcells.insert(i); } diff --git a/src/material.cpp b/src/material.cpp index aad7008e7..3ba9ab96c 100644 --- a/src/material.cpp +++ b/src/material.cpp @@ -1,6 +1,7 @@ #include "openmc/material.h" #include // for min, max, sort, fill +#include #include #include #include @@ -933,7 +934,7 @@ void Material::calculate_photon_xs(Particle& p) const void Material::set_id(int32_t id) { - Expects(id >= 0 || id == C_NONE); + assert(id >= 0 || id == C_NONE); // Clear entry in material map if an ID was already assigned before if (id_ != C_NONE) { @@ -961,9 +962,9 @@ void Material::set_id(int32_t id) model::material_map[id] = index_; } -void Material::set_density(double density, gsl::cstring_span units) +void Material::set_density(double density, const std::string& units) { - Expects(density >= 0.0); + assert(density >= 0.0); if (nuclide_.empty()) { throw std::runtime_error {"No nuclides exist in material yet."}; @@ -1006,8 +1007,8 @@ void Material::set_densities( const vector& name, const vector& density) { auto n = name.size(); - Expects(n > 0); - Expects(n == density.size()); + assert(n > 0); + assert(n == density.size()); if (n != nuclide_.size()) { nuclide_.resize(n); @@ -1017,7 +1018,7 @@ void Material::set_densities( } double sum_density = 0.0; - for (gsl::index i = 0; i < n; ++i) { + for (int64_t i = 0; i < n; ++i) { const auto& nuc {name[i]}; if (data::nuclide_map.find(nuc) == data::nuclide_map.end()) { int err = openmc_load_nuclide(nuc.c_str(), nullptr, 0); @@ -1026,7 +1027,7 @@ void Material::set_densities( } nuclide_[i] = data::nuclide_map.at(nuc); - Expects(density[i] > 0.0); + assert(density[i] > 0.0); atom_density_(i) = density[i]; sum_density += density[i]; diff --git a/src/mcpl_interface.cpp b/src/mcpl_interface.cpp index 5c3df026c..a8de5fdb0 100644 --- a/src/mcpl_interface.cpp +++ b/src/mcpl_interface.cpp @@ -112,7 +112,7 @@ vector mcpl_source_sites(std::string path) #ifdef OPENMC_MCPL void write_mcpl_source_bank(mcpl_outfile_t file_id, - gsl::span source_bank, const vector& bank_index) + span source_bank, const vector& bank_index) { int64_t dims_size = settings::n_particles; int64_t count_size = simulation::work_per_rank; @@ -188,8 +188,8 @@ void write_mcpl_source_bank(mcpl_outfile_t file_id, //============================================================================== -void write_mcpl_source_point(const char* filename, - gsl::span source_bank, const vector& bank_index) +void write_mcpl_source_point(const char* filename, span source_bank, + const vector& bank_index) { std::string filename_(filename); const auto extension = get_file_extension(filename_); diff --git a/src/mesh.cpp b/src/mesh.cpp index 97bf710ca..3e1f25c4b 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -1,9 +1,9 @@ #include "openmc/mesh.h" -#include // for copy, equal, min, min_element +#include // for copy, equal, min, min_element +#include #define _USE_MATH_DEFINES // to make M_PI declared in Intel and MSVC compilers #include // for ceil #include // for size_t -#include #include #ifdef OPENMC_MPI @@ -116,7 +116,7 @@ Mesh::Mesh(pugi::xml_node node) void Mesh::set_id(int32_t id) { - Expects(id >= 0 || id == C_NONE); + assert(id >= 0 || id == C_NONE); // Clear entry in mesh map in case one was already assigned if (id_ != C_NONE) { @@ -154,7 +154,7 @@ vector Mesh::volumes() const } int Mesh::material_volumes( - int n_sample, int bin, gsl::span result, uint64_t* seed) const + int n_sample, int bin, span result, uint64_t* seed) const { vector materials; vector hits; @@ -3184,13 +3184,13 @@ void LibMesh::set_score_data(const std::string& var_name, // set value vector value_dof_indices; dof_map.dof_indices(*it, value_dof_indices, value_num); - Ensures(value_dof_indices.size() == 1); + assert(value_dof_indices.size() == 1); eqn_sys.solution->set(value_dof_indices[0], values.at(bin)); // set std dev vector std_dev_dof_indices; dof_map.dof_indices(*it, std_dev_dof_indices, std_dev_num); - Ensures(std_dev_dof_indices.size() == 1); + assert(std_dev_dof_indices.size() == 1); eqn_sys.solution->set(std_dev_dof_indices[0], std_dev.at(bin)); } } diff --git a/src/nuclide.cpp b/src/nuclide.cpp index f720f848b..d78b7a010 100644 --- a/src/nuclide.cpp +++ b/src/nuclide.cpp @@ -21,7 +21,8 @@ #include "xtensor/xview.hpp" #include // for sort, min_element -#include // for to_string, stoi +#include +#include // for to_string, stoi namespace openmc { @@ -999,19 +1000,19 @@ void Nuclide::calculate_urr_xs(int i_temp, Particle& p) const } } -std::pair Nuclide::find_temperature(double T) const +std::pair Nuclide::find_temperature(double T) const { - Expects(T >= 0.0); + assert(T >= 0.0); // Determine temperature index - gsl::index i_temp = 0; + int64_t i_temp = 0; double f = 0.0; double kT = K_BOLTZMANN * T; - gsl::index n = kTs_.size(); + int64_t n = kTs_.size(); switch (settings::temperature_method) { case TemperatureMethod::NEAREST: { double max_diff = INFTY; - for (gsl::index t = 0; t < n; ++t) { + for (int64_t t = 0; t < n; ++t) { double diff = std::abs(kTs_[t] - kT); if (diff < max_diff) { i_temp = t; @@ -1038,17 +1039,17 @@ std::pair Nuclide::find_temperature(double T) const f = (kT - kTs_[i_temp]) / (kTs_[i_temp + 1] - kTs_[i_temp]); } - Ensures(i_temp >= 0 && i_temp < n); + assert(i_temp >= 0 && i_temp < n); return {i_temp, f}; } double Nuclide::collapse_rate(int MT, double temperature, - gsl::span energy, gsl::span flux) const + span energy, span flux) const { - Expects(MT > 0); - Expects(energy.size() > 0); - Expects(energy.size() == flux.size() + 1); + assert(MT > 0); + assert(energy.size() > 0); + assert(energy.size() == flux.size() + 1); int i_rx = reaction_index_[MT]; if (i_rx < 0) @@ -1056,7 +1057,7 @@ double Nuclide::collapse_rate(int MT, double temperature, const auto& rx = reactions_[i_rx]; // Determine temperature index - gsl::index i_temp; + int64_t i_temp; double f; std::tie(i_temp, f) = this->find_temperature(temperature); diff --git a/src/reaction.cpp b/src/reaction.cpp index 3bdc83746..60a14b2b3 100644 --- a/src/reaction.cpp +++ b/src/reaction.cpp @@ -65,8 +65,7 @@ Reaction::Reaction(hid_t group, const vector& temperatures) } } -double Reaction::xs( - gsl::index i_temp, gsl::index i_grid, double interp_factor) const +double Reaction::xs(int64_t i_temp, int64_t i_grid, double interp_factor) const { // If energy is below threshold, return 0. Otherwise interpolate between // nearest grid points @@ -82,9 +81,8 @@ double Reaction::xs(const NuclideMicroXS& micro) const return this->xs(micro.index_temp, micro.index_grid, micro.interp_factor); } -double Reaction::collapse_rate(gsl::index i_temp, - gsl::span energy, gsl::span flux, - const vector& grid) const +double Reaction::collapse_rate(int64_t i_temp, span energy, + span flux, const vector& grid) const { // Find index corresponding to first energy const auto& xs = xs_[i_temp].value; diff --git a/src/secondary_thermal.cpp b/src/secondary_thermal.cpp index 0b8e1ab42..8b9e8737c 100644 --- a/src/secondary_thermal.cpp +++ b/src/secondary_thermal.cpp @@ -4,10 +4,9 @@ #include "openmc/random_lcg.h" #include "openmc/search.h" -#include - #include "xtensor/xview.hpp" +#include #include // for log, exp namespace openmc { @@ -40,7 +39,7 @@ void CoherentElasticAE::sample( const auto& energies {xs_.bragg_edges()}; - Expects(E_in >= energies.front()); + assert(E_in >= energies.front()); const int i = lower_bound_index(energies.begin(), energies.end(), E_in); diff --git a/src/simulation.cpp b/src/simulation.cpp index 030e447f1..a3c6495ae 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -439,7 +439,7 @@ void finalize_batch() int w = std::to_string(settings::n_max_batches).size(); std::string source_point_filename = fmt::format("{0}source.{1:0{2}}", settings::path_output, simulation::current_batch, w); - gsl::span bankspan(simulation::source_bank); + span bankspan(simulation::source_bank); write_source_point(source_point_filename, bankspan, simulation::work_index, settings::source_mcpl_write); } @@ -447,7 +447,7 @@ void finalize_batch() // Write a continously-overwritten source point if requested. if (settings::source_latest) { auto filename = settings::path_output + "source"; - gsl::span bankspan(simulation::source_bank); + span bankspan(simulation::source_bank); write_source_point(filename, bankspan, simulation::work_index, settings::source_mcpl_write); } @@ -469,7 +469,7 @@ void finalize_batch() // Get span of source bank and calculate parallel index vector auto surf_work_index = mpi::calculate_parallel_index_vector( simulation::surf_source_bank.size()); - gsl::span surfbankspan(simulation::surf_source_bank.begin(), + span surfbankspan(simulation::surf_source_bank.begin(), simulation::surf_source_bank.size()); // Write surface source file diff --git a/src/state_point.cpp b/src/state_point.cpp index fcc389df1..ed8c6ed41 100644 --- a/src/state_point.cpp +++ b/src/state_point.cpp @@ -567,7 +567,7 @@ hid_t h5banktype() return banktype; } -void write_source_point(std::string filename, gsl::span source_bank, +void write_source_point(std::string filename, span source_bank, const vector& bank_index, bool use_mcpl) { std::string ext = use_mcpl ? "mcpl" : "h5"; @@ -584,8 +584,8 @@ void write_source_point(std::string filename, gsl::span source_bank, } } -void write_h5_source_point(const char* filename, - gsl::span source_bank, const vector& bank_index) +void write_h5_source_point(const char* filename, span source_bank, + const vector& bank_index) { // When using parallel HDF5, the file is written to collectively by all // processes. With MPI-only, the file is opened and written by the master @@ -620,7 +620,7 @@ void write_h5_source_point(const char* filename, file_close(file_id); } -void write_source_bank(hid_t group_id, gsl::span source_bank, +void write_source_bank(hid_t group_id, span source_bank, const vector& bank_index) { hid_t banktype = h5banktype(); diff --git a/src/surface.cpp b/src/surface.cpp index 3658b3fa1..0002275c3 100644 --- a/src/surface.cpp +++ b/src/surface.cpp @@ -7,7 +7,6 @@ #include #include -#include #include "openmc/array.h" #include "openmc/container_util.h" diff --git a/src/tallies/filter.cpp b/src/tallies/filter.cpp index 074212db4..0ce1ee20b 100644 --- a/src/tallies/filter.cpp +++ b/src/tallies/filter.cpp @@ -1,7 +1,8 @@ #include "openmc/tallies/filter.h" #include // for max -#include // for strcpy +#include +#include // for strcpy #include #include @@ -162,7 +163,7 @@ Filter* Filter::create(const std::string& type, int32_t id) void Filter::set_id(int32_t id) { - Expects(id >= 0 || id == C_NONE); + assert(id >= 0 || id == C_NONE); // Clear entry in filter map if an ID was already assigned before if (id_ != C_NONE) { diff --git a/src/tallies/filter_azimuthal.cpp b/src/tallies/filter_azimuthal.cpp index e77aa8bdc..6525f326d 100644 --- a/src/tallies/filter_azimuthal.cpp +++ b/src/tallies/filter_azimuthal.cpp @@ -34,14 +34,14 @@ void AzimuthalFilter::from_xml(pugi::xml_node node) this->set_bins(bins); } -void AzimuthalFilter::set_bins(gsl::span bins) +void AzimuthalFilter::set_bins(span bins) { // Clear existing bins bins_.clear(); bins_.reserve(bins.size()); // Copy bins, ensuring they are valid - for (gsl::index i = 0; i < bins.size(); ++i) { + for (int64_t i = 0; i < bins.size(); ++i) { if (i > 0 && bins[i] <= bins[i - 1]) { throw std::runtime_error { "Azimuthal bins must be monotonically increasing."}; diff --git a/src/tallies/filter_cell.cpp b/src/tallies/filter_cell.cpp index 794d2ae08..b545801ea 100644 --- a/src/tallies/filter_cell.cpp +++ b/src/tallies/filter_cell.cpp @@ -1,5 +1,7 @@ #include "openmc/tallies/filter_cell.h" +#include + #include #include "openmc/capi.h" @@ -25,7 +27,7 @@ void CellFilter::from_xml(pugi::xml_node node) this->set_cells(cells); } -void CellFilter::set_cells(gsl::span cells) +void CellFilter::set_cells(span cells) { // Clear existing cells cells_.clear(); @@ -34,8 +36,8 @@ void CellFilter::set_cells(gsl::span cells) // Update cells and mapping for (auto& index : cells) { - Expects(index >= 0); - Expects(index < model::cells.size()); + assert(index >= 0); + assert(index < model::cells.size()); cells_.push_back(index); map_[index] = cells_.size() - 1; } diff --git a/src/tallies/filter_cell_instance.cpp b/src/tallies/filter_cell_instance.cpp index 3e78a5bbe..0634175dd 100644 --- a/src/tallies/filter_cell_instance.cpp +++ b/src/tallies/filter_cell_instance.cpp @@ -1,5 +1,6 @@ #include "openmc/tallies/filter_cell_instance.h" +#include #include #include @@ -12,7 +13,7 @@ namespace openmc { -CellInstanceFilter::CellInstanceFilter(gsl::span instances) +CellInstanceFilter::CellInstanceFilter(span instances) { this->set_cell_instances(instances); } @@ -21,26 +22,26 @@ void CellInstanceFilter::from_xml(pugi::xml_node node) { // Get cell IDs/instances auto cells = get_node_array(node, "bins"); - Expects(cells.size() % 2 == 0); + assert(cells.size() % 2 == 0); // Convert into vector of CellInstance vector instances; - for (gsl::index i = 0; i < cells.size() / 2; ++i) { + for (int64_t i = 0; i < cells.size() / 2; ++i) { int32_t cell_id = cells[2 * i]; - gsl::index instance = cells[2 * i + 1]; + int64_t instance = cells[2 * i + 1]; auto search = model::cell_map.find(cell_id); if (search == model::cell_map.end()) { throw std::runtime_error {fmt::format( "Could not find cell {} specified on tally filter.", cell_id)}; } - gsl::index index = search->second; + int64_t index = search->second; instances.push_back({index, instance}); } this->set_cell_instances(instances); } -void CellInstanceFilter::set_cell_instances(gsl::span instances) +void CellInstanceFilter::set_cell_instances(span instances) { // Clear existing cells cell_instances_.clear(); @@ -50,8 +51,8 @@ void CellInstanceFilter::set_cell_instances(gsl::span instances) // Update cells and mapping for (auto& x : instances) { - Expects(x.index_cell >= 0); - Expects(x.index_cell < model::cells.size()); + assert(x.index_cell >= 0); + assert(x.index_cell < model::cells.size()); cell_instances_.push_back(x); cells_.insert(x.index_cell); map_[x] = cell_instances_.size() - 1; @@ -72,8 +73,8 @@ void CellInstanceFilter::set_cell_instances(gsl::span instances) void CellInstanceFilter::get_all_bins( const Particle& p, TallyEstimator estimator, FilterMatch& match) const { - gsl::index index_cell = p.lowest_coord().cell; - gsl::index instance = p.cell_instance(); + int64_t index_cell = p.lowest_coord().cell; + int64_t instance = p.cell_instance(); if (cells_.count(index_cell) > 0) { auto search = map_.find({index_cell, instance}); @@ -88,13 +89,13 @@ void CellInstanceFilter::get_all_bins( return; for (int i = 0; i < p.n_coord() - 1; i++) { - gsl::index index_cell = p.coord(i).cell; + int64_t index_cell = p.coord(i).cell; // if this cell isn't used on the filter, move on if (cells_.count(index_cell) == 0) continue; // if this cell is used in the filter, check the instance as well - gsl::index instance = cell_instance_at_level(p, i); + int64_t instance = cell_instance_at_level(p, i); auto search = map_.find({index_cell, instance}); if (search != map_.end()) { match.bins_.push_back(search->second); @@ -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}); - for (gsl::index i = 0; i < n; ++i) { + for (int64_t i = 0; i < n; ++i) { const auto& x = cell_instances_[i]; data(i, 0) = model::cells[x.index_cell]->id_; data(i, 1) = x.instance; diff --git a/src/tallies/filter_collision.cpp b/src/tallies/filter_collision.cpp index fbb186a23..c614c3c83 100644 --- a/src/tallies/filter_collision.cpp +++ b/src/tallies/filter_collision.cpp @@ -19,7 +19,7 @@ void CollisionFilter::from_xml(pugi::xml_node node) this->set_bins(bins); } -void CollisionFilter::set_bins(gsl::span bins) +void CollisionFilter::set_bins(span bins) { // Clear existing bins bins_.clear(); @@ -27,7 +27,7 @@ void CollisionFilter::set_bins(gsl::span bins) map_.clear(); // Copy bins - for (gsl::index i = 0; i < bins.size(); ++i) { + for (int64_t i = 0; i < bins.size(); ++i) { bins_.push_back(bins[i]); map_[bins[i]] = i; } diff --git a/src/tallies/filter_delayedgroup.cpp b/src/tallies/filter_delayedgroup.cpp index c6ec21766..01e39e554 100644 --- a/src/tallies/filter_delayedgroup.cpp +++ b/src/tallies/filter_delayedgroup.cpp @@ -11,7 +11,7 @@ void DelayedGroupFilter::from_xml(pugi::xml_node node) this->set_groups(groups); } -void DelayedGroupFilter::set_groups(gsl::span groups) +void DelayedGroupFilter::set_groups(span groups) { // Clear existing groups groups_.clear(); diff --git a/src/tallies/filter_distribcell.cpp b/src/tallies/filter_distribcell.cpp index c754dbd44..821e843da 100644 --- a/src/tallies/filter_distribcell.cpp +++ b/src/tallies/filter_distribcell.cpp @@ -1,5 +1,7 @@ #include "openmc/tallies/filter_distribcell.h" +#include + #include #include "openmc/cell.h" @@ -29,8 +31,8 @@ void DistribcellFilter::from_xml(pugi::xml_node node) void DistribcellFilter::set_cell(int32_t cell) { - Expects(cell >= 0); - Expects(cell < model::cells.size()); + assert(cell >= 0); + assert(cell < model::cells.size()); cell_ = cell; n_bins_ = model::cells[cell]->n_instances_; } diff --git a/src/tallies/filter_energy.cpp b/src/tallies/filter_energy.cpp index 4767dd175..0b954cce3 100644 --- a/src/tallies/filter_energy.cpp +++ b/src/tallies/filter_energy.cpp @@ -21,14 +21,14 @@ void EnergyFilter::from_xml(pugi::xml_node node) this->set_bins(bins); } -void EnergyFilter::set_bins(gsl::span bins) +void EnergyFilter::set_bins(span bins) { // Clear existing bins bins_.clear(); bins_.reserve(bins.size()); // Copy bins, ensuring they are valid - for (gsl::index i = 0; i < bins.size(); ++i) { + for (int64_t i = 0; i < bins.size(); ++i) { if (i > 0 && bins[i] <= bins[i - 1]) { throw std::runtime_error { "Energy bins must be monotonically increasing."}; @@ -46,7 +46,7 @@ void EnergyFilter::set_bins(gsl::span bins) if (!settings::run_CE) { if (n_bins_ == data::mg.num_energy_groups_) { matches_transport_groups_ = true; - for (gsl::index i = 0; i < n_bins_ + 1; ++i) { + for (int64_t i = 0; i < n_bins_ + 1; ++i) { if (data::mg.rev_energy_bins_[i] != bins_[i]) { matches_transport_groups_ = false; break; diff --git a/src/tallies/filter_energyfunc.cpp b/src/tallies/filter_energyfunc.cpp index 93ae24b2a..fc4ba0ff9 100644 --- a/src/tallies/filter_energyfunc.cpp +++ b/src/tallies/filter_energyfunc.cpp @@ -36,7 +36,7 @@ void EnergyFunctionFilter::from_xml(pugi::xml_node node) } void EnergyFunctionFilter::set_data( - gsl::span energy, gsl::span y) + span energy, span y) { // Check for consistent sizes with new data if (energy.size() != y.size()) { @@ -48,7 +48,7 @@ void EnergyFunctionFilter::set_data( y_.reserve(y.size()); // Copy over energy values, ensuring they are valid - for (gsl::index i = 0; i < energy.size(); ++i) { + for (int64_t i = 0; i < energy.size(); ++i) { if (i > 0 && energy[i] <= energy[i - 1]) { throw std::runtime_error { "Energy bins must be monotonically increasing."}; diff --git a/src/tallies/filter_material.cpp b/src/tallies/filter_material.cpp index 6669ab2fa..215c9af72 100644 --- a/src/tallies/filter_material.cpp +++ b/src/tallies/filter_material.cpp @@ -1,5 +1,7 @@ #include "openmc/tallies/filter_material.h" +#include + #include #include "openmc/capi.h" @@ -24,7 +26,7 @@ void MaterialFilter::from_xml(pugi::xml_node node) this->set_materials(mats); } -void MaterialFilter::set_materials(gsl::span materials) +void MaterialFilter::set_materials(span materials) { // Clear existing materials materials_.clear(); @@ -33,8 +35,8 @@ void MaterialFilter::set_materials(gsl::span materials) // Update materials and mapping for (auto& index : materials) { - Expects(index >= 0); - Expects(index < model::materials.size()); + assert(index >= 0); + assert(index < model::materials.size()); materials_.push_back(index); map_[index] = materials_.size() - 1; } diff --git a/src/tallies/filter_mesh.cpp b/src/tallies/filter_mesh.cpp index 03f7da978..4edfbec4b 100644 --- a/src/tallies/filter_mesh.cpp +++ b/src/tallies/filter_mesh.cpp @@ -1,7 +1,6 @@ #include "openmc/tallies/filter_mesh.h" #include -#include #include "openmc/capi.h" #include "openmc/constants.h" diff --git a/src/tallies/filter_mu.cpp b/src/tallies/filter_mu.cpp index 95bb3b210..63915a533 100644 --- a/src/tallies/filter_mu.cpp +++ b/src/tallies/filter_mu.cpp @@ -31,14 +31,14 @@ void MuFilter::from_xml(pugi::xml_node node) this->set_bins(bins); } -void MuFilter::set_bins(gsl::span bins) +void MuFilter::set_bins(span bins) { // Clear existing bins bins_.clear(); bins_.reserve(bins.size()); // Copy bins, ensuring they are valid - for (gsl::index i = 0; i < bins.size(); ++i) { + for (int64_t i = 0; i < bins.size(); ++i) { if (i > 0 && bins[i] <= bins[i - 1]) { throw std::runtime_error {"Mu bins must be monotonically increasing."}; } diff --git a/src/tallies/filter_particle.cpp b/src/tallies/filter_particle.cpp index 56d93a47f..eef1d1e63 100644 --- a/src/tallies/filter_particle.cpp +++ b/src/tallies/filter_particle.cpp @@ -18,7 +18,7 @@ void ParticleFilter::from_xml(pugi::xml_node node) this->set_particles(types); } -void ParticleFilter::set_particles(gsl::span particles) +void ParticleFilter::set_particles(span particles) { // Clear existing particles particles_.clear(); diff --git a/src/tallies/filter_polar.cpp b/src/tallies/filter_polar.cpp index d132ccf42..29be6a437 100644 --- a/src/tallies/filter_polar.cpp +++ b/src/tallies/filter_polar.cpp @@ -32,14 +32,14 @@ void PolarFilter::from_xml(pugi::xml_node node) this->set_bins(bins); } -void PolarFilter::set_bins(gsl::span bins) +void PolarFilter::set_bins(span bins) { // Clear existing bins bins_.clear(); bins_.reserve(bins.size()); // Copy bins, ensuring they are valid - for (gsl::index i = 0; i < bins.size(); ++i) { + for (int64_t i = 0; i < bins.size(); ++i) { if (i > 0 && bins[i] <= bins[i - 1]) { throw std::runtime_error {"Polar bins must be monotonically increasing."}; } diff --git a/src/tallies/filter_sph_harm.cpp b/src/tallies/filter_sph_harm.cpp index 359df379b..7441f32fb 100644 --- a/src/tallies/filter_sph_harm.cpp +++ b/src/tallies/filter_sph_harm.cpp @@ -1,9 +1,9 @@ #include "openmc/tallies/filter_sph_harm.h" +#include #include // For pair #include -#include #include "openmc/capi.h" #include "openmc/error.h" @@ -30,7 +30,7 @@ void SphericalHarmonicsFilter::set_order(int order) n_bins_ = (order_ + 1) * (order_ + 1); } -void SphericalHarmonicsFilter::set_cosine(gsl::cstring_span cosine) +void SphericalHarmonicsFilter::set_cosine(const std::string& cosine) { if (cosine == "scatter") { cosine_ = SphericalHarmonicsCosine::scatter; @@ -39,7 +39,7 @@ void SphericalHarmonicsFilter::set_cosine(gsl::cstring_span cosine) } else { throw std::invalid_argument {fmt::format("Unrecognized cosine type, \"{}\" " "in spherical harmonics filter", - gsl::to_string(cosine))}; + cosine)}; } } @@ -87,7 +87,7 @@ void SphericalHarmonicsFilter::to_statepoint(hid_t filter_group) const std::string SphericalHarmonicsFilter::text_label(int bin) const { - Expects(bin >= 0 && bin < n_bins_); + assert(bin >= 0 && bin < n_bins_); for (int n = 0; n < order_ + 1; n++) { if (bin < (n + 1) * (n + 1)) { int m = (bin - n * n) - n; diff --git a/src/tallies/filter_surface.cpp b/src/tallies/filter_surface.cpp index 1fbf8d44e..82f3d7178 100644 --- a/src/tallies/filter_surface.cpp +++ b/src/tallies/filter_surface.cpp @@ -1,5 +1,7 @@ #include "openmc/tallies/filter_surface.h" +#include + #include #include "openmc/error.h" @@ -26,7 +28,7 @@ void SurfaceFilter::from_xml(pugi::xml_node node) this->set_surfaces(surfaces); } -void SurfaceFilter::set_surfaces(gsl::span surfaces) +void SurfaceFilter::set_surfaces(span surfaces) { // Clear existing surfaces surfaces_.clear(); @@ -35,8 +37,8 @@ void SurfaceFilter::set_surfaces(gsl::span surfaces) // Update surfaces and mapping for (auto& index : surfaces) { - Expects(index >= 0); - Expects(index < model::surfaces.size()); + assert(index >= 0); + assert(index < model::surfaces.size()); surfaces_.push_back(index); map_[index] = surfaces_.size() - 1; } diff --git a/src/tallies/filter_time.cpp b/src/tallies/filter_time.cpp index 0378aa28d..948d1347a 100644 --- a/src/tallies/filter_time.cpp +++ b/src/tallies/filter_time.cpp @@ -21,7 +21,7 @@ void TimeFilter::from_xml(pugi::xml_node node) this->set_bins(bins); } -void TimeFilter::set_bins(gsl::span bins) +void TimeFilter::set_bins(span bins) { // Clear existing bins bins_.clear(); diff --git a/src/tallies/filter_universe.cpp b/src/tallies/filter_universe.cpp index 96ad0212d..48114cfad 100644 --- a/src/tallies/filter_universe.cpp +++ b/src/tallies/filter_universe.cpp @@ -1,5 +1,7 @@ #include "openmc/tallies/filter_universe.h" +#include + #include #include "openmc/cell.h" @@ -24,7 +26,7 @@ void UniverseFilter::from_xml(pugi::xml_node node) this->set_universes(universes); } -void UniverseFilter::set_universes(gsl::span universes) +void UniverseFilter::set_universes(span universes) { // Clear existing universes universes_.clear(); @@ -33,8 +35,8 @@ void UniverseFilter::set_universes(gsl::span universes) // Update universes and mapping for (auto& index : universes) { - Expects(index >= 0); - Expects(index < model::universes.size()); + assert(index >= 0); + assert(index < model::universes.size()); universes_.push_back(index); map_[index] = universes_.size() - 1; } diff --git a/src/tallies/filter_zernike.cpp b/src/tallies/filter_zernike.cpp index eb4c8bdfd..af5b595aa 100644 --- a/src/tallies/filter_zernike.cpp +++ b/src/tallies/filter_zernike.cpp @@ -1,11 +1,11 @@ #include "openmc/tallies/filter_zernike.h" +#include #include #include #include // For pair #include -#include #include "openmc/capi.h" #include "openmc/error.h" @@ -57,7 +57,7 @@ void ZernikeFilter::to_statepoint(hid_t filter_group) const std::string ZernikeFilter::text_label(int bin) const { - Expects(bin >= 0 && bin < n_bins_); + assert(bin >= 0 && bin < n_bins_); for (int n = 0; n < order_ + 1; n++) { int last = (n + 1) * (n + 2) / 2; if (bin < last) { diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index 4f33abf6b..96d684f71 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -39,7 +39,8 @@ #include #include // for max -#include // for size_t +#include +#include // for size_t #include namespace openmc { @@ -140,7 +141,7 @@ Tally::Tally(pugi::xml_node node) // Check for the presence of certain filter types bool has_energyout = energyout_filter_ >= 0; int particle_filter_index = C_NONE; - for (gsl::index j = 0; j < filters_.size(); ++j) { + for (int64_t j = 0; j < filters_.size(); ++j) { int i_filter = filters_[j]; const auto& f = model::tally_filters[i_filter].get(); @@ -345,7 +346,7 @@ Tally* Tally::create(int32_t id) void Tally::set_id(int32_t id) { - Expects(id >= 0 || id == C_NONE); + assert(id >= 0 || id == C_NONE); // Clear entry in tally map if an ID was already assigned before if (id_ != C_NONE) { @@ -401,7 +402,7 @@ bool Tally::has_filter(FilterType filter_type) const return false; } -void Tally::set_filters(gsl::span filters) +void Tally::set_filters(span filters) { // Clear old data. filters_.clear(); @@ -1370,7 +1371,7 @@ extern "C" int openmc_tally_set_filters( try { // Convert indices to filter pointers vector filters; - for (gsl::index i = 0; i < n; ++i) { + for (int64_t i = 0; i < n; ++i) { int32_t i_filt = indices[i]; filters.push_back(model::tally_filters.at(i_filt).get()); } diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index 19ae7c077..9daf21f75 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -1,6 +1,7 @@ #include "openmc/weight_windows.h" #include +#include #include #include #include @@ -32,7 +33,6 @@ #include "openmc/xml_interface.h" #include -#include namespace openmc { @@ -289,7 +289,7 @@ void WeightWindows::allocate_ww_bounds() void WeightWindows::set_id(int32_t id) { - Expects(id >= 0 || id == C_NONE); + assert(id >= 0 || id == C_NONE); // Clear entry in mesh map in case one was already assigned if (id_ != C_NONE) { @@ -317,7 +317,7 @@ void WeightWindows::set_id(int32_t id) variance_reduction::ww_map[id] = index_; } -void WeightWindows::set_energy_bounds(gsl::span bounds) +void WeightWindows::set_energy_bounds(span bounds) { energy_bounds_.clear(); energy_bounds_.insert(energy_bounds_.begin(), bounds.begin(), bounds.end()); @@ -452,7 +452,7 @@ void WeightWindows::set_bounds( } void WeightWindows::set_bounds( - gsl::span lower_bounds, gsl::span upper_bounds) + span lower_bounds, span upper_bounds) { check_bounds(lower_bounds, upper_bounds); auto shape = this->bounds_size(); @@ -466,8 +466,7 @@ void WeightWindows::set_bounds( xt::adapt(upper_bounds.data(), upper_ww_.shape()); } -void WeightWindows::set_bounds( - gsl::span lower_bounds, double ratio) +void WeightWindows::set_bounds(span lower_bounds, double ratio) { this->check_bounds(lower_bounds); diff --git a/vendor/gsl-lite b/vendor/gsl-lite deleted file mode 160000 index 913e86d49..000000000 --- a/vendor/gsl-lite +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 913e86d49c6a1acca980f4e325378f9dc393493a From 7638661fadaa30efac059374c8191d1c20c274b4 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 20 Feb 2025 13:07:07 -0600 Subject: [PATCH 266/671] Add nuclides_to_ignore argument on Model export methods (#3309) Co-authored-by: Patrick Shriwise --- openmc/model/model.py | 18 +++++++++++++----- tests/unit_tests/test_model.py | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index 5e93d9804..c7f358ce8 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -441,7 +441,8 @@ class Model: depletion_operator.cleanup_when_done = True depletion_operator.finalize() - def export_to_xml(self, directory='.', remove_surfs=False): + def export_to_xml(self, directory: PathLike = '.', remove_surfs: bool = False, + nuclides_to_ignore: Iterable[str] | None = None): """Export model to separate XML files. Parameters @@ -454,6 +455,9 @@ class Model: exporting. .. versionadded:: 0.13.1 + nuclides_to_ignore : list of str + Nuclides to ignore when exporting to XML. + """ # Create directory if required d = Path(directory) @@ -467,18 +471,19 @@ class Model: # for all materials in the geometry and use that to automatically build # a collection. if self.materials: - self.materials.export_to_xml(d) + self.materials.export_to_xml(d, nuclides_to_ignore=nuclides_to_ignore) else: materials = openmc.Materials(self.geometry.get_all_materials() .values()) - materials.export_to_xml(d) + materials.export_to_xml(d, nuclides_to_ignore=nuclides_to_ignore) if self.tallies: self.tallies.export_to_xml(d) if self.plots: self.plots.export_to_xml(d) - def export_to_model_xml(self, path='model.xml', remove_surfs=False): + def export_to_model_xml(self, path: PathLike = 'model.xml', remove_surfs: bool = False, + nuclides_to_ignore: Iterable[str] | None = None): """Export model to a single XML file. .. versionadded:: 0.13.3 @@ -491,6 +496,8 @@ class Model: remove_surfs : bool Whether or not to remove redundant surfaces from the geometry when exporting. + nuclides_to_ignore : list of str + Nuclides to ignore when exporting to XML. """ xml_path = Path(path) @@ -536,7 +543,8 @@ class Model: fh.write("\n") # Write the materials collection to the open XML file first. # This will write the XML header also - materials._write_xml(fh, False, level=1) + materials._write_xml(fh, False, level=1, + nuclides_to_ignore=nuclides_to_ignore) # Write remaining elements as a tree fh.write(ET.tostring(geometry_element, encoding="unicode")) fh.write(ET.tostring(settings_element, encoding="unicode")) diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 4b567c56d..4d7d5be2b 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -593,6 +593,24 @@ def test_single_xml_exec(run_in_tmpdir): pincell_model.run(path='subdir') +def test_nuclides_to_ignore(run_in_tmpdir, pin_model_attributes): + """Test nuclides_to_ignore when exporting a model XML""" + materials, geometry, settings = pin_model_attributes[:3] + model = openmc.Model(geometry=geometry, settings=settings) + + # grab one of the nuclides present in this model as a test + test_nuclide = list(materials[0].get_nuclides())[0] + + # exclude the test nuclide from the XML file during export + model.export_to_model_xml(nuclides_to_ignore=[test_nuclide]) + + # ensure that the nuclide doesn't appear after reading in + # the resulting XML model + xml_model = openmc.Model.from_model_xml() + for material in xml_model.materials: + assert test_nuclide not in material.get_nuclides() + + def test_model_plot(): # plots the geometry with source location and checks the resulting # matplotlib includes the correct coordinates for the scatter plot for all From 2b788ea6e0cc3ffa31daec33584214bbc667d622 Mon Sep 17 00:00:00 2001 From: Zoe Prieto <101403129+zoeprieto@users.noreply.github.com> Date: Thu, 20 Feb 2025 22:51:05 -0300 Subject: [PATCH 267/671] Streamline use of CompositeSurface with SurfaceFilter (#3167) Co-authored-by: Patrick Shriwise Co-authored-by: Paul Romano --- openmc/model/surface_composite.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index 88433118b..3a17b6169 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -33,6 +33,10 @@ class CompositeSurface(ABC): setattr(surf, name, s.rotate(rotation, pivot, order, inplace)) return surf + @property + def component_surfaces(self): + return [getattr(self, name) for name in self._surface_names] + @property def boundary_type(self): return getattr(self, self._surface_names[0]).boundary_type From d643ad0c416e91f61ff736438faff19009b88c70 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 21 Feb 2025 11:47:38 -0600 Subject: [PATCH 268/671] Simulation of decay photons through the D1S method (#3235) D1S FTW! --- CMakeLists.txt | 2 + docs/source/conf.py | 2 +- docs/source/pythonapi/base.rst | 1 + docs/source/pythonapi/capi.rst | 1 + docs/source/pythonapi/deplete.rst | 12 + docs/source/usersguide/decay_sources.rst | 92 +++++++ docs/source/usersguide/index.rst | 2 +- include/openmc/chain.h | 97 +++++++ include/openmc/endf.h | 4 + include/openmc/particle.h | 3 +- include/openmc/particle_data.h | 6 + include/openmc/reaction.h | 4 +- include/openmc/reaction_product.h | 6 + include/openmc/settings.h | 1 + include/openmc/tallies/filter.h | 1 + .../openmc/tallies/filter_parent_nuclide.h | 56 ++++ openmc/deplete/abc.py | 111 ++++---- openmc/deplete/d1s.py | 246 ++++++++++++++++++ openmc/filter.py | 31 ++- openmc/lib/core.py | 1 + openmc/lib/filter.py | 12 +- openmc/material.py | 5 +- openmc/settings.py | 25 +- src/chain.cpp | 116 +++++++++ src/initialize.cpp | 21 +- src/nuclide.cpp | 27 +- src/particle.cpp | 6 +- src/physics.cpp | 33 ++- src/reaction.cpp | 34 ++- src/reaction_product.cpp | 23 ++ src/settings.cpp | 6 + src/tallies/filter.cpp | 3 + src/tallies/filter_parent_nuclide.cpp | 79 ++++++ tests/chain_ni.xml | 173 ++++++++++++ tests/regression_tests/surface_source/test.py | 25 +- tests/unit_tests/test_d1s.py | 132 ++++++++++ 36 files changed, 1298 insertions(+), 101 deletions(-) create mode 100644 docs/source/usersguide/decay_sources.rst create mode 100644 include/openmc/chain.h create mode 100644 include/openmc/tallies/filter_parent_nuclide.h create mode 100644 openmc/deplete/d1s.py create mode 100644 src/chain.cpp create mode 100644 src/tallies/filter_parent_nuclide.cpp create mode 100644 tests/chain_ni.xml create mode 100644 tests/unit_tests/test_d1s.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 35694a14b..f76fcb247 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -342,6 +342,7 @@ list(APPEND libopenmc_SOURCES src/boundary_condition.cpp src/bremsstrahlung.cpp src/cell.cpp + src/chain.cpp src/cmfd_solver.cpp src/cross_sections.cpp src/dagmc.cpp @@ -424,6 +425,7 @@ list(APPEND libopenmc_SOURCES src/tallies/filter_meshsurface.cpp src/tallies/filter_mu.cpp src/tallies/filter_musurface.cpp + src/tallies/filter_parent_nuclide.cpp src/tallies/filter_particle.cpp src/tallies/filter_polar.cpp src/tallies/filter_sph_harm.cpp diff --git a/docs/source/conf.py b/docs/source/conf.py index 726269f0b..60bd406fb 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -52,7 +52,7 @@ if not on_rtd: templates_path = ['_templates'] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = {'.rst': 'restructuredtext'} # The encoding of source files. #source_encoding = 'utf-8' diff --git a/docs/source/pythonapi/base.rst b/docs/source/pythonapi/base.rst index d6d04444f..bfd942727 100644 --- a/docs/source/pythonapi/base.rst +++ b/docs/source/pythonapi/base.rst @@ -145,6 +145,7 @@ Constructing Tallies openmc.TimeFilter openmc.ZernikeFilter openmc.ZernikeRadialFilter + openmc.ParentNuclideFilter openmc.ParticleFilter openmc.RegularMesh openmc.RectilinearMesh diff --git a/docs/source/pythonapi/capi.rst b/docs/source/pythonapi/capi.rst index 9ceff83fd..c8e0e874d 100644 --- a/docs/source/pythonapi/capi.rst +++ b/docs/source/pythonapi/capi.rst @@ -79,6 +79,7 @@ Classes MeshSurfaceFilter MuFilter Nuclide + ParentNuclideFilter ParticleFilter PolarFilter RectilinearMesh diff --git a/docs/source/pythonapi/deplete.rst b/docs/source/pythonapi/deplete.rst index 8731a9a13..d7a779b12 100644 --- a/docs/source/pythonapi/deplete.rst +++ b/docs/source/pythonapi/deplete.rst @@ -287,3 +287,15 @@ the following abstract base classes: abc.Integrator abc.SIIntegrator abc.DepSystemSolver + +D1S Functions +------------- + +.. autosummary:: + :toctree: generated + :nosignatures: + :template: myfunction.rst + + d1s.prepare_tallies + d1s.time_correction_factors + d1s.apply_time_correction diff --git a/docs/source/usersguide/decay_sources.rst b/docs/source/usersguide/decay_sources.rst new file mode 100644 index 000000000..e61220696 --- /dev/null +++ b/docs/source/usersguide/decay_sources.rst @@ -0,0 +1,92 @@ +.. usersguide_decay_sources: + +============= +Decay Sources +============= + +Through the :ref:`depletion ` capabilities in OpenMC, it +is possible to simulate radiation emitted from the decay of activated materials. +For fusion energy systems, this is commonly done using what is known as the +`rigorous 2-step `_ (R2S) method. +In this method, a neutron transport calculation is used to determine the neutron +flux and reaction rates over a cell- or mesh-based spatial discretization of the +model. Then, the neutron flux in each discrete region is used to predict the +activated material composition using a depletion solver. Finally, a photon +transport calculation with a source based on the activity and energy spectrum of +the activated materials is used to determine a desired physical response (e.g., +a dose rate) at one or more locations of interest. + +Once a depletion simulation has been completed in OpenMC, the intrinsic decay +source can be determined as follows. First the activated material composition +can be determined using the :class:`openmc.deplete.Results` object. Indexing an +instance of this class with the timestep index returns a +:class:`~openmc.deplete.StepResult` object, which itself has a +:meth:`~openmc.deplete.StepResult.get_material` method. Once the activated +:class:`~openmc.Material` has been obtained, the +:meth:`~openmc.Material.get_decay_photon_energy` method will give the energy +spectrum of the decay photon source. The integral of the spectrum also indicates +the intensity of the source in units of [Bq]. Altogether, the workflow looks as +follows:: + + results = openmc.deplete.Results("depletion_results.h5") + + # Get results at last timestep + step = results[-1] + + # Get activated material composition for ID=1 + activated_mat = step.get_material('1') + + # Determine photon source + photon_energy = activated_mat.get_decay_photon_energy() + +By default, the :meth:`~openmc.Material.get_decay_photon_energy` method will +eliminate spectral lines with very low intensity, but this behavior can be +configured with the ``clip_tolerance`` argument. + +Direct 1-Step (D1S) Calculations +================================ + +OpenMC also includes built-in capability for performing shutdown dose rate +calculations using the `direct 1-step `_ +(D1S) method. In this method, a single coupled neutron--photon transport +calculation is used where the prompt photon production is replaced with photons +produced from the decay of radionuclides in an activated material. To obtain +properly scaled results, it is also necessary to apply time correction factors. +A normal neutron transport calculation can be extended to a D1S calculation with +a few helper functions. First, import the ``d1s`` submodule, which is part of +:mod:`openmc.deplete`:: + + from openmc.deplete import d1s + +First, you need to instruct OpenMC to use decay photon data instead of prompt +photon data. This is done with an attribute on the :class:`~openmc.Settings` +class:: + + model = openmc.Model() + ... + model.settings.use_decay_photons = True + +To prepare any tallies for use of the D1S method, you should call the +:func:`~openmc.deplete.d1s.prepare_tallies` function, which adds a +:class:`openmc.ParentNuclideFilter` (used later for assigning time correction +factors) to any applicable tally and returns a list of possible radionuclides +based on the :ref:`chain file `. Once the tallies are prepared, +the model can be simulated:: + + output_path = model.run() + +Finally, the time correction factors need to be computed and applied to the +relevant tallies. This can be done with the aid of the +:func:`~openmc.deplete.d1s.time_correction_factors` and +:func:`~openmc.deplete.d1s.apply_time_correction` functions:: + + # Compute time correction factors based on irradiation schedule + factors = d1s.time_correction_factors(nuclides, timesteps, source_rates) + + # Get tally from statepoint + with openmc.StatePoint(output_path) as sp: + dose_tally = sp.get_tally(name='dose tally') + + # Apply time correction factors + tally = d1s.apply_time_correction(tally, factors, time_index) + diff --git a/docs/source/usersguide/index.rst b/docs/source/usersguide/index.rst index 74651c011..aef9b1a1c 100644 --- a/docs/source/usersguide/index.rst +++ b/docs/source/usersguide/index.rst @@ -21,6 +21,7 @@ essential aspects of using OpenMC to perform simulations. tallies plots depletion + decay_sources scripts processing parallel @@ -28,4 +29,3 @@ essential aspects of using OpenMC to perform simulations. variance_reduction random_ray troubleshoot - \ No newline at end of file diff --git a/include/openmc/chain.h b/include/openmc/chain.h new file mode 100644 index 000000000..a3bc6f3a3 --- /dev/null +++ b/include/openmc/chain.h @@ -0,0 +1,97 @@ +//! \file chain.h +//! \brief Depletion chain and associated information + +#ifndef OPENMC_CHAIN_H +#define OPENMC_CHAIN_H + +#include +#include +#include + +#include "pugixml.hpp" + +#include "openmc/angle_energy.h" // for AngleEnergy +#include "openmc/distribution.h" // for UPtrDist +#include "openmc/memory.h" // for unique_ptr +#include "openmc/vector.h" + +namespace openmc { + +//============================================================================== +// Data for a nuclide in the depletion chain +//============================================================================== + +class ChainNuclide { +public: + // Types + struct Product { + std::string name; //!< Reaction product name + double branching_ratio; //!< Branching ratio + }; + + // Constructors, destructors + ChainNuclide(pugi::xml_node node); + ~ChainNuclide(); + + //! Compute the decay constant for the nuclide + //! \return Decay constant in [1/s] + double decay_constant() const { return std::log(2.0) / half_life_; } + + const Distribution* photon_energy() const { return photon_energy_.get(); } + const std::unordered_map>& reaction_products() const + { + return reaction_products_; + } + +private: + // Data members + std::string name_; //!< Name of nuclide + double half_life_ {0.0}; //!< Half-life in [s] + double decay_energy_ {0.0}; //!< Decay energy in [eV] + std::unordered_map> + reaction_products_; //!< Map of MT to reaction products + UPtrDist photon_energy_; //!< Decay photon energy distribution +}; + +//============================================================================== +// Angle-energy distribution for decay photon +//============================================================================== + +class DecayPhotonAngleEnergy : public AngleEnergy { +public: + explicit DecayPhotonAngleEnergy(const Distribution* dist) + : photon_energy_(dist) + {} + + //! Sample distribution for an angle and energy + //! \param[in] E_in Incoming energy in [eV] + //! \param[out] E_out Outgoing energy in [eV] + //! \param[out] mu Outgoing cosine with respect to current direction + //! \param[inout] seed Pseudorandom seed pointer + void sample( + double E_in, double& E_out, double& mu, uint64_t* seed) const override; + +private: + const Distribution* photon_energy_; +}; + +//============================================================================== +// Global variables +//============================================================================== + +namespace data { + +extern std::unordered_map chain_nuclide_map; +extern vector> chain_nuclides; + +} // namespace data + +//============================================================================== +// Non-member functions +//============================================================================== + +void read_chain_file_xml(); + +} // namespace openmc + +#endif // OPENMC_CHAIN_H diff --git a/include/openmc/endf.h b/include/openmc/endf.h index e580874b4..4a737eb88 100644 --- a/include/openmc/endf.h +++ b/include/openmc/endf.h @@ -53,6 +53,10 @@ public: //! \param[in] dset Dataset containing coefficients explicit Polynomial(hid_t dset); + //! Construct polynomial from coefficients + //! \param[in] coef Polynomial coefficients + explicit Polynomial(vector coef) : coef_(coef) {} + //! Evaluate the polynomials //! \param[in] x independent variable //! \return Polynomial evaluated at x diff --git a/include/openmc/particle.h b/include/openmc/particle.h index aaac864e1..0f37719b9 100644 --- a/include/openmc/particle.h +++ b/include/openmc/particle.h @@ -47,7 +47,8 @@ public: //! \param u Direction of the secondary particle //! \param E Energy of the secondary particle in [eV] //! \param type Particle type - void create_secondary(double wgt, Direction u, double E, ParticleType type); + //! \return Whether a secondary particle was created + bool create_secondary(double wgt, Direction u, double E, ParticleType type); //! split a particle // diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index e2fac79e3..6b318385e 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -49,6 +49,9 @@ struct SourceSite { int delayed_group {0}; int surf_id {0}; ParticleType particle; + + // Extra attributes that don't show up in source written to file + int parent_nuclide {-1}; int64_t parent_id; int64_t progeny_id; }; @@ -441,6 +444,7 @@ private: int event_nuclide_; int event_mt_; int delayed_group_ {0}; + int parent_nuclide_ {-1}; int n_bank_ {0}; double bank_second_E_ {0.0}; @@ -555,6 +559,8 @@ public: const int& event_nuclide() const { return event_nuclide_; } int& event_mt() { return event_mt_; } // MT number of collision int& delayed_group() { return delayed_group_; } // delayed group + const int& parent_nuclide() const { return parent_nuclide_; } + int& parent_nuclide() { return parent_nuclide_; } // Parent nuclide // Post-collision data double& bank_second_E() diff --git a/include/openmc/reaction.h b/include/openmc/reaction.h index d5f04d136..3314d1866 100644 --- a/include/openmc/reaction.h +++ b/include/openmc/reaction.h @@ -26,7 +26,9 @@ public: //! Construct reaction from HDF5 data //! \param[in] group HDF5 group containing reaction data //! \param[in] temperatures Desired temperatures for cross sections - explicit Reaction(hid_t group, const vector& temperatures); + //! \param[in] name Name of the nuclide + explicit Reaction( + hid_t group, const vector& temperatures, std::string name); //! Calculate cross section given temperautre/grid index, interpolation factor // diff --git a/include/openmc/reaction_product.h b/include/openmc/reaction_product.h index ce4fa8fc7..4fbbc1b62 100644 --- a/include/openmc/reaction_product.h +++ b/include/openmc/reaction_product.h @@ -7,6 +7,7 @@ #include "hdf5.h" #include "openmc/angle_energy.h" +#include "openmc/chain.h" #include "openmc/endf.h" #include "openmc/memory.h" // for unique_ptr #include "openmc/particle.h" @@ -37,6 +38,10 @@ public: //! \param[in] group HDF5 group containing data explicit ReactionProduct(hid_t group); + //! Construct reaction product for decay photon from chain nuclide product + //! \param[in] product Chain nuclide product + explicit ReactionProduct(const ChainNuclide::Product& product); + //! Sample an outgoing angle and energy //! \param[in] E_in Incoming energy in [eV] //! \param[out] E_out Outgoing energy in [eV] @@ -50,6 +55,7 @@ public: unique_ptr yield_; //!< Yield as a function of energy vector applicability_; //!< Applicability of distribution vector distribution_; //!< Secondary angle-energy distribution + int parent_nuclide_ = -1; //!< Index of chain nuclide that is parent }; } // namespace openmc diff --git a/include/openmc/settings.h b/include/openmc/settings.h index 9a4ce56ec..12835fdef 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -67,6 +67,7 @@ extern bool trigger_predict; //!< predict batches for triggers? extern bool uniform_source_sampling; //!< sample sources uniformly? extern bool ufs_on; //!< uniform fission site method on? extern bool urr_ptables_on; //!< use unresolved resonance prob. tables? +extern bool use_decay_photons; //!< use decay photons for D1S extern "C" bool weight_windows_on; //!< are weight windows are enabled? extern bool weight_window_checkpoint_surface; //!< enable weight window check //!< upon surface crossing? diff --git a/include/openmc/tallies/filter.h b/include/openmc/tallies/filter.h index 31dd609ed..3e982d0cf 100644 --- a/include/openmc/tallies/filter.h +++ b/include/openmc/tallies/filter.h @@ -36,6 +36,7 @@ enum class FilterType { MESH_SURFACE, MU, MUSURFACE, + PARENT_NUCLIDE, PARTICLE, POLAR, SPHERICAL_HARMONICS, diff --git a/include/openmc/tallies/filter_parent_nuclide.h b/include/openmc/tallies/filter_parent_nuclide.h new file mode 100644 index 000000000..53f8a5fa4 --- /dev/null +++ b/include/openmc/tallies/filter_parent_nuclide.h @@ -0,0 +1,56 @@ +#ifndef OPENMC_TALLIES_FILTER_PARENT_NUCLIDE_H +#define OPENMC_TALLIES_FILTER_PARENT_NUCLIDE_H + +#include +#include + +#include "openmc/span.h" +#include "openmc/tallies/filter.h" +#include "openmc/vector.h" + +namespace openmc { + +//============================================================================== +//! Bins events by parent nuclide (for decay photons) +//============================================================================== + +class ParentNuclideFilter : public Filter { +public: + //---------------------------------------------------------------------------- + // Constructors, destructors + + ~ParentNuclideFilter() = default; + + //---------------------------------------------------------------------------- + // Methods + + std::string type_str() const override { return "parentnuclide"; } + FilterType type() const override { return FilterType::PARENT_NUCLIDE; } + + 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 + + vector bins_; + vector nuclides_; + + std::unordered_map map_; +}; + +} // namespace openmc +#endif // OPENMC_TALLIES_FILTER_PARENT_NUCLIDE_H diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 784023f26..fc98239a5 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -41,6 +41,63 @@ _SECONDS_PER_HOUR = 60*60 _SECONDS_PER_DAY = 24*60*60 _SECONDS_PER_JULIAN_YEAR = 365.25*24*60*60 + +def _normalize_timesteps( + timesteps: Sequence[float] | Sequence[tuple[float, str]], + source_rates: float | Sequence[float], + timestep_units: str = 's', + operator: TransportOperator | None = None, +): + if not isinstance(source_rates, Sequence): + # Ensure that rate is single value if that is the case + source_rates = [source_rates] * len(timesteps) + + if len(source_rates) != len(timesteps): + raise ValueError( + "Number of time steps ({}) != number of powers ({})".format( + len(timesteps), len(source_rates))) + + # Get list of times / units + if isinstance(timesteps[0], Sequence): + times, units = zip(*timesteps) + else: + times = timesteps + units = [timestep_units] * len(timesteps) + + # Determine number of seconds for each timestep + seconds = [] + for timestep, unit, rate in zip(times, units, source_rates): + # Make sure values passed make sense + check_type('timestep', timestep, Real) + check_greater_than('timestep', timestep, 0.0, False) + check_type('timestep units', unit, str) + check_type('source rate', rate, Real) + check_greater_than('source rate', rate, 0.0, True) + + if unit in ('s', 'sec'): + seconds.append(timestep) + elif unit in ('min', 'minute'): + seconds.append(timestep*_SECONDS_PER_MINUTE) + elif unit in ('h', 'hr', 'hour'): + seconds.append(timestep*_SECONDS_PER_HOUR) + elif unit in ('d', 'day'): + seconds.append(timestep*_SECONDS_PER_DAY) + elif unit in ('a', 'year'): + seconds.append(timestep*_SECONDS_PER_JULIAN_YEAR) + elif unit.lower() == 'mwd/kg': + watt_days_per_kg = 1e6*timestep + kilograms = 1e-3*operator.heavy_metal + if rate == 0.0: + raise ValueError("Cannot specify a timestep in [MWd/kg] when" + " the power is zero.") + days = watt_days_per_kg * kilograms / rate + seconds.append(days*_SECONDS_PER_DAY) + else: + raise ValueError(f"Invalid timestep unit '{unit}'") + + return (np.asarray(seconds), np.asarray(source_rates)) + + OperatorResult = namedtuple('OperatorResult', ['k', 'rates']) OperatorResult.__doc__ = """\ Result of applying transport operator @@ -551,10 +608,10 @@ class Integrator(ABC): def __init__( self, operator: TransportOperator, - timesteps: Sequence[float], + timesteps: Sequence[float] | Sequence[tuple[float, str]], power: Optional[Union[float, Sequence[float]]] = None, power_density: Optional[Union[float, Sequence[float]]] = None, - source_rates: Optional[Sequence[float]] = None, + source_rates: Optional[Union[float, Sequence[float]]] = None, timestep_units: str = 's', solver: str = "cram48" ): @@ -582,53 +639,9 @@ class Integrator(ABC): elif source_rates is None: raise ValueError("Either power, power_density, or source_rates must be set") - if not isinstance(source_rates, Iterable): - # Ensure that rate is single value if that is the case - source_rates = [source_rates] * len(timesteps) - - if len(source_rates) != len(timesteps): - raise ValueError( - "Number of time steps ({}) != number of powers ({})".format( - len(timesteps), len(source_rates))) - - # Get list of times / units - if isinstance(timesteps[0], Iterable): - times, units = zip(*timesteps) - else: - times = timesteps - units = [timestep_units] * len(timesteps) - - # Determine number of seconds for each timestep - seconds = [] - for timestep, unit, rate in zip(times, units, source_rates): - # Make sure values passed make sense - check_type('timestep', timestep, Real) - check_greater_than('timestep', timestep, 0.0, False) - check_type('timestep units', unit, str) - check_type('source rate', rate, Real) - check_greater_than('source rate', rate, 0.0, True) - - if unit in ('s', 'sec'): - seconds.append(timestep) - elif unit in ('min', 'minute'): - seconds.append(timestep*_SECONDS_PER_MINUTE) - elif unit in ('h', 'hr', 'hour'): - seconds.append(timestep*_SECONDS_PER_HOUR) - elif unit in ('d', 'day'): - seconds.append(timestep*_SECONDS_PER_DAY) - elif unit in ('a', 'year'): - seconds.append(timestep*_SECONDS_PER_JULIAN_YEAR) - elif unit.lower() == 'mwd/kg': - watt_days_per_kg = 1e6*timestep - kilograms = 1e-3*operator.heavy_metal - if rate == 0.0: - raise ValueError("Cannot specify a timestep in [MWd/kg] when" - " the power is zero.") - days = watt_days_per_kg * kilograms / rate - seconds.append(days*_SECONDS_PER_DAY) - else: - raise ValueError(f"Invalid timestep unit '{unit}'") - + # Normalize timesteps and source rates + seconds, source_rates = _normalize_timesteps( + timesteps, source_rates, timestep_units, operator) self.timesteps = np.asarray(seconds) self.source_rates = np.asarray(source_rates) diff --git a/openmc/deplete/d1s.py b/openmc/deplete/d1s.py new file mode 100644 index 000000000..22fc6ec23 --- /dev/null +++ b/openmc/deplete/d1s.py @@ -0,0 +1,246 @@ +"""D1S module + +This module contains functionality to support the direct 1-step (D1S) method for +shutdown dose rate calculations. + +""" + +from copy import deepcopy +from typing import Sequence +from math import log, prod + +import numpy as np + +import openmc +from openmc.data import half_life +from .abc import _normalize_timesteps +from .chain import Chain + + +def get_radionuclides(model: openmc.Model, chain_file: str | None = None) -> list[str]: + """Determine all radionuclides that can be produced during D1S. + + Parameters + ---------- + model : openmc.Model + Model that should be used for determining what nuclides are present + chain_file : str, optional + Which chain file to use for inspecting decay data. If None is passed, + defaults to ``openmc.config['chain_file']`` + + Returns + ------- + List of nuclide names + + """ + # Determine what nuclides appear in model + model_nuclides = set(model.geometry.get_all_nuclides()) + + # Load chain file + if chain_file is None: + chain_file = openmc.config['chain_file'] + chain = Chain.from_xml(chain_file) + + radionuclides = set() + for nuclide in chain.nuclides: + # Restrict to set of nuclides present in model + if nuclide.name not in model_nuclides: + continue + + # Loop over reactions and add any targets that are unstable + for rx_tuple in nuclide.reactions: + target = rx_tuple.target + if target is None: + continue + target_nuclide = chain[target] + if target_nuclide.half_life is not None: + radionuclides.add(target_nuclide.name) + + return list(radionuclides) + + +def time_correction_factors( + nuclides: list[str], + timesteps: Sequence[float] | Sequence[tuple[float, str]], + source_rates: float | Sequence[float], + timestep_units: str = 's' +) -> dict[str, np.ndarray]: + """Calculate time correction factors for the D1S method. + + This function determines the time correction factor that should be applied + to photon tallies as part of the D1S method. + + Parameters + ---------- + nuclides : list of str + The name of the nuclide to find the time correction for, e.g., 'Ni65' + timesteps : iterable of float or iterable of tuple + Array of timesteps. Note that values are not cumulative. The units are + specified by the `timestep_units` argument when `timesteps` is an + iterable of float. Alternatively, units can be specified for each step + by passing a sequence of (value, unit) tuples. + source_rates : float or iterable of float + Source rate in [neutron/sec] for each interval in `timesteps` + timestep_units : {'s', 'min', 'h', 'd', 'a'}, optional + Units for values specified in the `timesteps` argument. 's' means + seconds, 'min' means minutes, 'h' means hours, and 'a' means Julian + years. + + Returns + ------- + dict + Dictionary mapping nuclide to an array of time correction factors for + each time. + + """ + + # Determine normalized timesteps and source rates + timesteps, source_rates = _normalize_timesteps( + timesteps, source_rates, timestep_units) + + # Calculate decay rate for each nuclide + decay_rate = np.array([log(2.0) / half_life(x) for x in nuclides]) + + n_timesteps = len(timesteps) + 1 + n_nuclides = len(nuclides) + + # Create a 2D array for the time correction factors + h = np.zeros((n_timesteps, n_nuclides)) + + for i, (dt, rate) in enumerate(zip(timesteps, source_rates)): + # Precompute the exponential terms. Since (1 - exp(-x)) is susceptible to + # roundoff error, use expm1 instead (which computes exp(x) - 1) + g = np.exp(-decay_rate*dt) + one_minus_g = -np.expm1(-decay_rate*dt) + + # Eq. (4) in doi:10.1016/j.fusengdes.2019.111399 + h[i + 1] = rate*one_minus_g + h[i]*g + + return {nuclides[i]: h[:, i] for i in range(n_nuclides)} + + +def apply_time_correction( + tally: openmc.Tally, + time_correction_factors: dict[str, np.ndarray], + index: int = -1, + sum_nuclides: bool = True +) -> openmc.Tally: + """Apply time correction factors to a tally. + + This function applies the time correction factors at the given index to a + tally that contains a :class:`~openmc.ParentNuclideFilter`. When + `sum_nuclides` is True, values over all parent nuclides will be summed, + leaving a single value for each filter combination. + + Parameters + ---------- + tally : openmc.Tally + Tally to apply the time correction factors to + time_correction_factors : dict + Time correction factors as returned by :func:`time_correction_factors` + index : int, optional + Index to use for the correction factors + sum_nuclides : bool + Whether to sum over the parent nuclides + + Returns + ------- + openmc.Tally + Derived tally with time correction factors applied + + """ + # Make sure the tally contains a ParentNuclideFilter + for i_filter, filter in enumerate(tally.filters): + if isinstance(filter, openmc.ParentNuclideFilter): + break + else: + raise ValueError('Tally must contain a ParentNuclideFilter') + + # Get list of radionuclides based on tally filter + radionuclides = [str(x) for x in tally.filters[i_filter].bins] + tcf = np.array([time_correction_factors[x][index] for x in radionuclides]) + + # Create copy of tally + new_tally = deepcopy(tally) + + # Determine number of bins in other filters + n_bins_before = prod([f.num_bins for f in tally.filters[:i_filter]]) + n_bins_after = prod([f.num_bins for f in tally.filters[i_filter + 1:]]) + + # Reshape sum and sum_sq, apply TCF, and sum along that axis + _, n_nuclides, n_scores = new_tally.shape + n_radionuclides = len(radionuclides) + shape = (n_bins_before, n_radionuclides, n_bins_after, n_nuclides, n_scores) + tally_sum = new_tally.sum.reshape(shape) + tally_sum_sq = new_tally.sum_sq.reshape(shape) + + # Apply TCF, broadcasting to the correct dimensions + tcf.shape = (1, -1, 1, 1, 1) + new_tally._sum = tally_sum * tcf + new_tally._sum_sq = tally_sum_sq * (tcf*tcf) + new_tally._mean = None + new_tally._std_dev = None + + shape = (-1, n_nuclides, n_scores) + + if sum_nuclides: + # Query the mean and standard deviation + mean = new_tally.mean + std_dev = new_tally.std_dev + + # Sum over parent nuclides (note that when combining different bins for + # parent nuclide, we can't work directly on sum_sq) + new_tally._mean = mean.sum(axis=1).reshape(shape) + new_tally._std_dev = np.linalg.norm(std_dev, axis=1).reshape(shape) + new_tally._derived = True + + # Remove ParentNuclideFilter + new_tally.filters.pop(i_filter) + else: + new_tally._sum.shape = shape + new_tally._sum_sq.shape = shape + + return new_tally + + +def prepare_tallies( + model: openmc.Model, + nuclides: list[str] | None = None, + chain_file: str | None = None +) -> list[str]: + """Prepare tallies for the D1S method. + + This function adds a :class:`~openmc.ParentNuclideFilter` to any tally that + has a particle filter with a single 'photon' bin. + + Parameters + ---------- + model : openmc.Model + Model to prepare tallies for + nuclides : list of str, optional + Nuclides to use for the parent nuclide filter. If None, radionuclides + are determined from :func:`get_radionuclides`. + chain_file : str, optional + Chain file to use for inspecting decay data. If None, defaults to + ``openmc.config['chain_file']`` + + Returns + ------- + list of str + List of parent nuclides being filtered on + + """ + if nuclides is None: + nuclides = get_radionuclides(model, chain_file) + filter = openmc.ParentNuclideFilter(nuclides) + + # Apply parent nuclide filter to any tally that has a particle filter with a + # single 'photon' bin + for tally in model.tallies: + for f in tally.filters: + if isinstance(f, openmc.ParticleFilter): + if list(f.bins) == ['photon']: + tally.filters.append(filter) + break + + return nuclides diff --git a/openmc/filter.py b/openmc/filter.py index b5926af5a..35a7e7c7e 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -25,7 +25,7 @@ _FILTER_TYPES = ( 'energyout', 'mu', 'musurface', 'polar', 'azimuthal', 'distribcell', 'delayedgroup', 'energyfunction', 'cellfrom', 'materialfrom', 'legendre', 'spatiallegendre', 'sphericalharmonics', 'zernike', 'zernikeradial', 'particle', 'cellinstance', - 'collision', 'time' + 'collision', 'time', 'parentnuclide' ) _CURRENT_NAMES = ( @@ -732,7 +732,7 @@ class SurfaceFilter(WithIDFilter): class ParticleFilter(Filter): - """Bins tally events based on the Particle type. + """Bins tally events based on the particle type. Parameters ---------- @@ -788,6 +788,33 @@ class ParticleFilter(Filter): return cls(bins, filter_id=filter_id) +class ParentNuclideFilter(ParticleFilter): + """Bins tally events based on the parent nuclide + + Parameters + ---------- + bins : str, or iterable of str + Names of nuclides (e.g., 'Ni65') + filter_id : int + Unique identifier for the filter + + Attributes + ---------- + bins : iterable of str + Names of nuclides + id : int + Unique identifier for the filter + num_bins : Integral + The number of filter bins + + """ + @Filter.bins.setter + def bins(self, bins): + bins = np.atleast_1d(bins) + cv.check_iterable_type('filter bins', bins, str) + self._bins = bins + + class MeshFilter(Filter): """Bins tally event locations by mesh elements. diff --git a/openmc/lib/core.py b/openmc/lib/core.py index 8561602e6..577913dcb 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -25,6 +25,7 @@ class _SourceSite(Structure): ('delayed_group', c_int), ('surf_id', c_int), ('particle', c_int), + ('parent_nuclide', c_int), ('parent_id', c_int64), ('progeny_id', c_int64)] diff --git a/openmc/lib/filter.py b/openmc/lib/filter.py index b30f5e662..b6086c567 100644 --- a/openmc/lib/filter.py +++ b/openmc/lib/filter.py @@ -21,9 +21,10 @@ __all__ = [ 'CellInstanceFilter', 'CollisionFilter', 'DistribcellFilter', 'DelayedGroupFilter', 'EnergyFilter', 'EnergyoutFilter', 'EnergyFunctionFilter', 'LegendreFilter', 'MaterialFilter', 'MaterialFromFilter', 'MeshFilter', 'MeshBornFilter', - 'MeshSurfaceFilter', 'MuFilter', 'MuSurfaceFilter', 'ParticleFilter', 'PolarFilter', - 'SphericalHarmonicsFilter', 'SpatialLegendreFilter', 'SurfaceFilter', 'UniverseFilter', - 'ZernikeFilter', 'ZernikeRadialFilter', 'filters' + 'MeshSurfaceFilter', 'MuFilter', 'MuSurfaceFilter', 'ParentNuclideFilter', + 'ParticleFilter', 'PolarFilter', 'SphericalHarmonicsFilter', + 'SpatialLegendreFilter', 'SurfaceFilter', 'UniverseFilter', 'ZernikeFilter', + 'ZernikeRadialFilter', 'filters' ] # Tally functions @@ -544,6 +545,10 @@ class MuSurfaceFilter(Filter): filter_type = 'musurface' +class ParentNuclideFilter(Filter): + filter_type = 'parentnuclide' + + class ParticleFilter(Filter): filter_type = 'particle' @@ -647,6 +652,7 @@ _FILTER_TYPE_MAP = { 'meshsurface': MeshSurfaceFilter, 'mu': MuFilter, 'musurface': MuSurfaceFilter, + 'parentnuclide': ParentNuclideFilter, 'particle': ParticleFilter, 'polar': PolarFilter, 'sphericalharmonics': SphericalHarmonicsFilter, diff --git a/openmc/material.py b/openmc/material.py index 343b2fff2..64458f571 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -308,8 +308,9 @@ class Material(IDManagerMixin): Returns ------- - Decay photon energy distribution. The integral of this distribution is - the total intensity of the photon source in the requested units. + Univariate or None + Decay photon energy distribution. The integral of this distribution + is the total intensity of the photon source in the requested units. """ cv.check_value('units', units, {'Bq', 'Bq/g', 'Bq/cm3'}) diff --git a/openmc/settings.py b/openmc/settings.py index 110d57c19..0e2a18399 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -275,6 +275,8 @@ class Settings: ufs_mesh : openmc.RegularMesh Mesh to be used for redistributing source sites via the uniform fission site (UFS) method. + use_decay_photons : bool + Produce decay photons from neutron reactions instead of prompt verbosity : int Verbosity during simulation between 1 and 10. Verbosity levels are described in :ref:`verbosity`. @@ -393,6 +395,7 @@ class Settings: self._weight_window_checkpoints = {} self._max_history_splits = None self._max_tracks = None + self._use_decay_photons = None self._random_ray = {} @@ -1143,6 +1146,15 @@ class Settings: self._random_ray = random_ray + @property + def use_decay_photons(self) -> bool: + return self._use_decay_photons + + @use_decay_photons.setter + def use_decay_photons(self, value): + cv.check_type('use decay photons', value, bool) + self._use_decay_photons = value + def _create_run_mode_subelement(self, root): elem = ET.SubElement(root, "run_mode") elem.text = self._run_mode.value @@ -1431,6 +1443,11 @@ class Settings: root.append(self.ufs_mesh.to_xml_element()) if mesh_memo is not None: mesh_memo.add(self.ufs_mesh.id) + def _create_use_decay_photons_subelement(self, root): + if self._use_decay_photons is not None: + element = ET.SubElement(root, "use_decay_photons") + element.text = str(self._use_decay_photons).lower() + def _create_resonance_scattering_subelement(self, root): res = self.resonance_scattering if res: @@ -1957,6 +1974,11 @@ class Settings: elif child.tag == 'sample_method': self.random_ray['sample_method'] = child.text + def _use_decay_photons_from_xml_element(self, root): + text = get_text(root, 'use_decay_photons') + if text is not None: + self.use_decay_photons = text in ('true', '1') + def to_xml_element(self, mesh_memo=None): """Create a 'settings' element to be written to an XML file. @@ -2021,6 +2043,7 @@ class Settings: self._create_max_history_splits_subelement(element) self._create_max_tracks_subelement(element) self._create_random_ray_subelement(element) + self._create_use_decay_photons_subelement(element) # Clean the indentation in the file to be user-readable clean_indentation(element) @@ -2126,8 +2149,8 @@ class Settings: settings._max_history_splits_from_xml_element(elem) settings._max_tracks_from_xml_element(elem) settings._random_ray_from_xml_element(elem) + settings._use_decay_photons_from_xml_element(elem) - # TODO: Get volume calculations return settings @classmethod diff --git a/src/chain.cpp b/src/chain.cpp new file mode 100644 index 000000000..e279d1f59 --- /dev/null +++ b/src/chain.cpp @@ -0,0 +1,116 @@ +//! \file chain.cpp +//! \brief Depletion chain and associated information + +#include "openmc/chain.h" + +#include // for getenv +#include // for make_unique +#include // for stod + +#include +#include + +#include "openmc/distribution.h" // for distribution_from_xml +#include "openmc/error.h" +#include "openmc/reaction.h" +#include "openmc/xml_interface.h" // for get_node_value + +namespace openmc { + +//============================================================================== +// ChainNuclide implementation +//============================================================================== + +ChainNuclide::ChainNuclide(pugi::xml_node node) +{ + name_ = get_node_value(node, "name"); + if (check_for_node(node, "half_life")) { + half_life_ = std::stod(get_node_value(node, "half_life")); + } + if (check_for_node(node, "decay_energy")) { + decay_energy_ = std::stod(get_node_value(node, "decay_energy")); + } + + // Read reactions to store MT -> product map + for (pugi::xml_node reaction_node : node.children("reaction")) { + std::string rx_name = get_node_value(reaction_node, "type"); + if (!reaction_node.attribute("target")) + continue; + std::string rx_target = get_node_value(reaction_node, "target"); + double branching_ratio = 1.0; + if (reaction_node.attribute("branching_ratio")) { + branching_ratio = + std::stod(get_node_value(reaction_node, "branching_ratio")); + } + int mt = reaction_type(rx_name); + reaction_products_[mt].push_back({rx_target, branching_ratio}); + } + + for (pugi::xml_node source_node : node.children("source")) { + auto particle = get_node_value(source_node, "particle"); + if (particle == "photon") { + photon_energy_ = distribution_from_xml(source_node); + break; + } + } + + // Set entry in mapping + data::chain_nuclide_map[name_] = data::chain_nuclides.size(); +} + +ChainNuclide::~ChainNuclide() +{ + data::chain_nuclide_map.erase(name_); +} + +//============================================================================== +// DecayPhotonAngleEnergy implementation +//============================================================================== + +void DecayPhotonAngleEnergy::sample( + double E_in, double& E_out, double& mu, uint64_t* seed) const +{ + E_out = photon_energy_->sample(seed); + mu = Uniform(-1., 1.).sample(seed); +} + +//============================================================================== +// Global variables +//============================================================================== + +namespace data { + +std::unordered_map chain_nuclide_map; +vector> chain_nuclides; + +} // namespace data + +//============================================================================== +// Non-member functions +//============================================================================== + +void read_chain_file_xml() +{ + char* chain_file_path = std::getenv("OPENMC_CHAIN_FILE"); + if (!chain_file_path) { + return; + } + + write_message(5, "Reading chain file: {}...", chain_file_path); + + pugi::xml_document doc; + auto result = doc.load_file(chain_file_path); + if (!result) { + fatal_error( + fmt::format("Error processing chain file: {}", chain_file_path)); + } + + // Get root element + pugi::xml_node root = doc.document_element(); + + for (auto node : root.children("nuclide")) { + data::chain_nuclides.push_back(std::make_unique(node)); + } +} + +} // namespace openmc diff --git a/src/initialize.cpp b/src/initialize.cpp index cc1eac9cf..5f717b137 100644 --- a/src/initialize.cpp +++ b/src/initialize.cpp @@ -12,6 +12,7 @@ #include #include "openmc/capi.h" +#include "openmc/chain.h" #include "openmc/constants.h" #include "openmc/cross_sections.h" #include "openmc/error.h" @@ -164,16 +165,17 @@ void initialize_mpi(MPI_Comm intracomm) MPI_Get_address(&b.delayed_group, &disp[5]); MPI_Get_address(&b.surf_id, &disp[6]); MPI_Get_address(&b.particle, &disp[7]); - MPI_Get_address(&b.parent_id, &disp[8]); - MPI_Get_address(&b.progeny_id, &disp[9]); - for (int i = 9; i >= 0; --i) { + MPI_Get_address(&b.parent_nuclide, &disp[8]); + MPI_Get_address(&b.parent_id, &disp[9]); + MPI_Get_address(&b.progeny_id, &disp[10]); + for (int i = 10; i >= 0; --i) { disp[i] -= disp[0]; } - int blocks[] {3, 3, 1, 1, 1, 1, 1, 1, 1, 1}; + int blocks[] {3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1}; MPI_Datatype types[] {MPI_DOUBLE, MPI_DOUBLE, MPI_DOUBLE, MPI_DOUBLE, - MPI_DOUBLE, MPI_INT, MPI_INT, MPI_INT, MPI_LONG, MPI_LONG}; - MPI_Type_create_struct(10, blocks, disp, types, &mpi::source_site); + MPI_DOUBLE, MPI_INT, MPI_INT, MPI_INT, MPI_INT, MPI_LONG, MPI_LONG}; + MPI_Type_create_struct(11, blocks, disp, types, &mpi::source_site); MPI_Type_commit(&mpi::source_site); } #endif // OPENMC_MPI @@ -371,6 +373,9 @@ bool read_model_xml() } } + // Read data from chain file + read_chain_file_xml(); + // Read materials and cross sections if (!check_for_node(root, "materials")) { fatal_error(fmt::format( @@ -423,6 +428,10 @@ void read_separate_xml_files() if (settings::run_mode != RunMode::PLOTTING) { read_cross_sections_xml(); } + + // Read data from chain file + read_chain_file_xml(); + read_materials_xml(); read_geometry_xml(); diff --git a/src/nuclide.cpp b/src/nuclide.cpp index d78b7a010..eaac2f756 100644 --- a/src/nuclide.cpp +++ b/src/nuclide.cpp @@ -248,7 +248,8 @@ Nuclide::Nuclide(hid_t group, const vector& temperature) for (auto name : group_names(rxs_group)) { if (starts_with(name, "reaction_")) { hid_t rx_group = open_group(rxs_group, name.c_str()); - reactions_.push_back(make_unique(rx_group, temps_to_read)); + reactions_.push_back( + make_unique(rx_group, temps_to_read, name_)); // Check for 0K elastic scattering const auto& rx = reactions_.back(); @@ -375,15 +376,15 @@ void Nuclide::create_derived( 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); for (const auto& p : rx->products_) { if (p.particle_ == ParticleType::photon) { - auto pprod = xt::view(xs_[t], xt::range(j, j + n), XS_PHOTON_PROD); for (int k = 0; k < n; ++k) { double E = grid_[t].energy[k + j]; - // For fission, artificially increase the photon yield to account - // for delayed photons + // For fission, artificially increase the photon yield to + // account for delayed photons double f = 1.0; if (settings::delayed_photon_scaling) { if (is_fission(rx->mt_)) { @@ -469,8 +470,8 @@ void Nuclide::create_derived( } } } else { - // Otherwise, assume that any that have 0 K elastic scattering data are - // resonant + // Otherwise, assume that any that have 0 K elastic scattering data + // are resonant resonant_ = !energy_0K_.empty(); } @@ -781,8 +782,8 @@ void Nuclide::calculate_xs( } for (int j = 0; j < DEPLETION_RX.size(); ++j) { - // If reaction is present and energy is greater than threshold, set the - // reaction xs appropriately + // If reaction is present and energy is greater than threshold, set + // the reaction xs appropriately int i_rx = reaction_index_[DEPLETION_RX[j]]; if (i_rx >= 0) { const auto& rx = reactions_[i_rx]; @@ -819,9 +820,9 @@ void Nuclide::calculate_xs( // Initialize URR probability table treatment to false micro.use_ptable = false; - // If there is S(a,b) data for this nuclide, we need to set the sab_scatter - // and sab_elastic cross sections and correct the total and elastic cross - // sections. + // If there is S(a,b) data for this nuclide, we need to set the + // sab_scatter and sab_elastic cross sections and correct the total and + // elastic cross sections. if (i_sab >= 0) this->calculate_sab_xs(i_sab, sab_frac, p); @@ -984,8 +985,8 @@ void Nuclide::calculate_urr_xs(int i_temp, Particle& p) const } // Set elastic, absorption, fission, total, and capture x/s. Note that the - // total x/s is calculated as a sum of partials instead of the table-provided - // value + // total x/s is calculated as a sum of partials instead of the + // table-provided value micro.elastic = elastic; micro.absorption = capture + fission; micro.fission = fission; diff --git a/src/particle.cpp b/src/particle.cpp index 5b46e2e63..c63c84674 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -75,13 +75,13 @@ double Particle::speed() const } } -void Particle::create_secondary( +bool Particle::create_secondary( double wgt, Direction u, double E, ParticleType type) { // If energy is below cutoff for this particle, don't create secondary // particle if (E < settings::energy_cutoff[static_cast(type)]) { - return; + return false; } auto& bank = secondary_bank().emplace_back(); @@ -92,6 +92,7 @@ void Particle::create_secondary( bank.E = settings::run_CE ? E : g(); bank.time = time(); bank_second_E() += bank.E; + return true; } void Particle::split(double wgt) @@ -137,6 +138,7 @@ void Particle::from_source(const SourceSite* src) E_last() = E(); time() = src->time; time_last() = src->time; + parent_nuclide() = src->parent_nuclide; } void Particle::event_calculate_xs() diff --git a/src/physics.cpp b/src/physics.cpp index c324ce6b9..e04f54c0b 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -2,6 +2,7 @@ #include "openmc/bank.h" #include "openmc/bremsstrahlung.h" +#include "openmc/chain.h" #include "openmc/constants.h" #include "openmc/distribution_multi.h" #include "openmc/eigenvalue.h" @@ -1145,9 +1146,21 @@ void sample_secondary_photons(Particle& p, int i_nuclide) // Sample the number of photons produced double y_t = p.neutron_xs(i_nuclide).photon_prod / p.neutron_xs(i_nuclide).total; - int y = static_cast(y_t); - if (prn(p.current_seed()) <= y_t - y) - ++y; + double photon_wgt = p.wgt(); + int y = 1; + + if (settings::use_decay_photons) { + // For decay photons, sample a single photon and modify the weight + if (y_t <= 0.0) + return; + photon_wgt *= y_t; + } else { + // For prompt photons, sample an integral number of photons with weight + // equal to the neutron's weight + y = static_cast(y_t); + if (prn(p.current_seed()) <= y_t - y) + ++y; + } // Sample each secondary photon for (int i = 0; i < y; ++i) { @@ -1170,15 +1183,19 @@ void sample_secondary_photons(Particle& p, int i_nuclide) // release and deposition. See D. P. Griesheimer, S. J. Douglass, and M. H. // Stedry, "Self-consistent energy normalization for quasistatic reactor // calculations", Proc. PHYSOR, Cambridge, UK, Mar 29-Apr 2, 2020. - double wgt; + double wgt = photon_wgt; if (settings::run_mode == RunMode::EIGENVALUE && !is_fission(rx->mt_)) { - wgt = simulation::keff * p.wgt(); - } else { - wgt = p.wgt(); + wgt *= simulation::keff; } // Create the secondary photon - p.create_secondary(wgt, u, E, ParticleType::photon); + bool created_photon = p.create_secondary(wgt, u, E, ParticleType::photon); + + // Tag secondary particle with parent nuclide + if (created_photon && settings::use_decay_photons) { + p.secondary_bank().back().parent_nuclide = + rx->products_[i_product].parent_nuclide_; + } } } diff --git a/src/reaction.cpp b/src/reaction.cpp index 60a14b2b3..971473438 100644 --- a/src/reaction.cpp +++ b/src/reaction.cpp @@ -1,17 +1,20 @@ #include "openmc/reaction.h" +#include // for remove_if #include #include #include // for move #include +#include "openmc/chain.h" #include "openmc/constants.h" #include "openmc/endf.h" #include "openmc/hdf5_interface.h" #include "openmc/random_lcg.h" #include "openmc/search.h" #include "openmc/secondary_uncorrelated.h" +#include "openmc/settings.h" namespace openmc { @@ -19,7 +22,8 @@ namespace openmc { // Reaction implementation //============================================================================== -Reaction::Reaction(hid_t group, const vector& temperatures) +Reaction::Reaction( + hid_t group, const vector& temperatures, std::string name) { read_attribute(group, "Q_value", q_value_); read_attribute(group, "mt", mt_); @@ -63,6 +67,34 @@ Reaction::Reaction(hid_t group, const vector& temperatures) close_group(pgroup); } } + + if (settings::use_decay_photons) { + // Remove photon products for D1S method + products_.erase( + std::remove_if(products_.begin(), products_.end(), + [](const auto& p) { return p.particle_ == ParticleType::photon; }), + products_.end()); + + // Determine product for D1S method + auto nuclide_it = data::chain_nuclide_map.find(name); + if (nuclide_it != data::chain_nuclide_map.end()) { + const auto& chain_nuc = data::chain_nuclides[nuclide_it->second]; + const auto& rx_products = chain_nuc->reaction_products(); + auto product_it = rx_products.find(mt_); + if (product_it != rx_products.end()) { + auto decay_products = product_it->second; + for (const auto& decay_product : decay_products) { + auto product_it = data::chain_nuclide_map.find(decay_product.name); + if (product_it != data::chain_nuclide_map.end()) { + const auto& product_nuc = data::chain_nuclides[product_it->second]; + if (product_nuc->photon_energy()) { + products_.emplace_back(decay_product); + } + } + } + } + } + } } double Reaction::xs(int64_t i_temp, int64_t i_grid, double interp_factor) const diff --git a/src/reaction_product.cpp b/src/reaction_product.cpp index 4cef8d3a9..3ba2c0cfb 100644 --- a/src/reaction_product.cpp +++ b/src/reaction_product.cpp @@ -83,6 +83,29 @@ ReactionProduct::ReactionProduct(hid_t group) } } +ReactionProduct::ReactionProduct(const ChainNuclide::Product& product) +{ + particle_ = ParticleType::photon; + emission_mode_ = EmissionMode::delayed; + + // Get chain nuclide object for radionuclide + parent_nuclide_ = data::chain_nuclide_map.at(product.name); + const auto& chain_nuc = data::chain_nuclides[parent_nuclide_].get(); + + // Determine decay constant in [s^-1] + decay_rate_ = chain_nuc->decay_constant(); + + // Determine number of photons per decay and set yield + double photon_per_sec = chain_nuc->photon_energy()->integral(); + double photon_per_decay = photon_per_sec / decay_rate_; + vector coef = {product.branching_ratio * photon_per_decay}; + yield_ = make_unique(coef); + + // Set decay photon angle-energy distribution + distribution_.push_back( + make_unique(chain_nuc->photon_energy())); +} + void ReactionProduct::sample( double E_in, double& E_out, double& mu, uint64_t* seed) const { diff --git a/src/settings.cpp b/src/settings.cpp index 60263c846..e2f5a033b 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -75,6 +75,7 @@ bool trigger_predict {false}; bool uniform_source_sampling {false}; bool ufs_on {false}; bool urr_ptables_on {true}; +bool use_decay_photons {false}; bool weight_windows_on {false}; bool weight_window_checkpoint_surface {false}; bool weight_window_checkpoint_collision {true}; @@ -1124,6 +1125,11 @@ void read_settings_xml(pugi::xml_node root) get_node_value_bool(ww_checkpoints, "surface"); } } + + if (check_for_node(root, "use_decay_photons")) { + settings::use_decay_photons = + get_node_value_bool(root, "use_decay_photons"); + } } void free_memory_settings() diff --git a/src/tallies/filter.cpp b/src/tallies/filter.cpp index 0ce1ee20b..6430d9ae1 100644 --- a/src/tallies/filter.cpp +++ b/src/tallies/filter.cpp @@ -28,6 +28,7 @@ #include "openmc/tallies/filter_meshsurface.h" #include "openmc/tallies/filter_mu.h" #include "openmc/tallies/filter_musurface.h" +#include "openmc/tallies/filter_parent_nuclide.h" #include "openmc/tallies/filter_particle.h" #include "openmc/tallies/filter_polar.h" #include "openmc/tallies/filter_sph_harm.h" @@ -137,6 +138,8 @@ Filter* Filter::create(const std::string& type, int32_t id) return Filter::create(id); } else if (type == "musurface") { return Filter::create(id); + } else if (type == "parentnuclide") { + return Filter::create(id); } else if (type == "particle") { return Filter::create(id); } else if (type == "polar") { diff --git a/src/tallies/filter_parent_nuclide.cpp b/src/tallies/filter_parent_nuclide.cpp new file mode 100644 index 000000000..d04941075 --- /dev/null +++ b/src/tallies/filter_parent_nuclide.cpp @@ -0,0 +1,79 @@ +#include "openmc/tallies/filter_parent_nuclide.h" + +#include // for int64_t + +#include + +#include "openmc/capi.h" +#include "openmc/chain.h" +#include "openmc/search.h" +#include "openmc/settings.h" +#include "openmc/xml_interface.h" + +namespace openmc { + +//============================================================================== +// ParentNuclideFilter implementation +//============================================================================== + +void ParentNuclideFilter::from_xml(pugi::xml_node node) +{ + nuclides_ = get_node_array(node, "bins"); + + // Convert nuclides to indices in data::chain_nuclides + std::vector bins; + for (const auto& nuclide : nuclides_) { + auto it = data::chain_nuclide_map.find(nuclide); + if (it != data::chain_nuclide_map.end()) { + bins.push_back(it->second); + } else { + // The default value of parent_nuclide is -1, so to prevent a score to + // this bin assign the value -2. + bins.push_back(-2); + } + } + this->set_bins(bins); +} + +void ParentNuclideFilter::set_bins(span bins) +{ + // Clear existing bins + bins_.clear(); + bins_.reserve(bins.size()); + map_.clear(); + + // Set bins based on chain nuclide indexing + for (int64_t i = 0; i < bins.size(); ++i) { + bins_.push_back(bins[i]); + map_[bins[i]] = i; + } + + n_bins_ = bins_.size(); +} + +void ParentNuclideFilter::get_all_bins( + const Particle& p, TallyEstimator estimator, FilterMatch& match) const +{ + // Get the particle's parent nuclide + int parent_nuclide = p.parent_nuclide(); + + // Find bin matching parent nuclide + auto search = map_.find(parent_nuclide); + if (search != map_.end()) { + match.bins_.push_back(search->second); + match.weights_.push_back(1.0); + } +} + +void ParentNuclideFilter::to_statepoint(hid_t filter_group) const +{ + Filter::to_statepoint(filter_group); + write_dataset(filter_group, "bins", nuclides_); +} + +std::string ParentNuclideFilter::text_label(int bin) const +{ + return fmt::format("Parent Nuclide {}", nuclides_[bin]); +} + +} // namespace openmc diff --git a/tests/chain_ni.xml b/tests/chain_ni.xml new file mode 100644 index 000000000..fc96d3d8b --- /dev/null +++ b/tests/chain_ni.xml @@ -0,0 +1,173 @@ + + + + + + + + + 557.6039 640.4896 655.72 678.8113 5847.93 5858.68 6448.81 6450.12 6499.05 6499.18 126000.0 4.281895544855688e-11 8.113092795419058e-12 2.881383486075155e-13 8.900165070824795e-12 6.680021392530635e-10 1.3098276096378766e-09 7.960187308411459e-11 1.5666266690172673e-10 2.8061406316612144e-14 4.106367573258232e-14 1.0245835494664239e-17 + + + 105210.0 231210.0 1.040592667426837e-17 8.004558980206438e-09 + + + 36.67648 65.20168 562.7812 610.8647 5155.619 5764.491 6377.571 2.257651440763611e-08 2.654244519541021e-09 1.1943434358627441e-08 2.0711244046714524e-10 3.863926291321637e-09 9.499586470057554e-10 5.918783030777615e-11 + + + + + + + + + + + + + + + + + + 780.0 6915.0 6930.0 7649.0 142651.0 189000.0 192343.0 334800.0 382000.0 1099245.0 1291590.0 1481700.0 1.0526870077721752e-12 1.0971242349369013e-11 2.1648606887732874e-11 3.9280951004469605e-12 1.8390802445841403e-09 1.622717862868359e-12 5.553301130705051e-09 4.868153588605077e-10 3.245435725736718e-11 1.018706213911803e-07 7.789045741768123e-08 1.0637817101025909e-10 + + + 750.0 6070.0 83569.95 130946.9 134942.1 141725.4 184634.1 191417.4 273599.0 327091.1 333874.4 465943.0 1091536.0 1283881.0 1565200.0 1.4515384373262843e-10 5.994999523918275e-11 1.4063554811525778e-10 2.3619560003972782e-09 2.703447959538686e-11 2.7218387619845277e-12 4.520385317374064e-11 4.498173915871091e-12 8.16767990977074e-08 8.519268780058884e-13 8.275861100628631e-14 9.574035390923319e-08 1.6299297619569002e-11 8.567950315944936e-12 3.245435725736718e-10 + + + + + + 120340.0 177610.0 297900.0 333000.0 349700.0 440500.0 542600.0 561400.0 603300.0 618400.0 657300.0 686000.0 696900.0 748100.0 769400.0 806300.0 925600.0 945400.0 978000.0 984100.0 989200.0 1027420.0 1097800.0 1205070.0 1275000.0 1285700.0 1381400.0 1403900.0 1538800.0 1618900.0 1645950.0 1659300.0 1837200.0 1879400.0 1889000.0 1899300.0 1972700.0 1999800.0 2011600.0 2177100.0 2230800.0 2484400.0 2754400.0 2920000.0 3191000.0 3204200.0 3239100.0 3364900.0 0.00010275887633317896 3.874515009283797e-05 0.00042956579450755135 4.2956579450755134e-06 3.116457724858706e-06 4.21142935791717e-06 1.263428807375151e-06 1.0949716330584643e-06 1.431885981691838e-06 1.802491765188549e-05 4.2956579450755134e-06 7.749030018567594e-06 2.1899432661169287e-06 1.558228862429353e-05 3.116457724858706e-06 3.7060578349671097e-06 6.569829798350785e-06 2.105714678958585e-06 1.431885981691838e-06 1.1792002202168078e-05 1.1792002202168078e-05 0.0008254401541517653 1.3476573945334946e-05 0.000842285871583434 1.1792002202168078e-05 7.159429908459189e-06 7.749030018567594e-06 2.2741718532752718e-06 5.306400990975635e-06 7.075201321300846e-06 0.00013476573945334945 1.4992688514185127e-05 2.6953147890669893e-06 5.053715229500604e-06 3.4533720734920796e-06 1.431885981691838e-06 1.1792002202168076e-06 2.526857614750302e-06 8.507087302992683e-05 4.042972183600483e-06 2.105714678958585e-06 2.3584004404336153e-06 1.4824231339868439e-05 1.3476573945334947e-06 1.6003431560085245e-06 8.422858715834341e-07 1.0949716330584643e-06 8.422858715834341e-07 + + + 613000.0 738800.1 773300.1 786899.9 873600.1 977699.9 1057900.0 1113610.0 1223500.0 1493500.0 1546600.0 1675000.0 1747100.0 1966500.0 2024800.0 2089000.0 2332110.0 2359120.0 2652590.0 2692270.0 2772900.0 2950510.0 3978000.0 7.969724371709087e-07 1.032441566335041e-06 1.1773456458206606e-05 1.5214928345990077e-06 1.267910695499173e-06 2.535821390998346e-05 1.267910695499173e-06 2.1735611922842964e-05 1.3947017650490904e-05 2.227900222091404e-06 2.173561192284297e-06 2.8980815897123955e-05 1.0867805961421485e-06 9.056504967851236e-05 1.7931879836345447e-05 4.709382583282643e-06 0.00014490407948561978 2.4452563413198337e-06 0.0004890512682639667 3.622601987140494e-07 0.0006882943775566939 0.00028980815897123956 9.056504967851236e-05 + + + + + + 619.9594 707.9362 726.02 741.741 6349.85 6362.71 7015.36 7016.95 7070.49 7070.66 510998.9 810759.2 863951.0 1674725.0 5.276288639112756e-10 1.3715809425619083e-10 3.759818598537813e-12 1.1767757246637665e-10 8.777123989590544e-09 1.719716697150835e-08 1.057271626846154e-09 2.0783510382283326e-09 5.201844116739253e-13 7.595270495013994e-13 3.374000922877054e-08 1.1259402850084962e-07 7.768987966558623e-10 5.854889482044179e-10 + + + 632864.1 1496834.0 2307600.0 1.3699223176071195e-09 1.1185811981783752e-07 8.491254034754873e-13 + + + 40.23492 70.00108 628.9213 675.1537 5583.843 6258.984 6938.274 803647.2 809913.1 810759.2 856839.0 863104.9 863951.0 1667613.0 1673879.0 1674725.0 2.3068291731266982e-07 2.4528685530196402e-08 1.3309519902564132e-07 1.9030055102434398e-09 4.481805021434518e-08 1.1035282426757231e-08 6.934054278993223e-10 3.366561452175404e-11 3.231448617974384e-12 4.640000480603615e-13 1.6314869068937087e-13 1.553797593311725e-14 2.2436836794554425e-15 3.3782714575729324e-14 3.2026247731116077e-15 4.614238015862169e-16 + + + + + + + + + + 751.0021 852.3381 876.89 883.6421 7417.82 7435.78 8222.319 8224.59 8287.88 8288.141 347140.0 826100.0 1173228.0 1332492.0 2158570.0 2505692.0 7.0614888325101086e-15 3.3564685966506987e-15 5.761418570090495e-17 2.3600648888691433e-15 1.3290046971181013e-13 2.5954235427719607e-13 1.6253028424014347e-14 3.186921843254948e-14 1.3932264913028456e-17 2.024904561736029e-17 3.125208966828774e-13 3.1668784197198245e-13 4.160694871171375e-09 4.166220240624728e-09 5.000334346926039e-14 8.333890578210064e-17 + + + 47.97734 78.78671 772.594 818.3802 6497.883 7313.626 8133.5 318151.8 1164895.0 1172220.0 1173228.0 1324159.0 1331484.0 1332492.0 1491392.0 2497359.0 2504684.0 2505692.0 2.430147074908485e-12 3.047352927658078e-13 1.6375715794161468e-12 1.653965175572544e-14 5.18885111875044e-13 1.2976030141139345e-13 8.558901456876446e-15 4.161944954758106e-09 6.241042306757062e-13 6.095415902793419e-14 8.94050113917305e-15 4.736991738545178e-13 4.616171159887578e-14 6.779692031857936e-15 5.000334346926038e-12 6.46709908869101e-21 6.3170890582832295e-22 9.678980517533168e-23 + + + + + + 751.028 852.3382 876.89 883.6886 7417.82 7435.78 8222.319 8224.59 8287.88 8288.141 67415.0 841700.0 909200.0 7.866502846971466e-08 3.756939402646081e-08 6.436568219422988e-10 2.637348005677196e-08 1.4671031864629482e-06 2.8654120987125446e-06 1.794509040062992e-07 3.5187803327436886e-07 1.538314140492697e-10 2.235774236842793e-10 9.880497991981766e-05 9.262966867482906e-07 4.219796017408879e-06 + + + 48.00067 78.83981 772.8435 818.5774 6498.158 7313.756 8133.501 59082.2 66406.9 67415.0 413499.9 833367.2 840691.9 841700.0 900867.2 908191.9 909200.0 1255285.0 2.6847340674754763e-05 3.3502043712197297e-06 1.814135374119958e-05 1.8444716489566865e-07 5.715307736044776e-06 1.430796977323619e-06 9.449890224831685e-08 1.2103610040177662e-05 1.246919266677299e-06 1.902588515965013e-07 5.134423559703298e-06 2.917834563257115e-10 2.862256762052218e-11 4.385088515066407e-12 9.241353278125446e-10 9.030363477255e-11 1.3801262380678854e-11 0.00011155702097900803 + + + + + + 1128900.0 1172900.0 1886300.0 1985000.0 2083000.0 2097000.0 2301800.0 2345900.0 3158200.0 3271000.0 3369500.0 3519000.0 4063100.0 0.0008553051126342747 0.006430865508528381 3.2154327542641905e-05 0.00012218644466203924 2.5723462034113525e-05 7.07395205938122e-05 0.0011318323295009952 0.0001028938481364541 6.430865508528381e-05 1.1254014639924668e-05 2.8938894788377717e-05 6.430865508528381e-06 2.5723462034113525e-05 + + + 1251800.0 1796100.0 1945400.0 2044800.0 2059000.0 2156900.0 2255700.0 3013160.0 4142070.0 5315000.0 2.5415396620531326e-05 0.00010782289475376927 2.9266214290308802e-05 9.318978760861486e-05 2.5415396620531326e-05 0.00018483924814931876 3.080654135821979e-05 0.0019870219176051766 0.005221708760218255 1.925408834888737e-05 + + + + + + 931100.0 1346100.0 0.11552453009332422 0.23104906018664845 + + + 5029800.0 5960900.0 7307000.0 0.11552453009332422 0.11552453009332422 2.079441541679836 + + + + + + 683.2094 778.5527 799.73 813.244 6873.16 6888.41 7606.53 7608.44 7666.77 7666.98 127164.0 161860.0 304100.0 379940.0 510998.9 541900.0 673440.0 696000.0 755300.0 906980.0 1046680.0 1224000.0 1279990.0 1350520.0 1377630.0 1603280.0 1730440.0 1757550.0 1897420.0 1919520.0 2133040.0 2730910.0 2804200.0 3177280.0 1.8638833690911883e-08 6.653293496572297e-09 1.4203876186268797e-10 5.409592966721624e-09 3.0832339673226584e-07 6.028687625463321e-07 3.74301586798566e-08 7.349918311628966e-08 2.4690572137876036e-11 3.5966442331457405e-11 9.014158418349326e-07 1.2284000197554473e-09 1.0604892256881559e-10 3.6233381877678664e-09 4.684800934777033e-06 1.9884172981652923e-10 2.6556417693274235e-09 4.860575617737382e-11 2.916345370642429e-10 3.3140288302754875e-09 7.246676375535733e-09 3.402402932416167e-09 5.2140720263001e-10 1.0604892256881559e-10 4.418705107033983e-06 2.1209784513763118e-10 2.827971268501749e-09 3.110768395351924e-07 1.502359736391554e-09 6.628057660550975e-07 1.546546787461894e-09 1.0737453410092578e-09 5.30244612844078e-09 6.009438945566218e-10 + + + 84790.04 154079.8 457929.9 531290.1 1129120.0 1342650.0 1504620.0 1757390.0 1884550.0 1.124957970946228e-09 3.2450710700371965e-09 1.5738594689680403e-08 1.0762819048956704e-09 1.8388736063544118e-09 6.652395693576253e-07 3.0611837094017556e-07 9.194368031772059e-07 3.488451400289987e-06 + + + 43.92123 75.12743 700.3887 749.9867 6033.611 6776.5 7523.621 119455.1 126238.4 127164.0 154151.1 160934.4 161860.0 665731.1 672514.4 673440.0 747591.1 754374.4 755300.0 1216291.0 1223074.0 1224000.0 1369921.0 1376704.0 1377630.0 1749841.0 1756624.0 1757550.0 1889711.0 1896494.0 1897420.0 1911811.0 1918594.0 1919520.0 2125331.0 2132114.0 2133040.0 7.252641897852818e-06 9.885065183674125e-07 4.453862207006767e-06 5.575973168934215e-08 1.3531437967587472e-06 3.3801747364315905e-07 2.1887831296943824e-08 1.739732899248527e-08 1.7397323584033486e-09 2.636641229198142e-10 1.272622225762379e-11 1.2627950688719498e-12 1.9160584871075314e-13 9.58686478614484e-13 9.268187990163621e-14 1.4090839543995835e-14 8.632381215411233e-14 8.34074667834699e-15 1.2155339619769639e-15 3.6814000810433283e-13 3.538499049712814e-14 5.158725716665081e-15 4.21544467211042e-10 4.0607899933642303e-11 2.1687004665322787e-10 1.822910020070542e-11 1.7482517516525527e-12 6.153697369032366e-11 7.181277376570916e-14 6.8808054292926055e-15 3.195035376537882e-13 3.115187100458958e-11 2.9826259472479385e-12 1.482406852545487e-10 6.031532471101388e-14 5.768616813006974e-15 4.833646927113489e-13 + + + + + + + + + + + + 1072500.0 2.890064045563861e-13 + + + 44.06754 75.28693 701.5928 750.7433 6035.851 6777.615 7523.68 6.689278658453111e-13 8.913674252402247e-14 4.1657845563004787e-13 5.378516121164032e-15 1.2583954438026756e-13 3.16650156158609e-14 2.0701132819599697e-15 + + + 683.3271 778.5529 799.73 813.5109 6873.16 6888.41 7606.53 7608.44 7666.77 7666.98 510998.9 1.7758787515451584e-15 6.581346326607483e-16 1.368589539770362e-17 5.248596182059754e-16 2.920762305855841e-14 5.711006429349971e-14 3.5457762265416177e-15 6.962612245690403e-15 2.338949640535961e-18 3.4071167337635547e-18 2.1386473937172572e-19 + + + + + + + + + + + + + + + + + + + + + + + + + + + + 66945.0 2.1704054025041888e-10 + + + + + + + + + + + + 820.0389 927.6652 951.42 959.5527 7984.67 8005.709 8862.7 8865.34 8933.11 8933.4 366270.0 507900.0 609500.0 770600.0 852700.0 954500.0 1115530.0 1481840.0 1623420.0 1724920.0 6.930967733828076e-11 4.4848940639261224e-11 6.225110448569713e-13 3.0091057755445835e-11 1.4120610720530457e-09 2.7533685191404717e-09 1.7112326402414573e-10 3.3482649978514024e-10 1.9318959861680173e-13 2.7929997279698636e-13 3.6755804457910027e-06 2.2374667416695354e-07 1.181887674027053e-07 7.92135402897521e-08 7.398075516810562e-08 1.3533064969775417e-09 1.1800832653644161e-05 1.804408662636722e-05 3.807302278163484e-07 3.0494506398560603e-07 + + + 56.92597 90.91714 855.942 897.216 6991.356 7880.445 8773.007 357291.1 365173.4 366270.0 412919.9 498921.1 506803.4 507900.0 514460.0 600521.1 608403.4 609500.0 656069.9 761621.1 769503.4 770600.0 1022350.0 1106551.0 1114434.0 1115530.0 1472861.0 1480743.0 1481840.0 1614441.0 1622324.0 1623420.0 1715941.0 1723824.0 1724920.0 2137900.0 2.167548391930537e-08 2.8616659297729836e-09 1.5876524466163717e-08 5.919283450210211e-11 4.8238674145478966e-09 1.1910611683428657e-09 8.251524863747208e-11 6.6528004309537836e-09 6.652800430953784e-10 9.941708721258623e-11 4.2452174979371803e-07 1.968970885650002e-10 1.9600207433178643e-11 2.840463936760989e-12 6.807646077773136e-07 6.748578656939676e-11 6.701302996997783e-12 9.996408692926368e-13 2.172327512457944e-05 2.732867560693677e-11 2.7011821751739386e-12 4.0351373644973695e-13 7.786723266486576e-06 1.953037934211798e-09 1.9247158318161006e-10 3.950919133474779e-11 1.818843931937816e-09 1.7899733933356285e-10 1.4705028396157966e-09 3.076300301948419e-11 3.022997962967563e-12 4.574485034565062e-11 2.213900812679635e-11 2.1742581302894385e-12 5.0020380532292804e-11 4.589424322094249e-05 + + + diff --git a/tests/regression_tests/surface_source/test.py b/tests/regression_tests/surface_source/test.py index 251e9e9ad..83b78e61d 100644 --- a/tests/regression_tests/surface_source/test.py +++ b/tests/regression_tests/surface_source/test.py @@ -10,6 +10,17 @@ from tests.testing_harness import PyAPITestHarness from tests.regression_tests import config +def assert_structured_arrays_close(arr1, arr2, rtol=1e-5, atol=1e-8): + assert arr1.dtype == arr2.dtype + + for field in arr1.dtype.names: + data1, data2 = arr1[field], arr2[field] + if data1.dtype.names: + assert_structured_arrays_close(data1, data2, rtol=rtol, atol=atol) + else: + np.testing.assert_allclose(data1, data2, rtol=rtol, atol=atol) + + @pytest.fixture def model(request): openmc.reset_auto_ids() @@ -76,16 +87,10 @@ class SurfaceSourceTestHarness(PyAPITestHarness): """Make sure the current surface_source.h5 agree with the reference.""" if self._model.settings.surf_source_write: with h5py.File("surface_source_true.h5", 'r') as f: - source_true = f['source_bank'][()] - # Convert dtye from mixed to a float for comparison assertion - source_true.dtype = 'float64' + source_true = np.sort(f['source_bank'][()]) with h5py.File("surface_source.h5", 'r') as f: - source_test = f['source_bank'][()] - # Convert dtye from mixed to a float for comparison assertion - source_test.dtype = 'float64' - np.testing.assert_allclose(np.sort(source_true), - np.sort(source_test), - atol=1e-07) + source_test = np.sort(f['source_bank'][()]) + assert_structured_arrays_close(source_true, source_test, atol=1e-07) def execute_test(self): """Build input XMLs, run OpenMC, check output and results.""" @@ -132,4 +137,4 @@ def test_surface_source_read(model): harness = SurfaceSourceTestHarness('statepoint.10.h5', model, 'inputs_true_read.dat') - harness.main() \ No newline at end of file + harness.main() diff --git a/tests/unit_tests/test_d1s.py b/tests/unit_tests/test_d1s.py new file mode 100644 index 000000000..7c92dd6d9 --- /dev/null +++ b/tests/unit_tests/test_d1s.py @@ -0,0 +1,132 @@ +from pathlib import Path +from math import exp + +import numpy as np +import pytest +import openmc +import openmc.deplete +from openmc.deplete import d1s + + +CHAIN_PATH = Path(__file__).parents[1] / "chain_ni.xml" + + +@pytest.fixture +def model(): + """Simple model with natural Ni""" + mat = openmc.Material() + mat.add_element('Ni', 1.0) + geom = openmc.Geometry([openmc.Cell(fill=mat)]) + return openmc.Model(geometry=geom) + + +def test_get_radionuclides(model): + # Check that radionuclides are correct and are unstable + nuclides = d1s.get_radionuclides(model, CHAIN_PATH) + assert sorted(nuclides) == [ + 'Co58', 'Co60', 'Co61', 'Co62', 'Co64', + 'Fe55', 'Fe59', 'Fe61', 'Ni57', 'Ni59', 'Ni63', 'Ni65' + ] + for nuc in nuclides: + assert openmc.data.half_life(nuc) is not None + + +@pytest.mark.parametrize("nuclide", ['Co60', 'Ni63', 'H3', 'Na24', 'K40']) +def test_time_correction_factors(nuclide): + # Irradiation schedule turning unit neutron source on and off + timesteps = [1.0, 1.0, 1.0] + source_rates = [1.0, 0.0, 1.0] + + # Compute expected solution + decay_rate = openmc.data.decay_constant(nuclide) + g = exp(-decay_rate) + expected = [0.0, (1 - g), (1 - g)*g, (1 - g)*(1 + g*g)] + + # Test against expected solution + tcf = d1s.time_correction_factors([nuclide], timesteps, source_rates) + assert tcf[nuclide] == pytest.approx(expected) + + # Make sure all values at first timestep and onward are positive (K40 case + # has very small decay constant that stresses this) + assert np.all(tcf[nuclide][1:] > 0.0) + + # Timesteps as a tuple + timesteps = [(1.0, 's'), (1.0, 's'), (1.0, 's')] + tcf = d1s.time_correction_factors([nuclide], timesteps, source_rates) + assert tcf[nuclide] == pytest.approx(expected) + + # Test changing units + timesteps = [1.0/60.0, 1.0/60.0, 1.0/60.0] + tcf = d1s.time_correction_factors([nuclide], timesteps, source_rates, + timestep_units='min') + assert tcf[nuclide] == pytest.approx(expected) + + +def test_prepare_tallies(model): + tally = openmc.Tally() + tally.filters = [openmc.ParticleFilter('photon')] + tally.scores = ['flux'] + model.tallies = [tally] + + # Check that prepare_tallies adds a ParentNuclideFilter + nuclides = ['Co58', 'Co60', 'Fe55'] + d1s.prepare_tallies(model, nuclides, chain_file=CHAIN_PATH) + assert tally.contains_filter(openmc.ParentNuclideFilter) + assert list(tally.filters[-1].bins) == nuclides + + # Get rid of parent nuclide filter + tally.filters.pop() + + # With no nuclides specified, filter should use get_radionuclides + radionuclides = d1s.get_radionuclides(model, CHAIN_PATH) + d1s.prepare_tallies(model, chain_file=CHAIN_PATH) + assert tally.contains_filter(openmc.ParentNuclideFilter) + assert sorted(tally.filters[-1].bins) == sorted(radionuclides) + + +def test_apply_time_correction(run_in_tmpdir): + # Make simple sphere model with elemental Ni + mat = openmc.Material() + mat.add_element('Ni', 1.0) + sphere = openmc.Sphere(r=10.0, boundary_type='vacuum') + cell = openmc.Cell(fill=mat, region=-sphere) + model = openmc.Model() + model.geometry = openmc.Geometry([cell]) + model.settings.run_mode = 'fixed source' + model.settings.batches = 3 + model.settings.particles = 10 + model.settings.photon_transport = True + model.settings.use_decay_photons = True + particle_filter = openmc.ParticleFilter('photon') + tally = openmc.Tally() + tally.filters = [particle_filter] + tally.scores = ['flux'] + model.tallies = [tally] + + # Prepare tallies for D1S and compute time correction factors + nuclides = d1s.prepare_tallies(model, chain_file=CHAIN_PATH) + factors = d1s.time_correction_factors(nuclides, [1.0e10], [1.0]) + + # Run OpenMC and get tally result + with openmc.config.patch('chain_file', CHAIN_PATH): + output_path = model.run() + with openmc.StatePoint(output_path) as sp: + tally = sp.tallies[tally.id] + flux = tally.mean.flatten() + + # Apply TCF and make sure results are consistent + result = d1s.apply_time_correction(tally, factors, sum_nuclides=False) + tcf = np.array([factors[nuc][-1] for nuc in nuclides]) + assert result.mean.flatten() == pytest.approx(tcf * flux) + + # Make sure summed results match a manual sum + result_summed = d1s.apply_time_correction(tally, factors) + assert result_summed.mean.flatten()[0] == pytest.approx(result.mean.sum()) + + # Make sure various tally methods work + result.get_values() + result_summed.get_values() + result.get_reshaped_data() + result_summed.get_reshaped_data() + result.get_pandas_dataframe() + result_summed.get_pandas_dataframe() From 6ae2001400c19a62c2a9e33a26dc6757818d61ff Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Fri, 21 Feb 2025 14:58:37 -0600 Subject: [PATCH 269/671] Enable overlap plotting from Python API (#3310) Co-authored-by: Paul Romano --- openmc/model/model.py | 5 +++++ openmc/plots.py | 13 +++++++++---- tests/unit_tests/test_model.py | 11 +++++++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index c7f358ce8..e8558f4ad 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -863,6 +863,8 @@ class Model: legend: bool = False, axis_units: str = 'cm', outline: bool | str = False, + show_overlaps: bool = False, + overlap_color: Sequence[int] | str | None = None, n_samples: int | None = None, plane_tolerance: float = 1., legend_kwargs: dict | None = None, @@ -941,6 +943,9 @@ class Model: plot.pixels = pixels plot.basis = basis plot.color_by = color_by + plot.show_overlaps = show_overlaps + if overlap_color is not None: + plot.overlap_color = overlap_color if colors is not None: plot.colors = colors self.plots.append(plot) diff --git a/openmc/plots.py b/openmc/plots.py index 246076598..9c1e31575 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -209,16 +209,21 @@ _PLOT_PARAMS = """ legend : bool Whether a legend showing material or cell names should be drawn + .. versionadded:: 0.14.0 + axis_units : {'km', 'm', 'cm', 'mm'} + Units used on the plot axis + .. versionadded:: 0.14.0 outline : bool or str Whether outlines between color boundaries should be drawn. If set to 'only', only outlines will be drawn. .. versionadded:: 0.14.0 - axis_units : {'km', 'm', 'cm', 'mm'} - Units used on the plot axis - - .. versionadded:: 0.14.0 + show_overlaps: bool + Indicate whether or not overlapping regions are shown. + Default is False. + overlap_color: Iterable of int or str + Color to apply to overlapping regions. Default is red. n_samples : int, optional The number of source particles to sample and add to plot. Defaults to None which doesn't plot any particles on the plot. diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 4d7d5be2b..ee5d8895c 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -638,3 +638,14 @@ def test_model_plot(): plot = model.plot(n_samples=1, plane_tolerance=0.1, basis="xy") coords = plot.axes.collections[0].get_offsets().data.flatten() assert (coords == np.array([])).all() + + # modify model to include another cell that overlaps the original cell entirely + model.geometry.root_universe.add_cell(openmc.Cell(region=-surface)) + axes = model.plot(show_overlaps=True) + white = np.array((1.0, 1.0, 1.0)) + red = np.array((1.0, 0.0, 0.0)) + axes_image = axes.get_images()[0] + image_data = axes_image.get_array() + # ensure that all of the data in the image data is either white or red + test_mask = (image_data == white) | (image_data == red) + assert np.all(test_mask), "Colors other than white or red found in overlap plot image" From fefe825e65a46d67f39a985d4b40ec7828d00711 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 21 Feb 2025 17:44:09 -0600 Subject: [PATCH 270/671] Handle reflex angles in CylinderSector (#3303) --- openmc/model/surface_composite.py | 18 ++++++++++-------- tests/unit_tests/test_surface_composite.py | 13 +++++++++++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index 3a17b6169..5962897de 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -135,6 +135,11 @@ class CylinderSector(CompositeSurface): if theta2 <= theta1: raise ValueError('theta2 must be greater than theta1.') + # Determine whether the angle between theta1 and theta2 is a reflex + # angle, in which case we need to use a union between the planar + # half-spaces + self._reflex = (theta2 - theta1 > 180.0) + phi1 = pi / 180 * theta1 phi2 = pi / 180 * theta2 @@ -169,6 +174,9 @@ class CylinderSector(CompositeSurface): **kwargs) self.plane2 = openmc.Plane.from_points(p1, p2_plane2, p3_plane2, **kwargs) + if axis == 'y': + self.plane1.flip_normal() + self.plane2.flip_normal() @classmethod def from_theta_alpha(cls, @@ -223,17 +231,11 @@ class CylinderSector(CompositeSurface): return cls(r1, r2, theta1, theta2, center=center, axis=axis, **kwargs) def __neg__(self): - if isinstance(self.inner_cyl, openmc.YCylinder): - return -self.outer_cyl & +self.inner_cyl & +self.plane1 & -self.plane2 + if self._reflex: + return -self.outer_cyl & +self.inner_cyl & (-self.plane1 | +self.plane2) else: return -self.outer_cyl & +self.inner_cyl & -self.plane1 & +self.plane2 - def __pos__(self): - if isinstance(self.inner_cyl, openmc.YCylinder): - return +self.outer_cyl | -self.inner_cyl | -self.plane1 | +self.plane2 - else: - return +self.outer_cyl | -self.inner_cyl | +self.plane1 | -self.plane2 - class IsogonalOctagon(CompositeSurface): r"""Infinite isogonal octagon composite surface diff --git a/tests/unit_tests/test_surface_composite.py b/tests/unit_tests/test_surface_composite.py index da7ffbcc4..a04fc73c5 100644 --- a/tests/unit_tests/test_surface_composite.py +++ b/tests/unit_tests/test_surface_composite.py @@ -207,6 +207,19 @@ def test_cylinder_sector(axis, indices, center): assert point_neg[indices] in -s assert point_neg[indices] not in +s + # Check __contains__ for sector with reflex angle + s_reflex = openmc.model.CylinderSector( + r1, r2, 0., 270., center=center, axis=axis.lower()) + points = [ + np.array([c1 + r1 + d, c2 + 0.01, 0.]), + np.array([c1, c2 + r1 + d, 0.]), + np.array([c1 - r1 - d, c2, 0.]), + np.array([c1 - 0.01, c2 - r1 - d, 0.]) + ] + for point_neg in points: + assert point_neg[indices] in -s_reflex + assert point_neg[indices] not in +s_reflex + # translate method t = uniform(-5.0, 5.0) s_t = s.translate((t, t, t)) From a2a5c2af19c21094215aaaaba2b66924a5fe7bd7 Mon Sep 17 00:00:00 2001 From: Ahnaf Tahmid Chowdhury Date: Sat, 22 Feb 2025 05:48:11 +0600 Subject: [PATCH 271/671] Add Versioning Support from `version.txt` (#3140) Co-authored-by: Jonathan Shimwell Co-authored-by: Paul Wilson Co-authored-by: Paul Romano --- .git_archival.txt | 3 + .gitattributes | 1 + .github/workflows/ci.yml | 5 +- CMakeLists.txt | 34 +++----- cmake/Modules/GetVersionFromGit.cmake | 109 ++++++++++++++++++++++++++ docs/source/conf.py | 8 +- include/openmc/version.h.in | 6 +- pyproject.toml | 6 +- src/mcpl_interface.cpp | 4 +- src/output.cpp | 21 +++-- 10 files changed, 153 insertions(+), 44 deletions(-) create mode 100644 .git_archival.txt create mode 100644 .gitattributes create mode 100644 cmake/Modules/GetVersionFromGit.cmake diff --git a/.git_archival.txt b/.git_archival.txt new file mode 100644 index 000000000..f2f118c0d --- /dev/null +++ b/.git_archival.txt @@ -0,0 +1,3 @@ +commit: $Format:%H$ +commit-date: $Format:%cI$ +describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$ \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..82bf71c1c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +.git_archival.txt export-subst \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de99bbbe2..7fe3e1557 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,9 +87,10 @@ jobs: RDMAV_FORK_SAFE: 1 steps: - - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 with: - fetch-depth: 2 + fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 diff --git a/CMakeLists.txt b/CMakeLists.txt index f76fcb247..385ce85aa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,11 +1,18 @@ cmake_minimum_required(VERSION 3.16 FATAL_ERROR) project(openmc C CXX) -# Set version numbers -set(OPENMC_VERSION_MAJOR 0) -set(OPENMC_VERSION_MINOR 15) -set(OPENMC_VERSION_RELEASE 1) -set(OPENMC_VERSION ${OPENMC_VERSION_MAJOR}.${OPENMC_VERSION_MINOR}.${OPENMC_VERSION_RELEASE}) +# Set module path +set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/Modules) + +include(GetVersionFromGit) + +# Output version information +message(STATUS "OpenMC version: ${OPENMC_VERSION}") +message(STATUS "OpenMC dev state: ${OPENMC_DEV_STATE}") +message(STATUS "OpenMC commit hash: ${OPENMC_COMMIT_HASH}") +message(STATUS "OpenMC commit count: ${OPENMC_COMMIT_COUNT}") + +# Generate version.h configure_file(include/openmc/version.h.in "${CMAKE_BINARY_DIR}/include/openmc/version.h" @ONLY) # Setup output directories @@ -13,9 +20,6 @@ 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) -# Set module path -set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/Modules) - # Enable correct usage of CXX_EXTENSIONS if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.22) cmake_policy(SET CMP0128 NEW) @@ -240,8 +244,6 @@ endif() #=============================================================================== # Update git submodules as needed #=============================================================================== - -find_package(Git) if(GIT_FOUND AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.git") option(GIT_SUBMODULE "Check submodules during build" ON) if(GIT_SUBMODULE) @@ -493,18 +495,6 @@ if (OPENMC_USE_MPI) target_compile_definitions(libopenmc PUBLIC -DOPENMC_MPI) endif() -# Set git SHA1 hash as a compile definition -if(GIT_FOUND) - execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse HEAD - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - RESULT_VARIABLE GIT_SHA1_SUCCESS - OUTPUT_VARIABLE GIT_SHA1 - ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) - if(GIT_SHA1_SUCCESS EQUAL 0) - target_compile_definitions(libopenmc PRIVATE -DGIT_SHA1="${GIT_SHA1}") - endif() -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} diff --git a/cmake/Modules/GetVersionFromGit.cmake b/cmake/Modules/GetVersionFromGit.cmake new file mode 100644 index 000000000..ee6bac827 --- /dev/null +++ b/cmake/Modules/GetVersionFromGit.cmake @@ -0,0 +1,109 @@ +# GetVersionFromGit.cmake +# Standalone script to retrieve versioning information from Git or .git_archival.txt. +# Customizable for any project by setting variables before including this file. + +# Configurable variables: +# - VERSION_PREFIX: Prefix for version tags (default: "v"). +# - VERSION_SUFFIX: Suffix for version tags (default: "[~+-]([a-zA-Z0-9]+)"). +# - VERSION_REGEX: Regex to extract version (default: "(?[0-9]+\\.[0-9]+\\.[0-9]+)"). +# - ARCHIVAL_FILE: Path to .git_archival.txt (default: "${CMAKE_SOURCE_DIR}/.git_archival.txt"). +# - DESCRIBE_NAME_KEY: Key for describe name in .git_archival.txt (default: "describe-name: "). +# - COMMIT_HASH_KEY: Key for commit hash in .git_archival.txt (default: "commit: "). + +# Default Format Example: +# 1.2.3 v1.2.3 v1.2.3-rc1 + +set(VERSION_PREFIX "v" CACHE STRING "Prefix used in version tags") +set(VERSION_SUFFIX "[~+-]([a-zA-Z0-9]+)" CACHE STRING "Suffix used in version tags") +set(VERSION_REGEX "?([0-9]+\\.[0-9]+\\.[0-9]+)" CACHE STRING "Regex for extracting version") +set(ARCHIVAL_FILE "${CMAKE_SOURCE_DIR}/.git_archival.txt" CACHE STRING "Path to .git_archival.txt") +set(DESCRIBE_NAME_KEY "describe-name: " CACHE STRING "Key for describe name in .git_archival.txt") +set(COMMIT_HASH_KEY "commit: " CACHE STRING "Key for commit hash in .git_archival.txt") + + +# Combine prefix and regex +set(VERSION_REGEX_WITH_PREFIX "^${VERSION_PREFIX}${VERSION_REGEX}") + +# Find Git +find_package(Git) + +# Attempt to retrieve version from Git +if(EXISTS "${CMAKE_SOURCE_DIR}/.git" AND GIT_FOUND) + message(STATUS "Using git describe for versioning") + + # Extract the version string + execute_process( + COMMAND git describe --tags --dirty + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE VERSION_STRING + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + # Extract the commit hash + execute_process( + COMMAND git rev-parse HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE COMMIT_HASH + OUTPUT_STRIP_TRAILING_WHITESPACE + ) +else() + message(STATUS "Using archival file for versioning: ${ARCHIVAL_FILE}") + if(EXISTS "${ARCHIVAL_FILE}") + file(READ "${ARCHIVAL_FILE}" ARCHIVAL_CONTENT) + + # Extract the describe-name line + string(REGEX MATCH "${DESCRIBE_NAME_KEY}([^\\n]+)" VERSION_STRING "${ARCHIVAL_CONTENT}") + if(VERSION_STRING MATCHES "${DESCRIBE_NAME_KEY}(.*)") + set(VERSION_STRING "${CMAKE_MATCH_1}") + else() + message(FATAL_ERROR "Could not extract version from ${ARCHIVAL_FILE}") + endif() + + # Extract the commit hash + string(REGEX MATCH "${COMMIT_HASH_KEY}([a-f0-9]+)" COMMIT_HASH "${ARCHIVAL_CONTENT}") + if(COMMIT_HASH MATCHES "${COMMIT_HASH_KEY}([a-f0-9]+)") + set(COMMIT_HASH "${CMAKE_MATCH_1}") + else() + message(FATAL_ERROR "Could not extract commit hash from ${ARCHIVAL_FILE}") + endif() + else() + message(FATAL_ERROR "Neither git describe nor ${ARCHIVAL_FILE} is available for versioning.") + endif() +endif() + +# Ensure version string format +if(VERSION_STRING MATCHES "${VERSION_REGEX_WITH_PREFIX}") + set(VERSION_NO_SUFFIX "${CMAKE_MATCH_1}") +else() + message(FATAL_ERROR "Invalid version format: Missing base version in ${VERSION_STRING}") +endif() + +# Check for development state +if(VERSION_STRING MATCHES "-([0-9]+)-g([0-9a-f]+)") + set(DEV_STATE "true") + set(COMMIT_COUNT "${CMAKE_MATCH_1}") + string(REGEX REPLACE "-([0-9]+)-g([0-9a-f]+)" "" VERSION_WITHOUT_META "${VERSION_STRING}") +else() + set(DEV_STATE "false") + set(VERSION_WITHOUT_META "${VERSION_STRING}") +endif() + +# Split and set version components +string(REPLACE "." ";" VERSION_LIST "${VERSION_NO_SUFFIX}") +list(GET VERSION_LIST 0 VERSION_MAJOR) +list(GET VERSION_LIST 1 VERSION_MINOR) +list(GET VERSION_LIST 2 VERSION_PATCH) + +# Increment patch number for dev versions +if(DEV_STATE) + math(EXPR VERSION_PATCH "${VERSION_PATCH} + 1") +endif() + +# Export variables +set(OPENMC_VERSION_MAJOR "${VERSION_MAJOR}") +set(OPENMC_VERSION_MINOR "${VERSION_MINOR}") +set(OPENMC_VERSION_PATCH "${VERSION_PATCH}") +set(OPENMC_VERSION "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}") +set(OPENMC_COMMIT_HASH "${COMMIT_HASH}") +set(OPENMC_DEV_STATE "${DEV_STATE}") +set(OPENMC_COMMIT_COUNT "${COMMIT_COUNT}") diff --git a/docs/source/conf.py b/docs/source/conf.py index 60bd406fb..b9db83507 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -68,10 +68,14 @@ copyright = '2011-2024, Massachusetts Institute of Technology, UChicago Argonne # |version| and |release|, also used in various other places throughout the # built documents. # + +import openmc + # The short X.Y version. -version = "0.15" +version = ".".join(openmc.__version__.split('.')[:2]) + # The full version, including alpha/beta/rc tags. -release = "0.15.1-dev" +release = openmc.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/include/openmc/version.h.in b/include/openmc/version.h.in index e1c2b0541..6dfc7c7dd 100644 --- a/include/openmc/version.h.in +++ b/include/openmc/version.h.in @@ -9,8 +9,10 @@ namespace openmc { // clang-format off constexpr int VERSION_MAJOR {@OPENMC_VERSION_MAJOR@}; constexpr int VERSION_MINOR {@OPENMC_VERSION_MINOR@}; -constexpr int VERSION_RELEASE {@OPENMC_VERSION_RELEASE@}; -constexpr bool VERSION_DEV {true}; +constexpr int VERSION_RELEASE {@OPENMC_VERSION_PATCH@}; +constexpr bool VERSION_DEV {@OPENMC_DEV_STATE@}; +constexpr const char* VERSION_COMMIT_COUNT = "@OPENMC_COMMIT_COUNT@"; +constexpr const char* VERSION_COMMIT_HASH = "@OPENMC_COMMIT_HASH@"; constexpr std::array VERSION {VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE}; // clang-format on diff --git a/pyproject.toml b/pyproject.toml index 1f8de10e3..6e8ed798e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools", "wheel"] +requires = ["setuptools", "setuptools-scm", "wheel"] build-backend = "setuptools.build_meta" [project] @@ -8,7 +8,7 @@ authors = [ {name = "The OpenMC Development Team", email = "openmc@anl.gov"}, ] description = "OpenMC" -version = "0.15.1-dev" +dynamic = ["version"] requires-python = ">=3.11" license = {file = "LICENSE"} classifiers = [ @@ -66,3 +66,5 @@ exclude = ['tests*'] "openmc.data.effective_dose" = ["**/*.txt"] "openmc.data" = ["*.txt", "*.DAT", "*.json", "*.h5"] "openmc.lib" = ["libopenmc.dylib", "libopenmc.so"] + +[tool.setuptools_scm] diff --git a/src/mcpl_interface.cpp b/src/mcpl_interface.cpp index a8de5fdb0..83ef63320 100644 --- a/src/mcpl_interface.cpp +++ b/src/mcpl_interface.cpp @@ -207,8 +207,8 @@ void write_mcpl_source_point(const char* filename, span source_bank, if (mpi::master) { file_id = mcpl_create_outfile(filename_.c_str()); if (VERSION_DEV) { - line = fmt::format("OpenMC {0}.{1}.{2}-development", VERSION_MAJOR, - VERSION_MINOR, VERSION_RELEASE); + line = fmt::format("OpenMC {0}.{1}.{2}-dev{3}", VERSION_MAJOR, + VERSION_MINOR, VERSION_RELEASE, VERSION_COMMIT_COUNT); } else { line = fmt::format( "OpenMC {0}.{1}.{2}", VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE); diff --git a/src/output.cpp b/src/output.cpp index 8494450c4..63e7fe553 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -75,13 +75,12 @@ void title() // Write version information fmt::print( " | The OpenMC Monte Carlo Code\n" - " Copyright | 2011-2024 MIT, UChicago Argonne LLC, and contributors\n" + " Copyright | 2011-2025 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" : ""); -#ifdef GIT_SHA1 - fmt::print(" Git SHA1 | {}\n", GIT_SHA1); -#endif + " Version | {}.{}.{}{}{}\n", + VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE, VERSION_DEV ? "-dev" : "", + VERSION_COMMIT_COUNT); + fmt::print(" Commit Hash | {}\n", VERSION_COMMIT_HASH); // Write the date and time fmt::print(" Date/Time | {}\n", time_stamp()); @@ -291,12 +290,10 @@ void print_usage() void print_version() { if (mpi::master) { - fmt::print("OpenMC version {}.{}.{}\n", VERSION_MAJOR, VERSION_MINOR, - VERSION_RELEASE); -#ifdef GIT_SHA1 - fmt::print("Git SHA1: {}\n", GIT_SHA1); -#endif - fmt::print("Copyright (c) 2011-2024 MIT, UChicago Argonne LLC, and " + 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 " "contributors\nMIT/X license at " "\n"); } From 53066768deb4ca809be34c27f9cd9b7cf281a748 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Mon, 24 Feb 2025 09:49:51 -0600 Subject: [PATCH 272/671] Random Ray Void Accuracy Fix (#3316) --- src/random_ray/flat_source_domain.cpp | 10 ++++------ src/random_ray/linear_source_domain.cpp | 3 +-- src/random_ray/random_ray.cpp | 1 + .../random_ray_void/flat/results_true.dat | 4 ++-- .../random_ray_void/linear/results_true.dat | 4 ++-- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index cfe747eec..cd4304b62 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -187,8 +187,7 @@ void FlatSourceDomain::normalize_scalar_flux_and_volumes( source_regions_.volume_naive(sr) = source_regions_.volume(sr) * normalization_factor; source_regions_.volume_sq(sr) = - (source_regions_.volume_sq_t(sr) / source_regions_.volume_t(sr)) * - volume_normalization_factor; + source_regions_.volume_sq_t(sr) / source_regions_.volume_t(sr); source_regions_.volume(sr) = source_regions_.volume_t(sr) * volume_normalization_factor; } @@ -463,12 +462,11 @@ void FlatSourceDomain::convert_source_regions_to_tallies() 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]; + for (int score = 0; score < tally.scores_.size(); score++) { + auto score_bin = tally.scores_[score]; // If a valid tally, filter, and score combination has been found, // then add it to the list of tally tasks for this source element. - TallyTask task(i_tally, filter_index, score_index, score_bin); + TallyTask task(i_tally, filter_index, score, score_bin); source_regions_.tally_task(sr, g).push_back(task); // Also add this task to the list of volume tasks for this source diff --git a/src/random_ray/linear_source_domain.cpp b/src/random_ray/linear_source_domain.cpp index 75d078d1b..a8300ba3f 100644 --- a/src/random_ray/linear_source_domain.cpp +++ b/src/random_ray/linear_source_domain.cpp @@ -127,8 +127,7 @@ void LinearSourceDomain::normalize_scalar_flux_and_volumes( source_regions_.volume(sr) = source_regions_.volume_t(sr) * volume_normalization_factor; source_regions_.volume_sq(sr) = - (source_regions_.volume_sq_t(sr) / source_regions_.volume_t(sr)) * - volume_normalization_factor; + source_regions_.volume_sq_t(sr) / source_regions_.volume_t(sr); if (source_regions_.volume_t(sr) > 0.0) { double inv_volume = 1.0 / source_regions_.volume_t(sr); source_regions_.centroid(sr) = source_regions_.centroid_t(sr); diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp index 9917cf0c5..f9ebb6f9d 100644 --- a/src/random_ray/random_ray.cpp +++ b/src/random_ray/random_ray.cpp @@ -703,6 +703,7 @@ void RandomRay::attenuate_flux_linear_source_void( // source region. The centroid and spatial momements estimates are scaled by // the ray segment length as part of length averaging of the estimates. domain_->source_regions_.volume(sr) += distance; + domain_->source_regions_.volume_sq(sr) += distance_2; domain_->source_regions_.centroid_iteration(sr) += midpoint * distance; moment_matrix_estimate *= distance; domain_->source_regions_.mom_matrix(sr) += moment_matrix_estimate; diff --git a/tests/regression_tests/random_ray_void/flat/results_true.dat b/tests/regression_tests/random_ray_void/flat/results_true.dat index bf395f25f..bd2f2d3b4 100644 --- a/tests/regression_tests/random_ray_void/flat/results_true.dat +++ b/tests/regression_tests/random_ray_void/flat/results_true.dat @@ -1,6 +1,6 @@ tally 1: -1.760401E+00 -1.554914E-01 +2.354630E+00 +2.777456E-01 tally 2: 1.056204E-01 5.741779E-04 diff --git a/tests/regression_tests/random_ray_void/linear/results_true.dat b/tests/regression_tests/random_ray_void/linear/results_true.dat index 77fade829..e289b2f13 100644 --- a/tests/regression_tests/random_ray_void/linear/results_true.dat +++ b/tests/regression_tests/random_ray_void/linear/results_true.dat @@ -1,6 +1,6 @@ tally 1: -1.762592E+00 -1.558698E-01 +2.356821E+00 +2.782548E-01 tally 2: 1.082935E-01 6.029854E-04 From c794065d46c275886ec9fea4eeb6e302b304586f Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Mon, 24 Feb 2025 16:11:18 -0600 Subject: [PATCH 273/671] Fix access order issues after applying tally results from `Model.run`. (#3313) Co-authored-by: Paul Romano --- openmc/statepoint.py | 3 +- openmc/tallies.py | 53 +++++++++++++++++++++++++------- tests/unit_tests/test_tallies.py | 32 ++++++++++++++++++- 3 files changed, 75 insertions(+), 13 deletions(-) diff --git a/openmc/statepoint.py b/openmc/statepoint.py index 5720f8cd0..f2fd48066 100644 --- a/openmc/statepoint.py +++ b/openmc/statepoint.py @@ -6,6 +6,7 @@ import warnings import h5py import numpy as np +from pathlib import Path from uncertainties import ufloat import openmc @@ -415,7 +416,7 @@ class StatePoint: # Create Tally object and assign basic properties tally = openmc.Tally(tally_id) - tally._sp_filename = self._f.filename + tally._sp_filename = Path(self._f.filename) tally.name = group['name'][()].decode() if 'name' in group else '' # Check if tally has multiply_density attribute diff --git a/openmc/tallies.py b/openmc/tallies.py index b0d78d357..585e54a7e 100644 --- a/openmc/tallies.py +++ b/openmc/tallies.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections.abc import Iterable, MutableSequence import copy -from functools import partial, reduce +from functools import partial, reduce, wraps from itertools import product from numbers import Integral, Real import operator @@ -179,6 +179,25 @@ class Tally(IDManagerMixin): parts.append('{: <15}=\t{}'.format('Multiply dens.', self.multiply_density)) return '\n\t'.join(parts) + @staticmethod + def ensure_results(f): + """A decorator to be applied to any method that might use tally results. + Results will be loaded if appropriate based on the tally properties. + + Args: + f function: Tally method to wrap + + Returns: + function: Wrapped function that reads tally results before calling + the methodif necessary + """ + @wraps(f) + def read(self): + if self._sp_filename is not None and not self.derived: + self._read_results() + return f(self) + return read + @property def name(self): return self._name @@ -218,6 +237,7 @@ class Tally(IDManagerMixin): self._filters = cv.CheckedList(_FILTER_CLASSES, 'tally filters', filters) @property + @ensure_results def nuclides(self): return self._nuclides @@ -314,6 +334,7 @@ class Tally(IDManagerMixin): triggers) @property + @ensure_results def num_realizations(self): return self._num_realizations @@ -340,11 +361,11 @@ class Tally(IDManagerMixin): with h5py.File(self._sp_filename, 'r') as f: # Set number of realizations group = f[f'tallies/tally {self.id}'] - self.num_realizations = int(group['n_realizations'][()]) + self._num_realizations = int(group['n_realizations'][()]) # Update nuclides nuclide_names = group['nuclides'][()] - self.nuclides = [name.decode().strip() for name in nuclide_names] + self._nuclides = [name.decode().strip() for name in nuclide_names] # Extract Tally data from the file data = group['results'] @@ -368,13 +389,11 @@ class Tally(IDManagerMixin): self._results_read = True @property + @ensure_results def sum(self): if not self._sp_filename or self.derived: return None - # Make sure results have been read - self._read_results() - if self.sparse: return np.reshape(self._sum.toarray(), self.shape) else: @@ -386,13 +405,11 @@ class Tally(IDManagerMixin): self._sum = sum @property + @ensure_results def sum_sq(self): if not self._sp_filename or self.derived: return None - # Make sure results have been read - self._read_results() - if self.sparse: return np.reshape(self._sum_sq.toarray(), self.shape) else: @@ -935,10 +952,24 @@ class Tally(IDManagerMixin): statepoint : openmc.PathLike or openmc.StatePoint Statepoint used to update tally results """ + # derived tallies are populated with data based on combined tallies + # and should not be modified + if self.derived: + return + if isinstance(statepoint, openmc.StatePoint): - self._sp_filename = statepoint._f.filename + self._sp_filename = Path(statepoint._f.filename) else: - self._sp_filename = str(statepoint) + self._sp_filename = Path(str(statepoint)) + + # reset these properties to ensure that any results access after this + # point are based on the current statepoint file + self._sum = None + self._sum_sq = None + self._mean = None + self._std_dev = None + self._num_realizations = 0 + self._results_read = False @classmethod def from_xml_element(cls, elem, **kwargs): diff --git a/tests/unit_tests/test_tallies.py b/tests/unit_tests/test_tallies.py index f044df5d0..ccc6ff343 100644 --- a/tests/unit_tests/test_tallies.py +++ b/tests/unit_tests/test_tallies.py @@ -106,14 +106,44 @@ def test_tally_application(sphere_model, run_in_tmpdir): tally.scores = ['flux', 'absorption', 'fission', 'scatter'] sphere_model.tallies = [tally] + # FIRST RUN # run the simulation and apply results sp_file = sphere_model.run(apply_tally_results=True) + # before calling for any property requiring results (including the equivalence check below), + # the following internal attributes of the original should be unset + assert tally._mean is None + assert tally._std_dev is None + assert tally._sum is None + assert tally._sum_sq is None + assert tally._num_realizations == 0 + # the statepoint file property should be set, however + assert tally._sp_filename == sp_file + with openmc.StatePoint(sp_file) as sp: assert tally in sp.tallies.values() sp_tally = sp.tallies[tally.id] # at this point the tally information regarding results should be the same - assert (sp_tally.mean == tally.mean).all() assert (sp_tally.std_dev == tally.std_dev).all() + assert (sp_tally.mean == tally.mean).all() assert sp_tally.nuclides == tally.nuclides + # SECOND RUN + # change the number of particles and ensure that the results are different + sphere_model.settings.particles += 1 + sp_file = sphere_model.run(apply_tally_results=True) + + assert (sp_tally.std_dev != tally.std_dev).any() + assert (sp_tally.mean != tally.mean).any() + + # now re-read data from the new stateopint file and + # ensure that the new results match those in + # the latest statepoint + with openmc.StatePoint(sp_file) as sp: + assert tally in sp.tallies.values() + sp_tally = sp.tallies[tally.id] + + # at this point the tally information regarding results should be the same + assert (sp_tally.std_dev == tally.std_dev).all() + assert (sp_tally.mean == tally.mean).all() + assert sp_tally.nuclides == tally.nuclides From cba132c4b487a2c8cd6318f6a049354575e2a7f4 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Mon, 24 Feb 2025 17:25:11 -0600 Subject: [PATCH 274/671] Random Ray Linear Source Stability Improvement (#3322) --- src/random_ray/linear_source_domain.cpp | 16 +- .../linear/results_true.dat | 12 +- .../linear_xy/results_true.dat | 12 +- .../linear_xy/results_true.dat | 168 +++++++++--------- .../test.py | 2 +- .../random_ray_void/linear/results_true.dat | 12 +- .../hybrid/results_true.dat | 12 +- .../naive/results_true.dat | 12 +- .../simulation_averaged/results_true.dat | 12 +- 9 files changed, 132 insertions(+), 126 deletions(-) diff --git a/src/random_ray/linear_source_domain.cpp b/src/random_ray/linear_source_domain.cpp index a8300ba3f..3819c7d70 100644 --- a/src/random_ray/linear_source_domain.cpp +++ b/src/random_ray/linear_source_domain.cpp @@ -78,13 +78,19 @@ void LinearSourceDomain::update_neutron_source(double k_eff) source_regions_.source(sr, g_out) = (scatter_flat + fission_flat * inverse_k_eff) / sigma_t; - // Compute the linear source terms - // In the first 10 iterations when the centroids and spatial moments - // are not well known, we will leave the source gradients as zero - // so as to avoid causing any numerical instability. - if (simulation::current_batch > 10) { + // Compute the linear source terms. In the first 10 iterations when the + // centroids and spatial moments are not well known, we will leave the + // source gradients as zero so as to avoid causing any numerical + // instability. If a negative source is encountered, this region must be + // very small/noisy or have poorly developed spatial moments, so we zero + // the source gradients (effectively making this a flat source region + // temporarily), so as to improve stability. + if (simulation::current_batch > 10 && + source_regions_.source(sr, g_out) >= 0.0) { source_regions_.source_gradients(sr, g_out) = invM * ((scatter_linear + fission_linear * inverse_k_eff) / sigma_t); + } else { + source_regions_.source_gradients(sr, g_out) = {0.0, 0.0, 0.0}; } } } diff --git a/tests/regression_tests/random_ray_fixed_source_linear/linear/results_true.dat b/tests/regression_tests/random_ray_fixed_source_linear/linear/results_true.dat index 46200b19c..2fc08d0a4 100644 --- a/tests/regression_tests/random_ray_fixed_source_linear/linear/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_linear/linear/results_true.dat @@ -1,9 +1,9 @@ tally 1: -2.339084E+00 -2.747299E-01 +2.339085E+00 +2.747304E-01 tally 2: -1.090051E-01 -6.073265E-04 +1.089827E-01 +6.069324E-04 tally 3: -7.300117E-03 -2.715425E-06 +7.300831E-03 +2.715940E-06 diff --git a/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/results_true.dat b/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/results_true.dat index ea711a925..f4c8546aa 100644 --- a/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/results_true.dat @@ -1,9 +1,9 @@ tally 1: -2.335701E+00 -2.742860E-01 +2.335703E+00 +2.742866E-01 tally 2: -1.081600E-01 -5.980902E-04 +1.081884E-01 +5.983316E-04 tally 3: -7.295015E-03 -2.711589E-06 +7.295389E-03 +2.711859E-06 diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/results_true.dat b/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/results_true.dat index 43a6d8258..49813144d 100644 --- a/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/results_true.dat @@ -1,168 +1,168 @@ tally 1: -1.583461E+02 -1.007023E+03 +1.583465E+02 +1.007029E+03 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.891511E+01 -1.392800E+02 +5.891526E+01 +1.392807E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.028333E+01 -1.649217E+01 +2.028338E+01 +1.649224E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.453681E+01 -2.413486E+01 +2.453687E+01 +2.413498E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.235799E+01 -1.099237E+02 +5.235812E+01 +1.099243E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -9.394382E+01 -3.543494E+02 +9.394406E+01 +3.543513E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.676269E+01 -1.303097E+02 +5.676284E+01 +1.303104E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.007853E+02 -1.616208E+03 +2.007858E+02 +1.616216E+03 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -7.319692E+01 -2.146291E+02 +7.319709E+01 +2.146301E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.566725E+01 -2.640687E+01 +2.566730E+01 +2.640697E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.934697E+01 -3.453012E+01 +2.934703E+01 +3.453026E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.737202E+01 -1.318574E+02 +5.737215E+01 +1.318580E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -9.495251E+01 -3.613355E+02 +9.495275E+01 +3.613372E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.799236E+01 -1.356172E+02 +5.799250E+01 +1.356178E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.066411E+02 -4.570264E+02 -1.254011E+01 -6.325603E+00 -3.052011E+01 -3.746896E+01 -4.977289E+01 -9.940494E+01 -2.345784E+00 -2.208432E-01 -5.709169E+00 -1.308139E+00 -1.953078E+01 -1.528285E+01 -1.998229E-01 -1.599785E-03 -4.863290E-01 -9.476135E-03 -2.307190E+01 -2.133049E+01 -2.432475E-01 -2.371063E-03 -5.920158E-01 -1.404471E-02 -5.299849E+01 -1.125570E+02 -1.959909E-01 -1.539183E-03 -4.770085E-01 -9.117394E-03 -1.186130E+02 -5.641029E+02 -5.781214E-02 -1.341752E-04 -1.430521E-01 -8.215282E-04 -7.995712E+01 -2.570085E+02 -3.452925E-01 -4.844032E-03 -9.604136E-01 -3.747567E-02 -1.556790E+02 -9.726171E+02 +1.066414E+02 +4.570291E+02 +1.254014E+01 +6.325640E+00 +3.052020E+01 +3.746918E+01 +4.977302E+01 +9.940548E+01 +2.345790E+00 +2.208444E-01 +5.709184E+00 +1.308146E+00 +1.953082E+01 +1.528292E+01 +1.998234E-01 +1.599792E-03 +4.863302E-01 +9.476180E-03 +2.307196E+01 +2.133059E+01 +2.432481E-01 +2.371074E-03 +5.920173E-01 +1.404478E-02 +5.299862E+01 +1.125576E+02 +1.959914E-01 +1.539191E-03 +4.770098E-01 +9.117441E-03 +1.186133E+02 +5.641060E+02 +5.781230E-02 +1.341759E-04 +1.430525E-01 +8.215326E-04 +7.995734E+01 +2.570099E+02 +3.452934E-01 +4.844058E-03 +9.604162E-01 +3.747587E-02 +1.556795E+02 +9.726232E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.816710E+01 -1.357523E+02 +5.816726E+01 +1.357531E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.018475E+01 -1.633543E+01 +2.018480E+01 +1.633551E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.453470E+01 -2.413604E+01 +2.453476E+01 +2.413615E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.269366E+01 -1.113096E+02 +5.269380E+01 +1.113102E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -9.550190E+01 -3.658245E+02 +9.550216E+01 +3.658264E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.846305E+01 -1.379618E+02 +5.846320E+01 +1.379625E+02 0.000000E+00 0.000000E+00 0.000000E+00 diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/test.py b/tests/regression_tests/random_ray_fixed_source_subcritical/test.py index 6c2c569c6..e2f3cf175 100644 --- a/tests/regression_tests/random_ray_fixed_source_subcritical/test.py +++ b/tests/regression_tests/random_ray_fixed_source_subcritical/test.py @@ -17,7 +17,7 @@ class MGXSTestHarness(TolerantPyAPITestHarness): @pytest.mark.parametrize("shape", ["flat", "linear_xy"]) -def test_random_ray_source(shape): +def test_random_ray_fixed_source_subcritical(shape): with change_directory(shape): openmc.reset_auto_ids() diff --git a/tests/regression_tests/random_ray_void/linear/results_true.dat b/tests/regression_tests/random_ray_void/linear/results_true.dat index e289b2f13..befa507a5 100644 --- a/tests/regression_tests/random_ray_void/linear/results_true.dat +++ b/tests/regression_tests/random_ray_void/linear/results_true.dat @@ -1,9 +1,9 @@ tally 1: -2.356821E+00 -2.782548E-01 +2.356818E+00 +2.782542E-01 tally 2: -1.082935E-01 -6.029854E-04 +1.082843E-01 +6.028734E-04 tally 3: -7.301975E-03 -2.717553E-06 +7.302705E-03 +2.718076E-06 diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/results_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/results_true.dat index 46200b19c..2fc08d0a4 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/results_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/results_true.dat @@ -1,9 +1,9 @@ tally 1: -2.339084E+00 -2.747299E-01 +2.339085E+00 +2.747304E-01 tally 2: -1.090051E-01 -6.073265E-04 +1.089827E-01 +6.069324E-04 tally 3: -7.300117E-03 -2.715425E-06 +7.300831E-03 +2.715940E-06 diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/naive/results_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/naive/results_true.dat index 02bfc5266..5258ffd9c 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/naive/results_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator_linear/naive/results_true.dat @@ -1,9 +1,9 @@ tally 1: -2.339575E+00 -2.748441E-01 +2.339567E+00 +2.748423E-01 tally 2: -1.086360E-01 -6.030557E-04 +1.085878E-01 +6.024509E-04 tally 3: -7.298937E-03 -2.741185E-06 +7.299803E-03 +2.741867E-06 diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/results_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/results_true.dat index 93e54feea..1e8aa9fb7 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/results_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/results_true.dat @@ -1,9 +1,9 @@ tally 1: -2.670874E+02 -4.432876E+05 +2.670850E+02 +4.432939E+05 tally 2: -1.117459E-01 -6.497788E-04 +1.116994E-01 +6.491358E-04 tally 3: -7.563753E-03 -2.947210E-06 +7.564527E-03 +2.947794E-06 From 244d630fd2c34d8473a0919bc614b499f8642414 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 24 Feb 2025 17:25:33 -0600 Subject: [PATCH 275/671] Clarify effect of CMAKE_BUILD_TYPE in docs (#3321) --- docs/source/usersguide/install.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/source/usersguide/install.rst b/docs/source/usersguide/install.rst index b86f5fee0..bb54594de 100644 --- a/docs/source/usersguide/install.rst +++ b/docs/source/usersguide/install.rst @@ -425,13 +425,16 @@ OpenMC can be configured for debug, release, or release with debug info by setti the `CMAKE_BUILD_TYPE` option. Debug - Enable debug compiler flags with no optimization `-O0 -g`. + Enable debug compiler flags with no optimization. On most platforms/compilers, + this is equivalent to `-O0 -g`. Release - Disable debug and enable optimization `-O3 -DNDEBUG`. + Disable debug and enable optimization. On most platforms/compilers, this is + equivalent to `-O3 -DNDEBUG`. RelWithDebInfo - (Default if no type is specified.) Enable optimization and debug `-O2 -g`. + (Default if no type is specified.) Enable optimization and debug. On most + platforms/compilers, this is equivalent to `-O2 -g`. Example of configuring for Debug mode: From fed4b2861687aae2943358f7be6e3cbb1057de59 Mon Sep 17 00:00:00 2001 From: Micah Gale Date: Mon, 24 Feb 2025 21:49:12 -0600 Subject: [PATCH 276/671] Mark a canonical URL for docs (#3324) --- docs/source/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index b9db83507..618cbe648 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -127,6 +127,7 @@ pygments_style = 'tango' import sphinx_rtd_theme html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +html_baseurl = "https://docs.openmc.org/en/stable/" html_logo = '_images/openmc_logo.png' From 1729b3bf91f4a99c57f5f330c5e18560c658a084 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 24 Feb 2025 21:49:47 -0600 Subject: [PATCH 277/671] Fixes for problems encountered with version determination (#3320) --- .readthedocs.yaml | 4 +++- Dockerfile | 2 +- LICENSE | 2 +- cmake/Modules/GetVersionFromGit.cmake | 5 +++++ docs/source/conf.py | 7 ++----- docs/source/license.rst | 2 +- man/man1/openmc.1 | 2 +- 7 files changed, 14 insertions(+), 10 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 8bbc7d01f..3578144b2 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -4,7 +4,9 @@ build: os: "ubuntu-24.04" tools: python: "3.12" - + jobs: + post_checkout: + - git fetch --unshallow || true sphinx: configuration: docs/source/conf.py diff --git a/Dockerfile b/Dockerfile index 583682ef5..1ae615a50 100644 --- a/Dockerfile +++ b/Dockerfile @@ -194,7 +194,7 @@ ENV LIBMESH_INSTALL_DIR=$HOME/LIBMESH # clone and install openmc RUN mkdir -p ${HOME}/OpenMC && cd ${HOME}/OpenMC \ - && git clone --shallow-submodules --recurse-submodules --single-branch -b ${openmc_branch} --depth=1 ${OPENMC_REPO} \ + && git clone --shallow-submodules --recurse-submodules --single-branch -b ${openmc_branch} ${OPENMC_REPO} \ && mkdir build && cd build ; \ if [ ${build_dagmc} = "on" ] && [ ${build_libmesh} = "on" ]; then \ cmake ../openmc \ diff --git a/LICENSE b/LICENSE index 1f9198c1e..8a60b66bf 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2011-2024 Massachusetts Institute of Technology, UChicago Argonne +Copyright (c) 2011-2025 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/cmake/Modules/GetVersionFromGit.cmake b/cmake/Modules/GetVersionFromGit.cmake index ee6bac827..42b3725f2 100644 --- a/cmake/Modules/GetVersionFromGit.cmake +++ b/cmake/Modules/GetVersionFromGit.cmake @@ -39,6 +39,11 @@ if(EXISTS "${CMAKE_SOURCE_DIR}/.git" AND GIT_FOUND) OUTPUT_STRIP_TRAILING_WHITESPACE ) + # If no tags are found, instruct user to fetch them + if(VERSION_STRING STREQUAL "") + message(FATAL_ERROR "No git tags found. Run 'git fetch --tags' and try again.") + endif() + # Extract the commit hash execute_process( COMMAND git rev-parse HEAD diff --git a/docs/source/conf.py b/docs/source/conf.py index 618cbe648..8aeff5cdc 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-2024, Massachusetts Institute of Technology, UChicago Argonne LLC, and OpenMC contributors' +copyright = '2011-2025, 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 @@ -71,11 +71,8 @@ copyright = '2011-2024, Massachusetts Institute of Technology, UChicago Argonne import openmc -# The short X.Y version. -version = ".".join(openmc.__version__.split('.')[:2]) - # The full version, including alpha/beta/rc tags. -release = openmc.__version__ +version = release = openmc.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/source/license.rst b/docs/source/license.rst index 23315e8fe..0a90a7441 100644 --- a/docs/source/license.rst +++ b/docs/source/license.rst @@ -4,7 +4,7 @@ License Agreement ================= -Copyright © 2011-2024 Massachusetts Institute of Technology, UChicago Argonne +Copyright © 2011-2025 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 d121a8956..7826a759a 100644 --- a/man/man1/openmc.1 +++ b/man/man1/openmc.1 @@ -63,7 +63,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-2024 Massachusetts Institute of Technology, UChicago +Copyright \(co 2011-2025 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 From 27258c009c60a6ca9b3be3e300c0811eab3de72f Mon Sep 17 00:00:00 2001 From: John Tramm Date: Tue, 25 Feb 2025 10:34:28 -0600 Subject: [PATCH 278/671] Random Ray Adjoint Source Logic Improvement (#3325) --- src/random_ray/flat_source_domain.cpp | 27 ++++++++++++------- .../inputs_true.dat | 3 ++- .../results_true.dat | 12 ++++----- .../random_ray_adjoint_fixed_source/test.py | 2 ++ 4 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index cd4304b62..ae0ecb824 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -1049,14 +1049,21 @@ void FlatSourceDomain::flatten_xs() void FlatSourceDomain::set_adjoint_sources(const vector& forward_flux) { - // Set the external source to 1/forward_flux - // The forward flux is given in terms of total for the forward simulation - // so we must convert it to a "per batch" quantity + // Set the external source to 1/forward_flux. If the forward flux is negative + // or zero, set the adjoint source to zero, as this is likely a very small + // source region that we don't need to bother trying to vector particles + // towards. Flux negativity in random ray is not related to the flux being + // small in magnitude, but rather due to the source region being physically + // small in volume and thus having a noisy flux estimate. #pragma omp parallel for for (int64_t sr = 0; sr < n_source_regions_; sr++) { for (int g = 0; g < negroups_; g++) { - source_regions_.external_source(sr, g) = - 1.0 / forward_flux[sr * negroups_ + g]; + double flux = forward_flux[sr * negroups_ + g]; + if (flux <= 0.0) { + source_regions_.external_source(sr, g) = 0.0; + } else { + source_regions_.external_source(sr, g) = 1.0 / flux; + } } } @@ -1064,12 +1071,12 @@ void FlatSourceDomain::set_adjoint_sources(const vector& forward_flux) // iteration) #pragma omp parallel for for (int64_t sr = 0; sr < n_source_regions_; sr++) { + int material = source_regions_.material(sr); + if (material == MATERIAL_VOID) { + continue; + } for (int g = 0; g < negroups_; g++) { - int material = source_regions_.material(sr); - if (material == MATERIAL_VOID) { - continue; - } - double sigma_t = sigma_t_[source_regions_.material(sr) * negroups_ + g]; + double sigma_t = sigma_t_[material * negroups_ + g]; source_regions_.external_source(sr, g) /= sigma_t; } } diff --git a/tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat b/tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat index 686987512..65ffd5af7 100644 --- a/tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat +++ b/tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat @@ -191,7 +191,7 @@ fixed source - 90 + 500 10 5 @@ -214,6 +214,7 @@ True True + naive diff --git a/tests/regression_tests/random_ray_adjoint_fixed_source/results_true.dat b/tests/regression_tests/random_ray_adjoint_fixed_source/results_true.dat index a216fa9fc..7178e2f11 100644 --- a/tests/regression_tests/random_ray_adjoint_fixed_source/results_true.dat +++ b/tests/regression_tests/random_ray_adjoint_fixed_source/results_true.dat @@ -1,9 +1,9 @@ tally 1: --7.235364E+03 -3.367109E+09 +5.790516E+04 +6.740859E+08 tally 2: -4.818311E+05 -6.269371E+10 +6.885455E+04 +9.482551E+08 tally 3: -1.515641E+06 -4.598791E+11 +1.956327E+05 +7.654469E+09 diff --git a/tests/regression_tests/random_ray_adjoint_fixed_source/test.py b/tests/regression_tests/random_ray_adjoint_fixed_source/test.py index 0295e36e9..6c2790fa0 100644 --- a/tests/regression_tests/random_ray_adjoint_fixed_source/test.py +++ b/tests/regression_tests/random_ray_adjoint_fixed_source/test.py @@ -16,5 +16,7 @@ class MGXSTestHarness(TolerantPyAPITestHarness): def test_random_ray_adjoint_fixed_source(): model = random_ray_three_region_cube() model.settings.random_ray['adjoint'] = True + model.settings.random_ray['volume_estimator'] = 'naive' + model.settings.particles = 500 harness = MGXSTestHarness('statepoint.10.h5', model) harness.main() From 865c80a5f9a51546c9ed56d059798a68ede241fe Mon Sep 17 00:00:00 2001 From: Olek <45364492+yardasol@users.noreply.github.com> Date: Tue, 25 Feb 2025 13:50:00 -0600 Subject: [PATCH 279/671] Reflect multigroup MicroXS in IndependentOperator docstrings (#3327) Co-authored-by: Paul Romano --- openmc/deplete/independent_operator.py | 74 ++++++++++++++------------ openmc/deplete/openmc_operator.py | 15 +----- 2 files changed, 42 insertions(+), 47 deletions(-) diff --git a/openmc/deplete/independent_operator.py b/openmc/deplete/independent_operator.py index 226682ffa..333ead0f8 100644 --- a/openmc/deplete/independent_operator.py +++ b/openmc/deplete/independent_operator.py @@ -1,7 +1,7 @@ """Transport-independent transport operator for depletion. This module implements a transport operator that runs independently of any -transport solver by using user-provided one-group cross sections. +transport solver by using user-provided multigroup fluxes and cross sections. """ @@ -24,14 +24,12 @@ from .helpers import ChainFissionHelper, ConstantFissionYieldHelper, SourceRateH class IndependentOperator(OpenMCOperator): - """Transport-independent transport operator that uses one-group cross - sections to calculate reaction rates. + """Transport-independent transport operator based on multigroup data. - Instances of this class can be used to perform depletion using one-group - cross sections and constant flux or constant power. Normally, a user needn't - call methods of this class directly. Instead, an instance of this class is - passed to an integrator class, such as - :class:`openmc.deplete.CECMIntegrator`. + Instances of this class can be used to perform depletion using multigroup + cross sections and multigroup fluxes. Normally, a user needn't call methods + of this class directly. Instead, an instance of this class is passed to an + integrator class, such as :class:`openmc.deplete.CECMIntegrator`. Note that passing an empty :class:`~openmc.deplete.MicroXS` instance to the ``micro_xs`` argument allows a decay-only calculation to be run. @@ -57,43 +55,41 @@ class IndependentOperator(OpenMCOperator): ``openmc.config['chain_file']``. keff : 2-tuple of float, optional keff eigenvalue and uncertainty from transport calculation. - Default is None. prev_results : Results, optional Results from a previous depletion calculation. normalization_mode : {"fission-q", "source-rate"} - Indicate how reaction rates should be calculated. - ``"fission-q"`` uses the fission Q values from the depletion chain to - compute the flux based on the power. ``"source-rate"`` uses a the - source rate (assumed to be neutron flux) to calculate the - reaction rates. + Indicate how reaction rates should be calculated. ``"fission-q"`` uses + the fission Q values from the depletion chain to compute the flux based + on the power. ``"source-rate"`` uses a the source rate (assumed to be + neutron flux) to calculate the reaction rates. fission_q : dict, optional Dictionary of nuclides and their fission Q values [eV]. If not given, - values will be pulled from the ``chain_file``. Only applicable - if ``"normalization_mode" == "fission-q"``. + values will be pulled from the ``chain_file``. Only applicable if + ``"normalization_mode" == "fission-q"``. reduce_chain : bool, optional - If True, use :meth:`openmc.deplete.Chain.reduce` to reduce the - depletion chain up to ``reduce_chain_level``. + If True, use :meth:`openmc.deplete.Chain.reduce` to reduce the depletion + chain up to ``reduce_chain_level``. reduce_chain_level : int, optional - Depth of the search when reducing the depletion chain. Only used - if ``reduce_chain`` evaluates to true. The default value of - ``None`` implies no limit on the depth. + Depth of the search when reducing the depletion chain. Only used if + ``reduce_chain`` evaluates to true. The default value of ``None`` + implies no limit on the depth. fission_yield_opts : dict of str to option, optional Optional arguments to pass to the :class:`openmc.deplete.helpers.FissionYieldHelper` object. Will be - passed directly on to the helper. Passing a value of None will use - the defaults for the associated helper. + passed directly on to the helper. Passing a value of None will use the + defaults for the associated helper. Attributes ---------- materials : openmc.Materials All materials present in the model - cross_sections : MicroXS - Object containing one-group cross-sections in [cm^2]. + cross_sections : list of MicroXS + Object containing multigroup cross-sections in [b] for each material. output_dir : pathlib.Path Path to output directory to save results. round_number : bool - Whether or not to round output to OpenMC to 8 digits. - Useful in testing, as OpenMC is incredibly sensitive to exact values. + Whether or not to round output to OpenMC to 8 digits. Useful in testing, + as OpenMC is incredibly sensitive to exact values. number : openmc.deplete.AtomNumber Total number of atoms in simulation. nuclides_with_data : set of str @@ -109,8 +105,8 @@ class IndependentOperator(OpenMCOperator): local_mats : list of str All burnable material IDs being managed by a single process prev_res : Results or None - Results from a previous depletion calculation. ``None`` if no - results are to be used. + Results from a previous depletion calculation. ``None`` if no results + are to be used. """ @@ -279,14 +275,26 @@ class IndependentOperator(OpenMCOperator): self.prev_res.append(new_res) def _get_nuclides_with_data(self, cross_sections: list[MicroXS]) -> set[str]: - """Finds nuclides with cross section data""" + """Finds nuclides with cross section data + + Parameters + ---------- + cross_sections : iterable of :class`~openmc.deplete.MicroXS` + List of multigroup cross-section data. + + Returns + ------- + nuclides : set of str + Set of nuclide names that have cross secton data + + """ return set(cross_sections[0].nuclides) class _IndependentRateHelper(ReactionRateHelper): - """Class for generating one-group reaction rates with flux and - one-group cross sections. + """Class for generating reaction rates with multigroup fluxes and + multigroup cross sections. - This class does not generate tallies, and instead stores cross sections + This class does not generate tallies and instead stores cross sections for each nuclide and transmutation reaction relevant for a depletion calculation. The reaction rate is calculated by multiplying the flux by the cross sections. diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py index 049e0dd37..64cb0a7e5 100644 --- a/openmc/deplete/openmc_operator.py +++ b/openmc/deplete/openmc_operator.py @@ -247,20 +247,7 @@ class OpenMCOperator(TransportOperator): @abstractmethod def _get_nuclides_with_data(self, cross_sections): - """Find nuclides with cross section data - - Parameters - ---------- - cross_sections : str or pandas.DataFrame - Path to continuous energy cross section library, or object - containing one-group cross-sections. - - Returns - ------- - nuclides : set of str - Set of nuclide names that have cross secton data - - """ + """Find nuclides with cross section data.""" def _extract_number(self, local_mats, volume, all_nuclides, prev_res=None): """Construct AtomNumber using geometry From e060534ff1d79885ef2b20ae169a8b9b5e63c3c5 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 26 Feb 2025 08:14:53 -0600 Subject: [PATCH 280/671] Compute material volumes in mesh elements based on raytracing (#3129) Co-authored-by: Olek <45364492+yardasol@users.noreply.github.com> Co-authored-by: Patrick Shriwise --- docs/source/pythonapi/base.rst | 1 + include/openmc/bounding_box.h | 4 + include/openmc/capi.h | 4 +- include/openmc/mesh.h | 95 +++++-- openmc/lib/mesh.py | 131 ++++++---- openmc/mesh.py | 227 +++++++++++++--- src/mesh.cpp | 465 ++++++++++++++++++++++++++------- src/particle.cpp | 1 - tests/unit_tests/test_lib.py | 36 +-- tests/unit_tests/test_mesh.py | 88 ++++++- 10 files changed, 814 insertions(+), 238 deletions(-) diff --git a/docs/source/pythonapi/base.rst b/docs/source/pythonapi/base.rst index bfd942727..e5b5b17c3 100644 --- a/docs/source/pythonapi/base.rst +++ b/docs/source/pythonapi/base.rst @@ -152,6 +152,7 @@ Constructing Tallies openmc.CylindricalMesh openmc.SphericalMesh openmc.UnstructuredMesh + openmc.MeshMaterialVolumes openmc.Trigger openmc.TallyDerivative openmc.Tally diff --git a/include/openmc/bounding_box.h b/include/openmc/bounding_box.h index 40f603583..d02d92cb4 100644 --- a/include/openmc/bounding_box.h +++ b/include/openmc/bounding_box.h @@ -4,6 +4,7 @@ #include // for min, max #include "openmc/constants.h" +#include "openmc/position.h" namespace openmc { @@ -54,6 +55,9 @@ struct BoundingBox { zmax = std::max(zmax, other.zmax); return *this; } + + inline Position min() const { return {xmin, ymin, zmin}; } + inline Position max() const { return {xmax, ymax, zmax}; } }; } // namespace openmc diff --git a/include/openmc/capi.h b/include/openmc/capi.h index 8edd99c07..3802692c3 100644 --- a/include/openmc/capi.h +++ b/include/openmc/capi.h @@ -110,8 +110,8 @@ int openmc_mesh_get_id(int32_t index, int32_t* id); int openmc_mesh_set_id(int32_t index, int32_t id); int openmc_mesh_get_n_elements(int32_t index, size_t* n); int openmc_mesh_get_volumes(int32_t index, double* volumes); -int openmc_mesh_material_volumes(int32_t index, int n_sample, int bin, - int result_size, void* result, int* hits, uint64_t* seed); +int openmc_mesh_material_volumes(int32_t index, int nx, int ny, int nz, + int max_mats, int32_t* materials, double* volumes); int openmc_meshsurface_filter_get_mesh(int32_t index, int32_t* index_mesh); int openmc_meshsurface_filter_set_mesh(int32_t index, int32_t index_mesh); int openmc_new_filter(const char* type, int32_t* index); diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index 3aef49c7e..943e49a5f 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -69,14 +69,70 @@ extern const libMesh::Parallel::Communicator* libmesh_comm; } // namespace settings #endif +//============================================================================== +//! Helper class for keeping track of volume for each material in a mesh element +// +//! This class is used in Mesh::material_volumes to manage for each mesh element +//! a list of (material, volume) pairs. The openmc.lib.Mesh class allocates two +//! 2D arrays, one for materials and one for volumes. Because we don't know a +//! priori how many materials there are in each element but at the same time we +//! can't dynamically size an array at runtime for performance reasons, we +//! assume a maximum number of materials per element. For each element, the set +//! of material indices are stored in a hash table with twice as many slots as +//! the assumed maximum number of materials per element. Collision resolution is +//! handled by open addressing with linear probing. +//============================================================================== + +namespace detail { + +class MaterialVolumes { +public: + MaterialVolumes(int32_t* mats, double* vols, int table_size) + : materials_(mats), volumes_(vols), table_size_(table_size) + {} + + //! Add volume for a given material in a mesh element + // + //! \param[in] index_elem Index of the mesh element + //! \param[in] index_material Index of the material within the model + //! \param[in] volume Volume to add + void add_volume(int index_elem, int index_material, double volume); + void add_volume_unsafe(int index_elem, int index_material, double volume); + + // Accessors + int32_t& materials(int i, int j) { return materials_[i * table_size_ + j]; } + const int32_t& materials(int i, int j) const + { + return materials_[i * table_size_ + j]; + } + + double& volumes(int i, int j) { return volumes_[i * table_size_ + j]; } + const double& volumes(int i, int j) const + { + return volumes_[i * table_size_ + j]; + } + + bool table_full() const { return table_full_; } + +private: + int32_t* materials_; //!< material index (bins, table_size) + double* volumes_; //!< volume in [cm^3] (bins, table_size) + int table_size_; //!< Size of hash table for each mesh element + bool table_full_ {false}; //!< Whether the hash table is full + + // Value used to indicate an empty slot in the hash table. We use -2 because + // the value -1 is used to indicate a void material. + static constexpr int EMPTY {-2}; +}; + +} // namespace detail + +//============================================================================== +//! Base mesh class +//============================================================================== + class Mesh { public: - // Types, aliases - struct MaterialVolume { - int32_t material; //!< material index - double volume; //!< volume in [cm^3] - }; - // Constructors and destructor Mesh() = default; Mesh(pugi::xml_node node); @@ -172,24 +228,17 @@ public: virtual std::string get_mesh_type() const = 0; - //! Determine volume of materials within a single mesh elemenet + //! Determine volume of materials within each mesh element // - //! \param[in] n_sample Number of samples within each element - //! \param[in] bin Index of mesh element - //! \param[out] Array of (material index, volume) for desired element - //! \param[inout] seed Pseudorandom number seed - //! \return Number of materials within element - int material_volumes( - int n_sample, int bin, span volumes, uint64_t* seed) const; - - //! Determine volume of materials within a single mesh elemenet - // - //! \param[in] n_sample Number of samples within each element - //! \param[in] bin Index of mesh element - //! \param[inout] seed Pseudorandom number seed - //! \return Vector of (material index, volume) for desired element - vector material_volumes( - int n_sample, int bin, uint64_t* seed) const; + //! \param[in] nx Number of samples in x direction + //! \param[in] ny Number of samples in y direction + //! \param[in] nz Number of samples in z direction + //! \param[in] max_materials Maximum number of materials in a single mesh + //! element + //! \param[inout] materials Array storing material indices + //! \param[inout] volumes Array storing volumes + void material_volumes(int nx, int ny, int nz, int max_materials, + int32_t* materials, double* volumes) const; //! Determine bounding box of mesh // diff --git a/openmc/lib/mesh.py b/openmc/lib/mesh.py index 16bec0198..3a720f98a 100644 --- a/openmc/lib/mesh.py +++ b/openmc/lib/mesh.py @@ -1,7 +1,7 @@ from collections.abc import Mapping, Sequence -from ctypes import (c_int, c_int32, c_char_p, c_double, POINTER, Structure, - create_string_buffer, c_uint64, c_size_t) -from random import getrandbits +from ctypes import (c_int, c_int32, c_char_p, c_double, POINTER, + create_string_buffer, c_size_t) +from math import sqrt import sys from weakref import WeakValueDictionary @@ -10,24 +10,20 @@ from numpy.ctypeslib import as_array from ..exceptions import AllocationError, InvalidIDError from . import _dll -from .core import _FortranObjectWithID +from .core import _FortranObjectWithID, quiet_dll from .error import _error_handler -from .material import Material from .plot import _Position from ..bounding_box import BoundingBox +from ..mesh import MeshMaterialVolumes __all__ = [ 'Mesh', 'RegularMesh', 'RectilinearMesh', 'CylindricalMesh', - 'SphericalMesh', 'UnstructuredMesh', 'meshes' + 'SphericalMesh', 'UnstructuredMesh', 'meshes', 'MeshMaterialVolumes' ] -class _MaterialVolume(Structure): - _fields_ = [ - ("material", c_int32), - ("volume", c_double) - ] - +arr_2d_int32 = np.ctypeslib.ndpointer(dtype=np.int32, ndim=2, flags='CONTIGUOUS') +arr_2d_double = np.ctypeslib.ndpointer(dtype=np.double, ndim=2, flags='CONTIGUOUS') # Mesh functions _dll.openmc_extend_meshes.argtypes = [c_int32, c_char_p, POINTER(c_int32), @@ -51,8 +47,7 @@ _dll.openmc_mesh_bounding_box.argtypes = [ _dll.openmc_mesh_bounding_box.restype = c_int _dll.openmc_mesh_bounding_box.errcheck = _error_handler _dll.openmc_mesh_material_volumes.argtypes = [ - c_int32, c_int, c_int, c_int, POINTER(_MaterialVolume), - POINTER(c_int), POINTER(c_uint64)] + c_int32, c_int, c_int, c_int, c_int, arr_2d_int32, arr_2d_double] _dll.openmc_mesh_material_volumes.restype = c_int _dll.openmc_mesh_material_volumes.errcheck = _error_handler _dll.openmc_mesh_get_plot_bins.argtypes = [ @@ -190,58 +185,81 @@ class Mesh(_FortranObjectWithID): def material_volumes( self, - n_samples: int = 10_000, - prn_seed: int | None = None - ) -> list[list[tuple[Material, float]]]: - """Determine volume of materials in each mesh element + n_samples: int | tuple[int, int, int] = 10_000, + max_materials: int = 4, + output: bool = True, + ) -> MeshMaterialVolumes: + """Determine volume of materials in each mesh element. + + This method works by raytracing repeatedly through the mesh to count the + estimated volume of each material in all mesh elements. Three sets of + rays are used: one set parallel to the x-axis, one parallel to the + y-axis, and one parallel to the z-axis. .. versionadded:: 0.15.0 + .. versionchanged:: 0.15.1 + Material volumes are now determined by raytracing rather than by + point sampling. + Parameters ---------- - n_samples : int - Number of samples in each mesh element - prn_seed : int - Pseudorandom number generator (PRNG) seed; if None, one will be - generated randomly. + n_samples : int or 3-tuple of int + Total number of rays to sample. The number of rays in each direction + is determined by the aspect ratio of the mesh bounding box. When + specified as a 3-tuple, it is interpreted as the number of rays in + the x, y, and z dimensions. + max_materials : int, optional + Estimated maximum number of materials in any given mesh element. + output : bool, optional + Whether or not to show output. Returns ------- - List of tuple of (material, volume) for each mesh element. Void volume - is represented by having a value of None in the first element of a - tuple. + MeshMaterialVolumes + Dictionary-like object that maps material IDs to an array of volumes + equal in size to the number of mesh elements. """ - if n_samples <= 0: - raise ValueError("Number of samples must be positive") - if prn_seed is None: - prn_seed = getrandbits(63) - prn_seed = c_uint64(prn_seed) + if isinstance(n_samples, int): + # Determine number of rays in each direction based on aspect ratios + # and using the relation (nx*ny + ny*nz + nx*nz) = n_samples + width_x, width_y, width_z = self.bounding_box.width + ax = width_x / width_z + ay = width_y / width_z + f = sqrt(n_samples/(ax*ay + ax + ay)) + nx = round(f * ax) + ny = round(f * ay) + nz = round(f) + else: + nx, ny, nz = n_samples - # Preallocate space for MaterialVolume results - size = 16 - result = (_MaterialVolume * size)() + # Value indicating an empty slot in the hash table (matches C++) + EMPTY_SLOT = -2 - hits = c_int() # Number of materials hit in a given element - volumes = [] - for i_element in range(self.n_elements): - while True: - try: + # Preallocate arrays for material indices and volumes + n = self.n_elements + slot_factor = 2 + table_size = slot_factor*max_materials + materials = np.full((n, table_size), EMPTY_SLOT, dtype=np.int32) + volumes = np.zeros((n, table_size), dtype=np.float64) + + # Run material volume calculation + while True: + try: + with quiet_dll(output): _dll.openmc_mesh_material_volumes( - self._index, n_samples, i_element, size, result, hits, prn_seed) - except AllocationError: - # Increase size of result array and try again - size *= 2 - result = (_MaterialVolume * size)() - else: - # If no error, break out of loop - break + self._index, nx, ny, nz, table_size, materials, volumes) + except AllocationError: + # Increase size of result array and try again + table_size *= 2 + materials = np.full((n, table_size), EMPTY_SLOT, dtype=np.int32) + volumes = np.zeros((n, table_size), dtype=np.float64) + else: + # If no error, break out of loop + break - volumes.append([ - (Material(index=r.material), r.volume) - for r in result[:hits.value] - ]) - return volumes + return MeshMaterialVolumes(materials, volumes) def get_plot_bins( self, @@ -306,7 +324,7 @@ class RegularMesh(Mesh): The lower-left corner of the structured mesh. If only two coordinate are given, it is assumed that the mesh is an x-y mesh. upper_right : numpy.ndarray - The upper-right corner of the structrued mesh. If only two coordinate + The upper-right corner of the structured mesh. If only two coordinate are given, it is assumed that the mesh is an x-y mesh. width : numpy.ndarray The width of mesh cells in each direction. @@ -395,7 +413,7 @@ class RectilinearMesh(Mesh): lower_left : numpy.ndarray The lower-left corner of the structured mesh. upper_right : numpy.ndarray - The upper-right corner of the structrued mesh. + The upper-right corner of the structured mesh. width : numpy.ndarray The width of mesh cells in each direction. n_elements : int @@ -500,7 +518,7 @@ class CylindricalMesh(Mesh): lower_left : numpy.ndarray The lower-left corner of the structured mesh. upper_right : numpy.ndarray - The upper-right corner of the structrued mesh. + The upper-right corner of the structured mesh. width : numpy.ndarray The width of mesh cells in each direction. n_elements : int @@ -605,7 +623,7 @@ class SphericalMesh(Mesh): lower_left : numpy.ndarray The lower-left corner of the structured mesh. upper_right : numpy.ndarray - The upper-right corner of the structrued mesh. + The upper-right corner of the structured mesh. width : numpy.ndarray The width of mesh cells in each direction. n_elements : int @@ -719,7 +737,6 @@ class _MeshMapping(Mapping): raise KeyError(str(e)) return _get_mesh(index.value) - def __iter__(self): for i in range(len(self)): yield _get_mesh(i).id diff --git a/openmc/mesh.py b/openmc/mesh.py index 4c594c9c6..08025f374 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -1,7 +1,7 @@ from __future__ import annotations import warnings from abc import ABC, abstractmethod -from collections.abc import Iterable, Sequence +from collections.abc import Iterable, Sequence, Mapping from functools import wraps from math import pi, sqrt, atan2 from numbers import Integral, Real @@ -21,6 +21,120 @@ from .surface import _BOUNDARY_TYPES from .utility_funcs import input_path +class MeshMaterialVolumes(Mapping): + """Results from a material volume in mesh calculation. + + This class provides multiple ways of accessing information about material + volumes in individual mesh elements. First, the class behaves like a + dictionary that maps material IDs to an array of volumes equal in size to + the number of mesh elements. Second, the class provides a :meth:`by_element` + method that gives all the material volumes for a specific mesh element. + + .. versionadded:: 0.15.1 + + Parameters + ---------- + materials : numpy.ndarray + Array of shape (elements, max_materials) storing material IDs + volumes : numpy.ndarray + Array of shape (elements, max_materials) storing material volumes + + See Also + -------- + openmc.MeshBase.material_volumes + + Examples + -------- + If you want to get the volume of a specific material in every mesh element, + index the object with the material ID: + + >>> volumes = mesh.material_volumes(...) + >>> volumes + {1: <32121 nonzero volumes> + 2: <338186 nonzero volumes> + 3: <49120 nonzero volumes>} + + If you want the volume of all materials in a specific mesh element, use the + :meth:`by_element` method: + + >>> volumes = mesh.material_volumes(...) + >>> volumes.by_element(42) + [(2, 31.87963824195591), (1, 6.129949130817542)] + + """ + def __init__(self, materials: np.ndarray, volumes: np.ndarray): + self._materials = materials + self._volumes = volumes + + @property + def num_elements(self) -> int: + return self._volumes.shape[0] + + def __iter__(self): + for mat in np.unique(self._materials): + if mat > 0: + yield mat + + def __len__(self) -> int: + return (np.unique(self._materials) > 0).sum() + + def __repr__(self) -> str: + ids, counts = np.unique(self._materials, return_counts=True) + return '{' + '\n '.join( + f'{id}: <{count} nonzero volumes>' for id, count in zip(ids, counts) if id > 0) + '}' + + def __getitem__(self, material_id: int) -> np.ndarray: + volumes = np.zeros(self.num_elements) + for i in range(self._volumes.shape[1]): + indices = (self._materials[:, i] == material_id) + volumes[indices] = self._volumes[indices, i] + return volumes + + def by_element(self, index_elem: int) -> list[tuple[int | None, float]]: + """Get a list of volumes for each material within a specific element. + + Parameters + ---------- + index_elem : int + Mesh element index + + Returns + ------- + list of tuple of (material ID, volume) + + """ + table_size = self._volumes.shape[1] + return [ + (m if m > -1 else None, self._volumes[index_elem, i]) + for i in range(table_size) + if (m := self._materials[index_elem, i]) != -2 + ] + + def save(self, filename: PathLike): + """Save material volumes to a .npz file. + + Parameters + ---------- + filename : path-like + Filename where data will be saved + """ + np.savez_compressed( + filename, materials=self._materials, volumes=self._volumes) + + @classmethod + def from_npz(cls, filename: PathLike) -> MeshMaterialVolumes: + """Generate material volumes from a .npz file + + Parameters + ---------- + filename : path-like + File where data will be read from + + """ + filedata = np.load(filename) + return cls(filedata['materials'], filedata['volumes']) + + class MeshBase(IDManagerMixin, ABC): """A mesh that partitions geometry for tallying purposes. @@ -170,8 +284,7 @@ class MeshBase(IDManagerMixin, ABC): def get_homogenized_materials( self, model: openmc.Model, - n_samples: int = 10_000, - prn_seed: int | None = None, + n_samples: int | tuple[int, int, int] = 10_000, include_void: bool = True, **kwargs ) -> list[openmc.Material]: @@ -184,15 +297,15 @@ class MeshBase(IDManagerMixin, ABC): model : openmc.Model Model containing materials to be homogenized and the associated geometry. - n_samples : int - Number of samples in each mesh element. - prn_seed : int, optional - Pseudorandom number generator (PRNG) seed; if None, one will be - generated randomly. + n_samples : int or 2-tuple of int + Total number of rays to sample. The number of rays in each direction + is determined by the aspect ratio of the mesh bounding box. When + specified as a 3-tuple, it is interpreted as the number of rays in + the x, y, and z dimensions. include_void : bool, optional Whether homogenization should include voids. **kwargs - Keyword-arguments passed to :func:`openmc.lib.init`. + Keyword-arguments passed to :meth:`MeshBase.material_volumes`. Returns ------- @@ -200,34 +313,8 @@ class MeshBase(IDManagerMixin, ABC): Homogenized material in each mesh element """ - import openmc.lib - - with change_directory(tmpdir=True): - # In order to get mesh into model, we temporarily replace the - # tallies with a single mesh tally using the current mesh - original_tallies = model.tallies - new_tally = openmc.Tally() - new_tally.filters = [openmc.MeshFilter(self)] - new_tally.scores = ['flux'] - model.tallies = [new_tally] - - # Export model to XML - model.export_to_model_xml() - - # Get material volume fractions - openmc.lib.init(**kwargs) - mesh = openmc.lib.tallies[new_tally.id].filters[0].mesh - mat_volume_by_element = [ - [ - (mat.id if mat is not None else None, volume) - for mat, volume in mat_volume_list - ] - for mat_volume_list in mesh.material_volumes(n_samples, prn_seed) - ] - openmc.lib.finalize() - - # Restore original tallies - model.tallies = original_tallies + vols = self.material_volumes(model, n_samples, **kwargs) + mat_volume_by_element = [vols.by_element(i) for i in range(vols.num_elements)] # Create homogenized material for each element materials = model.geometry.get_all_materials() @@ -274,6 +361,72 @@ class MeshBase(IDManagerMixin, ABC): return homogenized_materials + def material_volumes( + self, + model: openmc.Model, + n_samples: int | tuple[int, int, int] = 10_000, + max_materials: int = 4, + **kwargs + ) -> MeshMaterialVolumes: + """Determine volume of materials in each mesh element. + + This method works by raytracing repeatedly through the mesh to count the + estimated volume of each material in all mesh elements. Three sets of + rays are used: one set parallel to the x-axis, one parallel to the + y-axis, and one parallel to the z-axis. + + .. versionadded:: 0.15.1 + + Parameters + ---------- + model : openmc.Model + Model containing materials. + n_samples : int or 3-tuple of int + Total number of rays to sample. The number of rays in each direction + is determined by the aspect ratio of the mesh bounding box. When + specified as a 3-tuple, it is interpreted as the number of rays in + the x, y, and z dimensions. + max_materials : int, optional + Estimated maximum number of materials in any given mesh element. + **kwargs : dict + Keyword arguments passed to :func:`openmc.lib.init` + + Returns + ------- + Dictionary-like object that maps material IDs to an array of volumes + equal in size to the number of mesh elements. + + """ + import openmc.lib + + with change_directory(tmpdir=True): + # In order to get mesh into model, we temporarily replace the + # tallies with a single mesh tally using the current mesh + original_tallies = model.tallies + new_tally = openmc.Tally() + new_tally.filters = [openmc.MeshFilter(self)] + new_tally.scores = ['flux'] + model.tallies = [new_tally] + + # Export model to XML + model.export_to_model_xml() + + # Get material volume fractions + kwargs.setdefault('output', True) + if 'args' in kwargs: + kwargs['args'] = ['-c'] + kwargs['args'] + kwargs.setdefault('args', ['-c']) + openmc.lib.init(**kwargs) + mesh = openmc.lib.tallies[new_tally.id].filters[0].mesh + volumes = mesh.material_volumes( + n_samples, max_materials, output=kwargs['output']) + openmc.lib.finalize() + + # Restore original tallies + model.tallies = original_tallies + + return volumes + class StructuredMesh(MeshBase): """A base class for structured mesh functionality diff --git a/src/mesh.cpp b/src/mesh.cpp index 3e1f25c4b..a24b51a0b 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -6,10 +6,15 @@ #include // for size_t #include +#ifdef _MSC_VER +#include // for _InterlockedCompareExchange +#endif + #ifdef OPENMC_MPI #include "mpi.h" #endif +#include "xtensor/xadapt.hpp" #include "xtensor/xbuilder.hpp" #include "xtensor/xeval.hpp" #include "xtensor/xmath.hpp" @@ -29,13 +34,16 @@ #include "openmc/memory.h" #include "openmc/message_passing.h" #include "openmc/openmp_interface.h" +#include "openmc/output.h" #include "openmc/particle_data.h" #include "openmc/plot.h" #include "openmc/random_dist.h" #include "openmc/search.h" #include "openmc/settings.h" +#include "openmc/string_utils.h" #include "openmc/tallies/filter.h" #include "openmc/tallies/tally.h" +#include "openmc/timer.h" #include "openmc/volume_calc.h" #include "openmc/xml_interface.h" @@ -102,6 +110,113 @@ inline bool check_intersection_point(double x1, double x0, double y1, double y0, return false; } +//! Atomic compare-and-swap for signed 32-bit integer +// +//! \param[in,out] ptr Pointer to value to update +//! \param[in] expected Value to compare to +//! \param[in] desired If comparison is successful, value to update to +//! \return True if the comparison was successful and the value was updated +inline bool atomic_cas_int32(int32_t* ptr, int32_t& expected, int32_t desired) +{ +#if defined(__GNUC__) || defined(__clang__) + // For gcc/clang, use the __atomic_compare_exchange_n intrinsic + return __atomic_compare_exchange_n( + ptr, &expected, desired, false, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST); + +#elif defined(_MSC_VER) + // For MSVC, use the _InterlockedCompareExchange intrinsic + int32_t old_val = + _InterlockedCompareExchange(reinterpret_cast(ptr), + static_cast(desired), static_cast(expected)); + return (old_val == expected); + +#else +#error "No compare-and-swap implementation available for this compiler." +#endif +} + +namespace detail { + +//============================================================================== +// MaterialVolumes implementation +//============================================================================== + +void MaterialVolumes::add_volume( + int index_elem, int index_material, double volume) +{ + // This method handles adding elements to the materials hash table, + // implementing open addressing with linear probing. Consistency across + // multiple threads is handled by with an atomic compare-and-swap operation. + // Ideally, we would use #pragma omp atomic compare, but it was introduced in + // OpenMP 5.1 and is not widely supported yet. + + // Loop for linear probing + for (int attempt = 0; attempt < table_size_; ++attempt) { + // Determine slot to check + int slot = (index_material + attempt) % table_size_; + int32_t* slot_ptr = &this->materials(index_elem, slot); + + // Non-atomic read of current material + int32_t current_val = *slot_ptr; + + // Found the desired material; accumulate volume + if (current_val == index_material) { +#pragma omp atomic + this->volumes(index_elem, slot) += volume; + return; + } + + // Slot appears to be empty; attempt to claim + if (current_val == EMPTY) { + // Attempt compare-and-swap from EMPTY to index_material + int32_t expected_val = EMPTY; + bool claimed_slot = + atomic_cas_int32(slot_ptr, expected_val, index_material); + + // If we claimed the slot or another thread claimed it but the same + // material was inserted, proceed to accumulate + if (claimed_slot || (expected_val == index_material)) { +#pragma omp atomic + this->volumes(index_elem, slot) += volume; + return; + } + } + } + + // If table is full, set a flag that can be checked later + table_full_ = true; +} + +void MaterialVolumes::add_volume_unsafe( + int index_elem, int index_material, double volume) +{ + // Linear probe + for (int attempt = 0; attempt < table_size_; ++attempt) { + int slot = (index_material + attempt) % table_size_; + + // Read current material + int32_t current_val = this->materials(index_elem, slot); + + // Found the desired material; accumulate volume + if (current_val == index_material) { + this->volumes(index_elem, slot) += volume; + return; + } + + // Claim empty slot + if (current_val == EMPTY) { + this->materials(index_elem, slot) = index_material; + this->volumes(index_elem, slot) += volume; + return; + } + } + + // If table is full, set a flag that can be checked later + table_full_ = true; +} + +} // namespace detail + //============================================================================== // Mesh implementation //============================================================================== @@ -153,90 +268,235 @@ vector Mesh::volumes() const return volumes; } -int Mesh::material_volumes( - int n_sample, int bin, span result, uint64_t* seed) const +void Mesh::material_volumes(int nx, int ny, int nz, int table_size, + int32_t* materials, double* volumes) const { - vector materials; - vector hits; + if (mpi::master) { + header("MESH MATERIAL VOLUMES CALCULATION", 7); + } + write_message(7, "Number of rays (x) = {}", nx); + write_message(7, "Number of rays (y) = {}", ny); + write_message(7, "Number of rays (z) = {}", nz); + int64_t n_total = nx * ny + ny * nz + nx * nz; + write_message(7, "Total number of rays = {}", n_total); + write_message( + 7, "Maximum number of materials per mesh element = {}", table_size); + + Timer timer; + timer.start(); + + // Create object for keeping track of materials/volumes + detail::MaterialVolumes result(materials, volumes, table_size); + + // Determine bounding box + auto bbox = this->bounding_box(); + + std::array n_rays = {nx, ny, nz}; + + // Determine effective width of rays + Position width((bbox.xmax - bbox.xmin) / nx, (bbox.ymax - bbox.ymin) / ny, + (bbox.zmax - bbox.zmin) / nz); + + // Set flag for mesh being contained within model + bool out_of_model = false; #pragma omp parallel { - vector local_materials; - vector local_hits; - GeometryState geom; + // Preallocate vector for mesh indices and length fractions and particle + std::vector bins; + std::vector length_fractions; + Particle p; -#pragma omp for - for (int i = 0; i < n_sample; ++i) { - // Get seed for i-th sample - uint64_t seed_i = future_seed(3 * i, *seed); + SourceSite site; + site.E = 1.0; + site.particle = ParticleType::neutron; - // Sample position and set geometry state - geom.r() = this->sample_element(bin, &seed_i); - geom.u() = {1., 0., 0.}; - geom.n_coord() = 1; + for (int axis = 0; axis < 3; ++axis) { + // Set starting position and direction + site.r = {0.0, 0.0, 0.0}; + site.r[axis] = bbox.min()[axis]; + site.u = {0.0, 0.0, 0.0}; + site.u[axis] = 1.0; - // If this location is not in the geometry at all, move on to next block - if (!exhaustive_find_cell(geom)) - continue; + // Determine width of rays and number of rays in other directions + int ax1 = (axis + 1) % 3; + int ax2 = (axis + 2) % 3; + double min1 = bbox.min()[ax1]; + double min2 = bbox.min()[ax2]; + double d1 = width[ax1]; + double d2 = width[ax2]; + int n1 = n_rays[ax1]; + int n2 = n_rays[ax2]; - int i_material = geom.material(); + // Divide rays in first direction over MPI processes by computing starting + // and ending indices + int min_work = n1 / mpi::n_procs; + int remainder = n1 % mpi::n_procs; + int n1_local = (mpi::rank < remainder) ? min_work + 1 : min_work; + int i1_start = mpi::rank * min_work + std::min(mpi::rank, remainder); + int i1_end = i1_start + n1_local; - // Check if this material was previously hit and if so, increment count - auto it = - std::find(local_materials.begin(), local_materials.end(), i_material); - if (it == local_materials.end()) { - local_materials.push_back(i_material); - local_hits.push_back(1); - } else { - local_hits[it - local_materials.begin()]++; + // Loop over rays on face of bounding box +#pragma omp for collapse(2) + for (int i1 = i1_start; i1 < i1_end; ++i1) { + for (int i2 = 0; i2 < n2; ++i2) { + site.r[ax1] = min1 + (i1 + 0.5) * d1; + site.r[ax2] = min2 + (i2 + 0.5) * d2; + + p.from_source(&site); + + // Determine particle's location + if (!exhaustive_find_cell(p)) { + out_of_model = true; + continue; + } + + // Set birth cell attribute + if (p.cell_born() == C_NONE) + p.cell_born() = p.lowest_coord().cell; + + // Initialize last cells from current cell + for (int j = 0; j < p.n_coord(); ++j) { + p.cell_last(j) = p.coord(j).cell; + } + p.n_coord_last() = p.n_coord(); + + while (true) { + // Ray trace from r_start to r_end + Position r0 = p.r(); + double max_distance = bbox.max()[axis] - r0[axis]; + + // Find the distance to the nearest boundary + BoundaryInfo boundary = distance_to_boundary(p); + + // Advance particle forward + double distance = std::min(boundary.distance, max_distance); + p.move_distance(distance); + + // Determine what mesh elements were crossed by particle + bins.clear(); + length_fractions.clear(); + this->bins_crossed(r0, p.r(), p.u(), bins, length_fractions); + + // Add volumes to any mesh elements that were crossed + int i_material = p.material(); + if (i_material != C_NONE) { + i_material = model::materials[i_material]->id(); + } + for (int i_bin = 0; i_bin < bins.size(); i_bin++) { + int mesh_index = bins[i_bin]; + double length = distance * length_fractions[i_bin]; + + // Add volume to result + result.add_volume(mesh_index, i_material, length * d1 * d2); + } + + if (distance == max_distance) + break; + + // cross next geometric surface + for (int j = 0; j < p.n_coord(); ++j) { + p.cell_last(j) = p.coord(j).cell; + } + p.n_coord_last() = p.n_coord(); + + // Set surface that particle is on and adjust coordinate levels + p.surface() = boundary.surface; + p.n_coord() = boundary.coord_level; + + if (boundary.lattice_translation[0] != 0 || + boundary.lattice_translation[1] != 0 || + boundary.lattice_translation[2] != 0) { + // Particle crosses lattice boundary + cross_lattice(p, boundary); + } else { + // Particle crosses surface + const auto& surf {model::surfaces[p.surface_index()].get()}; + p.cross_surface(*surf); + } + } + } } - } // omp for - - // Reduce index/hits lists from each thread into a single copy - reduce_indices_hits(local_materials, local_hits, materials, hits); - } // omp parallel - - // Advance RNG seed - advance_prn_seed(3 * n_sample, seed); - - // Make sure span passed in is large enough - if (hits.size() > result.size()) { - return -1; - } - - // Convert hits to fractions - for (int i_mat = 0; i_mat < hits.size(); ++i_mat) { - double fraction = double(hits[i_mat]) / n_sample; - result[i_mat].material = materials[i_mat]; - result[i_mat].volume = fraction * this->volume(bin); - } - return hits.size(); -} - -vector Mesh::material_volumes( - int n_sample, int bin, uint64_t* seed) const -{ - // Create result vector with space for 8 pairs - vector result; - result.reserve(8); - - int size = -1; - while (true) { - // Get material volumes - size = this->material_volumes( - n_sample, bin, {result.data(), result.data() + result.capacity()}, seed); - - // If capacity was sufficient, resize the vector and return - if (size >= 0) { - result.resize(size); - break; } - - // Otherwise, increase capacity of the vector - result.reserve(2 * result.capacity()); } - return result; + // Check for errors + if (out_of_model) { + throw std::runtime_error("Mesh not fully contained in geometry."); + } else if (result.table_full()) { + throw std::runtime_error("Maximum number of materials for mesh material " + "volume calculation insufficient."); + } + + // Compute time for raytracing + double t_raytrace = timer.elapsed(); + +#ifdef OPENMC_MPI + // Combine results from multiple MPI processes + if (mpi::n_procs > 1) { + int total = this->n_bins() * table_size; + if (mpi::master) { + // Allocate temporary buffer for receiving data + std::vector mats(total); + std::vector vols(total); + + for (int i = 1; i < mpi::n_procs; ++i) { + // Receive material indices and volumes from process i + MPI_Recv( + mats.data(), total, MPI_INT, i, i, mpi::intracomm, MPI_STATUS_IGNORE); + MPI_Recv(vols.data(), total, MPI_DOUBLE, i, i, mpi::intracomm, + MPI_STATUS_IGNORE); + + // Combine with existing results; we can call thread unsafe version of + // add_volume because each thread is operating on a different element +#pragma omp for + for (int index_elem = 0; index_elem < n_bins(); ++index_elem) { + for (int k = 0; k < table_size; ++k) { + int index = index_elem * table_size + k; + result.add_volume_unsafe(index_elem, mats[index], vols[index]); + } + } + } + } else { + // Send material indices and volumes to process 0 + MPI_Send(materials, total, MPI_INT, 0, mpi::rank, mpi::intracomm); + MPI_Send(volumes, total, MPI_DOUBLE, 0, mpi::rank, mpi::intracomm); + } + } + + // Report time for MPI communication + double t_mpi = timer.elapsed() - t_raytrace; +#else + double t_mpi = 0.0; +#endif + + // Normalize based on known volumes of elements + for (int i = 0; i < this->n_bins(); ++i) { + // Estimated total volume in element i + double volume = 0.0; + for (int j = 0; j < table_size; ++j) { + volume += result.volumes(i, j); + } + // Renormalize volumes based on known volume of element i + double norm = this->volume(i) / volume; + for (int j = 0; j < table_size; ++j) { + result.volumes(i, j) *= norm; + } + } + + // Show elapsed time + timer.stop(); + double t_total = timer.elapsed(); + double t_normalize = t_total - t_raytrace - t_mpi; + if (mpi::master) { + header("Timing Statistics", 7); + show_time("Total time elapsed", t_total); + show_time("Ray tracing", t_raytrace, 1); + show_time("Ray tracing (per ray)", t_raytrace / n_total, 1); + show_time("MPI communication", t_mpi, 1); + show_time("Normalization", t_normalize, 1); + std::fflush(stdout); + } } void Mesh::to_hdf5(hid_t group) const @@ -616,8 +876,8 @@ xt::xtensor StructuredMesh::count_sites( } // raytrace through the mesh. The template class T will do the tallying. -// A modern optimizing compiler can recognize the noop method of T and eleminate -// that call entirely. +// A modern optimizing compiler can recognize the noop method of T and +// eliminate that call entirely. template void StructuredMesh::raytrace_mesh( Position r0, Position r1, const Direction& u, T tally) const @@ -685,7 +945,8 @@ void StructuredMesh::raytrace_mesh( if (traveled_distance >= total_distance) return; - // If we have not reached r1, we have hit a surface. Tally outward current + // If we have not reached r1, we have hit a surface. Tally outward + // current tally.surface(ijk, k, distances[k].max_surface, false); // Update cell and calculate distance to next surface in k-direction. @@ -697,15 +958,16 @@ void StructuredMesh::raytrace_mesh( // Check if we have left the interior of the mesh in_mesh = ((ijk[k] >= 1) && (ijk[k] <= shape_[k])); - // If we are still inside the mesh, tally inward current for the next cell + // If we are still inside the mesh, tally inward current for the next + // cell if (in_mesh) tally.surface(ijk, k, !distances[k].max_surface, true); } else { // not inside mesh - // For all directions outside the mesh, find the distance that we need to - // travel to reach the next surface. Use the largest distance, as only - // this will cross all outer surfaces. + // For all directions outside the mesh, find the distance that we need + // to travel to reach the next surface. Use the largest distance, as + // only this will cross all outer surfaces. int k_max {0}; for (int k = 0; k < n; ++k) { if ((ijk[k] < 1 || ijk[k] > shape_[k]) && @@ -719,7 +981,8 @@ void StructuredMesh::raytrace_mesh( if (traveled_distance >= total_distance) return; - // Calculate the new cell index and update all distances to next surfaces. + // Calculate the new cell index and update all distances to next + // surfaces. ijk = get_indices(r0 + (traveled_distance + TINY_BIT) * u, in_mesh); for (int k = 0; k < n; ++k) { distances[k] = @@ -1581,7 +1844,8 @@ double SphericalMesh::find_theta_crossing( const double b = r.dot(u) * cos_t_2 - r.z * u.z; const double c = r.dot(r) * cos_t_2 - r.z * r.z; - // if factor of s^2 is zero, direction of flight is parallel to theta surface + // if factor of s^2 is zero, direction of flight is parallel to theta + // surface if (std::abs(a) < FP_PRECISION) { // if b vanishes, direction of flight is within theta surface and crossing // is not possible @@ -1589,7 +1853,8 @@ double SphericalMesh::find_theta_crossing( return INFTY; const double s = -0.5 * c / b; - // Check if solution is in positive direction of flight and has correct sign + // Check if solution is in positive direction of flight and has correct + // sign if ((s > l) && (std::signbit(r.z + s * u.z) == sgn)) return s; @@ -1943,22 +2208,25 @@ extern "C" int openmc_mesh_bounding_box(int32_t index, double* ll, double* ur) return 0; } -extern "C" int openmc_mesh_material_volumes(int32_t index, int n_sample, - int bin, int result_size, void* result, int* hits, uint64_t* seed) +extern "C" int openmc_mesh_material_volumes(int32_t index, int nx, int ny, + int nz, int table_size, int32_t* materials, double* volumes) { - auto result_ = reinterpret_cast(result); - if (!result_) { - set_errmsg("Invalid result pointer passed to openmc_mesh_material_volumes"); - return OPENMC_E_INVALID_ARGUMENT; - } - if (int err = check_mesh(index)) return err; - int n = model::meshes[index]->material_volumes( - n_sample, bin, {result_, result_ + result_size}, seed); - *hits = n; - return (n == -1) ? OPENMC_E_ALLOCATE : 0; + try { + model::meshes[index]->material_volumes( + nx, ny, nz, table_size, materials, volumes); + } catch (const std::exception& e) { + set_errmsg(e.what()); + if (starts_with(e.what(), "Mesh")) { + return OPENMC_E_GEOMETRY; + } else { + return OPENMC_E_ALLOCATE; + } + } + + return 0; } extern "C" int openmc_mesh_get_plot_bins(int32_t index, Position origin, @@ -2394,7 +2662,8 @@ void MOABMesh::intersect_track(const moab::CartVect& start, moab::ErrorCode rval; vector tris; // get all intersections with triangles in the tet mesh - // (distances are relative to the start point, not the previous intersection) + // (distances are relative to the start point, not the previous + // intersection) rval = kdtree_->ray_intersect_triangles(kdtree_root_, FP_COINCIDENT, dir.array(), start.array(), tris, hits, 0, track_len); if (rval != moab::MB_SUCCESS) { @@ -2975,8 +3244,8 @@ void LibMesh::build_eqn_sys() void LibMesh::initialize() { if (!settings::libmesh_comm) { - fatal_error( - "Attempting to use an unstructured mesh without a libMesh communicator."); + fatal_error("Attempting to use an unstructured mesh without a libMesh " + "communicator."); } // assuming that unstructured meshes used in OpenMC are 3D @@ -3276,8 +3545,8 @@ void read_meshes(pugi::xml_node root) // Check to make sure multiple meshes in the same file don't share IDs int id = std::stoi(get_node_value(node, "id")); if (contains(mesh_ids, id)) { - fatal_error(fmt::format( - "Two or more meshes use the same unique ID '{}' in the same input file", + fatal_error(fmt::format("Two or more meshes use the same unique ID " + "'{}' in the same input file", id)); } mesh_ids.insert(id); diff --git a/src/particle.cpp b/src/particle.cpp index c63c84674..c51011d6b 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -293,7 +293,6 @@ void Particle::event_cross_surface() event() = TallyEvent::LATTICE; } else { // Particle crosses surface - // TODO: off-by-one const auto& surf {model::surfaces[surface_index()].get()}; // If BC, add particle to surface source before crossing surface if (surf->surf_source_ && surf->bc_) { diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index 64c16c238..8ab35335f 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -601,18 +601,18 @@ def test_regular_mesh(lib_init): mesh.set_parameters(lower_left=(-0.63, -0.63, -0.5), upper_right=(0.63, 0.63, 0.5)) vols = mesh.material_volumes() - assert len(vols) == 4 - for elem_vols in vols: + assert vols.num_elements == 4 + for i in range(vols.num_elements): + elem_vols = vols.by_element(i) assert sum(f[1] for f in elem_vols) == pytest.approx(1.26 * 1.26 / 4) - # If the mesh extends beyond the boundaries of the model, the volumes should - # still be reported correctly + # If the mesh extends beyond the boundaries of the model, we should get a + # GeometryError mesh.dimension = (1, 1, 1) mesh.set_parameters(lower_left=(-1.0, -1.0, -0.5), upper_right=(1.0, 1.0, 0.5)) - vols = mesh.material_volumes(100_000) - for elem_vols in vols: - assert sum(f[1] for f in elem_vols) == pytest.approx(1.26 * 1.26, 1e-2) + with pytest.raises(exc.GeometryError, match="not fully contained"): + vols = mesh.material_volumes() def test_regular_mesh_get_plot_bins(lib_init): @@ -683,11 +683,11 @@ def test_rectilinear_mesh(lib_init): mesh.set_grid([-w/2, -w/4, w/2], [-w/2, -w/4, w/2], [-0.5, 0.5]) vols = mesh.material_volumes() - assert len(vols) == 4 - assert sum(f[1] for f in vols[0]) == pytest.approx(w/4 * w/4) - assert sum(f[1] for f in vols[1]) == pytest.approx(w/4 * 3*w/4) - assert sum(f[1] for f in vols[2]) == pytest.approx(3*w/4 * w/4) - assert sum(f[1] for f in vols[3]) == pytest.approx(3*w/4 * 3*w/4) + assert vols.num_elements == 4 + assert sum(f[1] for f in vols.by_element(0)) == pytest.approx(w/4 * w/4) + assert sum(f[1] for f in vols.by_element(1)) == pytest.approx(w/4 * 3*w/4) + assert sum(f[1] for f in vols.by_element(2)) == pytest.approx(3*w/4 * w/4) + assert sum(f[1] for f in vols.by_element(3)) == pytest.approx(3*w/4 * 3*w/4) def test_cylindrical_mesh(lib_init): @@ -737,11 +737,11 @@ def test_cylindrical_mesh(lib_init): mesh.set_grid(r_grid, phi_grid, z_grid) vols = mesh.material_volumes() - assert len(vols) == 6 + assert vols.num_elements == 6 for i in range(0, 6, 2): - assert sum(f[1] for f in vols[i]) == pytest.approx(pi * 0.25**2 / 3) + assert sum(f[1] for f in vols.by_element(i)) == pytest.approx(pi * 0.25**2 / 3) for i in range(1, 6, 2): - assert sum(f[1] for f in vols[i]) == pytest.approx(pi * (0.5**2 - 0.25**2) / 3) + assert sum(f[1] for f in vols.by_element(i)) == pytest.approx(pi * (0.5**2 - 0.25**2) / 3) def test_spherical_mesh(lib_init): @@ -795,14 +795,14 @@ def test_spherical_mesh(lib_init): mesh.set_grid(r_grid, theta_grid, phi_grid) vols = mesh.material_volumes() - assert len(vols) == 12 + assert vols.num_elements == 12 d_theta = theta_grid[1] - theta_grid[0] d_phi = phi_grid[1] - phi_grid[0] for i in range(0, 12, 2): - assert sum(f[1] for f in vols[i]) == pytest.approx( + assert sum(f[1] for f in vols.by_element(i)) == pytest.approx( 0.25**3 / 3 * d_theta * d_phi * 2/pi) for i in range(1, 12, 2): - assert sum(f[1] for f in vols[i]) == pytest.approx( + assert sum(f[1] for f in vols.by_element(i)) == pytest.approx( (0.5**3 - 0.25**3) / 3 * d_theta * d_phi * 2/pi) diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index 3f307dda8..f1f5e9f67 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -1,4 +1,5 @@ from math import pi +from tempfile import TemporaryDirectory from pathlib import Path import numpy as np @@ -505,7 +506,7 @@ def test_mesh_get_homogenized_materials(): mesh.lower_left = (-1., -1., -1.) mesh.upper_right = (1., 1., 1.) mesh.dimension = (3, 1, 1) - m1, m2, m3 = mesh.get_homogenized_materials(model, n_samples=1_000_000) + m1, m2, m3 = mesh.get_homogenized_materials(model, n_samples=10_000) # Left mesh element should be only Fe56 assert m1.get_mass_density('Fe56') == pytest.approx(5.0) @@ -521,7 +522,7 @@ def test_mesh_get_homogenized_materials(): mesh_void.lower_left = (0.5, 0.5, -1.) mesh_void.upper_right = (1.5, 1.5, 1.) mesh_void.dimension = (1, 1, 1) - m4, = mesh_void.get_homogenized_materials(model, n_samples=1_000_000) + m4, = mesh_void.get_homogenized_materials(model, n_samples=(100, 100, 0)) # Mesh element that overlaps void should have half density assert m4.get_mass_density('H1') == pytest.approx(0.5, rel=1e-2) @@ -531,3 +532,86 @@ def test_mesh_get_homogenized_materials(): m5, = mesh_void.get_homogenized_materials( model, n_samples=1000, include_void=False) assert m5.get_mass_density('H1') == pytest.approx(1.0) + + +@pytest.fixture +def sphere_model(): + # Model with three materials separated by planes x=0 and z=0 + mats = [] + for i in range(3): + mat = openmc.Material() + mat.add_nuclide('H1', 1.0) + mat.set_density('g/cm3', float(i + 1)) + mats.append(mat) + + sph = openmc.Sphere(r=25.0, boundary_type='vacuum') + x0 = openmc.XPlane(0.0) + z0 = openmc.ZPlane(0.0) + cell1 = openmc.Cell(fill=mats[0], region=-sph & +x0 & +z0) + cell2 = openmc.Cell(fill=mats[1], region=-sph & -x0 & +z0) + cell3 = openmc.Cell(fill=mats[2], region=-sph & -z0) + model = openmc.Model() + model.geometry = openmc.Geometry([cell1, cell2, cell3]) + model.materials = openmc.Materials(mats) + return model + + +@pytest.mark.parametrize("n_rays", [1000, (10, 10, 0), (10, 0, 10), (0, 10, 10)]) +def test_material_volumes_regular_mesh(sphere_model, n_rays): + """Test the material_volumes method on a regular mesh""" + mesh = openmc.RegularMesh() + mesh.lower_left = (-1., -1., -1.) + mesh.upper_right = (1., 1., 1.) + mesh.dimension = (2, 2, 2) + volumes = mesh.material_volumes(sphere_model, n_rays) + mats = sphere_model.materials + np.testing.assert_almost_equal(volumes[mats[0].id], [0., 0., 0., 0., 0., 1., 0., 1.]) + np.testing.assert_almost_equal(volumes[mats[1].id], [0., 0., 0., 0., 1., 0., 1., 0.]) + np.testing.assert_almost_equal(volumes[mats[2].id], [1., 1., 1., 1., 0., 0., 0., 0.]) + assert volumes.by_element(4) == [(mats[1].id, 1.)] + assert volumes.by_element(0) == [(mats[2].id, 1.)] + + +def test_material_volumes_cylindrical_mesh(sphere_model): + """Test the material_volumes method on a cylindrical mesh""" + cyl_mesh = openmc.CylindricalMesh( + [0., 1.], [-1., 0., 1.,], [0.0, pi/4, 3*pi/4, 5*pi/4, 7*pi/4, 2*pi]) + volumes = cyl_mesh.material_volumes(sphere_model, (0, 100, 100)) + mats = sphere_model.materials + np.testing.assert_almost_equal(volumes[mats[0].id], [ + 0., 0., 0., 0., 0., + pi/8, pi/8, 0., pi/8, pi/8 + ]) + np.testing.assert_almost_equal(volumes[mats[1].id], [ + 0., 0., 0., 0., 0., + 0., pi/8, pi/4, pi/8, 0. + ]) + np.testing.assert_almost_equal(volumes[mats[2].id], [ + pi/8, pi/4, pi/4, pi/4, pi/8, + 0., 0., 0., 0., 0. + ]) + + +def test_mesh_material_volumes_serialize(): + materials = np.array([ + [1, -1, -2], + [-1, -2, -2], + [2, 1, -2], + [2, -2, -2] + ]) + volumes = np.array([ + [0.5, 0.5, 0.0], + [1.0, 0.0, 0.0], + [0.5, 0.5, 0.0], + [1.0, 0.0, 0.0] + ]) + volumes = openmc.MeshMaterialVolumes(materials, volumes) + with TemporaryDirectory() as tmpdir: + path = f'{tmpdir}/volumes.npz' + volumes.save(path) + new_volumes = openmc.MeshMaterialVolumes.from_npz(path) + + assert new_volumes.by_element(0) == [(1, 0.5), (None, 0.5)] + assert new_volumes.by_element(1) == [(None, 1.0)] + assert new_volumes.by_element(2) == [(2, 0.5), (1, 0.5)] + assert new_volumes.by_element(3) == [(2, 1.0)] From c26fde6665594620720c8d586698e18bbbb137ad Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Fri, 28 Feb 2025 15:00:40 +0100 Subject: [PATCH 281/671] Adding per kg as unit option on material functions (#3329) Co-authored-by: Jon Shimwell --- openmc/material.py | 28 +++++++++++++++++----------- tests/unit_tests/test_material.py | 5 +++++ 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/openmc/material.py b/openmc/material.py index 64458f571..41780ce36 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -300,7 +300,7 @@ class Material(IDManagerMixin): clip_tolerance : float Maximum fraction of :math:`\sum_i x_i p_i` for discrete distributions that will be discarded. - units : {'Bq', 'Bq/g', 'Bq/cm3'} + units : {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3'} Specifies the units on the integral of the distribution. volume : float, optional Volume of the material. If not passed, defaults to using the @@ -313,7 +313,7 @@ class Material(IDManagerMixin): is the total intensity of the photon source in the requested units. """ - cv.check_value('units', units, {'Bq', 'Bq/g', 'Bq/cm3'}) + cv.check_value('units', units, {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3'}) if units == 'Bq': multiplier = volume if volume is not None else self.volume if multiplier is None: @@ -322,6 +322,8 @@ class Material(IDManagerMixin): multiplier = 1 elif units == 'Bq/g': multiplier = 1.0 / self.get_mass_density() + elif units == 'Bq/kg': + multiplier = 1000.0 / self.get_mass_density() dists = [] probs = [] @@ -1132,16 +1134,16 @@ class Material(IDManagerMixin): def get_activity(self, units: str = 'Bq/cm3', by_nuclide: bool = False, volume: float | None = None) -> dict[str, float] | float: """Returns the activity of the material or for each nuclide in the - material in units of [Bq], [Bq/g] or [Bq/cm3]. + material in units of [Bq], [Bq/g], [Bq/kg] or [Bq/cm3]. .. versionadded:: 0.13.1 Parameters ---------- - units : {'Bq', 'Bq/g', 'Bq/cm3'} + units : {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3'} Specifies the type of activity to return, options include total - activity [Bq], specific [Bq/g] or volumetric activity [Bq/cm3]. - Default is volumetric activity [Bq/cm3]. + activity [Bq], specific [Bq/g, Bq/kg] or volumetric activity + [Bq/cm3]. Default is volumetric activity [Bq/cm3]. by_nuclide : bool Specifies if the activity should be returned for the material as a whole or per nuclide. Default is False. @@ -1159,7 +1161,7 @@ class Material(IDManagerMixin): of the material is returned as a float. """ - cv.check_value('units', units, {'Bq', 'Bq/g', 'Bq/cm3'}) + cv.check_value('units', units, {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3'}) cv.check_type('by_nuclide', by_nuclide, bool) if units == 'Bq': @@ -1168,6 +1170,8 @@ class Material(IDManagerMixin): multiplier = 1 elif units == 'Bq/g': multiplier = 1.0 / self.get_mass_density() + elif units == 'Bq/kg': + multiplier = 1000.0 / self.get_mass_density() activity = {} for nuclide, atoms_per_bcm in self.get_nuclide_atom_densities().items(): @@ -1179,15 +1183,15 @@ class Material(IDManagerMixin): def get_decay_heat(self, units: str = 'W', by_nuclide: bool = False, volume: float | None = None) -> dict[str, float] | float: """Returns the decay heat of the material or for each nuclide in the - material in units of [W], [W/g] or [W/cm3]. + material in units of [W], [W/g], [W/kg] or [W/cm3]. .. versionadded:: 0.13.3 Parameters ---------- - units : {'W', 'W/g', 'W/cm3'} + units : {'W', 'W/g', 'W/kg', 'W/cm3'} Specifies the units of decay heat to return. Options include total - heat [W], specific [W/g] or volumetric heat [W/cm3]. + heat [W], specific [W/g, W/kg] or volumetric heat [W/cm3]. Default is total heat [W]. by_nuclide : bool Specifies if the decay heat should be returned for the material as a @@ -1206,7 +1210,7 @@ class Material(IDManagerMixin): of the material is returned as a float. """ - cv.check_value('units', units, {'W', 'W/g', 'W/cm3'}) + cv.check_value('units', units, {'W', 'W/g', 'W/kg', 'W/cm3'}) cv.check_type('by_nuclide', by_nuclide, bool) if units == 'W': @@ -1215,6 +1219,8 @@ class Material(IDManagerMixin): multiplier = 1 elif units == 'W/g': multiplier = 1.0 / self.get_mass_density() + elif units == 'W/kg': + multiplier = 1000.0 / self.get_mass_density() decayheat = {} for nuclide, atoms_per_bcm in self.get_nuclide_atom_densities().items(): diff --git a/tests/unit_tests/test_material.py b/tests/unit_tests/test_material.py index c6a07cff9..ec55a7756 100644 --- a/tests/unit_tests/test_material.py +++ b/tests/unit_tests/test_material.py @@ -577,6 +577,7 @@ def test_get_activity(): m4.add_nuclide("H3", 1) m4.set_density('g/cm3', 1.5) assert pytest.approx(m4.get_activity(units='Bq/g')) == 355978108155965.94 # [Bq/g] + assert pytest.approx(m4.get_activity(units='Bq/kg')) == 355978108155965940 # [Bq/kg] assert pytest.approx(m4.get_activity(units='Bq/g', by_nuclide=True)["H3"]) == 355978108155965.94 # [Bq/g] assert pytest.approx(m4.get_activity(units='Bq/cm3')) == 355978108155965.94*3/2 # [Bq/cc] assert pytest.approx(m4.get_activity(units='Bq/cm3', by_nuclide=True)["H3"]) == 355978108155965.94*3/2 # [Bq/cc] @@ -626,6 +627,7 @@ def test_get_decay_heat(): m4.add_nuclide("I135", 1) m4.set_density('g/cm3', 1.5) assert pytest.approx(m4.get_decay_heat(units='W/g')) == 40175.15720273193 # [W/g] + assert pytest.approx(m4.get_decay_heat(units='W/kg')) == 40175157.20273193 # [W/kg] assert pytest.approx(m4.get_decay_heat(units='W/g', by_nuclide=True)["I135"]) == 40175.15720273193 # [W/g] assert pytest.approx(m4.get_decay_heat(units='W/cm3')) == 40175.15720273193*3/2 # [W/cc] assert pytest.approx(m4.get_decay_heat(units='W/cm3', by_nuclide=True)["I135"]) == 40175.15720273193*3/2 #[W/cc] @@ -656,6 +658,9 @@ def test_decay_photon_energy(): assert src.p * 2.0 == pytest.approx(src_v2.p) src_per_cm3 = m.get_decay_photon_energy(units='Bq/cm3', volume=100.0) assert (src.p == src_per_cm3.p).all() + src_per_bqg = m.get_decay_photon_energy(units='Bq/g') + src_per_bqkg = m.get_decay_photon_energy(units='Bq/kg') + assert pytest.approx(src_per_bqg.integral()) == src_per_bqkg.integral() / 1000. # If we add Xe135 (which has a tabular distribution), the photon source # should be a mixture distribution From 8fb48f125fd252a3d7d50c1cee500460bd9c3959 Mon Sep 17 00:00:00 2001 From: rzehumat <49885819+rzehumat@users.noreply.github.com> Date: Fri, 28 Feb 2025 17:38:31 +0100 Subject: [PATCH 282/671] Manually fix broken links (#3331) --- docs/source/devguide/styleguide.rst | 2 +- docs/source/devguide/tests.rst | 6 +++--- docs/source/devguide/workflow.rst | 2 +- docs/source/index.rst | 2 +- docs/source/io_formats/settings.rst | 2 +- docs/source/methods/cross_sections.rst | 4 ++-- docs/source/methods/geometry.rst | 2 +- docs/source/methods/photon_physics.rst | 8 ++++---- docs/source/usersguide/data.rst | 6 +++--- docs/source/usersguide/parallel.rst | 2 +- openmc/examples.py | 4 ++-- openmc/lib/weight_windows.py | 2 +- openmc/mgxs/__init__.py | 2 +- 13 files changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/source/devguide/styleguide.rst b/docs/source/devguide/styleguide.rst index b266f5f02..b8ec2d40f 100644 --- a/docs/source/devguide/styleguide.rst +++ b/docs/source/devguide/styleguide.rst @@ -55,7 +55,7 @@ Source Files Use a ``.cpp`` suffix for code files and ``.h`` for header files. Header files should always use include guards with the following style (See -`SF.8 `_): +`SF.8 `_): .. code-block:: C++ diff --git a/docs/source/devguide/tests.rst b/docs/source/devguide/tests.rst index 3665ad9d4..f2e39441a 100644 --- a/docs/source/devguide/tests.rst +++ b/docs/source/devguide/tests.rst @@ -87,9 +87,9 @@ that, consider the following: Debugging Tests in CI --------------------- -.. _tmate: `_ - -Tests can be debugged in CI using a feature called `tmate`_. CI debugging can be +Tests can be debugged in CI using a feature called +`tmate `_. +CI debugging can be enabled by including "[gha-debug]" in the commit message. When the test fails, a link similar to the one shown below will be provided in the GitHub Actions output after failure occurs. Logging into the provided link will allow you to diff --git a/docs/source/devguide/workflow.rst b/docs/source/devguide/workflow.rst index d6a3c6ba4..d9d36c161 100644 --- a/docs/source/devguide/workflow.rst +++ b/docs/source/devguide/workflow.rst @@ -126,7 +126,7 @@ reinstalling it). While the same effect can be achieved using the :envvar:`PYTHONPATH` environment variable, this is generally discouraged as it can interfere with virtual environments. -.. _git: http://git-scm.com/ +.. _git: https://git-scm.com/ .. _GitHub: https://github.com/ .. _git flow: https://nvie.com/git-model .. _valgrind: https://www.valgrind.org/ diff --git a/docs/source/index.rst b/docs/source/index.rst index a01055374..97666e7c3 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -12,7 +12,7 @@ files produced by NJOY. Parallelism is enabled via a hybrid MPI and OpenMP programming model. OpenMC was originally developed by members of the `Computational Reactor Physics -Group `_ at the `Massachusetts Institute of Technology +Group `_ at the `Massachusetts Institute of Technology `_ starting in 2011. Various universities, laboratories, and other organizations now contribute to the development of OpenMC. For more information on OpenMC, feel free to post a message on the `OpenMC Discourse diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 3ddba1d5e..0c225b1ed 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -238,7 +238,7 @@ based on the recommended value in LA-UR-14-24530_. .. note:: This element is not used in the multi-group :ref:`energy_mode`. -.. _LA-UR-14-24530: https://laws.lanl.gov/vhosts/mcnp.lanl.gov/pdf_files/la-ur-14-24530.pdf +.. _LA-UR-14-24530: https://mcnp.lanl.gov/pdf_files/TechReport_2014_LANL_LA-UR-14-24530_Brown.pdf --------------------------- ```` diff --git a/docs/source/methods/cross_sections.rst b/docs/source/methods/cross_sections.rst index 21a1ef8df..ad64b0e38 100644 --- a/docs/source/methods/cross_sections.rst +++ b/docs/source/methods/cross_sections.rst @@ -295,11 +295,11 @@ or even isotropic scattering. .. _Josey: https://doi.org/10.1016/j.jcp.2015.08.013 .. _WMP Library: https://github.com/mit-crpg/WMP_Library .. _MCNP: https://mcnp.lanl.gov -.. _Serpent: http://montecarlo.vtt.fi +.. _Serpent: https://serpent.vtt.fi/serpent/ .. _NJOY: https://www.njoy21.io/NJOY21/ .. _ENDF/B data: https://www.nndc.bnl.gov/endf-b8.0/ .. _Leppanen: https://doi.org/10.1016/j.anucene.2009.03.019 -.. _algorithms: http://ab-initio.mit.edu/wiki/index.php/Faddeeva_Package +.. _algorithms: http://ab-initio.mit.edu/faddeeva/ .. _NCrystal: https://github.com/mctools/ncrystal .. _NCrystal paper: https://doi.org/10.1016/j.cpc.2019.07.015 .. _using plugins: https://doi.org/10.1016/j.cpc.2021.108082 diff --git a/docs/source/methods/geometry.rst b/docs/source/methods/geometry.rst index b282ffdee..fa8bb3cf7 100644 --- a/docs/source/methods/geometry.rst +++ b/docs/source/methods/geometry.rst @@ -1066,5 +1066,5 @@ surface is known as in :ref:`reflection`. .. _constructive solid geometry: https://en.wikipedia.org/wiki/Constructive_solid_geometry .. _surfaces: https://en.wikipedia.org/wiki/Surface .. _MCNP: https://mcnp.lanl.gov -.. _Serpent: http://montecarlo.vtt.fi +.. _Serpent: https://serpent.vtt.fi/serpent/ .. _Monte Carlo Performance benchmark: https://github.com/mit-crpg/benchmarks/tree/master/mc-performance/openmc diff --git a/docs/source/methods/photon_physics.rst b/docs/source/methods/photon_physics.rst index bc912b943..42c68431a 100644 --- a/docs/source/methods/photon_physics.rst +++ b/docs/source/methods/photon_physics.rst @@ -1059,16 +1059,16 @@ emitted photon. .. _anomalous scattering: http://pd.chem.ucl.ac.uk/pdnn/diff1/anomscat.htm -.. _Kahn's rejection method: https://laws.lanl.gov/vhosts/mcnp.lanl.gov/pdf_files/aecu-3259_kahn.pdf +.. _Kahn's rejection method: https://mcnp.lanl.gov/pdf_files/TechReport_1956_RC_AECU-3259RM-1237-AEC_Kahn.pdf .. _Klein-Nishina: https://en.wikipedia.org/wiki/Klein%E2%80%93Nishina_formula -.. _LA-UR-04-0487: https://laws.lanl.gov/vhosts/mcnp.lanl.gov/pdf_files/la-ur-04-0487.pdf +.. _LA-UR-04-0487: https://mcnp.lanl.gov/pdf_files/TechReport_2004_LANL_LA-UR-04-0487_Sood.pdf -.. _LA-UR-04-0488: https://laws.lanl.gov/vhosts/mcnp.lanl.gov/pdf_files/la-ur-04-0488.pdf +.. _LA-UR-04-0488: https://mcnp.lanl.gov/pdf_files/TechReport_2004_LANL_LA-UR-04-0488_SoodWhite.pdf .. _Kaltiaisenaho: https://aaltodoc.aalto.fi/bitstream/handle/123456789/21004/master_Kaltiaisenaho_Toni_2016.pdf -.. _Salvat: https://www.oecd-nea.org/globalsearch/download.php?doc=77434 +.. _Salvat: https://doi.org/10.1787/32da5043-en .. _Sternheimer: https://doi.org/10.1103/PhysRevB.26.6067 diff --git a/docs/source/usersguide/data.rst b/docs/source/usersguide/data.rst index 6af297338..a1611de63 100644 --- a/docs/source/usersguide/data.rst +++ b/docs/source/usersguide/data.rst @@ -12,7 +12,7 @@ responsible for specifying one or more of the following: file (commonly named ``cross_sections.xml``) contains a listing of other data files, in particular neutron cross sections, photon cross sections, and windowed multipole data. Each of those files, in turn, uses a `HDF5 - `_ format (see :ref:`io_nuclear_data`). In + `_ format (see :ref:`io_nuclear_data`). In order to run transport simulations with continuous-energy cross sections, you need to specify this file. @@ -291,12 +291,12 @@ distributed with any pre-existing multigroup cross section libraries. However, if a multigroup library file is downloaded or generated, the path to the file needs to be specified as described in :ref:`usersguide_data_runtime`. For an example of how to create a multigroup library, see the `example notebook -<../examples/mg-mode-part-i.ipynb>`__. +`_. .. _NJOY: http://www.njoy21.io/ .. _NNDC: https://www.nndc.bnl.gov/endf .. _MCNP: https://mcnp.lanl.gov -.. _Serpent: http://montecarlo.vtt.fi +.. _Serpent: https://serpent.vtt.fi/serpent/ .. _ENDF/B: https://www.nndc.bnl.gov/endf-b7.1/acefiles.html .. _JEFF: https://www.oecd-nea.org/dbdata/jeff/jeff33/ .. _TENDL: https://tendl.web.psi.ch/tendl_2017/tendl2017.html diff --git a/docs/source/usersguide/parallel.rst b/docs/source/usersguide/parallel.rst index be4189604..e00ed5e97 100644 --- a/docs/source/usersguide/parallel.rst +++ b/docs/source/usersguide/parallel.rst @@ -102,4 +102,4 @@ performance on a machine when running in parallel: settings.output = {'tallies': False} .. _Haswell-EP: http://www.anandtech.com/show/8423/intel-xeon-e5-version-3-up-to-18-haswell-ep-cores-/4 -.. _bound: https://wiki.mpich.org/mpich/index.php/Using_the_Hydra_Process_Manager#Process-core_Binding +.. _bound: https://github.com/pmodels/mpich/blob/main/doc/wiki/how_to/Using_the_Hydra_Process_Manager.md#process-core-binding diff --git a/openmc/examples.py b/openmc/examples.py index 538b6ea4a..8d4bd1f04 100644 --- a/openmc/examples.py +++ b/openmc/examples.py @@ -11,7 +11,7 @@ def pwr_pin_cell() -> openmc.Model: This model is a single fuel pin with 2.4 w/o enriched UO2 corresponding to a beginning-of-cycle condition and borated water. The specifications are from - the `BEAVRS `_ benchmark. Note that the + the `BEAVRS `_ benchmark. Note that the number of particles/batches is initially set very low for testing purposes. Returns @@ -442,7 +442,7 @@ def pwr_assembly() -> openmc.Model: """Create a PWR assembly model. This model is a reflected 17x17 fuel assembly from the the `BEAVRS - `_ benchmark. The fuel is 2.4 w/o + `_ benchmark. The fuel is 2.4 w/o enriched UO2 corresponding to a beginning-of-cycle condition. Note that the number of particles/batches is initially set very low for testing purposes. diff --git a/openmc/lib/weight_windows.py b/openmc/lib/weight_windows.py index d92f01917..ed442d33f 100644 --- a/openmc/lib/weight_windows.py +++ b/openmc/lib/weight_windows.py @@ -276,7 +276,7 @@ class WeightWindows(_FortranObjectWithID): def update_magic(self, tally, value='mean', threshold=1.0, ratio=5.0): """Update weight window values using the MAGIC method - Reference: https://inis.iaea.org/search/48022314 + Reference: https://inis.iaea.org/records/231pm-zzy35 Parameters ---------- diff --git a/openmc/mgxs/__init__.py b/openmc/mgxs/__init__.py index 4f12a8fb4..6a29a8383 100644 --- a/openmc/mgxs/__init__.py +++ b/openmc/mgxs/__init__.py @@ -23,7 +23,7 @@ GROUP_STRUCTURES = {} - activation_ energy group structures "VITAMIN-J-42", "VITAMIN-J-175", "TRIPOLI-315", "CCFE-709_" and "UKAEA-1102_" -.. _CASMO: https://www.studsvik.com/SharepointFiles/CASMO-5%20Development%20and%20Applications.pdf +.. _CASMO: http://large.stanford.edu/courses/2013/ph241/dalvi1/docs/c5.physor2006.pdf .. _SCALE44: https://www-nds.iaea.org/publications/indc/indc-czr-0001.pdf .. _SCALE252: https://oecd-nea.org/science/wpncs/amct/workingarea/meeting2013/EGAMCT2013_08.pdf .. _MPACT: https://vera.ornl.gov/mpact/ From 39ad29d82e429b5e6f18cfb5f369bbfdddd93ed5 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 28 Feb 2025 13:50:35 -0600 Subject: [PATCH 283/671] Fix reading of horizontal field of view for ray-traced plots (#3330) Co-authored-by: Patrick Shriwise --- openmc/plots.py | 78 +++++++++++++++++++ src/plot.cpp | 12 +-- .../plot_projections/results_true.dat | 2 +- 3 files changed, 86 insertions(+), 6 deletions(-) diff --git a/openmc/plots.py b/openmc/plots.py index 9c1e31575..40fcf0c78 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -1312,6 +1312,50 @@ class WireframeRayTracePlot(RayTracePlot): Attributes ---------- + id : int + Unique identifier + name : str + Name of the plot + pixels : Iterable of int + Number of pixels to use in each direction + filename : str + Path to write the plot to + color_by : {'cell', 'material'} + Indicate whether the plot should be colored by cell or by material + background : Iterable of int or str + Color of the background + mask_components : Iterable of openmc.Cell or openmc.Material or int + The cells or materials (or corresponding IDs) to mask + mask_background : Iterable of int or str + Color to apply to all cells/materials listed in mask_components + show_overlaps : bool + Indicate whether or not overlapping regions are shown + overlap_color : Iterable of int or str + Color to apply to overlapping regions + colors : dict + Dictionary indicating that certain cells/materials should be + displayed with a particular color. The keys can be of type + :class:`~openmc.Cell`, :class:`~openmc.Material`, or int (ID for a + cell/material). + level : int + Universe depth to plot at + horizontal_field_of_view : float + Field of view horizontally, in units of degrees, defaults to 70. + camera_position : tuple or list of ndarray + Position of the camera in 3D space. Defaults to (1, 0, 0). + look_at : tuple or list of ndarray + The center of the camera's image points to this place in 3D space. + Set to (0, 0, 0) by default. + up : tuple or list of ndarray + Which way is up for the camera. Must not be parallel to the + line between look_at and camera_position. Set to (0, 0, 1) by default. + orthographic_width : float + If set to a nonzero value, an orthographic projection is used. + All rays traced from the orthographic pixel array travel in the + same direction. The width of the starting array must be specified, + unlike with the default perspective projection. The height of the + array is deduced from the ratio of pixel dimensions for the image. + Defaults to zero, i.e. using perspective projection. wireframe_thickness : int Line thickness employed for drawing wireframes around cells or material regions. Can be set to zero for no wireframes at all. Defaults to one @@ -1512,6 +1556,40 @@ class SolidRayTracePlot(RayTracePlot): Attributes ---------- + id : int + Unique identifier + name : str + Name of the plot + pixels : Iterable of int + Number of pixels to use in each direction + filename : str + Path to write the plot to + color_by : {'cell', 'material'} + Indicate whether the plot should be colored by cell or by material + overlap_color : Iterable of int or str + Color to apply to overlapping regions + colors : dict + Dictionary indicating that certain cells/materials should be + displayed with a particular color. The keys can be of type + :class:`~openmc.Cell`, :class:`~openmc.Material`, or int (ID for a + cell/material). + horizontal_field_of_view : float + Field of view horizontally, in units of degrees, defaults to 70. + camera_position : tuple or list of ndarray + Position of the camera in 3D space. Defaults to (1, 0, 0). + look_at : tuple or list of ndarray + The center of the camera's image points to this place in 3D space. + Set to (0, 0, 0) by default. + up : tuple or list of ndarray + Which way is up for the camera. Must not be parallel to the + line between look_at and camera_position. Set to (0, 0, 1) by default. + orthographic_width : float + If set to a nonzero value, an orthographic projection is used. + All rays traced from the orthographic pixel array travel in the + same direction. The width of the starting array must be specified, + unlike with the default perspective projection. The height of the + array is deduced from the ratio of pixel dimensions for the image. + Defaults to zero, i.e. using perspective projection. light_position : tuple or list of float Position of the light source in 3D space. Defaults to None, which places the light at the camera position. diff --git a/src/plot.cpp b/src/plot.cpp index 093f89f8c..dbc25b21e 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -1083,7 +1083,7 @@ WireframeRayTracePlot::WireframeRayTracePlot(pugi::xml_node node) void WireframeRayTracePlot::set_wireframe_color(pugi::xml_node plot_node) { - // Copy plot background color + // Copy plot wireframe color if (check_for_node(plot_node, "wireframe_color")) { vector w_rgb = get_node_array(plot_node, "wireframe_color"); if (w_rgb.size() == 3) { @@ -1503,13 +1503,15 @@ void RayTracePlot::set_look_at(pugi::xml_node node) void RayTracePlot::set_field_of_view(pugi::xml_node node) { // Defaults to 70 degree horizontal field of view (see .h file) - if (check_for_node(node, "field_of_view")) { - double fov = std::stod(get_node_value(node, "field_of_view", true)); + if (check_for_node(node, "horizontal_field_of_view")) { + double fov = + std::stod(get_node_value(node, "horizontal_field_of_view", true)); if (fov < 180.0 && fov > 0.0) { horizontal_field_of_view_ = fov; } else { - fatal_error(fmt::format( - "Field of view for plot {} out-of-range. Must be in (0, 180).", id())); + fatal_error(fmt::format("Horizontal field of view for plot {} " + "out-of-range. Must be in (0, 180) degrees.", + id())); } } } diff --git a/tests/regression_tests/plot_projections/results_true.dat b/tests/regression_tests/plot_projections/results_true.dat index 672280fdd..d5c6a7a58 100644 --- a/tests/regression_tests/plot_projections/results_true.dat +++ b/tests/regression_tests/plot_projections/results_true.dat @@ -1 +1 @@ -025804f1522eafd6e0e9566ce6b9b5603962f278de222c842fe3e06471290bb575676255bcd55e4f084bdcca4ee56d3c219827cb1ef2b5c3a90f7666986b55e9 \ No newline at end of file +6b90dfcf3059f86d623bb6496bb92d5b6ea2788b79639b61f865b31b503b84df9af64e59eacb04ccb02a225cfdb51bb7fa4b4f71e8e6ea20b2714266b34886ce \ No newline at end of file From e2557bbe22b8f7e6f8bd6253aaedd2c6f11c15cc Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 28 Feb 2025 20:04:27 -0600 Subject: [PATCH 284/671] Update pugixml to v1.15 (#3332) --- vendor/pugixml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/pugixml b/vendor/pugixml index 41b6ff21c..ee86beb30 160000 --- a/vendor/pugixml +++ b/vendor/pugixml @@ -1 +1 @@ -Subproject commit 41b6ff21c455865bb8ef67c5952b7f895b62bacc +Subproject commit ee86beb30e4973f5feffe3ce63bfa4fbadf72f38 From 239f7fed5e39eadfdad4f47c08f3f7b633862ab9 Mon Sep 17 00:00:00 2001 From: ahman24 <79746189+ahman24@users.noreply.github.com> Date: Wed, 5 Mar 2025 08:26:38 +0900 Subject: [PATCH 285/671] Implement user-configurable random number stride (#3067) Co-authored-by: Paul Romano --- docs/source/io_formats/settings.rst | 9 ++++++ docs/source/io_formats/statepoint.rst | 1 + include/openmc/capi.h | 2 ++ include/openmc/random_lcg.h | 14 +++++++++ openmc/lib/settings.py | 10 +++++++ openmc/settings.py | 25 ++++++++++++++++ openmc/statepoint.py | 6 ++++ src/finalize.cpp | 2 ++ src/initialize.cpp | 5 ++-- src/random_lcg.cpp | 12 +++++++- src/settings.cpp | 6 ++++ src/state_point.cpp | 8 +++++ tests/regression_tests/stride/__init__.py | 0 tests/regression_tests/stride/inputs_true.dat | 25 ++++++++++++++++ .../regression_tests/stride/results_true.dat | 2 ++ tests/regression_tests/stride/test.py | 29 +++++++++++++++++++ 16 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 tests/regression_tests/stride/__init__.py create mode 100644 tests/regression_tests/stride/inputs_true.dat create mode 100644 tests/regression_tests/stride/results_true.dat create mode 100644 tests/regression_tests/stride/test.py diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 0c225b1ed..5174cfd8b 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -538,6 +538,15 @@ pseudo-random number generator. *Default*: 1 +-------------------- +```` Element +-------------------- + +The ``stride`` element is used to specify how many random numbers are allocated +for each source particle history. + + *Default*: 152,917 + .. _source_element: -------------------- diff --git a/docs/source/io_formats/statepoint.rst b/docs/source/io_formats/statepoint.rst index fd00a3e77..3b1031769 100644 --- a/docs/source/io_formats/statepoint.rst +++ b/docs/source/io_formats/statepoint.rst @@ -23,6 +23,7 @@ The current version of the statepoint file format is 18.1. bank is present (1) or not (0). :Datasets: - **seed** (*int8_t*) -- Pseudo-random number generator seed. + - **stride** (*uint64_t*) -- Pseudo-random number generator stride. - **energy_mode** (*char[]*) -- Energy mode of the run, either 'continuous-energy' or 'multi-group'. - **run_mode** (*char[]*) -- Run mode used, either 'eigenvalue' or diff --git a/include/openmc/capi.h b/include/openmc/capi.h index 3802692c3..54257d093 100644 --- a/include/openmc/capi.h +++ b/include/openmc/capi.h @@ -71,6 +71,7 @@ int openmc_get_nuclide_index(const char name[], int* index); int openmc_add_unstructured_mesh( const char filename[], const char library[], int* id); int64_t openmc_get_seed(); +uint64_t openmc_get_stride(); int openmc_get_tally_index(int32_t id, int32_t* index); void openmc_get_tally_next_id(int32_t* id); int openmc_global_tallies(double** ptr); @@ -137,6 +138,7 @@ int openmc_reset_timers(); int openmc_run(); int openmc_sample_external_source(size_t n, uint64_t* seed, void* sites); void openmc_set_seed(int64_t new_seed); +void openmc_set_stride(uint64_t new_stride); int openmc_set_n_batches( int32_t n_batches, bool set_max_batches, bool add_statepoint_batch); int openmc_simulation_finalize(); diff --git a/include/openmc/random_lcg.h b/include/openmc/random_lcg.h index 4157b7cfe..5aecafed3 100644 --- a/include/openmc/random_lcg.h +++ b/include/openmc/random_lcg.h @@ -15,6 +15,7 @@ constexpr int STREAM_SOURCE {1}; constexpr int STREAM_URR_PTABLE {2}; constexpr int STREAM_VOLUME {3}; constexpr int64_t DEFAULT_SEED {1}; +constexpr uint64_t DEFAULT_STRIDE {152917ULL}; //============================================================================== //! Generate a pseudo-random number using a linear congruential generator. @@ -98,5 +99,18 @@ extern "C" int64_t openmc_get_seed(); extern "C" void openmc_set_seed(int64_t new_seed); +//============================================================================== +//! Get OpenMC's stride. +//============================================================================== + +extern "C" uint64_t openmc_get_stride(); + +//============================================================================== +//! Set OpenMC's stride. +//! @param new_stride Stride. +//============================================================================== + +extern "C" void openmc_set_stride(uint64_t new_stride); + } // namespace openmc #endif // OPENMC_RANDOM_LCG_H diff --git a/openmc/lib/settings.py b/openmc/lib/settings.py index 062670ef8..4fba8d48b 100644 --- a/openmc/lib/settings.py +++ b/openmc/lib/settings.py @@ -12,6 +12,8 @@ _RUN_MODES = {1: 'fixed source', _dll.openmc_set_seed.argtypes = [c_int64] _dll.openmc_get_seed.restype = c_int64 +_dll.openmc_set_stride.argtypes = [c_int64] +_dll.openmc_get_stride.restype = c_int64 _dll.openmc_get_n_batches.argtypes = [POINTER(c_int), c_bool] _dll.openmc_get_n_batches.restype = c_int _dll.openmc_get_n_batches.errcheck = _error_handler @@ -68,6 +70,14 @@ class _Settings: def seed(self, seed): _dll.openmc_set_seed(seed) + @property + def stride(self): + return _dll.openmc_get_stride() + + @stride.setter + def stride(self, stride): + _dll.openmc_set_stride(stride) + def set_batches(self, n_batches, set_max_batches=True, add_sp_batch=True): """Set number of batches or maximum number of batches diff --git a/openmc/settings.py b/openmc/settings.py index 0e2a18399..882a17b68 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -193,6 +193,8 @@ class Settings: The type of calculation to perform (default is 'eigenvalue') seed : int Seed for the linear congruential pseudorandom number generator + stride : int + Number of random numbers allocated for each source particle history source : Iterable of openmc.SourceBase Distribution of source sites in space, angle, and energy sourcepoint : dict @@ -338,6 +340,7 @@ class Settings: self._ptables = None self._uniform_source_sampling = None self._seed = None + self._stride = None self._survival_biasing = None # Shannon entropy mesh @@ -614,6 +617,16 @@ class Settings: cv.check_greater_than('random number generator seed', seed, 0) self._seed = seed + @property + def stride(self) -> int: + return self._stride + + @stride.setter + def stride(self, stride: int): + cv.check_type('random number generator stride', stride, Integral) + cv.check_greater_than('random number generator stride', stride, 0) + self._stride = stride + @property def survival_biasing(self) -> bool: return self._survival_biasing @@ -1336,6 +1349,11 @@ class Settings: element = ET.SubElement(root, "seed") element.text = str(self._seed) + def _create_stride_subelement(self, root): + if self._stride is not None: + element = ET.SubElement(root, "stride") + element.text = str(self._stride) + def _create_survival_biasing_subelement(self, root): if self._survival_biasing is not None: element = ET.SubElement(root, "survival_biasing") @@ -1763,6 +1781,11 @@ class Settings: if text is not None: self.seed = int(text) + def _stride_from_xml_element(self, root): + text = get_text(root, 'stride') + if text is not None: + self.stride = int(text) + def _survival_biasing_from_xml_element(self, root): text = get_text(root, 'survival_biasing') if text is not None: @@ -2014,6 +2037,7 @@ class Settings: self._create_plot_seed_subelement(element) self._create_ptables_subelement(element) self._create_seed_subelement(element) + self._create_stride_subelement(element) self._create_survival_biasing_subelement(element) self._create_cutoff_subelement(element) self._create_entropy_mesh_subelement(element, mesh_memo) @@ -2122,6 +2146,7 @@ class Settings: settings._plot_seed_from_xml_element(elem) settings._ptables_from_xml_element(elem) settings._seed_from_xml_element(elem) + settings._stride_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/openmc/statepoint.py b/openmc/statepoint.py index f2fd48066..715becf48 100644 --- a/openmc/statepoint.py +++ b/openmc/statepoint.py @@ -99,6 +99,8 @@ class StatePoint: and whose values are time values in seconds. seed : int Pseudorandom number generator seed + stride : int + Number of random numbers allocated for each particle history source : numpy.ndarray of compound datatype Array of source sites. The compound datatype has fields 'r', 'u', 'E', 'wgt', 'delayed_group', 'surf_id', and 'particle', corresponding to @@ -356,6 +358,10 @@ class StatePoint: def seed(self): return self._f['seed'][()] + @property + def stride(self): + return self._f['stride'][()] + @property def source(self): return self._f['source_bank'][()] if self.source_present else None diff --git a/src/finalize.cpp b/src/finalize.cpp index 981ec5cba..ad6f0cf62 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -159,6 +159,7 @@ int openmc_finalize() model::root_universe = -1; model::plotter_seed = 1; openmc::openmc_set_seed(DEFAULT_SEED); + openmc::openmc_set_stride(DEFAULT_STRIDE); // Deallocate arrays free_memory(); @@ -221,5 +222,6 @@ int openmc_hard_reset() // Reset the random number generator state openmc::openmc_set_seed(DEFAULT_SEED); + openmc::openmc_set_stride(DEFAULT_STRIDE); return 0; } diff --git a/src/initialize.cpp b/src/initialize.cpp index 5f717b137..4b821bee1 100644 --- a/src/initialize.cpp +++ b/src/initialize.cpp @@ -99,9 +99,10 @@ int openmc_init(int argc, char* argv[], const void* intracomm) } #endif - // Initialize random number generator -- if the user specifies a seed, it - // will be re-initialized later + // Initialize random number generator -- if the user specifies a seed and/or + // stride, it will be re-initialized later openmc::openmc_set_seed(DEFAULT_SEED); + openmc::openmc_set_stride(DEFAULT_STRIDE); // Copy previous locale and set locale to C. This is a workaround for an issue // whereby when openmc_init is called from the plotter, the Qt application diff --git a/src/random_lcg.cpp b/src/random_lcg.cpp index ca4467719..29457569b 100644 --- a/src/random_lcg.cpp +++ b/src/random_lcg.cpp @@ -10,7 +10,7 @@ int64_t master_seed {1}; // LCG parameters constexpr uint64_t prn_mult {6364136223846793005ULL}; // multiplication constexpr uint64_t prn_add {1442695040888963407ULL}; // additive factor, c -constexpr uint64_t prn_stride {152917LL}; // stride between particles +uint64_t prn_stride {DEFAULT_STRIDE}; // stride between particles //============================================================================== // PRN @@ -133,4 +133,14 @@ extern "C" void openmc_set_seed(int64_t new_seed) master_seed = new_seed; } +extern "C" uint64_t openmc_get_stride() +{ + return prn_stride; +} + +extern "C" void openmc_set_stride(uint64_t new_stride) +{ + prn_stride = new_stride; +} + } // namespace openmc diff --git a/src/settings.cpp b/src/settings.cpp index e2f5a033b..cd925be70 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -514,6 +514,12 @@ void read_settings_xml(pugi::xml_node root) openmc_set_seed(seed); } + // Copy random number stride if specified + if (check_for_node(root, "stride")) { + auto stride = std::stoull(get_node_value(root, "stride")); + openmc_set_stride(stride); + } + // Check for electron treatment if (check_for_node(root, "electron_treatment")) { auto temp_str = get_node_value(root, "electron_treatment", true, true); diff --git a/src/state_point.cpp b/src/state_point.cpp index ed8c6ed41..3b822715c 100644 --- a/src/state_point.cpp +++ b/src/state_point.cpp @@ -89,6 +89,9 @@ extern "C" int openmc_statepoint_write(const char* filename, bool* write_source) // Write out random number seed write_dataset(file_id, "seed", openmc_get_seed()); + // Write out random number stride + write_dataset(file_id, "stride", openmc_get_stride()); + // Write run information write_dataset(file_id, "energy_mode", settings::run_CE ? "continuous-energy" : "multi-group"); @@ -399,6 +402,11 @@ extern "C" int openmc_statepoint_load(const char* filename) read_dataset(file_id, "seed", seed); openmc_set_seed(seed); + // Read and overwrite random number stride + uint64_t stride; + read_dataset(file_id, "stride", stride); + openmc_set_stride(stride); + // It is not impossible for a state point to be generated from a CE run but // to be loaded in to an MG run (or vice versa), check to prevent that. read_dataset(file_id, "energy_mode", word); diff --git a/tests/regression_tests/stride/__init__.py b/tests/regression_tests/stride/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/stride/inputs_true.dat b/tests/regression_tests/stride/inputs_true.dat new file mode 100644 index 000000000..f93ec33d1 --- /dev/null +++ b/tests/regression_tests/stride/inputs_true.dat @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + eigenvalue + 1000 + 10 + 5 + + + -4 -4 -4 4 4 4 + + + 1529170 + + diff --git a/tests/regression_tests/stride/results_true.dat b/tests/regression_tests/stride/results_true.dat new file mode 100644 index 000000000..a65411150 --- /dev/null +++ b/tests/regression_tests/stride/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +2.978080E-01 6.106774E-03 diff --git a/tests/regression_tests/stride/test.py b/tests/regression_tests/stride/test.py new file mode 100644 index 000000000..f911af1f5 --- /dev/null +++ b/tests/regression_tests/stride/test.py @@ -0,0 +1,29 @@ +import pytest +import openmc + +from tests.testing_harness import PyAPITestHarness + + +@pytest.fixture +def model(): + u = openmc.Material() + u.add_nuclide('U235', 1.0) + u.set_density('g/cm3', 4.5) + sph = openmc.Sphere(r=10.0, boundary_type='vacuum') + cell = openmc.Cell(fill=u, region=-sph) + model = openmc.Model() + model.geometry = openmc.Geometry([cell]) + + model.settings.batches = 10 + model.settings.inactive = 5 + model.settings.particles = 1000 + model.settings.stride = 1_529_170 + model.settings.source = openmc.IndependentSource( + space=openmc.stats.Box([-4, -4, -4], [4, 4, 4]) + ) + return model + + +def test_seed(model): + harness = PyAPITestHarness('statepoint.10.h5', model) + harness.main() From ced8929128498c4837552ad95285956d517e12b5 Mon Sep 17 00:00:00 2001 From: Thomas Kittelmann Date: Wed, 5 Mar 2025 22:45:27 +0100 Subject: [PATCH 286/671] NCrystal becomes runtime rather than buildtime dependency (#3328) Co-authored-by: Paul Romano --- .github/workflows/ci.yml | 8 +- CMakeLists.txt | 25 +--- CODEOWNERS | 3 +- cmake/OpenMCConfig.cmake.in | 13 -- docs/source/usersguide/install.rst | 20 ++-- include/openmc/ncrystal_interface.h | 40 +++---- include/openmc/ncrystal_load.h | 127 ++++++++++++++++++++ openmc/lib/__init__.py | 3 - openmc/material.py | 6 +- src/material.cpp | 2 +- src/ncrystal_interface.cpp | 79 ++----------- src/ncrystal_load.cpp | 151 ++++++++++++++++++++++++ src/output.cpp | 5 - src/source.cpp | 2 +- tests/regression_tests/ncrystal/test.py | 5 +- tools/ci/gha-install.py | 8 +- tools/ci/gha-install.sh | 9 +- tools/ci/gha-script.sh | 5 - 18 files changed, 331 insertions(+), 180 deletions(-) create mode 100644 include/openmc/ncrystal_load.h create mode 100644 src/ncrystal_load.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7fe3e1557..877ec6833 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,6 @@ jobs: mpi: [n, y] omp: [n, y] dagmc: [n] - ncrystal: [n] libmesh: [n] event: [n] vectfit: [n] @@ -45,10 +44,6 @@ jobs: python-version: "3.11" mpi: y omp: y - - ncrystal: y - python-version: "3.11" - mpi: n - omp: n - libmesh: y python-version: "3.11" mpi: y @@ -66,7 +61,7 @@ jobs: omp: n mpi: y name: "Python ${{ matrix.python-version }} (omp=${{ matrix.omp }}, - mpi=${{ matrix.mpi }}, dagmc=${{ matrix.dagmc }}, ncrystal=${{ matrix.ncrystal }}, + mpi=${{ matrix.mpi }}, dagmc=${{ matrix.dagmc }}, libmesh=${{ matrix.libmesh }}, event=${{ matrix.event }} vectfit=${{ matrix.vectfit }})" @@ -75,7 +70,6 @@ jobs: PHDF5: ${{ matrix.mpi }} OMP: ${{ matrix.omp }} DAGMC: ${{ matrix.dagmc }} - NCRYSTAL: ${{ matrix.ncrystal }} EVENT: ${{ matrix.event }} VECTFIT: ${{ matrix.vectfit }} LIBMESH: ${{ matrix.libmesh }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 385ce85aa..461abe508 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,7 +37,6 @@ option(OPENMC_USE_DAGMC "Enable support for DAGMC (CAD) geometry" option(OPENMC_USE_LIBMESH "Enable support for libMesh unstructured mesh tallies" OFF) option(OPENMC_USE_MPI "Enable MPI" OFF) option(OPENMC_USE_MCPL "Enable MCPL" OFF) -option(OPENMC_USE_NCRYSTAL "Enable support for NCrystal scattering" OFF) option(OPENMC_USE_UWUW "Enable UWUW" OFF) message(STATUS "OPENMC_USE_OPENMP ${OPENMC_USE_OPENMP}") @@ -48,7 +47,6 @@ message(STATUS "OPENMC_USE_DAGMC ${OPENMC_USE_DAGMC}") message(STATUS "OPENMC_USE_LIBMESH ${OPENMC_USE_LIBMESH}") message(STATUS "OPENMC_USE_MPI ${OPENMC_USE_MPI}") message(STATUS "OPENMC_USE_MCPL ${OPENMC_USE_MCPL}") -message(STATUS "OPENMC_USE_NCRYSTAL ${OPENMC_USE_NCRYSTAL}") message(STATUS "OPENMC_USE_UWUW ${OPENMC_USE_UWUW}") # Warnings for deprecated options @@ -120,23 +118,6 @@ macro(find_package_write_status pkg) endif() endmacro() -#=============================================================================== -# NCrystal Scattering Support -#=============================================================================== - -if(OPENMC_USE_NCRYSTAL) - if(NOT DEFINED "NCrystal_DIR") - #Invocation of "ncrystal-config --show cmakedir" is needed to find NCrystal - #when it is installed from Python wheels: - execute_process( - COMMAND "ncrystal-config" "--show" "cmakedir" - OUTPUT_VARIABLE "NCrystal_DIR" OUTPUT_STRIP_TRAILING_WHITESPACE - ) - endif() - find_package(NCrystal 3.8.0 REQUIRED) - message(STATUS "Found NCrystal: ${NCrystal_DIR} (version ${NCrystal_VERSION})") -endif() - #=============================================================================== # DAGMC Geometry Support - need DAGMC/MOAB #=============================================================================== @@ -372,6 +353,7 @@ list(APPEND libopenmc_SOURCES src/mgxs.cpp src/mgxs_interface.cpp src/ncrystal_interface.cpp + src/ncrystal_load.cpp src/nuclide.cpp src/output.cpp src/particle.cpp @@ -548,11 +530,6 @@ if (OPENMC_USE_MCPL) target_link_libraries(libopenmc MCPL::mcpl) endif() -if(OPENMC_USE_NCRYSTAL) - target_compile_definitions(libopenmc PRIVATE NCRYSTAL) - target_link_libraries(libopenmc NCrystal::NCrystal) -endif() - #=============================================================================== # Log build info that this executable can report later #=============================================================================== diff --git a/CODEOWNERS b/CODEOWNERS index 6d452e366..c77366de7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -60,7 +60,8 @@ Dockerfile @shimwell src/random_ray/ @jtramm # NCrystal interface -src/ncrystal_interface.cpp @marquezj +src/ncrystal_interface.cpp @marquezj @tkittel +src/ncrystal_load.cpp @marquezj @tkittel # MCPL interface src/mcpl_interface.cpp @ebknudsen diff --git a/cmake/OpenMCConfig.cmake.in b/cmake/OpenMCConfig.cmake.in index 21c552303..5cab4790a 100644 --- a/cmake/OpenMCConfig.cmake.in +++ b/cmake/OpenMCConfig.cmake.in @@ -8,19 +8,6 @@ if(@OPENMC_USE_DAGMC@) find_package(DAGMC REQUIRED HINTS @DAGMC_DIR@) endif() -if(@OPENMC_USE_NCRYSTAL@) - if(NOT DEFINED "NCrystal_DIR") - #Invocation of "ncrystal-config --show cmakedir" is needed to find NCrystal - #when it is installed from Python wheels: - execute_process( - COMMAND "ncrystal-config" "--show" "cmakedir" - OUTPUT_VARIABLE "NCrystal_DIR" OUTPUT_STRIP_TRAILING_WHITESPACE - ) - endif() - find_package(NCrystal REQUIRED) - message(STATUS "Found NCrystal: ${NCrystal_DIR} (version ${NCrystal_VERSION})") -endif() - if(@OPENMC_USE_LIBMESH@) include(FindPkgConfig) list(APPEND CMAKE_PREFIX_PATH @LIBMESH_PREFIX@) diff --git a/docs/source/usersguide/install.rst b/docs/source/usersguide/install.rst index bb54594de..0aa561ee3 100644 --- a/docs/source/usersguide/install.rst +++ b/docs/source/usersguide/install.rst @@ -284,13 +284,13 @@ Prerequisites * NCrystal_ library for defining materials with enhanced thermal neutron transport - Adding this option allows the creation of materials from NCrystal, which - replaces the scattering kernel treatment of ACE files with a modular, - on-the-fly approach. To use it `install - `_ NCrystal and - turn on the option in the CMake configuration step:: - - cmake -DOPENMC_USE_NCRYSTAL=on .. + OpenMC supports the creation of materials from NCrystal, which replaces + the scattering kernel treatment of ACE files with a modular, on-the-fly + approach. OpenMC does not need any particular build option to use this, + but NCrystal must be installed on the system. Refer to `NCrystal + documentation + `_ for how this is + achieved. * libMesh_ mesh library framework for numerical simulations of partial differential equations @@ -393,12 +393,6 @@ OPENMC_USE_MCPL Turns on support for reading MCPL_ source files and writing MCPL source points and surface sources. (Default: off) -OPENMC_USE_NCRYSTAL - Turns on support for NCrystal materials. NCrystal must be `installed - `_ and `initialized - `_. - (Default: off) - OPENMC_USE_LIBMESH Enables the use of unstructured mesh tallies with libMesh_. (Default: off) diff --git a/include/openmc/ncrystal_interface.h b/include/openmc/ncrystal_interface.h index 22422e378..e9bd8ae79 100644 --- a/include/openmc/ncrystal_interface.h +++ b/include/openmc/ncrystal_interface.h @@ -1,10 +1,7 @@ #ifndef OPENMC_NCRYSTAL_INTERFACE_H #define OPENMC_NCRYSTAL_INTERFACE_H -#ifdef NCRYSTAL -#include "NCrystal/NCrystal.hh" -#endif - +#include "openmc/ncrystal_load.h" #include "openmc/particle.h" #include // for uint64_t @@ -17,28 +14,25 @@ namespace openmc { // Constants //============================================================================== -extern "C" const bool NCRYSTAL_ENABLED; - //! Energy in [eV] to switch between NCrystal and ENDF constexpr double NCRYSTAL_MAX_ENERGY {5.0}; //============================================================================== -// Wrapper class an NCrystal material +// Wrapper class for an NCrystal material //============================================================================== class NCrystalMat { public: //---------------------------------------------------------------------------- // Constructors - NCrystalMat() = default; + NCrystalMat() = default; // empty object explicit NCrystalMat(const std::string& cfg); //---------------------------------------------------------------------------- // Methods -#ifdef NCRYSTAL - //! Return configuration string - std::string cfg() const; + //! Return configuration string: + const std::string& cfg() const { return cfg_; } //! Get cross section from NCrystal material // @@ -52,25 +46,21 @@ public: void scatter(Particle& p) const; //! Whether the object holds a valid NCrystal material - operator bool() const; -#else + operator bool() const { return !cfg_.empty(); } - //---------------------------------------------------------------------------- - // Trivial methods when compiling without NCRYSTAL - std::string cfg() const { return ""; } - double xs(const Particle& p) const { return -1.0; } - void scatter(Particle& p) const {} - operator bool() const { return false; } -#endif + NCrystalMat clone() const + { + NCrystalMat c; + c.cfg_ = cfg_; + c.proc_ = proc_.clone(); + return c; + } private: //---------------------------------------------------------------------------- // Data members (only present when compiling with NCrystal support) -#ifdef NCRYSTAL - std::string cfg_; //!< NCrystal configuration string - std::shared_ptr - ptr_; //!< Pointer to NCrystal material object -#endif + std::string cfg_; //!< NCrystal configuration string + NCrystalScatProc proc_; //!< NCrystal scatter process }; //============================================================================== diff --git a/include/openmc/ncrystal_load.h b/include/openmc/ncrystal_load.h new file mode 100644 index 000000000..d85f24090 --- /dev/null +++ b/include/openmc/ncrystal_load.h @@ -0,0 +1,127 @@ +//! \file ncrystal_load.h +//! \brief Helper class taking care of loading NCrystal at runtime. + +#ifndef OPENMC_NCRYSTAL_LOAD_H +#define OPENMC_NCRYSTAL_LOAD_H + +#include // for swap +#include // for function +#include // for shared_ptr +#include // for move + +namespace NCrystalVirtualAPI { + +// NOTICE: Do NOT make ANY changes in the NCrystalVirtualAPI::VirtAPI_Type1_v1 +// class, it is required to stay exactly constant over time and compatible with +// the same definition used to compile the NCrystal library! But changes to +// white space, comments, and formatting is of course allowed. This API was +// introduced in NCrystal 4.1.0. + +//! Abstract base class for NCrystal interface which must be declared exactly as +// it is in NCrystal itself. + +class VirtAPI_Type1_v1 { +public: + // Note: neutron must be an array of length 4 with values {ekin,ux,uy,uz} + class ScatterProcess; + virtual const ScatterProcess* createScatter(const char* cfgstr) const = 0; + virtual const ScatterProcess* cloneScatter(const ScatterProcess*) const = 0; + virtual void deallocateScatter(const ScatterProcess*) const = 0; + virtual double crossSectionUncached( + const ScatterProcess&, const double* neutron) const = 0; + virtual void sampleScatterUncached(const ScatterProcess&, + std::function& rng, double* neutron) const = 0; + // Plumbing: + static constexpr unsigned interface_id = 1001; + virtual ~VirtAPI_Type1_v1() = default; + VirtAPI_Type1_v1() = default; + VirtAPI_Type1_v1(const VirtAPI_Type1_v1&) = delete; + VirtAPI_Type1_v1& operator=(const VirtAPI_Type1_v1&) = delete; + VirtAPI_Type1_v1(VirtAPI_Type1_v1&&) = delete; + VirtAPI_Type1_v1& operator=(VirtAPI_Type1_v1&&) = delete; +}; + +} // namespace NCrystalVirtualAPI + +namespace openmc { + +using NCrystalAPI = NCrystalVirtualAPI::VirtAPI_Type1_v1; + +//! Function which locates and loads NCrystal at runtime using the virtual API +std::shared_ptr load_ncrystal_api(); + +//! Class encapsulating exactly the parts of NCrystal needed by OpenMC + +class NCrystalScatProc final { +public: + //! Empty constructor which does not load NCrystal + NCrystalScatProc() {} + + //! Load NCrystal and instantiate a scattering process + //! \param cfgstr NCrystal cfg-string defining the material. + NCrystalScatProc(const char* cfgstr) + : api_(load_ncrystal_api()), p_(api_->createScatter(cfgstr)) + {} + + // Note: Neutron state array is {ekin,ux,uy,uz} + + //! Returns total scattering cross section in units of barns per atom. + //! \param neutron_state array {ekin,ux,uy,uz} with ekin (eV) and direction. + double cross_section(const double* neutron_state) const + { + return api_->crossSectionUncached(*p_, neutron_state); + } + + //! Returns total scattering cross section in units of barns per atom. + //! \param rng function returning random numbers in the unit interval + //! \param neutron_state array {ekin,ux,uy,uz} with ekin (eV) and direction. + void scatter(std::function& rng, double* neutron_state) const + { + api_->sampleScatterUncached(*p_, rng, neutron_state); + } + + //! Clones the object which is otherwise move-only + NCrystalScatProc clone() const + { + NCrystalScatProc c; + if (p_) { + c.api_ = api_; + c.p_ = api_->cloneScatter(p_); + } + return c; + } + + // Plumbing (move-only semantics, but supports explicit clone): + NCrystalScatProc(const NCrystalScatProc&) = delete; + NCrystalScatProc& operator=(const NCrystalScatProc&) = delete; + + NCrystalScatProc(NCrystalScatProc&& o) : api_(std::move(o.api_)), p_(nullptr) + { + std::swap(p_, o.p_); + } + + NCrystalScatProc& operator=(NCrystalScatProc&& o) + { + if (p_) { + api_->deallocateScatter(p_); + p_ = nullptr; + } + std::swap(api_, o.api_); + std::swap(p_, o.p_); + return *this; + } + + ~NCrystalScatProc() + { + if (p_) + api_->deallocateScatter(p_); + } + +private: + std::shared_ptr api_; + const NCrystalAPI::ScatterProcess* p_ = nullptr; +}; + +} // namespace openmc + +#endif diff --git a/openmc/lib/__init__.py b/openmc/lib/__init__.py index 5fe35b974..15642b42b 100644 --- a/openmc/lib/__init__.py +++ b/openmc/lib/__init__.py @@ -40,9 +40,6 @@ else: def _dagmc_enabled(): return c_bool.in_dll(_dll, "DAGMC_ENABLED").value -def _ncrystal_enabled(): - return c_bool.in_dll(_dll, "NCRYSTAL_ENABLED").value - def _coord_levels(): return c_int.in_dll(_dll, "n_coord_levels").value diff --git a/openmc/material.py b/openmc/material.py index 41780ce36..b104b374e 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -429,7 +429,11 @@ class Material(IDManagerMixin): """ - import NCrystal + try: + import NCrystal + except ModuleNotFoundError as e: + raise RuntimeError('The .from_ncrystal method requires' + ' NCrystal to be installed.') from e nc_mat = NCrystal.createInfo(cfg) def openmc_natabund(Z): diff --git a/src/material.cpp b/src/material.cpp index 3ba9ab96c..a1937aca4 100644 --- a/src/material.cpp +++ b/src/material.cpp @@ -368,7 +368,7 @@ Material& Material::clone() mat->name_ = name_; mat->nuclide_ = nuclide_; mat->element_ = element_; - mat->ncrystal_mat_ = ncrystal_mat_; + mat->ncrystal_mat_ = ncrystal_mat_.clone(); mat->atom_density_ = atom_density_; mat->density_ = density_; mat->density_gpcc_ = density_gpcc_; diff --git a/src/ncrystal_interface.cpp b/src/ncrystal_interface.cpp index b39f62d90..935d2b885 100644 --- a/src/ncrystal_interface.cpp +++ b/src/ncrystal_interface.cpp @@ -7,89 +7,34 @@ namespace openmc { //============================================================================== -// Constants +// NCrystalMat implementation //============================================================================== -#ifdef NCRYSTAL -const bool NCRYSTAL_ENABLED = true; -#else -const bool NCRYSTAL_ENABLED = false; -#endif - -//============================================================================== -// NCrystal wrapper class for the OpenMC random number generator -//============================================================================== - -#ifdef NCRYSTAL -class NCrystalRNGWrapper : public NCrystal::RNGStream { -public: - constexpr NCrystalRNGWrapper(uint64_t* seed) noexcept : openmc_seed_(seed) {} - -protected: - double actualGenerate() override - { - return std::max( - std::numeric_limits::min(), prn(openmc_seed_)); - } - -private: - uint64_t* openmc_seed_; -}; -#endif - -//============================================================================== -// NCrystal implementation -//============================================================================== - -NCrystalMat::NCrystalMat(const std::string& cfg) -{ -#ifdef NCRYSTAL - cfg_ = cfg; - ptr_ = NCrystal::FactImpl::createScatter(cfg); -#else - fatal_error("Your build of OpenMC does not support NCrystal materials."); -#endif -} - -#ifdef NCRYSTAL -std::string NCrystalMat::cfg() const -{ - return cfg_; -} +NCrystalMat::NCrystalMat(const std::string& cfg) : cfg_(cfg), proc_(cfg.c_str()) +{} double NCrystalMat::xs(const Particle& p) const { // Calculate scattering XS per atom with NCrystal, only once per material - NCrystal::CachePtr dummy_cache; - auto nc_energy = NCrystal::NeutronEnergy {p.E()}; - return ptr_->crossSection(dummy_cache, nc_energy, {p.u().x, p.u().y, p.u().z}) - .get(); + double neutron_state[4] = {p.E(), p.u().x, p.u().y, p.u().z}; + return proc_.cross_section(neutron_state); } void NCrystalMat::scatter(Particle& p) const { - NCrystalRNGWrapper rng(p.current_seed()); // Initialize RNG - // create a cache pointer for multi thread physics - NCrystal::CachePtr dummy_cache; - auto nc_energy = NCrystal::NeutronEnergy {p.E()}; - auto outcome = ptr_->sampleScatter( - dummy_cache, rng, nc_energy, {p.u().x, p.u().y, p.u().z}); - + // Scatter with NCrystal, using the OpenMC RNG stream: + uint64_t* seed = p.current_seed(); + std::function rng = [&seed]() { return prn(seed); }; + double neutron_state[4] = {p.E(), p.u().x, p.u().y, p.u().z}; + proc_.scatter(rng, neutron_state); // Modify attributes of particle - p.E() = outcome.ekin.get(); + p.E() = neutron_state[0]; Direction u_old {p.u()}; - p.u() = - Direction(outcome.direction[0], outcome.direction[1], outcome.direction[2]); + p.u() = Direction(neutron_state[1], neutron_state[2], neutron_state[3]); p.mu() = u_old.dot(p.u()); p.event_mt() = ELASTIC; } -NCrystalMat::operator bool() const -{ - return ptr_.get(); -} -#endif - //============================================================================== // Functions //============================================================================== diff --git a/src/ncrystal_load.cpp b/src/ncrystal_load.cpp new file mode 100644 index 000000000..b69f3a27f --- /dev/null +++ b/src/ncrystal_load.cpp @@ -0,0 +1,151 @@ +#include "openmc/ncrystal_load.h" + +#include // for isspace +#include // for strtoul +#include // for shared_ptr +#include // for mutex, lock_guard +#include + +#include +#include // for popen, pclose + +#include "openmc/error.h" + +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include // for LoadLibrary, GetProcAddress +#else +#include // for dlopen, dlsym, dlerror +#endif + +namespace openmc { +namespace { + +struct NCrystalConfig { + std::string shlibpath; + unsigned long intversion = 0; + std::string symbol_namespace; +}; + +NCrystalConfig query_ncrystal_config() +{ +#ifdef _WIN32 + FILE* pipe = _popen("ncrystal-config --show " + "intversion shlibpath namespace", + "r"); +#else + FILE* pipe = popen("ncrystal-config --show " + "intversion shlibpath namespace 2>/dev/null", + "r"); +#endif + if (!pipe) + return {}; // failure + auto readLine = [pipe](std::string& tgt) -> bool { + // Read line and discard trailing whitespace (including newline chars). + char buffer[4096]; + if (fgets(buffer, sizeof(buffer), pipe) == NULL) + return false; + tgt = buffer; + while (!tgt.empty() && std::isspace(tgt.back())) + tgt.pop_back(); + return true; + }; + auto parseIntVersion = [](const std::string& s) { + char* str_end = nullptr; + unsigned long v = std::strtoul(s.c_str(), &str_end, 10); + return (v >= 2002000 && v < 999999999 && str_end == s.c_str() + s.size()) + ? v + : 0; + }; + + NCrystalConfig res; + if (!readLine(res.shlibpath) || + !(res.intversion = parseIntVersion(res.shlibpath)) || + !readLine(res.shlibpath) || res.shlibpath.empty() || + !readLine(res.symbol_namespace)) { + res.intversion = 0; // failure + } + +#ifdef _WIN32 + auto returnCode = _pclose(pipe); +#else + auto returnCode = pclose(pipe); +#endif + if (returnCode == 0 && res.intversion >= 2002000) + return res; + return {}; // failure +} + +struct NCrystalAPIDB { + std::mutex mtx; + std::shared_ptr api; + using FctSignature = void* (*)(int); + FctSignature ncrystal_access_virtapi_fct = nullptr; +}; + +void* load_virtapi_raw(unsigned interface_id, NCrystalAPIDB& db) +{ + if (!db.ncrystal_access_virtapi_fct) { + auto cfg = query_ncrystal_config(); + if (!(cfg.intversion >= 4001000)) { + // This is the most likely error message people will see: + fatal_error("Could not locate a functioning and recent enough" + " NCrystal installation (required since geometry" + " contains NCrystal materials)."); + } +#ifdef _WIN32 + auto handle = LoadLibrary(cfg.shlibpath.c_str()); +#else + dlerror(); // clear previous errors + void* handle = dlopen(cfg.shlibpath.c_str(), RTLD_LOCAL | RTLD_LAZY); +#endif + if (!handle) + fatal_error("Loading of the NCrystal library failed"); + + std::string symbol = + fmt::format("ncrystal{}_access_virtual_api", cfg.symbol_namespace); + +#ifdef _WIN32 + void* addr = (void*)(intptr_t)GetProcAddress(handle, symbol.c_str()); + if (!addr) + fatal_error("GetProcAddress(" + "ncrystal_access_virtual_api) failed"); +#else + dlerror(); // clear previous errors + void* addr = dlsym(handle, symbol.c_str()); + if (!addr) + fatal_error("dlsym(ncrystal_access_virtual_api) failed"); +#endif + db.ncrystal_access_virtapi_fct = + reinterpret_cast(addr); + } + + void* result = (*db.ncrystal_access_virtapi_fct)(interface_id); + if (!result) + fatal_error("NCrystal installation does not support required interface."); + + return result; +} + +NCrystalAPIDB& get_ncrystal_api_db() +{ + static NCrystalAPIDB db; + return db; +} +} // namespace + +std::shared_ptr load_ncrystal_api() +{ + auto& db = get_ncrystal_api_db(); + std::lock_guard lock(db.mtx); + if (!db.api) { + void* raw_api = load_virtapi_raw(NCrystalAPI::interface_id, db); + if (!raw_api) + fatal_error("Problems loading NCrystal."); + db.api = *reinterpret_cast*>(raw_api); + } + return db.api; +} +} // namespace openmc diff --git a/src/output.cpp b/src/output.cpp index 63e7fe553..e20868efb 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -314,7 +314,6 @@ void print_build_info() std::string profiling(n); std::string coverage(n); std::string mcpl(n); - std::string ncrystal(n); std::string uwuw(n); #ifdef PHDF5 @@ -332,9 +331,6 @@ void print_build_info() #ifdef OPENMC_MCPL mcpl = y; #endif -#ifdef NCRYSTAL - ncrystal = y; -#endif #ifdef USE_LIBPNG png = y; #endif @@ -362,7 +358,6 @@ void print_build_info() fmt::print("DAGMC support: {}\n", dagmc); fmt::print("libMesh support: {}\n", libmesh); fmt::print("MCPL support: {}\n", mcpl); - fmt::print("NCrystal support: {}\n", ncrystal); fmt::print("Coverage testing: {}\n", coverage); fmt::print("Profiling flags: {}\n", profiling); fmt::print("UWUW support: {}\n", uwuw); diff --git a/src/source.cpp b/src/source.cpp index 2116809f2..c1b4ce260 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -4,7 +4,7 @@ #define HAS_DYNAMIC_LINKING #endif -#include // for move +#include // for move #ifdef HAS_DYNAMIC_LINKING #include // for dlopen, dlsym, dlclose, dlerror diff --git a/tests/regression_tests/ncrystal/test.py b/tests/regression_tests/ncrystal/test.py index e1c1e5ed6..8da05e1cf 100644 --- a/tests/regression_tests/ncrystal/test.py +++ b/tests/regression_tests/ncrystal/test.py @@ -6,12 +6,13 @@ import numpy as np import openmc import openmc.lib import pytest +import shutil from tests.testing_harness import PyAPITestHarness pytestmark = pytest.mark.skipif( - not openmc.lib._ncrystal_enabled(), - reason="NCrystal materials are not enabled.") + not shutil.which('ncrystal-config'), + reason="NCrystal is not installed.") def pencil_beam_model(cfg, E0, N): diff --git a/tools/ci/gha-install.py b/tools/ci/gha-install.py index 1eb9a55b4..1cc792f8d 100644 --- a/tools/ci/gha-install.py +++ b/tools/ci/gha-install.py @@ -3,7 +3,7 @@ import shutil import subprocess -def install(omp=False, mpi=False, phdf5=False, dagmc=False, libmesh=False, ncrystal=False): +def install(omp=False, mpi=False, phdf5=False, dagmc=False, libmesh=False): # Create build directory and change to it shutil.rmtree('build', ignore_errors=True) os.mkdir('build') @@ -40,9 +40,6 @@ def install(omp=False, mpi=False, phdf5=False, dagmc=False, libmesh=False, ncrys libmesh_path = os.environ.get('HOME') + '/LIBMESH' cmake_cmd.append('-DCMAKE_PREFIX_PATH=' + libmesh_path) - if ncrystal: - cmake_cmd.append('-DOPENMC_USE_NCRYSTAL=ON') - # Build in coverage mode for coverage testing cmake_cmd.append('-DOPENMC_ENABLE_COVERAGE=on') @@ -59,11 +56,10 @@ def main(): mpi = (os.environ.get('MPI') == 'y') phdf5 = (os.environ.get('PHDF5') == 'y') dagmc = (os.environ.get('DAGMC') == 'y') - ncrystal = (os.environ.get('NCRYSTAL') == 'y') libmesh = (os.environ.get('LIBMESH') == 'y') # Build and install - install(omp, mpi, phdf5, dagmc, libmesh, ncrystal) + install(omp, mpi, phdf5, dagmc, libmesh) if __name__ == '__main__': main() diff --git a/tools/ci/gha-install.sh b/tools/ci/gha-install.sh index 50f110ed4..d8a3a7600 100755 --- a/tools/ci/gha-install.sh +++ b/tools/ci/gha-install.sh @@ -14,12 +14,9 @@ if [[ $DAGMC = 'y' ]]; then ./tools/ci/gha-install-dagmc.sh fi -# Install NCrystal if needed -if [[ $NCRYSTAL = 'y' ]]; then - pip install 'ncrystal>=4.0.0' - #Basic quick verification: - nctool --test -fi +# Install NCrystal and verify installation +pip install 'ncrystal>=4.1.0' +nctool --test # Install vectfit for WMP generation if needed if [[ $VECTFIT = 'y' ]]; then diff --git a/tools/ci/gha-script.sh b/tools/ci/gha-script.sh index 4733907eb..c7c634ffa 100755 --- a/tools/ci/gha-script.sh +++ b/tools/ci/gha-script.sh @@ -14,10 +14,5 @@ if [[ $EVENT == 'y' ]]; then args="${args} --event " fi -# Check NCrystal installation -if [[ $NCRYSTAL = 'y' ]]; then - nctool --test -fi - # Run regression and unit tests pytest --cov=openmc -v $args tests From 557b714d8753fa9cf0e70a5fc6cdb52f8df0bdad Mon Sep 17 00:00:00 2001 From: Lewis Gross <43077972+lewisgross1296@users.noreply.github.com> Date: Wed, 5 Mar 2025 19:07:50 -0600 Subject: [PATCH 287/671] add continue feature for depletion (#3272) Co-authored-by: Connor Moreno Co-authored-by: Patrick Shriwise Co-authored-by: Lewis Gross Co-authored-by: Paul Romano --- openmc/deplete/abc.py | 75 ++++++++++++++++++---- openmc/deplete/results.py | 34 ++++++++-- tests/unit_tests/test_deplete_continue.py | 76 +++++++++++++++++++++++ 3 files changed, 169 insertions(+), 16 deletions(-) create mode 100644 tests/unit_tests/test_deplete_continue.py diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index fc98239a5..ab087b98e 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -1,6 +1,7 @@ """abc module. -This module contains Abstract Base Classes for implementing operator, integrator, depletion system solver, and operator helper classes +This module contains Abstract Base Classes for implementing operator, +integrator, depletion system solver, and operator helper classes """ from __future__ import annotations @@ -24,7 +25,8 @@ from openmc.utility_funcs import change_directory from openmc import Material from .stepresult import StepResult from .chain import Chain -from .results import Results +from .results import Results, _SECONDS_PER_MINUTE, _SECONDS_PER_HOUR, \ + _SECONDS_PER_DAY, _SECONDS_PER_JULIAN_YEAR from .pool import deplete from .reaction_rates import ReactionRates from .transfer_rates import TransferRates @@ -36,12 +38,6 @@ __all__ = [ "Integrator", "SIIntegrator", "DepSystemSolver", "add_params"] -_SECONDS_PER_MINUTE = 60 -_SECONDS_PER_HOUR = 60*60 -_SECONDS_PER_DAY = 24*60*60 -_SECONDS_PER_JULIAN_YEAR = 365.25*24*60*60 - - def _normalize_timesteps( timesteps: Sequence[float] | Sequence[tuple[float, str]], source_rates: float | Sequence[float], @@ -572,6 +568,18 @@ class Integrator(ABC): :attr:`solver`. .. versionadded:: 0.12 + continue_timesteps : bool, optional + Whether or not to treat the current solve as a continuation of a + previous simulation. Defaults to `False`. When `False`, the depletion + steps provided are appended to any previous steps. If `True`, the + timesteps provided to the `Integrator` must exacly match any that + exist in the `prev_results` passed to the `Operator`. The `power`, + `power_density`, or `source_rates` must match as well. The + method of specifying `power`, `power_density`, or + `source_rates` should be the same as the initial run. + + .. versionadded:: 0.15.1 + Attributes ---------- operator : openmc.deplete.abc.TransportOperator @@ -613,7 +621,8 @@ class Integrator(ABC): power_density: Optional[Union[float, Sequence[float]]] = None, source_rates: Optional[Union[float, Sequence[float]]] = None, timestep_units: str = 's', - solver: str = "cram48" + solver: str = "cram48", + continue_timesteps: bool = False, ): # Check number of stages previously used if operator.prev_res is not None: @@ -625,6 +634,8 @@ class Integrator(ABC): "this uses {}".format( self.__class__.__name__, res.data.shape[0], self._num_stages)) + elif continue_timesteps: + raise ValueError("Continuation run requires passing prev_results.") self.operator = operator self.chain = operator.chain @@ -642,6 +653,35 @@ class Integrator(ABC): # Normalize timesteps and source rates seconds, source_rates = _normalize_timesteps( timesteps, source_rates, timestep_units, operator) + + if continue_timesteps: + # Get timesteps and source rates from previous results + prev_times = operator.prev_res.get_times(timestep_units) + prev_source_rates = operator.prev_res.get_source_rates() + prev_timesteps = np.diff(prev_times) + + # Make sure parameters from the previous results are consistent with + # those passed to operator + num_prev = len(prev_timesteps) + if not np.array_equal(prev_timesteps, timesteps[:num_prev]): + raise ValueError( + "You are attempting to continue a run in which the previous timesteps " + "do not have the same initial timesteps as those provided to the " + "Integrator. Please make sure you are using the correct timesteps." + ) + if not np.array_equal(prev_source_rates, source_rates[:num_prev]): + raise ValueError( + "You are attempting to continue a run in which the previous results " + "do not have the same initial source rates, powers, or power densities " + "as those provided to the Integrator. Please make sure you are using " + "the correct powers, power densities, or source rates and previous " + "results file." + ) + + # Run with only the new time steps and source rates provided + seconds = seconds[num_prev:] + source_rates = source_rates[num_prev:] + self.timesteps = np.asarray(seconds) self.source_rates = np.asarray(source_rates) @@ -932,6 +972,18 @@ class SIIntegrator(Integrator): :attr:`solver`. .. versionadded:: 0.12 + continue_timesteps : bool, optional + Whether or not to treat the current solve as a continuation of a + previous simulation. Defaults to `False`. If `False`, all time + steps and source rates will be run in an append fashion and will run + after whatever time steps exist, if any. If `True`, the timesteps + provided to the `Integrator` must match exactly those that exist + in the `prev_results` passed to the `Opereator`. The `power`, + `power_density`, or `source_rates` must match as well. The + method of specifying `power`, `power_density`, or + `source_rates` should be the same as the initial run. + + .. versionadded:: 0.15.1 Attributes ---------- @@ -973,13 +1025,14 @@ class SIIntegrator(Integrator): source_rates: Optional[Sequence[float]] = None, timestep_units: str = 's', n_steps: int = 10, - solver: str = "cram48" + solver: str = "cram48", + continue_timesteps: bool = False, ): check_type("n_steps", n_steps, Integral) check_greater_than("n_steps", n_steps, 0) super().__init__( operator, timesteps, power, power_density, source_rates, - timestep_units=timestep_units, solver=solver) + timestep_units=timestep_units, solver=solver, continue_timesteps=continue_timesteps) self.n_steps = n_steps def _get_bos_data_from_operator(self, step_index, step_power, n_bos): diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index f897a8842..132ec572c 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -17,6 +17,11 @@ from openmc.checkvalue import PathLike __all__ = ["Results", "ResultsList"] +_SECONDS_PER_MINUTE = 60 +_SECONDS_PER_HOUR = 60*60 +_SECONDS_PER_DAY = 24*60*60 +_SECONDS_PER_JULIAN_YEAR = 365.25*24*60*60 # 365.25 due to the leap year + def _get_time_as(seconds: float, units: str) -> float: """Converts the time in seconds to time in different units @@ -31,13 +36,13 @@ def _get_time_as(seconds: float, units: str) -> float: """ if units == "a": - return seconds / (60 * 60 * 24 * 365.25) # 365.25 due to the leap year + return seconds / _SECONDS_PER_JULIAN_YEAR if units == "d": - return seconds / (60 * 60 * 24) + return seconds / _SECONDS_PER_DAY elif units == "h": - return seconds / (60 * 60) + return seconds / _SECONDS_PER_HOUR elif units == "min": - return seconds / 60 + return seconds / _SECONDS_PER_MINUTE else: return seconds @@ -71,7 +76,6 @@ class Results(list): data.append(StepResult.from_hdf5(fh, i)) super().__init__(data) - @classmethod def from_hdf5(cls, filename: PathLike): """Load in depletion results from a previous file @@ -460,6 +464,26 @@ class Results(list): return _get_time_as(times, time_units) + def get_source_rates(self) -> np.ndarray: + """ + .. versionadded:: 0.15.1 + + Returns + ------- + numpy.ndarray + 1-D vector of source rates at each point in the depletion simulation + with the units originally defined by the user. + + """ + # Results duplicate the final source rate at the final simulation time + source_rates = np.fromiter( + (r.source_rate for r in self), + dtype=self[0].source_rate.dtype, + count=len(self)-1, + ) + + return source_rates + def get_step_where( self, time, time_units: str = "d", atol: float = 1e-6, rtol: float = 1e-3 ) -> int: diff --git a/tests/unit_tests/test_deplete_continue.py b/tests/unit_tests/test_deplete_continue.py new file mode 100644 index 000000000..53c4c56d2 --- /dev/null +++ b/tests/unit_tests/test_deplete_continue.py @@ -0,0 +1,76 @@ +"""Unit tests for openmc.deplete continue run capability. + +These tests run in two steps: first a normal run and then a continue run using the previous results +""" + +import pytest +import openmc.deplete + +from tests import dummy_operator + + +def test_continue(run_in_tmpdir): + """Test to ensure that a properly defined continue run works""" + # set up the problem + bundle = dummy_operator.SCHEMES['predictor'] + operator = dummy_operator.DummyOperator() + + # initial depletion + bundle.solver(operator, [1.0, 2.0], [1.0, 2.0]).integrate() + + # set up continue run + prev_res = openmc.deplete.Results(operator.output_dir / "depletion_results.h5") + operator = dummy_operator.DummyOperator(prev_res) + + # if continue run happens, test passes + bundle.solver(operator, [1.0, 2.0, 3.0, 4.0], [1.0, 2.0, 3.0, 4.0], + continue_timesteps=True).integrate() + + +def test_mismatched_initial_times(run_in_tmpdir): + """Test to ensure that a continue run with different initial steps is properly caught""" + # set up the problem + bundle = dummy_operator.SCHEMES['predictor'] + operator = dummy_operator.DummyOperator() + + # perform initial steps + bundle.solver(operator, [0.75, 0.75], [1.0, 1.0]).integrate() + + # restart + prev_res = openmc.deplete.Results(operator.output_dir / "depletion_results.h5") + operator = dummy_operator.DummyOperator(prev_res) + + with pytest.raises( + ValueError, + match="You are attempting to continue a run in which the previous timesteps " + "do not have the same initial timesteps as those provided to the " + "Integrator. Please make sure you are using the correct timesteps.", + ): + bundle.solver( + operator, [0.75, 0.5, 0.75], [1.0, 1.0, 1.0], continue_timesteps=True + ).integrate() + + +def test_mismatched_initial_source_rates(run_in_tmpdir): + """Test to ensure that a continue run with different initial steps is properly caught""" + # set up the problem + bundle = dummy_operator.SCHEMES['predictor'] + operator = dummy_operator.DummyOperator() + + # perform initial steps + bundle.solver(operator, [0.75, 0.75], [1.0, 1.0]).integrate() + + # restart + prev_res = openmc.deplete.Results(operator.output_dir / "depletion_results.h5") + operator = dummy_operator.DummyOperator(prev_res) + + with pytest.raises( + ValueError, + match="You are attempting to continue a run in which the previous results " + "do not have the same initial source rates, powers, or power densities " + "as those provided to the Integrator. Please make sure you are using " + "the correct powers, power densities, or source rates and previous results file.", + ): + bundle.solver( + operator, [0.75, 0.75, 0.75], [1.0, 2.0, 1.0], continue_timesteps=True + ).integrate() From e878933b9027fa8cdef9852c806cccba908844eb Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 5 Mar 2025 23:48:04 -0600 Subject: [PATCH 288/671] Fix bug in Mesh::material_volumes for void materials (#3337) --- include/openmc/mesh.h | 4 --- src/mesh.cpp | 66 ++++++++++++++++++++++++++++--------------- 2 files changed, 44 insertions(+), 26 deletions(-) diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index 943e49a5f..8faf45f1b 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -119,10 +119,6 @@ private: double* volumes_; //!< volume in [cm^3] (bins, table_size) int table_size_; //!< Size of hash table for each mesh element bool table_full_ {false}; //!< Whether the hash table is full - - // Value used to indicate an empty slot in the hash table. We use -2 because - // the value -1 is used to indicate a void material. - static constexpr int EMPTY {-2}; }; } // namespace detail diff --git a/src/mesh.cpp b/src/mesh.cpp index a24b51a0b..101da7468 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -69,6 +69,10 @@ const bool LIBMESH_ENABLED = true; const bool LIBMESH_ENABLED = false; #endif +// Value used to indicate an empty slot in the hash table. We use -2 because +// the value -1 is used to indicate a void material. +constexpr int32_t EMPTY = -2; + namespace model { std::unordered_map mesh_map; @@ -113,7 +117,7 @@ inline bool check_intersection_point(double x1, double x0, double y1, double y0, //! Atomic compare-and-swap for signed 32-bit integer // //! \param[in,out] ptr Pointer to value to update -//! \param[in] expected Value to compare to +//! \param[in,out] expected Value to compare to //! \param[in] desired If comparison is successful, value to update to //! \return True if the comparison was successful and the value was updated inline bool atomic_cas_int32(int32_t* ptr, int32_t& expected, int32_t desired) @@ -152,8 +156,10 @@ void MaterialVolumes::add_volume( // Loop for linear probing for (int attempt = 0; attempt < table_size_; ++attempt) { - // Determine slot to check + // Determine slot to check, making sure it is positive int slot = (index_material + attempt) % table_size_; + if (slot < 0) + slot += table_size_; int32_t* slot_ptr = &this->materials(index_elem, slot); // Non-atomic read of current material @@ -192,7 +198,10 @@ void MaterialVolumes::add_volume_unsafe( { // Linear probe for (int attempt = 0; attempt < table_size_; ++attempt) { + // Determine slot to check, making sure it is positive int slot = (index_material + attempt) % table_size_; + if (slot < 0) + slot += table_size_; // Read current material int32_t current_val = this->materials(index_elem, slot); @@ -274,13 +283,15 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, if (mpi::master) { header("MESH MATERIAL VOLUMES CALCULATION", 7); } + write_message(7, "Number of mesh elements = {}", n_bins()); write_message(7, "Number of rays (x) = {}", nx); write_message(7, "Number of rays (y) = {}", ny); write_message(7, "Number of rays (z) = {}", nz); - int64_t n_total = nx * ny + ny * nz + nx * nz; + int64_t n_total = static_cast(nx) * ny + + static_cast(ny) * nz + + static_cast(nx) * nz; write_message(7, "Total number of rays = {}", n_total); - write_message( - 7, "Maximum number of materials per mesh element = {}", table_size); + write_message(7, "Table size per mesh element = {}", table_size); Timer timer; timer.start(); @@ -294,8 +305,9 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, std::array n_rays = {nx, ny, nz}; // Determine effective width of rays - Position width((bbox.xmax - bbox.xmin) / nx, (bbox.ymax - bbox.ymin) / ny, - (bbox.zmax - bbox.zmin) / nz); + Position width((nx > 0) ? (bbox.xmax - bbox.xmin) / nx : 0.0, + (ny > 0) ? (bbox.ymax - bbox.ymin) / ny : 0.0, + (nz > 0) ? (bbox.zmax - bbox.zmin) / nz : 0.0); // Set flag for mesh being contained within model bool out_of_model = false; @@ -327,6 +339,9 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, double d2 = width[ax2]; int n1 = n_rays[ax1]; int n2 = n_rays[ax2]; + if (n1 == 0 || n2 == 0) { + continue; + } // Divide rays in first direction over MPI processes by computing starting // and ending indices @@ -442,8 +457,8 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, for (int i = 1; i < mpi::n_procs; ++i) { // Receive material indices and volumes from process i - MPI_Recv( - mats.data(), total, MPI_INT, i, i, mpi::intracomm, MPI_STATUS_IGNORE); + MPI_Recv(mats.data(), total, MPI_INT32_T, i, i, mpi::intracomm, + MPI_STATUS_IGNORE); MPI_Recv(vols.data(), total, MPI_DOUBLE, i, i, mpi::intracomm, MPI_STATUS_IGNORE); @@ -453,13 +468,15 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, for (int index_elem = 0; index_elem < n_bins(); ++index_elem) { for (int k = 0; k < table_size; ++k) { int index = index_elem * table_size + k; - result.add_volume_unsafe(index_elem, mats[index], vols[index]); + if (mats[index] != EMPTY) { + result.add_volume_unsafe(index_elem, mats[index], vols[index]); + } } } } } else { // Send material indices and volumes to process 0 - MPI_Send(materials, total, MPI_INT, 0, mpi::rank, mpi::intracomm); + MPI_Send(materials, total, MPI_INT32_T, 0, mpi::rank, mpi::intracomm); MPI_Send(volumes, total, MPI_DOUBLE, 0, mpi::rank, mpi::intracomm); } } @@ -484,19 +501,24 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, } } - // Show elapsed time + // Get total time and normalization time timer.stop(); double t_total = timer.elapsed(); - double t_normalize = t_total - t_raytrace - t_mpi; - if (mpi::master) { - header("Timing Statistics", 7); - show_time("Total time elapsed", t_total); - show_time("Ray tracing", t_raytrace, 1); - show_time("Ray tracing (per ray)", t_raytrace / n_total, 1); - show_time("MPI communication", t_mpi, 1); - show_time("Normalization", t_normalize, 1); - std::fflush(stdout); - } + double t_norm = t_total - t_raytrace - t_mpi; + + // Show timing statistics + if (settings::verbosity < 7 || !mpi::master) + return; + header("Timing Statistics", 7); + fmt::print(" Total time elapsed = {:.4e} seconds\n", t_total); + fmt::print(" Ray tracing = {:.4e} seconds\n", t_raytrace); + fmt::print(" MPI communication = {:.4e} seconds\n", t_mpi); + fmt::print(" Normalization = {:.4e} seconds\n", t_norm); + fmt::print(" Calculation rate = {:.4e} rays/seconds\n", + n_total / t_raytrace); + fmt::print(" Calculation rate (per thread) = {:.4e} rays/seconds\n", + n_total / (t_raytrace * mpi::n_procs * num_threads())); + std::fflush(stdout); } void Mesh::to_hdf5(hid_t group) const From e12c65dff91e5654210a1a5b82f4bf57a0203386 Mon Sep 17 00:00:00 2001 From: Stefano Segantin <92783079+SteSeg@users.noreply.github.com> Date: Thu, 6 Mar 2025 08:44:10 -0500 Subject: [PATCH 289/671] openmc.Material.mix_materials() allows for **kwargs (#3336) --- openmc/material.py | 15 +++++++-------- tests/unit_tests/test_material.py | 6 ++++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/openmc/material.py b/openmc/material.py index b104b374e..f40ac805f 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -1467,7 +1467,7 @@ class Material(IDManagerMixin): @classmethod def mix_materials(cls, materials, fracs: Iterable[float], - percent_type: str = 'ao', name: str | None = None) -> Material: + percent_type: str = 'ao', **kwargs) -> Material: """Mix materials together based on atom, weight, or volume fractions .. versionadded:: 0.12 @@ -1482,10 +1482,8 @@ class Material(IDManagerMixin): Type of percentage, must be one of 'ao', 'wo', or 'vo', to signify atom percent (molar percent), weight percent, or volume percent, optional. Defaults to 'ao' - name : str - The name for the new material, optional. Defaults to concatenated - names of input materials with percentages indicated inside - parentheses. + **kwargs + Keyword arguments passed to :class:`openmc.Material` Returns ------- @@ -1544,10 +1542,11 @@ class Material(IDManagerMixin): openmc.data.AVOGADRO # Create the new material with the desired name - if name is None: - name = '-'.join([f'{m.name}({f})' for m, f in + if "name" not in kwargs: + kwargs["name"] = '-'.join([f'{m.name}({f})' for m, f in zip(materials, fracs)]) - new_mat = cls(name=name) + + new_mat = cls(**kwargs) # Compute atom fractions of nuclides and add them to the new material tot_nuclides_per_cc = np.sum([dens for dens in nuclides_per_cc.values()]) diff --git a/tests/unit_tests/test_material.py b/tests/unit_tests/test_material.py index ec55a7756..cb71cd09b 100644 --- a/tests/unit_tests/test_material.py +++ b/tests/unit_tests/test_material.py @@ -533,11 +533,13 @@ def test_mix_materials(): dens4 = 1. / (f0 / m1dens + f1 / m2dens) dens5 = f0*m1dens + f1*m2dens m3 = openmc.Material.mix_materials([m1, m2], [f0, f1], percent_type='ao') - m4 = openmc.Material.mix_materials([m1, m2], [f0, f1], percent_type='wo') - m5 = openmc.Material.mix_materials([m1, m2], [f0, f1], percent_type='vo') + m4 = openmc.Material.mix_materials([m1, m2], [f0, f1], percent_type='wo', material_id=999) + m5 = openmc.Material.mix_materials([m1, m2], [f0, f1], percent_type='vo', name='m5') assert m3.density == pytest.approx(dens3) assert m4.density == pytest.approx(dens4) assert m5.density == pytest.approx(dens5) + assert m4.id == 999 + assert m5.name == 'm5' def test_get_activity(): From e360cb467e9b97412c4fcbfbc58c0ff4226b5014 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Thu, 6 Mar 2025 17:39:05 +0100 Subject: [PATCH 290/671] added stable and unstable nuclides to the Chain object (#3338) Co-authored-by: Paul Romano --- openmc/deplete/chain.py | 20 ++++++++++++++++++-- tests/unit_tests/test_deplete_chain.py | 8 ++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index 2e81250dc..4998a2c3d 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -12,6 +12,7 @@ from collections import defaultdict, namedtuple from collections.abc import Mapping, Iterable from numbers import Real, Integral from warnings import warn +from typing import List import lxml.etree as ET import scipy.sparse as sp @@ -244,6 +245,10 @@ class Chain: Reactions that are tracked in the depletion chain nuclide_dict : dict of str to int Maps a nuclide name to an index in nuclides. + stable_nuclides : list of openmc.deplete.Nuclide + List of stable nuclides available in the chain. + unstable_nuclides : list of openmc.deplete.Nuclide + List of unstable nuclides available in the chain. fission_yields : None or iterable of dict List of effective fission yields for materials. Each dictionary should be of the form ``{parent: {product: yield}}`` with @@ -256,7 +261,7 @@ class Chain: """ def __init__(self): - self.nuclides = [] + self.nuclides: List[Nuclide] = [] self.reactions = [] self.nuclide_dict = {} self._fission_yields = None @@ -272,7 +277,18 @@ class Chain: """Number of nuclides in chain.""" return len(self.nuclides) - def add_nuclide(self, nuclide): + + @property + def stable_nuclides(self) -> List[Nuclide]: + """List of stable nuclides available in the chain""" + return [nuc for nuc in self.nuclides if nuc.half_life is None] + + @property + def unstable_nuclides(self) -> List[Nuclide]: + """List of unstable nuclides available in the chain""" + return [nuc for nuc in self.nuclides if nuc.half_life is not None] + + def add_nuclide(self, nuclide: Nuclide): """Add a nuclide to the depletion chain Parameters diff --git a/tests/unit_tests/test_deplete_chain.py b/tests/unit_tests/test_deplete_chain.py index 9753a53f7..13267cb05 100644 --- a/tests/unit_tests/test_deplete_chain.py +++ b/tests/unit_tests/test_deplete_chain.py @@ -86,6 +86,14 @@ def test_from_endf(endf_chain): assert nuc == chain[nuc.name] +def test_unstable_nuclides(simple_chain: Chain): + assert [nuc.name for nuc in simple_chain.unstable_nuclides] == ["A", "B"] + + +def test_stable_nuclides(simple_chain: Chain): + assert [nuc.name for nuc in simple_chain.stable_nuclides] == ["H1", "C"] + + def test_from_xml(simple_chain): """Read chain_test.xml and ensure all values are correct.""" # Unfortunately, this routine touches a lot of the code, but most of From 9bfce4ee1c65496f06b718a1fb9fb54d16f3a1a2 Mon Sep 17 00:00:00 2001 From: Stefano Segantin <92783079+SteSeg@users.noreply.github.com> Date: Thu, 6 Mar 2025 16:33:26 -0500 Subject: [PATCH 291/671] Determine nuclides correctly for DAGMC models in d1s.get_radionuclides (#3335) Co-authored-by: Paul Romano --- openmc/deplete/d1s.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openmc/deplete/d1s.py b/openmc/deplete/d1s.py index 22fc6ec23..8f6bffa86 100644 --- a/openmc/deplete/d1s.py +++ b/openmc/deplete/d1s.py @@ -33,8 +33,10 @@ def get_radionuclides(model: openmc.Model, chain_file: str | None = None) -> lis List of nuclide names """ - # Determine what nuclides appear in model - model_nuclides = set(model.geometry.get_all_nuclides()) + + # Determine what nuclides appear in the model + model_nuclides = {nuc for mat in model._materials_by_id.values() + for nuc in mat.get_nuclides()} # Load chain file if chain_file is None: From e8c9134ff6392e1e47c284d36607fc194c9537ac Mon Sep 17 00:00:00 2001 From: Perry <100789850+yrrepy@users.noreply.github.com> Date: Thu, 6 Mar 2025 16:00:29 -0800 Subject: [PATCH 292/671] Add option for survival biasing source normalization (#3070) Co-authored-by: Patrick Shriwise Co-authored-by: Paul Romano --- docs/source/io_formats/settings.rst | 7 ++++ include/openmc/particle_data.h | 13 +++++-- include/openmc/settings.h | 1 + openmc/settings.py | 53 +++++++++++++++++------------ src/physics.cpp | 8 ++++- src/physics_mg.cpp | 8 ++++- src/settings.cpp | 5 +++ src/simulation.cpp | 5 ++- tests/unit_tests/test_settings.py | 2 ++ 9 files changed, 76 insertions(+), 26 deletions(-) diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 5174cfd8b..0d5367727 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -81,6 +81,13 @@ time. *Default*: 1.0 + :survival_normalization: + If this element is set to "true", this will enable the use of survival + biasing source normalization, whereby the weight parameters, weight and + weight_avg, are multiplied per history by the start weight of said history. + + *Default*: false + :energy_neutron: The energy under which neutrons will be killed. diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index 6b318385e..15ae57893 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -434,6 +434,7 @@ private: int g_last_; double wgt_ {1.0}; + double wgt_born_ {1.0}; double mu_; double time_ {0.0}; double time_last_ {0.0}; @@ -532,12 +533,20 @@ public: int& g_last() { return g_last_; } const int& g_last() const { return g_last_; } - // Statistic weight of particle. Setting to zero - // indicates that the particle is dead. + // Statistic weight of particle. Setting to zero indicates that the particle + // is dead. double& wgt() { return wgt_; } double wgt() const { return wgt_; } + + // Statistic weight of particle at birth + double& wgt_born() { return wgt_born_; } + double wgt_born() const { return wgt_born_; } + + // Statistic weight of particle at last collision double& wgt_last() { return wgt_last_; } const double& wgt_last() const { return wgt_last_; } + + // Whether particle is alive bool alive() const { return wgt_ != 0.0; } // Polar scattering angle after a collision diff --git a/include/openmc/settings.h b/include/openmc/settings.h index 12835fdef..f3aff08b0 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -61,6 +61,7 @@ extern bool surf_source_write; //!< write surface source file? extern bool surf_mcpl_write; //!< write surface mcpl file? extern bool surf_source_read; //!< read surface source file? extern bool survival_biasing; //!< use survival biasing? +extern bool survival_normalization; //!< use survival normalization? extern bool temperature_multipole; //!< use multipole data? extern "C" bool trigger_on; //!< tally triggers enabled? extern bool trigger_predict; //!< predict batches for triggers? diff --git a/openmc/settings.py b/openmc/settings.py index 882a17b68..2f054ebd1 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -50,15 +50,19 @@ class Settings: Indicate whether fission neutrons should be created or not. cutoff : dict Dictionary defining weight cutoff, energy cutoff and time cutoff. The - dictionary may have ten keys, 'weight', 'weight_avg', 'energy_neutron', - 'energy_photon', 'energy_electron', 'energy_positron', 'time_neutron', - 'time_photon', 'time_electron', and 'time_positron'. Value for 'weight' - should be a float indicating weight cutoff below which particle undergo - Russian roulette. Value for 'weight_avg' should be a float indicating - weight assigned to particles that are not killed after Russian roulette. - Value of energy should be a float indicating energy in eV below which - particle type will be killed. Value of time should be a float in - seconds. Particles will be killed exactly at the specified time. + dictionary may have the following keys, 'weight', 'weight_avg', + 'survival_normalization', 'energy_neutron', 'energy_photon', + 'energy_electron', 'energy_positron', 'time_neutron', 'time_photon', + 'time_electron', and 'time_positron'. Value for 'weight' should be a + float indicating weight cutoff below which particle undergo Russian + roulette. Value for 'weight_avg' should be a float indicating weight + assigned to particles that are not killed after Russian roulette. Value + of energy should be a float indicating energy in eV below which particle + type will be killed. Value of time should be a float in seconds. + Particles will be killed exactly at the specified time. Value for + 'survival_normalization' is a bool indicating whether or not the weight + cutoff parameters will be applied relative to the particle's starting + weight or to its current weight. delayed_photon_scaling : bool Indicate whether to scale the fission photon yield by (EGP + EGD)/EGP where EGP is the energy release of prompt photons and EGD is the energy @@ -162,20 +166,20 @@ class Settings: 'naive', 'simulation_averaged', or 'hybrid'. The default is 'hybrid'. :source_shape: - Assumed shape of the source distribution within each source - region. Options are 'flat' (default), 'linear', or 'linear_xy'. + Assumed shape of the source distribution within each source region. + Options are 'flat' (default), 'linear', or 'linear_xy'. :volume_normalized_flux_tallies: - Whether to normalize flux tallies by volume (bool). The default - is 'False'. When enabled, flux tallies will be reported in units of - cm/cm^3. When disabled, flux tallies will be reported in units - of cm (i.e., total distance traveled by neutrons in the spatial - tally region). + Whether to normalize flux tallies by volume (bool). The default is + 'False'. When enabled, flux tallies will be reported in units of + cm/cm^3. When disabled, flux tallies will be reported in units of cm + (i.e., total distance traveled by neutrons in the spatial tally + region). :adjoint: Whether to run the random ray solver in adjoint mode (bool). The default is 'False'. :sample_method: - Sampling method for the ray starting location and direction of travel. - Options are `prng` (default) or 'halton`. + Sampling method for the ray starting location and direction of + travel. Options are `prng` (default) or 'halton`. .. versionadded:: 0.15.0 resonance_scattering : dict @@ -899,6 +903,8 @@ class Settings: cv.check_type('average survival weight', cutoff[key], Real) cv.check_greater_than('average survival weight', cutoff[key], 0.0) + elif key == 'survival_normalization': + cv.check_type('survival normalization', cutoff[key], bool) elif key in ['energy_neutron', 'energy_photon', 'energy_electron', 'energy_positron']: cv.check_type('energy cutoff', cutoff[key], Real) @@ -1364,7 +1370,8 @@ class Settings: element = ET.SubElement(root, "cutoff") for key, value in self._cutoff.items(): subelement = ET.SubElement(element, key) - subelement.text = str(value) + subelement.text = str(value) if key != 'survival_normalization' \ + else str(value).lower() def _create_entropy_mesh_subelement(self, root, mesh_memo=None): if self.entropy_mesh is None: @@ -1797,10 +1804,14 @@ class Settings: self.cutoff = {} for key in ('energy_neutron', 'energy_photon', 'energy_electron', 'energy_positron', 'weight', 'weight_avg', 'time_neutron', - 'time_photon', 'time_electron', 'time_positron'): + 'time_photon', 'time_electron', 'time_positron', + 'survival_normalization'): value = get_text(elem, key) if value is not None: - self.cutoff[key] = float(value) + if key == 'survival_normalization': + self.cutoff[key] = value in ('true', '1') + else: + self.cutoff[key] = float(value) def _entropy_mesh_from_xml_element(self, root, meshes): text = get_text(root, 'entropy_mesh') diff --git a/src/physics.cpp b/src/physics.cpp index e04f54c0b..72c04a5ca 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -153,7 +153,13 @@ void sample_neutron_reaction(Particle& p) // Play russian roulette if survival biasing is turned on if (settings::survival_biasing) { - if (p.wgt() < settings::weight_cutoff) { + // 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); } } diff --git a/src/physics_mg.cpp b/src/physics_mg.cpp index e381b27c9..c97a3d6f3 100644 --- a/src/physics_mg.cpp +++ b/src/physics_mg.cpp @@ -68,7 +68,13 @@ void sample_reaction(Particle& p) // Play Russian roulette if survival biasing is turned on if (settings::survival_biasing) { - if (p.wgt() < settings::weight_cutoff) { + // 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); } } diff --git a/src/settings.cpp b/src/settings.cpp index cd925be70..67fdfea66 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -69,6 +69,7 @@ bool surf_source_write {false}; bool surf_mcpl_write {false}; bool surf_source_read {false}; bool survival_biasing {false}; +bool survival_normalization {false}; bool temperature_multipole {false}; bool trigger_on {false}; bool trigger_predict {false}; @@ -624,6 +625,10 @@ void read_settings_xml(pugi::xml_node root) if (check_for_node(node_cutoff, "weight_avg")) { weight_survive = std::stod(get_node_value(node_cutoff, "weight_avg")); } + if (check_for_node(node_cutoff, "survival_normalization")) { + survival_normalization = + get_node_value_bool(node_cutoff, "survival_normalization"); + } if (check_for_node(node_cutoff, "energy_neutron")) { energy_cutoff[0] = std::stod(get_node_value(node_cutoff, "energy_neutron")); diff --git a/src/simulation.cpp b/src/simulation.cpp index a3c6495ae..74a7dbe63 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -586,6 +586,9 @@ void initialize_history(Particle& p, int64_t index_source) // Reset weight window ratio p.ww_factor() = 0.0; + // set particle history start weight + p.wgt_born() = p.wgt(); + // Reset pulse_height_storage std::fill(p.pht_storage().begin(), p.pht_storage().end(), 0); @@ -610,7 +613,7 @@ void initialize_history(Particle& p, int64_t index_source) write_message("Simulating Particle {}", p.id()); } -// Add paricle's starting weight to count for normalizing tallies later +// Add particle's starting weight to count for normalizing tallies later #pragma omp atomic simulation::total_weight += p.wgt(); diff --git a/tests/unit_tests/test_settings.py b/tests/unit_tests/test_settings.py index 02a476251..397d70434 100644 --- a/tests/unit_tests/test_settings.py +++ b/tests/unit_tests/test_settings.py @@ -26,6 +26,7 @@ def test_export_to_xml(run_in_tmpdir): s.plot_seed = 100 s.survival_biasing = True s.cutoff = {'weight': 0.25, 'weight_avg': 0.5, 'energy_neutron': 1.0e-5, + 'survival_normalization': True, 'energy_photon': 1000.0, 'energy_electron': 1.0e-5, 'energy_positron': 1.0e-5, 'time_neutron': 1.0e-5, 'time_photon': 1.0e-5, 'time_electron': 1.0e-5, @@ -99,6 +100,7 @@ def test_export_to_xml(run_in_tmpdir): assert s.seed == 17 assert s.survival_biasing assert s.cutoff == {'weight': 0.25, 'weight_avg': 0.5, + 'survival_normalization': True, 'energy_neutron': 1.0e-5, 'energy_photon': 1000.0, 'energy_electron': 1.0e-5, 'energy_positron': 1.0e-5, 'time_neutron': 1.0e-5, 'time_photon': 1.0e-5, From 9b5678b5f03d87a54dea9a8a2d2d20d850206ec6 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Thu, 6 Mar 2025 20:48:31 -0600 Subject: [PATCH 293/671] Random Ray Source Region Mesh Subdivision (Cell-Under-Voxel Geometry) (#3333) Co-authored-by: Paul Romano --- docs/source/_images/2x2_sr_mesh.png | Bin 0 -> 109424 bytes docs/source/io_formats/settings.rst | 18 + docs/source/usersguide/random_ray.rst | 100 +- docs/source/usersguide/variance_reduction.rst | 12 +- include/openmc/constants.h | 4 + .../openmc/random_ray/flat_source_domain.h | 51 +- include/openmc/random_ray/parallel_map.h | 193 + include/openmc/random_ray/random_ray.h | 19 +- .../openmc/random_ray/random_ray_simulation.h | 10 +- include/openmc/random_ray/source_region.h | 326 +- openmc/settings.py | 55 +- src/finalize.cpp | 3 + src/random_ray/flat_source_domain.cpp | 466 +- src/random_ray/linear_source_domain.cpp | 35 +- src/random_ray/random_ray.cpp | 315 +- src/random_ray/random_ray_simulation.cpp | 185 +- src/random_ray/source_region.cpp | 229 +- src/settings.cpp | 26 + src/weight_windows.cpp | 13 +- .../random_ray_fixed_source_mesh/__init__.py | 0 .../flat/inputs_true.dat | 271 + .../flat/results_true.dat | 9 + .../linear/inputs_true.dat | 271 + .../linear/results_true.dat | 9 + .../random_ray_fixed_source_mesh/test.py | 53 + .../random_ray_k_eff_mesh/__init__.py | 0 .../random_ray_k_eff_mesh/inputs_true.dat | 119 + .../random_ray_k_eff_mesh/results_true.dat | 171 + .../random_ray_k_eff_mesh/test.py | 36 + .../weightwindows_fw_cadis_mesh/__init__.py | 0 .../flat/inputs_true.dat | 265 + .../flat/results_true.dat | 6760 +++++++++++++++++ .../linear/inputs_true.dat | 265 + .../linear/results_true.dat | 6760 +++++++++++++++++ .../weightwindows_fw_cadis_mesh/test.py | 49 + 35 files changed, 16638 insertions(+), 460 deletions(-) create mode 100644 docs/source/_images/2x2_sr_mesh.png create mode 100644 include/openmc/random_ray/parallel_map.h create mode 100644 tests/regression_tests/random_ray_fixed_source_mesh/__init__.py create mode 100644 tests/regression_tests/random_ray_fixed_source_mesh/flat/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_mesh/flat/results_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_mesh/linear/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_mesh/linear/results_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_mesh/test.py create mode 100644 tests/regression_tests/random_ray_k_eff_mesh/__init__.py create mode 100644 tests/regression_tests/random_ray_k_eff_mesh/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_k_eff_mesh/results_true.dat create mode 100644 tests/regression_tests/random_ray_k_eff_mesh/test.py create mode 100644 tests/regression_tests/weightwindows_fw_cadis_mesh/__init__.py create mode 100644 tests/regression_tests/weightwindows_fw_cadis_mesh/flat/inputs_true.dat create mode 100644 tests/regression_tests/weightwindows_fw_cadis_mesh/flat/results_true.dat create mode 100644 tests/regression_tests/weightwindows_fw_cadis_mesh/linear/inputs_true.dat create mode 100644 tests/regression_tests/weightwindows_fw_cadis_mesh/linear/results_true.dat create mode 100644 tests/regression_tests/weightwindows_fw_cadis_mesh/test.py diff --git a/docs/source/_images/2x2_sr_mesh.png b/docs/source/_images/2x2_sr_mesh.png new file mode 100644 index 0000000000000000000000000000000000000000..5cdc684d3b94c2e4f92032b0a8ee284e024c2519 GIT binary patch literal 109424 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\O3+|R```>Do86nkp25zPfJVHq@R|dY7^MW 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. *Default*: prng + :source_region_meshes: + Relates meshes to spatial domains for subdividing source regions with each domain. + + :mesh: + Contains an ``id`` attribute and one or more ```` sub-elements. + + :id: + The unique identifier for the mesh. + + :domain: + Each domain element has an ``id`` attribute and a ``type`` attribute. + + :id: + The unique identifier for the domain. + + :type: + The type of the domain. Can be ``material``, ``cell``, or ``universe``. + ---------------------------------- ```` Element ---------------------------------- diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index eff1764d4..f28aed784 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -319,20 +319,24 @@ Default behavior using OpenMC's native PRNG can be manually specified as:: .. _subdivision_fsr: ----------------------------------- -Subdivision of Flat Source Regions ----------------------------------- +----------------------------- +Subdivision of Source Regions +----------------------------- -While the scattering and fission sources in Monte Carlo -are treated continuously, they are assumed to be invariant (flat) within a -MOC or random ray flat source region (FSR). This introduces bias into the -simulation, which can be remedied by reducing the physical size of the FSR -to dimensions below that of typical mean free paths of particles. +While the scattering and fission sources in Monte Carlo are treated +continuously, they are assumed to have a shape (flat or linear) within a MOC or +random ray source region (SR). This introduces bias into the simulation that can +be remedied by reducing the physical size of the SR to be smaller than the +typical mean free paths of particles. While use of linear sources in OpenMC +greatly reduces the error stemming from this approximation, subdivision is still +typically required. -In OpenMC, this subdivision currently must be done manually. The level of +In OpenMC, this subdivision can be done either manually by the user (by defining +additional surfaces and cells in the geometry) or automatically by assigning a +mesh to one or more cells, universes, or material types. The level of subdivision needed will be dependent on the fidelity the user requires. For -typical light water reactor analysis, consider the following example subdivision -of a two-dimensional 2x2 reflective pincell lattice: +typical light water reactor analysis, consider the following example of manual +subdivision of a two-dimensional 2x2 reflective pincell lattice: .. figure:: ../_images/2x2_materials.jpeg :class: with-border @@ -344,9 +348,79 @@ of a two-dimensional 2x2 reflective pincell lattice: :class: with-border :width: 400 - FSR decomposition for an asymmetrical 2x2 lattice (1.26 cm pitch) + Manual decomposition for an asymmetrical 2x2 lattice (1.26 cm pitch) -In the future, automated subdivision of FSRs via mesh overlay may be supported. +Geometry cells can also be subdivided into small source regions by assigning a +mesh to a list of domains, with each domain being of type +:class:`openmc.Material`, :class:`openmc.Cell`, or :class:`openmc.Universe`. The +idea of defining a source region as a combination of a base geometry cell and a +mesh element is known as "cell-under-voxel" style geometry, although in OpenMC +the mesh can be any kind and is not restricted to 3D regular voxels. An example +of overlaying a simple 2D mesh over a geometry is given as:: + + sr_mesh = openmc.RegularMesh() + sr_mesh.dimension = (n, n) + sr_mesh.lower_left = (0.0, 0.0) + sr_mesh.upper_right = (x, y) + domain = geometry.root_universe + settings.random_ray['source_region_meshes'] = [(sr_mesh, [domain])] + +In the above example, we apply a single :math:`n \times n` uniform mesh over the +entire domain by assigning it to the root universe of the geometry. +Alternatively, we might want to apply a finer or coarser mesh to different +regions of a 3D problem, for instance, as:: + + fuel = openmc.Material(name='UO2 fuel') + ... + water = openmc.Material(name='hot borated water') + ... + clad = openmc.Material(name='Zr cladding') + ... + + coarse_mesh = openmc.RegularMesh() + coarse_mesh.dimension = (n, n, n) + coarse_mesh.lower_left = (0.0, 0.0, 0.0) + coarse_mesh.upper_right = (x, y, z) + + fine_mesh = openmc.RegularMesh() + fine_mesh.dimension = (2*n, 2*n, 2*n) + fine_mesh.lower_left = (0.0, 0.0, 0.0) + fine_mesh.upper_right = (x, y, z) + + settings.random_ray['source_region_meshes'] = [(fine_mesh, [fuel, clad]), (coarse_mesh, [water])] + +Note that we don't need to adjust the outer bounds of the mesh to tightly wrap +the domain we assign the mesh to. Rather, OpenMC will dynamically generate +source regions based on the mesh bins rays actually visit, such that no +additional memory is wasted even if a domain only intersects a few mesh bins. +Going back to our 2x2 lattice example, if using a mesh-based subdivision, this +might look as below: + +.. figure:: ../_images/2x2_sr_mesh.png + :class: with-border + :width: 400 + + 20x20 overlaid "cell-under-voxel" mesh decomposition for an asymmetrical 2x2 lattice (1.26 cm pitch) + +Note that mesh-bashed subdivision is much easier for a user to implement but +does have a few downsides compared to manual subdivision. Manual subdivision can +be done with the specifics of the geometry in mind. As in the pincell example, +it is more efficient to subdivide the fuel region into azimuthal sectors and +radial rings as opposed to a Cartesian mesh. This is more efficient because the +regions are a more uniform size and follow the material boundaries closer, +resulting in the need for fewer source regions. Fewer source regions tends to +equate to a faster computational speed and/or the need for fewer rays per batch +to achieve good statistics. Additionally, applying a mesh often tends to create +a few very small source regions, as shown in the above picture where corners of +the mesh happen to intersect close to the actual fuel-moderator interface. These +small regions are rarely visited by rays, which can result in inaccurate +estimates of the source within those small regions and, thereby, numerical +instability. However, OpenMC utilizes several techniques to detect these small +source regions and mitigate instabilities that are associated with them. In +conclusion, mesh overlay is a great way to subdivide any geometry into smaller +source regions. It can be used while retaining stability, though typically at +the cost of generating more source regions relative to an optimal manual +subdivision. .. _usersguide_flux_norm: diff --git a/docs/source/usersguide/variance_reduction.rst b/docs/source/usersguide/variance_reduction.rst index 124f34203..f8ee9c35c 100644 --- a/docs/source/usersguide/variance_reduction.rst +++ b/docs/source/usersguide/variance_reduction.rst @@ -130,9 +130,15 @@ random ray mode can be found in the :ref:`Random Ray User Guide `. .. warning:: If using FW-CADIS weight window generation, ensure that the selected weight - window mesh does not subdivide any cells in the problem. In the future, this - restriction is intended to be relaxed, but for now subdivision of cells by a - mesh tally will result in undefined behavior. + window mesh does not subdivide any source regions in the problem. This can + be ensured by assigning the weight window tally mesh to the root universe so + as to create source region boundaries that conform to the mesh, as in the + example below. + +:: + + root = model.geometry.root_universe + settings.random_ray['source_region_meshes'] = [(ww_mesh, [root])] 6. When running your multigroup random ray input deck, OpenMC will automatically run a forward solve followed by an adjoint solve, with a diff --git a/include/openmc/constants.h b/include/openmc/constants.h index cfcacbabf..2a7ce1990 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -59,6 +59,10 @@ constexpr double RADIAL_MESH_TOL {1e-10}; // Maximum number of random samples per history constexpr int MAX_SAMPLE {100000}; +// Avg. number of hits per batch to be defined as a "small" +// source region in the random ray solver +constexpr double MIN_HITS_PER_BATCH {1.5}; + // ============================================================================ // MATH AND PHYSICAL CONSTANTS diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index 011ff7ccd..39d87d663 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -4,8 +4,10 @@ #include "openmc/constants.h" #include "openmc/openmp_interface.h" #include "openmc/position.h" +#include "openmc/random_ray/parallel_map.h" #include "openmc/random_ray/source_region.h" #include "openmc/source.h" +#include #include namespace openmc { @@ -37,7 +39,6 @@ public: void random_ray_tally(); virtual void accumulate_iteration_flux(); void output_to_vtk() const; - void all_reduce_replicated_source_regions(); void convert_external_sources(); void count_external_source_regions(); void set_adjoint_sources(const vector& forward_flux); @@ -47,12 +48,34 @@ public: void flatten_xs(); void transpose_scattering_matrix(); void serialize_final_fluxes(vector& flux); + void apply_meshes(); + void apply_mesh_to_cell_instances(int32_t i_cell, int32_t mesh_idx, + int target_material_id, const vector& instances, + bool is_target_void); + void apply_mesh_to_cell_and_children(int32_t i_cell, int32_t mesh_idx, + int32_t target_material_id, bool is_target_void); + void prepare_base_source_regions(); + SourceRegionHandle get_subdivided_source_region_handle( + int64_t sr, int mesh_bin, Position r, double dist, Direction u); + void finalize_discovered_source_regions(); + int64_t n_source_regions() const + { + return source_regions_.n_source_regions(); + } + int64_t n_source_elements() const + { + return source_regions_.n_source_regions() * negroups_; + } //---------------------------------------------------------------------------- // Static Data members static bool volume_normalized_flux_tallies_; static bool adjoint_; // If the user wants outputs based on the adjoint flux + // Static variables to store source region meshes and domains + static std::unordered_map>> + mesh_domain_map_; + //---------------------------------------------------------------------------- // Static data members static RandomRayVolumeEstimator volume_estimator_; @@ -61,7 +84,6 @@ public: // Public Data members bool mapped_all_tallies_ {false}; // If all source regions have been visited - int64_t n_source_regions_ {0}; // Total number of source regions in the model int64_t n_external_source_regions_ {0}; // Total number of source regions with // non-zero external source terms @@ -84,6 +106,27 @@ public: // The abstract container holding all source region-specific data SourceRegionContainer source_regions_; + // Base source region container. When source region subdivision via mesh + // is in use, this container holds the original (non-subdivided) material + // filled cell instance source regions. These are useful as they can be + // initialized with external source and mesh domain information ahead of time. + // Then, dynamically discovered source regions can be initialized by cloning + // their base region. + SourceRegionContainer base_source_regions_; + + // Parallel hash map holding all source regions discovered during + // a single iteration. This is a threadsafe data structure that is cleaned + // out after each iteration and stored in the "source_regions_" container. + // It is keyed with a SourceRegionKey, which combines the base source + // region index and the mesh bin. + ParallelMap + discovered_source_regions_; + + // Map that relates a SourceRegionKey to the index at which the source + // region can be found in the "source_regions_" container. + std::unordered_map + source_region_map_; + protected: //---------------------------------------------------------------------------- // Methods @@ -100,9 +143,7 @@ protected: //---------------------------------------------------------------------------- // Private data members - int negroups_; // Number of energy groups in simulation - int64_t n_source_elements_ {0}; // Total number of source regions in the model - // times the number of energy groups + int negroups_; // Number of energy groups in simulation double simulation_volume_; // Total physical volume of the simulation domain, as diff --git a/include/openmc/random_ray/parallel_map.h b/include/openmc/random_ray/parallel_map.h new file mode 100644 index 000000000..7f4f06d99 --- /dev/null +++ b/include/openmc/random_ray/parallel_map.h @@ -0,0 +1,193 @@ +#ifndef OPENMC_RANDOM_RAY_PARALLEL_HASH_MAP_H +#define OPENMC_RANDOM_RAY_PARALLEL_HASH_MAP_H + +#include "openmc/openmp_interface.h" + +#include +#include + +namespace openmc { + +/* + * The ParallelMap class allows for threadsafe access to a map-like data + * structure. It is implemented as a hash table with a fixed number of buckets, + * each of which contains a mutex lock and an unordered_map. The class provides + * a subset of the functionality of std::unordered_map. Users must first lock + * the object (using the key) before accessing or modifying the map. The object + * is locked by bucket, allowing for multiple threads to manipulate different + * keys simultaneously, though sometimes threads will need to wait if keys + * happen to be in the same bucket. The ParallelMap will generate pointers to + * hold values, rather than direct storage of values, so as to allow for + * pointers to values to remain valid even after the lock has been released + * (though locking of those values is then left to the user). Iterators to the + * class are provided but are not threadsafe, and are meant to be used only in a + * serial context (e.g., to dump the contents of the map to another data + * structure). + */ + +template +class ParallelMap { + + //---------------------------------------------------------------------------- + // Helper structs and classes + + struct Bucket { + OpenMPMutex lock_; + std::unordered_map, HashFunctor> map_; + }; + + // The iterator yields a pair: (const KeyType&, ValueType&) + class iterator { + public: + using iterator_category = std::forward_iterator_tag; + using value_type = std::pair; + using difference_type = std::ptrdiff_t; + using pointer = void; // Not providing pointer semantics. + using reference = value_type; + + iterator(std::vector* buckets, std::size_t bucket_index, + typename std::unordered_map, + HashFunctor>::iterator inner_it) + : buckets_(buckets), bucket_index_(bucket_index), inner_it_(inner_it) + { + // Advance to the first valid element if necessary. + advance_to_valid(); + } + + // Dereference returns a pair of (key, value). + reference operator*() const + { + return {inner_it_->first, *inner_it_->second}; + } + + iterator& operator++() + { + ++inner_it_; + advance_to_valid(); + return *this; + } + + iterator operator++(int) + { + iterator tmp = *this; + ++(*this); + return tmp; + } + + bool operator==(const iterator& other) const + { + // Two iterators are equal if they refer to the same bucket vector and are + // both at end, or if they have the same bucket index and inner iterator. + return buckets_ == other.buckets_ && + bucket_index_ == other.bucket_index_ && + (bucket_index_ == buckets_->size() || + inner_it_ == other.inner_it_); + } + + bool operator!=(const iterator& other) const { return !(*this == other); } + + private: + // Helper function: if we are at the end of the current bucket, advance to + // the next non-empty bucket. + void advance_to_valid() + { + while (bucket_index_ < buckets_->size() && + inner_it_ == (*buckets_)[bucket_index_].map_.end()) { + ++bucket_index_; + if (bucket_index_ < buckets_->size()) + inner_it_ = (*buckets_)[bucket_index_].map_.begin(); + } + } + + std::vector* buckets_; + std::size_t bucket_index_; + typename std::unordered_map, + HashFunctor>::iterator inner_it_; + }; + +public: + //---------------------------------------------------------------------------- + // Constructor + ParallelMap(int n_buckets = 1000) : buckets_(n_buckets) {} + + //---------------------------------------------------------------------------- + // Public Methods + void lock(const KeyType& key) + { + Bucket& bucket = get_bucket(key); + bucket.lock_.lock(); + } + + void unlock(const KeyType& key) + { + Bucket& bucket = get_bucket(key); + bucket.lock_.unlock(); + } + + void clear() + { + for (auto& bucket : buckets_) { + bucket.map_.clear(); + } + } + + bool contains(const KeyType& key) + { + Bucket& bucket = get_bucket(key); + // C++20 + // return bucket.map_.contains(key); + return bucket.map_.find(key) != bucket.map_.end(); + } + + ValueType& operator[](const KeyType& key) + { + Bucket& bucket = get_bucket(key); + return *bucket.map_[key].get(); + } + + ValueType* emplace(KeyType key, const ValueType& value) + { + Bucket& bucket = get_bucket(key); + // Attempt to emplace the new element into the unordered_map within the + auto result = bucket.map_.emplace(key, std::make_unique(value)); + auto it = result.first; + return it->second.get(); + } + + // Return iterator to first element. + iterator begin() + { + std::size_t bucket_index = 0; + auto inner_it = buckets_.empty() + ? typename std::unordered_map, HashFunctor>::iterator() + : buckets_[0].map_.begin(); + return iterator(&buckets_, bucket_index, inner_it); + } + + // Return iterator to one-past-last element. + iterator end() + { + // End is signaled by bucket_index_ equal to buckets_.size() + return iterator(&buckets_, buckets_.size(), + typename std::unordered_map, + HashFunctor>::iterator()); + } + +private: + //---------------------------------------------------------------------------- + // Private Methods + Bucket& get_bucket(const KeyType& key) + { + return buckets_[hash(key) % buckets_.size()]; + } + + //---------------------------------------------------------------------------- + // Private Data Fields + HashFunctor hash; + vector buckets_; +}; + +} // namespace openmc + +#endif // OPENMC_RANDOM_RAY_PARALLEL_HASH_MAP_H diff --git a/include/openmc/random_ray/random_ray.h b/include/openmc/random_ray/random_ray.h index df1ad70bc..abf2a2688 100644 --- a/include/openmc/random_ray/random_ray.h +++ b/include/openmc/random_ray/random_ray.h @@ -25,11 +25,17 @@ public: //---------------------------------------------------------------------------- // Methods void event_advance_ray(); - void attenuate_flux(double distance, bool is_active); - void attenuate_flux_flat_source(double distance, bool is_active); - void attenuate_flux_flat_source_void(double distance, bool is_active); - void attenuate_flux_linear_source(double distance, bool is_active); - void attenuate_flux_linear_source_void(double distance, bool is_active); + void attenuate_flux(double distance, bool is_active, double offset = 0.0); + void attenuate_flux_inner( + double distance, bool is_active, int64_t sr, int mesh_bin, Position r); + void attenuate_flux_flat_source( + SourceRegionHandle& srh, double distance, bool is_active, Position r); + void attenuate_flux_flat_source_void( + SourceRegionHandle& srh, double distance, bool is_active, Position r); + void attenuate_flux_linear_source( + SourceRegionHandle& srh, double distance, bool is_active, Position r); + void attenuate_flux_linear_source_void( + SourceRegionHandle& srh, double distance, bool is_active, Position r); void initialize_ray(uint64_t ray_id, FlatSourceDomain* domain); uint64_t transport_history_based_single_ray(); @@ -42,6 +48,7 @@ public: static double distance_active_; // Active ray length static unique_ptr ray_source_; // Starting source for ray sampling static RandomRaySourceShape source_shape_; // Flag for linear source + static bool mesh_subdivision_enabled_; // Flag for mesh subdivision static RandomRaySampleMethod sample_method_; // Flag for sampling method //---------------------------------------------------------------------------- @@ -55,6 +62,8 @@ private: // Private data members vector delta_psi_; vector delta_moments_; + vector mesh_bins_; + vector mesh_fractional_lengths_; int negroups_; FlatSourceDomain* domain_ {nullptr}; // pointer to domain that has flat source diff --git a/include/openmc/random_ray/random_ray_simulation.h b/include/openmc/random_ray/random_ray_simulation.h index 01f12bffb..b94e7401b 100644 --- a/include/openmc/random_ray/random_ray_simulation.h +++ b/include/openmc/random_ray/random_ray_simulation.h @@ -20,10 +20,13 @@ public: //---------------------------------------------------------------------------- // Methods void compute_segment_correction_factors(); - void prepare_fixed_sources(); - void prepare_fixed_sources_adjoint(vector& forward_flux); + void apply_fixed_sources_and_mesh_domains(); + void prepare_fixed_sources_adjoint(vector& forward_flux, + SourceRegionContainer& forward_source_regions, + SourceRegionContainer& forward_base_source_regions, + std::unordered_map& + forward_source_region_map); void simulate(); - void reduce_simulation_statistics(); void output_simulation_results() const; void instability_check( int64_t n_hits, double k_eff, double& avg_miss_rate) const; @@ -63,6 +66,7 @@ private: void openmc_run_random_ray(); void validate_random_ray_inputs(); +void openmc_reset_random_ray(); } // namespace openmc diff --git a/include/openmc/random_ray/source_region.h b/include/openmc/random_ray/source_region.h index e41d6b824..5c5b31f39 100644 --- a/include/openmc/random_ray/source_region.h +++ b/include/openmc/random_ray/source_region.h @@ -44,7 +44,7 @@ inline void hash_combine(size_t& seed, const size_t v) } //---------------------------------------------------------------------------- -// Helper Structs +// Helper Structs and Classes // A mapping object that is used to map between a specific random ray // source region and an OpenMC native tally bin that it should score to @@ -81,11 +81,234 @@ struct TallyTask { }; }; +// The SourceRegionKey combines a base source region (i.e., a material +// filled cell instance) with a mesh bin. This key is used as a handle +// for dynamically discovered source regions when subdividing source +// regions with meshes. +class SourceRegionKey { +public: + int64_t base_source_region_id; + int64_t mesh_bin; + SourceRegionKey() = default; + SourceRegionKey(int64_t source_region, int64_t bin) + : base_source_region_id(source_region), mesh_bin(bin) + {} + + // Equality operator required by the unordered_map + bool operator==(const SourceRegionKey& other) const + { + return base_source_region_id == other.base_source_region_id && + mesh_bin == other.mesh_bin; + } + + // Less than operator required by std::sort + bool operator<(const SourceRegionKey& other) const + { + if (base_source_region_id < other.base_source_region_id) { + return true; + } else if (base_source_region_id > other.base_source_region_id) { + return false; + } else { + return mesh_bin < other.mesh_bin; + } + } + + // Hashing functor required by the unordered_map + struct HashFunctor { + size_t operator()(const SourceRegionKey& key) const + { + size_t seed = 0; + hash_combine(seed, key.base_source_region_id); + hash_combine(seed, key.mesh_bin); + return seed; + } + }; +}; + +// Forward declaration of SourceRegion +class SourceRegion; + +class SourceRegionHandle { +public: + //---------------------------------------------------------------------------- + // Constructors + SourceRegionHandle(SourceRegion& sr); + SourceRegionHandle() = default; + + // All fields are commented/described in the SourceRegion class definition + // below + + //---------------------------------------------------------------------------- + // Public Data members + int negroups_; + bool is_numerical_fp_artifact_ {false}; + bool is_linear_ {false}; + + // Scalar fields + int* material_; + int* is_small_; + int* n_hits_; + int* birthday_; + OpenMPMutex* lock_; + double* volume_; + double* volume_t_; + double* volume_sq_; + double* volume_sq_t_; + double* volume_naive_; + int* position_recorded_; + int* external_source_present_; + Position* position_; + Position* centroid_; + Position* centroid_iteration_; + Position* centroid_t_; + MomentMatrix* mom_matrix_; + MomentMatrix* mom_matrix_t_; + // A set of volume tally tasks. This more complicated data structure is + // convenient for ensuring that volumes are only tallied once per source + // region, regardless of how many energy groups are used for tallying. + std::unordered_set* volume_task_; + + // Mesh that subdivides this source region + int* mesh_; + int64_t* parent_sr_; + + // Energy group-wise 1D arrays + double* scalar_flux_old_; + double* scalar_flux_new_; + float* source_; + float* external_source_; + double* scalar_flux_final_; + + MomentArray* source_gradients_; + MomentArray* flux_moments_old_; + MomentArray* flux_moments_new_; + MomentArray* flux_moments_t_; + + // 2D array representing values for all energy groups x tally + // tasks. Each group may have a different number of tally tasks + // associated with it, necessitating the use of a jagged array. + vector* tally_task_; + + //---------------------------------------------------------------------------- + // Public Accessors + + int& material() { return *material_; } + const int material() const { return *material_; } + + int& is_small() { return *is_small_; } + const int is_small() const { return *is_small_; } + + int& n_hits() { return *n_hits_; } + const int n_hits() const { return *n_hits_; } + + void lock() { lock_->lock(); } + void unlock() { lock_->unlock(); } + + double& volume() { return *volume_; } + const double volume() const { return *volume_; } + + double& volume_t() { return *volume_t_; } + const double volume_t() const { return *volume_t_; } + + double& volume_sq() { return *volume_sq_; } + const double volume_sq() const { return *volume_sq_; } + + double& volume_sq_t() { return *volume_sq_t_; } + const double volume_sq_t() const { return *volume_sq_t_; } + + double& volume_naive() { return *volume_naive_; } + const double volume_naive() const { return *volume_naive_; } + + int& position_recorded() { return *position_recorded_; } + const int position_recorded() const { return *position_recorded_; } + + int& external_source_present() { return *external_source_present_; } + const int external_source_present() const + { + return *external_source_present_; + } + + Position& position() { return *position_; } + const Position position() const { return *position_; } + + Position& centroid() { return *centroid_; } + const Position centroid() const { return *centroid_; } + + Position& centroid_iteration() { return *centroid_iteration_; } + const Position centroid_iteration() const { return *centroid_iteration_; } + + Position& centroid_t() { return *centroid_t_; } + const Position centroid_t() const { return *centroid_t_; } + + MomentMatrix& mom_matrix() { return *mom_matrix_; } + const MomentMatrix mom_matrix() const { return *mom_matrix_; } + + MomentMatrix& mom_matrix_t() { return *mom_matrix_t_; } + const MomentMatrix mom_matrix_t() const { return *mom_matrix_t_; } + + std::unordered_set& volume_task() + { + return *volume_task_; + } + const std::unordered_set& volume_task() + const + { + return *volume_task_; + } + + int& mesh() { return *mesh_; } + const int mesh() const { return *mesh_; } + + int64_t& parent_sr() { return *parent_sr_; } + const int64_t parent_sr() const { return *parent_sr_; } + + double& scalar_flux_old(int g) { return scalar_flux_old_[g]; } + const double scalar_flux_old(int g) const { return scalar_flux_old_[g]; } + + double& scalar_flux_new(int g) { return scalar_flux_new_[g]; } + const double scalar_flux_new(int g) const { return scalar_flux_new_[g]; } + + double& scalar_flux_final(int g) { return scalar_flux_final_[g]; } + const double scalar_flux_final(int g) const { return scalar_flux_final_[g]; } + + float& source(int g) { return source_[g]; } + const float source(int g) const { return source_[g]; } + + float& external_source(int g) { return external_source_[g]; } + const float external_source(int g) const { return external_source_[g]; } + + MomentArray& source_gradients(int g) { return source_gradients_[g]; } + const MomentArray source_gradients(int g) const + { + return source_gradients_[g]; + } + + MomentArray& flux_moments_old(int g) { return flux_moments_old_[g]; } + const MomentArray flux_moments_old(int g) const + { + return flux_moments_old_[g]; + } + + MomentArray& flux_moments_new(int g) { return flux_moments_new_[g]; } + const MomentArray flux_moments_new(int g) const + { + return flux_moments_new_[g]; + } + + MomentArray& flux_moments_t(int g) { return flux_moments_t_[g]; } + const MomentArray flux_moments_t(int g) const { return flux_moments_t_[g]; } + + vector& tally_task(int g) { return tally_task_[g]; } + const vector& tally_task(int g) const { return tally_task_[g]; } + +}; // class SourceRegionHandle + class SourceRegion { public: //---------------------------------------------------------------------------- // Constructors SourceRegion(int negroups, bool is_linear); + SourceRegion(const SourceRegionHandle& handle, int64_t parent_sr); SourceRegion() = default; //---------------------------------------------------------------------------- @@ -104,7 +327,13 @@ public: double volume_naive_ {0.0}; //!< Volume as integrated from this iteration only int position_recorded_ {0}; //!< Has the position been recorded yet? int external_source_present_ { - 0}; //!< Is an external source present in this region? + 0}; //!< Is an external source present in this region? + int is_small_ {0}; //!< Is it "small", receiving < 1.5 hits per iteration? + int n_hits_ {0}; //!< Number of total hits (ray crossings) + // Mesh that subdivides this source region + int mesh_ {C_NONE}; //!< Index in openmc::model::meshes array that subdivides + //!< this source region + int64_t parent_sr_ {C_NONE}; //!< Index of a parent source region Position position_ { 0.0, 0.0, 0.0}; //!< A position somewhere inside the region Position centroid_ {0.0, 0.0, 0.0}; //!< The centroid @@ -150,7 +379,6 @@ public: // tasks. Each group may have a different number of tally tasks // associated with it, necessitating the use of a jagged array. vector> tally_task_; - }; // class SourceRegion class SourceRegionContainer { @@ -165,28 +393,34 @@ public: //---------------------------------------------------------------------------- // Public Accessors int& material(int64_t sr) { return material_[sr]; } - const int& material(int64_t sr) const { return material_[sr]; } + const int material(int64_t sr) const { return material_[sr]; } + + int& is_small(int64_t sr) { return is_small_[sr]; } + const int is_small(int64_t sr) const { return is_small_[sr]; } + + int& n_hits(int64_t sr) { return n_hits_[sr]; } + const int n_hits(int64_t sr) const { return n_hits_[sr]; } OpenMPMutex& lock(int64_t sr) { return lock_[sr]; } const OpenMPMutex& lock(int64_t sr) const { return lock_[sr]; } double& volume(int64_t sr) { return volume_[sr]; } - const double& volume(int64_t sr) const { return volume_[sr]; } + const double volume(int64_t sr) const { return volume_[sr]; } double& volume_t(int64_t sr) { return volume_t_[sr]; } - const double& volume_t(int64_t sr) const { return volume_t_[sr]; } + const double volume_t(int64_t sr) const { return volume_t_[sr]; } double& volume_sq(int64_t sr) { return volume_sq_[sr]; } - const double& volume_sq(int64_t sr) const { return volume_sq_[sr]; } + const double volume_sq(int64_t sr) const { return volume_sq_[sr]; } double& volume_sq_t(int64_t sr) { return volume_sq_t_[sr]; } - const double& volume_sq_t(int64_t sr) const { return volume_sq_t_[sr]; } + const double volume_sq_t(int64_t sr) const { return volume_sq_t_[sr]; } double& volume_naive(int64_t sr) { return volume_naive_[sr]; } - const double& volume_naive(int64_t sr) const { return volume_naive_[sr]; } + const double volume_naive(int64_t sr) const { return volume_naive_[sr]; } int& position_recorded(int64_t sr) { return position_recorded_[sr]; } - const int& position_recorded(int64_t sr) const + const int position_recorded(int64_t sr) const { return position_recorded_[sr]; } @@ -195,31 +429,31 @@ public: { return external_source_present_[sr]; } - const int& external_source_present(int64_t sr) const + const int external_source_present(int64_t sr) const { return external_source_present_[sr]; } Position& position(int64_t sr) { return position_[sr]; } - const Position& position(int64_t sr) const { return position_[sr]; } + const Position position(int64_t sr) const { return position_[sr]; } Position& centroid(int64_t sr) { return centroid_[sr]; } - const Position& centroid(int64_t sr) const { return centroid_[sr]; } + const Position centroid(int64_t sr) const { return centroid_[sr]; } Position& centroid_iteration(int64_t sr) { return centroid_iteration_[sr]; } - const Position& centroid_iteration(int64_t sr) const + const Position centroid_iteration(int64_t sr) const { return centroid_iteration_[sr]; } Position& centroid_t(int64_t sr) { return centroid_t_[sr]; } - const Position& centroid_t(int64_t sr) const { return centroid_t_[sr]; } + const Position centroid_t(int64_t sr) const { return centroid_t_[sr]; } MomentMatrix& mom_matrix(int64_t sr) { return mom_matrix_[sr]; } - const MomentMatrix& mom_matrix(int64_t sr) const { return mom_matrix_[sr]; } + const MomentMatrix mom_matrix(int64_t sr) const { return mom_matrix_[sr]; } MomentMatrix& mom_matrix_t(int64_t sr) { return mom_matrix_t_[sr]; } - const MomentMatrix& mom_matrix_t(int64_t sr) const + const MomentMatrix mom_matrix_t(int64_t sr) const { return mom_matrix_t_[sr]; } @@ -228,12 +462,12 @@ public: { return source_gradients_[index(sr, g)]; } - const MomentArray& source_gradients(int64_t sr, int g) const + const MomentArray source_gradients(int64_t sr, int g) const { return source_gradients_[index(sr, g)]; } MomentArray& source_gradients(int64_t se) { return source_gradients_[se]; } - const MomentArray& source_gradients(int64_t se) const + const MomentArray source_gradients(int64_t se) const { return source_gradients_[se]; } @@ -242,12 +476,12 @@ public: { return flux_moments_old_[index(sr, g)]; } - const MomentArray& flux_moments_old(int64_t sr, int g) const + const MomentArray flux_moments_old(int64_t sr, int g) const { return flux_moments_old_[index(sr, g)]; } MomentArray& flux_moments_old(int64_t se) { return flux_moments_old_[se]; } - const MomentArray& flux_moments_old(int64_t se) const + const MomentArray flux_moments_old(int64_t se) const { return flux_moments_old_[se]; } @@ -256,12 +490,12 @@ public: { return flux_moments_new_[index(sr, g)]; } - const MomentArray& flux_moments_new(int64_t sr, int g) const + const MomentArray flux_moments_new(int64_t sr, int g) const { return flux_moments_new_[index(sr, g)]; } MomentArray& flux_moments_new(int64_t se) { return flux_moments_new_[se]; } - const MomentArray& flux_moments_new(int64_t se) const + const MomentArray flux_moments_new(int64_t se) const { return flux_moments_new_[se]; } @@ -270,12 +504,12 @@ public: { return flux_moments_t_[index(sr, g)]; } - const MomentArray& flux_moments_t(int64_t sr, int g) const + const MomentArray flux_moments_t(int64_t sr, int g) const { return flux_moments_t_[index(sr, g)]; } MomentArray& flux_moments_t(int64_t se) { return flux_moments_t_[se]; } - const MomentArray& flux_moments_t(int64_t se) const + const MomentArray flux_moments_t(int64_t se) const { return flux_moments_t_[se]; } @@ -284,12 +518,12 @@ public: { return scalar_flux_old_[index(sr, g)]; } - const double& scalar_flux_old(int64_t sr, int g) const + const double scalar_flux_old(int64_t sr, int g) const { return scalar_flux_old_[index(sr, g)]; } double& scalar_flux_old(int64_t se) { return scalar_flux_old_[se]; } - const double& scalar_flux_old(int64_t se) const + const double scalar_flux_old(int64_t se) const { return scalar_flux_old_[se]; } @@ -298,12 +532,12 @@ public: { return scalar_flux_new_[index(sr, g)]; } - const double& scalar_flux_new(int64_t sr, int g) const + const double scalar_flux_new(int64_t sr, int g) const { return scalar_flux_new_[index(sr, g)]; } double& scalar_flux_new(int64_t se) { return scalar_flux_new_[se]; } - const double& scalar_flux_new(int64_t se) const + const double scalar_flux_new(int64_t se) const { return scalar_flux_new_[se]; } @@ -312,34 +546,31 @@ public: { return scalar_flux_final_[index(sr, g)]; } - const double& scalar_flux_final(int64_t sr, int g) const + const double scalar_flux_final(int64_t sr, int g) const { return scalar_flux_final_[index(sr, g)]; } double& scalar_flux_final(int64_t se) { return scalar_flux_final_[se]; } - const double& scalar_flux_final(int64_t se) const + const double scalar_flux_final(int64_t se) const { return scalar_flux_final_[se]; } float& source(int64_t sr, int g) { return source_[index(sr, g)]; } - const float& source(int64_t sr, int g) const { return source_[index(sr, g)]; } + const float source(int64_t sr, int g) const { return source_[index(sr, g)]; } float& source(int64_t se) { return source_[se]; } - const float& source(int64_t se) const { return source_[se]; } + const float source(int64_t se) const { return source_[se]; } float& external_source(int64_t sr, int g) { return external_source_[index(sr, g)]; } - const float& external_source(int64_t sr, int g) const + const float external_source(int64_t sr, int g) const { return external_source_[index(sr, g)]; } float& external_source(int64_t se) { return external_source_[se]; } - const float& external_source(int64_t se) const - { - return external_source_[se]; - } + const float external_source(int64_t se) const { return external_source_[se]; } vector& tally_task(int64_t sr, int g) { @@ -365,13 +596,26 @@ public: return volume_task_[sr]; } + int& mesh(int64_t sr) { return mesh_[sr]; } + const int mesh(int64_t sr) const { return mesh_[sr]; } + + int64_t& parent_sr(int64_t sr) { return parent_sr_[sr]; } + const int64_t parent_sr(int64_t sr) const { return parent_sr_[sr]; } + //---------------------------------------------------------------------------- // Public Methods void push_back(const SourceRegion& sr); void assign(int n_source_regions, const SourceRegion& source_region); void flux_swap(); - void mpi_sync_ranks(bool reduce_position); + int64_t n_source_regions() const { return n_source_regions_; } + int64_t n_source_elements() const { return n_source_regions_ * negroups_; } + int& negroups() { return negroups_; } + const int negroups() const { return negroups_; } + bool& is_linear() { return is_linear_; } + const bool is_linear() const { return is_linear_; } + SourceRegionHandle get_source_region_handle(int64_t sr); + void adjoint_reset(); private: //---------------------------------------------------------------------------- @@ -382,6 +626,10 @@ private: // SoA storage for scalar fields (one item per source region) vector material_; + vector is_small_; + vector n_hits_; + vector mesh_; + vector parent_sr_; vector lock_; vector volume_; vector volume_t_; diff --git a/openmc/settings.py b/openmc/settings.py index 2f054ebd1..ead2c0d00 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -7,11 +7,12 @@ from pathlib import Path import lxml.etree as ET +import openmc import openmc.checkvalue as cv from openmc.checkvalue import PathLike from openmc.stats.multivariate import MeshSpatial from ._xml import clean_indentation, get_text, reorder_attributes -from .mesh import _read_meshes, RegularMesh +from .mesh import _read_meshes, RegularMesh, MeshBase from .source import SourceBase, MeshSource, IndependentSource from .utility_funcs import input_path from .volume import VolumeCalculation @@ -180,6 +181,12 @@ class Settings: :sample_method: Sampling method for the ray starting location and direction of travel. Options are `prng` (default) or 'halton`. + :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, + or openmc.Universe. The mesh will be applied to the listed domains + to subdivide source regions so as to improve accuracy and/or conform + with tally meshes. .. versionadded:: 0.15.0 resonance_scattering : dict @@ -1156,6 +1163,18 @@ class Settings: cv.check_type('volume normalized flux tallies', value, bool) elif key == 'adjoint': cv.check_type('adjoint', value, bool) + elif key == 'source_region_meshes': + cv.check_type('source region meshes', value, Iterable) + for mesh, domains in value: + cv.check_type('mesh', mesh, MeshBase) + cv.check_type('domains', domains, Iterable) + valid_types = (openmc.Material, openmc.Cell, openmc.Universe) + for domain in domains: + if not isinstance(domain, valid_types): + raise ValueError( + f'Invalid domain type: {type(domain)}. Expected ' + 'openmc.Material, openmc.Cell, or openmc.Universe.') + cv.check_type('adjoint', value, bool) elif key == 'sample_method': cv.check_value('sample method', value, ('prng', 'halton')) @@ -1573,7 +1592,7 @@ class Settings: root.append(wwg.mesh.to_xml_element()) if mesh_memo is not None: - mesh_memo.add(wwg.mesh) + mesh_memo.add(wwg.mesh.id) def _create_weight_windows_file_element(self, root): if self.weight_windows_file is not None: @@ -1604,13 +1623,25 @@ class Settings: elem = ET.SubElement(root, "max_tracks") elem.text = str(self._max_tracks) - def _create_random_ray_subelement(self, root): + def _create_random_ray_subelement(self, root, mesh_memo=None): if self._random_ray: element = ET.SubElement(root, "random_ray") for key, value in self._random_ray.items(): if key == 'ray_source' and isinstance(value, SourceBase): source_element = value.to_xml_element() element.append(source_element) + elif key == 'source_region_meshes': + subelement = ET.SubElement(element, 'source_region_meshes') + for mesh, domains in value: + mesh_elem = ET.SubElement(subelement, 'mesh') + mesh_elem.set('id', str(mesh.id)) + for domain in domains: + domain_elem = ET.SubElement(mesh_elem, 'domain') + domain_elem.set('id', str(domain.id)) + domain_elem.set('type', domain.__class__.__name__.lower()) + if mesh_memo is not None and mesh.id not in mesh_memo: + root.append(mesh.to_xml_element()) + mesh_memo.add(mesh.id) else: subelement = ET.SubElement(element, key) subelement.text = str(value) @@ -2007,6 +2038,22 @@ class Settings: ) elif child.tag == 'sample_method': self.random_ray['sample_method'] = child.text + elif child.tag == 'source_region_meshes': + self.random_ray['source_region_meshes'] = [] + for mesh_elem in child.findall('mesh'): + mesh = MeshBase.from_xml_element(mesh_elem) + domains = [] + for domain_elem in mesh_elem.findall('domain'): + domain_id = int(domain_elem.get('id')) + domain_type = domain_elem.get('type') + if domain_type == 'material': + domain = openmc.Material(domain_id) + elif domain_type == 'cell': + domain = openmc.Cell(domain_id) + elif domain_type == 'universe': + domain = openmc.Universe(domain_id) + domains.append(domain) + self.random_ray['source_region_meshes'].append((mesh, domains)) def _use_decay_photons_from_xml_element(self, root): text = get_text(root, 'use_decay_photons') @@ -2077,7 +2124,7 @@ class Settings: self._create_weight_window_checkpoints_subelement(element) self._create_max_history_splits_subelement(element) self._create_max_tracks_subelement(element) - self._create_random_ray_subelement(element) + self._create_random_ray_subelement(element, mesh_memo) self._create_use_decay_photons_subelement(element) # Clean the indentation in the file to be user-readable diff --git a/src/finalize.cpp b/src/finalize.cpp index ad6f0cf62..ed3d0e7f4 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -17,6 +17,7 @@ #include "openmc/photon.h" #include "openmc/plot.h" #include "openmc/random_lcg.h" +#include "openmc/random_ray/random_ray_simulation.h" #include "openmc/settings.h" #include "openmc/simulation.h" #include "openmc/source.h" @@ -174,6 +175,8 @@ int openmc_finalize() MPI_Type_free(&mpi::source_site); #endif + openmc_reset_random_ray(); + return 0; } diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index ae0ecb824..33651574f 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -1,6 +1,7 @@ #include "openmc/random_ray/flat_source_domain.h" #include "openmc/cell.h" +#include "openmc/constants.h" #include "openmc/eigenvalue.h" #include "openmc/geometry.h" #include "openmc/material.h" @@ -14,6 +15,7 @@ #include "openmc/tallies/tally.h" #include "openmc/tallies/tally_scoring.h" #include "openmc/timer.h" +#include "openmc/weight_windows.h" #include @@ -28,6 +30,8 @@ RandomRayVolumeEstimator FlatSourceDomain::volume_estimator_ { RandomRayVolumeEstimator::HYBRID}; bool FlatSourceDomain::volume_normalized_flux_tallies_ {false}; bool FlatSourceDomain::adjoint_ {false}; +std::unordered_map>> + FlatSourceDomain::mesh_domain_map_; FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) { @@ -35,20 +39,21 @@ FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) // indices, and store the material type The reason for the offsets is that // some cell types may not have material fills, and therefore do not // produce FSRs. Thus, we cannot index into the global arrays directly + int base_source_regions = 0; for (const auto& c : model::cells) { if (c->type_ != Fill::MATERIAL) { source_region_offsets_.push_back(-1); } else { - source_region_offsets_.push_back(n_source_regions_); - n_source_regions_ += c->n_instances_; - n_source_elements_ += c->n_instances_ * negroups_; + source_region_offsets_.push_back(base_source_regions); + base_source_regions += c->n_instances_; } } - // Initialize cell-wise arrays + // Initialize source regions. bool is_linear = RandomRay::source_shape_ != RandomRaySourceShape::FLAT; source_regions_ = SourceRegionContainer(negroups_, is_linear); - source_regions_.assign(n_source_regions_, SourceRegion(negroups_, is_linear)); + source_regions_.assign( + base_source_regions, SourceRegion(negroups_, is_linear)); // Initialize materials int64_t source_region_id = 0; @@ -62,7 +67,7 @@ FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) } // Sanity check - if (source_region_id != n_source_regions_) { + if (source_region_id != base_source_regions) { fatal_error("Unexpected number of source regions"); } @@ -92,12 +97,13 @@ void FlatSourceDomain::batch_reset() { // Reset scalar fluxes and iteration volume tallies to zero #pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions(); sr++) { source_regions_.volume(sr) = 0.0; source_regions_.volume_sq(sr) = 0.0; } + #pragma omp parallel for - for (int64_t se = 0; se < n_source_elements_; se++) { + for (int64_t se = 0; se < n_source_elements(); se++) { source_regions_.scalar_flux_new(se) = 0.0; } } @@ -105,7 +111,7 @@ void FlatSourceDomain::batch_reset() void FlatSourceDomain::accumulate_iteration_flux() { #pragma omp parallel for - for (int64_t se = 0; se < n_source_elements_; se++) { + for (int64_t se = 0; se < n_source_elements(); se++) { source_regions_.scalar_flux_final(se) += source_regions_.scalar_flux_new(se); } @@ -121,13 +127,13 @@ void FlatSourceDomain::update_neutron_source(double k_eff) // Reset all source regions to zero (important for void regions) #pragma omp parallel for - for (int64_t se = 0; se < n_source_elements_; se++) { + for (int64_t se = 0; se < n_source_elements(); se++) { source_regions_.source(se) = 0.0; } // Add scattering + fission source #pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions(); sr++) { int material = source_regions_.material(sr); if (material == MATERIAL_VOID) { continue; @@ -155,7 +161,7 @@ void FlatSourceDomain::update_neutron_source(double k_eff) // Add external source if in fixed source mode if (settings::run_mode == RunMode::FIXED_SOURCE) { #pragma omp parallel for - for (int64_t se = 0; se < n_source_elements_; se++) { + for (int64_t se = 0; se < n_source_elements(); se++) { source_regions_.source(se) += source_regions_.external_source(se); } } @@ -174,14 +180,14 @@ void FlatSourceDomain::normalize_scalar_flux_and_volumes( // Normalize scalar flux to total distance travelled by all rays this // iteration #pragma omp parallel for - for (int64_t se = 0; se < n_source_elements_; se++) { + for (int64_t se = 0; se < n_source_elements(); se++) { source_regions_.scalar_flux_new(se) *= normalization_factor; } // Accumulate cell-wise ray length tallies collected this iteration, then // update the simulation-averaged cell-wise volume estimates #pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions(); sr++) { source_regions_.volume_t(sr) += source_regions_.volume(sr); source_regions_.volume_sq_t(sr) += source_regions_.volume_sq(sr); source_regions_.volume_naive(sr) = @@ -225,9 +231,10 @@ void FlatSourceDomain::set_flux_to_source(int64_t sr, int g) int64_t FlatSourceDomain::add_source_to_scalar_flux() { int64_t n_hits = 0; + double inverse_batch = 1.0 / simulation::current_batch; #pragma omp parallel for reduction(+ : n_hits) - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions(); sr++) { double volume_simulation_avg = source_regions_.volume(sr); double volume_iteration = source_regions_.volume_naive(sr); @@ -237,6 +244,14 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() n_hits++; } + // Set the SR to small status if its expected number of hits + // per iteration is less than 1.5 + if (source_regions_.n_hits(sr) * inverse_batch < MIN_HITS_PER_BATCH) { + source_regions_.is_small(sr) = 1; + } else { + source_regions_.is_small(sr) = 0; + } + // The volume treatment depends on the volume estimator type // and whether or not an external source is present in the cell. double volume; @@ -248,7 +263,8 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() volume = volume_simulation_avg; break; case RandomRayVolumeEstimator::HYBRID: - if (source_regions_.external_source_present(sr)) { + if (source_regions_.external_source_present(sr) || + source_regions_.is_small(sr)) { volume = volume_iteration; } else { volume = volume_simulation_avg; @@ -279,16 +295,12 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() // in the cell we will use the previous iteration's flux estimate. This // injects a small degree of correlation into the simulation, but this // is going to be trivial when the miss rate is a few percent or less. - if (source_regions_.external_source_present(sr)) { + if (source_regions_.external_source_present(sr) && !adjoint_) { set_flux_to_old_flux(sr, g); } else { set_flux_to_source(sr, g); } } - // If the FSR was not hit this iteration, and it has never been hit in - // any iteration (i.e., volume is zero), then we want to set this to 0 - // to avoid dividing anything by a zero volume. This happens implicitly - // given that the new scalar flux arrays are set to zero each iteration. } } @@ -304,10 +316,10 @@ double FlatSourceDomain::compute_k_eff(double k_eff_old) const double fission_rate_new = 0; // Vector for gathering fission source terms for Shannon entropy calculation - vector p(n_source_regions_, 0.0f); + vector p(n_source_regions(), 0.0f); #pragma omp parallel for reduction(+ : fission_rate_old, fission_rate_new) - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions(); sr++) { // If simulation averaged volume is zero, don't include this cell double volume = source_regions_.volume(sr); @@ -350,7 +362,7 @@ double FlatSourceDomain::compute_k_eff(double k_eff_old) const double inverse_sum = 1 / fission_rate_new; #pragma omp parallel for reduction(+ : H) - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions(); sr++) { // Only if FSR has non-negative and non-zero fission source if (p[sr] > 0.0f) { // Normalize to total weight of bank sites. p_i for better performance @@ -408,7 +420,7 @@ void FlatSourceDomain::convert_source_regions_to_tallies() // Attempt to generate mapping for all source regions #pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions(); sr++) { // If this source region has not been hit by a ray yet, then // we aren't going to be able to map it, so skip it. @@ -423,6 +435,7 @@ void FlatSourceDomain::convert_source_regions_to_tallies() Particle p; p.r() = source_regions_.position(sr); p.r_last() = source_regions_.position(sr); + p.u() = {1.0, 0.0, 0.0}; bool found = exhaustive_find_cell(p); // Loop over energy groups (so as to support energy filters) @@ -517,7 +530,7 @@ double FlatSourceDomain::compute_fixed_source_normalization_factor() const // total external source strength in the simulation. double simulation_external_source_strength = 0.0; #pragma omp parallel for reduction(+ : simulation_external_source_strength) - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions(); sr++) { int material = source_regions_.material(sr); double volume = source_regions_.volume(sr) * simulation_volume_; for (int g = 0; g < negroups_; g++) { @@ -570,7 +583,7 @@ void FlatSourceDomain::random_ray_tally() // element, we check if there are any scores needed and apply // them. #pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions(); sr++) { // The fsr.volume_ is the unitless fractional simulation averaged volume // (i.e., it is the FSR's fraction of the overall simulation volume). The // simulation_volume_ is the total 3D physical volume in cm^3 of the @@ -673,30 +686,6 @@ void FlatSourceDomain::random_ray_tally() openmc::simulation::time_tallies.stop(); } -void FlatSourceDomain::all_reduce_replicated_source_regions() -{ -#ifdef OPENMC_MPI - // If we only have 1 MPI rank, no need - // to reduce anything. - if (mpi::n_procs <= 1) - return; - - simulation::time_bank_sendrecv.start(); - - // First, we broadcast the fully mapped tally status variable so that - // all ranks are on the same page - int mapped_all_tallies_i = static_cast(mapped_all_tallies_); - MPI_Bcast(&mapped_all_tallies_i, 1, MPI_INT, 0, mpi::intracomm); - - bool reduce_position = - simulation::current_batch > settings::n_inactive && !mapped_all_tallies_i; - - source_regions_.mpi_sync_ranks(reduce_position); - - simulation::time_bank_sendrecv.stop(); -#endif -} - double FlatSourceDomain::evaluate_flux_at_point( Position r, int64_t sr, int g) const { @@ -765,8 +754,9 @@ void FlatSourceDomain::output_to_vtk() const // Relate voxel spatial locations to random ray source regions vector voxel_indices(Nx * Ny * Nz); vector voxel_positions(Nx * Ny * Nz); - -#pragma omp parallel for collapse(3) + vector weight_windows(Nx * Ny * Nz); + float min_weight = 1e20; +#pragma omp parallel for collapse(3) reduction(min : min_weight) for (int z = 0; z < Nz; z++) { for (int y = 0; y < Ny; y++) { for (int x = 0; x < Nx; x++) { @@ -776,12 +766,49 @@ void FlatSourceDomain::output_to_vtk() const sample.x = ll.x + x_delta / 2.0 + x * x_delta; Particle p; p.r() = sample; + p.r_last() = sample; + p.E() = 1.0; + p.E_last() = 1.0; + p.u() = {1.0, 0.0, 0.0}; + bool found = exhaustive_find_cell(p); + if (!found) { + voxel_indices[z * Ny * Nx + y * Nx + x] = -1; + voxel_positions[z * Ny * Nx + y * Nx + x] = sample; + weight_windows[z * Ny * Nx + y * Nx + x] = 0.0; + continue; + } + int i_cell = p.lowest_coord().cell; - int64_t source_region_idx = - source_region_offsets_[i_cell] + p.cell_instance(); - voxel_indices[z * Ny * Nx + y * Nx + x] = source_region_idx; + int64_t sr = source_region_offsets_[i_cell] + p.cell_instance(); + if (RandomRay::mesh_subdivision_enabled_) { + int mesh_idx = base_source_regions_.mesh(sr); + int mesh_bin; + if (mesh_idx == C_NONE) { + mesh_bin = 0; + } else { + mesh_bin = model::meshes[mesh_idx]->get_bin(p.r()); + } + SourceRegionKey sr_key {sr, mesh_bin}; + auto it = source_region_map_.find(sr_key); + if (it != source_region_map_.end()) { + sr = it->second; + } else { + sr = -1; + } + } + + voxel_indices[z * Ny * Nx + y * Nx + x] = sr; voxel_positions[z * Ny * Nx + y * Nx + x] = sample; + + if (variance_reduction::weight_windows.size() == 1) { + WeightWindow ww = + variance_reduction::weight_windows[0]->get_weight_window(p); + float weight = ww.lower_weight; + weight_windows[z * Ny * Nx + y * Nx + x] = weight; + if (weight < min_weight) + min_weight = weight; + } } } } @@ -798,10 +825,14 @@ void FlatSourceDomain::output_to_vtk() const std::fprintf(plot, "BINARY\n"); std::fprintf(plot, "DATASET STRUCTURED_POINTS\n"); std::fprintf(plot, "DIMENSIONS %d %d %d\n", Nx, Ny, Nz); - std::fprintf(plot, "ORIGIN 0 0 0\n"); + std::fprintf(plot, "ORIGIN %lf %lf %lf\n", ll.x, ll.y, ll.z); std::fprintf(plot, "SPACING %lf %lf %lf\n", x_delta, y_delta, z_delta); std::fprintf(plot, "POINT_DATA %d\n", Nx * Ny * Nz); + int64_t num_neg = 0; + int64_t num_samples = 0; + float min_flux = 0.0; + float max_flux = -1.0e20; // Plot multigroup flux data for (int g = 0; g < negroups_; g++) { std::fprintf(plot, "SCALARS flux_group_%d float\n", g); @@ -809,12 +840,36 @@ void FlatSourceDomain::output_to_vtk() const for (int i = 0; i < Nx * Ny * Nz; i++) { int64_t fsr = voxel_indices[i]; int64_t source_element = fsr * negroups_ + g; - float flux = evaluate_flux_at_point(voxel_positions[i], fsr, g); + float flux = 0; + if (fsr >= 0) { + flux = evaluate_flux_at_point(voxel_positions[i], fsr, g); + if (flux < 0.0) + flux = FlatSourceDomain::evaluate_flux_at_point( + voxel_positions[i], fsr, g); + } + if (flux < 0.0) { + num_neg++; + if (flux < min_flux) { + min_flux = flux; + } + } + if (flux > max_flux) + max_flux = flux; + num_samples++; flux = convert_to_big_endian(flux); std::fwrite(&flux, sizeof(float), 1, plot); } } + // Slightly negative fluxes can be normal when sampling corners of linear + // source regions. However, very common and high magnitude negative fluxes + // may indicate numerical instability. + if (num_neg > 0) { + warning(fmt::format("{} plot samples ({:.4f}%) contained negative fluxes " + "(minumum found = {:.2e} maximum_found = {:.2e})", + num_neg, (100.0 * num_neg) / num_samples, min_flux, max_flux)); + } + // Plot FSRs std::fprintf(plot, "SCALARS FSRs float\n"); std::fprintf(plot, "LOOKUP_TABLE default\n"); @@ -828,7 +883,9 @@ void FlatSourceDomain::output_to_vtk() const std::fprintf(plot, "SCALARS Materials int\n"); std::fprintf(plot, "LOOKUP_TABLE default\n"); for (int fsr : voxel_indices) { - int mat = source_regions_.material(fsr); + int mat = -1; + if (fsr >= 0) + mat = source_regions_.material(fsr); mat = convert_to_big_endian(mat); std::fwrite(&mat, sizeof(int), 1, plot); } @@ -839,15 +896,16 @@ void FlatSourceDomain::output_to_vtk() const std::fprintf(plot, "LOOKUP_TABLE default\n"); for (int i = 0; i < Nx * Ny * Nz; i++) { int64_t fsr = voxel_indices[i]; - float total_fission = 0.0; - int mat = source_regions_.material(fsr); - if (mat != MATERIAL_VOID) { - for (int g = 0; g < negroups_; g++) { - int64_t source_element = fsr * negroups_ + g; - float flux = evaluate_flux_at_point(voxel_positions[i], fsr, g); - double sigma_f = sigma_f_[mat * negroups_ + g]; - total_fission += sigma_f * flux; + if (fsr >= 0) { + int mat = source_regions_.material(fsr); + if (mat != MATERIAL_VOID) { + for (int g = 0; g < negroups_; g++) { + int64_t source_element = fsr * negroups_ + g; + float flux = evaluate_flux_at_point(voxel_positions[i], fsr, g); + double sigma_f = sigma_f_[mat * negroups_ + g]; + total_fission += sigma_f * flux; + } } } total_fission = convert_to_big_endian(total_fission); @@ -859,14 +917,29 @@ void FlatSourceDomain::output_to_vtk() const for (int i = 0; i < Nx * Ny * Nz; i++) { int64_t fsr = voxel_indices[i]; float total_external = 0.0f; - for (int g = 0; g < negroups_; g++) { - total_external += source_regions_.external_source(fsr, g); + if (fsr >= 0) { + for (int g = 0; g < negroups_; g++) { + total_external += source_regions_.external_source(fsr, g); + } } total_external = convert_to_big_endian(total_external); std::fwrite(&total_external, sizeof(float), 1, plot); } } + // Plot weight window data + if (variance_reduction::weight_windows.size() == 1) { + std::fprintf(plot, "SCALARS weight_window_lower float\n"); + std::fprintf(plot, "LOOKUP_TABLE default\n"); + for (int i = 0; i < Nx * Ny * Nz; i++) { + float weight = weight_windows[i]; + if (weight == 0.0) + weight = min_weight; + weight = convert_to_big_endian(weight); + std::fwrite(&weight, sizeof(float), 1, plot); + } + } + std::fclose(plot); } } @@ -938,7 +1011,7 @@ void FlatSourceDomain::count_external_source_regions() { n_external_source_regions_ = 0; #pragma omp parallel for reduction(+ : n_external_source_regions_) - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions(); sr++) { if (source_regions_.external_source_present(sr)) { n_external_source_regions_++; } @@ -984,7 +1057,7 @@ void FlatSourceDomain::convert_external_sources() // Divide the fixed source term by sigma t (to save time when applying each // iteration) #pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions(); sr++) { int material = source_regions_.material(sr); if (material == MATERIAL_VOID) { continue; @@ -1056,7 +1129,7 @@ void FlatSourceDomain::set_adjoint_sources(const vector& forward_flux) // small in magnitude, but rather due to the source region being physically // small in volume and thus having a noisy flux estimate. #pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions(); sr++) { for (int g = 0; g < negroups_; g++) { double flux = forward_flux[sr * negroups_ + g]; if (flux <= 0.0) { @@ -1064,13 +1137,16 @@ void FlatSourceDomain::set_adjoint_sources(const vector& forward_flux) } else { source_regions_.external_source(sr, g) = 1.0 / flux; } + if (flux > 0.0) { + source_regions_.external_source_present(sr) = 1; + } } } // Divide the fixed source term by sigma t (to save time when applying each // iteration) #pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions(); sr++) { int material = source_regions_.material(sr); if (material == MATERIAL_VOID) { continue; @@ -1103,12 +1179,252 @@ void FlatSourceDomain::transpose_scattering_matrix() void FlatSourceDomain::serialize_final_fluxes(vector& flux) { // Ensure array is correct size - flux.resize(n_source_regions_ * negroups_); + flux.resize(n_source_regions() * negroups_); // Serialize the final fluxes for output #pragma omp parallel for - for (int64_t se = 0; se < n_source_elements_; se++) { + for (int64_t se = 0; se < n_source_elements(); se++) { flux[se] = source_regions_.scalar_flux_final(se); } } +void FlatSourceDomain::apply_mesh_to_cell_instances(int32_t i_cell, + int32_t mesh_idx, int target_material_id, const vector& instances, + bool is_target_void) +{ + Cell& cell = *model::cells[i_cell]; + if (cell.type_ != Fill::MATERIAL) + return; + for (int32_t j : instances) { + int cell_material_idx = cell.material(j); + int cell_material_id = (cell_material_idx == C_NONE) + ? C_NONE + : model::materials[cell_material_idx]->id(); + + if ((target_material_id == C_NONE && !is_target_void) || + cell_material_id == target_material_id) { + int64_t sr = source_region_offsets_[i_cell] + j; + if (source_regions_.mesh(sr) != C_NONE) { + // print out the source region that is broken: + fatal_error(fmt::format("Source region {} already has mesh idx {} " + "applied, but trying to apply mesh idx {}", + sr, source_regions_.mesh(sr), mesh_idx)); + } + source_regions_.mesh(sr) = mesh_idx; + } + } +} + +void FlatSourceDomain::apply_mesh_to_cell_and_children(int32_t i_cell, + int32_t mesh_idx, int32_t target_material_id, bool is_target_void) +{ + Cell& cell = *model::cells[i_cell]; + + if (cell.type_ == Fill::MATERIAL) { + vector instances(cell.n_instances_); + std::iota(instances.begin(), instances.end(), 0); + apply_mesh_to_cell_instances( + i_cell, mesh_idx, target_material_id, instances, is_target_void); + } else if (target_material_id == C_NONE && !is_target_void) { + for (int j = 0; j < cell.n_instances_; j++) { + std::unordered_map> cell_instance_list = + cell.get_contained_cells(j, nullptr); + for (const auto& pair : cell_instance_list) { + int32_t i_child_cell = pair.first; + apply_mesh_to_cell_instances(i_child_cell, mesh_idx, target_material_id, + pair.second, is_target_void); + } + } + } +} + +void FlatSourceDomain::apply_meshes() +{ + // Skip if there are no mappings between mesh IDs and domains + if (mesh_domain_map_.empty()) + return; + + // Loop over meshes + for (int mesh_idx = 0; mesh_idx < model::meshes.size(); mesh_idx++) { + Mesh* mesh = model::meshes[mesh_idx].get(); + int mesh_id = mesh->id(); + + // Skip if mesh id is not present in the map + if (mesh_domain_map_.find(mesh_id) == mesh_domain_map_.end()) + continue; + + // Loop over domains associated with the mesh + for (auto& domain : mesh_domain_map_[mesh_id]) { + Source::DomainType domain_type = domain.first; + int domain_id = domain.second; + + if (domain_type == Source::DomainType::MATERIAL) { + for (int i_cell = 0; i_cell < model::cells.size(); i_cell++) { + if (domain_id == C_NONE) { + apply_mesh_to_cell_and_children(i_cell, mesh_idx, domain_id, true); + } else { + apply_mesh_to_cell_and_children(i_cell, mesh_idx, domain_id, false); + } + } + } else if (domain_type == Source::DomainType::CELL) { + int32_t i_cell = model::cell_map[domain_id]; + apply_mesh_to_cell_and_children(i_cell, mesh_idx, C_NONE, false); + } else if (domain_type == Source::DomainType::UNIVERSE) { + int32_t i_universe = model::universe_map[domain_id]; + Universe& universe = *model::universes[i_universe]; + for (int32_t i_cell : universe.cells_) { + apply_mesh_to_cell_and_children(i_cell, mesh_idx, C_NONE, false); + } + } + } + } +} + +void FlatSourceDomain::prepare_base_source_regions() +{ + std::swap(source_regions_, base_source_regions_); + source_regions_.negroups() = base_source_regions_.negroups(); + source_regions_.is_linear() = base_source_regions_.is_linear(); +} + +SourceRegionHandle FlatSourceDomain::get_subdivided_source_region_handle( + int64_t sr, int mesh_bin, Position r, double dist, Direction u) +{ + SourceRegionKey sr_key {sr, mesh_bin}; + + // Case 1: Check if the source region key is already present in the permanent + // map. This is the most common condition, as any source region visited in a + // previous power iteration will already be present in the permanent map. If + // the source region key is found, we translate the key into a specific 1D + // source region index and return a handle its position in the + // source_regions_ vector. + auto it = source_region_map_.find(sr_key); + if (it != source_region_map_.end()) { + int64_t sr = it->second; + return source_regions_.get_source_region_handle(sr); + } + + // Case 2: Check if the source region key is present in the temporary (thread + // safe) map. This is a common occurrence in the first power iteration when + // the source region has already been visited already by some other ray. We + // begin by locking the temporary map before any operations are performed. The + // lock is not global over the full data structure -- it will be dependent on + // which key is used. + discovered_source_regions_.lock(sr_key); + + // If the key is found in the temporary map, then we return a handle to the + // source region that is stored in the temporary map. + if (discovered_source_regions_.contains(sr_key)) { + SourceRegionHandle handle {discovered_source_regions_[sr_key]}; + discovered_source_regions_.unlock(sr_key); + return handle; + } + + // Case 3: The source region key is not present anywhere, but it is only due + // to floating point artifacts. These artifacts occur when the overlaid mesh + // overlaps with actual geometry surfaces. In these cases, roundoff error may + // result in the ray tracer detecting an additional (very short) segment + // though a mesh bin that is actually past the physical source region + // boundary. This is a result of the the multi-level ray tracing treatment in + // OpenMC, which depending on the number of universes in the hierarchy etc can + // result in the wrong surface being selected as the nearest. This can happen + // in a lattice when there are two directions that both are very close in + // distance, within the tolerance of FP_REL_PRECISION, and the are thus + // treated as being equivalent so alternative logic is used. However, when we + // go and ray trace on this with the mesh tracer we may go past the surface + // bounding the current source region. + // + // To filter out this case, before we create the new source region, we double + // check that the actual starting point of this segment (r) is still in the + // same geometry source region that we started in. If an artifact is detected, + // we discard the segment (and attenuation through it) as it is not really a + // valid source region and will have only an infinitessimally small cell + // combined with the mesh bin. Thankfully, this is a fairly rare condition, + // and only triggers for very short ray lengths. It can be fixed by decreasing + // the value of FP_REL_PRECISION in constants.h, but this may have unknown + // consequences for the general ray tracer, so for now we do the below sanity + // checks before generating phantom source regions. A significant extra cost + // is incurred in instantiating the GeometryState object and doing a cell + // lookup, but again, this is going to be an extremely rare thing to check + // after the first power iteration has completed. + + // Sanity check on source region id + GeometryState gs; + gs.r() = r + TINY_BIT * u; + gs.u() = {1.0, 0.0, 0.0}; + exhaustive_find_cell(gs); + int gs_i_cell = gs.lowest_coord().cell; + int64_t sr_found = source_region_offsets_[gs_i_cell] + gs.cell_instance(); + if (sr_found != sr) { + discovered_source_regions_.unlock(sr_key); + SourceRegionHandle handle; + handle.is_numerical_fp_artifact_ = true; + return handle; + } + + // Sanity check on mesh bin + int mesh_idx = base_source_regions_.mesh(sr); + if (mesh_idx == C_NONE) { + if (mesh_bin != 0) { + discovered_source_regions_.unlock(sr_key); + SourceRegionHandle handle; + handle.is_numerical_fp_artifact_ = true; + return handle; + } + } else { + Mesh* mesh = model::meshes[mesh_idx].get(); + int bin_found = mesh->get_bin(r + TINY_BIT * u); + if (bin_found != mesh_bin) { + discovered_source_regions_.unlock(sr_key); + SourceRegionHandle handle; + handle.is_numerical_fp_artifact_ = true; + return handle; + } + } + + // Case 4: The source region key is valid, but is not present anywhere. This + // condition only occurs the first time the source region is discovered + // (typically in the first power iteration). In this case, we need to handle + // creation of the new source region and its storage into the parallel map. + // The new source region is created by copying the base source region, so as + // to inherit material, external source, and some flux properties etc. We + // also pass the base source region id to allow the new source region to + // know which base source region it is derived from. + SourceRegion* sr_ptr = discovered_source_regions_.emplace( + sr_key, {base_source_regions_.get_source_region_handle(sr), sr}); + discovered_source_regions_.unlock(sr_key); + SourceRegionHandle handle {*sr_ptr}; + return handle; +} + +void FlatSourceDomain::finalize_discovered_source_regions() +{ + // Extract keys for entries with a valid volume. + vector keys; + for (const auto& pair : discovered_source_regions_) { + if (pair.second.volume_ > 0.0) { + keys.push_back(pair.first); + } + } + + if (!keys.empty()) { + // Sort the keys, so as to ensure reproducible ordering given that source + // regions may have been added to discovered_source_regions_ in an arbitrary + // order due to shared memory threading. + std::sort(keys.begin(), keys.end()); + + // Append the source regions in the sorted key order. + for (const auto& key : keys) { + const SourceRegion& sr = discovered_source_regions_[key]; + source_region_map_[key] = source_regions_.n_source_regions(); + source_regions_.push_back(sr); + } + + // If any new source regions were discovered, we need to update the + // tally mapping between source regions and tally bins. + mapped_all_tallies_ = false; + } + + discovered_source_regions_.clear(); +} + } // namespace openmc diff --git a/src/random_ray/linear_source_domain.cpp b/src/random_ray/linear_source_domain.cpp index 3819c7d70..81412164e 100644 --- a/src/random_ray/linear_source_domain.cpp +++ b/src/random_ray/linear_source_domain.cpp @@ -24,12 +24,12 @@ void LinearSourceDomain::batch_reset() { FlatSourceDomain::batch_reset(); #pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions(); sr++) { source_regions_.centroid_iteration(sr) = {0.0, 0.0, 0.0}; source_regions_.mom_matrix(sr) = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; } #pragma omp parallel for - for (int64_t se = 0; se < n_source_elements_; se++) { + for (int64_t se = 0; se < n_source_elements(); se++) { source_regions_.flux_moments_new(se) = {0.0, 0.0, 0.0}; } } @@ -40,8 +40,14 @@ void LinearSourceDomain::update_neutron_source(double k_eff) double inverse_k_eff = 1.0 / k_eff; +// Reset all source regions to zero (important for void regions) #pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t se = 0; se < n_source_elements(); se++) { + source_regions_.source(se) = 0.0; + } + +#pragma omp parallel for + for (int64_t sr = 0; sr < n_source_regions(); sr++) { int material = source_regions_.material(sr); if (material == MATERIAL_VOID) { continue; @@ -98,7 +104,7 @@ void LinearSourceDomain::update_neutron_source(double k_eff) if (settings::run_mode == RunMode::FIXED_SOURCE) { // Add external source to flat source term if in fixed source mode #pragma omp parallel for - for (int64_t se = 0; se < n_source_elements_; se++) { + for (int64_t se = 0; se < n_source_elements(); se++) { source_regions_.source(se) += source_regions_.external_source(se); } } @@ -115,7 +121,7 @@ void LinearSourceDomain::normalize_scalar_flux_and_volumes( // Normalize flux to total distance travelled by all rays this iteration #pragma omp parallel for - for (int64_t se = 0; se < n_source_elements_; se++) { + for (int64_t se = 0; se < n_source_elements(); se++) { source_regions_.scalar_flux_new(se) *= normalization_factor; source_regions_.flux_moments_new(se) *= normalization_factor; } @@ -123,7 +129,7 @@ void LinearSourceDomain::normalize_scalar_flux_and_volumes( // Accumulate cell-wise ray length tallies collected this iteration, then // update the simulation-averaged cell-wise volume estimates #pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions(); sr++) { source_regions_.centroid_t(sr) += source_regions_.centroid_iteration(sr); source_regions_.mom_matrix_t(sr) += source_regions_.mom_matrix(sr); source_regions_.volume_t(sr) += source_regions_.volume(sr); @@ -154,13 +160,22 @@ void LinearSourceDomain::set_flux_to_flux_plus_source( source_regions_.scalar_flux_new(sr, g) /= volume; source_regions_.scalar_flux_new(sr, g) += source_regions_.source(sr, g); } - source_regions_.flux_moments_new(sr, g) *= (1.0 / volume); + // If a source region is small, then the moments are likely noisy, so we zero + // them. This is reasonable, given that small regions can get by with a flat + // source approximation anyhow. + if (source_regions_.is_small(sr)) { + source_regions_.flux_moments_new(sr, g) = {0.0, 0.0, 0.0}; + } else { + source_regions_.flux_moments_new(sr, g) *= (1.0 / volume); + } } void LinearSourceDomain::set_flux_to_old_flux(int64_t sr, int g) { - source_regions_.scalar_flux_new(g) = source_regions_.scalar_flux_old(g); - source_regions_.flux_moments_new(g) = source_regions_.flux_moments_old(g); + source_regions_.scalar_flux_new(sr, g) = + source_regions_.scalar_flux_old(sr, g); + source_regions_.flux_moments_new(sr, g) = + source_regions_.flux_moments_old(sr, g); } void LinearSourceDomain::accumulate_iteration_flux() @@ -170,7 +185,7 @@ void LinearSourceDomain::accumulate_iteration_flux() // Accumulate scalar flux moments #pragma omp parallel for - for (int64_t se = 0; se < n_source_elements_; se++) { + for (int64_t se = 0; se < n_source_elements(); se++) { source_regions_.flux_moments_t(se) += source_regions_.flux_moments_new(se); } } diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp index f9ebb6f9d..9a9d1394a 100644 --- a/src/random_ray/random_ray.cpp +++ b/src/random_ray/random_ray.cpp @@ -237,6 +237,7 @@ double RandomRay::distance_inactive_; double RandomRay::distance_active_; unique_ptr RandomRay::ray_source_; RandomRaySourceShape RandomRay::source_shape_ {RandomRaySourceShape::FLAT}; +bool RandomRay::mesh_subdivision_enabled_ {false}; RandomRaySampleMethod RandomRay::sample_method_ {RandomRaySampleMethod::PRNG}; RandomRay::RandomRay() @@ -313,7 +314,7 @@ void RandomRay::event_advance_ray() wgt() = 0.0; } - attenuate_flux(distance_alive, true); + attenuate_flux(distance_alive, true, distance_dead); distance_travelled_ = distance_alive; } else { distance_travelled_ += distance; @@ -327,23 +328,84 @@ void RandomRay::event_advance_ray() } } -void RandomRay::attenuate_flux(double distance, bool is_active) +void RandomRay::attenuate_flux(double distance, bool is_active, double offset) { + // Determine source region index etc. + int i_cell = lowest_coord().cell; + + // The base source region is the spatial region index + int64_t sr = domain_->source_region_offsets_[i_cell] + cell_instance(); + + // Perform ray tracing across mesh + if (mesh_subdivision_enabled_) { + // Determine the mesh index for the base source region, if any + int mesh_idx = domain_->base_source_regions_.mesh(sr); + + if (mesh_idx == C_NONE) { + // If there's no mesh being applied to this cell, then + // we just attenuate the flux as normal, and set + // the mesh bin to 0 + attenuate_flux_inner(distance, is_active, sr, 0, r()); + } else { + // If there is a mesh being applied to this cell, then + // we loop over all the bin crossings and attenuate + // separately. + Mesh* mesh = model::meshes[mesh_idx].get(); + + // We adjust the start and end positions of the ray slightly + // to accomodate for floating point precision issues that tend + // to occur at mesh boundaries that overlap with geometry lattice + // boundaries. + Position start = r() + (offset + TINY_BIT) * u(); + Position end = start + (distance - 2.0 * TINY_BIT) * u(); + double reduced_distance = (end - start).norm(); + + // Ray trace through the mesh and record bins and lengths + mesh_bins_.resize(0); + mesh_fractional_lengths_.resize(0); + mesh->bins_crossed(start, end, u(), mesh_bins_, mesh_fractional_lengths_); + + // Loop over all mesh bins and attenuate flux + for (int b = 0; b < mesh_bins_.size(); b++) { + double physical_length = reduced_distance * mesh_fractional_lengths_[b]; + attenuate_flux_inner( + physical_length, is_active, sr, mesh_bins_[b], start); + start += physical_length * u(); + } + } + } else { + attenuate_flux_inner(distance, is_active, sr, C_NONE, r()); + } +} + +void RandomRay::attenuate_flux_inner( + double distance, bool is_active, int64_t sr, int mesh_bin, Position r) +{ + SourceRegionHandle srh; + if (mesh_subdivision_enabled_) { + srh = domain_->get_subdivided_source_region_handle( + sr, mesh_bin, r, distance, u()); + if (srh.is_numerical_fp_artifact_) { + return; + } + } else { + srh = domain_->source_regions_.get_source_region_handle(sr); + } switch (source_shape_) { case RandomRaySourceShape::FLAT: if (this->material() == MATERIAL_VOID) { - attenuate_flux_flat_source_void(distance, is_active); + attenuate_flux_flat_source_void(srh, distance, is_active, r); } else { - attenuate_flux_flat_source(distance, is_active); + attenuate_flux_flat_source(srh, distance, is_active, r); } break; case RandomRaySourceShape::LINEAR: case RandomRaySourceShape::LINEAR_XY: if (this->material() == MATERIAL_VOID) { - attenuate_flux_linear_source_void(distance, is_active); + attenuate_flux_linear_source_void(srh, distance, is_active, r); } else { - attenuate_flux_linear_source(distance, is_active); + attenuate_flux_linear_source(srh, distance, is_active, r); } break; default: @@ -364,18 +426,13 @@ void RandomRay::attenuate_flux(double distance, bool is_active) // than use of many atomic operations corresponding to each energy group // individually (at least on CPU). Several other bookkeeping tasks are also // performed when inside the lock. -void RandomRay::attenuate_flux_flat_source(double distance, bool is_active) +void RandomRay::attenuate_flux_flat_source( + SourceRegionHandle& srh, double distance, bool is_active, Position r) { // The number of geometric intersections is counted for reporting purposes n_event()++; - // Determine source region index etc. - int i_cell = lowest_coord().cell; - - // The source region is the spatial region index - int64_t sr = domain_->source_region_offsets_[i_cell] + cell_instance(); - - // The source element is the energy-specific region index + // Get material int material = this->material(); // MOC incoming flux attenuation + source contribution/attenuation equation @@ -383,115 +440,99 @@ void RandomRay::attenuate_flux_flat_source(double distance, bool is_active) float sigma_t = domain_->sigma_t_[material * negroups_ + g]; float tau = sigma_t * distance; float exponential = cjosey_exponential(tau); // exponential = 1 - exp(-tau) - float new_delta_psi = - (angular_flux_[g] - domain_->source_regions_.source(sr, g)) * exponential; + float new_delta_psi = (angular_flux_[g] - srh.source(g)) * exponential; delta_psi_[g] = new_delta_psi; angular_flux_[g] -= new_delta_psi; } // If ray is in the active phase (not in dead zone), make contributions to // source region bookkeeping + + // Aquire lock for source region + srh.lock(); + if (is_active) { - - // Aquire lock for source region - domain_->source_regions_.lock(sr).lock(); - // Accumulate delta psi into new estimate of source region flux for // this iteration for (int g = 0; g < negroups_; g++) { - domain_->source_regions_.scalar_flux_new(sr, g) += delta_psi_[g]; + srh.scalar_flux_new(g) += delta_psi_[g]; } // Accomulate volume (ray distance) into this iteration's estimate // of the source region's volume - domain_->source_regions_.volume(sr) += distance; + srh.volume() += distance; - // Tally valid position inside the source region (e.g., midpoint of - // the ray) if not done already - if (!domain_->source_regions_.position_recorded(sr)) { - Position midpoint = r() + u() * (distance / 2.0); - domain_->source_regions_.position(sr) = midpoint; - domain_->source_regions_.position_recorded(sr) = 1; - } - - // Release lock - domain_->source_regions_.lock(sr).unlock(); + srh.n_hits() += 1; } + + // Tally valid position inside the source region (e.g., midpoint of + // the ray) if not done already + if (!srh.position_recorded()) { + Position midpoint = r + u() * (distance / 2.0); + srh.position() = midpoint; + srh.position_recorded() = 1; + } + + // Release lock + srh.unlock(); } // Alternative flux attenuation function for true void regions. -void RandomRay::attenuate_flux_flat_source_void(double distance, bool is_active) +void RandomRay::attenuate_flux_flat_source_void( + SourceRegionHandle& srh, double distance, bool is_active, Position r) { // The number of geometric intersections is counted for reporting purposes n_event()++; - // Determine source region index etc. - int i_cell = lowest_coord().cell; - - // The source region is the spatial region index - int64_t sr = domain_->source_region_offsets_[i_cell] + cell_instance(); + int material = this->material(); // If ray is in the active phase (not in dead zone), make contributions to // source region bookkeeping if (is_active) { // Aquire lock for source region - domain_->source_regions_.lock(sr).lock(); + srh.lock(); // Accumulate delta psi into new estimate of source region flux for // this iteration for (int g = 0; g < negroups_; g++) { - domain_->source_regions_.scalar_flux_new(sr, g) += - angular_flux_[g] * distance; + srh.scalar_flux_new(g) += angular_flux_[g] * distance; } // Accomulate volume (ray distance) into this iteration's estimate // of the source region's volume - domain_->source_regions_.volume(sr) += distance; - domain_->source_regions_.volume_sq(sr) += distance * distance; + srh.volume() += distance; + srh.volume_sq() += distance * distance; + srh.n_hits() += 1; // Tally valid position inside the source region (e.g., midpoint of // the ray) if not done already - if (!domain_->source_regions_.position_recorded(sr)) { - Position midpoint = r() + u() * (distance / 2.0); - domain_->source_regions_.position(sr) = midpoint; - domain_->source_regions_.position_recorded(sr) = 1; + if (!srh.position_recorded()) { + Position midpoint = r + u() * (distance / 2.0); + srh.position() = midpoint; + srh.position_recorded() = 1; } // Release lock - domain_->source_regions_.lock(sr).unlock(); + srh.unlock(); } // Add source to incoming angular flux, assuming void region for (int g = 0; g < negroups_; g++) { - angular_flux_[g] += - domain_->source_regions_.external_source(sr, g) * distance; + angular_flux_[g] += srh.external_source(g) * distance; } } -void RandomRay::attenuate_flux_linear_source(double distance, bool is_active) +void RandomRay::attenuate_flux_linear_source( + SourceRegionHandle& srh, double distance, bool is_active, Position r) { - // Cast domain to LinearSourceDomain - LinearSourceDomain* domain = dynamic_cast(domain_); - if (!domain) { - fatal_error("RandomRay::attenuate_flux_linear_source() called with " - "non-LinearSourceDomain domain."); - } - // The number of geometric intersections is counted for reporting purposes n_event()++; - // Determine source region index etc. - int i_cell = lowest_coord().cell; - - // The source region is the spatial region index - int64_t sr = domain_->source_region_offsets_[i_cell] + cell_instance(); - - // The source element is the energy-specific region index int material = this->material(); - Position& centroid = domain_->source_regions_.centroid(sr); - Position midpoint = r() + u() * (distance / 2.0); + Position& centroid = srh.centroid(); + Position midpoint = r + u() * (distance / 2.0); // Determine the local position of the midpoint and the ray origin // relative to the source region's centroid @@ -503,9 +544,9 @@ void RandomRay::attenuate_flux_linear_source(double distance, bool is_active) // be no estimate of its centroid. We detect this by checking if it has // any accumulated volume. If its volume is zero, just use the midpoint // of the ray as the region's centroid. - if (domain_->source_regions_.volume_t(sr)) { + if (srh.volume_t()) { rm_local = midpoint - centroid; - r0_local = r() - centroid; + r0_local = r - centroid; } else { rm_local = {0.0, 0.0, 0.0}; r0_local = -u() * 0.5 * distance; @@ -530,10 +571,8 @@ void RandomRay::attenuate_flux_linear_source(double distance, bool is_active) // calculated from the source gradients dot product with local centroid // and direction, respectively. float spatial_source = - domain_->source_regions_.source(sr, g) + - rm_local.dot(domain_->source_regions_.source_gradients(sr, g)); - float dir_source = - u().dot(domain_->source_regions_.source_gradients(sr, g)); + srh.source(g) + rm_local.dot(srh.source_gradients(g)); + float dir_source = u().dot(srh.source_gradients(g)); float gn = exponentialG(tau); float f1 = 1.0f - tau * gn; @@ -566,44 +605,47 @@ void RandomRay::attenuate_flux_linear_source(double distance, bool is_active) } } + // Compute an estimate of the spatial moments matrix for the source + // region based on parameters from this ray's crossing + MomentMatrix moment_matrix_estimate; + moment_matrix_estimate.compute_spatial_moments_matrix( + rm_local, u(), distance); + + // Aquire lock for source region + srh.lock(); + // If ray is in the active phase (not in dead zone), make contributions to // source region bookkeeping + if (is_active) { - // Compute an estimate of the spatial moments matrix for the source - // region based on parameters from this ray's crossing - MomentMatrix moment_matrix_estimate; - moment_matrix_estimate.compute_spatial_moments_matrix( - rm_local, u(), distance); - - // Aquire lock for source region - domain_->source_regions_.lock(sr).lock(); - // Accumulate deltas into the new estimate of source region flux for this // iteration for (int g = 0; g < negroups_; g++) { - domain_->source_regions_.scalar_flux_new(sr, g) += delta_psi_[g]; - domain_->source_regions_.flux_moments_new(sr, g) += delta_moments_[g]; + srh.scalar_flux_new(g) += delta_psi_[g]; + srh.flux_moments_new(g) += delta_moments_[g]; } // Accumulate the volume (ray segment distance), centroid, and spatial // momement estimates into the running totals for the iteration for this - // source region. The centroid and spatial momements estimates are scaled by - // the ray segment length as part of length averaging of the estimates. - domain_->source_regions_.volume(sr) += distance; - domain_->source_regions_.centroid_iteration(sr) += midpoint * distance; + // source region. The centroid and spatial momements estimates are scaled + // by the ray segment length as part of length averaging of the estimates. + srh.volume() += distance; + srh.centroid_iteration() += midpoint * distance; moment_matrix_estimate *= distance; - domain_->source_regions_.mom_matrix(sr) += moment_matrix_estimate; + srh.mom_matrix() += moment_matrix_estimate; - // Tally valid position inside the source region (e.g., midpoint of - // the ray) if not done already - if (!domain_->source_regions_.position_recorded(sr)) { - domain_->source_regions_.position(sr) = midpoint; - domain_->source_regions_.position_recorded(sr) = 1; - } - - // Release lock - domain_->source_regions_.lock(sr).unlock(); + srh.n_hits() += 1; } + + // Tally valid position inside the source region (e.g., midpoint of + // the ray) if not done already + if (!srh.position_recorded()) { + srh.position() = midpoint; + srh.position_recorded() = 1; + } + + // Release lock + srh.unlock(); } // If traveling through a void region, the source term is either zero @@ -615,26 +657,13 @@ void RandomRay::attenuate_flux_linear_source(double distance, bool is_active) // estimating the flux at specific pixel coordinates. Thus, plots will look // nicer/more accurate if we record flux moments, so this function is useful. void RandomRay::attenuate_flux_linear_source_void( - double distance, bool is_active) + SourceRegionHandle& srh, double distance, bool is_active, Position r) { - // Cast domain to LinearSourceDomain - LinearSourceDomain* domain = dynamic_cast(domain_); - if (!domain) { - fatal_error("RandomRay::attenuate_flux_linear_source() called with " - "non-LinearSourceDomain domain."); - } - // The number of geometric intersections is counted for reporting purposes n_event()++; - // Determine source region index etc. - int i_cell = lowest_coord().cell; - - // The source region is the spatial region index - int64_t sr = domain_->source_region_offsets_[i_cell] + cell_instance(); - - Position& centroid = domain_->source_regions_.centroid(sr); - Position midpoint = r() + u() * (distance / 2.0); + Position& centroid = srh.centroid(); + Position midpoint = r + u() * (distance / 2.0); // Determine the local position of the midpoint and the ray origin // relative to the source region's centroid @@ -646,9 +675,9 @@ void RandomRay::attenuate_flux_linear_source_void( // be no estimate of its centroid. We detect this by checking if it has // any accumulated volume. If its volume is zero, just use the midpoint // of the ray as the region's centroid. - if (domain_->source_regions_.volume_t(sr)) { + if (srh.volume_t()) { rm_local = midpoint - centroid; - r0_local = r() - centroid; + r0_local = r - centroid; } else { rm_local = {0.0, 0.0, 0.0}; r0_local = -u() * 0.5 * distance; @@ -659,7 +688,7 @@ void RandomRay::attenuate_flux_linear_source_void( // transport through a void region is greatly simplified. Here we // compute the updated flux moments. for (int g = 0; g < negroups_; g++) { - float spatial_source = domain_->source_regions_.source(sr, g); + float spatial_source = srh.external_source(g); float new_delta_psi = (angular_flux_[g] - spatial_source) * distance; float h1 = 0.5f; h1 = h1 * angular_flux_[g]; @@ -688,41 +717,41 @@ void RandomRay::attenuate_flux_linear_source_void( rm_local, u(), distance); // Aquire lock for source region - domain_->source_regions_.lock(sr).lock(); + srh.lock(); // Accumulate delta psi into new estimate of source region flux for // this iteration, and update flux momements for (int g = 0; g < negroups_; g++) { - domain_->source_regions_.scalar_flux_new(sr, g) += - angular_flux_[g] * distance; - domain_->source_regions_.flux_moments_new(sr, g) += delta_moments_[g]; + srh.scalar_flux_new(g) += angular_flux_[g] * distance; + srh.flux_moments_new(g) += delta_moments_[g]; } // Accumulate the volume (ray segment distance), centroid, and spatial // momement estimates into the running totals for the iteration for this // source region. The centroid and spatial momements estimates are scaled by // the ray segment length as part of length averaging of the estimates. - domain_->source_regions_.volume(sr) += distance; - domain_->source_regions_.volume_sq(sr) += distance_2; - domain_->source_regions_.centroid_iteration(sr) += midpoint * distance; + srh.volume() += distance; + srh.volume_sq() += distance_2; + srh.centroid_iteration() += midpoint * distance; moment_matrix_estimate *= distance; - domain_->source_regions_.mom_matrix(sr) += moment_matrix_estimate; + srh.mom_matrix() += moment_matrix_estimate; // Tally valid position inside the source region (e.g., midpoint of // the ray) if not done already - if (!domain_->source_regions_.position_recorded(sr)) { - domain_->source_regions_.position(sr) = midpoint; - domain_->source_regions_.position_recorded(sr) = 1; + if (!srh.position_recorded()) { + srh.position() = midpoint; + srh.position_recorded() = 1; } + srh.n_hits() += 1; + // Release lock - domain_->source_regions_.lock(sr).unlock(); + srh.unlock(); } // Add source to incoming angular flux, assuming void region for (int g = 0; g < negroups_; g++) { - angular_flux_[g] += - domain_->source_regions_.external_source(sr, g) * distance; + angular_flux_[g] += srh.external_source(g) * distance; } } @@ -738,7 +767,7 @@ void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain) wgt() = 1.0; // set identifier for particle - id() = simulation::work_index[mpi::rank] + ray_id; + id() = ray_id; // generate source site using sample method SourceSite site; @@ -775,8 +804,26 @@ void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain) int i_cell = lowest_coord().cell; int64_t sr = domain_->source_region_offsets_[i_cell] + cell_instance(); - for (int g = 0; g < negroups_; g++) { - angular_flux_[g] = domain_->source_regions_.source(sr, g); + SourceRegionHandle srh; + if (mesh_subdivision_enabled_) { + int mesh_idx = domain_->base_source_regions_.mesh(sr); + int mesh_bin; + if (mesh_idx == C_NONE) { + mesh_bin = 0; + } else { + Mesh* mesh = model::meshes[mesh_idx].get(); + mesh_bin = mesh->get_bin(r()); + } + srh = + domain_->get_subdivided_source_region_handle(sr, mesh_bin, r(), 0.0, u()); + } else { + srh = domain_->source_regions_.get_source_region_handle(sr); + } + + if (!srh.is_numerical_fp_artifact_) { + for (int g = 0; g < negroups_; g++) { + angular_flux_[g] = srh.source(g); + } } } diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index f1477c3fe..bf01d2f57 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -14,6 +14,7 @@ #include "openmc/tallies/tally.h" #include "openmc/tallies/tally_scoring.h" #include "openmc/timer.h" +#include "openmc/weight_windows.h" namespace openmc { @@ -48,13 +49,17 @@ void openmc_run_random_ray() // Declare forward flux so that it can be saved for later adjoint simulation vector forward_flux; + SourceRegionContainer forward_source_regions; + SourceRegionContainer forward_base_source_regions; + std::unordered_map + forward_source_region_map; { // Initialize Random Ray Simulation Object RandomRaySimulation sim; // Initialize fixed sources, if present - sim.prepare_fixed_sources(); + sim.apply_fixed_sources_and_mesh_domains(); // Begin main simulation timer simulation::time_total.start(); @@ -77,12 +82,13 @@ void openmc_run_random_ray() forward_flux[i] *= source_normalization_factor; } + forward_source_regions = sim.domain()->source_regions_; + forward_source_region_map = sim.domain()->source_region_map_; + forward_base_source_regions = sim.domain()->base_source_regions_; + // Finalize OpenMC openmc_simulation_finalize(); - // Reduce variables across MPI ranks - sim.reduce_simulation_statistics(); - // Output all simulation results sim.output_simulation_results(); } @@ -107,7 +113,9 @@ void openmc_run_random_ray() RandomRaySimulation adjoint_sim; // Initialize adjoint fixed sources, if present - adjoint_sim.prepare_fixed_sources_adjoint(forward_flux); + adjoint_sim.prepare_fixed_sources_adjoint(forward_flux, + forward_source_regions, forward_base_source_regions, + forward_source_region_map); // Transpose scattering matrix adjoint_sim.domain()->transpose_scattering_matrix(); @@ -127,9 +135,6 @@ void openmc_run_random_ray() // Finalize OpenMC openmc_simulation_finalize(); - // Reduce variables across MPI ranks - adjoint_sim.reduce_simulation_statistics(); - // Output all simulation results adjoint_sim.output_simulation_results(); } @@ -321,12 +326,36 @@ void validate_random_ray_inputs() #ifdef OPENMC_MPI if (mpi::n_procs > 1) { warning( - "Domain replication in random ray is supported, but suffers from poor " - "scaling of source all-reduce operations. Performance may severely " - "degrade beyond just a few MPI ranks. Domain decomposition may be " - "implemented in the future to provide efficient scaling."); + "MPI parallelism is not supported by the random ray solver. All work " + "will be performed by rank 0. Domain decomposition may be implemented in " + "the future to provide efficient MPI scaling."); } #endif + + // Warn about instability resulting from linear sources in small regions + // when generating weight windows with FW-CADIS and an overlaid mesh. + /////////////////////////////////////////////////////////////////// + if (RandomRay::source_shape_ == RandomRaySourceShape::LINEAR && + RandomRay::mesh_subdivision_enabled_ && + variance_reduction::weight_windows.size() > 0) { + warning( + "Linear sources may result in negative fluxes in small source regions " + "generated by mesh subdivision. Negative sources may result in low " + "quality FW-CADIS weight windows. We recommend you use flat source mode " + "when generating weight windows with an overlaid mesh tally."); + } +} + +void openmc_reset_random_ray() +{ + FlatSourceDomain::volume_estimator_ = RandomRayVolumeEstimator::HYBRID; + FlatSourceDomain::volume_normalized_flux_tallies_ = false; + FlatSourceDomain::adjoint_ = false; + FlatSourceDomain::mesh_domain_map_.clear(); + RandomRay::ray_source_.reset(); + RandomRay::source_shape_ = RandomRaySourceShape::FLAT; + RandomRay::mesh_subdivision_enabled_ = false; + RandomRay::sample_method_ = RandomRaySampleMethod::PRNG; } //============================================================================== @@ -361,19 +390,29 @@ RandomRaySimulation::RandomRaySimulation() domain_->flatten_xs(); } -void RandomRaySimulation::prepare_fixed_sources() +void RandomRaySimulation::apply_fixed_sources_and_mesh_domains() { if (settings::run_mode == RunMode::FIXED_SOURCE) { // Transfer external source user inputs onto random ray source regions domain_->convert_external_sources(); domain_->count_external_source_regions(); } + domain_->apply_meshes(); } void RandomRaySimulation::prepare_fixed_sources_adjoint( - vector& forward_flux) + vector& forward_flux, SourceRegionContainer& forward_source_regions, + SourceRegionContainer& forward_base_source_regions, + std::unordered_map& + forward_source_region_map) { if (settings::run_mode == RunMode::FIXED_SOURCE) { + if (RandomRay::mesh_subdivision_enabled_) { + domain_->source_regions_ = forward_source_regions; + domain_->source_region_map_ = forward_source_region_map; + domain_->base_source_regions_ = forward_base_source_regions; + domain_->source_regions_.adjoint_reset(); + } domain_->set_adjoint_sources(forward_flux); } } @@ -382,75 +421,93 @@ void RandomRaySimulation::simulate() { // Random ray power iteration loop while (simulation::current_batch < settings::n_batches) { - // Initialize the current batch initialize_batch(); initialize_generation(); - // Reset total starting particle weight used for normalizing tallies - simulation::total_weight = 1.0; + // MPI not supported in random ray solver, so all work is done by rank 0 + // TODO: Implement domain decomposition for MPI parallelism + if (mpi::master) { - // Update source term (scattering + fission) - domain_->update_neutron_source(k_eff_); + // Reset total starting particle weight used for normalizing tallies + simulation::total_weight = 1.0; - // Reset scalar fluxes, iteration volume tallies, and region hit flags to - // zero - domain_->batch_reset(); + // Update source term (scattering + fission) + domain_->update_neutron_source(k_eff_); - // Start timer for transport - simulation::time_transport.start(); + // Reset scalar fluxes, iteration volume tallies, and region hit flags to + // zero + domain_->batch_reset(); + + // At the beginning of the simulation, if mesh subvivision is in use, we + // need to swap the main source region container into the base container, + // as the main source region container will be used to hold the true + // subdivided source regions. The base container will therefore only + // contain the external source region information, the mesh indices, + // material properties, and initial guess values for the flux/source. + if (RandomRay::mesh_subdivision_enabled_ && + simulation::current_batch == 1 && !FlatSourceDomain::adjoint_) { + domain_->prepare_base_source_regions(); + } + + // Start timer for transport + simulation::time_transport.start(); // Transport sweep over all random rays for the iteration #pragma omp parallel for schedule(dynamic) \ reduction(+ : total_geometric_intersections_) - for (int i = 0; i < simulation::work_per_rank; i++) { - RandomRay ray(i, domain_.get()); - total_geometric_intersections_ += - ray.transport_history_based_single_ray(); - } + for (int i = 0; i < settings::n_particles; i++) { + RandomRay ray(i, domain_.get()); + total_geometric_intersections_ += + ray.transport_history_based_single_ray(); + } - simulation::time_transport.stop(); + simulation::time_transport.stop(); - // If using multiple MPI ranks, perform all reduce on all transport results - domain_->all_reduce_replicated_source_regions(); + // If using mesh subdivision, add any newly discovered source regions + // to the main source region container. + if (RandomRay::mesh_subdivision_enabled_) { + domain_->finalize_discovered_source_regions(); + } - // Normalize scalar flux and update volumes - domain_->normalize_scalar_flux_and_volumes( - settings::n_particles * RandomRay::distance_active_); + // Normalize scalar flux and update volumes + domain_->normalize_scalar_flux_and_volumes( + settings::n_particles * RandomRay::distance_active_); - // Add source to scalar flux, compute number of FSR hits - int64_t n_hits = domain_->add_source_to_scalar_flux(); + // Add source to scalar flux, compute number of FSR hits + int64_t n_hits = domain_->add_source_to_scalar_flux(); - if (settings::run_mode == RunMode::EIGENVALUE) { - // Compute random ray k-eff - k_eff_ = domain_->compute_k_eff(k_eff_); + if (settings::run_mode == RunMode::EIGENVALUE) { + // Compute random ray k-eff + k_eff_ = domain_->compute_k_eff(k_eff_); - // Store random ray k-eff into OpenMC's native k-eff variable - global_tally_tracklength = k_eff_; - } + // Store random ray k-eff into OpenMC's native k-eff variable + global_tally_tracklength = k_eff_; + } - // Execute all tallying tasks, if this is an active batch - if (simulation::current_batch > settings::n_inactive) { + // Execute all tallying tasks, if this is an active batch + if (simulation::current_batch > settings::n_inactive) { - // Add this iteration's scalar flux estimate to final accumulated estimate - domain_->accumulate_iteration_flux(); + // Add this iteration's scalar flux estimate to final accumulated + // estimate + domain_->accumulate_iteration_flux(); - if (mpi::master) { // Generate mapping between source regions and tallies if (!domain_->mapped_all_tallies_) { domain_->convert_source_regions_to_tallies(); } - // Use above mapping to contribute FSR flux data to appropriate tallies + // Use above mapping to contribute FSR flux data to appropriate + // tallies domain_->random_ray_tally(); } - } - // Set phi_old = phi_new - domain_->flux_swap(); + // Set phi_old = phi_new + domain_->flux_swap(); - // Check for any obvious insabilities/nans/infs - instability_check(n_hits, k_eff_, avg_miss_rate_); + // Check for any obvious insabilities/nans/infs + instability_check(n_hits, k_eff_, avg_miss_rate_); + } // End MPI master work // Finalize the current batch finalize_generation(); @@ -458,27 +515,13 @@ void RandomRaySimulation::simulate() } // End random ray power iteration loop } -void RandomRaySimulation::reduce_simulation_statistics() -{ - // Reduce number of intersections -#ifdef OPENMC_MPI - if (mpi::n_procs > 1) { - uint64_t total_geometric_intersections_reduced = 0; - MPI_Reduce(&total_geometric_intersections_, - &total_geometric_intersections_reduced, 1, MPI_UNSIGNED_LONG, MPI_SUM, 0, - mpi::intracomm); - total_geometric_intersections_ = total_geometric_intersections_reduced; - } -#endif -} - void RandomRaySimulation::output_simulation_results() const { // Print random ray results if (mpi::master) { print_results_random_ray(total_geometric_intersections_, avg_miss_rate_ / settings::n_batches, negroups_, - domain_->n_source_regions_, domain_->n_external_source_regions_); + domain_->n_source_regions(), domain_->n_external_source_regions_); if (model::plots.size() > 0) { domain_->output_to_vtk(); } @@ -490,8 +533,8 @@ void RandomRaySimulation::output_simulation_results() const void RandomRaySimulation::instability_check( int64_t n_hits, double k_eff, double& avg_miss_rate) const { - double percent_missed = ((domain_->n_source_regions_ - n_hits) / - static_cast(domain_->n_source_regions_)) * + double percent_missed = ((domain_->n_source_regions() - n_hits) / + static_cast(domain_->n_source_regions())) * 100.0; avg_miss_rate += percent_missed; diff --git a/src/random_ray/source_region.cpp b/src/random_ray/source_region.cpp index 4ac86ea8c..6c0df0157 100644 --- a/src/random_ray/source_region.cpp +++ b/src/random_ray/source_region.cpp @@ -2,9 +2,36 @@ #include "openmc/error.h" #include "openmc/message_passing.h" +#include "openmc/simulation.h" namespace openmc { +//============================================================================== +// SourceRegionHandle implementation +//============================================================================== +SourceRegionHandle::SourceRegionHandle(SourceRegion& sr) + : negroups_(sr.scalar_flux_old_.size()), material_(&sr.material_), + is_small_(&sr.is_small_), n_hits_(&sr.n_hits_), + is_linear_(sr.source_gradients_.size() > 0), lock_(&sr.lock_), + volume_(&sr.volume_), volume_t_(&sr.volume_t_), volume_sq_(&sr.volume_sq_), + volume_sq_t_(&sr.volume_sq_t_), volume_naive_(&sr.volume_naive_), + position_recorded_(&sr.position_recorded_), + external_source_present_(&sr.external_source_present_), + position_(&sr.position_), centroid_(&sr.centroid_), + centroid_iteration_(&sr.centroid_iteration_), centroid_t_(&sr.centroid_t_), + mom_matrix_(&sr.mom_matrix_), mom_matrix_t_(&sr.mom_matrix_t_), + volume_task_(&sr.volume_task_), mesh_(&sr.mesh_), + parent_sr_(&sr.parent_sr_), scalar_flux_old_(sr.scalar_flux_old_.data()), + scalar_flux_new_(sr.scalar_flux_new_.data()), source_(sr.source_.data()), + external_source_(sr.external_source_.data()), + scalar_flux_final_(sr.scalar_flux_final_.data()), + source_gradients_(sr.source_gradients_.data()), + flux_moments_old_(sr.flux_moments_old_.data()), + flux_moments_new_(sr.flux_moments_new_.data()), + flux_moments_t_(sr.flux_moments_t_.data()), + tally_task_(sr.tally_task_.data()) +{} + //============================================================================== // SourceRegion implementation //============================================================================== @@ -25,7 +52,6 @@ SourceRegion::SourceRegion(int negroups, bool is_linear) scalar_flux_final_.assign(negroups, 0.0); tally_task_.resize(negroups); - if (is_linear) { source_gradients_.resize(negroups); flux_moments_old_.resize(negroups); @@ -34,15 +60,37 @@ SourceRegion::SourceRegion(int negroups, bool is_linear) } } +SourceRegion::SourceRegion(const SourceRegionHandle& handle, int64_t parent_sr) + : SourceRegion(handle.negroups_, handle.is_linear_) +{ + material_ = handle.material(); + mesh_ = handle.mesh(); + parent_sr_ = parent_sr; + for (int g = 0; g < scalar_flux_new_.size(); g++) { + scalar_flux_old_[g] = handle.scalar_flux_old(g); + source_[g] = handle.source(g); + } + + if (settings::run_mode == RunMode::FIXED_SOURCE) { + external_source_present_ = handle.external_source_present(); + for (int g = 0; g < scalar_flux_new_.size(); g++) { + external_source_[g] = handle.external_source(g); + } + } +} + //============================================================================== // SourceRegionContainer implementation //============================================================================== + void SourceRegionContainer::push_back(const SourceRegion& sr) { n_source_regions_++; // Scalar fields material_.push_back(sr.material_); + is_small_.push_back(sr.is_small_); + n_hits_.push_back(sr.n_hits_); lock_.push_back(sr.lock_); volume_.push_back(sr.volume_); volume_t_.push_back(sr.volume_t_); @@ -53,6 +101,8 @@ void SourceRegionContainer::push_back(const SourceRegion& sr) external_source_present_.push_back(sr.external_source_present_); position_.push_back(sr.position_); volume_task_.push_back(sr.volume_task_); + mesh_.push_back(sr.mesh_); + parent_sr_.push_back(sr.parent_sr_); // Only store these fields if is_linear_ is true if (is_linear_) { @@ -92,6 +142,8 @@ void SourceRegionContainer::assign( // Clear existing data n_source_regions_ = 0; material_.clear(); + is_small_.clear(); + n_hits_.clear(); lock_.clear(); volume_.clear(); volume_t_.clear(); @@ -101,6 +153,8 @@ void SourceRegionContainer::assign( position_recorded_.clear(); external_source_present_.clear(); position_.clear(); + mesh_.clear(); + parent_sr_.clear(); if (is_linear_) { centroid_.clear(); @@ -140,103 +194,88 @@ void SourceRegionContainer::flux_swap() } } -void SourceRegionContainer::mpi_sync_ranks(bool reduce_position) +SourceRegionHandle SourceRegionContainer::get_source_region_handle(int64_t sr) { -#ifdef OPENMC_MPI + SourceRegionHandle handle; + handle.negroups_ = negroups(); + handle.material_ = &material(sr); + handle.is_small_ = &is_small(sr); + handle.n_hits_ = &n_hits(sr); + handle.is_linear_ = is_linear(); + handle.lock_ = &lock(sr); + handle.volume_ = &volume(sr); + handle.volume_t_ = &volume_t(sr); + handle.volume_sq_ = &volume_sq(sr); + handle.volume_sq_t_ = &volume_sq_t(sr); + handle.volume_naive_ = &volume_naive(sr); + handle.position_recorded_ = &position_recorded(sr); + handle.external_source_present_ = &external_source_present(sr); + handle.position_ = &position(sr); + handle.volume_task_ = &volume_task(sr); + handle.mesh_ = &mesh(sr); + handle.parent_sr_ = &parent_sr(sr); + handle.scalar_flux_old_ = &scalar_flux_old(sr, 0); + handle.scalar_flux_new_ = &scalar_flux_new(sr, 0); + handle.source_ = &source(sr, 0); + handle.external_source_ = &external_source(sr, 0); + handle.scalar_flux_final_ = &scalar_flux_final(sr, 0); + handle.tally_task_ = &tally_task(sr, 0); - // The "position_recorded" variable needs to be allreduced (and maxed), - // as whether or not a cell was hit will affect some decisions in how the - // source is calculated in the next iteration so as to avoid dividing - // by zero. We take the max rather than the sum as the hit values are - // expected to be zero or 1. - MPI_Allreduce(MPI_IN_PLACE, position_recorded_.data(), 1, MPI_INT, MPI_MAX, - mpi::intracomm); - - // The position variable is more complicated to reduce than the others, - // as we do not want the sum of all positions in each cell, rather, we - // want to just pick any single valid position. Thus, we perform a gather - // and then pick the first valid position we find for all source regions - // that have had a position recorded. This operation does not need to - // be broadcast back to other ranks, as this value is only used for the - // tally conversion operation, which is only performed on the master rank. - // While this is expensive, it only needs to be done for active batches, - // and only if we have not mapped all the tallies yet. Once tallies are - // fully mapped, then the position vector is fully populated, so this - // operation can be skipped. - - // Then, we perform the gather of position data, if needed - if (reduce_position) { - - // Master rank will gather results and pick valid positions - if (mpi::master) { - // Initialize temporary vector for receiving positions - vector> all_position; - all_position.resize(mpi::n_procs); - for (int i = 0; i < mpi::n_procs; i++) { - all_position[i].resize(n_source_regions_); - } - - // Copy master rank data into gathered vector for convenience - all_position[0] = position_; - - // Receive all data into gather vector - for (int i = 1; i < mpi::n_procs; i++) { - MPI_Recv(all_position[i].data(), n_source_regions_ * 3, MPI_DOUBLE, i, - 0, mpi::intracomm, MPI_STATUS_IGNORE); - } - - // Scan through gathered data and pick first valid cell posiiton - for (int64_t sr = 0; sr < n_source_regions_; sr++) { - if (position_recorded_[sr] == 1) { - for (int i = 0; i < mpi::n_procs; i++) { - if (all_position[i][sr].x != 0.0 || all_position[i][sr].y != 0.0 || - all_position[i][sr].z != 0.0) { - position_[sr] = all_position[i][sr]; - break; - } - } - } - } - } else { - // Other ranks just send in their data - MPI_Send(position_.data(), n_source_regions_ * 3, MPI_DOUBLE, 0, 0, - mpi::intracomm); - } + if (handle.is_linear_) { + handle.centroid_ = ¢roid(sr); + handle.centroid_iteration_ = ¢roid_iteration(sr); + handle.centroid_t_ = ¢roid_t(sr); + handle.mom_matrix_ = &mom_matrix(sr); + handle.mom_matrix_t_ = &mom_matrix_t(sr); + handle.source_gradients_ = &source_gradients(sr, 0); + handle.flux_moments_old_ = &flux_moments_old(sr, 0); + handle.flux_moments_new_ = &flux_moments_new(sr, 0); + handle.flux_moments_t_ = &flux_moments_t(sr, 0); } - // For the rest of the source region data, we simply perform an all reduce, - // as these values will be needed on all ranks for transport during the - // next iteration. - MPI_Allreduce(MPI_IN_PLACE, volume_.data(), n_source_regions_, MPI_DOUBLE, - MPI_SUM, mpi::intracomm); - MPI_Allreduce(MPI_IN_PLACE, volume_sq_.data(), n_source_regions_, MPI_DOUBLE, - MPI_SUM, mpi::intracomm); - - MPI_Allreduce(MPI_IN_PLACE, scalar_flux_new_.data(), - n_source_regions_ * negroups_, MPI_DOUBLE, MPI_SUM, mpi::intracomm); - - if (is_linear_) { - // We are going to assume we can safely cast Position, MomentArray, - // and MomentMatrix to contiguous arrays of doubles for the MPI - // allreduce operation. This is a safe assumption as typically - // compilers will at most pad to 8 byte boundaries. If a new FP32 - // MomentArray type is introduced, then there will likely be padding, in - // which case this function will need to become more complex. - if (sizeof(MomentArray) != 3 * sizeof(double) || - sizeof(MomentMatrix) != 6 * sizeof(double)) { - fatal_error( - "Unexpected buffer padding in linear source domain reduction."); - } - - MPI_Allreduce(MPI_IN_PLACE, static_cast(flux_moments_new_.data()), - n_source_regions_ * negroups_ * 3, MPI_DOUBLE, MPI_SUM, mpi::intracomm); - MPI_Allreduce(MPI_IN_PLACE, static_cast(mom_matrix_.data()), - n_source_regions_ * 6, MPI_DOUBLE, MPI_SUM, mpi::intracomm); - MPI_Allreduce(MPI_IN_PLACE, static_cast(centroid_iteration_.data()), - n_source_regions_ * 3, MPI_DOUBLE, MPI_SUM, mpi::intracomm); - } - -#endif + return handle; } -} // namespace openmc \ No newline at end of file +void SourceRegionContainer::adjoint_reset() +{ + std::fill(n_hits_.begin(), n_hits_.end(), 0); + std::fill(volume_.begin(), volume_.end(), 0.0); + std::fill(volume_t_.begin(), volume_t_.end(), 0.0); + std::fill(volume_sq_.begin(), volume_sq_.end(), 0.0); + std::fill(volume_sq_t_.begin(), volume_sq_t_.end(), 0.0); + std::fill(volume_naive_.begin(), volume_naive_.end(), 0.0); + std::fill( + external_source_present_.begin(), external_source_present_.end(), 0); + std::fill(external_source_.begin(), external_source_.end(), 0.0); + std::fill(centroid_.begin(), centroid_.end(), Position {0.0, 0.0, 0.0}); + std::fill(centroid_iteration_.begin(), centroid_iteration_.end(), + Position {0.0, 0.0, 0.0}); + std::fill(centroid_t_.begin(), centroid_t_.end(), Position {0.0, 0.0, 0.0}); + std::fill(mom_matrix_.begin(), mom_matrix_.end(), + MomentMatrix {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}); + std::fill(mom_matrix_t_.begin(), mom_matrix_t_.end(), + MomentMatrix {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}); + for (auto& task_set : volume_task_) { + task_set.clear(); + } + std::fill(scalar_flux_old_.begin(), scalar_flux_old_.end(), 0.0); + std::fill(scalar_flux_new_.begin(), scalar_flux_new_.end(), 0.0); + std::fill(scalar_flux_final_.begin(), scalar_flux_final_.end(), 0.0); + std::fill(source_.begin(), source_.end(), 0.0f); + std::fill(external_source_.begin(), external_source_.end(), 0.0f); + + std::fill(source_gradients_.begin(), source_gradients_.end(), + MomentArray {0.0, 0.0, 0.0}); + std::fill(flux_moments_old_.begin(), flux_moments_old_.end(), + MomentArray {0.0, 0.0, 0.0}); + std::fill(flux_moments_new_.begin(), flux_moments_new_.end(), + MomentArray {0.0, 0.0, 0.0}); + std::fill(flux_moments_t_.begin(), flux_moments_t_.end(), + MomentArray {0.0, 0.0, 0.0}); + + for (auto& task_set : tally_task_) { + task_set.clear(); + } +} + +} // namespace openmc diff --git a/src/settings.cpp b/src/settings.cpp index 67fdfea66..49b6590ec 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -1,4 +1,5 @@ #include "openmc/settings.h" +#include "openmc/random_ray/flat_source_domain.h" #include // for ceil, pow #include // for numeric_limits @@ -319,6 +320,31 @@ void get_run_parameters(pugi::xml_node node_base) fatal_error("Unrecognized sample method: " + temp_str); } } + if (check_for_node(random_ray_node, "source_region_meshes")) { + pugi::xml_node node_source_region_meshes = + random_ray_node.child("source_region_meshes"); + for (pugi::xml_node node_mesh : + node_source_region_meshes.children("mesh")) { + int mesh_id = std::stoi(node_mesh.attribute("id").value()); + for (pugi::xml_node node_domain : node_mesh.children("domain")) { + int domain_id = std::stoi(node_domain.attribute("id").value()); + std::string domain_type = node_domain.attribute("type").value(); + Source::DomainType type; + if (domain_type == "material") { + type = Source::DomainType::MATERIAL; + } else if (domain_type == "cell") { + type = Source::DomainType::CELL; + } else if (domain_type == "universe") { + type = Source::DomainType::UNIVERSE; + } else { + throw std::runtime_error("Unknown domain type: " + domain_type); + } + FlatSourceDomain::mesh_domain_map_[mesh_id].emplace_back( + type, domain_id); + RandomRay::mesh_subdivision_enabled_ = true; + } + } + } } } diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index 9daf21f75..0efa5a6ae 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -657,11 +657,18 @@ void WeightWindows::update_weights(const Tally* tally, const std::string& value, } } - xt::noalias(new_bounds) = 1.0 / new_bounds; - + // We take the inverse, but are careful not to divide by zero e.g. if some + // mesh bins are not reachable in the physical geometry. + xt::noalias(new_bounds) = + xt::where(xt::not_equal(new_bounds, 0.0), 1.0 / new_bounds, 0.0); auto max_val = xt::amax(new_bounds)(); - xt::noalias(new_bounds) = new_bounds / (2.0 * max_val); + + // For bins that were missed, we use the minimum weight window value. This + // shouldn't matter except for plotting. + auto min_val = xt::amin(new_bounds)(); + xt::noalias(new_bounds) = + xt::where(xt::not_equal(new_bounds, 0.0), new_bounds, min_val); } // make sure that values where the mean is zero are set s.t. the weight window diff --git a/tests/regression_tests/random_ray_fixed_source_mesh/__init__.py b/tests/regression_tests/random_ray_fixed_source_mesh/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_fixed_source_mesh/flat/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_mesh/flat/inputs_true.dat new file mode 100644 index 000000000..67c2ffc15 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_mesh/flat/inputs_true.dat @@ -0,0 +1,271 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 30 + 15 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + + + + + + + + + + + + flat + + + 24 24 24 + 0.0 0.0 0.0 + 30.0 30.0 30.0 + + + 36 36 36 + 0.0 0.0 0.0 + 30.0 30.0 30.0 + + + 30 30 30 + 0.0 0.0 0.0 + 30.0 30.0 30.0 + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_fixed_source_mesh/flat/results_true.dat b/tests/regression_tests/random_ray_fixed_source_mesh/flat/results_true.dat new file mode 100644 index 000000000..0b0eaa447 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_mesh/flat/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +1.750296E+00 +2.051829E-01 +tally 2: +8.045199E-02 +4.384408E-04 +tally 3: +5.216828E-03 +1.840861E-06 diff --git a/tests/regression_tests/random_ray_fixed_source_mesh/linear/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_mesh/linear/inputs_true.dat new file mode 100644 index 000000000..10b7c74f1 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_mesh/linear/inputs_true.dat @@ -0,0 +1,271 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 30 + 15 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + + + + + + + + + + + + linear + + + 24 24 24 + 0.0 0.0 0.0 + 30.0 30.0 30.0 + + + 36 36 36 + 0.0 0.0 0.0 + 30.0 30.0 30.0 + + + 30 30 30 + 0.0 0.0 0.0 + 30.0 30.0 30.0 + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_fixed_source_mesh/linear/results_true.dat b/tests/regression_tests/random_ray_fixed_source_mesh/linear/results_true.dat new file mode 100644 index 000000000..23754a157 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_mesh/linear/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +1.780361E+00 +2.137912E-01 +tally 2: +8.230400E-02 +4.596391E-04 +tally 3: +5.207797E-03 +1.834531E-06 diff --git a/tests/regression_tests/random_ray_fixed_source_mesh/test.py b/tests/regression_tests/random_ray_fixed_source_mesh/test.py new file mode 100644 index 000000000..0b93b2a7a --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_mesh/test.py @@ -0,0 +1,53 @@ +import os + +import openmc +from openmc.examples import random_ray_three_region_cube +from openmc.utility_funcs import change_directory +import pytest + +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 make_mesh(dim): + width = 30.0 + mesh = openmc.RegularMesh() + mesh.dimension = (dim, dim, dim) + mesh.lower_left = (0.0, 0.0, 0.0) + mesh.upper_right = (width, width, width) + return mesh + + +@pytest.mark.parametrize("shape", ["flat", "linear"]) +def test_random_ray_fixed_source_mesh(shape): + with change_directory(shape): + openmc.reset_auto_ids() + model = random_ray_three_region_cube() + + # We will apply three different mesh resolutions to three different domain types + source_universe = model.geometry.get_universes_by_name('source universe')[0] + void_cell = model.geometry.get_cells_by_name('infinite void region')[0] + absorber_mat = model.geometry.get_materials_by_name('absorber')[0] + + model.settings.random_ray['source_region_meshes'] = [ + (make_mesh(24), [source_universe]), + (make_mesh(36), [void_cell]), + (make_mesh(30), [absorber_mat]) + ] + + # We also test flat/linear source shapes to ensure they are both + # working correctly with the mesh overlay logic + model.settings.random_ray['source_shape'] = shape + + model.settings.inactive = 15 + model.settings.batches = 30 + + harness = MGXSTestHarness('statepoint.30.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_k_eff_mesh/__init__.py b/tests/regression_tests/random_ray_k_eff_mesh/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_k_eff_mesh/inputs_true.dat b/tests/regression_tests/random_ray_k_eff_mesh/inputs_true.dat new file mode 100644 index 000000000..cdc717198 --- /dev/null +++ b/tests/regression_tests/random_ray_k_eff_mesh/inputs_true.dat @@ -0,0 +1,119 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.126 0.126 + 10 10 + -0.63 -0.63 + +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 + + + 1.26 1.26 + 2 2 + -1.26 -1.26 + +2 2 +2 5 + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + multi-group + + 100.0 + 20.0 + + + -1.26 -1.26 -1 1.26 1.26 1 + + + True + + + + + + + + 40 40 + -1.26 -1.26 + 1.26 1.26 + + + + + 2 2 + -1.26 -1.26 + 1.26 1.26 + + + 1 + + + 1e-05 0.0635 10.0 100.0 1000.0 500000.0 1000000.0 20000000.0 + + + 1 2 + flux fission nu-fission + analog + + + diff --git a/tests/regression_tests/random_ray_k_eff_mesh/results_true.dat b/tests/regression_tests/random_ray_k_eff_mesh/results_true.dat new file mode 100644 index 000000000..535db3b55 --- /dev/null +++ b/tests/regression_tests/random_ray_k_eff_mesh/results_true.dat @@ -0,0 +1,171 @@ +k-combined: +8.379203E-01 8.057199E-03 +tally 1: +5.080171E+00 +5.167984E+00 +1.880341E+00 +7.079266E-01 +4.576373E+00 +4.193319E+00 +2.859914E+00 +1.638769E+00 +4.243332E-01 +3.607732E-02 +1.032742E+00 +2.136998E-01 +1.692643E+00 +5.794069E-01 +5.445214E-02 +5.995212E-04 +1.325256E-01 +3.551193E-03 +2.372336E+00 +1.147031E+00 +7.807378E-02 +1.242019E-03 +1.900160E-01 +7.356955E-03 +7.135636E+00 +1.035026E+01 +8.273225E-02 +1.392069E-03 +2.013562E-01 +8.245961E-03 +2.044034E+01 +8.394042E+01 +3.100485E-02 +1.932097E-04 +7.671933E-02 +1.182985E-03 +1.313652E+01 +3.451847E+01 +1.764978E-01 +6.230420E-03 +4.909196E-01 +4.820140E-02 +7.585874E+00 +1.150936E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.386790E+00 +2.295327E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.820058E+00 +6.729238E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.694013E+00 +1.481363E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +7.453818E+00 +1.128202E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.823642E+01 +6.682239E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.137903E+01 +2.590265E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.589529E+00 +4.219985E+00 +1.712446E+00 +5.873675E-01 +4.167750E+00 +3.479202E+00 +2.728285E+00 +1.492132E+00 +4.104063E-01 +3.377132E-02 +9.988468E-01 +2.000404E-01 +1.660506E+00 +5.567967E-01 +5.427391E-02 +5.944708E-04 +1.320918E-01 +3.521277E-03 +2.305889E+00 +1.082691E+00 +7.695793E-02 +1.205644E-03 +1.873002E-01 +7.141490E-03 +7.076637E+00 +1.017946E+01 +8.323139E-02 +1.408687E-03 +2.025710E-01 +8.344398E-03 +2.095897E+01 +8.825741E+01 +3.236513E-02 +2.104220E-04 +8.008525E-02 +1.288373E-03 +1.358006E+01 +3.689205E+01 +1.862562E-01 +6.941428E-03 +5.180621E-01 +5.370209E-02 +5.067386E+00 +5.141748E+00 +1.912704E+00 +7.328484E-01 +4.655140E+00 +4.340941E+00 +2.858992E+00 +1.637705E+00 +4.331050E-01 +3.759875E-02 +1.054091E+00 +2.227118E-01 +1.692974E+00 +5.796396E-01 +5.560487E-02 +6.246120E-04 +1.353311E-01 +3.699815E-03 +2.368737E+00 +1.143184E+00 +7.950085E-02 +1.286135E-03 +1.934892E-01 +7.618268E-03 +7.119767E+00 +1.030095E+01 +8.427627E-02 +1.442764E-03 +2.051141E-01 +8.546249E-03 +2.047651E+01 +8.426275E+01 +3.183786E-02 +2.037156E-04 +7.878057E-02 +1.247311E-03 +1.326415E+01 +3.519010E+01 +1.833688E-01 +6.726832E-03 +5.100312E-01 +5.204188E-02 diff --git a/tests/regression_tests/random_ray_k_eff_mesh/test.py b/tests/regression_tests/random_ray_k_eff_mesh/test.py new file mode 100644 index 000000000..cffdaf8bb --- /dev/null +++ b/tests/regression_tests/random_ray_k_eff_mesh/test.py @@ -0,0 +1,36 @@ +import os + +import openmc +from openmc.examples import random_ray_lattice + +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_k_eff_mesh(): + model = random_ray_lattice() + + # The model already has some geometrical subdivisions + # up to a 10x10 grid in the moderator region. So, we + # increase the resolution 40x40 applied over the full + # 2x2 lattice. + pitch = 1.26 + dim = 40 + mesh = openmc.RegularMesh() + mesh.dimension = (dim, dim) + mesh.lower_left = (-pitch, -pitch) + mesh.upper_right = (pitch, pitch) + + root = model.geometry.root_universe + + model.settings.random_ray['source_region_meshes'] = [(mesh, [root])] + + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/weightwindows_fw_cadis_mesh/__init__.py b/tests/regression_tests/weightwindows_fw_cadis_mesh/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/inputs_true.dat b/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/inputs_true.dat new file mode 100644 index 000000000..ffb2ac112 --- /dev/null +++ b/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/inputs_true.dat @@ -0,0 +1,265 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 750 + 30 + 20 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + + 1 + neutron + 10 + 1 + true + fw_cadis + + + + 15 15 15 + 0.0 0.0 0.0 + 30.0 30.0 30.0 + + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + + + + + + flat + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/results_true.dat b/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/results_true.dat new file mode 100644 index 000000000..e14b7e199 --- /dev/null +++ b/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/results_true.dat @@ -0,0 +1,6760 @@ +RegularMesh + ID = 1 + Name = + Dimensions = 3 + Voxels = [15 15 15] + Lower left = [0. 0. 0.] + Upper Right = [np.float64(30.0), np.float64(30.0), np.float64(30.0)] + Width = [2. 2. 2.] +Lower Bounds +1.71e-01 +1.73e-01 +1.72e-01 +1.73e-01 +1.71e-01 +1.63e-01 +1.66e-01 +1.70e-01 +1.68e-01 +1.76e-01 +1.73e-01 +1.79e-01 +2.62e-01 +4.44e-01 +1.68e-01 +1.63e-01 +1.76e-01 +1.73e-01 +1.52e-01 +1.66e-01 +1.72e-01 +1.68e-01 +1.68e-01 +1.66e-01 +1.67e-01 +1.76e-01 +1.73e-01 +2.44e-01 +3.63e-01 +1.23e-01 +1.71e-01 +1.66e-01 +1.69e-01 +1.71e-01 +1.67e-01 +1.71e-01 +1.64e-01 +1.56e-01 +1.67e-01 +1.64e-01 +1.68e-01 +1.82e-01 +2.60e-01 +4.13e-01 +1.55e-01 +1.68e-01 +1.62e-01 +1.66e-01 +1.65e-01 +1.62e-01 +1.64e-01 +1.63e-01 +1.66e-01 +1.63e-01 +1.67e-01 +1.58e-01 +1.79e-01 +2.52e-01 +4.10e-01 +1.35e-01 +1.65e-01 +1.74e-01 +1.67e-01 +1.71e-01 +1.66e-01 +1.62e-01 +1.60e-01 +1.66e-01 +1.55e-01 +1.67e-01 +1.66e-01 +1.82e-01 +2.68e-01 +3.46e-01 +1.32e-01 +1.70e-01 +1.70e-01 +1.68e-01 +1.68e-01 +1.69e-01 +1.59e-01 +1.62e-01 +1.59e-01 +1.57e-01 +1.55e-01 +1.72e-01 +1.80e-01 +2.59e-01 +2.97e-01 +1.07e-01 +1.68e-01 +1.60e-01 +1.58e-01 +1.61e-01 +1.58e-01 +1.59e-01 +1.56e-01 +1.53e-01 +1.57e-01 +1.52e-01 +1.59e-01 +1.54e-01 +2.42e-01 +3.30e-01 +1.22e-01 +1.64e-01 +1.65e-01 +1.69e-01 +1.64e-01 +1.60e-01 +1.66e-01 +1.60e-01 +1.52e-01 +1.45e-01 +1.53e-01 +1.57e-01 +1.40e-01 +2.08e-01 +1.90e-01 +7.93e-02 +1.65e-01 +1.65e-01 +1.71e-01 +1.72e-01 +1.63e-01 +1.61e-01 +1.57e-01 +1.54e-01 +1.49e-01 +1.41e-01 +1.45e-01 +1.53e-01 +2.36e-01 +1.80e-01 +5.76e-02 +1.67e-01 +1.75e-01 +1.74e-01 +1.71e-01 +1.66e-01 +1.54e-01 +1.55e-01 +1.52e-01 +1.53e-01 +1.41e-01 +1.53e-01 +1.49e-01 +2.14e-01 +1.76e-01 +6.71e-02 +1.72e-01 +1.66e-01 +1.74e-01 +1.70e-01 +1.69e-01 +1.57e-01 +1.50e-01 +1.53e-01 +1.48e-01 +1.47e-01 +1.62e-01 +1.49e-01 +2.03e-01 +1.35e-01 +4.07e-02 +1.81e-01 +1.80e-01 +1.72e-01 +1.65e-01 +1.84e-01 +1.74e-01 +1.58e-01 +1.68e-01 +1.59e-01 +1.57e-01 +1.58e-01 +1.48e-01 +2.16e-01 +1.05e-01 +3.37e-02 +2.61e-01 +2.52e-01 +2.37e-01 +2.51e-01 +2.64e-01 +2.43e-01 +2.39e-01 +2.32e-01 +2.32e-01 +2.29e-01 +2.22e-01 +2.19e-01 +2.07e-01 +8.02e-02 +2.21e-02 +5.00e-01 +3.68e-01 +3.88e-01 +2.65e-01 +2.86e-01 +3.17e-01 +2.60e-01 +2.39e-01 +1.88e-01 +1.97e-01 +1.84e-01 +1.50e-01 +9.99e-02 +3.88e-02 +1.90e-02 +2.18e-01 +1.48e-01 +1.61e-01 +8.76e-02 +9.43e-02 +1.16e-01 +8.45e-02 +8.13e-02 +7.00e-02 +6.16e-02 +6.49e-02 +4.46e-02 +3.71e-02 +1.62e-02 +7.33e-03 +1.70e-01 +1.69e-01 +1.70e-01 +1.68e-01 +1.63e-01 +1.59e-01 +1.75e-01 +1.69e-01 +1.59e-01 +1.64e-01 +1.77e-01 +1.79e-01 +2.63e-01 +3.92e-01 +1.48e-01 +1.73e-01 +1.74e-01 +1.70e-01 +1.66e-01 +1.68e-01 +1.62e-01 +1.62e-01 +1.67e-01 +1.60e-01 +1.64e-01 +1.67e-01 +1.80e-01 +2.42e-01 +3.72e-01 +1.45e-01 +1.71e-01 +1.65e-01 +1.67e-01 +1.65e-01 +1.69e-01 +1.70e-01 +1.64e-01 +1.68e-01 +1.60e-01 +1.70e-01 +1.70e-01 +1.74e-01 +2.62e-01 +3.29e-01 +1.15e-01 +1.73e-01 +1.75e-01 +1.70e-01 +1.69e-01 +1.69e-01 +1.63e-01 +1.66e-01 +1.63e-01 +1.61e-01 +1.61e-01 +1.70e-01 +1.79e-01 +2.47e-01 +3.64e-01 +1.32e-01 +1.72e-01 +1.73e-01 +1.71e-01 +1.66e-01 +1.63e-01 +1.63e-01 +1.67e-01 +1.60e-01 +1.56e-01 +1.63e-01 +1.72e-01 +1.71e-01 +2.52e-01 +3.43e-01 +1.40e-01 +1.68e-01 +1.70e-01 +1.67e-01 +1.66e-01 +1.72e-01 +1.61e-01 +1.56e-01 +1.56e-01 +1.53e-01 +1.55e-01 +1.65e-01 +1.78e-01 +2.53e-01 +3.66e-01 +1.33e-01 +1.70e-01 +1.69e-01 +1.60e-01 +1.64e-01 +1.70e-01 +1.63e-01 +1.53e-01 +1.50e-01 +1.50e-01 +1.48e-01 +1.53e-01 +1.66e-01 +2.34e-01 +2.99e-01 +1.15e-01 +1.65e-01 +1.63e-01 +1.59e-01 +1.58e-01 +1.59e-01 +1.54e-01 +1.54e-01 +1.47e-01 +1.40e-01 +1.38e-01 +1.27e-01 +1.22e-01 +1.99e-01 +1.77e-01 +7.13e-02 +1.58e-01 +1.75e-01 +1.68e-01 +1.66e-01 +1.66e-01 +1.58e-01 +1.56e-01 +1.44e-01 +1.48e-01 +1.45e-01 +1.37e-01 +1.40e-01 +2.01e-01 +1.36e-01 +4.17e-02 +1.65e-01 +1.76e-01 +1.69e-01 +1.69e-01 +1.57e-01 +1.67e-01 +1.56e-01 +1.50e-01 +1.53e-01 +1.43e-01 +1.51e-01 +1.41e-01 +2.04e-01 +1.50e-01 +4.51e-02 +1.69e-01 +1.75e-01 +1.71e-01 +1.63e-01 +1.69e-01 +1.67e-01 +1.60e-01 +1.50e-01 +1.53e-01 +1.44e-01 +1.57e-01 +1.49e-01 +2.02e-01 +1.35e-01 +4.08e-02 +1.79e-01 +1.72e-01 +1.73e-01 +1.70e-01 +1.74e-01 +1.78e-01 +1.68e-01 +1.59e-01 +1.57e-01 +1.62e-01 +1.62e-01 +1.57e-01 +1.95e-01 +9.20e-02 +3.15e-02 +2.48e-01 +2.65e-01 +2.54e-01 +2.58e-01 +2.81e-01 +2.53e-01 +2.33e-01 +2.40e-01 +2.16e-01 +2.19e-01 +2.43e-01 +2.22e-01 +1.99e-01 +6.18e-02 +1.77e-02 +3.13e-01 +2.98e-01 +2.98e-01 +3.16e-01 +3.67e-01 +3.51e-01 +2.81e-01 +2.76e-01 +2.24e-01 +1.66e-01 +1.70e-01 +1.65e-01 +1.10e-01 +3.67e-02 +1.54e-02 +1.11e-01 +1.10e-01 +1.18e-01 +1.08e-01 +1.34e-01 +1.41e-01 +9.92e-02 +9.96e-02 +9.39e-02 +5.19e-02 +5.40e-02 +5.45e-02 +4.22e-02 +1.88e-02 +7.33e-03 +1.73e-01 +1.68e-01 +1.74e-01 +1.68e-01 +1.60e-01 +1.60e-01 +1.61e-01 +1.59e-01 +1.70e-01 +1.61e-01 +1.62e-01 +1.76e-01 +2.90e-01 +4.08e-01 +1.58e-01 +1.72e-01 +1.68e-01 +1.62e-01 +1.66e-01 +1.59e-01 +1.63e-01 +1.58e-01 +1.60e-01 +1.59e-01 +1.65e-01 +1.63e-01 +1.77e-01 +2.45e-01 +3.92e-01 +1.64e-01 +1.71e-01 +1.74e-01 +1.72e-01 +1.57e-01 +1.65e-01 +1.63e-01 +1.63e-01 +1.69e-01 +1.58e-01 +1.61e-01 +1.61e-01 +1.77e-01 +2.45e-01 +3.36e-01 +1.29e-01 +1.57e-01 +1.68e-01 +1.66e-01 +1.63e-01 +1.63e-01 +1.59e-01 +1.61e-01 +1.65e-01 +1.56e-01 +1.59e-01 +1.65e-01 +1.60e-01 +2.43e-01 +3.30e-01 +1.08e-01 +1.62e-01 +1.62e-01 +1.61e-01 +1.54e-01 +1.63e-01 +1.64e-01 +1.50e-01 +1.57e-01 +1.51e-01 +1.50e-01 +1.67e-01 +1.72e-01 +2.44e-01 +2.79e-01 +8.59e-02 +1.61e-01 +1.64e-01 +1.57e-01 +1.55e-01 +1.57e-01 +1.57e-01 +1.51e-01 +1.49e-01 +1.54e-01 +1.56e-01 +1.67e-01 +1.70e-01 +2.42e-01 +3.48e-01 +1.32e-01 +1.67e-01 +1.73e-01 +1.64e-01 +1.62e-01 +1.60e-01 +1.61e-01 +1.56e-01 +1.47e-01 +1.51e-01 +1.54e-01 +1.57e-01 +1.59e-01 +2.26e-01 +2.53e-01 +9.32e-02 +1.63e-01 +1.63e-01 +1.60e-01 +1.55e-01 +1.57e-01 +1.58e-01 +1.54e-01 +1.47e-01 +1.46e-01 +1.48e-01 +1.43e-01 +1.50e-01 +2.09e-01 +1.68e-01 +6.28e-02 +1.57e-01 +1.64e-01 +1.64e-01 +1.60e-01 +1.60e-01 +1.58e-01 +1.58e-01 +1.57e-01 +1.47e-01 +1.50e-01 +1.52e-01 +1.43e-01 +2.11e-01 +1.66e-01 +6.17e-02 +1.65e-01 +1.72e-01 +1.62e-01 +1.64e-01 +1.66e-01 +1.57e-01 +1.57e-01 +1.50e-01 +1.40e-01 +1.48e-01 +1.46e-01 +1.44e-01 +1.95e-01 +1.25e-01 +4.98e-02 +1.61e-01 +1.66e-01 +1.70e-01 +1.64e-01 +1.67e-01 +1.67e-01 +1.68e-01 +1.58e-01 +1.43e-01 +1.45e-01 +1.46e-01 +1.43e-01 +1.94e-01 +1.25e-01 +3.60e-02 +1.61e-01 +1.74e-01 +1.74e-01 +1.76e-01 +1.73e-01 +1.72e-01 +1.68e-01 +1.61e-01 +1.59e-01 +1.56e-01 +1.59e-01 +1.57e-01 +1.77e-01 +1.09e-01 +3.74e-02 +2.48e-01 +2.48e-01 +2.49e-01 +2.42e-01 +2.47e-01 +2.55e-01 +2.19e-01 +2.25e-01 +2.25e-01 +2.17e-01 +2.21e-01 +2.26e-01 +2.15e-01 +6.24e-02 +1.90e-02 +3.68e-01 +2.72e-01 +3.41e-01 +3.58e-01 +3.64e-01 +2.32e-01 +2.34e-01 +2.20e-01 +1.88e-01 +1.78e-01 +1.34e-01 +1.25e-01 +1.07e-01 +4.68e-02 +1.29e-02 +1.29e-01 +8.63e-02 +1.42e-01 +1.36e-01 +1.37e-01 +8.94e-02 +8.09e-02 +8.37e-02 +5.69e-02 +6.46e-02 +3.15e-02 +3.86e-02 +3.51e-02 +2.57e-02 +1.29e-02 +1.69e-01 +1.62e-01 +1.66e-01 +1.70e-01 +1.69e-01 +1.61e-01 +1.58e-01 +1.51e-01 +1.52e-01 +1.53e-01 +1.52e-01 +1.73e-01 +2.54e-01 +4.12e-01 +1.51e-01 +1.68e-01 +1.75e-01 +1.65e-01 +1.59e-01 +1.62e-01 +1.56e-01 +1.57e-01 +1.55e-01 +1.59e-01 +1.66e-01 +1.59e-01 +1.67e-01 +2.35e-01 +3.25e-01 +1.29e-01 +1.62e-01 +1.64e-01 +1.68e-01 +1.59e-01 +1.51e-01 +1.59e-01 +1.53e-01 +1.53e-01 +1.55e-01 +1.63e-01 +1.60e-01 +1.65e-01 +2.33e-01 +2.78e-01 +1.07e-01 +1.62e-01 +1.63e-01 +1.60e-01 +1.61e-01 +1.54e-01 +1.57e-01 +1.59e-01 +1.51e-01 +1.51e-01 +1.58e-01 +1.54e-01 +1.61e-01 +2.39e-01 +2.48e-01 +1.01e-01 +1.55e-01 +1.54e-01 +1.61e-01 +1.66e-01 +1.55e-01 +1.53e-01 +1.56e-01 +1.51e-01 +1.48e-01 +1.53e-01 +1.63e-01 +1.62e-01 +2.42e-01 +3.21e-01 +1.18e-01 +1.66e-01 +1.52e-01 +1.62e-01 +1.62e-01 +1.56e-01 +1.52e-01 +1.55e-01 +1.57e-01 +1.55e-01 +1.53e-01 +1.53e-01 +1.64e-01 +2.46e-01 +3.03e-01 +1.11e-01 +1.63e-01 +1.59e-01 +1.68e-01 +1.56e-01 +1.52e-01 +1.55e-01 +1.56e-01 +1.49e-01 +1.49e-01 +1.51e-01 +1.45e-01 +1.49e-01 +2.22e-01 +2.88e-01 +1.02e-01 +1.66e-01 +1.62e-01 +1.61e-01 +1.50e-01 +1.50e-01 +1.51e-01 +1.59e-01 +1.46e-01 +1.43e-01 +1.42e-01 +1.41e-01 +1.51e-01 +2.22e-01 +1.88e-01 +6.51e-02 +1.60e-01 +1.59e-01 +1.57e-01 +1.61e-01 +1.55e-01 +1.57e-01 +1.53e-01 +1.53e-01 +1.45e-01 +1.44e-01 +1.47e-01 +1.59e-01 +2.35e-01 +1.91e-01 +5.69e-02 +1.62e-01 +1.59e-01 +1.63e-01 +1.59e-01 +1.55e-01 +1.54e-01 +1.50e-01 +1.47e-01 +1.50e-01 +1.46e-01 +1.43e-01 +1.43e-01 +2.21e-01 +1.37e-01 +5.36e-02 +1.66e-01 +1.65e-01 +1.50e-01 +1.68e-01 +1.65e-01 +1.61e-01 +1.55e-01 +1.49e-01 +1.47e-01 +1.51e-01 +1.43e-01 +1.50e-01 +1.92e-01 +9.97e-02 +2.93e-02 +1.73e-01 +1.65e-01 +1.70e-01 +7.83e-02 +1.73e-01 +1.78e-01 +1.65e-01 +1.50e-01 +1.60e-01 +1.55e-01 +1.57e-01 +1.55e-01 +1.89e-01 +8.91e-02 +2.68e-02 +2.32e-01 +2.36e-01 +2.40e-01 +2.31e-01 +2.41e-01 +2.39e-01 +2.01e-01 +2.12e-01 +2.21e-01 +2.12e-01 +2.04e-01 +2.02e-01 +2.14e-01 +8.26e-02 +2.65e-02 +3.07e-01 +2.51e-01 +2.10e-01 +2.95e-01 +2.86e-01 +2.41e-01 +1.69e-01 +1.21e-01 +1.92e-01 +1.82e-01 +1.29e-01 +1.24e-01 +1.04e-01 +4.53e-02 +1.74e-02 +1.16e-01 +8.31e-02 +8.94e-02 +9.43e-02 +1.16e-01 +9.01e-02 +7.46e-02 +3.83e-02 +6.35e-02 +6.43e-02 +4.23e-02 +4.16e-02 +3.17e-02 +2.60e-02 +1.26e-02 +1.63e-01 +1.58e-01 +1.60e-01 +1.62e-01 +1.60e-01 +1.59e-01 +1.60e-01 +1.51e-01 +1.47e-01 +1.55e-01 +1.51e-01 +1.68e-01 +2.47e-01 +3.18e-01 +1.27e-01 +1.64e-01 +1.61e-01 +1.56e-01 +1.63e-01 +1.63e-01 +1.54e-01 +1.50e-01 +1.49e-01 +1.56e-01 +1.58e-01 +1.58e-01 +1.64e-01 +2.39e-01 +3.04e-01 +1.13e-01 +1.65e-01 +1.63e-01 +1.62e-01 +1.57e-01 +1.57e-01 +1.57e-01 +1.53e-01 +1.49e-01 +1.57e-01 +1.63e-01 +1.63e-01 +1.70e-01 +2.39e-01 +2.29e-01 +8.46e-02 +1.62e-01 +1.63e-01 +1.58e-01 +1.58e-01 +1.56e-01 +1.55e-01 +1.50e-01 +1.51e-01 +1.52e-01 +1.60e-01 +1.57e-01 +1.64e-01 +2.46e-01 +2.16e-01 +7.36e-02 +1.56e-01 +1.54e-01 +1.56e-01 +1.62e-01 +1.52e-01 +1.50e-01 +1.55e-01 +1.52e-01 +1.52e-01 +1.50e-01 +1.58e-01 +1.67e-01 +2.47e-01 +3.21e-01 +1.15e-01 +1.57e-01 +1.55e-01 +1.56e-01 +1.58e-01 +1.48e-01 +1.43e-01 +1.42e-01 +1.56e-01 +1.49e-01 +1.52e-01 +1.51e-01 +1.58e-01 +2.31e-01 +2.63e-01 +9.53e-02 +1.48e-01 +1.56e-01 +1.59e-01 +1.52e-01 +1.60e-01 +1.43e-01 +1.43e-01 +1.44e-01 +1.45e-01 +1.43e-01 +1.48e-01 +1.51e-01 +2.09e-01 +2.10e-01 +7.84e-02 +1.57e-01 +1.60e-01 +1.55e-01 +1.53e-01 +1.57e-01 +1.44e-01 +1.44e-01 +1.36e-01 +1.36e-01 +1.46e-01 +1.43e-01 +1.46e-01 +2.17e-01 +1.67e-01 +7.08e-02 +1.59e-01 +1.56e-01 +1.56e-01 +1.54e-01 +1.53e-01 +1.52e-01 +1.50e-01 +1.42e-01 +1.37e-01 +1.39e-01 +1.41e-01 +1.47e-01 +2.03e-01 +1.75e-01 +5.51e-02 +1.63e-01 +1.62e-01 +1.57e-01 +1.53e-01 +1.45e-01 +1.52e-01 +1.52e-01 +1.43e-01 +1.41e-01 +1.38e-01 +1.39e-01 +1.52e-01 +2.05e-01 +1.61e-01 +6.97e-02 +1.62e-01 +1.56e-01 +1.58e-01 +1.55e-01 +1.58e-01 +1.56e-01 +1.53e-01 +1.53e-01 +1.49e-01 +1.41e-01 +1.44e-01 +1.57e-01 +2.11e-01 +1.22e-01 +4.27e-02 +1.66e-01 +1.69e-01 +1.61e-01 +1.62e-01 +1.68e-01 +1.74e-01 +1.60e-01 +1.57e-01 +1.57e-01 +1.62e-01 +1.48e-01 +1.54e-01 +2.17e-01 +1.14e-01 +3.49e-02 +2.43e-01 +2.30e-01 +2.33e-01 +2.43e-01 +2.46e-01 +2.48e-01 +2.46e-01 +2.21e-01 +2.22e-01 +2.12e-01 +2.00e-01 +2.02e-01 +2.14e-01 +8.94e-02 +3.01e-02 +2.79e-01 +2.63e-01 +2.52e-01 +2.63e-01 +3.00e-01 +2.68e-01 +2.93e-01 +1.90e-01 +2.03e-01 +1.54e-01 +1.13e-01 +1.27e-01 +8.82e-02 +3.87e-02 +1.55e-02 +1.03e-01 +9.09e-02 +9.09e-02 +8.99e-02 +1.07e-01 +8.75e-02 +1.17e-01 +5.68e-02 +7.16e-02 +5.22e-02 +3.31e-02 +3.49e-02 +2.69e-02 +1.66e-02 +1.14e-02 +1.46e-01 +1.55e-01 +1.57e-01 +1.59e-01 +1.63e-01 +1.54e-01 +1.49e-01 +1.48e-01 +1.45e-01 +1.48e-01 +1.48e-01 +1.54e-01 +2.45e-01 +2.69e-01 +8.34e-02 +1.54e-01 +1.59e-01 +1.57e-01 +1.51e-01 +1.55e-01 +1.57e-01 +1.60e-01 +1.49e-01 +1.54e-01 +1.50e-01 +1.54e-01 +1.62e-01 +2.41e-01 +2.97e-01 +1.15e-01 +1.60e-01 +1.61e-01 +1.61e-01 +1.53e-01 +1.56e-01 +1.52e-01 +1.53e-01 +1.57e-01 +1.52e-01 +1.52e-01 +1.55e-01 +1.69e-01 +2.49e-01 +2.52e-01 +8.15e-02 +1.59e-01 +1.59e-01 +1.52e-01 +1.50e-01 +1.53e-01 +1.59e-01 +1.47e-01 +1.51e-01 +1.51e-01 +1.56e-01 +1.56e-01 +1.64e-01 +2.40e-01 +2.46e-01 +8.06e-02 +1.58e-01 +1.59e-01 +1.58e-01 +1.56e-01 +1.53e-01 +1.46e-01 +1.48e-01 +1.48e-01 +1.47e-01 +1.53e-01 +1.48e-01 +1.62e-01 +2.39e-01 +2.66e-01 +1.02e-01 +1.55e-01 +1.54e-01 +1.57e-01 +1.52e-01 +1.48e-01 +1.50e-01 +1.40e-01 +1.37e-01 +1.50e-01 +1.50e-01 +1.46e-01 +1.52e-01 +2.23e-01 +2.22e-01 +8.99e-02 +1.58e-01 +1.60e-01 +1.54e-01 +1.47e-01 +1.47e-01 +1.45e-01 +1.42e-01 +1.28e-01 +1.33e-01 +1.40e-01 +1.51e-01 +1.53e-01 +2.08e-01 +1.44e-01 +5.19e-02 +1.57e-01 +1.54e-01 +1.54e-01 +1.42e-01 +1.51e-01 +1.44e-01 +1.35e-01 +1.28e-01 +1.11e-01 +1.40e-01 +1.49e-01 +1.55e-01 +2.22e-01 +1.31e-01 +4.13e-02 +1.56e-01 +1.53e-01 +1.57e-01 +1.42e-01 +1.52e-01 +1.49e-01 +1.45e-01 +1.32e-01 +1.30e-01 +1.27e-01 +1.41e-01 +1.41e-01 +2.11e-01 +1.55e-01 +4.43e-02 +1.49e-01 +1.52e-01 +1.53e-01 +1.56e-01 +1.47e-01 +1.45e-01 +1.40e-01 +1.37e-01 +1.43e-01 +1.35e-01 +1.34e-01 +1.42e-01 +1.98e-01 +1.25e-01 +3.99e-02 +1.57e-01 +1.48e-01 +1.49e-01 +1.52e-01 +1.50e-01 +1.48e-01 +1.50e-01 +1.48e-01 +1.44e-01 +1.43e-01 +1.35e-01 +1.39e-01 +1.94e-01 +1.07e-01 +3.11e-02 +1.62e-01 +1.61e-01 +1.61e-01 +1.63e-01 +1.63e-01 +1.60e-01 +1.55e-01 +1.55e-01 +1.50e-01 +1.56e-01 +1.43e-01 +1.32e-01 +1.83e-01 +1.02e-01 +2.51e-02 +2.34e-01 +2.25e-01 +2.20e-01 +2.27e-01 +2.41e-01 +2.35e-01 +2.13e-01 +2.04e-01 +2.13e-01 +2.16e-01 +2.20e-01 +1.80e-01 +1.76e-01 +9.68e-02 +3.54e-02 +3.12e-01 +2.64e-01 +2.26e-01 +2.68e-01 +2.60e-01 +2.43e-01 +2.35e-01 +1.81e-01 +1.60e-01 +1.47e-01 +1.51e-01 +1.34e-01 +7.35e-02 +4.05e-02 +2.19e-02 +1.06e-01 +9.90e-02 +8.77e-02 +9.61e-02 +9.91e-02 +7.56e-02 +1.05e-01 +6.20e-02 +5.67e-02 +3.97e-02 +4.31e-02 +4.78e-02 +2.87e-02 +1.59e-02 +8.92e-03 +1.61e-01 +1.59e-01 +1.57e-01 +1.59e-01 +1.51e-01 +1.49e-01 +1.54e-01 +1.51e-01 +1.53e-01 +1.49e-01 +1.52e-01 +1.48e-01 +2.27e-01 +1.82e-01 +8.22e-02 +1.61e-01 +1.54e-01 +1.57e-01 +1.56e-01 +1.53e-01 +1.49e-01 +1.46e-01 +1.41e-01 +1.51e-01 +1.51e-01 +1.52e-01 +1.52e-01 +2.22e-01 +2.08e-01 +7.81e-02 +1.54e-01 +1.58e-01 +1.52e-01 +1.53e-01 +1.48e-01 +1.49e-01 +1.45e-01 +1.43e-01 +1.44e-01 +1.48e-01 +1.58e-01 +1.55e-01 +2.25e-01 +2.48e-01 +1.01e-01 +1.57e-01 +1.48e-01 +1.51e-01 +1.47e-01 +1.46e-01 +1.52e-01 +1.48e-01 +1.47e-01 +1.46e-01 +1.48e-01 +1.49e-01 +1.57e-01 +2.21e-01 +1.69e-01 +6.52e-02 +1.57e-01 +1.55e-01 +1.55e-01 +1.53e-01 +1.51e-01 +1.55e-01 +1.46e-01 +1.49e-01 +1.42e-01 +1.45e-01 +1.45e-01 +1.57e-01 +2.19e-01 +1.36e-01 +4.96e-02 +1.57e-01 +1.50e-01 +1.53e-01 +1.57e-01 +1.54e-01 +1.50e-01 +1.45e-01 +1.40e-01 +1.30e-01 +1.38e-01 +1.42e-01 +1.51e-01 +2.13e-01 +1.41e-01 +4.31e-02 +1.56e-01 +1.57e-01 +1.53e-01 +1.52e-01 +1.51e-01 +1.44e-01 +1.42e-01 +1.28e-01 +1.21e-01 +1.26e-01 +1.40e-01 +1.53e-01 +2.16e-01 +1.61e-01 +4.81e-02 +1.48e-01 +1.48e-01 +1.47e-01 +1.49e-01 +1.49e-01 +1.43e-01 +1.40e-01 +1.23e-01 +8.97e-02 +1.34e-01 +1.43e-01 +1.48e-01 +2.10e-01 +1.68e-01 +5.02e-02 +1.53e-01 +7.09e-02 +1.47e-01 +1.48e-01 +1.43e-01 +1.46e-01 +6.84e-02 +1.27e-01 +1.24e-01 +1.31e-01 +1.42e-01 +1.48e-01 +2.01e-01 +1.56e-01 +5.15e-02 +1.50e-01 +1.52e-01 +1.52e-01 +1.47e-01 +1.42e-01 +1.35e-01 +1.39e-01 +1.38e-01 +1.37e-01 +1.35e-01 +1.36e-01 +1.36e-01 +1.92e-01 +1.22e-01 +4.76e-02 +1.52e-01 +1.61e-01 +1.49e-01 +1.45e-01 +1.40e-01 +1.40e-01 +1.34e-01 +1.35e-01 +1.38e-01 +1.38e-01 +1.27e-01 +1.35e-01 +1.77e-01 +7.50e-02 +2.64e-02 +1.58e-01 +1.56e-01 +1.50e-01 +1.45e-01 +1.50e-01 +1.50e-01 +1.42e-01 +1.42e-01 +1.52e-01 +1.46e-01 +1.38e-01 +1.30e-01 +1.44e-01 +6.83e-02 +2.86e-02 +2.28e-01 +2.11e-01 +2.08e-01 +2.12e-01 +2.17e-01 +2.09e-01 +2.06e-01 +1.89e-01 +2.08e-01 +1.88e-01 +1.88e-01 +1.66e-01 +1.33e-01 +4.13e-02 +1.70e-02 +2.67e-01 +2.70e-01 +2.31e-01 +1.72e-01 +1.66e-01 +2.16e-01 +1.96e-01 +1.52e-01 +1.41e-01 +1.31e-01 +1.44e-01 +1.17e-01 +4.82e-02 +2.09e-02 +8.63e-03 +9.47e-02 +1.04e-01 +9.02e-02 +5.55e-02 +5.39e-02 +6.95e-02 +7.10e-02 +4.92e-02 +4.74e-02 +3.52e-02 +4.69e-02 +4.03e-02 +1.69e-02 +9.25e-03 +5.98e-03 +1.46e-01 +1.50e-01 +1.48e-01 +1.53e-01 +1.56e-01 +1.57e-01 +1.56e-01 +1.54e-01 +1.44e-01 +1.42e-01 +1.44e-01 +1.52e-01 +2.10e-01 +1.56e-01 +5.53e-02 +1.50e-01 +1.57e-01 +1.57e-01 +1.57e-01 +1.54e-01 +1.51e-01 +1.52e-01 +1.42e-01 +1.42e-01 +1.39e-01 +1.45e-01 +1.50e-01 +2.11e-01 +1.18e-01 +3.92e-02 +1.49e-01 +1.48e-01 +1.55e-01 +1.54e-01 +1.51e-01 +1.50e-01 +1.48e-01 +1.48e-01 +1.41e-01 +1.38e-01 +1.45e-01 +1.54e-01 +2.08e-01 +1.62e-01 +5.89e-02 +1.48e-01 +1.49e-01 +1.51e-01 +1.50e-01 +1.51e-01 +1.49e-01 +1.48e-01 +1.47e-01 +1.34e-01 +1.33e-01 +1.53e-01 +1.54e-01 +1.96e-01 +1.55e-01 +6.15e-02 +1.51e-01 +1.53e-01 +1.59e-01 +1.48e-01 +1.51e-01 +1.48e-01 +1.45e-01 +1.41e-01 +1.38e-01 +1.37e-01 +1.49e-01 +1.46e-01 +2.06e-01 +1.25e-01 +3.47e-02 +1.51e-01 +1.47e-01 +1.52e-01 +1.53e-01 +1.44e-01 +1.40e-01 +1.45e-01 +1.38e-01 +1.39e-01 +1.34e-01 +1.36e-01 +1.50e-01 +1.96e-01 +1.29e-01 +4.48e-02 +1.49e-01 +1.43e-01 +1.49e-01 +1.46e-01 +1.48e-01 +1.42e-01 +1.35e-01 +1.27e-01 +1.24e-01 +1.26e-01 +1.29e-01 +1.38e-01 +1.87e-01 +1.53e-01 +5.84e-02 +1.46e-01 +1.51e-01 +1.44e-01 +1.45e-01 +1.45e-01 +1.41e-01 +1.31e-01 +1.24e-01 +1.29e-01 +1.30e-01 +1.43e-01 +1.42e-01 +1.91e-01 +1.36e-01 +5.08e-02 +1.39e-01 +1.40e-01 +1.43e-01 +1.39e-01 +1.43e-01 +1.39e-01 +1.35e-01 +1.27e-01 +1.23e-01 +1.22e-01 +1.41e-01 +1.37e-01 +1.86e-01 +1.09e-01 +3.41e-02 +1.44e-01 +1.41e-01 +1.44e-01 +1.37e-01 +1.37e-01 +1.39e-01 +1.29e-01 +1.33e-01 +1.20e-01 +1.18e-01 +1.29e-01 +1.36e-01 +1.94e-01 +1.23e-01 +3.96e-02 +1.50e-01 +1.47e-01 +1.48e-01 +1.39e-01 +1.35e-01 +1.36e-01 +1.34e-01 +1.40e-01 +1.32e-01 +1.27e-01 +1.23e-01 +1.26e-01 +1.61e-01 +8.34e-02 +2.18e-02 +1.58e-01 +1.51e-01 +1.50e-01 +1.46e-01 +1.45e-01 +1.35e-01 +1.30e-01 +1.41e-01 +1.39e-01 +1.36e-01 +1.32e-01 +1.22e-01 +1.45e-01 +5.48e-02 +1.56e-02 +2.28e-01 +2.05e-01 +2.09e-01 +1.89e-01 +2.03e-01 +1.97e-01 +1.63e-01 +1.67e-01 +1.68e-01 +1.92e-01 +1.83e-01 +1.55e-01 +1.14e-01 +3.43e-02 +1.15e-02 +2.24e-01 +2.31e-01 +1.67e-01 +1.43e-01 +1.53e-01 +1.76e-01 +1.13e-01 +9.77e-02 +8.62e-02 +1.49e-01 +1.29e-01 +8.35e-02 +4.44e-02 +1.21e-02 +4.84e-03 +7.75e-02 +7.83e-02 +5.37e-02 +5.23e-02 +4.63e-02 +6.06e-02 +4.19e-02 +3.16e-02 +2.72e-02 +5.09e-02 +4.06e-02 +2.76e-02 +1.94e-02 +8.81e-03 +2.34e-03 +1.54e-01 +1.58e-01 +1.62e-01 +1.48e-01 +1.57e-01 +1.52e-01 +1.49e-01 +1.54e-01 +1.42e-01 +1.36e-01 +1.36e-01 +1.50e-01 +2.08e-01 +1.23e-01 +3.56e-02 +1.51e-01 +1.53e-01 +1.59e-01 +1.57e-01 +1.46e-01 +1.55e-01 +1.50e-01 +1.49e-01 +1.41e-01 +1.41e-01 +1.42e-01 +1.48e-01 +2.00e-01 +1.32e-01 +4.13e-02 +1.48e-01 +1.50e-01 +1.53e-01 +1.52e-01 +1.54e-01 +1.42e-01 +1.48e-01 +1.50e-01 +1.40e-01 +1.37e-01 +1.38e-01 +1.53e-01 +2.20e-01 +1.65e-01 +5.51e-02 +1.52e-01 +1.44e-01 +1.49e-01 +1.46e-01 +1.48e-01 +1.48e-01 +1.45e-01 +1.46e-01 +1.38e-01 +1.40e-01 +1.43e-01 +1.39e-01 +1.95e-01 +1.78e-01 +7.38e-02 +1.48e-01 +1.45e-01 +1.44e-01 +1.47e-01 +1.46e-01 +1.44e-01 +1.42e-01 +1.45e-01 +1.43e-01 +1.37e-01 +1.43e-01 +1.49e-01 +1.94e-01 +1.37e-01 +4.11e-02 +1.52e-01 +1.48e-01 +1.45e-01 +1.38e-01 +1.38e-01 +1.40e-01 +1.41e-01 +1.32e-01 +1.36e-01 +1.36e-01 +1.37e-01 +1.48e-01 +2.02e-01 +1.53e-01 +5.25e-02 +1.45e-01 +1.41e-01 +1.44e-01 +1.36e-01 +1.36e-01 +1.36e-01 +1.33e-01 +1.32e-01 +1.26e-01 +1.32e-01 +1.37e-01 +1.38e-01 +1.71e-01 +1.19e-01 +4.26e-02 +1.40e-01 +1.43e-01 +1.46e-01 +1.39e-01 +1.38e-01 +1.34e-01 +1.29e-01 +1.26e-01 +1.28e-01 +1.28e-01 +1.34e-01 +1.39e-01 +1.70e-01 +7.71e-02 +3.01e-02 +1.38e-01 +1.12e-01 +1.38e-01 +1.41e-01 +1.44e-01 +1.46e-01 +1.34e-01 +1.11e-01 +1.24e-01 +1.20e-01 +1.22e-01 +1.33e-01 +1.84e-01 +8.77e-02 +2.26e-02 +1.45e-01 +1.37e-01 +1.37e-01 +1.29e-01 +1.34e-01 +1.37e-01 +1.34e-01 +1.29e-01 +1.23e-01 +1.19e-01 +1.21e-01 +1.25e-01 +1.84e-01 +9.19e-02 +2.31e-02 +1.48e-01 +1.32e-01 +1.36e-01 +1.34e-01 +1.41e-01 +1.34e-01 +1.32e-01 +1.27e-01 +1.27e-01 +1.20e-01 +1.10e-01 +1.17e-01 +1.65e-01 +9.68e-02 +2.83e-02 +1.48e-01 +1.35e-01 +1.35e-01 +1.43e-01 +1.44e-01 +1.40e-01 +1.43e-01 +1.35e-01 +1.34e-01 +1.29e-01 +1.17e-01 +9.99e-02 +9.90e-02 +5.49e-02 +1.66e-02 +2.22e-01 +2.00e-01 +2.03e-01 +2.10e-01 +2.09e-01 +1.99e-01 +1.86e-01 +1.93e-01 +1.79e-01 +1.90e-01 +1.70e-01 +1.45e-01 +1.22e-01 +4.35e-02 +1.22e-02 +2.30e-01 +1.84e-01 +1.79e-01 +1.67e-01 +1.72e-01 +1.61e-01 +1.13e-01 +1.16e-01 +1.06e-01 +1.13e-01 +1.20e-01 +8.32e-02 +4.37e-02 +1.70e-02 +5.87e-03 +7.01e-02 +5.48e-02 +4.65e-02 +4.76e-02 +5.00e-02 +5.48e-02 +3.77e-02 +3.45e-02 +3.12e-02 +3.14e-02 +3.88e-02 +2.77e-02 +1.27e-02 +6.38e-03 +1.50e-03 +1.48e-01 +1.54e-01 +1.58e-01 +1.56e-01 +1.55e-01 +1.46e-01 +1.44e-01 +1.48e-01 +1.42e-01 +1.44e-01 +1.40e-01 +1.54e-01 +1.94e-01 +1.38e-01 +5.04e-02 +1.51e-01 +1.53e-01 +1.49e-01 +1.49e-01 +1.49e-01 +1.53e-01 +1.51e-01 +1.50e-01 +1.45e-01 +1.36e-01 +1.42e-01 +1.57e-01 +2.09e-01 +1.55e-01 +4.56e-02 +1.55e-01 +1.48e-01 +1.43e-01 +1.44e-01 +1.46e-01 +1.54e-01 +1.50e-01 +1.51e-01 +1.54e-01 +1.38e-01 +1.36e-01 +1.52e-01 +2.10e-01 +1.66e-01 +5.44e-02 +1.48e-01 +1.47e-01 +1.46e-01 +1.43e-01 +1.49e-01 +1.48e-01 +1.51e-01 +1.49e-01 +1.42e-01 +1.38e-01 +1.39e-01 +1.49e-01 +1.94e-01 +1.42e-01 +5.42e-02 +1.43e-01 +1.39e-01 +1.46e-01 +1.48e-01 +1.48e-01 +1.42e-01 +1.41e-01 +1.46e-01 +1.40e-01 +1.38e-01 +1.43e-01 +1.43e-01 +1.95e-01 +1.20e-01 +3.81e-02 +1.49e-01 +1.42e-01 +1.47e-01 +1.42e-01 +1.43e-01 +1.43e-01 +1.42e-01 +1.41e-01 +1.36e-01 +1.33e-01 +1.38e-01 +1.42e-01 +1.87e-01 +1.10e-01 +4.00e-02 +1.46e-01 +1.44e-01 +1.42e-01 +1.42e-01 +1.42e-01 +1.43e-01 +1.36e-01 +1.37e-01 +1.32e-01 +1.37e-01 +1.30e-01 +1.42e-01 +1.97e-01 +1.41e-01 +4.74e-02 +1.42e-01 +1.46e-01 +1.45e-01 +1.44e-01 +1.40e-01 +1.37e-01 +1.29e-01 +1.32e-01 +1.28e-01 +1.22e-01 +1.29e-01 +1.44e-01 +1.79e-01 +8.11e-02 +2.49e-02 +1.36e-01 +1.40e-01 +1.42e-01 +1.45e-01 +1.42e-01 +1.35e-01 +1.28e-01 +1.25e-01 +1.21e-01 +1.20e-01 +1.24e-01 +1.37e-01 +1.76e-01 +8.67e-02 +2.55e-02 +1.41e-01 +1.39e-01 +1.39e-01 +1.42e-01 +1.37e-01 +1.33e-01 +1.30e-01 +1.23e-01 +1.22e-01 +1.24e-01 +1.16e-01 +1.28e-01 +1.68e-01 +1.03e-01 +3.30e-02 +1.41e-01 +1.35e-01 +1.34e-01 +1.33e-01 +1.33e-01 +1.34e-01 +1.37e-01 +1.31e-01 +1.21e-01 +1.17e-01 +1.11e-01 +1.13e-01 +1.57e-01 +8.80e-02 +2.97e-02 +1.53e-01 +1.39e-01 +1.45e-01 +1.45e-01 +1.38e-01 +1.49e-01 +1.41e-01 +1.36e-01 +1.24e-01 +1.23e-01 +1.14e-01 +9.70e-02 +1.01e-01 +5.74e-02 +1.83e-02 +2.39e-01 +1.91e-01 +1.79e-01 +2.07e-01 +1.97e-01 +2.03e-01 +1.92e-01 +2.07e-01 +1.99e-01 +2.04e-01 +1.76e-01 +1.35e-01 +1.07e-01 +3.67e-02 +1.30e-02 +1.91e-01 +1.54e-01 +1.49e-01 +1.68e-01 +1.82e-01 +1.72e-01 +1.48e-01 +1.49e-01 +1.30e-01 +1.06e-01 +9.60e-02 +7.07e-02 +4.00e-02 +1.71e-02 +7.45e-03 +7.31e-02 +6.14e-02 +6.36e-02 +5.75e-02 +6.28e-02 +5.80e-02 +4.73e-02 +4.31e-02 +4.49e-02 +3.41e-02 +3.27e-02 +2.30e-02 +1.26e-02 +7.67e-03 +3.56e-03 +1.42e-01 +1.47e-01 +1.56e-01 +1.59e-01 +1.58e-01 +1.56e-01 +1.58e-01 +1.63e-01 +1.48e-01 +1.51e-01 +1.49e-01 +1.44e-01 +1.75e-01 +9.73e-02 +3.77e-02 +1.45e-01 +1.50e-01 +1.49e-01 +1.50e-01 +1.53e-01 +1.52e-01 +1.51e-01 +1.54e-01 +1.52e-01 +1.51e-01 +1.48e-01 +1.46e-01 +1.93e-01 +1.30e-01 +4.16e-02 +1.46e-01 +1.48e-01 +1.46e-01 +1.53e-01 +1.47e-01 +1.54e-01 +1.58e-01 +1.45e-01 +1.47e-01 +1.46e-01 +1.45e-01 +1.50e-01 +1.99e-01 +1.42e-01 +4.85e-02 +1.46e-01 +1.48e-01 +1.56e-01 +1.44e-01 +1.54e-01 +1.56e-01 +1.52e-01 +1.46e-01 +1.44e-01 +1.37e-01 +1.38e-01 +1.46e-01 +1.94e-01 +1.11e-01 +3.61e-02 +1.45e-01 +1.48e-01 +1.51e-01 +1.46e-01 +1.54e-01 +1.54e-01 +1.45e-01 +1.44e-01 +1.48e-01 +1.40e-01 +1.46e-01 +1.49e-01 +1.93e-01 +1.06e-01 +3.16e-02 +1.45e-01 +1.48e-01 +1.50e-01 +1.56e-01 +1.46e-01 +1.51e-01 +1.44e-01 +1.41e-01 +1.32e-01 +1.41e-01 +1.41e-01 +1.40e-01 +2.04e-01 +1.15e-01 +3.10e-02 +1.41e-01 +1.46e-01 +1.41e-01 +1.48e-01 +1.45e-01 +1.45e-01 +1.44e-01 +1.38e-01 +1.34e-01 +1.34e-01 +1.38e-01 +1.42e-01 +1.85e-01 +1.26e-01 +3.53e-02 +1.38e-01 +1.45e-01 +1.45e-01 +1.49e-01 +1.41e-01 +1.40e-01 +1.33e-01 +1.34e-01 +1.33e-01 +1.36e-01 +1.35e-01 +1.37e-01 +1.76e-01 +1.16e-01 +4.10e-02 +1.35e-01 +1.39e-01 +1.44e-01 +1.43e-01 +1.44e-01 +1.40e-01 +1.27e-01 +1.29e-01 +1.24e-01 +1.24e-01 +1.21e-01 +1.25e-01 +1.74e-01 +9.17e-02 +2.60e-02 +1.39e-01 +1.35e-01 +1.40e-01 +1.43e-01 +1.45e-01 +1.32e-01 +1.32e-01 +1.29e-01 +1.20e-01 +1.22e-01 +1.14e-01 +1.15e-01 +1.43e-01 +8.26e-02 +2.82e-02 +1.37e-01 +1.36e-01 +1.37e-01 +1.38e-01 +1.39e-01 +1.32e-01 +1.34e-01 +1.31e-01 +1.19e-01 +1.18e-01 +1.10e-01 +1.13e-01 +1.51e-01 +5.50e-02 +1.63e-02 +1.36e-01 +1.38e-01 +1.40e-01 +1.44e-01 +1.37e-01 +1.43e-01 +1.44e-01 +1.39e-01 +1.26e-01 +1.21e-01 +1.14e-01 +1.05e-01 +1.24e-01 +4.77e-02 +1.43e-02 +1.96e-01 +1.79e-01 +1.80e-01 +1.95e-01 +1.99e-01 +1.94e-01 +1.81e-01 +1.92e-01 +1.80e-01 +1.60e-01 +1.61e-01 +1.34e-01 +7.13e-02 +2.07e-02 +7.42e-03 +1.76e-01 +9.49e-02 +1.22e-01 +1.31e-01 +1.35e-01 +1.34e-01 +1.06e-01 +1.42e-01 +1.15e-01 +7.49e-02 +8.42e-02 +5.97e-02 +2.86e-02 +1.13e-02 +3.88e-03 +5.74e-02 +3.06e-02 +3.68e-02 +4.09e-02 +5.27e-02 +4.71e-02 +3.30e-02 +4.67e-02 +3.85e-02 +2.55e-02 +2.40e-02 +1.51e-02 +1.04e-02 +6.23e-03 +3.39e-03 +1.51e-01 +1.61e-01 +1.68e-01 +1.62e-01 +1.62e-01 +1.64e-01 +1.64e-01 +1.55e-01 +1.53e-01 +1.51e-01 +1.54e-01 +1.45e-01 +1.43e-01 +3.50e-02 +1.38e-02 +1.65e-01 +1.68e-01 +1.64e-01 +1.54e-01 +1.51e-01 +1.54e-01 +1.57e-01 +1.50e-01 +1.50e-01 +1.49e-01 +1.55e-01 +1.49e-01 +1.60e-01 +6.68e-02 +2.43e-02 +1.59e-01 +1.68e-01 +1.59e-01 +1.54e-01 +1.55e-01 +1.66e-01 +1.64e-01 +1.60e-01 +1.47e-01 +1.49e-01 +1.42e-01 +1.60e-01 +2.06e-01 +1.03e-01 +2.77e-02 +1.56e-01 +1.59e-01 +1.54e-01 +1.54e-01 +1.59e-01 +1.65e-01 +5.07e-02 +1.43e-01 +1.52e-01 +1.45e-01 +1.35e-01 +1.44e-01 +1.79e-01 +9.16e-02 +3.06e-02 +1.50e-01 +1.51e-01 +1.54e-01 +1.55e-01 +1.59e-01 +1.59e-01 +1.53e-01 +1.47e-01 +1.53e-01 +1.51e-01 +1.40e-01 +1.45e-01 +1.90e-01 +1.07e-01 +2.94e-02 +1.50e-01 +1.52e-01 +1.48e-01 +1.54e-01 +1.52e-01 +1.51e-01 +1.50e-01 +1.43e-01 +1.47e-01 +1.46e-01 +1.38e-01 +1.35e-01 +1.78e-01 +9.46e-02 +3.09e-02 +1.41e-01 +1.42e-01 +1.53e-01 +1.56e-01 +1.53e-01 +1.48e-01 +1.48e-01 +1.41e-01 +1.38e-01 +1.40e-01 +1.40e-01 +1.30e-01 +1.49e-01 +9.60e-02 +3.33e-02 +1.42e-01 +1.46e-01 +1.40e-01 +1.50e-01 +1.45e-01 +1.45e-01 +1.50e-01 +1.45e-01 +1.41e-01 +1.33e-01 +1.36e-01 +1.37e-01 +1.71e-01 +7.74e-02 +2.54e-02 +1.45e-01 +1.43e-01 +1.47e-01 +1.51e-01 +1.49e-01 +1.54e-01 +1.42e-01 +1.37e-01 +1.34e-01 +1.33e-01 +1.27e-01 +1.27e-01 +1.66e-01 +7.29e-02 +2.38e-02 +1.44e-01 +1.39e-01 +1.51e-01 +1.49e-01 +1.48e-01 +1.46e-01 +1.38e-01 +1.29e-01 +1.23e-01 +1.18e-01 +1.23e-01 +1.14e-01 +1.60e-01 +7.60e-02 +2.69e-02 +1.34e-01 +1.33e-01 +1.39e-01 +1.41e-01 +1.50e-01 +1.43e-01 +1.31e-01 +1.28e-01 +1.23e-01 +1.19e-01 +1.12e-01 +1.08e-01 +1.37e-01 +5.02e-02 +1.48e-02 +1.33e-01 +5.34e-02 +1.34e-01 +1.36e-01 +1.35e-01 +1.30e-01 +1.37e-01 +1.40e-01 +1.27e-01 +1.22e-01 +1.08e-01 +1.01e-01 +1.15e-01 +4.47e-02 +1.28e-02 +1.73e-01 +1.60e-01 +1.60e-01 +1.70e-01 +1.65e-01 +1.66e-01 +1.67e-01 +1.80e-01 +1.81e-01 +1.46e-01 +1.35e-01 +1.18e-01 +6.95e-02 +2.21e-02 +7.42e-03 +1.30e-01 +7.03e-02 +8.47e-02 +1.05e-01 +8.11e-02 +8.16e-02 +7.32e-02 +7.94e-02 +1.08e-01 +7.59e-02 +5.62e-02 +4.38e-02 +2.63e-02 +6.47e-03 +2.09e-03 +5.13e-02 +2.67e-02 +3.48e-02 +3.62e-02 +2.93e-02 +2.83e-02 +2.47e-02 +2.60e-02 +3.29e-02 +2.55e-02 +1.63e-02 +1.59e-02 +1.02e-02 +3.46e-03 +1.50e-03 +2.17e-01 +2.25e-01 +2.43e-01 +2.43e-01 +2.31e-01 +2.22e-01 +2.22e-01 +2.12e-01 +2.21e-01 +1.98e-01 +1.99e-01 +2.04e-01 +1.57e-01 +3.11e-02 +7.85e-03 +2.30e-01 +2.10e-01 +2.47e-01 +2.20e-01 +2.21e-01 +2.29e-01 +2.22e-01 +2.06e-01 +1.98e-01 +2.04e-01 +1.90e-01 +1.83e-01 +1.78e-01 +5.75e-02 +1.61e-02 +2.24e-01 +2.08e-01 +2.36e-01 +2.21e-01 +2.12e-01 +2.36e-01 +2.08e-01 +2.30e-01 +2.06e-01 +2.08e-01 +1.93e-01 +1.84e-01 +2.10e-01 +9.89e-02 +3.10e-02 +2.25e-01 +2.06e-01 +2.18e-01 +2.25e-01 +2.19e-01 +2.39e-01 +2.22e-01 +2.20e-01 +2.12e-01 +2.02e-01 +1.75e-01 +1.62e-01 +1.73e-01 +6.25e-02 +1.80e-02 +2.16e-01 +2.06e-01 +2.12e-01 +2.21e-01 +2.09e-01 +2.25e-01 +2.54e-01 +2.18e-01 +2.16e-01 +2.24e-01 +1.99e-01 +1.77e-01 +1.57e-01 +7.90e-02 +2.92e-02 +2.13e-01 +1.96e-01 +2.07e-01 +2.04e-01 +2.00e-01 +2.17e-01 +2.17e-01 +2.01e-01 +2.06e-01 +2.15e-01 +1.92e-01 +1.66e-01 +1.68e-01 +8.03e-02 +2.44e-02 +1.90e-01 +1.93e-01 +2.12e-01 +2.07e-01 +2.00e-01 +2.08e-01 +2.04e-01 +1.97e-01 +1.68e-01 +1.90e-01 +1.82e-01 +1.49e-01 +1.55e-01 +6.10e-02 +1.96e-02 +1.94e-01 +1.89e-01 +2.00e-01 +2.16e-01 +2.22e-01 +2.06e-01 +2.02e-01 +1.99e-01 +1.77e-01 +1.77e-01 +1.83e-01 +1.68e-01 +1.29e-01 +3.50e-02 +1.16e-02 +1.93e-01 +2.00e-01 +2.02e-01 +2.11e-01 +2.16e-01 +1.93e-01 +2.15e-01 +2.05e-01 +1.80e-01 +1.82e-01 +1.71e-01 +1.65e-01 +1.61e-01 +4.47e-02 +1.50e-02 +1.94e-01 +2.06e-01 +2.19e-01 +2.03e-01 +2.12e-01 +1.96e-01 +2.02e-01 +1.81e-01 +1.66e-01 +1.60e-01 +1.62e-01 +1.62e-01 +1.56e-01 +4.92e-02 +1.53e-02 +1.78e-01 +1.86e-01 +2.05e-01 +2.04e-01 +1.97e-01 +2.15e-01 +1.88e-01 +1.63e-01 +1.57e-01 +1.38e-01 +1.47e-01 +1.39e-01 +1.01e-01 +4.20e-02 +1.54e-02 +1.47e-01 +1.52e-01 +1.77e-01 +1.74e-01 +1.84e-01 +1.91e-01 +2.00e-01 +1.85e-01 +1.68e-01 +1.42e-01 +1.34e-01 +1.28e-01 +9.77e-02 +2.94e-02 +1.17e-02 +1.28e-01 +1.39e-01 +1.56e-01 +1.44e-01 +1.64e-01 +1.66e-01 +1.39e-01 +1.87e-01 +1.87e-01 +1.57e-01 +1.18e-01 +9.64e-02 +6.30e-02 +1.82e-02 +8.08e-03 +7.18e-02 +4.39e-02 +5.20e-02 +6.11e-02 +4.96e-02 +4.82e-02 +4.41e-02 +5.38e-02 +8.79e-02 +5.77e-02 +2.70e-02 +2.61e-02 +1.49e-02 +4.93e-03 +2.78e-03 +2.74e-02 +1.41e-02 +1.81e-02 +1.93e-02 +1.48e-02 +1.51e-02 +1.60e-02 +1.57e-02 +2.80e-02 +2.17e-02 +1.10e-02 +8.28e-03 +4.76e-03 +2.53e-03 +9.95e-04 +2.15e-01 +2.13e-01 +2.69e-01 +1.97e-01 +2.97e-01 +2.44e-01 +2.16e-01 +2.27e-01 +1.95e-01 +1.69e-01 +1.21e-01 +1.27e-01 +1.07e-01 +2.85e-02 +7.97e-03 +2.71e-01 +1.94e-01 +3.20e-01 +3.60e-01 +2.90e-01 +2.34e-01 +1.99e-01 +2.04e-01 +1.73e-01 +1.40e-01 +1.29e-01 +1.08e-01 +8.99e-02 +4.08e-02 +1.48e-02 +2.36e-01 +2.27e-01 +2.95e-01 +2.38e-01 +1.88e-01 +2.38e-01 +1.95e-01 +2.03e-01 +1.85e-01 +1.65e-01 +1.78e-01 +1.40e-01 +7.70e-02 +4.33e-02 +2.87e-02 +2.64e-01 +2.27e-01 +2.72e-01 +3.07e-01 +2.33e-01 +2.58e-01 +2.71e-01 +2.36e-01 +2.17e-01 +1.73e-01 +1.26e-01 +9.14e-02 +6.26e-02 +3.03e-02 +1.37e-02 +2.60e-01 +1.95e-01 +2.57e-01 +3.08e-01 +2.65e-01 +2.75e-01 +2.70e-01 +2.25e-01 +1.83e-01 +1.71e-01 +1.54e-01 +8.13e-02 +5.27e-02 +2.61e-02 +1.31e-02 +2.09e-01 +1.59e-01 +2.23e-01 +2.30e-01 +1.63e-01 +1.84e-01 +2.17e-01 +1.62e-01 +1.39e-01 +1.79e-01 +1.60e-01 +1.05e-01 +5.74e-02 +2.47e-02 +1.55e-02 +1.50e-01 +1.80e-01 +2.09e-01 +2.09e-01 +1.83e-01 +2.00e-01 +1.82e-01 +1.57e-01 +1.32e-01 +1.22e-01 +9.76e-02 +6.91e-02 +4.61e-02 +2.65e-02 +1.49e-02 +1.12e-01 +1.42e-01 +1.70e-01 +1.73e-01 +1.86e-01 +1.93e-01 +1.34e-01 +1.27e-01 +9.53e-02 +8.75e-02 +9.91e-02 +7.87e-02 +4.52e-02 +2.11e-02 +7.92e-03 +1.22e-01 +1.39e-01 +1.69e-01 +2.22e-01 +1.74e-01 +1.60e-01 +1.21e-01 +1.47e-01 +1.19e-01 +1.07e-01 +9.89e-02 +8.43e-02 +5.88e-02 +2.01e-02 +8.74e-03 +9.13e-02 +1.61e-01 +2.14e-01 +1.61e-01 +1.43e-01 +1.01e-01 +1.25e-01 +1.42e-01 +1.15e-01 +9.15e-02 +8.87e-02 +6.91e-02 +5.19e-02 +2.72e-02 +8.44e-03 +9.87e-02 +1.23e-01 +1.61e-01 +1.11e-01 +1.42e-01 +1.45e-01 +1.19e-01 +1.11e-01 +8.68e-02 +5.91e-02 +7.60e-02 +6.84e-02 +4.18e-02 +2.17e-02 +9.67e-03 +8.56e-02 +7.21e-02 +1.04e-01 +9.72e-02 +9.63e-02 +1.38e-01 +1.19e-01 +7.48e-02 +6.52e-02 +5.51e-02 +5.18e-02 +5.14e-02 +3.71e-02 +9.91e-03 +3.65e-03 +4.31e-02 +5.32e-02 +6.28e-02 +4.91e-02 +5.90e-02 +8.59e-02 +7.51e-02 +7.34e-02 +6.11e-02 +6.19e-02 +4.42e-02 +3.60e-02 +2.68e-02 +1.01e-02 +4.00e-03 +1.92e-02 +2.02e-02 +2.30e-02 +1.98e-02 +2.54e-02 +3.35e-02 +2.56e-02 +3.19e-02 +3.49e-02 +2.85e-02 +1.72e-02 +1.18e-02 +8.37e-03 +4.19e-03 +2.02e-03 +1.38e-02 +7.42e-03 +8.36e-03 +8.57e-03 +9.08e-03 +1.23e-02 +8.17e-03 +1.02e-02 +1.45e-02 +1.35e-02 +6.28e-03 +3.59e-03 +3.28e-03 +1.49e-03 +5.93e-04 +7.32e-02 +7.34e-02 +9.95e-02 +6.68e-02 +1.09e-01 +8.20e-02 +7.49e-02 +7.94e-02 +7.32e-02 +5.71e-02 +3.93e-02 +3.97e-02 +3.57e-02 +2.10e-02 +7.29e-03 +1.10e-01 +5.23e-02 +1.07e-01 +1.46e-01 +1.24e-01 +8.85e-02 +7.13e-02 +6.52e-02 +6.37e-02 +5.06e-02 +3.96e-02 +3.23e-02 +2.70e-02 +1.79e-02 +8.68e-03 +8.76e-02 +6.94e-02 +1.01e-01 +9.79e-02 +5.28e-02 +8.50e-02 +7.16e-02 +6.22e-02 +6.61e-02 +4.64e-02 +6.19e-02 +5.31e-02 +2.85e-02 +1.22e-02 +1.28e-02 +8.68e-02 +7.27e-02 +1.06e-01 +1.20e-01 +8.06e-02 +8.67e-02 +9.61e-02 +8.95e-02 +8.71e-02 +6.56e-02 +4.39e-02 +3.10e-02 +2.08e-02 +1.24e-02 +5.85e-03 +1.06e-01 +7.84e-02 +9.79e-02 +1.14e-01 +1.02e-01 +9.64e-02 +1.00e-01 +7.46e-02 +7.16e-02 +5.01e-02 +5.24e-02 +2.38e-02 +1.47e-02 +8.69e-03 +5.50e-03 +6.48e-02 +5.48e-02 +7.05e-02 +7.64e-02 +6.19e-02 +5.28e-02 +6.84e-02 +5.17e-02 +4.47e-02 +5.44e-02 +5.78e-02 +4.35e-02 +2.02e-02 +8.44e-03 +4.29e-03 +6.15e-02 +5.80e-02 +7.42e-02 +8.23e-02 +4.82e-02 +5.80e-02 +5.99e-02 +5.52e-02 +5.07e-02 +4.11e-02 +3.25e-02 +2.74e-02 +1.58e-02 +7.93e-03 +7.33e-03 +3.07e-02 +5.33e-02 +5.17e-02 +5.32e-02 +5.86e-02 +6.36e-02 +3.92e-02 +4.20e-02 +3.02e-02 +2.37e-02 +2.60e-02 +2.49e-02 +1.38e-02 +1.14e-02 +8.22e-03 +3.87e-02 +4.20e-02 +5.55e-02 +6.88e-02 +6.66e-02 +6.23e-02 +3.77e-02 +4.86e-02 +4.16e-02 +3.47e-02 +2.92e-02 +2.68e-02 +2.21e-02 +9.30e-03 +3.87e-03 +2.33e-02 +4.93e-02 +6.49e-02 +6.24e-02 +4.58e-02 +2.96e-02 +4.02e-02 +4.66e-02 +4.25e-02 +2.79e-02 +2.67e-02 +2.26e-02 +1.37e-02 +1.36e-02 +7.38e-03 +3.11e-02 +5.02e-02 +5.87e-02 +3.06e-02 +4.64e-02 +4.05e-02 +2.71e-02 +3.77e-02 +3.09e-02 +2.18e-02 +2.34e-02 +1.91e-02 +1.39e-02 +1.02e-02 +7.91e-03 +2.26e-02 +2.44e-02 +3.23e-02 +2.90e-02 +2.99e-02 +4.72e-02 +4.07e-02 +3.01e-02 +2.01e-02 +1.70e-02 +1.57e-02 +1.51e-02 +1.50e-02 +4.17e-03 +1.63e-03 +1.86e-02 +1.84e-02 +2.18e-02 +2.46e-02 +1.84e-02 +3.15e-02 +2.72e-02 +2.27e-02 +1.73e-02 +2.03e-02 +1.19e-02 +1.20e-02 +1.14e-02 +4.19e-03 +1.45e-03 +6.21e-03 +9.69e-03 +9.74e-03 +7.36e-03 +1.15e-02 +1.57e-02 +1.41e-02 +1.64e-02 +1.88e-02 +1.03e-02 +1.09e-02 +6.84e-03 +4.29e-03 +2.69e-03 +2.28e-03 +4.54e-03 +5.03e-03 +5.40e-03 +5.12e-03 +6.04e-03 +1.01e-02 +6.83e-03 +6.36e-03 +1.16e-02 +7.66e-03 +5.81e-03 +3.71e-03 +1.48e-03 +1.00e-03 +1.45e-03 +Upper Bounds +8.55e-01 +8.64e-01 +8.59e-01 +8.67e-01 +8.53e-01 +8.16e-01 +8.31e-01 +8.50e-01 +8.39e-01 +8.80e-01 +8.66e-01 +8.96e-01 +1.31e+00 +2.22e+00 +8.38e-01 +8.15e-01 +8.82e-01 +8.67e-01 +7.61e-01 +8.32e-01 +8.60e-01 +8.42e-01 +8.42e-01 +8.32e-01 +8.34e-01 +8.80e-01 +8.65e-01 +1.22e+00 +1.82e+00 +6.13e-01 +8.56e-01 +8.32e-01 +8.43e-01 +8.53e-01 +8.33e-01 +8.57e-01 +8.19e-01 +7.78e-01 +8.33e-01 +8.19e-01 +8.41e-01 +9.08e-01 +1.30e+00 +2.06e+00 +7.76e-01 +8.38e-01 +8.08e-01 +8.28e-01 +8.25e-01 +8.10e-01 +8.21e-01 +8.13e-01 +8.31e-01 +8.17e-01 +8.36e-01 +7.91e-01 +8.95e-01 +1.26e+00 +2.05e+00 +6.73e-01 +8.25e-01 +8.69e-01 +8.34e-01 +8.56e-01 +8.29e-01 +8.11e-01 +8.01e-01 +8.32e-01 +7.75e-01 +8.33e-01 +8.31e-01 +9.11e-01 +1.34e+00 +1.73e+00 +6.60e-01 +8.48e-01 +8.49e-01 +8.41e-01 +8.41e-01 +8.43e-01 +7.95e-01 +8.08e-01 +7.96e-01 +7.85e-01 +7.74e-01 +8.58e-01 +8.98e-01 +1.29e+00 +1.49e+00 +5.33e-01 +8.40e-01 +7.98e-01 +7.91e-01 +8.03e-01 +7.88e-01 +7.96e-01 +7.81e-01 +7.66e-01 +7.83e-01 +7.62e-01 +7.93e-01 +7.72e-01 +1.21e+00 +1.65e+00 +6.11e-01 +8.18e-01 +8.25e-01 +8.44e-01 +8.18e-01 +8.00e-01 +8.28e-01 +8.00e-01 +7.60e-01 +7.23e-01 +7.65e-01 +7.83e-01 +7.01e-01 +1.04e+00 +9.49e-01 +3.97e-01 +8.24e-01 +8.27e-01 +8.57e-01 +8.58e-01 +8.13e-01 +8.03e-01 +7.83e-01 +7.70e-01 +7.46e-01 +7.07e-01 +7.24e-01 +7.65e-01 +1.18e+00 +9.01e-01 +2.88e-01 +8.34e-01 +8.73e-01 +8.70e-01 +8.56e-01 +8.29e-01 +7.71e-01 +7.75e-01 +7.58e-01 +7.64e-01 +7.06e-01 +7.64e-01 +7.43e-01 +1.07e+00 +8.82e-01 +3.35e-01 +8.58e-01 +8.29e-01 +8.72e-01 +8.48e-01 +8.46e-01 +7.87e-01 +7.50e-01 +7.65e-01 +7.38e-01 +7.34e-01 +8.10e-01 +7.43e-01 +1.02e+00 +6.73e-01 +2.03e-01 +9.03e-01 +9.01e-01 +8.60e-01 +8.26e-01 +9.18e-01 +8.72e-01 +7.91e-01 +8.39e-01 +7.95e-01 +7.87e-01 +7.88e-01 +7.38e-01 +1.08e+00 +5.23e-01 +1.69e-01 +1.31e+00 +1.26e+00 +1.18e+00 +1.25e+00 +1.32e+00 +1.21e+00 +1.20e+00 +1.16e+00 +1.16e+00 +1.14e+00 +1.11e+00 +1.10e+00 +1.03e+00 +4.01e-01 +1.10e-01 +2.50e+00 +1.84e+00 +1.94e+00 +1.32e+00 +1.43e+00 +1.59e+00 +1.30e+00 +1.19e+00 +9.40e-01 +9.83e-01 +9.19e-01 +7.52e-01 +4.99e-01 +1.94e-01 +9.48e-02 +1.09e+00 +7.40e-01 +8.05e-01 +4.38e-01 +4.72e-01 +5.81e-01 +4.22e-01 +4.06e-01 +3.50e-01 +3.08e-01 +3.25e-01 +2.23e-01 +1.85e-01 +8.09e-02 +3.66e-02 +8.48e-01 +8.45e-01 +8.50e-01 +8.39e-01 +8.13e-01 +7.97e-01 +8.76e-01 +8.45e-01 +7.95e-01 +8.18e-01 +8.84e-01 +8.93e-01 +1.31e+00 +1.96e+00 +7.41e-01 +8.63e-01 +8.69e-01 +8.49e-01 +8.32e-01 +8.42e-01 +8.08e-01 +8.09e-01 +8.36e-01 +7.99e-01 +8.20e-01 +8.33e-01 +9.00e-01 +1.21e+00 +1.86e+00 +7.23e-01 +8.57e-01 +8.27e-01 +8.35e-01 +8.27e-01 +8.44e-01 +8.48e-01 +8.19e-01 +8.39e-01 +8.01e-01 +8.48e-01 +8.52e-01 +8.71e-01 +1.31e+00 +1.65e+00 +5.73e-01 +8.64e-01 +8.75e-01 +8.50e-01 +8.46e-01 +8.43e-01 +8.17e-01 +8.28e-01 +8.15e-01 +8.07e-01 +8.04e-01 +8.51e-01 +8.94e-01 +1.24e+00 +1.82e+00 +6.58e-01 +8.61e-01 +8.65e-01 +8.57e-01 +8.28e-01 +8.15e-01 +8.13e-01 +8.36e-01 +8.01e-01 +7.82e-01 +8.17e-01 +8.58e-01 +8.55e-01 +1.26e+00 +1.71e+00 +6.99e-01 +8.39e-01 +8.48e-01 +8.35e-01 +8.30e-01 +8.61e-01 +8.03e-01 +7.79e-01 +7.79e-01 +7.65e-01 +7.73e-01 +8.23e-01 +8.92e-01 +1.26e+00 +1.83e+00 +6.65e-01 +8.50e-01 +8.43e-01 +8.01e-01 +8.18e-01 +8.51e-01 +8.15e-01 +7.63e-01 +7.50e-01 +7.52e-01 +7.40e-01 +7.66e-01 +8.29e-01 +1.17e+00 +1.50e+00 +5.74e-01 +8.24e-01 +8.15e-01 +7.95e-01 +7.88e-01 +7.97e-01 +7.70e-01 +7.69e-01 +7.35e-01 +6.98e-01 +6.91e-01 +6.35e-01 +6.08e-01 +9.93e-01 +8.87e-01 +3.56e-01 +7.90e-01 +8.73e-01 +8.38e-01 +8.31e-01 +8.29e-01 +7.89e-01 +7.80e-01 +7.18e-01 +7.42e-01 +7.23e-01 +6.87e-01 +7.01e-01 +1.01e+00 +6.82e-01 +2.08e-01 +8.24e-01 +8.78e-01 +8.46e-01 +8.43e-01 +7.84e-01 +8.35e-01 +7.79e-01 +7.48e-01 +7.63e-01 +7.14e-01 +7.56e-01 +7.03e-01 +1.02e+00 +7.48e-01 +2.25e-01 +8.45e-01 +8.73e-01 +8.56e-01 +8.16e-01 +8.44e-01 +8.36e-01 +7.99e-01 +7.51e-01 +7.67e-01 +7.20e-01 +7.86e-01 +7.45e-01 +1.01e+00 +6.73e-01 +2.04e-01 +8.93e-01 +8.59e-01 +8.64e-01 +8.51e-01 +8.70e-01 +8.91e-01 +8.41e-01 +7.94e-01 +7.85e-01 +8.08e-01 +8.08e-01 +7.83e-01 +9.73e-01 +4.60e-01 +1.58e-01 +1.24e+00 +1.32e+00 +1.27e+00 +1.29e+00 +1.40e+00 +1.26e+00 +1.17e+00 +1.20e+00 +1.08e+00 +1.09e+00 +1.21e+00 +1.11e+00 +9.96e-01 +3.09e-01 +8.83e-02 +1.56e+00 +1.49e+00 +1.49e+00 +1.58e+00 +1.83e+00 +1.75e+00 +1.41e+00 +1.38e+00 +1.12e+00 +8.28e-01 +8.50e-01 +8.27e-01 +5.48e-01 +1.84e-01 +7.71e-02 +5.56e-01 +5.50e-01 +5.89e-01 +5.41e-01 +6.72e-01 +7.04e-01 +4.96e-01 +4.98e-01 +4.70e-01 +2.59e-01 +2.70e-01 +2.72e-01 +2.11e-01 +9.38e-02 +3.66e-02 +8.67e-01 +8.42e-01 +8.70e-01 +8.39e-01 +7.98e-01 +8.02e-01 +8.06e-01 +7.94e-01 +8.48e-01 +8.04e-01 +8.10e-01 +8.81e-01 +1.45e+00 +2.04e+00 +7.89e-01 +8.58e-01 +8.42e-01 +8.10e-01 +8.30e-01 +7.96e-01 +8.14e-01 +7.91e-01 +8.00e-01 +7.95e-01 +8.24e-01 +8.16e-01 +8.87e-01 +1.22e+00 +1.96e+00 +8.18e-01 +8.56e-01 +8.69e-01 +8.58e-01 +7.87e-01 +8.24e-01 +8.16e-01 +8.16e-01 +8.43e-01 +7.92e-01 +8.05e-01 +8.03e-01 +8.83e-01 +1.22e+00 +1.68e+00 +6.47e-01 +7.87e-01 +8.38e-01 +8.29e-01 +8.15e-01 +8.16e-01 +7.97e-01 +8.04e-01 +8.23e-01 +7.82e-01 +7.95e-01 +8.24e-01 +7.98e-01 +1.22e+00 +1.65e+00 +5.42e-01 +8.09e-01 +8.09e-01 +8.05e-01 +7.72e-01 +8.14e-01 +8.20e-01 +7.50e-01 +7.87e-01 +7.53e-01 +7.48e-01 +8.36e-01 +8.59e-01 +1.22e+00 +1.39e+00 +4.29e-01 +8.07e-01 +8.21e-01 +7.86e-01 +7.76e-01 +7.84e-01 +7.85e-01 +7.56e-01 +7.44e-01 +7.72e-01 +7.80e-01 +8.33e-01 +8.48e-01 +1.21e+00 +1.74e+00 +6.61e-01 +8.36e-01 +8.64e-01 +8.19e-01 +8.11e-01 +8.00e-01 +8.07e-01 +7.80e-01 +7.34e-01 +7.56e-01 +7.70e-01 +7.83e-01 +7.97e-01 +1.13e+00 +1.26e+00 +4.66e-01 +8.16e-01 +8.15e-01 +7.98e-01 +7.75e-01 +7.87e-01 +7.92e-01 +7.70e-01 +7.35e-01 +7.31e-01 +7.42e-01 +7.14e-01 +7.50e-01 +1.05e+00 +8.42e-01 +3.14e-01 +7.86e-01 +8.18e-01 +8.21e-01 +8.01e-01 +7.99e-01 +7.90e-01 +7.89e-01 +7.83e-01 +7.33e-01 +7.48e-01 +7.58e-01 +7.15e-01 +1.06e+00 +8.32e-01 +3.09e-01 +8.27e-01 +8.58e-01 +8.09e-01 +8.21e-01 +8.32e-01 +7.85e-01 +7.86e-01 +7.51e-01 +7.01e-01 +7.38e-01 +7.30e-01 +7.22e-01 +9.77e-01 +6.23e-01 +2.49e-01 +8.03e-01 +8.31e-01 +8.48e-01 +8.19e-01 +8.34e-01 +8.33e-01 +8.42e-01 +7.89e-01 +7.16e-01 +7.24e-01 +7.28e-01 +7.16e-01 +9.71e-01 +6.27e-01 +1.80e-01 +8.04e-01 +8.69e-01 +8.69e-01 +8.82e-01 +8.64e-01 +8.59e-01 +8.38e-01 +8.03e-01 +7.94e-01 +7.80e-01 +7.95e-01 +7.83e-01 +8.87e-01 +5.44e-01 +1.87e-01 +1.24e+00 +1.24e+00 +1.24e+00 +1.21e+00 +1.24e+00 +1.28e+00 +1.10e+00 +1.13e+00 +1.12e+00 +1.09e+00 +1.11e+00 +1.13e+00 +1.07e+00 +3.12e-01 +9.52e-02 +1.84e+00 +1.36e+00 +1.70e+00 +1.79e+00 +1.82e+00 +1.16e+00 +1.17e+00 +1.10e+00 +9.39e-01 +8.88e-01 +6.70e-01 +6.23e-01 +5.35e-01 +2.34e-01 +6.47e-02 +6.45e-01 +4.31e-01 +7.11e-01 +6.80e-01 +6.87e-01 +4.47e-01 +4.04e-01 +4.19e-01 +2.85e-01 +3.23e-01 +1.57e-01 +1.93e-01 +1.76e-01 +1.29e-01 +6.44e-02 +8.45e-01 +8.09e-01 +8.31e-01 +8.52e-01 +8.43e-01 +8.06e-01 +7.88e-01 +7.57e-01 +7.58e-01 +7.66e-01 +7.61e-01 +8.67e-01 +1.27e+00 +2.06e+00 +7.56e-01 +8.40e-01 +8.74e-01 +8.23e-01 +7.97e-01 +8.08e-01 +7.78e-01 +7.85e-01 +7.76e-01 +7.95e-01 +8.30e-01 +7.96e-01 +8.37e-01 +1.18e+00 +1.62e+00 +6.44e-01 +8.11e-01 +8.19e-01 +8.40e-01 +7.94e-01 +7.57e-01 +7.93e-01 +7.66e-01 +7.66e-01 +7.76e-01 +8.15e-01 +7.99e-01 +8.24e-01 +1.16e+00 +1.39e+00 +5.36e-01 +8.08e-01 +8.15e-01 +8.00e-01 +8.03e-01 +7.68e-01 +7.86e-01 +7.93e-01 +7.55e-01 +7.56e-01 +7.89e-01 +7.68e-01 +8.06e-01 +1.20e+00 +1.24e+00 +5.05e-01 +7.77e-01 +7.72e-01 +8.04e-01 +8.28e-01 +7.75e-01 +7.64e-01 +7.80e-01 +7.54e-01 +7.38e-01 +7.65e-01 +8.13e-01 +8.11e-01 +1.21e+00 +1.61e+00 +5.92e-01 +8.28e-01 +7.61e-01 +8.10e-01 +8.12e-01 +7.80e-01 +7.60e-01 +7.76e-01 +7.84e-01 +7.75e-01 +7.67e-01 +7.63e-01 +8.21e-01 +1.23e+00 +1.51e+00 +5.55e-01 +8.13e-01 +7.95e-01 +8.40e-01 +7.80e-01 +7.59e-01 +7.73e-01 +7.80e-01 +7.46e-01 +7.45e-01 +7.55e-01 +7.27e-01 +7.44e-01 +1.11e+00 +1.44e+00 +5.11e-01 +8.31e-01 +8.12e-01 +8.03e-01 +7.49e-01 +7.48e-01 +7.53e-01 +7.93e-01 +7.32e-01 +7.17e-01 +7.10e-01 +7.05e-01 +7.55e-01 +1.11e+00 +9.42e-01 +3.25e-01 +8.00e-01 +7.96e-01 +7.87e-01 +8.07e-01 +7.73e-01 +7.86e-01 +7.64e-01 +7.65e-01 +7.27e-01 +7.21e-01 +7.35e-01 +7.93e-01 +1.18e+00 +9.54e-01 +2.85e-01 +8.11e-01 +7.95e-01 +8.15e-01 +7.97e-01 +7.74e-01 +7.69e-01 +7.52e-01 +7.33e-01 +7.51e-01 +7.28e-01 +7.13e-01 +7.16e-01 +1.10e+00 +6.86e-01 +2.68e-01 +8.31e-01 +8.25e-01 +7.50e-01 +8.42e-01 +8.24e-01 +8.05e-01 +7.74e-01 +7.47e-01 +7.33e-01 +7.57e-01 +7.16e-01 +7.50e-01 +9.60e-01 +4.98e-01 +1.46e-01 +8.65e-01 +8.27e-01 +8.48e-01 +3.91e-01 +8.66e-01 +8.92e-01 +8.23e-01 +7.50e-01 +8.02e-01 +7.77e-01 +7.85e-01 +7.73e-01 +9.44e-01 +4.45e-01 +1.34e-01 +1.16e+00 +1.18e+00 +1.20e+00 +1.16e+00 +1.20e+00 +1.19e+00 +1.01e+00 +1.06e+00 +1.10e+00 +1.06e+00 +1.02e+00 +1.01e+00 +1.07e+00 +4.13e-01 +1.32e-01 +1.53e+00 +1.26e+00 +1.05e+00 +1.47e+00 +1.43e+00 +1.20e+00 +8.45e-01 +6.05e-01 +9.60e-01 +9.09e-01 +6.43e-01 +6.18e-01 +5.19e-01 +2.26e-01 +8.71e-02 +5.78e-01 +4.15e-01 +4.47e-01 +4.72e-01 +5.79e-01 +4.50e-01 +3.73e-01 +1.91e-01 +3.18e-01 +3.21e-01 +2.12e-01 +2.08e-01 +1.59e-01 +1.30e-01 +6.29e-02 +8.13e-01 +7.92e-01 +7.98e-01 +8.10e-01 +8.01e-01 +7.94e-01 +7.98e-01 +7.55e-01 +7.35e-01 +7.76e-01 +7.54e-01 +8.38e-01 +1.23e+00 +1.59e+00 +6.35e-01 +8.22e-01 +8.05e-01 +7.80e-01 +8.16e-01 +8.13e-01 +7.71e-01 +7.48e-01 +7.45e-01 +7.82e-01 +7.91e-01 +7.88e-01 +8.20e-01 +1.19e+00 +1.52e+00 +5.64e-01 +8.23e-01 +8.14e-01 +8.08e-01 +7.83e-01 +7.86e-01 +7.83e-01 +7.67e-01 +7.43e-01 +7.86e-01 +8.17e-01 +8.13e-01 +8.52e-01 +1.19e+00 +1.15e+00 +4.23e-01 +8.08e-01 +8.13e-01 +7.88e-01 +7.92e-01 +7.82e-01 +7.73e-01 +7.50e-01 +7.57e-01 +7.60e-01 +8.01e-01 +7.85e-01 +8.21e-01 +1.23e+00 +1.08e+00 +3.68e-01 +7.79e-01 +7.72e-01 +7.80e-01 +8.09e-01 +7.59e-01 +7.50e-01 +7.76e-01 +7.62e-01 +7.59e-01 +7.48e-01 +7.90e-01 +8.33e-01 +1.23e+00 +1.60e+00 +5.75e-01 +7.85e-01 +7.73e-01 +7.79e-01 +7.88e-01 +7.39e-01 +7.13e-01 +7.08e-01 +7.78e-01 +7.45e-01 +7.62e-01 +7.55e-01 +7.92e-01 +1.16e+00 +1.31e+00 +4.76e-01 +7.40e-01 +7.79e-01 +7.94e-01 +7.60e-01 +7.99e-01 +7.16e-01 +7.14e-01 +7.21e-01 +7.25e-01 +7.16e-01 +7.41e-01 +7.53e-01 +1.04e+00 +1.05e+00 +3.92e-01 +7.83e-01 +8.02e-01 +7.76e-01 +7.66e-01 +7.84e-01 +7.21e-01 +7.22e-01 +6.78e-01 +6.82e-01 +7.30e-01 +7.15e-01 +7.28e-01 +1.08e+00 +8.33e-01 +3.54e-01 +7.96e-01 +7.79e-01 +7.80e-01 +7.68e-01 +7.67e-01 +7.60e-01 +7.52e-01 +7.12e-01 +6.86e-01 +6.95e-01 +7.05e-01 +7.37e-01 +1.02e+00 +8.76e-01 +2.76e-01 +8.13e-01 +8.09e-01 +7.87e-01 +7.66e-01 +7.26e-01 +7.62e-01 +7.59e-01 +7.17e-01 +7.04e-01 +6.89e-01 +6.94e-01 +7.59e-01 +1.02e+00 +8.04e-01 +3.49e-01 +8.12e-01 +7.81e-01 +7.92e-01 +7.74e-01 +7.90e-01 +7.78e-01 +7.64e-01 +7.66e-01 +7.45e-01 +7.07e-01 +7.19e-01 +7.86e-01 +1.05e+00 +6.12e-01 +2.13e-01 +8.28e-01 +8.45e-01 +8.05e-01 +8.12e-01 +8.40e-01 +8.71e-01 +8.00e-01 +7.85e-01 +7.85e-01 +8.08e-01 +7.39e-01 +7.70e-01 +1.09e+00 +5.71e-01 +1.75e-01 +1.22e+00 +1.15e+00 +1.17e+00 +1.21e+00 +1.23e+00 +1.24e+00 +1.23e+00 +1.10e+00 +1.11e+00 +1.06e+00 +1.00e+00 +1.01e+00 +1.07e+00 +4.47e-01 +1.50e-01 +1.39e+00 +1.31e+00 +1.26e+00 +1.31e+00 +1.50e+00 +1.34e+00 +1.46e+00 +9.51e-01 +1.01e+00 +7.68e-01 +5.65e-01 +6.36e-01 +4.41e-01 +1.94e-01 +7.74e-02 +5.17e-01 +4.55e-01 +4.54e-01 +4.50e-01 +5.33e-01 +4.37e-01 +5.86e-01 +2.84e-01 +3.58e-01 +2.61e-01 +1.66e-01 +1.75e-01 +1.34e-01 +8.28e-02 +5.70e-02 +7.29e-01 +7.75e-01 +7.84e-01 +7.97e-01 +8.15e-01 +7.72e-01 +7.47e-01 +7.39e-01 +7.27e-01 +7.42e-01 +7.41e-01 +7.72e-01 +1.22e+00 +1.35e+00 +4.17e-01 +7.68e-01 +7.93e-01 +7.83e-01 +7.57e-01 +7.76e-01 +7.86e-01 +7.98e-01 +7.43e-01 +7.69e-01 +7.51e-01 +7.70e-01 +8.08e-01 +1.20e+00 +1.48e+00 +5.74e-01 +7.98e-01 +8.06e-01 +8.03e-01 +7.63e-01 +7.79e-01 +7.61e-01 +7.64e-01 +7.83e-01 +7.61e-01 +7.60e-01 +7.76e-01 +8.43e-01 +1.25e+00 +1.26e+00 +4.08e-01 +7.94e-01 +7.94e-01 +7.59e-01 +7.50e-01 +7.63e-01 +7.94e-01 +7.36e-01 +7.53e-01 +7.57e-01 +7.78e-01 +7.78e-01 +8.22e-01 +1.20e+00 +1.23e+00 +4.03e-01 +7.88e-01 +7.93e-01 +7.89e-01 +7.79e-01 +7.63e-01 +7.30e-01 +7.40e-01 +7.42e-01 +7.36e-01 +7.64e-01 +7.39e-01 +8.10e-01 +1.19e+00 +1.33e+00 +5.12e-01 +7.76e-01 +7.68e-01 +7.84e-01 +7.62e-01 +7.39e-01 +7.49e-01 +7.00e-01 +6.85e-01 +7.48e-01 +7.48e-01 +7.30e-01 +7.60e-01 +1.12e+00 +1.11e+00 +4.49e-01 +7.89e-01 +8.02e-01 +7.72e-01 +7.35e-01 +7.33e-01 +7.24e-01 +7.11e-01 +6.42e-01 +6.64e-01 +7.00e-01 +7.57e-01 +7.64e-01 +1.04e+00 +7.18e-01 +2.59e-01 +7.83e-01 +7.69e-01 +7.68e-01 +7.09e-01 +7.54e-01 +7.20e-01 +6.74e-01 +6.39e-01 +5.53e-01 +6.99e-01 +7.47e-01 +7.76e-01 +1.11e+00 +6.53e-01 +2.06e-01 +7.78e-01 +7.65e-01 +7.86e-01 +7.10e-01 +7.60e-01 +7.43e-01 +7.27e-01 +6.62e-01 +6.51e-01 +6.35e-01 +7.04e-01 +7.07e-01 +1.06e+00 +7.73e-01 +2.21e-01 +7.46e-01 +7.60e-01 +7.63e-01 +7.80e-01 +7.34e-01 +7.23e-01 +7.02e-01 +6.87e-01 +7.13e-01 +6.75e-01 +6.69e-01 +7.11e-01 +9.92e-01 +6.25e-01 +1.99e-01 +7.86e-01 +7.39e-01 +7.47e-01 +7.61e-01 +7.48e-01 +7.41e-01 +7.50e-01 +7.38e-01 +7.18e-01 +7.15e-01 +6.76e-01 +6.94e-01 +9.71e-01 +5.36e-01 +1.55e-01 +8.10e-01 +8.03e-01 +8.04e-01 +8.15e-01 +8.13e-01 +8.02e-01 +7.77e-01 +7.76e-01 +7.50e-01 +7.78e-01 +7.13e-01 +6.60e-01 +9.13e-01 +5.08e-01 +1.25e-01 +1.17e+00 +1.12e+00 +1.10e+00 +1.13e+00 +1.21e+00 +1.17e+00 +1.07e+00 +1.02e+00 +1.07e+00 +1.08e+00 +1.10e+00 +8.99e-01 +8.82e-01 +4.84e-01 +1.77e-01 +1.56e+00 +1.32e+00 +1.13e+00 +1.34e+00 +1.30e+00 +1.22e+00 +1.17e+00 +9.07e-01 +7.99e-01 +7.33e-01 +7.55e-01 +6.69e-01 +3.68e-01 +2.03e-01 +1.10e-01 +5.31e-01 +4.95e-01 +4.39e-01 +4.81e-01 +4.96e-01 +3.78e-01 +5.26e-01 +3.10e-01 +2.83e-01 +1.99e-01 +2.15e-01 +2.39e-01 +1.44e-01 +7.93e-02 +4.46e-02 +8.04e-01 +7.95e-01 +7.83e-01 +7.95e-01 +7.54e-01 +7.47e-01 +7.72e-01 +7.56e-01 +7.67e-01 +7.43e-01 +7.58e-01 +7.40e-01 +1.14e+00 +9.11e-01 +4.11e-01 +8.04e-01 +7.68e-01 +7.85e-01 +7.81e-01 +7.65e-01 +7.45e-01 +7.31e-01 +7.07e-01 +7.56e-01 +7.56e-01 +7.59e-01 +7.59e-01 +1.11e+00 +1.04e+00 +3.91e-01 +7.71e-01 +7.89e-01 +7.60e-01 +7.67e-01 +7.41e-01 +7.47e-01 +7.26e-01 +7.15e-01 +7.19e-01 +7.39e-01 +7.88e-01 +7.73e-01 +1.12e+00 +1.24e+00 +5.06e-01 +7.87e-01 +7.41e-01 +7.55e-01 +7.36e-01 +7.30e-01 +7.59e-01 +7.40e-01 +7.37e-01 +7.32e-01 +7.38e-01 +7.44e-01 +7.85e-01 +1.11e+00 +8.43e-01 +3.26e-01 +7.83e-01 +7.76e-01 +7.73e-01 +7.64e-01 +7.54e-01 +7.73e-01 +7.28e-01 +7.45e-01 +7.11e-01 +7.24e-01 +7.27e-01 +7.83e-01 +1.10e+00 +6.80e-01 +2.48e-01 +7.83e-01 +7.52e-01 +7.66e-01 +7.85e-01 +7.69e-01 +7.48e-01 +7.23e-01 +6.99e-01 +6.49e-01 +6.92e-01 +7.08e-01 +7.57e-01 +1.07e+00 +7.06e-01 +2.16e-01 +7.82e-01 +7.85e-01 +7.66e-01 +7.58e-01 +7.56e-01 +7.22e-01 +7.09e-01 +6.42e-01 +6.03e-01 +6.32e-01 +6.99e-01 +7.64e-01 +1.08e+00 +8.03e-01 +2.40e-01 +7.40e-01 +7.41e-01 +7.33e-01 +7.45e-01 +7.43e-01 +7.15e-01 +6.99e-01 +6.15e-01 +4.48e-01 +6.70e-01 +7.13e-01 +7.40e-01 +1.05e+00 +8.40e-01 +2.51e-01 +7.67e-01 +3.55e-01 +7.36e-01 +7.41e-01 +7.16e-01 +7.29e-01 +3.42e-01 +6.33e-01 +6.18e-01 +6.57e-01 +7.08e-01 +7.42e-01 +1.00e+00 +7.79e-01 +2.57e-01 +7.51e-01 +7.62e-01 +7.59e-01 +7.36e-01 +7.09e-01 +6.77e-01 +6.93e-01 +6.91e-01 +6.86e-01 +6.77e-01 +6.81e-01 +6.82e-01 +9.60e-01 +6.08e-01 +2.38e-01 +7.58e-01 +8.07e-01 +7.45e-01 +7.27e-01 +7.02e-01 +6.98e-01 +6.68e-01 +6.74e-01 +6.92e-01 +6.88e-01 +6.37e-01 +6.76e-01 +8.84e-01 +3.75e-01 +1.32e-01 +7.90e-01 +7.78e-01 +7.50e-01 +7.25e-01 +7.49e-01 +7.50e-01 +7.08e-01 +7.12e-01 +7.61e-01 +7.28e-01 +6.88e-01 +6.51e-01 +7.19e-01 +3.41e-01 +1.43e-01 +1.14e+00 +1.06e+00 +1.04e+00 +1.06e+00 +1.09e+00 +1.05e+00 +1.03e+00 +9.47e-01 +1.04e+00 +9.40e-01 +9.42e-01 +8.31e-01 +6.65e-01 +2.06e-01 +8.49e-02 +1.33e+00 +1.35e+00 +1.16e+00 +8.61e-01 +8.28e-01 +1.08e+00 +9.81e-01 +7.58e-01 +7.04e-01 +6.54e-01 +7.20e-01 +5.86e-01 +2.41e-01 +1.04e-01 +4.32e-02 +4.74e-01 +5.21e-01 +4.51e-01 +2.77e-01 +2.70e-01 +3.48e-01 +3.55e-01 +2.46e-01 +2.37e-01 +1.76e-01 +2.35e-01 +2.02e-01 +8.43e-02 +4.62e-02 +2.99e-02 +7.31e-01 +7.48e-01 +7.39e-01 +7.66e-01 +7.81e-01 +7.83e-01 +7.80e-01 +7.68e-01 +7.20e-01 +7.09e-01 +7.19e-01 +7.61e-01 +1.05e+00 +7.78e-01 +2.76e-01 +7.52e-01 +7.85e-01 +7.84e-01 +7.87e-01 +7.71e-01 +7.57e-01 +7.62e-01 +7.09e-01 +7.08e-01 +6.96e-01 +7.23e-01 +7.49e-01 +1.06e+00 +5.92e-01 +1.96e-01 +7.43e-01 +7.38e-01 +7.76e-01 +7.70e-01 +7.56e-01 +7.48e-01 +7.41e-01 +7.42e-01 +7.06e-01 +6.90e-01 +7.25e-01 +7.69e-01 +1.04e+00 +8.10e-01 +2.94e-01 +7.40e-01 +7.47e-01 +7.57e-01 +7.48e-01 +7.55e-01 +7.44e-01 +7.42e-01 +7.35e-01 +6.70e-01 +6.64e-01 +7.63e-01 +7.71e-01 +9.80e-01 +7.74e-01 +3.07e-01 +7.55e-01 +7.66e-01 +7.97e-01 +7.40e-01 +7.53e-01 +7.39e-01 +7.26e-01 +7.07e-01 +6.89e-01 +6.87e-01 +7.44e-01 +7.30e-01 +1.03e+00 +6.26e-01 +1.73e-01 +7.55e-01 +7.36e-01 +7.61e-01 +7.64e-01 +7.21e-01 +7.01e-01 +7.26e-01 +6.89e-01 +6.95e-01 +6.71e-01 +6.81e-01 +7.49e-01 +9.78e-01 +6.44e-01 +2.24e-01 +7.44e-01 +7.16e-01 +7.45e-01 +7.32e-01 +7.41e-01 +7.10e-01 +6.77e-01 +6.35e-01 +6.21e-01 +6.30e-01 +6.43e-01 +6.90e-01 +9.34e-01 +7.67e-01 +2.92e-01 +7.32e-01 +7.55e-01 +7.18e-01 +7.24e-01 +7.26e-01 +7.05e-01 +6.53e-01 +6.21e-01 +6.43e-01 +6.52e-01 +7.13e-01 +7.09e-01 +9.53e-01 +6.82e-01 +2.54e-01 +6.97e-01 +7.00e-01 +7.14e-01 +6.97e-01 +7.15e-01 +6.93e-01 +6.77e-01 +6.33e-01 +6.17e-01 +6.11e-01 +7.05e-01 +6.87e-01 +9.29e-01 +5.46e-01 +1.70e-01 +7.20e-01 +7.06e-01 +7.18e-01 +6.87e-01 +6.85e-01 +6.94e-01 +6.44e-01 +6.64e-01 +5.99e-01 +5.89e-01 +6.44e-01 +6.80e-01 +9.68e-01 +6.14e-01 +1.98e-01 +7.51e-01 +7.34e-01 +7.39e-01 +6.97e-01 +6.75e-01 +6.82e-01 +6.70e-01 +7.02e-01 +6.60e-01 +6.35e-01 +6.16e-01 +6.32e-01 +8.05e-01 +4.17e-01 +1.09e-01 +7.89e-01 +7.57e-01 +7.50e-01 +7.29e-01 +7.26e-01 +6.77e-01 +6.48e-01 +7.04e-01 +6.97e-01 +6.79e-01 +6.60e-01 +6.12e-01 +7.24e-01 +2.74e-01 +7.82e-02 +1.14e+00 +1.03e+00 +1.04e+00 +9.43e-01 +1.02e+00 +9.87e-01 +8.16e-01 +8.35e-01 +8.39e-01 +9.59e-01 +9.14e-01 +7.74e-01 +5.71e-01 +1.72e-01 +5.75e-02 +1.12e+00 +1.16e+00 +8.35e-01 +7.13e-01 +7.64e-01 +8.80e-01 +5.67e-01 +4.88e-01 +4.31e-01 +7.46e-01 +6.47e-01 +4.18e-01 +2.22e-01 +6.04e-02 +2.42e-02 +3.88e-01 +3.92e-01 +2.68e-01 +2.61e-01 +2.31e-01 +3.03e-01 +2.09e-01 +1.58e-01 +1.36e-01 +2.55e-01 +2.03e-01 +1.38e-01 +9.69e-02 +4.41e-02 +1.17e-02 +7.71e-01 +7.88e-01 +8.12e-01 +7.42e-01 +7.85e-01 +7.60e-01 +7.46e-01 +7.71e-01 +7.12e-01 +6.80e-01 +6.82e-01 +7.51e-01 +1.04e+00 +6.16e-01 +1.78e-01 +7.57e-01 +7.66e-01 +7.96e-01 +7.87e-01 +7.31e-01 +7.75e-01 +7.52e-01 +7.45e-01 +7.07e-01 +7.05e-01 +7.08e-01 +7.39e-01 +9.99e-01 +6.60e-01 +2.06e-01 +7.40e-01 +7.48e-01 +7.67e-01 +7.62e-01 +7.72e-01 +7.12e-01 +7.41e-01 +7.52e-01 +6.99e-01 +6.87e-01 +6.89e-01 +7.63e-01 +1.10e+00 +8.27e-01 +2.75e-01 +7.59e-01 +7.20e-01 +7.47e-01 +7.32e-01 +7.40e-01 +7.41e-01 +7.25e-01 +7.31e-01 +6.89e-01 +7.01e-01 +7.16e-01 +6.95e-01 +9.76e-01 +8.88e-01 +3.69e-01 +7.39e-01 +7.23e-01 +7.22e-01 +7.34e-01 +7.32e-01 +7.19e-01 +7.08e-01 +7.23e-01 +7.13e-01 +6.86e-01 +7.13e-01 +7.47e-01 +9.68e-01 +6.86e-01 +2.05e-01 +7.59e-01 +7.40e-01 +7.23e-01 +6.89e-01 +6.89e-01 +6.98e-01 +7.04e-01 +6.61e-01 +6.80e-01 +6.80e-01 +6.86e-01 +7.42e-01 +1.01e+00 +7.65e-01 +2.63e-01 +7.27e-01 +7.07e-01 +7.21e-01 +6.82e-01 +6.82e-01 +6.79e-01 +6.66e-01 +6.58e-01 +6.32e-01 +6.61e-01 +6.87e-01 +6.88e-01 +8.56e-01 +5.97e-01 +2.13e-01 +7.02e-01 +7.13e-01 +7.28e-01 +6.93e-01 +6.91e-01 +6.69e-01 +6.46e-01 +6.28e-01 +6.40e-01 +6.38e-01 +6.71e-01 +6.96e-01 +8.50e-01 +3.86e-01 +1.50e-01 +6.91e-01 +5.58e-01 +6.91e-01 +7.07e-01 +7.18e-01 +7.31e-01 +6.70e-01 +5.57e-01 +6.18e-01 +6.02e-01 +6.11e-01 +6.66e-01 +9.21e-01 +4.38e-01 +1.13e-01 +7.24e-01 +6.84e-01 +6.84e-01 +6.47e-01 +6.69e-01 +6.83e-01 +6.69e-01 +6.47e-01 +6.14e-01 +5.97e-01 +6.05e-01 +6.26e-01 +9.22e-01 +4.59e-01 +1.15e-01 +7.38e-01 +6.58e-01 +6.82e-01 +6.72e-01 +7.05e-01 +6.71e-01 +6.60e-01 +6.37e-01 +6.35e-01 +6.02e-01 +5.50e-01 +5.85e-01 +8.26e-01 +4.84e-01 +1.41e-01 +7.40e-01 +6.74e-01 +6.74e-01 +7.15e-01 +7.22e-01 +6.98e-01 +7.17e-01 +6.74e-01 +6.70e-01 +6.46e-01 +5.85e-01 +5.00e-01 +4.95e-01 +2.74e-01 +8.29e-02 +1.11e+00 +9.99e-01 +1.02e+00 +1.05e+00 +1.05e+00 +9.96e-01 +9.29e-01 +9.64e-01 +8.97e-01 +9.51e-01 +8.48e-01 +7.23e-01 +6.11e-01 +2.17e-01 +6.09e-02 +1.15e+00 +9.21e-01 +8.94e-01 +8.34e-01 +8.60e-01 +8.05e-01 +5.65e-01 +5.80e-01 +5.28e-01 +5.64e-01 +6.00e-01 +4.16e-01 +2.18e-01 +8.48e-02 +2.93e-02 +3.50e-01 +2.74e-01 +2.33e-01 +2.38e-01 +2.50e-01 +2.74e-01 +1.88e-01 +1.72e-01 +1.56e-01 +1.57e-01 +1.94e-01 +1.39e-01 +6.35e-02 +3.19e-02 +7.49e-03 +7.41e-01 +7.68e-01 +7.90e-01 +7.78e-01 +7.76e-01 +7.30e-01 +7.18e-01 +7.38e-01 +7.09e-01 +7.21e-01 +7.00e-01 +7.72e-01 +9.68e-01 +6.90e-01 +2.52e-01 +7.57e-01 +7.65e-01 +7.45e-01 +7.47e-01 +7.46e-01 +7.65e-01 +7.54e-01 +7.52e-01 +7.26e-01 +6.82e-01 +7.10e-01 +7.86e-01 +1.05e+00 +7.77e-01 +2.28e-01 +7.74e-01 +7.42e-01 +7.17e-01 +7.21e-01 +7.29e-01 +7.68e-01 +7.52e-01 +7.57e-01 +7.68e-01 +6.89e-01 +6.78e-01 +7.59e-01 +1.05e+00 +8.28e-01 +2.72e-01 +7.40e-01 +7.34e-01 +7.31e-01 +7.16e-01 +7.44e-01 +7.39e-01 +7.56e-01 +7.44e-01 +7.10e-01 +6.88e-01 +6.97e-01 +7.44e-01 +9.71e-01 +7.11e-01 +2.71e-01 +7.13e-01 +6.97e-01 +7.29e-01 +7.42e-01 +7.39e-01 +7.08e-01 +7.05e-01 +7.31e-01 +6.98e-01 +6.88e-01 +7.17e-01 +7.13e-01 +9.73e-01 +6.01e-01 +1.91e-01 +7.45e-01 +7.09e-01 +7.35e-01 +7.10e-01 +7.14e-01 +7.15e-01 +7.08e-01 +7.06e-01 +6.78e-01 +6.67e-01 +6.90e-01 +7.12e-01 +9.35e-01 +5.48e-01 +2.00e-01 +7.32e-01 +7.22e-01 +7.10e-01 +7.08e-01 +7.09e-01 +7.14e-01 +6.80e-01 +6.85e-01 +6.58e-01 +6.83e-01 +6.50e-01 +7.09e-01 +9.87e-01 +7.05e-01 +2.37e-01 +7.08e-01 +7.29e-01 +7.25e-01 +7.21e-01 +7.00e-01 +6.83e-01 +6.44e-01 +6.62e-01 +6.42e-01 +6.10e-01 +6.47e-01 +7.20e-01 +8.94e-01 +4.05e-01 +1.25e-01 +6.82e-01 +6.98e-01 +7.11e-01 +7.24e-01 +7.09e-01 +6.74e-01 +6.40e-01 +6.25e-01 +6.06e-01 +6.01e-01 +6.22e-01 +6.85e-01 +8.81e-01 +4.33e-01 +1.27e-01 +7.04e-01 +6.95e-01 +6.94e-01 +7.08e-01 +6.83e-01 +6.66e-01 +6.52e-01 +6.14e-01 +6.08e-01 +6.22e-01 +5.79e-01 +6.40e-01 +8.39e-01 +5.15e-01 +1.65e-01 +7.06e-01 +6.74e-01 +6.69e-01 +6.66e-01 +6.63e-01 +6.70e-01 +6.87e-01 +6.53e-01 +6.04e-01 +5.84e-01 +5.54e-01 +5.65e-01 +7.87e-01 +4.40e-01 +1.48e-01 +7.67e-01 +6.96e-01 +7.26e-01 +7.26e-01 +6.91e-01 +7.44e-01 +7.07e-01 +6.80e-01 +6.22e-01 +6.16e-01 +5.70e-01 +4.85e-01 +5.06e-01 +2.87e-01 +9.16e-02 +1.20e+00 +9.57e-01 +8.97e-01 +1.03e+00 +9.83e-01 +1.01e+00 +9.58e-01 +1.03e+00 +9.95e-01 +1.02e+00 +8.79e-01 +6.76e-01 +5.34e-01 +1.83e-01 +6.50e-02 +9.53e-01 +7.68e-01 +7.45e-01 +8.42e-01 +9.10e-01 +8.62e-01 +7.39e-01 +7.46e-01 +6.49e-01 +5.31e-01 +4.80e-01 +3.53e-01 +2.00e-01 +8.55e-02 +3.73e-02 +3.66e-01 +3.07e-01 +3.18e-01 +2.87e-01 +3.14e-01 +2.90e-01 +2.36e-01 +2.15e-01 +2.24e-01 +1.70e-01 +1.63e-01 +1.15e-01 +6.32e-02 +3.84e-02 +1.78e-02 +7.11e-01 +7.35e-01 +7.78e-01 +7.95e-01 +7.89e-01 +7.78e-01 +7.90e-01 +8.15e-01 +7.38e-01 +7.56e-01 +7.46e-01 +7.19e-01 +8.73e-01 +4.87e-01 +1.89e-01 +7.24e-01 +7.51e-01 +7.47e-01 +7.52e-01 +7.63e-01 +7.61e-01 +7.53e-01 +7.70e-01 +7.59e-01 +7.54e-01 +7.39e-01 +7.28e-01 +9.65e-01 +6.51e-01 +2.08e-01 +7.30e-01 +7.39e-01 +7.32e-01 +7.64e-01 +7.36e-01 +7.69e-01 +7.88e-01 +7.27e-01 +7.35e-01 +7.32e-01 +7.27e-01 +7.50e-01 +9.97e-01 +7.08e-01 +2.42e-01 +7.31e-01 +7.41e-01 +7.80e-01 +7.20e-01 +7.72e-01 +7.80e-01 +7.58e-01 +7.29e-01 +7.22e-01 +6.85e-01 +6.91e-01 +7.32e-01 +9.70e-01 +5.55e-01 +1.80e-01 +7.25e-01 +7.42e-01 +7.54e-01 +7.29e-01 +7.69e-01 +7.68e-01 +7.23e-01 +7.19e-01 +7.39e-01 +6.99e-01 +7.30e-01 +7.43e-01 +9.67e-01 +5.29e-01 +1.58e-01 +7.27e-01 +7.38e-01 +7.50e-01 +7.78e-01 +7.29e-01 +7.53e-01 +7.20e-01 +7.07e-01 +6.58e-01 +7.03e-01 +7.03e-01 +6.98e-01 +1.02e+00 +5.77e-01 +1.55e-01 +7.04e-01 +7.29e-01 +7.04e-01 +7.41e-01 +7.23e-01 +7.27e-01 +7.22e-01 +6.89e-01 +6.68e-01 +6.72e-01 +6.90e-01 +7.11e-01 +9.24e-01 +6.32e-01 +1.76e-01 +6.92e-01 +7.24e-01 +7.27e-01 +7.47e-01 +7.06e-01 +7.02e-01 +6.65e-01 +6.69e-01 +6.63e-01 +6.78e-01 +6.76e-01 +6.84e-01 +8.81e-01 +5.82e-01 +2.05e-01 +6.77e-01 +6.97e-01 +7.18e-01 +7.16e-01 +7.22e-01 +6.99e-01 +6.33e-01 +6.46e-01 +6.22e-01 +6.20e-01 +6.04e-01 +6.27e-01 +8.72e-01 +4.59e-01 +1.30e-01 +6.94e-01 +6.76e-01 +6.98e-01 +7.17e-01 +7.24e-01 +6.61e-01 +6.58e-01 +6.43e-01 +5.98e-01 +6.12e-01 +5.72e-01 +5.75e-01 +7.13e-01 +4.13e-01 +1.41e-01 +6.83e-01 +6.80e-01 +6.85e-01 +6.92e-01 +6.94e-01 +6.60e-01 +6.70e-01 +6.56e-01 +5.95e-01 +5.92e-01 +5.52e-01 +5.66e-01 +7.57e-01 +2.75e-01 +8.14e-02 +6.80e-01 +6.88e-01 +6.98e-01 +7.21e-01 +6.86e-01 +7.14e-01 +7.21e-01 +6.93e-01 +6.28e-01 +6.06e-01 +5.69e-01 +5.25e-01 +6.18e-01 +2.39e-01 +7.13e-02 +9.78e-01 +8.94e-01 +9.01e-01 +9.75e-01 +9.93e-01 +9.72e-01 +9.04e-01 +9.60e-01 +9.01e-01 +8.02e-01 +8.03e-01 +6.71e-01 +3.57e-01 +1.04e-01 +3.71e-02 +8.79e-01 +4.74e-01 +6.09e-01 +6.57e-01 +6.73e-01 +6.70e-01 +5.29e-01 +7.08e-01 +5.73e-01 +3.74e-01 +4.21e-01 +2.98e-01 +1.43e-01 +5.64e-02 +1.94e-02 +2.87e-01 +1.53e-01 +1.84e-01 +2.04e-01 +2.63e-01 +2.36e-01 +1.65e-01 +2.34e-01 +1.92e-01 +1.28e-01 +1.20e-01 +7.57e-02 +5.20e-02 +3.12e-02 +1.69e-02 +7.55e-01 +8.03e-01 +8.38e-01 +8.11e-01 +8.12e-01 +8.21e-01 +8.19e-01 +7.76e-01 +7.65e-01 +7.57e-01 +7.69e-01 +7.23e-01 +7.14e-01 +1.75e-01 +6.92e-02 +8.24e-01 +8.38e-01 +8.21e-01 +7.68e-01 +7.55e-01 +7.68e-01 +7.83e-01 +7.52e-01 +7.51e-01 +7.46e-01 +7.74e-01 +7.44e-01 +8.02e-01 +3.34e-01 +1.21e-01 +7.93e-01 +8.41e-01 +7.94e-01 +7.70e-01 +7.76e-01 +8.31e-01 +8.21e-01 +7.99e-01 +7.37e-01 +7.45e-01 +7.12e-01 +8.00e-01 +1.03e+00 +5.17e-01 +1.38e-01 +7.80e-01 +7.93e-01 +7.72e-01 +7.70e-01 +7.94e-01 +8.27e-01 +2.53e-01 +7.14e-01 +7.62e-01 +7.24e-01 +6.76e-01 +7.22e-01 +8.97e-01 +4.58e-01 +1.53e-01 +7.51e-01 +7.55e-01 +7.71e-01 +7.75e-01 +7.94e-01 +7.94e-01 +7.67e-01 +7.35e-01 +7.64e-01 +7.55e-01 +7.00e-01 +7.25e-01 +9.50e-01 +5.35e-01 +1.47e-01 +7.48e-01 +7.62e-01 +7.42e-01 +7.69e-01 +7.62e-01 +7.55e-01 +7.49e-01 +7.14e-01 +7.33e-01 +7.29e-01 +6.88e-01 +6.77e-01 +8.92e-01 +4.73e-01 +1.54e-01 +7.07e-01 +7.12e-01 +7.63e-01 +7.78e-01 +7.67e-01 +7.39e-01 +7.39e-01 +7.04e-01 +6.89e-01 +7.02e-01 +7.00e-01 +6.50e-01 +7.45e-01 +4.80e-01 +1.67e-01 +7.10e-01 +7.31e-01 +7.02e-01 +7.48e-01 +7.26e-01 +7.23e-01 +7.48e-01 +7.26e-01 +7.06e-01 +6.63e-01 +6.82e-01 +6.87e-01 +8.55e-01 +3.87e-01 +1.27e-01 +7.27e-01 +7.16e-01 +7.34e-01 +7.54e-01 +7.46e-01 +7.70e-01 +7.11e-01 +6.87e-01 +6.70e-01 +6.64e-01 +6.35e-01 +6.35e-01 +8.28e-01 +3.65e-01 +1.19e-01 +7.19e-01 +6.95e-01 +7.56e-01 +7.46e-01 +7.40e-01 +7.30e-01 +6.89e-01 +6.43e-01 +6.17e-01 +5.90e-01 +6.17e-01 +5.70e-01 +8.00e-01 +3.80e-01 +1.34e-01 +6.68e-01 +6.64e-01 +6.96e-01 +7.05e-01 +7.48e-01 +7.14e-01 +6.55e-01 +6.42e-01 +6.17e-01 +5.96e-01 +5.59e-01 +5.39e-01 +6.83e-01 +2.51e-01 +7.41e-02 +6.65e-01 +2.67e-01 +6.72e-01 +6.79e-01 +6.77e-01 +6.50e-01 +6.85e-01 +6.99e-01 +6.33e-01 +6.12e-01 +5.40e-01 +5.07e-01 +5.77e-01 +2.23e-01 +6.42e-02 +8.66e-01 +8.00e-01 +8.00e-01 +8.48e-01 +8.27e-01 +8.32e-01 +8.36e-01 +9.02e-01 +9.05e-01 +7.29e-01 +6.76e-01 +5.88e-01 +3.48e-01 +1.10e-01 +3.71e-02 +6.49e-01 +3.52e-01 +4.24e-01 +5.24e-01 +4.05e-01 +4.08e-01 +3.66e-01 +3.97e-01 +5.38e-01 +3.80e-01 +2.81e-01 +2.19e-01 +1.32e-01 +3.23e-02 +1.05e-02 +2.57e-01 +1.34e-01 +1.74e-01 +1.81e-01 +1.46e-01 +1.42e-01 +1.23e-01 +1.30e-01 +1.65e-01 +1.27e-01 +8.17e-02 +7.95e-02 +5.09e-02 +1.73e-02 +7.50e-03 +1.08e+00 +1.13e+00 +1.22e+00 +1.22e+00 +1.16e+00 +1.11e+00 +1.11e+00 +1.06e+00 +1.11e+00 +9.89e-01 +9.97e-01 +1.02e+00 +7.84e-01 +1.55e-01 +3.93e-02 +1.15e+00 +1.05e+00 +1.23e+00 +1.10e+00 +1.10e+00 +1.15e+00 +1.11e+00 +1.03e+00 +9.91e-01 +1.02e+00 +9.48e-01 +9.15e-01 +8.91e-01 +2.88e-01 +8.05e-02 +1.12e+00 +1.04e+00 +1.18e+00 +1.10e+00 +1.06e+00 +1.18e+00 +1.04e+00 +1.15e+00 +1.03e+00 +1.04e+00 +9.63e-01 +9.19e-01 +1.05e+00 +4.94e-01 +1.55e-01 +1.12e+00 +1.03e+00 +1.09e+00 +1.12e+00 +1.10e+00 +1.19e+00 +1.11e+00 +1.10e+00 +1.06e+00 +1.01e+00 +8.73e-01 +8.08e-01 +8.67e-01 +3.12e-01 +9.00e-02 +1.08e+00 +1.03e+00 +1.06e+00 +1.10e+00 +1.04e+00 +1.13e+00 +1.27e+00 +1.09e+00 +1.08e+00 +1.12e+00 +9.93e-01 +8.86e-01 +7.84e-01 +3.95e-01 +1.46e-01 +1.07e+00 +9.78e-01 +1.03e+00 +1.02e+00 +1.00e+00 +1.09e+00 +1.08e+00 +1.01e+00 +1.03e+00 +1.07e+00 +9.59e-01 +8.28e-01 +8.42e-01 +4.01e-01 +1.22e-01 +9.48e-01 +9.63e-01 +1.06e+00 +1.04e+00 +9.98e-01 +1.04e+00 +1.02e+00 +9.83e-01 +8.41e-01 +9.51e-01 +9.08e-01 +7.46e-01 +7.77e-01 +3.05e-01 +9.78e-02 +9.70e-01 +9.47e-01 +9.99e-01 +1.08e+00 +1.11e+00 +1.03e+00 +1.01e+00 +9.93e-01 +8.83e-01 +8.87e-01 +9.14e-01 +8.38e-01 +6.43e-01 +1.75e-01 +5.80e-02 +9.67e-01 +9.99e-01 +1.01e+00 +1.06e+00 +1.08e+00 +9.67e-01 +1.08e+00 +1.02e+00 +8.98e-01 +9.10e-01 +8.57e-01 +8.25e-01 +8.06e-01 +2.24e-01 +7.49e-02 +9.70e-01 +1.03e+00 +1.10e+00 +1.01e+00 +1.06e+00 +9.81e-01 +1.01e+00 +9.04e-01 +8.30e-01 +8.02e-01 +8.09e-01 +8.10e-01 +7.80e-01 +2.46e-01 +7.63e-02 +8.90e-01 +9.28e-01 +1.02e+00 +1.02e+00 +9.83e-01 +1.07e+00 +9.40e-01 +8.16e-01 +7.84e-01 +6.91e-01 +7.33e-01 +6.96e-01 +5.05e-01 +2.10e-01 +7.72e-02 +7.36e-01 +7.62e-01 +8.87e-01 +8.71e-01 +9.21e-01 +9.57e-01 +1.00e+00 +9.26e-01 +8.38e-01 +7.10e-01 +6.68e-01 +6.38e-01 +4.88e-01 +1.47e-01 +5.84e-02 +6.38e-01 +6.97e-01 +7.81e-01 +7.21e-01 +8.22e-01 +8.29e-01 +6.94e-01 +9.33e-01 +9.36e-01 +7.85e-01 +5.91e-01 +4.82e-01 +3.15e-01 +9.11e-02 +4.04e-02 +3.59e-01 +2.19e-01 +2.60e-01 +3.06e-01 +2.48e-01 +2.41e-01 +2.20e-01 +2.69e-01 +4.39e-01 +2.89e-01 +1.35e-01 +1.30e-01 +7.47e-02 +2.46e-02 +1.39e-02 +1.37e-01 +7.05e-02 +9.05e-02 +9.63e-02 +7.38e-02 +7.57e-02 +7.98e-02 +7.83e-02 +1.40e-01 +1.09e-01 +5.52e-02 +4.14e-02 +2.38e-02 +1.27e-02 +4.98e-03 +1.08e+00 +1.06e+00 +1.34e+00 +9.87e-01 +1.49e+00 +1.22e+00 +1.08e+00 +1.13e+00 +9.74e-01 +8.45e-01 +6.07e-01 +6.33e-01 +5.34e-01 +1.43e-01 +3.98e-02 +1.35e+00 +9.68e-01 +1.60e+00 +1.80e+00 +1.45e+00 +1.17e+00 +9.94e-01 +1.02e+00 +8.65e-01 +7.01e-01 +6.46e-01 +5.40e-01 +4.50e-01 +2.04e-01 +7.38e-02 +1.18e+00 +1.14e+00 +1.48e+00 +1.19e+00 +9.38e-01 +1.19e+00 +9.74e-01 +1.02e+00 +9.23e-01 +8.23e-01 +8.89e-01 +7.01e-01 +3.85e-01 +2.16e-01 +1.44e-01 +1.32e+00 +1.14e+00 +1.36e+00 +1.53e+00 +1.16e+00 +1.29e+00 +1.36e+00 +1.18e+00 +1.09e+00 +8.67e-01 +6.32e-01 +4.57e-01 +3.13e-01 +1.52e-01 +6.87e-02 +1.30e+00 +9.74e-01 +1.29e+00 +1.54e+00 +1.33e+00 +1.38e+00 +1.35e+00 +1.13e+00 +9.14e-01 +8.56e-01 +7.71e-01 +4.06e-01 +2.63e-01 +1.30e-01 +6.54e-02 +1.05e+00 +7.94e-01 +1.12e+00 +1.15e+00 +8.13e-01 +9.20e-01 +1.09e+00 +8.11e-01 +6.93e-01 +8.96e-01 +8.00e-01 +5.26e-01 +2.87e-01 +1.23e-01 +7.75e-02 +7.48e-01 +9.01e-01 +1.04e+00 +1.05e+00 +9.13e-01 +1.00e+00 +9.12e-01 +7.83e-01 +6.62e-01 +6.12e-01 +4.88e-01 +3.46e-01 +2.31e-01 +1.33e-01 +7.44e-02 +5.62e-01 +7.11e-01 +8.51e-01 +8.66e-01 +9.29e-01 +9.65e-01 +6.70e-01 +6.37e-01 +4.77e-01 +4.37e-01 +4.96e-01 +3.94e-01 +2.26e-01 +1.06e-01 +3.96e-02 +6.10e-01 +6.97e-01 +8.44e-01 +1.11e+00 +8.69e-01 +7.98e-01 +6.03e-01 +7.36e-01 +5.96e-01 +5.37e-01 +4.95e-01 +4.22e-01 +2.94e-01 +1.01e-01 +4.37e-02 +4.56e-01 +8.07e-01 +1.07e+00 +8.05e-01 +7.16e-01 +5.03e-01 +6.26e-01 +7.10e-01 +5.73e-01 +4.58e-01 +4.44e-01 +3.45e-01 +2.60e-01 +1.36e-01 +4.22e-02 +4.93e-01 +6.13e-01 +8.04e-01 +5.55e-01 +7.11e-01 +7.23e-01 +5.94e-01 +5.56e-01 +4.34e-01 +2.96e-01 +3.80e-01 +3.42e-01 +2.09e-01 +1.08e-01 +4.84e-02 +4.28e-01 +3.61e-01 +5.20e-01 +4.86e-01 +4.81e-01 +6.92e-01 +5.95e-01 +3.74e-01 +3.26e-01 +2.76e-01 +2.59e-01 +2.57e-01 +1.85e-01 +4.96e-02 +1.83e-02 +2.15e-01 +2.66e-01 +3.14e-01 +2.45e-01 +2.95e-01 +4.30e-01 +3.75e-01 +3.67e-01 +3.05e-01 +3.09e-01 +2.21e-01 +1.80e-01 +1.34e-01 +5.03e-02 +2.00e-02 +9.60e-02 +1.01e-01 +1.15e-01 +9.88e-02 +1.27e-01 +1.67e-01 +1.28e-01 +1.60e-01 +1.75e-01 +1.43e-01 +8.60e-02 +5.91e-02 +4.19e-02 +2.09e-02 +1.01e-02 +6.92e-02 +3.71e-02 +4.18e-02 +4.29e-02 +4.54e-02 +6.16e-02 +4.09e-02 +5.12e-02 +7.23e-02 +6.74e-02 +3.14e-02 +1.80e-02 +1.64e-02 +7.47e-03 +2.97e-03 +3.66e-01 +3.67e-01 +4.98e-01 +3.34e-01 +5.46e-01 +4.10e-01 +3.75e-01 +3.97e-01 +3.66e-01 +2.85e-01 +1.96e-01 +1.98e-01 +1.79e-01 +1.05e-01 +3.65e-02 +5.51e-01 +2.62e-01 +5.35e-01 +7.29e-01 +6.22e-01 +4.43e-01 +3.56e-01 +3.26e-01 +3.18e-01 +2.53e-01 +1.98e-01 +1.61e-01 +1.35e-01 +8.96e-02 +4.34e-02 +4.38e-01 +3.47e-01 +5.06e-01 +4.90e-01 +2.64e-01 +4.25e-01 +3.58e-01 +3.11e-01 +3.30e-01 +2.32e-01 +3.09e-01 +2.65e-01 +1.43e-01 +6.12e-02 +6.39e-02 +4.34e-01 +3.64e-01 +5.30e-01 +6.02e-01 +4.03e-01 +4.34e-01 +4.80e-01 +4.48e-01 +4.35e-01 +3.28e-01 +2.19e-01 +1.55e-01 +1.04e-01 +6.22e-02 +2.93e-02 +5.28e-01 +3.92e-01 +4.89e-01 +5.69e-01 +5.08e-01 +4.82e-01 +5.01e-01 +3.73e-01 +3.58e-01 +2.50e-01 +2.62e-01 +1.19e-01 +7.33e-02 +4.35e-02 +2.75e-02 +3.24e-01 +2.74e-01 +3.53e-01 +3.82e-01 +3.09e-01 +2.64e-01 +3.42e-01 +2.59e-01 +2.24e-01 +2.72e-01 +2.89e-01 +2.17e-01 +1.01e-01 +4.22e-02 +2.14e-02 +3.08e-01 +2.90e-01 +3.71e-01 +4.11e-01 +2.41e-01 +2.90e-01 +2.99e-01 +2.76e-01 +2.53e-01 +2.06e-01 +1.62e-01 +1.37e-01 +7.91e-02 +3.97e-02 +3.67e-02 +1.54e-01 +2.67e-01 +2.58e-01 +2.66e-01 +2.93e-01 +3.18e-01 +1.96e-01 +2.10e-01 +1.51e-01 +1.19e-01 +1.30e-01 +1.25e-01 +6.88e-02 +5.68e-02 +4.11e-02 +1.94e-01 +2.10e-01 +2.78e-01 +3.44e-01 +3.33e-01 +3.12e-01 +1.89e-01 +2.43e-01 +2.08e-01 +1.74e-01 +1.46e-01 +1.34e-01 +1.11e-01 +4.65e-02 +1.94e-02 +1.16e-01 +2.46e-01 +3.24e-01 +3.12e-01 +2.29e-01 +1.48e-01 +2.01e-01 +2.33e-01 +2.12e-01 +1.40e-01 +1.33e-01 +1.13e-01 +6.83e-02 +6.78e-02 +3.69e-02 +1.55e-01 +2.51e-01 +2.94e-01 +1.53e-01 +2.32e-01 +2.02e-01 +1.35e-01 +1.89e-01 +1.55e-01 +1.09e-01 +1.17e-01 +9.56e-02 +6.95e-02 +5.08e-02 +3.96e-02 +1.13e-01 +1.22e-01 +1.62e-01 +1.45e-01 +1.49e-01 +2.36e-01 +2.03e-01 +1.50e-01 +1.01e-01 +8.51e-02 +7.83e-02 +7.56e-02 +7.49e-02 +2.08e-02 +8.15e-03 +9.29e-02 +9.19e-02 +1.09e-01 +1.23e-01 +9.19e-02 +1.58e-01 +1.36e-01 +1.13e-01 +8.64e-02 +1.01e-01 +5.95e-02 +6.00e-02 +5.68e-02 +2.10e-02 +7.24e-03 +3.10e-02 +4.84e-02 +4.87e-02 +3.68e-02 +5.75e-02 +7.85e-02 +7.07e-02 +8.18e-02 +9.39e-02 +5.13e-02 +5.46e-02 +3.42e-02 +2.14e-02 +1.34e-02 +1.14e-02 +2.27e-02 +2.51e-02 +2.70e-02 +2.56e-02 +3.02e-02 +5.07e-02 +3.42e-02 +3.18e-02 +5.82e-02 +3.83e-02 +2.90e-02 +1.85e-02 +7.38e-03 +5.00e-03 +7.27e-03 \ No newline at end of file diff --git a/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/inputs_true.dat b/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/inputs_true.dat new file mode 100644 index 000000000..6aa91ca79 --- /dev/null +++ b/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/inputs_true.dat @@ -0,0 +1,265 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 750 + 30 + 20 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + + 1 + neutron + 10 + 1 + true + fw_cadis + + + + 15 15 15 + 0.0 0.0 0.0 + 30.0 30.0 30.0 + + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + + + + + + linear + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/results_true.dat b/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/results_true.dat new file mode 100644 index 000000000..3afdc56ca --- /dev/null +++ b/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/results_true.dat @@ -0,0 +1,6760 @@ +RegularMesh + ID = 1 + Name = + Dimensions = 3 + Voxels = [15 15 15] + Lower left = [0. 0. 0.] + Upper Right = [np.float64(30.0), np.float64(30.0), np.float64(30.0)] + Width = [2. 2. 2.] +Lower Bounds +1.77e-01 +1.81e-01 +1.81e-01 +1.81e-01 +1.78e-01 +1.74e-01 +1.70e-01 +1.76e-01 +1.80e-01 +1.88e-01 +1.75e-01 +1.93e-01 +2.76e-01 +4.28e-01 +1.58e-01 +1.74e-01 +1.82e-01 +1.81e-01 +1.63e-01 +1.78e-01 +1.81e-01 +1.77e-01 +1.77e-01 +1.74e-01 +1.72e-01 +1.81e-01 +1.82e-01 +2.58e-01 +3.27e-01 +1.07e-01 +1.82e-01 +1.75e-01 +1.81e-01 +1.82e-01 +1.76e-01 +1.77e-01 +1.74e-01 +1.66e-01 +1.66e-01 +1.69e-01 +1.76e-01 +1.87e-01 +2.67e-01 +4.16e-01 +1.57e-01 +1.76e-01 +1.74e-01 +1.72e-01 +1.76e-01 +1.67e-01 +1.68e-01 +1.70e-01 +1.75e-01 +1.73e-01 +1.73e-01 +1.65e-01 +1.84e-01 +2.67e-01 +3.99e-01 +1.28e-01 +1.75e-01 +1.82e-01 +1.82e-01 +1.84e-01 +1.72e-01 +1.72e-01 +1.67e-01 +1.67e-01 +1.66e-01 +1.74e-01 +1.69e-01 +1.90e-01 +2.82e-01 +3.31e-01 +1.29e-01 +1.80e-01 +1.79e-01 +1.79e-01 +1.78e-01 +1.76e-01 +1.69e-01 +1.68e-01 +1.63e-01 +1.70e-01 +1.66e-01 +1.71e-01 +1.92e-01 +2.70e-01 +2.88e-01 +1.02e-01 +1.76e-01 +1.72e-01 +1.68e-01 +1.71e-01 +1.66e-01 +1.68e-01 +1.65e-01 +1.66e-01 +1.62e-01 +1.60e-01 +1.61e-01 +1.66e-01 +2.39e-01 +3.32e-01 +1.23e-01 +1.82e-01 +1.81e-01 +1.79e-01 +1.70e-01 +1.67e-01 +1.72e-01 +1.65e-01 +1.62e-01 +1.47e-01 +1.56e-01 +1.56e-01 +1.44e-01 +2.01e-01 +1.76e-01 +7.55e-02 +1.76e-01 +1.76e-01 +1.80e-01 +1.82e-01 +1.71e-01 +1.67e-01 +1.65e-01 +1.62e-01 +1.54e-01 +1.55e-01 +1.54e-01 +1.55e-01 +2.28e-01 +1.67e-01 +5.36e-02 +1.77e-01 +1.85e-01 +1.83e-01 +1.81e-01 +1.79e-01 +1.61e-01 +1.64e-01 +1.58e-01 +1.61e-01 +1.60e-01 +1.55e-01 +1.53e-01 +2.33e-01 +1.74e-01 +6.51e-02 +1.81e-01 +1.79e-01 +1.83e-01 +1.79e-01 +1.80e-01 +1.69e-01 +1.58e-01 +1.66e-01 +1.59e-01 +1.59e-01 +1.70e-01 +1.56e-01 +2.11e-01 +1.28e-01 +3.74e-02 +1.90e-01 +1.90e-01 +1.82e-01 +1.71e-01 +1.90e-01 +1.82e-01 +1.72e-01 +1.79e-01 +1.69e-01 +1.69e-01 +1.66e-01 +1.51e-01 +2.22e-01 +9.62e-02 +3.14e-02 +2.75e-01 +2.73e-01 +2.51e-01 +2.62e-01 +2.73e-01 +2.67e-01 +2.53e-01 +2.43e-01 +2.41e-01 +2.42e-01 +2.39e-01 +2.23e-01 +2.12e-01 +7.28e-02 +2.10e-02 +5.00e-01 +3.50e-01 +3.91e-01 +2.55e-01 +2.81e-01 +3.18e-01 +2.54e-01 +2.37e-01 +1.86e-01 +1.97e-01 +1.81e-01 +1.50e-01 +9.74e-02 +3.76e-02 +1.86e-02 +2.17e-01 +1.42e-01 +1.63e-01 +8.18e-02 +9.18e-02 +1.14e-01 +8.12e-02 +7.94e-02 +6.97e-02 +6.23e-02 +6.28e-02 +4.48e-02 +3.69e-02 +1.59e-02 +7.16e-03 +1.71e-01 +1.76e-01 +1.78e-01 +1.74e-01 +1.77e-01 +1.76e-01 +1.81e-01 +1.77e-01 +1.69e-01 +1.76e-01 +1.83e-01 +1.84e-01 +2.71e-01 +3.99e-01 +1.54e-01 +1.79e-01 +1.79e-01 +1.80e-01 +1.75e-01 +1.76e-01 +1.71e-01 +1.74e-01 +1.74e-01 +1.72e-01 +1.73e-01 +1.77e-01 +1.85e-01 +2.54e-01 +3.67e-01 +1.34e-01 +1.80e-01 +1.76e-01 +1.74e-01 +1.77e-01 +1.76e-01 +1.77e-01 +1.74e-01 +1.78e-01 +1.72e-01 +1.80e-01 +1.81e-01 +1.81e-01 +2.66e-01 +3.32e-01 +1.15e-01 +1.82e-01 +1.86e-01 +1.81e-01 +1.83e-01 +1.73e-01 +1.67e-01 +1.72e-01 +1.75e-01 +1.70e-01 +1.69e-01 +1.75e-01 +1.84e-01 +2.58e-01 +3.55e-01 +1.26e-01 +1.80e-01 +1.82e-01 +1.79e-01 +1.73e-01 +1.66e-01 +1.70e-01 +1.77e-01 +1.69e-01 +1.68e-01 +1.69e-01 +1.78e-01 +1.83e-01 +2.75e-01 +3.02e-01 +1.32e-01 +1.75e-01 +1.78e-01 +1.77e-01 +1.69e-01 +1.79e-01 +1.69e-01 +1.65e-01 +1.63e-01 +1.63e-01 +1.67e-01 +1.74e-01 +1.87e-01 +2.69e-01 +3.43e-01 +1.20e-01 +1.80e-01 +1.82e-01 +1.68e-01 +1.67e-01 +1.77e-01 +1.70e-01 +1.59e-01 +1.60e-01 +1.58e-01 +1.61e-01 +1.65e-01 +1.67e-01 +2.34e-01 +2.93e-01 +1.13e-01 +1.71e-01 +1.72e-01 +1.65e-01 +1.65e-01 +1.65e-01 +1.60e-01 +1.57e-01 +1.46e-01 +1.43e-01 +1.50e-01 +1.48e-01 +1.06e-01 +1.93e-01 +1.68e-01 +7.02e-02 +1.69e-01 +1.86e-01 +1.75e-01 +1.74e-01 +1.77e-01 +1.65e-01 +1.61e-01 +1.54e-01 +1.53e-01 +1.55e-01 +1.47e-01 +1.41e-01 +1.98e-01 +1.23e-01 +3.84e-02 +1.78e-01 +1.86e-01 +1.76e-01 +1.77e-01 +1.65e-01 +1.75e-01 +1.66e-01 +1.54e-01 +1.59e-01 +1.54e-01 +1.63e-01 +1.52e-01 +2.17e-01 +1.45e-01 +4.33e-02 +1.80e-01 +1.87e-01 +1.79e-01 +1.73e-01 +1.81e-01 +1.76e-01 +1.67e-01 +1.60e-01 +1.63e-01 +1.60e-01 +1.69e-01 +1.55e-01 +2.16e-01 +1.34e-01 +4.11e-02 +1.88e-01 +1.80e-01 +1.84e-01 +1.81e-01 +1.84e-01 +1.87e-01 +1.74e-01 +1.69e-01 +1.66e-01 +1.73e-01 +1.70e-01 +1.62e-01 +2.01e-01 +8.29e-02 +2.86e-02 +2.58e-01 +2.81e-01 +2.59e-01 +2.69e-01 +2.95e-01 +2.69e-01 +2.46e-01 +2.56e-01 +2.33e-01 +2.31e-01 +2.59e-01 +2.41e-01 +2.08e-01 +5.36e-02 +1.40e-02 +3.12e-01 +2.97e-01 +2.71e-01 +3.04e-01 +3.70e-01 +3.54e-01 +2.77e-01 +2.79e-01 +2.19e-01 +1.62e-01 +1.62e-01 +1.63e-01 +1.08e-01 +3.47e-02 +1.48e-02 +1.09e-01 +1.08e-01 +1.05e-01 +1.01e-01 +1.35e-01 +1.42e-01 +9.52e-02 +9.99e-02 +9.32e-02 +5.18e-02 +5.18e-02 +5.34e-02 +4.23e-02 +1.87e-02 +7.19e-03 +1.80e-01 +1.75e-01 +1.82e-01 +1.75e-01 +1.71e-01 +1.70e-01 +1.69e-01 +1.65e-01 +1.75e-01 +1.71e-01 +1.68e-01 +1.86e-01 +3.12e-01 +4.27e-01 +1.66e-01 +1.81e-01 +1.77e-01 +1.70e-01 +1.73e-01 +1.73e-01 +1.72e-01 +1.64e-01 +1.66e-01 +1.66e-01 +1.73e-01 +1.70e-01 +1.89e-01 +2.63e-01 +4.02e-01 +1.64e-01 +1.79e-01 +1.80e-01 +1.83e-01 +1.71e-01 +1.73e-01 +1.71e-01 +1.70e-01 +1.75e-01 +1.69e-01 +1.68e-01 +1.68e-01 +1.80e-01 +2.49e-01 +3.42e-01 +1.29e-01 +1.63e-01 +1.73e-01 +1.74e-01 +1.71e-01 +1.68e-01 +1.65e-01 +1.68e-01 +1.73e-01 +1.72e-01 +1.65e-01 +1.72e-01 +1.69e-01 +2.52e-01 +3.19e-01 +1.06e-01 +1.67e-01 +1.70e-01 +1.72e-01 +1.64e-01 +1.71e-01 +1.71e-01 +1.57e-01 +1.64e-01 +1.61e-01 +1.62e-01 +1.73e-01 +1.79e-01 +2.63e-01 +2.66e-01 +7.74e-02 +1.68e-01 +1.72e-01 +1.64e-01 +1.66e-01 +1.65e-01 +1.66e-01 +1.58e-01 +1.59e-01 +1.63e-01 +1.63e-01 +1.70e-01 +1.75e-01 +2.51e-01 +3.47e-01 +1.31e-01 +1.72e-01 +1.79e-01 +1.70e-01 +1.70e-01 +1.69e-01 +1.71e-01 +1.70e-01 +1.55e-01 +1.59e-01 +1.65e-01 +1.70e-01 +1.71e-01 +2.43e-01 +2.46e-01 +9.20e-02 +1.70e-01 +1.70e-01 +1.66e-01 +1.63e-01 +1.66e-01 +1.63e-01 +1.59e-01 +1.58e-01 +1.57e-01 +1.60e-01 +1.55e-01 +1.53e-01 +2.20e-01 +1.63e-01 +6.37e-02 +1.70e-01 +1.70e-01 +1.73e-01 +1.69e-01 +1.66e-01 +1.64e-01 +1.63e-01 +1.64e-01 +1.52e-01 +1.54e-01 +1.53e-01 +1.49e-01 +2.12e-01 +1.64e-01 +6.14e-02 +1.74e-01 +1.78e-01 +1.73e-01 +1.75e-01 +1.74e-01 +1.65e-01 +1.67e-01 +1.59e-01 +1.48e-01 +1.57e-01 +1.55e-01 +1.59e-01 +2.06e-01 +1.23e-01 +4.80e-02 +1.69e-01 +1.78e-01 +1.77e-01 +1.74e-01 +1.77e-01 +1.73e-01 +1.74e-01 +1.68e-01 +1.52e-01 +1.58e-01 +1.58e-01 +1.55e-01 +2.12e-01 +1.22e-01 +3.34e-02 +1.75e-01 +1.85e-01 +1.86e-01 +1.90e-01 +1.80e-01 +1.79e-01 +1.76e-01 +1.71e-01 +1.67e-01 +1.71e-01 +1.69e-01 +1.69e-01 +1.86e-01 +1.02e-01 +3.54e-02 +2.66e-01 +2.66e-01 +2.64e-01 +2.50e-01 +2.60e-01 +2.58e-01 +2.34e-01 +2.42e-01 +2.35e-01 +2.29e-01 +2.33e-01 +2.42e-01 +2.28e-01 +5.44e-02 +1.71e-02 +3.77e-01 +2.75e-01 +3.41e-01 +3.51e-01 +3.64e-01 +2.31e-01 +2.32e-01 +2.18e-01 +1.82e-01 +1.77e-01 +1.28e-01 +1.16e-01 +1.03e-01 +4.16e-02 +1.06e-02 +1.33e-01 +8.71e-02 +1.41e-01 +1.34e-01 +1.37e-01 +8.83e-02 +7.82e-02 +8.36e-02 +5.61e-02 +6.39e-02 +2.90e-02 +3.69e-02 +3.46e-02 +2.60e-02 +1.24e-02 +1.77e-01 +1.71e-01 +1.75e-01 +1.79e-01 +1.73e-01 +1.71e-01 +1.64e-01 +1.66e-01 +1.59e-01 +1.61e-01 +1.55e-01 +1.79e-01 +2.67e-01 +4.26e-01 +1.56e-01 +1.75e-01 +1.82e-01 +1.71e-01 +1.65e-01 +1.69e-01 +1.66e-01 +1.69e-01 +1.63e-01 +1.65e-01 +1.74e-01 +1.69e-01 +1.76e-01 +2.47e-01 +3.31e-01 +1.30e-01 +1.69e-01 +1.73e-01 +1.77e-01 +1.67e-01 +1.55e-01 +1.67e-01 +1.59e-01 +1.63e-01 +1.58e-01 +1.70e-01 +1.70e-01 +1.84e-01 +2.44e-01 +2.71e-01 +1.02e-01 +1.69e-01 +1.70e-01 +1.73e-01 +1.69e-01 +1.66e-01 +1.67e-01 +1.64e-01 +1.60e-01 +1.62e-01 +1.63e-01 +1.63e-01 +1.67e-01 +2.48e-01 +2.40e-01 +9.84e-02 +1.62e-01 +1.64e-01 +1.69e-01 +1.71e-01 +1.62e-01 +1.58e-01 +1.62e-01 +1.58e-01 +1.62e-01 +1.61e-01 +1.71e-01 +1.67e-01 +2.50e-01 +3.22e-01 +1.17e-01 +1.74e-01 +1.60e-01 +1.69e-01 +1.72e-01 +1.67e-01 +1.60e-01 +1.65e-01 +1.68e-01 +1.62e-01 +1.60e-01 +1.63e-01 +1.76e-01 +2.53e-01 +2.99e-01 +1.10e-01 +1.69e-01 +1.67e-01 +1.76e-01 +1.59e-01 +1.59e-01 +1.58e-01 +1.64e-01 +1.59e-01 +1.55e-01 +1.59e-01 +1.51e-01 +1.60e-01 +2.50e-01 +2.88e-01 +1.01e-01 +1.72e-01 +1.71e-01 +1.68e-01 +1.63e-01 +1.59e-01 +1.61e-01 +1.66e-01 +1.56e-01 +1.54e-01 +1.51e-01 +1.49e-01 +1.58e-01 +2.33e-01 +1.85e-01 +6.62e-02 +1.69e-01 +1.65e-01 +1.63e-01 +1.67e-01 +1.62e-01 +1.63e-01 +1.63e-01 +1.61e-01 +1.55e-01 +1.53e-01 +1.59e-01 +1.69e-01 +2.48e-01 +1.87e-01 +5.29e-02 +1.71e-01 +1.66e-01 +1.73e-01 +1.68e-01 +1.65e-01 +1.64e-01 +1.59e-01 +1.52e-01 +1.60e-01 +1.52e-01 +1.48e-01 +1.54e-01 +2.32e-01 +1.35e-01 +5.41e-02 +1.76e-01 +1.78e-01 +1.68e-01 +1.78e-01 +1.73e-01 +1.69e-01 +1.62e-01 +1.61e-01 +1.59e-01 +1.61e-01 +1.58e-01 +1.60e-01 +2.06e-01 +9.41e-02 +2.78e-02 +1.88e-01 +1.77e-01 +1.78e-01 +8.59e-02 +1.82e-01 +1.90e-01 +1.76e-01 +1.63e-01 +1.69e-01 +1.69e-01 +1.69e-01 +1.68e-01 +1.97e-01 +7.83e-02 +2.27e-02 +2.47e-01 +2.50e-01 +2.59e-01 +2.51e-01 +2.57e-01 +2.55e-01 +2.15e-01 +2.28e-01 +2.42e-01 +2.25e-01 +2.12e-01 +2.14e-01 +2.28e-01 +7.82e-02 +2.46e-02 +3.10e-01 +2.49e-01 +2.07e-01 +2.92e-01 +2.88e-01 +2.33e-01 +1.62e-01 +1.13e-01 +1.90e-01 +1.80e-01 +1.16e-01 +1.07e-01 +9.41e-02 +4.21e-02 +1.63e-02 +1.17e-01 +8.58e-02 +8.78e-02 +9.13e-02 +1.15e-01 +8.57e-02 +6.78e-02 +3.63e-02 +6.34e-02 +6.38e-02 +4.06e-02 +3.60e-02 +2.77e-02 +2.56e-02 +1.21e-02 +1.71e-01 +1.68e-01 +1.63e-01 +1.68e-01 +1.72e-01 +1.66e-01 +1.65e-01 +1.51e-01 +1.57e-01 +1.63e-01 +1.57e-01 +1.78e-01 +2.58e-01 +3.15e-01 +1.24e-01 +1.72e-01 +1.71e-01 +1.66e-01 +1.69e-01 +1.71e-01 +1.63e-01 +1.58e-01 +1.51e-01 +1.62e-01 +1.68e-01 +1.67e-01 +1.76e-01 +2.54e-01 +3.04e-01 +1.12e-01 +1.75e-01 +1.72e-01 +1.70e-01 +1.68e-01 +1.63e-01 +1.64e-01 +1.63e-01 +1.54e-01 +1.65e-01 +1.69e-01 +1.68e-01 +1.76e-01 +2.52e-01 +2.21e-01 +8.02e-02 +1.68e-01 +1.71e-01 +1.65e-01 +1.66e-01 +1.63e-01 +1.64e-01 +1.61e-01 +1.62e-01 +1.61e-01 +1.66e-01 +1.64e-01 +1.72e-01 +2.60e-01 +2.09e-01 +7.14e-02 +1.63e-01 +1.61e-01 +1.63e-01 +1.69e-01 +1.61e-01 +1.62e-01 +1.62e-01 +1.61e-01 +1.58e-01 +1.54e-01 +1.64e-01 +1.76e-01 +2.59e-01 +3.27e-01 +1.16e-01 +1.60e-01 +1.60e-01 +1.63e-01 +1.68e-01 +1.62e-01 +1.54e-01 +1.50e-01 +1.64e-01 +1.63e-01 +1.58e-01 +1.58e-01 +1.68e-01 +2.43e-01 +2.59e-01 +9.33e-02 +1.61e-01 +1.63e-01 +1.68e-01 +1.62e-01 +1.69e-01 +1.53e-01 +1.53e-01 +1.57e-01 +1.50e-01 +1.54e-01 +1.57e-01 +1.59e-01 +2.17e-01 +2.06e-01 +7.58e-02 +1.71e-01 +1.70e-01 +1.63e-01 +1.57e-01 +1.61e-01 +1.53e-01 +1.56e-01 +1.49e-01 +1.49e-01 +1.59e-01 +1.49e-01 +1.55e-01 +2.27e-01 +1.62e-01 +6.88e-02 +1.71e-01 +1.67e-01 +1.66e-01 +1.60e-01 +1.61e-01 +1.62e-01 +1.63e-01 +1.54e-01 +1.50e-01 +1.48e-01 +1.51e-01 +1.55e-01 +2.15e-01 +1.65e-01 +4.95e-02 +1.71e-01 +1.73e-01 +1.68e-01 +1.62e-01 +1.57e-01 +1.62e-01 +1.59e-01 +1.56e-01 +1.53e-01 +1.47e-01 +1.49e-01 +1.62e-01 +2.14e-01 +1.58e-01 +6.86e-02 +1.70e-01 +1.66e-01 +1.69e-01 +1.66e-01 +1.67e-01 +1.62e-01 +1.63e-01 +1.63e-01 +1.58e-01 +1.52e-01 +1.54e-01 +1.67e-01 +2.26e-01 +1.15e-01 +4.03e-02 +1.74e-01 +1.82e-01 +1.70e-01 +1.72e-01 +1.77e-01 +1.85e-01 +1.68e-01 +1.65e-01 +1.63e-01 +1.74e-01 +1.63e-01 +1.66e-01 +2.29e-01 +1.07e-01 +3.27e-02 +2.58e-01 +2.47e-01 +2.50e-01 +2.57e-01 +2.60e-01 +2.66e-01 +2.59e-01 +2.39e-01 +2.35e-01 +2.29e-01 +2.16e-01 +2.25e-01 +2.33e-01 +8.96e-02 +3.01e-02 +2.73e-01 +2.51e-01 +2.52e-01 +2.63e-01 +3.05e-01 +2.69e-01 +2.91e-01 +1.85e-01 +2.00e-01 +1.47e-01 +1.01e-01 +1.17e-01 +8.80e-02 +3.85e-02 +1.53e-02 +1.00e-01 +8.71e-02 +9.09e-02 +9.00e-02 +1.08e-01 +8.70e-02 +1.17e-01 +5.47e-02 +7.04e-02 +5.07e-02 +3.08e-02 +3.14e-02 +2.64e-02 +1.69e-02 +1.16e-02 +1.60e-01 +1.64e-01 +1.63e-01 +1.67e-01 +1.70e-01 +1.62e-01 +1.54e-01 +1.52e-01 +1.52e-01 +1.55e-01 +1.57e-01 +1.63e-01 +2.61e-01 +2.58e-01 +7.58e-02 +1.59e-01 +1.68e-01 +1.63e-01 +1.58e-01 +1.65e-01 +1.65e-01 +1.67e-01 +1.52e-01 +1.58e-01 +1.56e-01 +1.60e-01 +1.69e-01 +2.49e-01 +2.88e-01 +1.11e-01 +1.66e-01 +1.69e-01 +1.70e-01 +1.63e-01 +1.63e-01 +1.59e-01 +1.63e-01 +1.65e-01 +1.58e-01 +1.58e-01 +1.60e-01 +1.75e-01 +2.57e-01 +2.53e-01 +8.19e-02 +1.67e-01 +1.68e-01 +1.58e-01 +1.58e-01 +1.61e-01 +1.67e-01 +1.54e-01 +1.55e-01 +1.61e-01 +1.61e-01 +1.63e-01 +1.70e-01 +2.53e-01 +2.35e-01 +7.59e-02 +1.63e-01 +1.66e-01 +1.68e-01 +1.64e-01 +1.59e-01 +1.59e-01 +1.57e-01 +1.58e-01 +1.58e-01 +1.57e-01 +1.58e-01 +1.66e-01 +2.50e-01 +2.58e-01 +9.81e-02 +1.60e-01 +1.61e-01 +1.64e-01 +1.61e-01 +1.60e-01 +1.56e-01 +1.51e-01 +1.50e-01 +1.63e-01 +1.64e-01 +1.57e-01 +1.64e-01 +2.37e-01 +2.18e-01 +9.03e-02 +1.68e-01 +1.70e-01 +1.64e-01 +1.56e-01 +1.58e-01 +1.53e-01 +1.51e-01 +1.43e-01 +1.51e-01 +1.53e-01 +1.55e-01 +1.63e-01 +2.19e-01 +1.33e-01 +4.89e-02 +1.67e-01 +1.60e-01 +1.61e-01 +1.55e-01 +1.64e-01 +1.56e-01 +1.49e-01 +1.45e-01 +1.44e-01 +1.51e-01 +1.57e-01 +1.64e-01 +2.32e-01 +1.18e-01 +3.65e-02 +1.62e-01 +1.67e-01 +1.68e-01 +1.55e-01 +1.59e-01 +1.57e-01 +1.58e-01 +1.46e-01 +1.43e-01 +1.39e-01 +1.48e-01 +1.54e-01 +2.26e-01 +1.45e-01 +4.05e-02 +1.55e-01 +1.63e-01 +1.63e-01 +1.67e-01 +1.56e-01 +1.53e-01 +1.50e-01 +1.49e-01 +1.49e-01 +1.45e-01 +1.47e-01 +1.53e-01 +2.10e-01 +1.20e-01 +3.82e-02 +1.65e-01 +1.59e-01 +1.61e-01 +1.61e-01 +1.56e-01 +1.56e-01 +1.60e-01 +1.59e-01 +1.54e-01 +1.53e-01 +1.48e-01 +1.52e-01 +2.07e-01 +1.01e-01 +3.00e-02 +1.77e-01 +1.71e-01 +1.68e-01 +1.71e-01 +1.75e-01 +1.69e-01 +1.64e-01 +1.68e-01 +1.61e-01 +1.61e-01 +1.53e-01 +1.46e-01 +1.95e-01 +9.40e-02 +2.21e-02 +2.46e-01 +2.38e-01 +2.31e-01 +2.41e-01 +2.54e-01 +2.50e-01 +2.28e-01 +2.20e-01 +2.21e-01 +2.26e-01 +2.35e-01 +2.02e-01 +1.88e-01 +9.58e-02 +3.52e-02 +3.06e-01 +2.59e-01 +2.23e-01 +2.61e-01 +2.57e-01 +2.36e-01 +2.32e-01 +1.78e-01 +1.53e-01 +1.37e-01 +1.47e-01 +1.36e-01 +7.24e-02 +4.03e-02 +2.23e-02 +1.04e-01 +9.82e-02 +8.79e-02 +9.20e-02 +9.75e-02 +7.29e-02 +1.04e-01 +6.06e-02 +5.37e-02 +3.67e-02 +4.29e-02 +4.85e-02 +2.87e-02 +1.59e-02 +9.17e-03 +1.71e-01 +1.67e-01 +1.65e-01 +1.69e-01 +1.59e-01 +1.60e-01 +1.62e-01 +1.62e-01 +1.57e-01 +1.49e-01 +1.52e-01 +1.53e-01 +2.42e-01 +1.80e-01 +8.19e-02 +1.68e-01 +1.63e-01 +1.67e-01 +1.65e-01 +1.63e-01 +1.54e-01 +1.59e-01 +1.52e-01 +1.59e-01 +1.52e-01 +1.49e-01 +1.55e-01 +2.25e-01 +2.01e-01 +7.57e-02 +1.63e-01 +1.59e-01 +1.62e-01 +1.61e-01 +1.57e-01 +1.56e-01 +1.50e-01 +1.51e-01 +1.48e-01 +1.53e-01 +1.61e-01 +1.58e-01 +2.32e-01 +2.49e-01 +1.01e-01 +1.67e-01 +1.54e-01 +1.55e-01 +1.52e-01 +1.55e-01 +1.61e-01 +1.57e-01 +1.54e-01 +1.54e-01 +1.54e-01 +1.56e-01 +1.64e-01 +2.25e-01 +1.56e-01 +6.14e-02 +1.67e-01 +1.65e-01 +1.61e-01 +1.57e-01 +1.58e-01 +1.62e-01 +1.55e-01 +1.57e-01 +1.50e-01 +1.54e-01 +1.55e-01 +1.68e-01 +2.32e-01 +1.28e-01 +4.68e-02 +1.63e-01 +1.57e-01 +1.60e-01 +1.63e-01 +1.61e-01 +1.58e-01 +1.50e-01 +1.46e-01 +1.43e-01 +1.52e-01 +1.55e-01 +1.61e-01 +2.25e-01 +1.35e-01 +4.13e-02 +1.64e-01 +1.62e-01 +1.60e-01 +1.62e-01 +1.61e-01 +1.51e-01 +1.50e-01 +1.41e-01 +1.46e-01 +1.49e-01 +1.51e-01 +1.64e-01 +2.31e-01 +1.50e-01 +4.56e-02 +1.53e-01 +1.55e-01 +1.54e-01 +1.57e-01 +1.55e-01 +1.49e-01 +1.50e-01 +1.44e-01 +1.43e-01 +1.47e-01 +1.51e-01 +1.57e-01 +2.19e-01 +1.63e-01 +4.89e-02 +1.64e-01 +6.61e-02 +1.57e-01 +1.53e-01 +1.48e-01 +1.53e-01 +7.58e-02 +1.40e-01 +1.38e-01 +1.40e-01 +1.50e-01 +1.57e-01 +2.10e-01 +1.54e-01 +5.14e-02 +1.56e-01 +1.58e-01 +1.61e-01 +1.57e-01 +1.52e-01 +1.47e-01 +1.51e-01 +1.48e-01 +1.44e-01 +1.48e-01 +1.44e-01 +1.43e-01 +2.07e-01 +1.20e-01 +4.74e-02 +1.62e-01 +1.71e-01 +1.62e-01 +1.57e-01 +1.52e-01 +1.51e-01 +1.44e-01 +1.43e-01 +1.46e-01 +1.46e-01 +1.36e-01 +1.45e-01 +1.86e-01 +7.07e-02 +2.51e-02 +1.70e-01 +1.71e-01 +1.63e-01 +1.56e-01 +1.60e-01 +1.61e-01 +1.56e-01 +1.55e-01 +1.59e-01 +1.53e-01 +1.48e-01 +1.41e-01 +1.55e-01 +6.13e-02 +2.68e-02 +2.40e-01 +2.25e-01 +2.21e-01 +2.27e-01 +2.31e-01 +2.20e-01 +2.21e-01 +1.99e-01 +2.15e-01 +1.98e-01 +2.02e-01 +1.80e-01 +1.35e-01 +3.74e-02 +1.62e-02 +2.64e-01 +2.69e-01 +2.32e-01 +1.55e-01 +1.50e-01 +1.99e-01 +1.79e-01 +1.44e-01 +1.30e-01 +1.25e-01 +1.44e-01 +1.15e-01 +4.22e-02 +1.96e-02 +8.51e-03 +9.50e-02 +1.04e-01 +8.97e-02 +4.80e-02 +4.87e-02 +6.44e-02 +6.38e-02 +4.70e-02 +4.40e-02 +3.28e-02 +4.72e-02 +4.03e-02 +1.52e-02 +8.66e-03 +6.15e-03 +1.56e-01 +1.57e-01 +1.54e-01 +1.60e-01 +1.65e-01 +1.66e-01 +1.65e-01 +1.58e-01 +1.47e-01 +1.40e-01 +1.36e-01 +1.59e-01 +2.22e-01 +1.52e-01 +5.31e-02 +1.61e-01 +1.62e-01 +1.69e-01 +1.63e-01 +1.58e-01 +1.58e-01 +1.59e-01 +1.47e-01 +1.45e-01 +1.39e-01 +1.30e-01 +1.48e-01 +2.15e-01 +1.09e-01 +3.46e-02 +1.58e-01 +1.57e-01 +1.62e-01 +1.63e-01 +1.59e-01 +1.57e-01 +1.56e-01 +1.58e-01 +1.52e-01 +1.45e-01 +1.50e-01 +1.60e-01 +2.17e-01 +1.56e-01 +5.66e-02 +1.60e-01 +1.57e-01 +1.60e-01 +1.57e-01 +1.60e-01 +1.58e-01 +1.54e-01 +1.53e-01 +1.47e-01 +1.42e-01 +1.57e-01 +1.66e-01 +2.10e-01 +1.46e-01 +6.02e-02 +1.62e-01 +1.61e-01 +1.68e-01 +1.54e-01 +1.54e-01 +1.59e-01 +1.50e-01 +1.49e-01 +1.50e-01 +1.46e-01 +1.58e-01 +1.59e-01 +2.16e-01 +1.18e-01 +3.24e-02 +1.61e-01 +1.55e-01 +1.60e-01 +1.59e-01 +1.50e-01 +1.45e-01 +1.50e-01 +1.45e-01 +1.47e-01 +1.46e-01 +1.48e-01 +1.58e-01 +2.13e-01 +1.24e-01 +4.33e-02 +1.58e-01 +1.55e-01 +1.58e-01 +1.51e-01 +1.57e-01 +1.54e-01 +1.50e-01 +1.39e-01 +1.40e-01 +1.40e-01 +1.39e-01 +1.49e-01 +2.03e-01 +1.50e-01 +5.78e-02 +1.51e-01 +1.59e-01 +1.53e-01 +1.53e-01 +1.52e-01 +1.52e-01 +1.46e-01 +1.43e-01 +1.42e-01 +1.41e-01 +1.52e-01 +1.52e-01 +2.06e-01 +1.37e-01 +5.13e-02 +1.55e-01 +1.52e-01 +1.56e-01 +1.49e-01 +1.53e-01 +1.49e-01 +1.45e-01 +1.39e-01 +1.35e-01 +1.33e-01 +1.48e-01 +1.46e-01 +1.98e-01 +1.08e-01 +3.36e-02 +1.53e-01 +1.53e-01 +1.49e-01 +1.43e-01 +1.45e-01 +1.48e-01 +1.38e-01 +1.42e-01 +1.29e-01 +1.29e-01 +1.38e-01 +1.45e-01 +2.11e-01 +1.20e-01 +3.89e-02 +1.60e-01 +1.55e-01 +1.59e-01 +1.49e-01 +1.48e-01 +1.48e-01 +1.47e-01 +1.50e-01 +1.41e-01 +1.37e-01 +1.31e-01 +1.36e-01 +1.77e-01 +7.63e-02 +1.94e-02 +1.69e-01 +1.64e-01 +1.65e-01 +1.57e-01 +1.52e-01 +1.48e-01 +1.44e-01 +1.54e-01 +1.48e-01 +1.49e-01 +1.40e-01 +1.31e-01 +1.53e-01 +4.85e-02 +1.42e-02 +2.42e-01 +2.23e-01 +2.24e-01 +2.03e-01 +2.18e-01 +2.15e-01 +1.75e-01 +1.74e-01 +1.73e-01 +2.02e-01 +2.01e-01 +1.69e-01 +1.22e-01 +3.16e-02 +1.08e-02 +2.16e-01 +2.25e-01 +1.60e-01 +1.34e-01 +1.44e-01 +1.68e-01 +1.02e-01 +8.69e-02 +7.20e-02 +1.43e-01 +1.29e-01 +8.21e-02 +4.29e-02 +1.08e-02 +4.27e-03 +7.46e-02 +7.65e-02 +5.07e-02 +4.76e-02 +4.31e-02 +5.72e-02 +3.77e-02 +2.92e-02 +2.28e-02 +4.79e-02 +4.01e-02 +2.76e-02 +1.93e-02 +8.26e-03 +2.13e-03 +1.63e-01 +1.66e-01 +1.71e-01 +1.63e-01 +1.67e-01 +1.59e-01 +1.56e-01 +1.61e-01 +1.49e-01 +1.45e-01 +1.38e-01 +1.54e-01 +2.17e-01 +1.17e-01 +3.57e-02 +1.59e-01 +1.61e-01 +1.68e-01 +1.63e-01 +1.62e-01 +1.62e-01 +1.60e-01 +1.56e-01 +1.49e-01 +1.47e-01 +1.44e-01 +1.52e-01 +2.09e-01 +1.26e-01 +3.94e-02 +1.57e-01 +1.61e-01 +1.58e-01 +1.59e-01 +1.60e-01 +1.51e-01 +1.56e-01 +1.60e-01 +1.49e-01 +1.45e-01 +1.44e-01 +1.61e-01 +2.41e-01 +1.63e-01 +5.31e-02 +1.60e-01 +1.54e-01 +1.60e-01 +1.53e-01 +1.56e-01 +1.57e-01 +1.55e-01 +1.53e-01 +1.46e-01 +1.47e-01 +1.49e-01 +1.47e-01 +2.08e-01 +1.76e-01 +7.44e-02 +1.55e-01 +1.52e-01 +1.53e-01 +1.52e-01 +1.52e-01 +1.51e-01 +1.51e-01 +1.53e-01 +1.48e-01 +1.40e-01 +1.50e-01 +1.61e-01 +2.05e-01 +1.29e-01 +4.01e-02 +1.60e-01 +1.58e-01 +1.50e-01 +1.47e-01 +1.42e-01 +1.46e-01 +1.50e-01 +1.46e-01 +1.45e-01 +1.42e-01 +1.48e-01 +1.60e-01 +2.17e-01 +1.50e-01 +5.21e-02 +1.53e-01 +1.49e-01 +1.52e-01 +1.45e-01 +1.45e-01 +1.43e-01 +1.40e-01 +1.38e-01 +1.37e-01 +1.36e-01 +1.46e-01 +1.49e-01 +1.88e-01 +1.14e-01 +4.21e-02 +1.48e-01 +1.50e-01 +1.52e-01 +1.43e-01 +1.48e-01 +1.44e-01 +1.36e-01 +1.34e-01 +1.37e-01 +1.37e-01 +1.46e-01 +1.47e-01 +1.84e-01 +7.19e-02 +2.90e-02 +1.48e-01 +1.19e-01 +1.48e-01 +1.48e-01 +1.49e-01 +1.54e-01 +1.45e-01 +1.21e-01 +1.29e-01 +1.30e-01 +1.31e-01 +1.39e-01 +1.93e-01 +7.96e-02 +2.03e-02 +1.53e-01 +1.46e-01 +1.46e-01 +1.41e-01 +1.47e-01 +1.45e-01 +1.42e-01 +1.37e-01 +1.29e-01 +1.27e-01 +1.30e-01 +1.33e-01 +1.95e-01 +8.81e-02 +2.19e-02 +1.56e-01 +1.42e-01 +1.45e-01 +1.43e-01 +1.52e-01 +1.48e-01 +1.44e-01 +1.40e-01 +1.41e-01 +1.31e-01 +1.18e-01 +1.31e-01 +1.83e-01 +9.56e-02 +2.75e-02 +1.63e-01 +1.47e-01 +1.48e-01 +1.57e-01 +1.55e-01 +1.50e-01 +1.56e-01 +1.42e-01 +1.44e-01 +1.39e-01 +1.29e-01 +1.20e-01 +1.52e-01 +5.41e-02 +1.62e-02 +2.32e-01 +2.08e-01 +2.20e-01 +2.22e-01 +2.23e-01 +2.17e-01 +1.99e-01 +2.05e-01 +1.92e-01 +2.03e-01 +1.97e-01 +1.60e-01 +1.37e-01 +4.00e-02 +1.07e-02 +2.25e-01 +1.79e-01 +1.71e-01 +1.67e-01 +1.69e-01 +1.54e-01 +1.02e-01 +1.09e-01 +9.88e-02 +1.08e-01 +1.18e-01 +8.29e-02 +4.15e-02 +1.49e-02 +5.20e-03 +6.83e-02 +5.09e-02 +4.28e-02 +4.83e-02 +4.92e-02 +5.19e-02 +3.56e-02 +3.22e-02 +2.88e-02 +3.04e-02 +3.83e-02 +2.79e-02 +1.21e-02 +5.81e-03 +1.30e-03 +1.57e-01 +1.62e-01 +1.67e-01 +1.62e-01 +1.61e-01 +1.50e-01 +1.52e-01 +1.57e-01 +1.53e-01 +1.51e-01 +1.43e-01 +1.61e-01 +2.04e-01 +1.43e-01 +5.22e-02 +1.59e-01 +1.63e-01 +1.58e-01 +1.58e-01 +1.58e-01 +1.62e-01 +1.60e-01 +1.58e-01 +1.58e-01 +1.43e-01 +1.49e-01 +1.60e-01 +2.21e-01 +1.56e-01 +4.57e-02 +1.63e-01 +1.57e-01 +1.51e-01 +1.54e-01 +1.53e-01 +1.60e-01 +1.55e-01 +1.60e-01 +1.60e-01 +1.45e-01 +1.40e-01 +1.59e-01 +2.23e-01 +1.66e-01 +5.44e-02 +1.53e-01 +1.54e-01 +1.54e-01 +1.50e-01 +1.57e-01 +1.53e-01 +1.59e-01 +1.56e-01 +1.48e-01 +1.45e-01 +1.45e-01 +1.58e-01 +2.07e-01 +1.44e-01 +5.45e-02 +1.50e-01 +1.46e-01 +1.56e-01 +1.56e-01 +1.54e-01 +1.48e-01 +1.50e-01 +1.54e-01 +1.46e-01 +1.46e-01 +1.50e-01 +1.52e-01 +2.09e-01 +1.16e-01 +3.68e-02 +1.56e-01 +1.50e-01 +1.56e-01 +1.48e-01 +1.50e-01 +1.51e-01 +1.48e-01 +1.49e-01 +1.43e-01 +1.41e-01 +1.46e-01 +1.53e-01 +2.02e-01 +1.07e-01 +3.92e-02 +1.55e-01 +1.55e-01 +1.53e-01 +1.49e-01 +1.47e-01 +1.48e-01 +1.41e-01 +1.44e-01 +1.41e-01 +1.41e-01 +1.37e-01 +1.51e-01 +2.06e-01 +1.37e-01 +4.68e-02 +1.52e-01 +1.53e-01 +1.54e-01 +1.53e-01 +1.48e-01 +1.43e-01 +1.32e-01 +1.41e-01 +1.35e-01 +1.27e-01 +1.35e-01 +1.52e-01 +1.90e-01 +7.26e-02 +2.22e-02 +1.47e-01 +1.47e-01 +1.49e-01 +1.50e-01 +1.49e-01 +1.42e-01 +1.39e-01 +1.32e-01 +1.28e-01 +1.26e-01 +1.34e-01 +1.45e-01 +1.87e-01 +8.17e-02 +2.42e-02 +1.49e-01 +1.48e-01 +1.47e-01 +1.50e-01 +1.45e-01 +1.43e-01 +1.38e-01 +1.30e-01 +1.29e-01 +1.31e-01 +1.22e-01 +1.36e-01 +1.78e-01 +1.04e-01 +3.33e-02 +1.48e-01 +1.45e-01 +1.44e-01 +1.42e-01 +1.41e-01 +1.43e-01 +1.46e-01 +1.38e-01 +1.29e-01 +1.25e-01 +1.18e-01 +1.25e-01 +1.79e-01 +8.54e-02 +2.89e-02 +1.64e-01 +1.47e-01 +1.55e-01 +1.56e-01 +1.47e-01 +1.59e-01 +1.48e-01 +1.44e-01 +1.36e-01 +1.33e-01 +1.26e-01 +1.10e-01 +1.31e-01 +5.72e-02 +1.80e-02 +2.46e-01 +2.05e-01 +1.90e-01 +2.18e-01 +2.16e-01 +2.14e-01 +2.02e-01 +2.24e-01 +2.13e-01 +2.23e-01 +1.86e-01 +1.51e-01 +1.18e-01 +3.41e-02 +1.21e-02 +1.87e-01 +1.53e-01 +1.50e-01 +1.67e-01 +1.83e-01 +1.69e-01 +1.39e-01 +1.42e-01 +1.29e-01 +1.05e-01 +9.27e-02 +6.73e-02 +3.93e-02 +1.65e-02 +7.05e-03 +7.21e-02 +6.08e-02 +6.40e-02 +5.54e-02 +6.28e-02 +5.59e-02 +4.39e-02 +4.02e-02 +4.46e-02 +3.36e-02 +3.13e-02 +2.12e-02 +1.23e-02 +7.79e-03 +3.56e-03 +1.51e-01 +1.56e-01 +1.63e-01 +1.66e-01 +1.67e-01 +1.65e-01 +1.66e-01 +1.70e-01 +1.53e-01 +1.56e-01 +1.51e-01 +1.51e-01 +1.83e-01 +9.73e-02 +3.81e-02 +1.53e-01 +1.59e-01 +1.54e-01 +1.57e-01 +1.61e-01 +1.59e-01 +1.57e-01 +1.58e-01 +1.58e-01 +1.58e-01 +1.52e-01 +1.52e-01 +2.00e-01 +1.30e-01 +4.17e-02 +1.54e-01 +1.56e-01 +1.53e-01 +1.62e-01 +1.55e-01 +1.59e-01 +1.66e-01 +1.53e-01 +1.56e-01 +1.56e-01 +1.52e-01 +1.57e-01 +2.13e-01 +1.42e-01 +4.79e-02 +1.53e-01 +1.59e-01 +1.67e-01 +1.55e-01 +1.64e-01 +1.66e-01 +1.61e-01 +1.55e-01 +1.54e-01 +1.50e-01 +1.45e-01 +1.54e-01 +2.09e-01 +1.08e-01 +3.44e-02 +1.54e-01 +1.57e-01 +1.60e-01 +1.54e-01 +1.62e-01 +1.59e-01 +1.56e-01 +1.54e-01 +1.57e-01 +1.52e-01 +1.52e-01 +1.56e-01 +2.03e-01 +9.74e-02 +2.86e-02 +1.52e-01 +1.57e-01 +1.58e-01 +1.62e-01 +1.56e-01 +1.57e-01 +1.52e-01 +1.48e-01 +1.44e-01 +1.47e-01 +1.50e-01 +1.51e-01 +2.12e-01 +1.09e-01 +2.91e-02 +1.51e-01 +1.54e-01 +1.48e-01 +1.58e-01 +1.54e-01 +1.52e-01 +1.51e-01 +1.44e-01 +1.44e-01 +1.43e-01 +1.45e-01 +1.51e-01 +1.99e-01 +1.23e-01 +3.39e-02 +1.48e-01 +1.50e-01 +1.55e-01 +1.59e-01 +1.47e-01 +1.46e-01 +1.37e-01 +1.40e-01 +1.41e-01 +1.47e-01 +1.42e-01 +1.45e-01 +1.87e-01 +1.10e-01 +3.91e-02 +1.43e-01 +1.48e-01 +1.52e-01 +1.52e-01 +1.56e-01 +1.49e-01 +1.38e-01 +1.38e-01 +1.34e-01 +1.30e-01 +1.26e-01 +1.34e-01 +1.85e-01 +8.58e-02 +2.44e-02 +1.48e-01 +1.45e-01 +1.47e-01 +1.50e-01 +1.53e-01 +1.41e-01 +1.37e-01 +1.35e-01 +1.27e-01 +1.27e-01 +1.23e-01 +1.24e-01 +1.55e-01 +8.14e-02 +2.87e-02 +1.44e-01 +1.47e-01 +1.45e-01 +1.44e-01 +1.45e-01 +1.40e-01 +1.42e-01 +1.37e-01 +1.27e-01 +1.28e-01 +1.17e-01 +1.21e-01 +1.63e-01 +5.00e-02 +1.53e-02 +1.47e-01 +1.48e-01 +1.47e-01 +1.53e-01 +1.45e-01 +1.50e-01 +1.52e-01 +1.47e-01 +1.34e-01 +1.29e-01 +1.22e-01 +1.13e-01 +1.35e-01 +4.55e-02 +1.41e-02 +2.05e-01 +1.89e-01 +1.85e-01 +2.07e-01 +2.14e-01 +2.10e-01 +1.91e-01 +1.99e-01 +1.97e-01 +1.79e-01 +1.74e-01 +1.47e-01 +7.71e-02 +1.94e-02 +6.96e-03 +1.71e-01 +9.07e-02 +1.18e-01 +1.23e-01 +1.31e-01 +1.29e-01 +1.02e-01 +1.39e-01 +1.15e-01 +7.59e-02 +8.17e-02 +5.28e-02 +2.69e-02 +1.04e-02 +3.19e-03 +5.55e-02 +2.88e-02 +3.51e-02 +3.72e-02 +5.09e-02 +4.45e-02 +3.21e-02 +4.62e-02 +3.95e-02 +2.61e-02 +2.32e-02 +1.25e-02 +9.63e-03 +6.12e-03 +3.33e-03 +1.57e-01 +1.68e-01 +1.77e-01 +1.71e-01 +1.70e-01 +1.76e-01 +1.72e-01 +1.62e-01 +1.62e-01 +1.58e-01 +1.58e-01 +1.48e-01 +1.49e-01 +3.17e-02 +1.30e-02 +1.73e-01 +1.78e-01 +1.74e-01 +1.61e-01 +1.60e-01 +1.64e-01 +1.67e-01 +1.62e-01 +1.58e-01 +1.56e-01 +1.59e-01 +1.53e-01 +1.62e-01 +5.38e-02 +2.31e-02 +1.66e-01 +1.78e-01 +1.66e-01 +1.64e-01 +1.63e-01 +1.71e-01 +1.77e-01 +1.69e-01 +1.55e-01 +1.57e-01 +1.50e-01 +1.66e-01 +2.15e-01 +9.77e-02 +2.59e-02 +1.65e-01 +1.70e-01 +1.68e-01 +1.65e-01 +1.69e-01 +1.74e-01 +6.32e-02 +1.61e-01 +1.64e-01 +1.57e-01 +1.44e-01 +1.50e-01 +1.92e-01 +8.51e-02 +2.93e-02 +1.60e-01 +1.58e-01 +1.61e-01 +1.66e-01 +1.68e-01 +1.70e-01 +1.63e-01 +1.61e-01 +1.62e-01 +1.60e-01 +1.44e-01 +1.42e-01 +1.97e-01 +1.03e-01 +2.80e-02 +1.55e-01 +1.62e-01 +1.58e-01 +1.62e-01 +1.63e-01 +1.64e-01 +1.60e-01 +1.52e-01 +1.55e-01 +1.57e-01 +1.44e-01 +1.41e-01 +1.88e-01 +9.04e-02 +2.91e-02 +1.49e-01 +1.51e-01 +1.62e-01 +1.64e-01 +1.61e-01 +1.56e-01 +1.57e-01 +1.49e-01 +1.46e-01 +1.48e-01 +1.45e-01 +1.41e-01 +1.63e-01 +9.25e-02 +3.25e-02 +1.52e-01 +1.51e-01 +1.52e-01 +1.59e-01 +1.57e-01 +1.54e-01 +1.55e-01 +1.52e-01 +1.48e-01 +1.42e-01 +1.43e-01 +1.45e-01 +1.82e-01 +6.97e-02 +2.31e-02 +1.55e-01 +1.54e-01 +1.58e-01 +1.59e-01 +1.58e-01 +1.62e-01 +1.50e-01 +1.47e-01 +1.42e-01 +1.42e-01 +1.33e-01 +1.37e-01 +1.74e-01 +6.53e-02 +2.18e-02 +1.52e-01 +1.45e-01 +1.60e-01 +1.58e-01 +1.58e-01 +1.54e-01 +1.50e-01 +1.37e-01 +1.30e-01 +1.26e-01 +1.30e-01 +1.23e-01 +1.71e-01 +7.09e-02 +2.53e-02 +1.42e-01 +1.41e-01 +1.47e-01 +1.47e-01 +1.58e-01 +1.48e-01 +1.36e-01 +1.35e-01 +1.33e-01 +1.29e-01 +1.19e-01 +1.14e-01 +1.45e-01 +4.24e-02 +1.27e-02 +1.42e-01 +6.20e-02 +1.44e-01 +1.45e-01 +1.42e-01 +1.37e-01 +1.42e-01 +1.47e-01 +1.33e-01 +1.30e-01 +1.15e-01 +1.07e-01 +1.24e-01 +3.83e-02 +1.19e-02 +1.85e-01 +1.70e-01 +1.73e-01 +1.81e-01 +1.77e-01 +1.76e-01 +1.75e-01 +1.93e-01 +1.94e-01 +1.58e-01 +1.45e-01 +1.24e-01 +7.16e-02 +2.05e-02 +7.17e-03 +1.27e-01 +6.47e-02 +8.27e-02 +1.04e-01 +7.86e-02 +7.86e-02 +7.04e-02 +6.74e-02 +1.02e-01 +7.60e-02 +5.37e-02 +4.16e-02 +2.54e-02 +6.01e-03 +1.97e-03 +5.06e-02 +2.49e-02 +3.45e-02 +3.64e-02 +2.84e-02 +2.70e-02 +2.34e-02 +2.43e-02 +3.12e-02 +2.55e-02 +1.56e-02 +1.52e-02 +9.91e-03 +3.26e-03 +1.47e-03 +2.30e-01 +2.43e-01 +2.55e-01 +2.55e-01 +2.51e-01 +2.39e-01 +2.30e-01 +2.27e-01 +2.29e-01 +2.07e-01 +2.02e-01 +2.14e-01 +1.61e-01 +2.95e-02 +7.43e-03 +2.45e-01 +2.22e-01 +2.61e-01 +2.31e-01 +2.30e-01 +2.40e-01 +2.34e-01 +2.18e-01 +2.03e-01 +2.11e-01 +1.97e-01 +1.92e-01 +1.80e-01 +4.88e-02 +1.49e-02 +2.38e-01 +2.17e-01 +2.52e-01 +2.29e-01 +2.25e-01 +2.44e-01 +2.15e-01 +2.39e-01 +2.19e-01 +2.19e-01 +2.01e-01 +1.92e-01 +2.20e-01 +9.45e-02 +2.94e-02 +2.36e-01 +2.27e-01 +2.34e-01 +2.40e-01 +2.35e-01 +2.56e-01 +2.38e-01 +2.37e-01 +2.25e-01 +2.08e-01 +1.82e-01 +1.60e-01 +1.81e-01 +5.67e-02 +1.61e-02 +2.29e-01 +2.18e-01 +2.26e-01 +2.37e-01 +2.21e-01 +2.35e-01 +2.69e-01 +2.31e-01 +2.30e-01 +2.32e-01 +2.01e-01 +1.47e-01 +1.58e-01 +7.81e-02 +2.93e-02 +2.28e-01 +2.05e-01 +2.20e-01 +2.25e-01 +2.12e-01 +2.31e-01 +2.33e-01 +2.13e-01 +2.09e-01 +2.24e-01 +1.97e-01 +1.71e-01 +1.79e-01 +7.52e-02 +2.31e-02 +2.03e-01 +2.07e-01 +2.27e-01 +2.19e-01 +2.11e-01 +2.22e-01 +2.17e-01 +2.08e-01 +1.77e-01 +2.01e-01 +1.89e-01 +1.57e-01 +1.63e-01 +5.48e-02 +1.75e-02 +2.07e-01 +2.13e-01 +2.09e-01 +2.25e-01 +2.36e-01 +2.27e-01 +2.11e-01 +2.10e-01 +1.87e-01 +1.89e-01 +1.88e-01 +1.79e-01 +1.33e-01 +3.03e-02 +1.06e-02 +2.08e-01 +2.14e-01 +2.12e-01 +2.27e-01 +2.27e-01 +2.05e-01 +2.29e-01 +2.21e-01 +1.93e-01 +2.02e-01 +1.81e-01 +1.73e-01 +1.67e-01 +4.08e-02 +1.39e-02 +2.00e-01 +2.19e-01 +2.28e-01 +2.14e-01 +2.28e-01 +2.06e-01 +2.17e-01 +1.92e-01 +1.79e-01 +1.74e-01 +1.72e-01 +1.71e-01 +1.60e-01 +4.62e-02 +1.43e-02 +1.82e-01 +1.92e-01 +2.16e-01 +2.14e-01 +2.12e-01 +2.26e-01 +1.94e-01 +1.68e-01 +1.64e-01 +1.50e-01 +1.55e-01 +1.48e-01 +1.04e-01 +4.02e-02 +1.44e-02 +1.56e-01 +1.60e-01 +1.93e-01 +1.87e-01 +1.94e-01 +2.01e-01 +2.07e-01 +1.85e-01 +1.76e-01 +1.49e-01 +1.39e-01 +1.38e-01 +1.04e-01 +2.84e-02 +1.17e-02 +1.33e-01 +1.46e-01 +1.65e-01 +1.58e-01 +1.74e-01 +1.70e-01 +1.37e-01 +1.94e-01 +1.93e-01 +1.64e-01 +1.20e-01 +1.02e-01 +6.63e-02 +1.85e-02 +8.29e-03 +6.84e-02 +4.09e-02 +5.11e-02 +6.13e-02 +4.88e-02 +4.65e-02 +3.93e-02 +4.74e-02 +8.51e-02 +5.59e-02 +2.40e-02 +2.44e-02 +1.30e-02 +4.68e-03 +2.65e-03 +2.59e-02 +1.30e-02 +1.81e-02 +1.96e-02 +1.49e-02 +1.49e-02 +1.50e-02 +1.42e-02 +2.71e-02 +2.16e-02 +1.05e-02 +7.72e-03 +4.11e-03 +2.35e-03 +9.64e-04 +2.21e-01 +2.15e-01 +2.72e-01 +1.92e-01 +3.01e-01 +2.43e-01 +2.13e-01 +2.18e-01 +1.88e-01 +1.66e-01 +1.13e-01 +1.21e-01 +1.08e-01 +2.92e-02 +8.37e-03 +2.75e-01 +1.89e-01 +3.28e-01 +3.64e-01 +2.91e-01 +2.34e-01 +1.86e-01 +1.90e-01 +1.53e-01 +1.35e-01 +1.21e-01 +1.01e-01 +8.79e-02 +3.91e-02 +1.37e-02 +2.37e-01 +2.28e-01 +2.92e-01 +2.21e-01 +1.70e-01 +2.29e-01 +1.86e-01 +1.95e-01 +1.65e-01 +1.42e-01 +1.73e-01 +1.38e-01 +7.39e-02 +4.16e-02 +2.91e-02 +2.73e-01 +2.33e-01 +2.78e-01 +3.06e-01 +2.31e-01 +2.52e-01 +2.69e-01 +2.39e-01 +2.13e-01 +1.63e-01 +1.20e-01 +8.39e-02 +5.83e-02 +2.86e-02 +1.32e-02 +2.63e-01 +1.92e-01 +2.54e-01 +3.11e-01 +2.65e-01 +2.75e-01 +2.71e-01 +2.21e-01 +1.81e-01 +1.68e-01 +1.49e-01 +7.05e-02 +5.03e-02 +2.54e-02 +1.28e-02 +2.09e-01 +1.61e-01 +2.21e-01 +2.28e-01 +1.55e-01 +1.78e-01 +2.16e-01 +1.50e-01 +1.32e-01 +1.76e-01 +1.57e-01 +1.01e-01 +5.61e-02 +2.32e-02 +1.53e-02 +1.50e-01 +1.81e-01 +2.03e-01 +2.04e-01 +1.79e-01 +1.94e-01 +1.81e-01 +1.52e-01 +1.26e-01 +1.13e-01 +9.16e-02 +6.72e-02 +3.94e-02 +2.35e-02 +1.45e-02 +1.06e-01 +1.43e-01 +1.61e-01 +1.57e-01 +1.80e-01 +1.80e-01 +1.24e-01 +1.22e-01 +8.18e-02 +7.30e-02 +9.33e-02 +7.39e-02 +3.89e-02 +2.07e-02 +8.17e-03 +1.13e-01 +1.37e-01 +1.61e-01 +2.21e-01 +1.71e-01 +1.51e-01 +1.16e-01 +1.45e-01 +1.13e-01 +1.05e-01 +9.41e-02 +7.14e-02 +5.35e-02 +1.91e-02 +7.31e-03 +8.55e-02 +1.58e-01 +2.08e-01 +1.60e-01 +1.37e-01 +8.33e-02 +1.16e-01 +1.39e-01 +1.15e-01 +9.22e-02 +8.69e-02 +6.24e-02 +4.30e-02 +2.48e-02 +7.57e-03 +9.22e-02 +1.13e-01 +1.60e-01 +1.04e-01 +1.40e-01 +1.35e-01 +1.08e-01 +1.02e-01 +8.56e-02 +5.91e-02 +7.46e-02 +6.28e-02 +3.81e-02 +2.04e-02 +9.21e-03 +7.85e-02 +6.83e-02 +1.01e-01 +9.42e-02 +9.47e-02 +1.38e-01 +1.13e-01 +6.40e-02 +6.03e-02 +5.21e-02 +4.63e-02 +4.67e-02 +3.57e-02 +9.24e-03 +3.46e-03 +3.82e-02 +4.99e-02 +6.21e-02 +4.80e-02 +5.79e-02 +8.37e-02 +6.86e-02 +6.66e-02 +5.19e-02 +5.77e-02 +4.00e-02 +3.46e-02 +2.69e-02 +9.16e-03 +3.73e-03 +1.66e-02 +1.94e-02 +2.26e-02 +1.79e-02 +2.45e-02 +3.08e-02 +2.00e-02 +2.89e-02 +3.35e-02 +2.83e-02 +1.58e-02 +1.10e-02 +7.32e-03 +3.83e-03 +1.85e-03 +1.35e-02 +7.32e-03 +8.41e-03 +8.10e-03 +9.00e-03 +1.12e-02 +5.57e-03 +9.30e-03 +1.48e-02 +1.39e-02 +6.30e-03 +3.42e-03 +2.99e-03 +1.40e-03 +5.26e-04 +7.56e-02 +7.51e-02 +1.02e-01 +6.63e-02 +1.09e-01 +8.00e-02 +7.46e-02 +7.69e-02 +7.23e-02 +5.75e-02 +3.77e-02 +3.71e-02 +3.59e-02 +2.14e-02 +7.54e-03 +1.13e-01 +5.14e-02 +1.11e-01 +1.48e-01 +1.23e-01 +8.67e-02 +6.58e-02 +5.93e-02 +5.79e-02 +5.00e-02 +3.78e-02 +2.92e-02 +2.64e-02 +1.76e-02 +8.34e-03 +8.72e-02 +6.82e-02 +1.03e-01 +9.93e-02 +4.68e-02 +8.00e-02 +6.54e-02 +5.74e-02 +5.88e-02 +4.17e-02 +6.05e-02 +5.26e-02 +2.82e-02 +1.18e-02 +1.25e-02 +8.93e-02 +6.93e-02 +1.05e-01 +1.22e-01 +7.82e-02 +8.44e-02 +9.53e-02 +9.08e-02 +8.61e-02 +6.38e-02 +4.26e-02 +2.94e-02 +1.95e-02 +1.16e-02 +5.85e-03 +1.07e-01 +7.48e-02 +9.61e-02 +1.16e-01 +1.00e-01 +9.56e-02 +1.01e-01 +7.22e-02 +7.15e-02 +4.92e-02 +5.10e-02 +2.14e-02 +1.39e-02 +8.55e-03 +5.42e-03 +6.30e-02 +5.46e-02 +6.94e-02 +7.56e-02 +5.76e-02 +4.99e-02 +6.77e-02 +4.79e-02 +4.29e-02 +5.31e-02 +5.73e-02 +4.32e-02 +2.01e-02 +8.20e-03 +4.18e-03 +6.21e-02 +5.98e-02 +7.34e-02 +7.91e-02 +4.60e-02 +5.73e-02 +6.01e-02 +5.26e-02 +5.01e-02 +3.98e-02 +3.12e-02 +2.62e-02 +1.44e-02 +6.44e-03 +6.95e-03 +2.95e-02 +5.38e-02 +4.94e-02 +4.81e-02 +5.65e-02 +6.07e-02 +3.70e-02 +3.88e-02 +2.59e-02 +2.05e-02 +2.51e-02 +2.29e-02 +1.19e-02 +1.08e-02 +8.34e-03 +3.62e-02 +4.00e-02 +5.20e-02 +6.63e-02 +6.42e-02 +5.90e-02 +3.69e-02 +4.87e-02 +4.00e-02 +3.31e-02 +2.85e-02 +2.46e-02 +2.05e-02 +8.72e-03 +3.45e-03 +2.17e-02 +4.81e-02 +6.28e-02 +6.24e-02 +4.43e-02 +2.41e-02 +3.73e-02 +4.41e-02 +4.25e-02 +2.80e-02 +2.62e-02 +2.10e-02 +1.12e-02 +1.28e-02 +6.90e-03 +2.96e-02 +4.88e-02 +5.84e-02 +2.83e-02 +4.58e-02 +3.78e-02 +2.43e-02 +3.47e-02 +3.08e-02 +2.18e-02 +2.32e-02 +1.70e-02 +1.26e-02 +9.95e-03 +8.03e-03 +2.02e-02 +2.21e-02 +3.09e-02 +2.79e-02 +2.92e-02 +4.73e-02 +3.85e-02 +2.69e-02 +1.89e-02 +1.70e-02 +1.44e-02 +1.42e-02 +1.51e-02 +4.21e-03 +1.55e-03 +1.72e-02 +1.70e-02 +2.18e-02 +2.50e-02 +1.84e-02 +3.18e-02 +2.53e-02 +2.01e-02 +1.37e-02 +1.89e-02 +1.02e-02 +1.15e-02 +1.16e-02 +3.85e-03 +1.22e-03 +5.45e-03 +9.53e-03 +9.49e-03 +6.68e-03 +1.08e-02 +1.59e-02 +1.34e-02 +1.53e-02 +1.86e-02 +1.03e-02 +1.07e-02 +6.50e-03 +3.98e-03 +2.38e-03 +2.29e-03 +4.46e-03 +5.06e-03 +5.22e-03 +4.55e-03 +5.72e-03 +1.00e-02 +6.07e-03 +5.95e-03 +1.20e-02 +8.03e-03 +5.95e-03 +3.61e-03 +1.35e-03 +8.82e-04 +1.47e-03 +Upper Bounds +8.83e-01 +9.04e-01 +9.04e-01 +9.06e-01 +8.90e-01 +8.71e-01 +8.51e-01 +8.82e-01 +8.99e-01 +9.42e-01 +8.75e-01 +9.67e-01 +1.38e+00 +2.14e+00 +7.89e-01 +8.70e-01 +9.12e-01 +9.06e-01 +8.15e-01 +8.92e-01 +9.06e-01 +8.84e-01 +8.83e-01 +8.68e-01 +8.58e-01 +9.07e-01 +9.12e-01 +1.29e+00 +1.63e+00 +5.33e-01 +9.10e-01 +8.73e-01 +9.05e-01 +9.08e-01 +8.81e-01 +8.85e-01 +8.70e-01 +8.31e-01 +8.30e-01 +8.46e-01 +8.79e-01 +9.36e-01 +1.33e+00 +2.08e+00 +7.86e-01 +8.80e-01 +8.72e-01 +8.61e-01 +8.81e-01 +8.35e-01 +8.38e-01 +8.52e-01 +8.74e-01 +8.63e-01 +8.63e-01 +8.27e-01 +9.20e-01 +1.33e+00 +2.00e+00 +6.39e-01 +8.76e-01 +9.10e-01 +9.11e-01 +9.20e-01 +8.60e-01 +8.61e-01 +8.34e-01 +8.36e-01 +8.28e-01 +8.72e-01 +8.44e-01 +9.49e-01 +1.41e+00 +1.65e+00 +6.46e-01 +9.01e-01 +8.97e-01 +8.94e-01 +8.90e-01 +8.81e-01 +8.46e-01 +8.38e-01 +8.15e-01 +8.49e-01 +8.29e-01 +8.56e-01 +9.61e-01 +1.35e+00 +1.44e+00 +5.12e-01 +8.79e-01 +8.59e-01 +8.42e-01 +8.55e-01 +8.31e-01 +8.41e-01 +8.24e-01 +8.31e-01 +8.11e-01 +7.99e-01 +8.06e-01 +8.32e-01 +1.19e+00 +1.66e+00 +6.17e-01 +9.09e-01 +9.03e-01 +8.95e-01 +8.51e-01 +8.33e-01 +8.62e-01 +8.26e-01 +8.10e-01 +7.35e-01 +7.80e-01 +7.82e-01 +7.20e-01 +1.00e+00 +8.79e-01 +3.78e-01 +8.81e-01 +8.79e-01 +8.98e-01 +9.09e-01 +8.56e-01 +8.36e-01 +8.27e-01 +8.09e-01 +7.69e-01 +7.73e-01 +7.71e-01 +7.76e-01 +1.14e+00 +8.33e-01 +2.68e-01 +8.83e-01 +9.23e-01 +9.16e-01 +9.05e-01 +8.94e-01 +8.03e-01 +8.19e-01 +7.89e-01 +8.04e-01 +7.99e-01 +7.77e-01 +7.67e-01 +1.16e+00 +8.69e-01 +3.26e-01 +9.04e-01 +8.93e-01 +9.15e-01 +8.96e-01 +8.99e-01 +8.47e-01 +7.92e-01 +8.32e-01 +7.94e-01 +7.96e-01 +8.49e-01 +7.81e-01 +1.06e+00 +6.40e-01 +1.87e-01 +9.49e-01 +9.48e-01 +9.08e-01 +8.57e-01 +9.51e-01 +9.10e-01 +8.62e-01 +8.93e-01 +8.43e-01 +8.47e-01 +8.29e-01 +7.55e-01 +1.11e+00 +4.81e-01 +1.57e-01 +1.38e+00 +1.37e+00 +1.26e+00 +1.31e+00 +1.37e+00 +1.34e+00 +1.26e+00 +1.21e+00 +1.21e+00 +1.21e+00 +1.20e+00 +1.12e+00 +1.06e+00 +3.64e-01 +1.05e-01 +2.50e+00 +1.75e+00 +1.96e+00 +1.28e+00 +1.40e+00 +1.59e+00 +1.27e+00 +1.18e+00 +9.31e-01 +9.87e-01 +9.06e-01 +7.50e-01 +4.87e-01 +1.88e-01 +9.31e-02 +1.08e+00 +7.08e-01 +8.14e-01 +4.09e-01 +4.59e-01 +5.72e-01 +4.06e-01 +3.97e-01 +3.49e-01 +3.11e-01 +3.14e-01 +2.24e-01 +1.84e-01 +7.93e-02 +3.58e-02 +8.56e-01 +8.80e-01 +8.90e-01 +8.72e-01 +8.83e-01 +8.79e-01 +9.07e-01 +8.87e-01 +8.46e-01 +8.81e-01 +9.13e-01 +9.22e-01 +1.35e+00 +1.99e+00 +7.68e-01 +8.95e-01 +8.96e-01 +9.01e-01 +8.77e-01 +8.78e-01 +8.57e-01 +8.71e-01 +8.72e-01 +8.61e-01 +8.65e-01 +8.84e-01 +9.25e-01 +1.27e+00 +1.84e+00 +6.71e-01 +9.00e-01 +8.80e-01 +8.68e-01 +8.83e-01 +8.81e-01 +8.83e-01 +8.68e-01 +8.89e-01 +8.61e-01 +9.00e-01 +9.04e-01 +9.07e-01 +1.33e+00 +1.66e+00 +5.75e-01 +9.10e-01 +9.28e-01 +9.04e-01 +9.15e-01 +8.67e-01 +8.37e-01 +8.58e-01 +8.74e-01 +8.49e-01 +8.43e-01 +8.73e-01 +9.20e-01 +1.29e+00 +1.77e+00 +6.31e-01 +8.99e-01 +9.11e-01 +8.97e-01 +8.66e-01 +8.29e-01 +8.49e-01 +8.85e-01 +8.47e-01 +8.39e-01 +8.47e-01 +8.91e-01 +9.17e-01 +1.38e+00 +1.51e+00 +6.62e-01 +8.74e-01 +8.88e-01 +8.84e-01 +8.46e-01 +8.97e-01 +8.43e-01 +8.27e-01 +8.17e-01 +8.15e-01 +8.35e-01 +8.69e-01 +9.33e-01 +1.35e+00 +1.72e+00 +6.01e-01 +9.01e-01 +9.12e-01 +8.41e-01 +8.34e-01 +8.86e-01 +8.49e-01 +7.94e-01 +7.99e-01 +7.91e-01 +8.03e-01 +8.26e-01 +8.34e-01 +1.17e+00 +1.46e+00 +5.66e-01 +8.53e-01 +8.62e-01 +8.25e-01 +8.26e-01 +8.26e-01 +7.99e-01 +7.85e-01 +7.29e-01 +7.14e-01 +7.51e-01 +7.41e-01 +5.28e-01 +9.65e-01 +8.41e-01 +3.51e-01 +8.43e-01 +9.30e-01 +8.75e-01 +8.72e-01 +8.86e-01 +8.25e-01 +8.06e-01 +7.70e-01 +7.66e-01 +7.77e-01 +7.36e-01 +7.06e-01 +9.91e-01 +6.14e-01 +1.92e-01 +8.89e-01 +9.28e-01 +8.82e-01 +8.85e-01 +8.26e-01 +8.77e-01 +8.28e-01 +7.72e-01 +7.97e-01 +7.70e-01 +8.14e-01 +7.60e-01 +1.08e+00 +7.25e-01 +2.17e-01 +8.99e-01 +9.33e-01 +8.95e-01 +8.67e-01 +9.07e-01 +8.80e-01 +8.33e-01 +7.98e-01 +8.16e-01 +7.99e-01 +8.47e-01 +7.75e-01 +1.08e+00 +6.72e-01 +2.06e-01 +9.39e-01 +8.99e-01 +9.19e-01 +9.05e-01 +9.21e-01 +9.37e-01 +8.70e-01 +8.47e-01 +8.32e-01 +8.66e-01 +8.48e-01 +8.12e-01 +1.00e+00 +4.14e-01 +1.43e-01 +1.29e+00 +1.41e+00 +1.29e+00 +1.35e+00 +1.47e+00 +1.34e+00 +1.23e+00 +1.28e+00 +1.17e+00 +1.16e+00 +1.29e+00 +1.20e+00 +1.04e+00 +2.68e-01 +6.98e-02 +1.56e+00 +1.49e+00 +1.36e+00 +1.52e+00 +1.85e+00 +1.77e+00 +1.39e+00 +1.40e+00 +1.09e+00 +8.12e-01 +8.12e-01 +8.15e-01 +5.39e-01 +1.74e-01 +7.42e-02 +5.46e-01 +5.41e-01 +5.27e-01 +5.05e-01 +6.76e-01 +7.08e-01 +4.76e-01 +5.00e-01 +4.66e-01 +2.59e-01 +2.59e-01 +2.67e-01 +2.12e-01 +9.34e-02 +3.59e-02 +8.98e-01 +8.76e-01 +9.11e-01 +8.74e-01 +8.57e-01 +8.49e-01 +8.45e-01 +8.23e-01 +8.73e-01 +8.56e-01 +8.41e-01 +9.32e-01 +1.56e+00 +2.14e+00 +8.32e-01 +9.04e-01 +8.85e-01 +8.48e-01 +8.66e-01 +8.66e-01 +8.58e-01 +8.22e-01 +8.28e-01 +8.32e-01 +8.65e-01 +8.49e-01 +9.47e-01 +1.32e+00 +2.01e+00 +8.20e-01 +8.96e-01 +9.02e-01 +9.15e-01 +8.55e-01 +8.63e-01 +8.56e-01 +8.51e-01 +8.73e-01 +8.47e-01 +8.41e-01 +8.38e-01 +8.99e-01 +1.24e+00 +1.71e+00 +6.45e-01 +8.13e-01 +8.67e-01 +8.68e-01 +8.57e-01 +8.41e-01 +8.27e-01 +8.41e-01 +8.63e-01 +8.60e-01 +8.27e-01 +8.62e-01 +8.45e-01 +1.26e+00 +1.59e+00 +5.28e-01 +8.37e-01 +8.48e-01 +8.61e-01 +8.18e-01 +8.55e-01 +8.57e-01 +7.87e-01 +8.18e-01 +8.05e-01 +8.09e-01 +8.66e-01 +8.95e-01 +1.31e+00 +1.33e+00 +3.87e-01 +8.40e-01 +8.59e-01 +8.21e-01 +8.32e-01 +8.27e-01 +8.30e-01 +7.89e-01 +7.95e-01 +8.16e-01 +8.14e-01 +8.48e-01 +8.74e-01 +1.25e+00 +1.74e+00 +6.56e-01 +8.62e-01 +8.97e-01 +8.49e-01 +8.48e-01 +8.43e-01 +8.56e-01 +8.49e-01 +7.73e-01 +7.97e-01 +8.23e-01 +8.50e-01 +8.53e-01 +1.22e+00 +1.23e+00 +4.60e-01 +8.51e-01 +8.51e-01 +8.30e-01 +8.17e-01 +8.30e-01 +8.17e-01 +7.95e-01 +7.91e-01 +7.83e-01 +7.99e-01 +7.74e-01 +7.63e-01 +1.10e+00 +8.15e-01 +3.18e-01 +8.51e-01 +8.51e-01 +8.64e-01 +8.45e-01 +8.30e-01 +8.19e-01 +8.14e-01 +8.18e-01 +7.60e-01 +7.72e-01 +7.64e-01 +7.45e-01 +1.06e+00 +8.19e-01 +3.07e-01 +8.70e-01 +8.92e-01 +8.63e-01 +8.73e-01 +8.70e-01 +8.23e-01 +8.34e-01 +7.96e-01 +7.41e-01 +7.85e-01 +7.76e-01 +7.97e-01 +1.03e+00 +6.15e-01 +2.40e-01 +8.46e-01 +8.88e-01 +8.83e-01 +8.69e-01 +8.83e-01 +8.65e-01 +8.68e-01 +8.40e-01 +7.58e-01 +7.91e-01 +7.91e-01 +7.77e-01 +1.06e+00 +6.09e-01 +1.67e-01 +8.76e-01 +9.27e-01 +9.32e-01 +9.51e-01 +9.02e-01 +8.97e-01 +8.78e-01 +8.56e-01 +8.35e-01 +8.55e-01 +8.47e-01 +8.43e-01 +9.29e-01 +5.11e-01 +1.77e-01 +1.33e+00 +1.33e+00 +1.32e+00 +1.25e+00 +1.30e+00 +1.29e+00 +1.17e+00 +1.21e+00 +1.18e+00 +1.15e+00 +1.16e+00 +1.21e+00 +1.14e+00 +2.72e-01 +8.55e-02 +1.89e+00 +1.37e+00 +1.71e+00 +1.75e+00 +1.82e+00 +1.16e+00 +1.16e+00 +1.09e+00 +9.10e-01 +8.87e-01 +6.38e-01 +5.79e-01 +5.13e-01 +2.08e-01 +5.28e-02 +6.66e-01 +4.35e-01 +7.04e-01 +6.72e-01 +6.85e-01 +4.41e-01 +3.91e-01 +4.18e-01 +2.80e-01 +3.20e-01 +1.45e-01 +1.85e-01 +1.73e-01 +1.30e-01 +6.20e-02 +8.83e-01 +8.54e-01 +8.74e-01 +8.97e-01 +8.66e-01 +8.53e-01 +8.20e-01 +8.29e-01 +7.95e-01 +8.07e-01 +7.74e-01 +8.95e-01 +1.34e+00 +2.13e+00 +7.78e-01 +8.75e-01 +9.11e-01 +8.53e-01 +8.24e-01 +8.43e-01 +8.30e-01 +8.44e-01 +8.16e-01 +8.23e-01 +8.70e-01 +8.45e-01 +8.81e-01 +1.24e+00 +1.66e+00 +6.51e-01 +8.43e-01 +8.65e-01 +8.86e-01 +8.33e-01 +7.75e-01 +8.33e-01 +7.93e-01 +8.14e-01 +7.91e-01 +8.49e-01 +8.48e-01 +9.21e-01 +1.22e+00 +1.35e+00 +5.11e-01 +8.45e-01 +8.50e-01 +8.67e-01 +8.43e-01 +8.32e-01 +8.35e-01 +8.18e-01 +8.02e-01 +8.09e-01 +8.16e-01 +8.13e-01 +8.33e-01 +1.24e+00 +1.20e+00 +4.92e-01 +8.11e-01 +8.21e-01 +8.46e-01 +8.56e-01 +8.12e-01 +7.90e-01 +8.10e-01 +7.88e-01 +8.08e-01 +8.07e-01 +8.57e-01 +8.34e-01 +1.25e+00 +1.61e+00 +5.85e-01 +8.68e-01 +7.99e-01 +8.45e-01 +8.59e-01 +8.33e-01 +7.98e-01 +8.23e-01 +8.40e-01 +8.12e-01 +8.01e-01 +8.15e-01 +8.78e-01 +1.27e+00 +1.49e+00 +5.48e-01 +8.43e-01 +8.37e-01 +8.81e-01 +7.97e-01 +7.95e-01 +7.90e-01 +8.20e-01 +7.94e-01 +7.75e-01 +7.96e-01 +7.56e-01 +8.00e-01 +1.25e+00 +1.44e+00 +5.06e-01 +8.60e-01 +8.53e-01 +8.41e-01 +8.13e-01 +7.94e-01 +8.04e-01 +8.30e-01 +7.78e-01 +7.68e-01 +7.54e-01 +7.46e-01 +7.89e-01 +1.17e+00 +9.25e-01 +3.31e-01 +8.47e-01 +8.26e-01 +8.14e-01 +8.34e-01 +8.09e-01 +8.13e-01 +8.14e-01 +8.04e-01 +7.77e-01 +7.65e-01 +7.94e-01 +8.47e-01 +1.24e+00 +9.33e-01 +2.64e-01 +8.55e-01 +8.28e-01 +8.67e-01 +8.38e-01 +8.25e-01 +8.22e-01 +7.96e-01 +7.62e-01 +7.99e-01 +7.61e-01 +7.38e-01 +7.68e-01 +1.16e+00 +6.75e-01 +2.70e-01 +8.80e-01 +8.88e-01 +8.39e-01 +8.92e-01 +8.67e-01 +8.43e-01 +8.12e-01 +8.07e-01 +7.97e-01 +8.03e-01 +7.88e-01 +8.01e-01 +1.03e+00 +4.71e-01 +1.39e-01 +9.42e-01 +8.84e-01 +8.89e-01 +4.30e-01 +9.12e-01 +9.52e-01 +8.80e-01 +8.13e-01 +8.44e-01 +8.47e-01 +8.47e-01 +8.42e-01 +9.87e-01 +3.92e-01 +1.14e-01 +1.24e+00 +1.25e+00 +1.29e+00 +1.25e+00 +1.29e+00 +1.28e+00 +1.08e+00 +1.14e+00 +1.21e+00 +1.12e+00 +1.06e+00 +1.07e+00 +1.14e+00 +3.91e-01 +1.23e-01 +1.55e+00 +1.24e+00 +1.03e+00 +1.46e+00 +1.44e+00 +1.17e+00 +8.10e-01 +5.67e-01 +9.52e-01 +9.00e-01 +5.78e-01 +5.33e-01 +4.71e-01 +2.10e-01 +8.15e-02 +5.84e-01 +4.29e-01 +4.39e-01 +4.57e-01 +5.73e-01 +4.28e-01 +3.39e-01 +1.82e-01 +3.17e-01 +3.19e-01 +2.03e-01 +1.80e-01 +1.39e-01 +1.28e-01 +6.06e-02 +8.53e-01 +8.42e-01 +8.16e-01 +8.39e-01 +8.62e-01 +8.30e-01 +8.25e-01 +7.57e-01 +7.84e-01 +8.17e-01 +7.86e-01 +8.91e-01 +1.29e+00 +1.58e+00 +6.19e-01 +8.62e-01 +8.53e-01 +8.28e-01 +8.47e-01 +8.54e-01 +8.13e-01 +7.89e-01 +7.56e-01 +8.10e-01 +8.39e-01 +8.37e-01 +8.80e-01 +1.27e+00 +1.52e+00 +5.61e-01 +8.76e-01 +8.58e-01 +8.51e-01 +8.40e-01 +8.16e-01 +8.22e-01 +8.17e-01 +7.68e-01 +8.23e-01 +8.47e-01 +8.38e-01 +8.80e-01 +1.26e+00 +1.11e+00 +4.01e-01 +8.41e-01 +8.55e-01 +8.26e-01 +8.31e-01 +8.17e-01 +8.22e-01 +8.05e-01 +8.08e-01 +8.05e-01 +8.32e-01 +8.20e-01 +8.61e-01 +1.30e+00 +1.05e+00 +3.57e-01 +8.15e-01 +8.06e-01 +8.14e-01 +8.44e-01 +8.05e-01 +8.12e-01 +8.12e-01 +8.06e-01 +7.90e-01 +7.71e-01 +8.19e-01 +8.79e-01 +1.29e+00 +1.63e+00 +5.79e-01 +7.99e-01 +7.98e-01 +8.14e-01 +8.39e-01 +8.09e-01 +7.70e-01 +7.51e-01 +8.22e-01 +8.15e-01 +7.92e-01 +7.89e-01 +8.42e-01 +1.22e+00 +1.30e+00 +4.67e-01 +8.04e-01 +8.15e-01 +8.41e-01 +8.12e-01 +8.44e-01 +7.64e-01 +7.65e-01 +7.83e-01 +7.51e-01 +7.69e-01 +7.83e-01 +7.95e-01 +1.09e+00 +1.03e+00 +3.79e-01 +8.53e-01 +8.48e-01 +8.17e-01 +7.87e-01 +8.03e-01 +7.66e-01 +7.80e-01 +7.47e-01 +7.47e-01 +7.94e-01 +7.46e-01 +7.73e-01 +1.13e+00 +8.10e-01 +3.44e-01 +8.55e-01 +8.35e-01 +8.31e-01 +8.01e-01 +8.04e-01 +8.11e-01 +8.17e-01 +7.71e-01 +7.52e-01 +7.41e-01 +7.55e-01 +7.73e-01 +1.08e+00 +8.26e-01 +2.48e-01 +8.55e-01 +8.65e-01 +8.39e-01 +8.10e-01 +7.84e-01 +8.10e-01 +7.97e-01 +7.80e-01 +7.63e-01 +7.36e-01 +7.45e-01 +8.12e-01 +1.07e+00 +7.90e-01 +3.43e-01 +8.52e-01 +8.31e-01 +8.47e-01 +8.29e-01 +8.34e-01 +8.10e-01 +8.16e-01 +8.13e-01 +7.91e-01 +7.58e-01 +7.69e-01 +8.34e-01 +1.13e+00 +5.77e-01 +2.01e-01 +8.71e-01 +9.08e-01 +8.51e-01 +8.61e-01 +8.87e-01 +9.24e-01 +8.39e-01 +8.24e-01 +8.13e-01 +8.68e-01 +8.13e-01 +8.31e-01 +1.14e+00 +5.34e-01 +1.63e-01 +1.29e+00 +1.23e+00 +1.25e+00 +1.29e+00 +1.30e+00 +1.33e+00 +1.29e+00 +1.19e+00 +1.17e+00 +1.14e+00 +1.08e+00 +1.12e+00 +1.16e+00 +4.48e-01 +1.51e-01 +1.37e+00 +1.25e+00 +1.26e+00 +1.31e+00 +1.52e+00 +1.34e+00 +1.46e+00 +9.26e-01 +1.00e+00 +7.33e-01 +5.03e-01 +5.86e-01 +4.40e-01 +1.93e-01 +7.63e-02 +5.00e-01 +4.36e-01 +4.55e-01 +4.50e-01 +5.38e-01 +4.35e-01 +5.87e-01 +2.74e-01 +3.52e-01 +2.53e-01 +1.54e-01 +1.57e-01 +1.32e-01 +8.46e-02 +5.80e-02 +7.99e-01 +8.20e-01 +8.15e-01 +8.33e-01 +8.51e-01 +8.10e-01 +7.68e-01 +7.62e-01 +7.58e-01 +7.77e-01 +7.87e-01 +8.14e-01 +1.30e+00 +1.29e+00 +3.79e-01 +7.97e-01 +8.41e-01 +8.13e-01 +7.89e-01 +8.26e-01 +8.24e-01 +8.37e-01 +7.59e-01 +7.92e-01 +7.78e-01 +7.98e-01 +8.46e-01 +1.24e+00 +1.44e+00 +5.54e-01 +8.32e-01 +8.44e-01 +8.48e-01 +8.16e-01 +8.13e-01 +7.97e-01 +8.17e-01 +8.24e-01 +7.90e-01 +7.89e-01 +7.98e-01 +8.75e-01 +1.29e+00 +1.26e+00 +4.10e-01 +8.34e-01 +8.40e-01 +7.91e-01 +7.91e-01 +8.06e-01 +8.33e-01 +7.71e-01 +7.76e-01 +8.05e-01 +8.07e-01 +8.15e-01 +8.50e-01 +1.26e+00 +1.18e+00 +3.80e-01 +8.13e-01 +8.32e-01 +8.39e-01 +8.18e-01 +7.93e-01 +7.93e-01 +7.84e-01 +7.91e-01 +7.91e-01 +7.83e-01 +7.90e-01 +8.30e-01 +1.25e+00 +1.29e+00 +4.90e-01 +8.01e-01 +8.04e-01 +8.21e-01 +8.03e-01 +7.98e-01 +7.82e-01 +7.55e-01 +7.52e-01 +8.14e-01 +8.19e-01 +7.83e-01 +8.19e-01 +1.18e+00 +1.09e+00 +4.51e-01 +8.40e-01 +8.48e-01 +8.18e-01 +7.81e-01 +7.89e-01 +7.65e-01 +7.53e-01 +7.17e-01 +7.54e-01 +7.65e-01 +7.76e-01 +8.17e-01 +1.09e+00 +6.64e-01 +2.45e-01 +8.36e-01 +7.99e-01 +8.04e-01 +7.74e-01 +8.20e-01 +7.78e-01 +7.45e-01 +7.25e-01 +7.18e-01 +7.55e-01 +7.87e-01 +8.22e-01 +1.16e+00 +5.88e-01 +1.82e-01 +8.12e-01 +8.33e-01 +8.42e-01 +7.73e-01 +7.95e-01 +7.85e-01 +7.91e-01 +7.28e-01 +7.16e-01 +6.95e-01 +7.39e-01 +7.71e-01 +1.13e+00 +7.23e-01 +2.02e-01 +7.76e-01 +8.13e-01 +8.17e-01 +8.37e-01 +7.81e-01 +7.65e-01 +7.51e-01 +7.47e-01 +7.44e-01 +7.26e-01 +7.33e-01 +7.63e-01 +1.05e+00 +6.00e-01 +1.91e-01 +8.27e-01 +7.94e-01 +8.07e-01 +8.05e-01 +7.80e-01 +7.80e-01 +8.00e-01 +7.93e-01 +7.69e-01 +7.64e-01 +7.39e-01 +7.58e-01 +1.04e+00 +5.07e-01 +1.50e-01 +8.87e-01 +8.55e-01 +8.41e-01 +8.56e-01 +8.73e-01 +8.47e-01 +8.19e-01 +8.38e-01 +8.05e-01 +8.07e-01 +7.65e-01 +7.30e-01 +9.77e-01 +4.70e-01 +1.10e-01 +1.23e+00 +1.19e+00 +1.16e+00 +1.21e+00 +1.27e+00 +1.25e+00 +1.14e+00 +1.10e+00 +1.11e+00 +1.13e+00 +1.17e+00 +1.01e+00 +9.39e-01 +4.79e-01 +1.76e-01 +1.53e+00 +1.30e+00 +1.11e+00 +1.31e+00 +1.28e+00 +1.18e+00 +1.16e+00 +8.91e-01 +7.64e-01 +6.86e-01 +7.35e-01 +6.78e-01 +3.62e-01 +2.02e-01 +1.12e-01 +5.19e-01 +4.91e-01 +4.40e-01 +4.60e-01 +4.87e-01 +3.65e-01 +5.18e-01 +3.03e-01 +2.69e-01 +1.84e-01 +2.14e-01 +2.43e-01 +1.44e-01 +7.93e-02 +4.59e-02 +8.53e-01 +8.34e-01 +8.24e-01 +8.44e-01 +7.93e-01 +7.98e-01 +8.11e-01 +8.08e-01 +7.84e-01 +7.45e-01 +7.60e-01 +7.66e-01 +1.21e+00 +9.01e-01 +4.10e-01 +8.39e-01 +8.13e-01 +8.33e-01 +8.25e-01 +8.17e-01 +7.68e-01 +7.94e-01 +7.61e-01 +7.94e-01 +7.58e-01 +7.43e-01 +7.76e-01 +1.12e+00 +1.01e+00 +3.78e-01 +8.15e-01 +7.96e-01 +8.12e-01 +8.03e-01 +7.86e-01 +7.79e-01 +7.51e-01 +7.57e-01 +7.42e-01 +7.66e-01 +8.04e-01 +7.92e-01 +1.16e+00 +1.25e+00 +5.04e-01 +8.34e-01 +7.68e-01 +7.75e-01 +7.60e-01 +7.73e-01 +8.04e-01 +7.87e-01 +7.68e-01 +7.69e-01 +7.72e-01 +7.79e-01 +8.20e-01 +1.12e+00 +7.80e-01 +3.07e-01 +8.35e-01 +8.24e-01 +8.03e-01 +7.87e-01 +7.88e-01 +8.12e-01 +7.74e-01 +7.87e-01 +7.48e-01 +7.70e-01 +7.77e-01 +8.41e-01 +1.16e+00 +6.39e-01 +2.34e-01 +8.17e-01 +7.87e-01 +7.98e-01 +8.15e-01 +8.06e-01 +7.88e-01 +7.52e-01 +7.32e-01 +7.17e-01 +7.61e-01 +7.74e-01 +8.04e-01 +1.13e+00 +6.75e-01 +2.06e-01 +8.22e-01 +8.10e-01 +8.02e-01 +8.08e-01 +8.05e-01 +7.56e-01 +7.48e-01 +7.03e-01 +7.31e-01 +7.43e-01 +7.54e-01 +8.22e-01 +1.16e+00 +7.52e-01 +2.28e-01 +7.64e-01 +7.75e-01 +7.68e-01 +7.87e-01 +7.73e-01 +7.45e-01 +7.48e-01 +7.22e-01 +7.13e-01 +7.34e-01 +7.57e-01 +7.86e-01 +1.10e+00 +8.15e-01 +2.44e-01 +8.21e-01 +3.30e-01 +7.87e-01 +7.66e-01 +7.39e-01 +7.65e-01 +3.79e-01 +6.99e-01 +6.88e-01 +6.98e-01 +7.52e-01 +7.85e-01 +1.05e+00 +7.70e-01 +2.57e-01 +7.79e-01 +7.89e-01 +8.06e-01 +7.85e-01 +7.59e-01 +7.34e-01 +7.57e-01 +7.39e-01 +7.18e-01 +7.39e-01 +7.21e-01 +7.16e-01 +1.03e+00 +6.02e-01 +2.37e-01 +8.09e-01 +8.54e-01 +8.12e-01 +7.85e-01 +7.60e-01 +7.56e-01 +7.18e-01 +7.13e-01 +7.31e-01 +7.31e-01 +6.81e-01 +7.25e-01 +9.31e-01 +3.54e-01 +1.26e-01 +8.50e-01 +8.54e-01 +8.14e-01 +7.80e-01 +8.02e-01 +8.05e-01 +7.81e-01 +7.77e-01 +7.97e-01 +7.65e-01 +7.40e-01 +7.05e-01 +7.73e-01 +3.06e-01 +1.34e-01 +1.20e+00 +1.12e+00 +1.11e+00 +1.13e+00 +1.15e+00 +1.10e+00 +1.10e+00 +9.95e-01 +1.08e+00 +9.88e-01 +1.01e+00 +9.01e-01 +6.74e-01 +1.87e-01 +8.09e-02 +1.32e+00 +1.34e+00 +1.16e+00 +7.75e-01 +7.51e-01 +9.97e-01 +8.93e-01 +7.22e-01 +6.51e-01 +6.25e-01 +7.18e-01 +5.73e-01 +2.11e-01 +9.82e-02 +4.26e-02 +4.75e-01 +5.18e-01 +4.49e-01 +2.40e-01 +2.44e-01 +3.22e-01 +3.19e-01 +2.35e-01 +2.20e-01 +1.64e-01 +2.36e-01 +2.02e-01 +7.60e-02 +4.33e-02 +3.07e-02 +7.81e-01 +7.87e-01 +7.72e-01 +8.00e-01 +8.25e-01 +8.31e-01 +8.27e-01 +7.89e-01 +7.35e-01 +7.02e-01 +6.79e-01 +7.96e-01 +1.11e+00 +7.59e-01 +2.66e-01 +8.05e-01 +8.08e-01 +8.45e-01 +8.16e-01 +7.90e-01 +7.90e-01 +7.97e-01 +7.35e-01 +7.24e-01 +6.97e-01 +6.50e-01 +7.41e-01 +1.08e+00 +5.46e-01 +1.73e-01 +7.91e-01 +7.84e-01 +8.12e-01 +8.14e-01 +7.93e-01 +7.86e-01 +7.80e-01 +7.88e-01 +7.62e-01 +7.26e-01 +7.48e-01 +8.00e-01 +1.08e+00 +7.80e-01 +2.83e-01 +7.99e-01 +7.86e-01 +8.00e-01 +7.83e-01 +8.01e-01 +7.89e-01 +7.68e-01 +7.67e-01 +7.34e-01 +7.08e-01 +7.83e-01 +8.28e-01 +1.05e+00 +7.28e-01 +3.01e-01 +8.10e-01 +8.05e-01 +8.41e-01 +7.70e-01 +7.70e-01 +7.93e-01 +7.51e-01 +7.44e-01 +7.48e-01 +7.29e-01 +7.92e-01 +7.97e-01 +1.08e+00 +5.90e-01 +1.62e-01 +8.06e-01 +7.76e-01 +8.01e-01 +7.96e-01 +7.50e-01 +7.27e-01 +7.51e-01 +7.26e-01 +7.36e-01 +7.31e-01 +7.41e-01 +7.92e-01 +1.07e+00 +6.18e-01 +2.17e-01 +7.91e-01 +7.77e-01 +7.90e-01 +7.54e-01 +7.85e-01 +7.70e-01 +7.48e-01 +6.96e-01 +6.99e-01 +7.02e-01 +6.96e-01 +7.43e-01 +1.02e+00 +7.52e-01 +2.89e-01 +7.56e-01 +7.94e-01 +7.67e-01 +7.64e-01 +7.61e-01 +7.60e-01 +7.28e-01 +7.14e-01 +7.09e-01 +7.03e-01 +7.62e-01 +7.59e-01 +1.03e+00 +6.86e-01 +2.56e-01 +7.75e-01 +7.59e-01 +7.81e-01 +7.45e-01 +7.66e-01 +7.46e-01 +7.25e-01 +6.93e-01 +6.75e-01 +6.66e-01 +7.38e-01 +7.32e-01 +9.88e-01 +5.41e-01 +1.68e-01 +7.63e-01 +7.64e-01 +7.46e-01 +7.17e-01 +7.23e-01 +7.40e-01 +6.88e-01 +7.11e-01 +6.45e-01 +6.43e-01 +6.92e-01 +7.27e-01 +1.06e+00 +5.99e-01 +1.94e-01 +7.99e-01 +7.77e-01 +7.94e-01 +7.47e-01 +7.40e-01 +7.39e-01 +7.34e-01 +7.52e-01 +7.06e-01 +6.86e-01 +6.55e-01 +6.81e-01 +8.86e-01 +3.82e-01 +9.69e-02 +8.46e-01 +8.18e-01 +8.26e-01 +7.84e-01 +7.60e-01 +7.38e-01 +7.20e-01 +7.68e-01 +7.41e-01 +7.47e-01 +7.02e-01 +6.54e-01 +7.66e-01 +2.43e-01 +7.12e-02 +1.21e+00 +1.12e+00 +1.12e+00 +1.01e+00 +1.09e+00 +1.07e+00 +8.74e-01 +8.72e-01 +8.67e-01 +1.01e+00 +1.01e+00 +8.47e-01 +6.09e-01 +1.58e-01 +5.39e-02 +1.08e+00 +1.12e+00 +8.01e-01 +6.68e-01 +7.20e-01 +8.39e-01 +5.11e-01 +4.35e-01 +3.60e-01 +7.14e-01 +6.43e-01 +4.11e-01 +2.15e-01 +5.39e-02 +2.14e-02 +3.73e-01 +3.83e-01 +2.54e-01 +2.38e-01 +2.15e-01 +2.86e-01 +1.89e-01 +1.46e-01 +1.14e-01 +2.39e-01 +2.00e-01 +1.38e-01 +9.65e-02 +4.13e-02 +1.06e-02 +8.16e-01 +8.31e-01 +8.55e-01 +8.17e-01 +8.37e-01 +7.96e-01 +7.81e-01 +8.05e-01 +7.46e-01 +7.27e-01 +6.91e-01 +7.68e-01 +1.08e+00 +5.87e-01 +1.78e-01 +7.93e-01 +8.07e-01 +8.40e-01 +8.16e-01 +8.12e-01 +8.09e-01 +7.98e-01 +7.79e-01 +7.43e-01 +7.35e-01 +7.18e-01 +7.62e-01 +1.05e+00 +6.30e-01 +1.97e-01 +7.86e-01 +8.04e-01 +7.90e-01 +7.96e-01 +8.02e-01 +7.57e-01 +7.80e-01 +7.99e-01 +7.43e-01 +7.26e-01 +7.20e-01 +8.04e-01 +1.21e+00 +8.14e-01 +2.65e-01 +8.01e-01 +7.70e-01 +8.02e-01 +7.66e-01 +7.78e-01 +7.86e-01 +7.74e-01 +7.63e-01 +7.30e-01 +7.36e-01 +7.45e-01 +7.37e-01 +1.04e+00 +8.80e-01 +3.72e-01 +7.76e-01 +7.61e-01 +7.65e-01 +7.59e-01 +7.59e-01 +7.54e-01 +7.53e-01 +7.66e-01 +7.41e-01 +7.01e-01 +7.50e-01 +8.07e-01 +1.03e+00 +6.47e-01 +2.01e-01 +8.00e-01 +7.91e-01 +7.50e-01 +7.33e-01 +7.09e-01 +7.28e-01 +7.48e-01 +7.31e-01 +7.26e-01 +7.09e-01 +7.38e-01 +8.00e-01 +1.08e+00 +7.50e-01 +2.61e-01 +7.64e-01 +7.45e-01 +7.60e-01 +7.26e-01 +7.24e-01 +7.17e-01 +6.99e-01 +6.92e-01 +6.87e-01 +6.82e-01 +7.32e-01 +7.47e-01 +9.42e-01 +5.70e-01 +2.11e-01 +7.38e-01 +7.50e-01 +7.58e-01 +7.14e-01 +7.38e-01 +7.21e-01 +6.81e-01 +6.69e-01 +6.84e-01 +6.84e-01 +7.32e-01 +7.33e-01 +9.18e-01 +3.59e-01 +1.45e-01 +7.38e-01 +5.93e-01 +7.38e-01 +7.39e-01 +7.46e-01 +7.70e-01 +7.25e-01 +6.07e-01 +6.46e-01 +6.49e-01 +6.56e-01 +6.97e-01 +9.65e-01 +3.98e-01 +1.02e-01 +7.63e-01 +7.31e-01 +7.32e-01 +7.05e-01 +7.34e-01 +7.26e-01 +7.09e-01 +6.85e-01 +6.43e-01 +6.34e-01 +6.51e-01 +6.67e-01 +9.76e-01 +4.41e-01 +1.09e-01 +7.80e-01 +7.12e-01 +7.26e-01 +7.17e-01 +7.58e-01 +7.39e-01 +7.19e-01 +7.02e-01 +7.03e-01 +6.55e-01 +5.91e-01 +6.53e-01 +9.17e-01 +4.78e-01 +1.37e-01 +8.17e-01 +7.34e-01 +7.39e-01 +7.85e-01 +7.75e-01 +7.52e-01 +7.79e-01 +7.11e-01 +7.19e-01 +6.96e-01 +6.43e-01 +6.00e-01 +7.61e-01 +2.70e-01 +8.10e-02 +1.16e+00 +1.04e+00 +1.10e+00 +1.11e+00 +1.11e+00 +1.08e+00 +9.96e-01 +1.02e+00 +9.59e-01 +1.01e+00 +9.84e-01 +7.98e-01 +6.84e-01 +2.00e-01 +5.36e-02 +1.12e+00 +8.97e-01 +8.53e-01 +8.36e-01 +8.45e-01 +7.72e-01 +5.11e-01 +5.45e-01 +4.94e-01 +5.38e-01 +5.92e-01 +4.14e-01 +2.08e-01 +7.44e-02 +2.60e-02 +3.41e-01 +2.55e-01 +2.14e-01 +2.42e-01 +2.46e-01 +2.60e-01 +1.78e-01 +1.61e-01 +1.44e-01 +1.52e-01 +1.91e-01 +1.39e-01 +6.05e-02 +2.90e-02 +6.48e-03 +7.87e-01 +8.11e-01 +8.37e-01 +8.09e-01 +8.06e-01 +7.52e-01 +7.59e-01 +7.86e-01 +7.65e-01 +7.53e-01 +7.16e-01 +8.07e-01 +1.02e+00 +7.17e-01 +2.61e-01 +7.93e-01 +8.13e-01 +7.88e-01 +7.89e-01 +7.88e-01 +8.08e-01 +8.00e-01 +7.90e-01 +7.89e-01 +7.16e-01 +7.47e-01 +7.98e-01 +1.11e+00 +7.82e-01 +2.29e-01 +8.16e-01 +7.85e-01 +7.57e-01 +7.70e-01 +7.67e-01 +7.99e-01 +7.75e-01 +7.99e-01 +8.00e-01 +7.27e-01 +7.00e-01 +7.95e-01 +1.12e+00 +8.32e-01 +2.72e-01 +7.66e-01 +7.71e-01 +7.72e-01 +7.50e-01 +7.83e-01 +7.66e-01 +7.97e-01 +7.79e-01 +7.41e-01 +7.26e-01 +7.24e-01 +7.90e-01 +1.04e+00 +7.19e-01 +2.72e-01 +7.50e-01 +7.28e-01 +7.78e-01 +7.78e-01 +7.70e-01 +7.41e-01 +7.52e-01 +7.68e-01 +7.29e-01 +7.30e-01 +7.52e-01 +7.62e-01 +1.04e+00 +5.78e-01 +1.84e-01 +7.79e-01 +7.50e-01 +7.82e-01 +7.38e-01 +7.48e-01 +7.53e-01 +7.41e-01 +7.47e-01 +7.17e-01 +7.06e-01 +7.29e-01 +7.63e-01 +1.01e+00 +5.33e-01 +1.96e-01 +7.73e-01 +7.74e-01 +7.65e-01 +7.47e-01 +7.35e-01 +7.39e-01 +7.07e-01 +7.21e-01 +7.07e-01 +7.06e-01 +6.86e-01 +7.53e-01 +1.03e+00 +6.87e-01 +2.34e-01 +7.58e-01 +7.65e-01 +7.72e-01 +7.64e-01 +7.41e-01 +7.16e-01 +6.58e-01 +7.03e-01 +6.75e-01 +6.34e-01 +6.76e-01 +7.61e-01 +9.52e-01 +3.63e-01 +1.11e-01 +7.33e-01 +7.35e-01 +7.46e-01 +7.50e-01 +7.43e-01 +7.09e-01 +6.97e-01 +6.61e-01 +6.39e-01 +6.31e-01 +6.70e-01 +7.26e-01 +9.35e-01 +4.08e-01 +1.21e-01 +7.44e-01 +7.40e-01 +7.34e-01 +7.49e-01 +7.23e-01 +7.13e-01 +6.92e-01 +6.51e-01 +6.43e-01 +6.54e-01 +6.11e-01 +6.81e-01 +8.92e-01 +5.18e-01 +1.67e-01 +7.39e-01 +7.24e-01 +7.18e-01 +7.10e-01 +7.07e-01 +7.17e-01 +7.28e-01 +6.89e-01 +6.45e-01 +6.27e-01 +5.89e-01 +6.27e-01 +8.94e-01 +4.27e-01 +1.44e-01 +8.20e-01 +7.35e-01 +7.73e-01 +7.79e-01 +7.33e-01 +7.94e-01 +7.38e-01 +7.21e-01 +6.82e-01 +6.66e-01 +6.30e-01 +5.52e-01 +6.56e-01 +2.86e-01 +9.02e-02 +1.23e+00 +1.02e+00 +9.49e-01 +1.09e+00 +1.08e+00 +1.07e+00 +1.01e+00 +1.12e+00 +1.07e+00 +1.12e+00 +9.32e-01 +7.55e-01 +5.90e-01 +1.70e-01 +6.04e-02 +9.35e-01 +7.67e-01 +7.48e-01 +8.36e-01 +9.13e-01 +8.43e-01 +6.94e-01 +7.11e-01 +6.45e-01 +5.23e-01 +4.64e-01 +3.36e-01 +1.97e-01 +8.24e-02 +3.52e-02 +3.61e-01 +3.04e-01 +3.20e-01 +2.77e-01 +3.14e-01 +2.79e-01 +2.19e-01 +2.01e-01 +2.23e-01 +1.68e-01 +1.56e-01 +1.06e-01 +6.15e-02 +3.90e-02 +1.78e-02 +7.55e-01 +7.78e-01 +8.17e-01 +8.31e-01 +8.37e-01 +8.23e-01 +8.31e-01 +8.49e-01 +7.67e-01 +7.79e-01 +7.53e-01 +7.56e-01 +9.13e-01 +4.87e-01 +1.90e-01 +7.63e-01 +7.96e-01 +7.72e-01 +7.84e-01 +8.07e-01 +7.97e-01 +7.85e-01 +7.92e-01 +7.90e-01 +7.89e-01 +7.62e-01 +7.60e-01 +9.99e-01 +6.50e-01 +2.08e-01 +7.70e-01 +7.78e-01 +7.67e-01 +8.10e-01 +7.76e-01 +7.97e-01 +8.29e-01 +7.63e-01 +7.81e-01 +7.78e-01 +7.59e-01 +7.86e-01 +1.06e+00 +7.09e-01 +2.39e-01 +7.64e-01 +7.94e-01 +8.36e-01 +7.73e-01 +8.21e-01 +8.29e-01 +8.06e-01 +7.74e-01 +7.69e-01 +7.48e-01 +7.27e-01 +7.70e-01 +1.04e+00 +5.38e-01 +1.72e-01 +7.68e-01 +7.86e-01 +7.98e-01 +7.72e-01 +8.10e-01 +7.95e-01 +7.80e-01 +7.69e-01 +7.86e-01 +7.59e-01 +7.61e-01 +7.80e-01 +1.02e+00 +4.87e-01 +1.43e-01 +7.60e-01 +7.84e-01 +7.88e-01 +8.10e-01 +7.80e-01 +7.87e-01 +7.60e-01 +7.40e-01 +7.18e-01 +7.35e-01 +7.48e-01 +7.57e-01 +1.06e+00 +5.45e-01 +1.46e-01 +7.56e-01 +7.71e-01 +7.41e-01 +7.88e-01 +7.68e-01 +7.62e-01 +7.57e-01 +7.22e-01 +7.18e-01 +7.13e-01 +7.27e-01 +7.54e-01 +9.95e-01 +6.13e-01 +1.70e-01 +7.39e-01 +7.50e-01 +7.74e-01 +7.93e-01 +7.34e-01 +7.32e-01 +6.83e-01 +7.01e-01 +7.06e-01 +7.36e-01 +7.12e-01 +7.25e-01 +9.34e-01 +5.51e-01 +1.95e-01 +7.17e-01 +7.39e-01 +7.59e-01 +7.62e-01 +7.78e-01 +7.43e-01 +6.89e-01 +6.89e-01 +6.69e-01 +6.52e-01 +6.30e-01 +6.68e-01 +9.23e-01 +4.29e-01 +1.22e-01 +7.39e-01 +7.23e-01 +7.35e-01 +7.50e-01 +7.65e-01 +7.07e-01 +6.87e-01 +6.74e-01 +6.34e-01 +6.34e-01 +6.14e-01 +6.22e-01 +7.74e-01 +4.07e-01 +1.43e-01 +7.20e-01 +7.36e-01 +7.25e-01 +7.20e-01 +7.27e-01 +7.02e-01 +7.08e-01 +6.86e-01 +6.37e-01 +6.42e-01 +5.84e-01 +6.05e-01 +8.13e-01 +2.50e-01 +7.65e-02 +7.35e-01 +7.38e-01 +7.34e-01 +7.63e-01 +7.24e-01 +7.52e-01 +7.60e-01 +7.36e-01 +6.72e-01 +6.44e-01 +6.12e-01 +5.65e-01 +6.76e-01 +2.27e-01 +7.04e-02 +1.03e+00 +9.44e-01 +9.23e-01 +1.03e+00 +1.07e+00 +1.05e+00 +9.55e-01 +9.95e-01 +9.86e-01 +8.94e-01 +8.68e-01 +7.36e-01 +3.85e-01 +9.70e-02 +3.48e-02 +8.54e-01 +4.54e-01 +5.88e-01 +6.14e-01 +6.53e-01 +6.43e-01 +5.10e-01 +6.95e-01 +5.75e-01 +3.80e-01 +4.09e-01 +2.64e-01 +1.34e-01 +5.20e-02 +1.59e-02 +2.77e-01 +1.44e-01 +1.76e-01 +1.86e-01 +2.54e-01 +2.23e-01 +1.61e-01 +2.31e-01 +1.97e-01 +1.30e-01 +1.16e-01 +6.26e-02 +4.81e-02 +3.06e-02 +1.67e-02 +7.86e-01 +8.39e-01 +8.83e-01 +8.55e-01 +8.50e-01 +8.79e-01 +8.60e-01 +8.12e-01 +8.09e-01 +7.89e-01 +7.90e-01 +7.42e-01 +7.44e-01 +1.58e-01 +6.48e-02 +8.67e-01 +8.89e-01 +8.72e-01 +8.03e-01 +7.98e-01 +8.19e-01 +8.35e-01 +8.10e-01 +7.91e-01 +7.79e-01 +7.97e-01 +7.65e-01 +8.12e-01 +2.69e-01 +1.15e-01 +8.28e-01 +8.91e-01 +8.28e-01 +8.22e-01 +8.14e-01 +8.56e-01 +8.86e-01 +8.46e-01 +7.76e-01 +7.84e-01 +7.49e-01 +8.31e-01 +1.07e+00 +4.89e-01 +1.30e-01 +8.26e-01 +8.49e-01 +8.39e-01 +8.27e-01 +8.44e-01 +8.70e-01 +3.16e-01 +8.04e-01 +8.19e-01 +7.85e-01 +7.20e-01 +7.52e-01 +9.62e-01 +4.26e-01 +1.46e-01 +8.01e-01 +7.89e-01 +8.03e-01 +8.28e-01 +8.39e-01 +8.52e-01 +8.17e-01 +8.07e-01 +8.12e-01 +8.02e-01 +7.19e-01 +7.09e-01 +9.85e-01 +5.13e-01 +1.40e-01 +7.74e-01 +8.11e-01 +7.91e-01 +8.10e-01 +8.13e-01 +8.20e-01 +7.99e-01 +7.62e-01 +7.73e-01 +7.84e-01 +7.21e-01 +7.04e-01 +9.41e-01 +4.52e-01 +1.46e-01 +7.47e-01 +7.56e-01 +8.11e-01 +8.22e-01 +8.04e-01 +7.79e-01 +7.83e-01 +7.46e-01 +7.28e-01 +7.40e-01 +7.25e-01 +7.07e-01 +8.17e-01 +4.63e-01 +1.62e-01 +7.58e-01 +7.56e-01 +7.60e-01 +7.96e-01 +7.87e-01 +7.69e-01 +7.75e-01 +7.58e-01 +7.39e-01 +7.10e-01 +7.14e-01 +7.27e-01 +9.10e-01 +3.49e-01 +1.16e-01 +7.76e-01 +7.69e-01 +7.90e-01 +7.93e-01 +7.91e-01 +8.11e-01 +7.48e-01 +7.33e-01 +7.10e-01 +7.10e-01 +6.65e-01 +6.83e-01 +8.72e-01 +3.27e-01 +1.09e-01 +7.58e-01 +7.27e-01 +8.00e-01 +7.89e-01 +7.90e-01 +7.69e-01 +7.48e-01 +6.87e-01 +6.49e-01 +6.32e-01 +6.50e-01 +6.16e-01 +8.53e-01 +3.55e-01 +1.26e-01 +7.12e-01 +7.03e-01 +7.35e-01 +7.34e-01 +7.90e-01 +7.42e-01 +6.78e-01 +6.75e-01 +6.64e-01 +6.43e-01 +5.93e-01 +5.68e-01 +7.24e-01 +2.12e-01 +6.37e-02 +7.12e-01 +3.10e-01 +7.21e-01 +7.24e-01 +7.11e-01 +6.84e-01 +7.09e-01 +7.37e-01 +6.65e-01 +6.48e-01 +5.74e-01 +5.37e-01 +6.18e-01 +1.92e-01 +5.93e-02 +9.23e-01 +8.49e-01 +8.63e-01 +9.07e-01 +8.87e-01 +8.82e-01 +8.74e-01 +9.65e-01 +9.71e-01 +7.90e-01 +7.23e-01 +6.21e-01 +3.58e-01 +1.02e-01 +3.58e-02 +6.34e-01 +3.24e-01 +4.14e-01 +5.18e-01 +3.93e-01 +3.93e-01 +3.52e-01 +3.37e-01 +5.09e-01 +3.80e-01 +2.69e-01 +2.08e-01 +1.27e-01 +3.00e-02 +9.84e-03 +2.53e-01 +1.24e-01 +1.72e-01 +1.82e-01 +1.42e-01 +1.35e-01 +1.17e-01 +1.22e-01 +1.56e-01 +1.28e-01 +7.82e-02 +7.58e-02 +4.95e-02 +1.63e-02 +7.34e-03 +1.15e+00 +1.21e+00 +1.27e+00 +1.27e+00 +1.26e+00 +1.19e+00 +1.15e+00 +1.14e+00 +1.15e+00 +1.04e+00 +1.01e+00 +1.07e+00 +8.04e-01 +1.47e-01 +3.71e-02 +1.23e+00 +1.11e+00 +1.30e+00 +1.15e+00 +1.15e+00 +1.20e+00 +1.17e+00 +1.09e+00 +1.02e+00 +1.06e+00 +9.84e-01 +9.61e-01 +9.02e-01 +2.44e-01 +7.45e-02 +1.19e+00 +1.09e+00 +1.26e+00 +1.15e+00 +1.13e+00 +1.22e+00 +1.07e+00 +1.19e+00 +1.10e+00 +1.09e+00 +1.01e+00 +9.62e-01 +1.10e+00 +4.73e-01 +1.47e-01 +1.18e+00 +1.14e+00 +1.17e+00 +1.20e+00 +1.17e+00 +1.28e+00 +1.19e+00 +1.18e+00 +1.12e+00 +1.04e+00 +9.10e-01 +8.02e-01 +9.06e-01 +2.84e-01 +8.05e-02 +1.14e+00 +1.09e+00 +1.13e+00 +1.18e+00 +1.11e+00 +1.18e+00 +1.35e+00 +1.16e+00 +1.15e+00 +1.16e+00 +1.00e+00 +7.35e-01 +7.88e-01 +3.90e-01 +1.47e-01 +1.14e+00 +1.03e+00 +1.10e+00 +1.12e+00 +1.06e+00 +1.15e+00 +1.16e+00 +1.07e+00 +1.05e+00 +1.12e+00 +9.87e-01 +8.55e-01 +8.93e-01 +3.76e-01 +1.15e-01 +1.02e+00 +1.04e+00 +1.14e+00 +1.09e+00 +1.05e+00 +1.11e+00 +1.08e+00 +1.04e+00 +8.85e-01 +1.00e+00 +9.45e-01 +7.84e-01 +8.14e-01 +2.74e-01 +8.75e-02 +1.04e+00 +1.06e+00 +1.04e+00 +1.12e+00 +1.18e+00 +1.13e+00 +1.06e+00 +1.05e+00 +9.34e-01 +9.47e-01 +9.38e-01 +8.93e-01 +6.67e-01 +1.52e-01 +5.28e-02 +1.04e+00 +1.07e+00 +1.06e+00 +1.13e+00 +1.13e+00 +1.03e+00 +1.15e+00 +1.11e+00 +9.64e-01 +1.01e+00 +9.05e-01 +8.67e-01 +8.37e-01 +2.04e-01 +6.93e-02 +1.00e+00 +1.10e+00 +1.14e+00 +1.07e+00 +1.14e+00 +1.03e+00 +1.08e+00 +9.61e-01 +8.93e-01 +8.70e-01 +8.58e-01 +8.56e-01 +8.00e-01 +2.31e-01 +7.13e-02 +9.12e-01 +9.60e-01 +1.08e+00 +1.07e+00 +1.06e+00 +1.13e+00 +9.68e-01 +8.42e-01 +8.21e-01 +7.51e-01 +7.76e-01 +7.41e-01 +5.22e-01 +2.01e-01 +7.22e-02 +7.80e-01 +8.00e-01 +9.64e-01 +9.33e-01 +9.70e-01 +1.00e+00 +1.03e+00 +9.27e-01 +8.81e-01 +7.47e-01 +6.96e-01 +6.90e-01 +5.22e-01 +1.42e-01 +5.84e-02 +6.66e-01 +7.30e-01 +8.24e-01 +7.89e-01 +8.68e-01 +8.50e-01 +6.87e-01 +9.72e-01 +9.67e-01 +8.19e-01 +6.02e-01 +5.08e-01 +3.32e-01 +9.24e-02 +4.15e-02 +3.42e-01 +2.04e-01 +2.56e-01 +3.06e-01 +2.44e-01 +2.32e-01 +1.97e-01 +2.37e-01 +4.25e-01 +2.80e-01 +1.20e-01 +1.22e-01 +6.52e-02 +2.34e-02 +1.33e-02 +1.30e-01 +6.51e-02 +9.03e-02 +9.81e-02 +7.43e-02 +7.44e-02 +7.50e-02 +7.08e-02 +1.36e-01 +1.08e-01 +5.23e-02 +3.86e-02 +2.05e-02 +1.18e-02 +4.82e-03 +1.10e+00 +1.08e+00 +1.36e+00 +9.59e-01 +1.50e+00 +1.22e+00 +1.06e+00 +1.09e+00 +9.39e-01 +8.29e-01 +5.63e-01 +6.05e-01 +5.41e-01 +1.46e-01 +4.18e-02 +1.38e+00 +9.47e-01 +1.64e+00 +1.82e+00 +1.45e+00 +1.17e+00 +9.32e-01 +9.52e-01 +7.63e-01 +6.75e-01 +6.06e-01 +5.05e-01 +4.40e-01 +1.95e-01 +6.83e-02 +1.18e+00 +1.14e+00 +1.46e+00 +1.11e+00 +8.51e-01 +1.15e+00 +9.29e-01 +9.75e-01 +8.23e-01 +7.12e-01 +8.66e-01 +6.90e-01 +3.69e-01 +2.08e-01 +1.46e-01 +1.36e+00 +1.17e+00 +1.39e+00 +1.53e+00 +1.15e+00 +1.26e+00 +1.34e+00 +1.20e+00 +1.06e+00 +8.13e-01 +6.01e-01 +4.19e-01 +2.92e-01 +1.43e-01 +6.62e-02 +1.31e+00 +9.58e-01 +1.27e+00 +1.56e+00 +1.32e+00 +1.38e+00 +1.36e+00 +1.10e+00 +9.06e-01 +8.42e-01 +7.44e-01 +3.53e-01 +2.51e-01 +1.27e-01 +6.42e-02 +1.05e+00 +8.06e-01 +1.10e+00 +1.14e+00 +7.77e-01 +8.88e-01 +1.08e+00 +7.49e-01 +6.61e-01 +8.78e-01 +7.85e-01 +5.07e-01 +2.81e-01 +1.16e-01 +7.65e-02 +7.49e-01 +9.07e-01 +1.01e+00 +1.02e+00 +8.96e-01 +9.71e-01 +9.06e-01 +7.59e-01 +6.31e-01 +5.64e-01 +4.58e-01 +3.36e-01 +1.97e-01 +1.17e-01 +7.24e-02 +5.31e-01 +7.17e-01 +8.03e-01 +7.87e-01 +9.01e-01 +9.00e-01 +6.18e-01 +6.08e-01 +4.09e-01 +3.65e-01 +4.67e-01 +3.69e-01 +1.95e-01 +1.03e-01 +4.09e-02 +5.66e-01 +6.84e-01 +8.03e-01 +1.10e+00 +8.56e-01 +7.53e-01 +5.80e-01 +7.25e-01 +5.65e-01 +5.23e-01 +4.71e-01 +3.57e-01 +2.67e-01 +9.56e-02 +3.66e-02 +4.28e-01 +7.89e-01 +1.04e+00 +7.98e-01 +6.86e-01 +4.17e-01 +5.80e-01 +6.93e-01 +5.76e-01 +4.61e-01 +4.35e-01 +3.12e-01 +2.15e-01 +1.24e-01 +3.78e-02 +4.61e-01 +5.64e-01 +7.98e-01 +5.21e-01 +7.00e-01 +6.75e-01 +5.39e-01 +5.12e-01 +4.28e-01 +2.95e-01 +3.73e-01 +3.14e-01 +1.91e-01 +1.02e-01 +4.60e-02 +3.92e-01 +3.42e-01 +5.06e-01 +4.71e-01 +4.73e-01 +6.89e-01 +5.67e-01 +3.20e-01 +3.01e-01 +2.61e-01 +2.32e-01 +2.34e-01 +1.79e-01 +4.62e-02 +1.73e-02 +1.91e-01 +2.50e-01 +3.10e-01 +2.40e-01 +2.89e-01 +4.18e-01 +3.43e-01 +3.33e-01 +2.60e-01 +2.88e-01 +2.00e-01 +1.73e-01 +1.35e-01 +4.58e-02 +1.87e-02 +8.30e-02 +9.70e-02 +1.13e-01 +8.94e-02 +1.22e-01 +1.54e-01 +1.00e-01 +1.45e-01 +1.68e-01 +1.41e-01 +7.91e-02 +5.48e-02 +3.66e-02 +1.92e-02 +9.23e-03 +6.77e-02 +3.66e-02 +4.21e-02 +4.05e-02 +4.50e-02 +5.61e-02 +2.78e-02 +4.65e-02 +7.38e-02 +6.93e-02 +3.15e-02 +1.71e-02 +1.50e-02 +7.02e-03 +2.63e-03 +3.78e-01 +3.76e-01 +5.09e-01 +3.31e-01 +5.46e-01 +4.00e-01 +3.73e-01 +3.85e-01 +3.61e-01 +2.87e-01 +1.88e-01 +1.85e-01 +1.80e-01 +1.07e-01 +3.77e-02 +5.65e-01 +2.57e-01 +5.55e-01 +7.39e-01 +6.13e-01 +4.33e-01 +3.29e-01 +2.97e-01 +2.89e-01 +2.50e-01 +1.89e-01 +1.46e-01 +1.32e-01 +8.81e-02 +4.17e-02 +4.36e-01 +3.41e-01 +5.13e-01 +4.96e-01 +2.34e-01 +4.00e-01 +3.27e-01 +2.87e-01 +2.94e-01 +2.08e-01 +3.02e-01 +2.63e-01 +1.41e-01 +5.88e-02 +6.26e-02 +4.47e-01 +3.47e-01 +5.27e-01 +6.11e-01 +3.91e-01 +4.22e-01 +4.77e-01 +4.54e-01 +4.31e-01 +3.19e-01 +2.13e-01 +1.47e-01 +9.76e-02 +5.82e-02 +2.92e-02 +5.35e-01 +3.74e-01 +4.80e-01 +5.78e-01 +5.02e-01 +4.78e-01 +5.06e-01 +3.61e-01 +3.58e-01 +2.46e-01 +2.55e-01 +1.07e-01 +6.95e-02 +4.27e-02 +2.71e-02 +3.15e-01 +2.73e-01 +3.47e-01 +3.78e-01 +2.88e-01 +2.50e-01 +3.39e-01 +2.40e-01 +2.15e-01 +2.65e-01 +2.87e-01 +2.16e-01 +1.00e-01 +4.10e-02 +2.09e-02 +3.10e-01 +2.99e-01 +3.67e-01 +3.96e-01 +2.30e-01 +2.86e-01 +3.00e-01 +2.63e-01 +2.50e-01 +1.99e-01 +1.56e-01 +1.31e-01 +7.20e-02 +3.22e-02 +3.48e-02 +1.47e-01 +2.69e-01 +2.47e-01 +2.40e-01 +2.82e-01 +3.03e-01 +1.85e-01 +1.94e-01 +1.29e-01 +1.02e-01 +1.25e-01 +1.15e-01 +5.95e-02 +5.42e-02 +4.17e-02 +1.81e-01 +2.00e-01 +2.60e-01 +3.31e-01 +3.21e-01 +2.95e-01 +1.85e-01 +2.44e-01 +2.00e-01 +1.66e-01 +1.43e-01 +1.23e-01 +1.02e-01 +4.36e-02 +1.72e-02 +1.08e-01 +2.40e-01 +3.14e-01 +3.12e-01 +2.21e-01 +1.20e-01 +1.86e-01 +2.21e-01 +2.13e-01 +1.40e-01 +1.31e-01 +1.05e-01 +5.58e-02 +6.38e-02 +3.45e-02 +1.48e-01 +2.44e-01 +2.92e-01 +1.42e-01 +2.29e-01 +1.89e-01 +1.21e-01 +1.73e-01 +1.54e-01 +1.09e-01 +1.16e-01 +8.49e-02 +6.30e-02 +4.98e-02 +4.02e-02 +1.01e-01 +1.10e-01 +1.55e-01 +1.40e-01 +1.46e-01 +2.37e-01 +1.93e-01 +1.34e-01 +9.47e-02 +8.48e-02 +7.21e-02 +7.09e-02 +7.53e-02 +2.10e-02 +7.74e-03 +8.60e-02 +8.49e-02 +1.09e-01 +1.25e-01 +9.19e-02 +1.59e-01 +1.27e-01 +1.01e-01 +6.87e-02 +9.44e-02 +5.12e-02 +5.73e-02 +5.81e-02 +1.92e-02 +6.10e-03 +2.72e-02 +4.77e-02 +4.74e-02 +3.34e-02 +5.41e-02 +7.93e-02 +6.70e-02 +7.67e-02 +9.31e-02 +5.17e-02 +5.34e-02 +3.25e-02 +1.99e-02 +1.19e-02 +1.14e-02 +2.23e-02 +2.53e-02 +2.61e-02 +2.28e-02 +2.86e-02 +5.00e-02 +3.04e-02 +2.98e-02 +5.98e-02 +4.02e-02 +2.97e-02 +1.80e-02 +6.73e-03 +4.41e-03 +7.35e-03 \ No newline at end of file diff --git a/tests/regression_tests/weightwindows_fw_cadis_mesh/test.py b/tests/regression_tests/weightwindows_fw_cadis_mesh/test.py new file mode 100644 index 000000000..680e9dc6d --- /dev/null +++ b/tests/regression_tests/weightwindows_fw_cadis_mesh/test.py @@ -0,0 +1,49 @@ +import os + +import openmc +from openmc.utility_funcs import change_directory +from openmc.examples import random_ray_three_region_cube +import pytest + +from tests.testing_harness import WeightWindowPyAPITestHarness + + +class MGXSTestHarness(WeightWindowPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + + +@pytest.mark.parametrize("shape", ["flat", "linear"]) +def test_weight_windows_fw_cadis_mesh(shape): + with change_directory(shape): + openmc.reset_auto_ids() + + model = random_ray_three_region_cube() + + # The base model has a resolution of 12, so we overlay + # something else for FW-CADIS + n = 15 + width = 30.0 + ww_mesh = openmc.RegularMesh() + ww_mesh.dimension = (n, n, n) + ww_mesh.lower_left = (0.0, 0.0, 0.0) + ww_mesh.upper_right = (width, width, width) + + wwg = openmc.WeightWindowGenerator( + method="fw_cadis", mesh=ww_mesh, max_realizations=model.settings.batches) + model.settings.weight_window_generators = wwg + + root = model.geometry.root_universe + model.settings.random_ray['source_region_meshes'] = [(ww_mesh, [root])] + + model.settings.particles = 750 + model.settings.batches = 30 + model.settings.inactive = 20 + + model.settings.random_ray['source_shape'] = shape + + harness = MGXSTestHarness('statepoint.30.h5', model) + harness.main() From 906548db20d4ce3c322e7317992c73b51daeb11d Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 7 Mar 2025 14:49:36 -0600 Subject: [PATCH 294/671] Release notes for 0.15.1 (#3340) Co-authored-by: Patrick Shriwise --- CONTRIBUTING.md | 2 +- docs/source/devguide/docker.rst | 9 +- docs/source/devguide/styleguide.rst | 9 +- docs/source/devguide/user-input.rst | 4 +- docs/source/devguide/workflow.rst | 2 +- docs/source/methods/cross_sections.rst | 4 +- docs/source/methods/depletion.rst | 2 +- docs/source/methods/geometry.rst | 2 +- docs/source/methods/neutron_physics.rst | 12 +- docs/source/methods/parallelization.rst | 6 +- docs/source/methods/photon_physics.rst | 2 +- docs/source/methods/tallies.rst | 2 +- docs/source/publications.rst | 10 +- docs/source/pythonapi/deplete.rst | 2 +- docs/source/quickinstall.rst | 49 ++--- docs/source/releasenotes/0.15.1.rst | 224 +++++++++++++++++++++++ docs/source/releasenotes/index.rst | 1 + docs/source/usersguide/basics.rst | 2 +- docs/source/usersguide/beginners.rst | 12 +- docs/source/usersguide/data.rst | 18 +- docs/source/usersguide/decay_sources.rst | 16 +- docs/source/usersguide/install.rst | 54 ++---- docs/source/usersguide/parallel.rst | 2 +- include/openmc/random_dist.h | 4 +- openmc/data/ace.py | 6 +- openmc/data/endf.py | 8 +- openmc/data/fission_energy.py | 2 +- openmc/deplete/integrators.py | 4 +- openmc/examples.py | 5 +- openmc/mgxs/__init__.py | 6 +- openmc/tallies.py | 2 +- src/math_functions.cpp | 2 +- tools/dev/generate_release_notes.py | 17 ++ 33 files changed, 352 insertions(+), 150 deletions(-) create mode 100644 docs/source/releasenotes/0.15.1.rst create mode 100644 tools/dev/generate_release_notes.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 378ca346a..184c522d4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,7 @@ openmc@anl.gov. ## Resources - [GitHub Repository](https://github.com/openmc-dev/openmc) -- [Documentation](http://docs.openmc.org/en/latest) +- [Documentation](https://docs.openmc.org/en/latest) - [Discussion Forum](https://openmc.discourse.group) - [Slack Community](https://openmc.slack.com/signup) (If you don't see your domain listed, contact openmc@anl.gov) diff --git a/docs/source/devguide/docker.rst b/docs/source/devguide/docker.rst index 0b2191168..50ff29bd2 100644 --- a/docs/source/devguide/docker.rst +++ b/docs/source/devguide/docker.rst @@ -45,12 +45,11 @@ Now you can run the following to create a `Docker container`_ called This command will open an interactive shell running from within the Docker container where you have access to use OpenMC. -.. note:: The ``docker run`` command supports many - `options `_ +.. note:: The ``docker run`` command supports many options_ for spawning containers -- including `mounting volumes`_ from the host filesystem -- which many users will find useful. -.. _Docker image: https://docs.docker.com/engine/reference/commandline/images/ +.. _Docker image: https://docs.docker.com/get-started/docker-concepts/the-basics/what-is-an-image/ .. _Docker container: https://www.docker.com/resources/what-container -.. _options: https://docs.docker.com/engine/reference/commandline/run/ -.. _mounting volumes: https://docs.docker.com/storage/volumes/ +.. _options: https://docs.docker.com/reference/cli/docker/container/run/ +.. _mounting volumes: https://docs.docker.com/engine/storage/volumes/ diff --git a/docs/source/devguide/styleguide.rst b/docs/source/devguide/styleguide.rst index b8ec2d40f..2c882b034 100644 --- a/docs/source/devguide/styleguide.rst +++ b/docs/source/devguide/styleguide.rst @@ -47,7 +47,8 @@ is more difficult to comment out a large section of code that uses C-style comments.) Do not use C-style casting. Always use the C++-style casts ``static_cast``, -``const_cast``, or ``reinterpret_cast``. (See `ES.49 `_) +``const_cast``, or ``reinterpret_cast``. (See `ES.49 +`_) Source Files ------------ @@ -156,11 +157,11 @@ Prefer pathlib_ when working with filesystem paths over functions in the os_ module or other standard-library modules. Functions that accept arguments that represent a filesystem path should work with both strings and Path_ objects. -.. _C++ Core Guidelines: http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines -.. _PEP8: https://www.python.org/dev/peps/pep-0008/ +.. _C++ Core Guidelines: https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines +.. _PEP8: https://peps.python.org/pep-0008/ .. _numpydoc: https://numpydoc.readthedocs.io/en/latest/format.html .. _numpy: https://numpy.org/ -.. _scipy: https://www.scipy.org/ +.. _scipy: https://scipy.org/ .. _matplotlib: https://matplotlib.org/ .. _pandas: https://pandas.pydata.org/ .. _h5py: https://www.h5py.org/ diff --git a/docs/source/devguide/user-input.rst b/docs/source/devguide/user-input.rst index a26f98b19..bbae3b715 100644 --- a/docs/source/devguide/user-input.rst +++ b/docs/source/devguide/user-input.rst @@ -55,6 +55,6 @@ developer or send a message to the `developers mailing list`_. .. _property attribute: https://docs.python.org/3.6/library/functions.html#property -.. _XML Schema Part 2: http://www.w3.org/TR/xmlschema-2/ -.. _boolean: http://www.w3.org/TR/xmlschema-2/#boolean +.. _XML Schema Part 2: https://www.w3.org/TR/xmlschema-2/ +.. _boolean: https://www.w3.org/TR/xmlschema-2/#boolean .. _developers mailing list: https://groups.google.com/forum/?fromgroups=#!forum/openmc-dev diff --git a/docs/source/devguide/workflow.rst b/docs/source/devguide/workflow.rst index d9d36c161..d600c236f 100644 --- a/docs/source/devguide/workflow.rst +++ b/docs/source/devguide/workflow.rst @@ -129,7 +129,7 @@ can interfere with virtual environments. .. _git: https://git-scm.com/ .. _GitHub: https://github.com/ .. _git flow: https://nvie.com/git-model -.. _valgrind: https://www.valgrind.org/ +.. _valgrind: https://valgrind.org/ .. _style guide: https://docs.openmc.org/en/latest/devguide/styleguide.html .. _pull request: https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests .. _openmc-dev/openmc: https://github.com/openmc-dev/openmc diff --git a/docs/source/methods/cross_sections.rst b/docs/source/methods/cross_sections.rst index ad64b0e38..a66abb3ed 100644 --- a/docs/source/methods/cross_sections.rst +++ b/docs/source/methods/cross_sections.rst @@ -295,8 +295,8 @@ or even isotropic scattering. .. _Josey: https://doi.org/10.1016/j.jcp.2015.08.013 .. _WMP Library: https://github.com/mit-crpg/WMP_Library .. _MCNP: https://mcnp.lanl.gov -.. _Serpent: https://serpent.vtt.fi/serpent/ -.. _NJOY: https://www.njoy21.io/NJOY21/ +.. _Serpent: https://serpent.vtt.fi +.. _NJOY: https://www.njoy21.io/ .. _ENDF/B data: https://www.nndc.bnl.gov/endf-b8.0/ .. _Leppanen: https://doi.org/10.1016/j.anucene.2009.03.019 .. _algorithms: http://ab-initio.mit.edu/faddeeva/ diff --git a/docs/source/methods/depletion.rst b/docs/source/methods/depletion.rst index 87a9976dd..edcf2c3f5 100644 --- a/docs/source/methods/depletion.rst +++ b/docs/source/methods/depletion.rst @@ -114,7 +114,7 @@ The predictor method only requires one evaluation and its error converges as twice as expensive as the predictor method, but achieves an error of :math:`\mathcal{O}(h^2)`. An exhaustive description of time integration methods and their merits can be found in the `thesis of Colin Josey -`_. +`_. OpenMC does not rely on a single time integration method but rather has several classes that implement different algorithms. For example, the diff --git a/docs/source/methods/geometry.rst b/docs/source/methods/geometry.rst index fa8bb3cf7..05cda4b64 100644 --- a/docs/source/methods/geometry.rst +++ b/docs/source/methods/geometry.rst @@ -1066,5 +1066,5 @@ surface is known as in :ref:`reflection`. .. _constructive solid geometry: https://en.wikipedia.org/wiki/Constructive_solid_geometry .. _surfaces: https://en.wikipedia.org/wiki/Surface .. _MCNP: https://mcnp.lanl.gov -.. _Serpent: https://serpent.vtt.fi/serpent/ +.. _Serpent: https://serpent.vtt.fi .. _Monte Carlo Performance benchmark: https://github.com/mit-crpg/benchmarks/tree/master/mc-performance/openmc diff --git a/docs/source/methods/neutron_physics.rst b/docs/source/methods/neutron_physics.rst index 70ace4a35..fe8b8ad85 100644 --- a/docs/source/methods/neutron_physics.rst +++ b/docs/source/methods/neutron_physics.rst @@ -1743,19 +1743,19 @@ types. .. _Watt fission spectrum: https://doi.org/10.1103/PhysRev.87.1037 -.. _Foderaro: http://hdl.handle.net/1721.1/1716 +.. _Foderaro: https://dspace.mit.edu/handle/1721.1/1716 .. _OECD: https://www.oecd-nea.org/tools/abstract/detail/NEA-1792 .. _NJOY: https://www.njoy21.io/NJOY2016/ -.. _PREPRO: https://www-nds.iaea.org/ndspub/endf/prepro/ +.. _PREPRO: https://www-nds.iaea.org/public/endf/prepro/ .. _ENDF-6 Format: https://www.oecd-nea.org/dbdata/data/manual-endf/endf102.pdf -.. _Monte Carlo Sampler: https://permalink.lanl.gov/object/tr?what=info:lanl-repo/lareport/LA-09721-MS +.. _Monte Carlo Sampler: https://mcnp.lanl.gov/pdf_files/TechReport_1983_LANL_LA-9721-MS_EverettCashwell.pdf -.. _LA-UR-14-27694: https://permalink.lanl.gov/object/tr?what=info:lanl-repo/lareport/LA-UR-14-27694 +.. _LA-UR-14-27694: https://www.osti.gov/biblio/1159204 .. _MC21: https://www.osti.gov/biblio/903083 @@ -1763,6 +1763,4 @@ types. .. _Sutton and Brown: https://www.osti.gov/biblio/307911 -.. _lectures: https://laws.lanl.gov/vhosts/mcnp.lanl.gov/pdf_files/la-ur-05-4983.pdf - -.. _MCNP Manual: https://laws.lanl.gov/vhosts/mcnp.lanl.gov/pdf_files/la-ur-03-1987.pdf +.. _lectures: https://mcnp.lanl.gov/pdf_files/TechReport_2005_LANL_LA-UR-05-4983_Brown.pdf diff --git a/docs/source/methods/parallelization.rst b/docs/source/methods/parallelization.rst index d9fc15c9f..87ac48590 100644 --- a/docs/source/methods/parallelization.rst +++ b/docs/source/methods/parallelization.rst @@ -609,17 +609,17 @@ is actually independent of the number of nodes: .. _first paper: https://doi.org/10.2307/2280232 -.. _work of Forrest Brown: http://hdl.handle.net/2027.42/24996 +.. _work of Forrest Brown: https://deepblue.lib.umich.edu/handle/2027.42/24996 .. _Brissenden and Garlick: https://doi.org/10.1016/0306-4549(86)90095-2 -.. _MPICH: http://www.mpich.org +.. _MPICH: https://www.mpich.org .. _binomial tree: https://www.mcs.anl.gov/~thakur/papers/ijhpca-coll.pdf .. _Geary: https://doi.org/10.2307/2342070 -.. _Barnett: http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.51.7772 +.. _Barnett: https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.51.7772 .. _single-instruction multiple-data: https://en.wikipedia.org/wiki/SIMD diff --git a/docs/source/methods/photon_physics.rst b/docs/source/methods/photon_physics.rst index 42c68431a..22d2c7f26 100644 --- a/docs/source/methods/photon_physics.rst +++ b/docs/source/methods/photon_physics.rst @@ -1059,7 +1059,7 @@ emitted photon. .. _anomalous scattering: http://pd.chem.ucl.ac.uk/pdnn/diff1/anomscat.htm -.. _Kahn's rejection method: https://mcnp.lanl.gov/pdf_files/TechReport_1956_RC_AECU-3259RM-1237-AEC_Kahn.pdf +.. _Kahn's rejection method: https://doi.org/10.2172/4353680 .. _Klein-Nishina: https://en.wikipedia.org/wiki/Klein%E2%80%93Nishina_formula diff --git a/docs/source/methods/tallies.rst b/docs/source/methods/tallies.rst index 4b56d3955..57e05d84f 100644 --- a/docs/source/methods/tallies.rst +++ b/docs/source/methods/tallies.rst @@ -522,4 +522,4 @@ improve the estimate of the percentile. .. _unpublished rational approximation: https://stackedboxes.org/2017/05/01/acklams-normal-quantile-function/ -.. _MC21: http://www.osti.gov/bridge/servlets/purl/903083-HT5p1o/903083.pdf +.. _MC21: https://www.osti.gov/servlets/purl/903083 diff --git a/docs/source/publications.rst b/docs/source/publications.rst index 88d0a97b9..a2d60d5af 100644 --- a/docs/source/publications.rst +++ b/docs/source/publications.rst @@ -138,8 +138,8 @@ Geometry and Visualization *Trans. Am. Nucl. Soc.*, **114**, 391-394 (2016). - Derek M. Lax, "`Memory efficient indexing algorithm for physical properties in - OpenMC `_," S. M. Thesis, Massachusetts - Institute of Technology (2015). + OpenMC `_," S. M. Thesis, + Massachusetts Institute of Technology (2015). - Derek Lax, William Boyd, Nicholas Horelik, Benoit Forget, and Kord Smith, "A memory efficient algorithm for classifying unique regions in constructive @@ -399,7 +399,8 @@ Doppler Broadening - Jonathan A. Walsh, Benoit Forget, Kord S. Smith, Brian C. Kiedrowski, and Forrest B. Brown, "`Direct, on-the-fly calculation of unresolved resonance region cross sections in Monte Carlo simulations - `_," *Proc. Joint Int. Conf. M&C+SNA+MC*, + `_," *Proc. Joint Int. Conf. + M&C+SNA+MC*, Nashville, Tennessee, Apr. 19--23 (2015). - Colin Josey, Benoit Forget, and Kord Smith, "`Windowed multipole sensitivity @@ -596,7 +597,8 @@ Depletion - Matthew S. Ellis, Colin Josey, Benoit Forget, and Kord Smith, "`Spatially Continuous Depletion Algorithm for Monte Carlo Simulations - `_," *Trans. Am. Nucl. Soc.*, **115**, + `_," *Trans. Am. Nucl. Soc.*, + **115**, 1221-1224 (2016). - Anas Gul, K. S. Chaudri, R. Khan, and M. Azeen, "`Development and verification diff --git a/docs/source/pythonapi/deplete.rst b/docs/source/pythonapi/deplete.rst index d7a779b12..5d02fa6b9 100644 --- a/docs/source/pythonapi/deplete.rst +++ b/docs/source/pythonapi/deplete.rst @@ -26,7 +26,7 @@ provided to obtain reaction rates from cross-section data. Several classes are provided that implement different time-integration algorithms for depletion calculations, which are described in detail in Colin Josey's thesis, `Development and analysis of high order neutron transport-depletion coupling -algorithms `_. +algorithms `_. .. autosummary:: :toctree: generated diff --git a/docs/source/quickinstall.rst b/docs/source/quickinstall.rst index 323cd7fd4..21526242f 100644 --- a/docs/source/quickinstall.rst +++ b/docs/source/quickinstall.rst @@ -8,56 +8,35 @@ This quick install guide outlines the basic steps needed to install OpenMC on your computer. For more detailed instructions on configuring and installing OpenMC, see :ref:`usersguide_install` in the User's Manual. --------------------------------------------------- -Installing on Linux/Mac with Mamba and conda-forge --------------------------------------------------- +---------------------------------- +Installing on Linux/Mac with Conda +---------------------------------- -`Conda `_ is an open source package management +`Conda `_ is an open source package management system and environments management system for installing multiple versions of software packages and their dependencies and switching easily between them. -`Mamba `_ is a cross-platform package -manager and is compatible with `conda` packages. -OpenMC can be installed in a `conda` environment with `mamba`. -First, `conda` should be installed with one of the following installers: -`Miniconda `_, -`Anaconda `_, or `Miniforge `_. -Once you have `conda` installed on your system, OpenMC can be installed via the -`conda-forge` channel with `mamba`. +OpenMC can be installed in a `conda` environment. First, `conda` should be +`installed `_ +with either Anaconda Distribution or Miniconda. Once you have `conda` installed +on your system, OpenMC can be installed via the `conda-forge` channel. First, add the `conda-forge` channel with: .. code-block:: sh conda config --add channels conda-forge + conda config --set channel_priority strict -Then create and activate a new conda enviroment called `openmc-env` in -which to install OpenMC. +Then create and activate a new conda enviroment called `openmc-env` (or whatever +you wish) with OpenMC installed. .. code-block:: sh - conda create -n openmc-env + conda create --name openmc-env openmc conda activate openmc-env -Then install `mamba`, which will be used to install OpenMC. - -.. code-block:: sh - - conda install mamba - -To list the versions of OpenMC that are available on the `conda-forge` channel, -in your terminal window or an Anaconda Prompt run: - -.. code-block:: sh - - mamba search openmc - -OpenMC can then be installed with: - -.. code-block:: sh - - mamba install openmc - -You are now in a conda environment called `openmc-env` that has OpenMC installed. +You are now in a conda environment called `openmc-env` that has OpenMC +installed. ------------------------------------------- Installing on Linux/Mac/Windows with Docker diff --git a/docs/source/releasenotes/0.15.1.rst b/docs/source/releasenotes/0.15.1.rst new file mode 100644 index 000000000..d879b50ed --- /dev/null +++ b/docs/source/releasenotes/0.15.1.rst @@ -0,0 +1,224 @@ +==================== +What's New in 0.15.1 +==================== + +.. currentmodule:: openmc + +------- +Summary +------- + +This release of OpenMC includes many bug fixes, performance improvements, and +several notable new features. The random ray solver continues to receive many +updates and improvements, which are listed below in more detail. A new +:class:`~openmc.SolidRayTracePlot` class has been added that enables attractive +3D visualization using Phong shading. Several composite surfaces have been +introduced (which help to further expand the capabilities of the +`openmc_mcnp_adapter `_). +The :meth:`openmc.Mesh.material_volumes` method has been completely +reimplemented with a new approach based on ray tracing that greatly improves +performance and can be executed in parallel. Tally results can be automatically +applied to input :class:`~openmc.Tally` objects with :meth:`openmc.Model.run`, +bypassing boilerplate code for collecting tally results from statepoint files. +Finally, a new :mod:`openmc.deplete.d1s` submodule has been added that enables +Direct 1-Step (D1S) calculations of shutdown dose rate for fusion applications. + +------------------------------------ +Compatibility Notes and Deprecations +------------------------------------ + +The ``openmc.ProjectionPlot`` class has been renamed to +:class:`openmc.WireframeRayTracePlot` to be in better alignment with the newly +introduced :class:`openmc.SolidRayTracePlot` class. + +NCrystal has been moved from a build-time dependency to a runtime dependency, +which means there is no longer a ``OPENMC_USE_NCRYSTAL`` CMake option. Instead, +OpenMC will look for an installed version of NCrystal using the +``ncrystal-config`` command. + +------------ +New Features +------------ + +- Numerous improvements have been made in the random ray solver: + - Calculation of Shannon entropy now works with random ray (`#3030 `_) + - Support for linear sources (`#3072 `_) + - Ability to slove for adjoint flux (`#3191 `_) + - Support randomized Quasi-Monte Carlo sampling (`#3268 `_) + - FW-CADIS weight window generation (`#3273 `_) + - Source region mesh subdivision(`#3333 `_) +- Several new composite surfaces have been added: + - :class:`openmc.model.OrthogonalBox` (`#3118 `_) + - :class:`openmc.model.ConicalFrustum` (`#3151 `_) + - :class:`openmc.model.Vessel` (`#3168 `_) +- The :meth:`openmc.Model.plot` method now supports plotting source sites + (`#2863 `_) +- The :func:`openmc.stats.delta_function` convenience function can be used for + specifying distributions with a single point (`#3090 + `_) +- Added a :meth:`openmc,Material.get_element_atom_densities` method (`#3103 + `_) +- Several third-party dependencies have been removed: + - Cython (`#3111 `_) + - gsl-lite (`#3225 `_) +- Added a new :class:`openmc.MuSurfaceFilter` class that filters tally events by + the cosine of angle of a surface crossing (`#2768 + `_) +- Introduced a :class:`openmc.ParticleList` class for manipulating a list of + source particles (`#3148 `_) +- Support dose coefficients from ICRP 74 in + :func:`openmc.data.dose_coefficients` (`#3020 + `_) +- Introduced a new :attr:`openmc.Settings.uniform_source_sampling` option + (`#3195 `_) +- Ability to differentiate materials in DAGMC universes (`#3056 + `_) +- Added methods to automatically apply results to existing Tally objects. + (`#2671 `_) +- Implemented a new :class:`openmc.SolidRayTracePlot` class that can produce a + 3D visualization based on Phong shading (`#2655 + `_) +- The :meth:`openmc.UnstructuredMesh.write_data_to_vtk` method now supports + writing a VTU file (`#3290 `_) +- Composite surfaces now have a + :attr:`~openmc.CompositeSurface.component_surfaces` attribute that provides + the underlying primitive surfaces (`#3167 + `_) +- A new :mod:`openmc.deplete.d1s` submodule has been added that enables Direct + 1-Step (D1S) calculations of shutdown dose rate for fusion applications + (`#3235 `_) + +--------------------------- +Bug Fixes and Small Changes +--------------------------- + +- run microxs with mpi (`#3028 `_) +- Rely on std::filesystem for file_utils (`#3042 `_) +- Random Ray Normalization Improvements (`#3051 `_) +- Alternative Random Ray Volume Estimators (`#3060 `_) +- Random Ray Testing Simplification (`#3061 `_) +- Fix hyperlinks in `random_ray.rst` (`#3064 `_) +- Add missing show_overlaps option to plots.xml input file documentation (`#3068 `_) +- Remove use of pkg_resources package (`#3069 `_) +- Add option for survival biasing source normalization (`#3070 `_) +- Enforce sequence type when setting ``Setting.track`` (`#3071 `_) +- Moving most of setup.py to pyproject.toml (`#3074 `_) +- Enforce non-negative percents for ``material.add_nuclide`` to prevent unintended ao/wo flipping (`#3075 `_) +- Include batch statistics discussion in methodology introduction (`#3076 `_) +- Add -DCMAKE_BUILD_TYPE=Release flag for MOAB in Dockerfile (`#3077 `_) +- Adjust decay data reader to better handle non-normalized branching ratios (`#3080 `_) +- Correct openmc.Geometry initializer to accept iterables of ``openmc.Cell`` (`#3081 `_) +- Replace all deprecated Python typing imports and syntax with updated forms (`#3085 `_) +- Fix ParticleFilter to work with set inputs (`#3092 `_) +- packages used for testing moved to tests section of pyprojects.toml (`#3094 `_) +- removed unused which function in CI scripts (`#3095 `_) +- Improve description of probabilities for ``openmc.stats.Tabular`` class (`#3099 `_) +- Ensure RegularMesh repr shows value for width of the mesh (`#3100 `_) +- Replacing endf c functions with package (`#3101 `_) +- Fix random ray solver to correctly simulate fixed source problems with fissionable materials (`#3106 `_) +- Improve error for nuclide temperature not found (`#3110 `_) +- Added error if cross sections path is a folder (`#3115 `_) +- Implement bounding_box operation for meshes (`#3119 `_) +- allowing varible offsets for ``polygon.offset`` (`#3120 `_) +- Write surface source files per batch (`#3124 `_) +- Mat ids reset (`#3125 `_) +- Tweaking title of feature issue template (`#3127 `_) +- Fix a typo in feature request template (`#3128 `_) +- Update quickinstall instructions for macOS (`#3130 `_) +- adapt the openmc-update-inputs script for surfaces (`#3131 `_) +- Theory documentation on PCG random number generator (`#3134 `_) +- Adding tmate action to CI for debugging (`#3138 `_) +- Add Versioning Support from `version.txt` (`#3140 `_) +- Correct failure due to progress bar values (`#3143 `_) +- Avoid writing subnormal nuclide densities to XML (`#3144 `_) +- Immediately resolve complement operators for regions (`#3145 `_) +- Improve Detection of libMesh Installation via `LIBMESH_ROOT` and CMake's PkgConfig (`#3149 `_) +- Fix for UWUW Macro Conflict (`#3150 `_) +- Consistency in treatment of paths for files specified within the Model class (`#3153 `_) +- Improve clipping of Mixture distributions (`#3154 `_) +- Fix check for trigger score name (`#3155 `_) +- Prepare point query data structures on meshes when applying Weight Windows (`#3157 `_) +- Add PointCloud spatial distribution (`#3161 `_) +- Update fmt submodule to version 11.0.2 (`#3162 `_) +- Move to support python 3.13 (`#3165 `_) +- avoid zero division if source rate of previous result is zero (`#3169 `_) +- Fix path handling for thermal ACE generation (`#3171 `_) +- Update `fmt` Formatters for Compatibility with Versions below 11 (`#3172 `_) +- added subfolders to txt search command in pyproject (`#3174 `_) +- added list to doc string arg for plot_xs (`#3178 `_) +- enable polymorphism for mix_materials (`#3180 `_) +- Fix plot_xs type hint (`#3184 `_) +- Enable adaptive mesh support on libMesh tallies (`#3185 `_) +- Reset values of lattice offset tables when allocated (`#3188 `_) +- Update surface_composite.py (`#3189 `_) +- add export_model_xml arguments to ``Model.plot_geometry`` and ``Model.calculate_volumes`` (`#3190 `_) +- Fixes in MicroXS.from_multigroup_flux (`#3192 `_) +- Fix documentation typo in ``boundary_type`` (`#3196 `_) +- Fix docstring for ``Model.plot`` (`#3198 `_) +- Apply weight windows at collisions in multigroup transport mode. (`#3199 `_) +- External sources alias sampler (`#3201 `_) +- Add test for flux bias with weight windows in multigroup mode (`#3202 `_) +- Fix bin index to DoF ID mapping bug in adaptive libMesh meshes (`#3206 `_) +- Ensure ``libMesh::ReplicatedMesh`` is used for LibMesh tallies (`#3208 `_) +- Set Model attributes only if needed (`#3209 `_) +- adding unstrucutred mesh file suffix to docstring (`#3211 `_) +- Write and read mesh name attribute (`#3221 `_) +- Adjust for secondary particle energy directly in heating scores (`#3227 `_) +- Correct normalization of thermal elastic in non standard ENDF-6 files (`#3234 `_) +- Adding '#define _USE_MATH_DEFINES' to make M_PI declared in Intel and MSVC compilers (`#3238 `_) +- updated link to log mapping technique (`#3241 `_) +- Fix for erroneously non-zero tally results of photon threshold reactions (`#3242 `_) +- Fix type comparison (`#3244 `_) +- Enable the LegendreFilter filter to be used in photon tallies for orders greater than P0. (`#3245 `_) +- Enable UWUW library when building with DAGMC in CI (`#3246 `_) +- Remove top-level import of ``openmc.lib`` (`#3250 `_) +- updated docker file to latest DAGMC (`#3251 `_) +- Write mesh type as a dataset always (`#3253 `_) +- Update to a consistent definition of the r2 parameter for cones (`#3254 `_) +- Add Patrick Shriwise to technical committee (`#3255 `_) +- Change `Zernike` documentation in polynomial.py (`#3258 `_) +- Bug fix for Polygon 'yz' basis (`#3259 `_) +- Add constant for invalid surface tokens. (`#3260 `_) +- Update plots.py for PathLike to string handling error (`#3261 `_) +- Fix bug in WeightWindowGenerator for empty energy bounds (`#3263 `_) +- Update recognized thermal scattering materials for ENDF/B-VIII.1 (`#3267 `_) +- simplify mechanism to detect if geometry entity is DAG (`#3269 `_) +- Fix bug in ``Surface.normalize`` (`#3270 `_) +- Tweak To Sphinx Install Documentation (`#3271 `_) +- add continue feature for depletion (`#3272 `_) +- Updates for building with NCrystal support (and fix CI) (`#3274 `_) +- Added missing documentation (`#3275 `_) +- fix the bug in function differentiate_mats() (`#3277 `_) +- Fix the bug in the ``Material.from_xml_element`` function (`#3278 `_) +- Doc typo fix for rand ray mgxs (`#3280 `_) +- Consolidate plotting capabilities in Model.plot (`#3282 `_) +- adding non elastic MT number (`#3285 `_) +- Fix ``Tabular.from_xml_element`` for histogram case (`#3287 `_) +- Random Ray Source Region Refactor (`#3288 `_) +- added terminal output showing compile options selected (`#3291 `_) +- Random ray consistency changes (`#3298 `_) +- Random Ray Explicit Void Treatment (`#3299 `_) +- removed old command line scripts (`#3300 `_) +- Avoid end of life ubuntu 20.04 in ReadTheDocs runner (`#3301 `_) +- Avoid error in CI from newlines in commit message (`#3302 `_) +- Handle reflex angles in CylinderSector (`#3303 `_) +- Relax requirement on polar/azimuthal axis for wwinp conversion (`#3307 `_) +- Add nuclides_to_ignore argument on Model export methods (`#3309 `_) +- Enable overlap plotting from Python API (`#3310 `_) +- Fix access order issues after applying tally results from `Model.run` (`#3313 `_) +- Random Ray Void Accuracy Fix (`#3316 `_) +- Fixes for problems encountered with version determination (`#3320 `_) +- Clarify effect of CMAKE_BUILD_TYPE in docs (`#3321 `_) +- Random Ray Linear Source Stability Improvement (`#3322 `_) +- Mark a canonical URL for docs (`#3324 `_) +- Random Ray Adjoint Source Logic Improvement (`#3325 `_) +- Reflect multigroup MicroXS in IndependentOperator docstrings (`#3327 `_) +- NCrystal becomes runtime rather than buildtime dependency (`#3328 `_) +- Adding per kg as unit option on material functions (`#3329 `_) +- Fix reading of horizontal field of view for ray-traced plots (`#3330 `_) +- Manually fix broken links (`#3331 `_) +- Update pugixml to v1.15 (`#3332 `_) +- Determine nuclides correctly for DAGMC models in d1s.get_radionuclides (`#3335 `_) +- openmc.Material.mix_materials() allows for keyword arguments (`#3336 `_) +- Fix bug in ``Mesh::material_volumes`` for void materials (`#3337 `_) +- added stable and unstable nuclides to the Chain object (`#3338 `_) diff --git a/docs/source/releasenotes/index.rst b/docs/source/releasenotes/index.rst index bde120589..2927cc458 100644 --- a/docs/source/releasenotes/index.rst +++ b/docs/source/releasenotes/index.rst @@ -7,6 +7,7 @@ Release Notes .. toctree:: :maxdepth: 1 + 0.15.1 0.15.0 0.14.0 0.13.3 diff --git a/docs/source/usersguide/basics.rst b/docs/source/usersguide/basics.rst index 60b599588..c0bc2f976 100644 --- a/docs/source/usersguide/basics.rst +++ b/docs/source/usersguide/basics.rst @@ -53,7 +53,7 @@ eXtensible Markup Language (XML) Unlike many other Monte Carlo codes which use an arbitrary-format ASCII file with "cards" to specify a particular geometry, materials, and associated run settings, the input files for OpenMC are structured in a set of `XML -`_ files. XML, which stands for eXtensible Markup +`_ files. XML, which stands for eXtensible Markup Language, is a simple format that allows data to be exchanged efficiently between different programs and interfaces. diff --git a/docs/source/usersguide/beginners.rst b/docs/source/usersguide/beginners.rst index eef927b84..6876a3324 100644 --- a/docs/source/usersguide/beginners.rst +++ b/docs/source/usersguide/beginners.rst @@ -109,8 +109,8 @@ familiar with. Whether you plan on working in Linux, macOS, or Windows, you should be comfortable working in a command line environment. There are many resources online for learning command line environments. If you are using Linux or Mac OS X (also Unix-derived), `this tutorial -`_ will help you get acquainted with -commonly-used commands. +`_ will help you get acquainted +with commonly-used commands. To reap the full benefits of OpenMC, you should also have basic proficiency in the use of `Python `_, as OpenMC includes a rich Python @@ -127,8 +127,8 @@ are hosted at `GitHub`_. In order to receive updates to the code directly, submit `bug reports`_, and perform other development tasks, you may want to sign up for a free account on GitHub. Once you have an account, you can follow `these instructions -`_ on -how to set up your computer for using GitHub. +`_ +on how to set up your computer for using GitHub. If you are new to nuclear engineering, you may want to review the NRC's `Reactor Concepts Manual`_. This manual describes the basics of nuclear power for @@ -149,9 +149,9 @@ and `Volume II`_. You may also find it helpful to review the following terms: .. _neutron transport: https://en.wikipedia.org/wiki/Neutron_transport .. _discretization: https://en.wikipedia.org/wiki/Discretization .. _constructive solid geometry: https://en.wikipedia.org/wiki/Constructive_solid_geometry -.. _git: http://git-scm.com/ +.. _git: https://git-scm.com/ .. _git tutorials: https://git-scm.com/doc -.. _Reactor Concepts Manual: http://www.tayloredge.com/periodic/trivia/ReactorConcepts.pdf +.. _Reactor Concepts Manual: https://www.tayloredge.com/periodic/trivia/ReactorConcepts.pdf .. _Volume I: https://www.standards.doe.gov/standards-documents/1000/1019-bhdbk-1993-v1 .. _Volume II: https://www.standards.doe.gov/standards-documents/1000/1019-bhdbk-1993-v2 .. _OpenMC source code: https://github.com/openmc-dev/openmc diff --git a/docs/source/usersguide/data.rst b/docs/source/usersguide/data.rst index a1611de63..2a9cd36db 100644 --- a/docs/source/usersguide/data.rst +++ b/docs/source/usersguide/data.rst @@ -12,9 +12,9 @@ responsible for specifying one or more of the following: file (commonly named ``cross_sections.xml``) contains a listing of other data files, in particular neutron cross sections, photon cross sections, and windowed multipole data. Each of those files, in turn, uses a `HDF5 - `_ format (see :ref:`io_nuclear_data`). In - order to run transport simulations with continuous-energy cross sections, you - need to specify this file. + `_ format (see + :ref:`io_nuclear_data`). In order to run transport simulations with + continuous-energy cross sections, you need to specify this file. - **Depletion chain (XML)** -- A :ref:`depletion chain XML ` file contains decay data, fission product yields, and information on what @@ -69,7 +69,7 @@ If you want to persistently set the environment variables used to initialized the configuration, export them from your shell profile (``.profile`` or ``.bashrc`` in bash_). -.. _bash: http://www.linuxfromscratch.org/blfs/view/6.3/postlfs/profile.html +.. _bash: https://www.linuxfromscratch.org/blfs/view/6.3/postlfs/profile.html -------------------------------- Continuous-Energy Cross Sections @@ -290,16 +290,16 @@ calculation to be performed. Therefore, at this point in time, OpenMC is not distributed with any pre-existing multigroup cross section libraries. However, if a multigroup library file is downloaded or generated, the path to the file needs to be specified as described in :ref:`usersguide_data_runtime`. For an -example of how to create a multigroup library, see the `example notebook -`_. +example of how to create a multigroup library, see this `MG mode notebook +`_. -.. _NJOY: http://www.njoy21.io/ +.. _NJOY: https://www.njoy21.io/ .. _NNDC: https://www.nndc.bnl.gov/endf .. _MCNP: https://mcnp.lanl.gov -.. _Serpent: https://serpent.vtt.fi/serpent/ +.. _Serpent: https://serpent.vtt.fi .. _ENDF/B: https://www.nndc.bnl.gov/endf-b7.1/acefiles.html .. _JEFF: https://www.oecd-nea.org/dbdata/jeff/jeff33/ -.. _TENDL: https://tendl.web.psi.ch/tendl_2017/tendl2017.html +.. _TENDL: https://tendl.web.psi.ch/tendl_2023/tendl2023.html .. _Seltzer and Berger: https://doi.org/10.1016/0092-640X(86)90014-8 .. _NIST ESTAR database: https://physics.nist.gov/PhysRefData/Star/Text/ESTAR.html .. _Biggs et al.: https://doi.org/10.1016/0092-640X(75)90030-3 diff --git a/docs/source/usersguide/decay_sources.rst b/docs/source/usersguide/decay_sources.rst index e61220696..a228f8e66 100644 --- a/docs/source/usersguide/decay_sources.rst +++ b/docs/source/usersguide/decay_sources.rst @@ -47,14 +47,14 @@ Direct 1-Step (D1S) Calculations ================================ OpenMC also includes built-in capability for performing shutdown dose rate -calculations using the `direct 1-step `_ -(D1S) method. In this method, a single coupled neutron--photon transport -calculation is used where the prompt photon production is replaced with photons -produced from the decay of radionuclides in an activated material. To obtain -properly scaled results, it is also necessary to apply time correction factors. -A normal neutron transport calculation can be extended to a D1S calculation with -a few helper functions. First, import the ``d1s`` submodule, which is part of -:mod:`openmc.deplete`:: +calculations using the `direct 1-step +`_ (D1S) method. In this method, +a single coupled neutron--photon transport calculation is used where the prompt +photon production is replaced with photons produced from the decay of +radionuclides in an activated material. To obtain properly scaled results, it is +also necessary to apply time correction factors. A normal neutron transport +calculation can be extended to a D1S calculation with a few helper functions. +First, import the ``d1s`` submodule, which is part of :mod:`openmc.deplete`:: from openmc.deplete import d1s diff --git a/docs/source/usersguide/install.rst b/docs/source/usersguide/install.rst index 0aa561ee3..d294770e0 100644 --- a/docs/source/usersguide/install.rst +++ b/docs/source/usersguide/install.rst @@ -8,56 +8,35 @@ Installation and Configuration .. _install_conda: --------------------------------------------------- -Installing on Linux/Mac with Mamba and conda-forge --------------------------------------------------- +---------------------------------- +Installing on Linux/Mac with Conda +---------------------------------- -`Conda `_ is an open source package management -systems and environments management system for installing multiple versions of +`Conda`_ is an open source package management +system and environments management system for installing multiple versions of software packages and their dependencies and switching easily between them. -`Mamba `_ is a cross-platform package -manager and is compatible with `conda` packages. -OpenMC can be installed in a `conda` environment with `mamba`. -First, `conda` should be installed with one of the following installers: -`Miniconda `_, -`Anaconda `_, or `Miniforge `_. -Once you have `conda` installed on your system, OpenMC can be installed via the -`conda-forge` channel with `mamba`. +OpenMC can be installed in a `conda` environment. First, `conda` should be +`installed `_ +with either Anaconda Distribution or Miniconda. Once you have `conda` installed +on your system, OpenMC can be installed via the `conda-forge` channel. First, add the `conda-forge` channel with: .. code-block:: sh conda config --add channels conda-forge + conda config --set channel_priority strict -Then create and activate a new conda enviroment called `openmc-env` in -which to install OpenMC. +Then create and activate a new conda enviroment called `openmc-env` (or whatever +you wish) with OpenMC installed. .. code-block:: sh - conda create -n openmc-env + conda create --name openmc-env openmc conda activate openmc-env -Then install `mamba`, which will be used to install OpenMC. - -.. code-block:: sh - - conda install mamba - -To list the versions of OpenMC that are available on the `conda-forge` channel, -in your terminal window or an Anaconda Prompt run: - -.. code-block:: sh - - mamba search openmc - -OpenMC can then be installed with: - -.. code-block:: sh - - mamba install openmc - -You are now in a conda environment called `openmc-env` that has OpenMC installed. +You are now in a conda environment called `openmc-env` that has OpenMC +installed. ------------------------------------------- Installing on Linux/Mac/Windows with Docker @@ -557,7 +536,7 @@ distributions. notebook `_. - `h5py `_ + `h5py `_ h5py provides Python bindings to the HDF5 library. Since OpenMC outputs various HDF5 files, h5py is needed to provide access to data within these files from Python. @@ -610,5 +589,6 @@ wrapper is used when installing h5py: CC= HDF5_MPI=ON HDF5_DIR= python -m pip install --no-binary=h5py h5py +.. _Mamba: https://mamba.readthedocs.io/en/latest/ .. _Conda: https://conda.io/en/latest/ .. _pip: https://pip.pypa.io/en/stable/ diff --git a/docs/source/usersguide/parallel.rst b/docs/source/usersguide/parallel.rst index e00ed5e97..ecbdd20b6 100644 --- a/docs/source/usersguide/parallel.rst +++ b/docs/source/usersguide/parallel.rst @@ -101,5 +101,5 @@ performance on a machine when running in parallel: settings = openmc.Settings() settings.output = {'tallies': False} -.. _Haswell-EP: http://www.anandtech.com/show/8423/intel-xeon-e5-version-3-up-to-18-haswell-ep-cores-/4 +.. _Haswell-EP: https://www.anandtech.com/show/8423/intel-xeon-e5-version-3-up-to-18-haswell-ep-cores-/4 .. _bound: https://github.com/pmodels/mpich/blob/main/doc/wiki/how_to/Using_the_Hydra_Process_Manager.md#process-core-binding diff --git a/include/openmc/random_dist.h b/include/openmc/random_dist.h index 11e88ab8c..32f055b53 100644 --- a/include/openmc/random_dist.h +++ b/include/openmc/random_dist.h @@ -64,8 +64,8 @@ extern "C" double watt_spectrum(double a, double b, uint64_t* seed); //! Samples from a normal distribution with a given mean and standard deviation //! The PDF is defined as s(x) = (1/2*sigma*sqrt(2) * e-((mu-x)/2*sigma)^2 //! Its sampled according to -//! http://www-pdg.lbl.gov/2009/reviews/rpp2009-rev-monte-carlo-techniques.pdf -//! section 33.4.4 +//! https://pdg.lbl.gov/2023/reviews/rpp2023-rev-monte-carlo-techniques.pdf +//! section 42.4.4 //! //! \param mean mean of the Gaussian distribution //! \param std_dev standard deviation of the Gaussian distribution diff --git a/openmc/data/ace.py b/openmc/data/ace.py index 1247593a8..6ccb76c92 100644 --- a/openmc/data/ace.py +++ b/openmc/data/ace.py @@ -9,9 +9,9 @@ unresolved resonance region, and tabulated data in the fast region. After the ENDF data has been reconstructed and Doppler-broadened, the ACER module generates ACE-format cross sections. -.. _MCNP: https://laws.lanl.gov/vhosts/mcnp.lanl.gov/ -.. _NJOY: http://t2.lanl.gov/codes.shtml -.. _ENDF: http://www.nndc.bnl.gov/endf +.. _MCNP: https://mcnp.lanl.gov/ +.. _NJOY: https://www.njoy21.io/ +.. _ENDF: https://www.nndc.bnl.gov/endf-library/ """ diff --git a/openmc/data/endf.py b/openmc/data/endf.py index 63cd092eb..eca374469 100644 --- a/openmc/data/endf.py +++ b/openmc/data/endf.py @@ -1,9 +1,9 @@ """Module for parsing and manipulating data from ENDF evaluations. -All the classes and functions in this module are based on document -ENDF-102 titled "Data Formats and Procedures for the Evaluated Nuclear -Data File ENDF-6". The latest version from June 2009 can be found at -http://www-nds.iaea.org/ndspub/documents/endf/endf102/endf102.pdf +All the classes and functions in this module are based on document ENDF-102 +titled "Data Formats and Procedures for the Evaluated Nuclear Data File ENDF-6". +The version from September 2023 can be found at +https://www.nndc.bnl.gov/endfdocs/ENDF-102-2023.pdf """ import io diff --git a/openmc/data/fission_energy.py b/openmc/data/fission_energy.py index 870881dba..3c7998ee2 100644 --- a/openmc/data/fission_energy.py +++ b/openmc/data/fission_energy.py @@ -44,7 +44,7 @@ class FissionEnergyRelease(EqualityMixin): ---------- [1] D. G. Madland, "Total prompt energy release in the neutron-induced fission of ^235U, ^238U, and ^239Pu", Nuclear Physics A 772:113--137 (2006). - + Attributes ---------- diff --git a/openmc/deplete/integrators.py b/openmc/deplete/integrators.py index a877c4900..50810c88a 100644 --- a/openmc/deplete/integrators.py +++ b/openmc/deplete/integrators.py @@ -446,7 +446,7 @@ class SICELIIntegrator(SIIntegrator): `_. Detailed algorithm can be found in section 3.2 in `Colin Josey's thesis - `_. + `_. """ _num_stages = 2 @@ -512,7 +512,7 @@ class SILEQIIntegrator(SIIntegrator): `_. Detailed algorithm can be found in Section 3.2 in `Colin Josey's thesis - `_. + `_. """ _num_stages = 2 diff --git a/openmc/examples.py b/openmc/examples.py index 8d4bd1f04..5578d513e 100644 --- a/openmc/examples.py +++ b/openmc/examples.py @@ -11,8 +11,9 @@ def pwr_pin_cell() -> openmc.Model: This model is a single fuel pin with 2.4 w/o enriched UO2 corresponding to a beginning-of-cycle condition and borated water. The specifications are from - the `BEAVRS `_ benchmark. Note that the - number of particles/batches is initially set very low for testing purposes. + the `BEAVRS `_ benchmark. Note that + the number of particles/batches is initially set very low for testing + purposes. Returns ------- diff --git a/openmc/mgxs/__init__.py b/openmc/mgxs/__init__.py index 6a29a8383..901c1eabb 100644 --- a/openmc/mgxs/__init__.py +++ b/openmc/mgxs/__init__.py @@ -14,7 +14,7 @@ GROUP_STRUCTURES = {} - "SHEM-361_" designed for LWR analysis to eliminate self-shielding calculations of thermal resonances ([HFA2005]_, [SAN2007]_, [HEB2008]_) - "SCALE-X" (where X is 44 which is designed for criticality analysis - and 252 is designed for thermal reactors) for the SCALE code suite + and 252 is designed for thermal reactors) for the SCALE code suite ([ZAL1999]_ and [REARDEN2013]_) - "MPACT-X" (where X is 51 (PWR), 60 (BWR), 69 (Magnox)) from the MPACT_ reactor physics code ([KIM2019]_ and [KIM2020]_) @@ -28,12 +28,12 @@ GROUP_STRUCTURES = {} .. _SCALE252: https://oecd-nea.org/science/wpncs/amct/workingarea/meeting2013/EGAMCT2013_08.pdf .. _MPACT: https://vera.ornl.gov/mpact/ .. _XMAS-172: https://www-nds.iaea.org/wimsd/energy.htm -.. _SHEM-361: https://www.polymtl.ca/merlin/downloads/FP214.pdf +.. _SHEM-361: http://merlin.polymtl.ca/downloads/FP214.pdf .. _activation: https://fispact.ukaea.uk/wiki/Keyword:GETXS .. _VITAMIN-J-42: https://www.oecd-nea.org/dbdata/nds_jefreports/jefreport-10.pdf .. _CCFE-709: https://fispact.ukaea.uk/wiki/CCFE-709_group_structure .. _UKAEA-1102: https://fispact.ukaea.uk/wiki/UKAEA-1102_group_structure -.. _ECCO-1968: http://serpent.vtt.fi/mediawiki/index.php/ECCO_1968-group_structure +.. _ECCO-1968: https://serpent.vtt.fi/mediawiki/index.php/ECCO_1968-group_structure .. [SAR1990] Sartori, E., OECD/NEA Data Bank: Standard Energy Group Structures of Cross Section Libraries for Reactor Shielding, Reactor Cell and Fusion Neutronics Applications: VITAMIN-J, ECCO-33, ECCO-2000 and XMAS JEF/DOC-315 diff --git a/openmc/tallies.py b/openmc/tallies.py index 585e54a7e..ebb313551 100644 --- a/openmc/tallies.py +++ b/openmc/tallies.py @@ -1502,7 +1502,7 @@ class Tally(IDManagerMixin): df.columns = pd.MultiIndex.from_tuples(columns) # Modify the df.to_string method so that it prints formatted strings. - # Credit to http://stackoverflow.com/users/3657742/chrisb for this trick + # Credit to https://stackoverflow.com/users/3657742/chrisb for this trick df.to_string = partial(df.to_string, float_format=float_format.format) return df diff --git a/src/math_functions.cpp b/src/math_functions.cpp index ba1c6c3db..5469b56c8 100644 --- a/src/math_functions.cpp +++ b/src/math_functions.cpp @@ -650,7 +650,7 @@ void calc_zn(int n, double rho, double phi, double zn[]) // =========================================================================== // Determine vector of sin(n*phi) and cos(n*phi). This takes advantage of the // following recurrence relations so that only a single sin/cos have to be - // evaluated (http://mathworld.wolfram.com/Multiple-AngleFormulas.html) + // evaluated (https://mathworld.wolfram.com/Multiple-AngleFormulas.html) // // sin(nx) = 2 cos(x) sin((n-1)x) - sin((n-2)x) // cos(nx) = 2 cos(x) cos((n-1)x) - cos((n-2)x) diff --git a/tools/dev/generate_release_notes.py b/tools/dev/generate_release_notes.py new file mode 100644 index 000000000..dee46ac49 --- /dev/null +++ b/tools/dev/generate_release_notes.py @@ -0,0 +1,17 @@ +import argparse +import re +import subprocess + +parser = argparse.ArgumentParser() +parser.add_argument('tag') +args = parser.parse_args() + +proc = subprocess.run(["git", "log", "--format=%s", f"{args.tag}.."], capture_output=True, text=True) +data = [] +for line in proc.stdout.rstrip().split('\n'): + m = re.match(r'(.*) \(\#(\d+)\)', line) + if m is not None: + data.append(m.groups()) + +for comment, num in sorted(data, key=lambda x: int(x[1])): + print(f'- {comment} (`#{num} `_)') From 58f2a21771a3c50100cef8f8a70f67302542f4d9 Mon Sep 17 00:00:00 2001 From: Adam Nelson <1037107+nelsonag@users.noreply.github.com> Date: Fri, 14 Mar 2025 11:57:33 -0500 Subject: [PATCH 295/671] Hotfix: Remove errant openmc.Settings.random_ray check and removed of not useful warning in MG mode (#3344) --- openmc/settings.py | 1 - src/scattdata.cpp | 10 ---------- 2 files changed, 11 deletions(-) diff --git a/openmc/settings.py b/openmc/settings.py index ead2c0d00..17c24ba81 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -1174,7 +1174,6 @@ class Settings: raise ValueError( f'Invalid domain type: {type(domain)}. Expected ' 'openmc.Material, openmc.Cell, or openmc.Universe.') - cv.check_type('adjoint', value, bool) elif key == 'sample_method': cv.check_value('sample method', value, ('prng', 'halton')) diff --git a/src/scattdata.cpp b/src/scattdata.cpp index 813f9e19b..21b18cbd9 100644 --- a/src/scattdata.cpp +++ b/src/scattdata.cpp @@ -37,22 +37,12 @@ void ScattData::base_init(int order, const xt::xtensor& in_gmin, mult[gin] = in_mult[gin]; // Make sure the multiplicity does not have 0s - unsigned long int num_converted = 0; for (int go = 0; go < mult[gin].size(); go++) { if (mult[gin][go] == 0.) { - num_converted += 1; mult[gin][go] = 1.; } } - if (num_converted > 0) { - // Raise a warning to the user if we did have to do the conversion - std::string msg = - std::to_string(num_converted) + - " entries in the Multiplicity Matrix were changed from 0 to 1"; - warning(msg); - } - // Make sure the energy is normalized double norm = std::accumulate(energy[gin].begin(), energy[gin].end(), 0.); From 08e7043f8d0cf3bfce5b47d468442079dd17de03 Mon Sep 17 00:00:00 2001 From: Kevin Sawatzky <66632997+nuclearkevin@users.noreply.github.com> Date: Tue, 18 Mar 2025 16:19:15 -0500 Subject: [PATCH 296/671] Throw an error if a spherical harmonics order larger than 10 is provided. (#3354) --- src/tallies/filter_sph_harm.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tallies/filter_sph_harm.cpp b/src/tallies/filter_sph_harm.cpp index 7441f32fb..1989e6ef1 100644 --- a/src/tallies/filter_sph_harm.cpp +++ b/src/tallies/filter_sph_harm.cpp @@ -25,6 +25,9 @@ void SphericalHarmonicsFilter::set_order(int order) if (order < 0) { throw std::invalid_argument { "Spherical harmonics order must be non-negative."}; + } else if (order > 10) { + throw std::invalid_argument {"Spherical harmonics orders greater than 10 " + "are currently not supported!"}; } order_ = order; n_bins_ = (order_ + 1) * (order_ + 1); From 18d9f9755190fc8bdcf51d6d5dba67531a42ce5d Mon Sep 17 00:00:00 2001 From: Joffrey Dorville <54550047+JoffreyDorville@users.noreply.github.com> Date: Tue, 18 Mar 2025 16:25:48 -0500 Subject: [PATCH 297/671] Correcting the size of the displacement list in the SourceSite MPI interface object (#3356) --- src/initialize.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/initialize.cpp b/src/initialize.cpp index 4b821bee1..c5803976b 100644 --- a/src/initialize.cpp +++ b/src/initialize.cpp @@ -157,7 +157,7 @@ void initialize_mpi(MPI_Comm intracomm) // Create bank datatype SourceSite b; - MPI_Aint disp[10]; + MPI_Aint disp[11]; MPI_Get_address(&b.r, &disp[0]); MPI_Get_address(&b.u, &disp[1]); MPI_Get_address(&b.E, &disp[2]); From e23760b0264c66fb7bb373aa0596801e5209d920 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 19 Mar 2025 09:09:47 -0500 Subject: [PATCH 298/671] Add release notes for 0.15.2 (#3357) --- docs/source/releasenotes/0.15.2.rst | 20 ++++++++++++++++++++ docs/source/releasenotes/index.rst | 1 + 2 files changed, 21 insertions(+) create mode 100644 docs/source/releasenotes/0.15.2.rst diff --git a/docs/source/releasenotes/0.15.2.rst b/docs/source/releasenotes/0.15.2.rst new file mode 100644 index 000000000..e72df37d3 --- /dev/null +++ b/docs/source/releasenotes/0.15.2.rst @@ -0,0 +1,20 @@ +==================== +What's New in 0.15.2 +==================== + +.. currentmodule:: openmc + +------- +Summary +------- + +This is a hotfix release to fix an MPI-related bug that was inadvertently +introduced in the prior release. + +--------------------------- +Bug Fixes and Small Changes +--------------------------- + +- Remove errant ``openmc.Settings.random_ray`` check and removed of not useful warning in MG mode (`#3344 `_) +- Throw an error if a spherical harmonics order larger than 10 is provided. (`#3354 `_) +- Correcting the size of the displacement list in the SourceSite MPI interface object (`#3356 `_) diff --git a/docs/source/releasenotes/index.rst b/docs/source/releasenotes/index.rst index 2927cc458..d24b83f9e 100644 --- a/docs/source/releasenotes/index.rst +++ b/docs/source/releasenotes/index.rst @@ -7,6 +7,7 @@ Release Notes .. toctree:: :maxdepth: 1 + 0.15.2 0.15.1 0.15.0 0.14.0 From 277390b220b363036193845302efdeb5f70387b1 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Thu, 20 Mar 2025 08:00:34 -0400 Subject: [PATCH 299/671] added kg units to doc string in results class (#3358) --- openmc/deplete/results.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index 132ec572c..7427abd73 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -113,9 +113,9 @@ class Results(list): ---------- mat : openmc.Material, str Material object or material id to evaluate - units : {'Bq', 'Bq/g', 'Bq/cm3'} + units : {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3'} Specifies the type of activity to return, options include total - activity [Bq], specific [Bq/g] or volumetric activity [Bq/cm3]. + activity [Bq], specific [Bq/g, Bq/kg] or volumetric activity [Bq/cm3]. by_nuclide : bool Specifies if the activity should be returned for the material as a whole or per nuclide. Default is False. @@ -231,9 +231,9 @@ class Results(list): ---------- mat : openmc.Material, str Material object or material id to evaluate. - units : {'W', 'W/g', 'W/cm3'} + units : {'W', 'W/g', 'W/kg', 'W/cm3'} Specifies the units of decay heat to return. Options include total - heat [W], specific [W/g] or volumetric heat [W/cm3]. + heat [W], specific [W/g, W/kg] or volumetric heat [W/cm3]. by_nuclide : bool Specifies if the decay heat should be returned for the material as a whole or per nuclide. Default is False. From 3f3649da086fd296cde37dcda9218514f8c16c98 Mon Sep 17 00:00:00 2001 From: Ahnaf Tahmid Chowdhury Date: Sat, 22 Mar 2025 03:33:28 +0600 Subject: [PATCH 300/671] Handle Missing Tags in Versioning by Setting Default to 0 (#3359) Co-authored-by: Micah Gale Co-authored-by: Patrick Shriwise --- cmake/Modules/GetVersionFromGit.cmake | 10 ++++++++-- docs/source/devguide/workflow.rst | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/cmake/Modules/GetVersionFromGit.cmake b/cmake/Modules/GetVersionFromGit.cmake index 42b3725f2..3736955ff 100644 --- a/cmake/Modules/GetVersionFromGit.cmake +++ b/cmake/Modules/GetVersionFromGit.cmake @@ -37,11 +37,17 @@ if(EXISTS "${CMAKE_SOURCE_DIR}/.git" AND GIT_FOUND) WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE VERSION_STRING OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET ) - # If no tags are found, instruct user to fetch them + # If no tags are found, set version to 0 and show a warning if(VERSION_STRING STREQUAL "") - message(FATAL_ERROR "No git tags found. Run 'git fetch --tags' and try again.") + set(VERSION_STRING "0.0.0") + message(WARNING + "No git tags found. Version set to 0.0.0.\n" + "Run 'git fetch --tags' to ensure proper versioning.\n" + "For more information, see OpenMC developer documentation." + ) endif() # Extract the commit hash diff --git a/docs/source/devguide/workflow.rst b/docs/source/devguide/workflow.rst index d600c236f..c49326a20 100644 --- a/docs/source/devguide/workflow.rst +++ b/docs/source/devguide/workflow.rst @@ -91,6 +91,30 @@ features and bug fixes. The general steps for contributing are as follows: 6. After the pull request has been thoroughly vetted, it is merged back into the *develop* branch of openmc-dev/openmc. +Setting Up Upstream Tracking (Required for Versioning) +------------------------------------------------------ + +By default, your fork **does not** include tags from the upstream OpenMC repository. +OpenMC relies on `git describe --tags` for versioning in source builds, and missing tags can lead +to incorrect version detection (i.e., ``0.0.0``). To ensure proper versioning, follow these steps: + +1. **Add the Upstream Repository** + This allows you to fetch updates from the main OpenMC repository. + + .. code-block:: sh + + git remote add upstream https://github.com/openmc-dev/openmc.git + +2. **Fetch and Push Tags** + Retrieve tags from the upstream repository and update your fork: + + .. code-block:: sh + + git fetch --tags upstream + git push --tags origin + +This ensures that both your **local** and **remote** fork have the correct versioning information. + Private Development ------------------- From 24655dfd5db9fced3baad9ccc7197615e25ef520 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Mon, 31 Mar 2025 09:29:30 -0500 Subject: [PATCH 301/671] Report plot ID instead of index for unsupported plot types in random ray mode (#3361) --- src/random_ray/random_ray_simulation.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index bf01d2f57..36517bee4 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -303,20 +303,21 @@ void validate_random_ray_inputs() for (int p = 0; p < model::plots.size(); p++) { // Get handle to OpenMC plot object - Plot* openmc_plot = dynamic_cast(model::plots[p].get()); + const auto& openmc_plottable = model::plots[p]; + Plot* openmc_plot = dynamic_cast(openmc_plottable.get()); // Random ray plots only support voxel plots if (!openmc_plot) { warning(fmt::format( "Plot {} will not be used for end of simulation data plotting -- only " "voxel plotting is allowed in random ray mode.", - p)); + openmc_plottable->id())); continue; } else if (openmc_plot->type_ != Plot::PlotType::voxel) { warning(fmt::format( "Plot {} will not be used for end of simulation data plotting -- only " "voxel plotting is allowed in random ray mode.", - p)); + openmc_plottable->id())); continue; } } From b67771a3d0451be6857e8459a6d2a15a476ec994 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Tue, 1 Apr 2025 21:47:49 -0500 Subject: [PATCH 302/671] Random Ray AutoMagic Setup (#3351) Co-authored-by: Paul Romano --- .github/workflows/ci.yml | 5 + docs/source/io_formats/settings.rst | 8 + docs/source/pythonapi/deplete.rst | 8 +- docs/source/usersguide/random_ray.rst | 212 +++++++- docs/source/usersguide/variance_reduction.rst | 75 +-- .../openmc/random_ray/flat_source_domain.h | 10 + openmc/model/model.py | 482 ++++++++++++++++++ openmc/settings.py | 17 +- src/random_ray/flat_source_domain.cpp | 55 ++ src/random_ray/random_ray_simulation.cpp | 44 +- src/settings.cpp | 9 + .../mgxs_library_ce_to_mg/test.py | 9 +- .../mgxs_library_ce_to_mg_nuclides/test.py | 9 +- .../random_ray_auto_convert/__init__.py | 0 .../infinite_medium/inputs_true.dat | 64 +++ .../infinite_medium/results_true.dat | 2 + .../material_wise/inputs_true.dat | 64 +++ .../material_wise/results_true.dat | 2 + .../stochastic_slab/inputs_true.dat | 64 +++ .../stochastic_slab/results_true.dat | 2 + .../random_ray_auto_convert/test.py | 54 ++ .../__init__.py | 0 .../inputs_true.dat | 65 +++ .../results_true.dat | 2 + .../random_ray_diagonal_stabilization/test.py | 61 +++ tools/ci/gha-install-dagmc.sh | 4 +- 26 files changed, 1239 insertions(+), 88 deletions(-) create mode 100644 tests/regression_tests/random_ray_auto_convert/__init__.py create mode 100644 tests/regression_tests/random_ray_auto_convert/infinite_medium/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert/infinite_medium/results_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert/material_wise/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert/material_wise/results_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert/stochastic_slab/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert/stochastic_slab/results_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert/test.py create mode 100644 tests/regression_tests/random_ray_diagonal_stabilization/__init__.py create mode 100644 tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_diagonal_stabilization/results_true.dat create mode 100644 tests/regression_tests/random_ray_diagonal_stabilization/test.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 877ec6833..38a49b602 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,6 +81,11 @@ jobs: RDMAV_FORK_SAFE: 1 steps: + - name: Setup cmake + uses: jwlawson/actions-setup-cmake@v2 + with: + cmake-version: '3.31' + - name: Checkout repository uses: actions/checkout@v4 with: diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index a59d08049..cef9bf911 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -487,6 +487,14 @@ found in the :ref:`random ray user guide `. :type: The type of the domain. Can be ``material``, ``cell``, or ``universe``. + + :diagonal_stabilization_rho: + The rho factor for use with diagonal stabilization. This technique is + applied when negative diagonal (in-group) elements are detected in + the scattering matrix of input MGXS data, which is a common feature + of transport corrected MGXS data. + + *Default*: 1.0 ---------------------------------- ```` Element diff --git a/docs/source/pythonapi/deplete.rst b/docs/source/pythonapi/deplete.rst index 5d02fa6b9..3967f1a18 100644 --- a/docs/source/pythonapi/deplete.rst +++ b/docs/source/pythonapi/deplete.rst @@ -78,20 +78,18 @@ A minimal example for performing depletion would be: >>> import openmc.deplete >>> geometry = openmc.Geometry.from_xml() >>> settings = openmc.Settings.from_xml() - >>> model = openmc.model.Model(geometry, settings) + >>> model = openmc.Model(geometry, settings) # Representation of a depletion chain >>> chain_file = "chain_casl.xml" - >>> operator = openmc.deplete.CoupledOperator( - ... model, chain_file) + >>> operator = openmc.deplete.CoupledOperator(model, chain_file) # Set up 5 time steps of one day each >>> dt = [24 * 60 * 60] * 5 >>> power = 1e6 # constant power of 1 MW # Deplete using mid-point predictor-corrector - >>> cecm = openmc.deplete.CECMIntegrator( - ... operator, dt, power) + >>> cecm = openmc.deplete.CECMIntegrator(operator, dt, power) >>> cecm.integrate() Internal Classes and Functions diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index f28aed784..ce9821196 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -11,6 +11,76 @@ active batches `. However, there are a couple of settings that are unique to the random ray solver and a few areas that the random ray run strategy differs, both of which will be described in this section. +.. _quick_start: + +----------- +Quick Start +----------- + +While this page contains a comprehensive guide to the random ray solver and +its various parameters, the process of converting an existing continuous energy +Monte Carlo model to a random ray model can be largely automated via convenience +functions in OpenMC's Python interface:: + + # Define continuous energy model as normal + model = openmc.Model() + ... + + # Convert model to multigroup (will auto-generate MGXS library if needed) + model.convert_to_multigroup() + + # Convert model to random ray and initialize random ray parameters + # to reasonable defaults based on the specifics of the geometry + model.convert_to_random_ray() + + # (Optional) Overlay source region decomposition mesh to improve fidelity of the + # random ray solver. Adjust 'n' for fidelity vs runtime. + n = 100 + mesh = openmc.RegularMesh() + mesh.dimension = (n, n, n) + mesh.lower_left = model.geometry.bounding_box.lower_left + mesh.upper_right = model.geometry.bounding_box.upper_right + model.settings.random_ray['source_region_meshes'] = [(mesh, [model.geometry.root_universe])] + + # (Optional) Improve fidelity of the random ray solver by enabling linear sources + model.settings.random_ray['source_shape'] = 'linear' + + # (Optional) Increase the number of rays/batch, to reduce uncertainty + model.settings.particles = 500 + +The above strategy first converts the continuous energy model to a multigroup +one using the :meth:`openmc.Model.convert_to_multigroup` method. By default, +this will internally run a coarsely converged continuous energy Monte Carlo +simulation to produce an estimated multigroup macroscopic cross section set for +each material specified in the model, and store this data into a multigroup +cross section library file (``mgxs.h5``) that can be used by the random ray +solver. + +The :meth:`openmc.Model.convert_to_random_ray` method enables random ray mode +and performs an analysis of the model geometry to determine reasonable values +for all required parameters. If default behavior is not satisfactory, the user +can manually adjust the settings in the :attr:`~openmc.Settings.random_ray` +dictionary in the :class:`openmc.Settings` as described in the sections below. + +Finally a few optional steps are shown. The first (recommended) step overlays a +mesh over the geometry to create smaller source regions so that source +resolution improves and the random ray solver becomes more accurate. Varying the +mesh resolution can be used to trade off between accuracy and runtime. +High-fidelity fission reactor simulation may require source region sizes below 1 +cm, while larger fixed source problems with some tolerance for error may be able +to use source regions of 10 or 100 cm. + +We also enable linear sources, which can improve the accuracy of the random ray +solver and/or allow for a much coarser mesh resolution to be overlaid. Finally, +the number of rays per batch is adjusted. The goal here is to ensure that the +source region miss rate is below 1%, which is reported by OpenMC at the end of +the simulation (or before via a warning if it is very high). + +.. warning:: + If using a mesh filter for tallying or weight window generation, ensure that + the same mesh is used for source region decomposition via + ``model.settings.random_ray['source_region_meshes']``. + ------------------------ Enabling Random Ray Mode ------------------------ @@ -557,29 +627,118 @@ variety of problem types (or through a multidimensional parameter sweep of design variables) with only modest errors and at greatly reduced cost as compared to using only continuous energy Monte Carlo. +~~~~~~~~~~~~ +The Easy Way +~~~~~~~~~~~~ + +The easiest way to generate a multigroup cross section library is to use the +:meth:`openmc.Model.convert_to_multigroup` method. This method will +automatically output a multigroup cross section library file (``mgxs.h5``) from +a continuous energy Monte Carlo model and alter the material definitions in the +model to use these multigroup cross sections. An example is given below:: + + # Assume we already have a working continuous energy model + model.convert_to_multigroup( + method="material_wise", + groups="CASMO-2", + nparticles=2000, + overwrite_mgxs_library=False, + mgxs_path="mgxs.h5", + correction=None + ) + +The most important parameter to set is the ``method`` parameter, which can be +either "stochastic_slab", "material_wise", or "infinite_medium". An overview +of these methods is given below: + +.. list-table:: Comparison of Automatic MGXS Generation Methods + :header-rows: 1 + :widths: 10 30 30 30 + + * - Method + - Description + - Pros + - Cons + * - ``material_wise`` (default) + - * Higher Fidelity + * Runs a CE simulation with the original geometry and source, tallying + cross sections with a material filter. + - * Typically the most accurate of the three methods + * Accurately captures (averaged over the full problem domain) + both spatial and resonance self shielding effects + - * Potentially slower as the full geometry must be run + * If a material is only present far from the source and doesn't get tallied + to in the CE simulation, the MGXS will be zero for that material. + * - ``stochastic_slab`` + - * Medium Fidelity + * Runs a CE simulation with a greatly simplified geometry, where materials + are randomly assigned to layers in a 1D "stochastic slab sandwich" geometry + - * Still captures resonant self shielding and resonance effects between materials + * Fast due to the simplified geometry + * Able to produce cross section data for all materials, regardless of how + far they are from the source in the original geometry + - * Does not capture most spatial self shielding effects, e.g., no lattice physics. + * - ``infinite_medium`` + - * Lower Fidelity + * Runs one CE simulation per material independently. Each simulation is just + an infinite medium slowing down problem, with an assumed external source term. + - * Simple + - * Poor accuracy (no spatial information, no lattice physics, no resonance effects + between materials) + * May hang if a material has a k-infinity greater than 1.0 + +When selecting a non-default energy group structure, you can manually define +group boundaries or specify the name of a known group structure (a list of which +can be found at :data:`openmc.mgxs.GROUP_STRUCTURES`). The ``nparticles`` +parameter can be adjusted upward to improve the fidelity of the generated cross +section library. The ``correction`` parameter can be set to ``"P0"`` to enable +P0 transport correction. The ``overwrite_mgxs_library`` parameter can be set to +``True`` to overwrite an existing MGXS library file, or ``False`` to skip +generation and use an existing library file. + +.. note:: + MGXS transport correction (via setting the ``correction`` parameter in the + :meth:`openmc.Model.convert_to_multigroup` method to ``"P0"``) may + result in negative in-group scattering cross sections, which can cause + numerical instability. To mitigate this, during a random ray solve OpenMC + will automatically apply + `diagonal stabilization `_ + with a :math:`\rho` default value of 1.0, which can be adjusted with the + ``settings.random_ray['diagonal_stabilization_rho']`` parameter. + +Ultimately, the methods described above are all just approximations. +Approximations in the generated MGXS data will fundamentally limit the potential +accuracy of the random ray solver. However, the methods described above are all +useful in that they can provide a good starting point for a random ray +simulation, and if more fidelity is needed the user may wish to follow the +instructions below or experiment with transport correction techniques to improve +the fidelity of the generated MGXS data. + +~~~~~~~~~~~~ +The Hard Way +~~~~~~~~~~~~ + We give here a quick summary of how to produce a multigroup cross section data file (``mgxs.h5``) from a starting point of a typical continuous energy Monte -Carlo input file. Notably, continuous energy input files define materials as a -mixture of nuclides with different densities, whereas multigroup materials are -simply defined by which name they correspond to in a ``mgxs.h5`` library file. +Carlo model. Notably, continuous energy models define materials as a mixture of +nuclides with different densities, whereas multigroup materials are simply +defined by which name they correspond to in a ``mgxs.h5`` library file. To generate the cross section data, we begin with a continuous energy Monte -Carlo input deck and add in the required tallies that will be needed to generate -our library. In this example, we will specify material-wise cross sections and a -two group energy decomposition:: +Carlo model and add in the tallies that are needed to generate our library. In +this example, we will specify material-wise cross sections and a two-group +energy decomposition:: # Define geometry ... - ... geometry = openmc.Geometry() ... - ... # Initialize MGXS library with a finished OpenMC geometry object mgxs_lib = openmc.mgxs.Library(geometry) # Pick energy group structure - groups = openmc.mgxs.EnergyGroups(openmc.mgxs.GROUP_STRUCTURES['CASMO-2']) + groups = openmc.mgxs.EnergyGroups('CASMO-2') mgxs_lib.energy_groups = groups # Disable transport correction @@ -587,7 +746,7 @@ two group energy decomposition:: # Specify needed cross sections for random ray mgxs_lib.mgxs_types = ['total', 'absorption', 'nu-fission', 'fission', - 'nu-scatter matrix', 'multiplicity matrix', 'chi'] + 'nu-scatter matrix', 'multiplicity matrix', 'chi'] # Specify a "cell" domain type for the cross section tally filters mgxs_lib.domain_type = "material" @@ -614,13 +773,13 @@ two group energy decomposition:: ... When selecting an energy decomposition, you can manually define group boundaries -or pick out a group structure already known to OpenMC (a list of which can be -found at :class:`openmc.mgxs.GROUP_STRUCTURES`). Once the above input deck has -been run, the resulting statepoint file will contain the needed flux and -reaction rate tally data so that a MGXS library file can be generated. Below is -the postprocessing script needed to generate the ``mgxs.h5`` library file given -a statepoint file (e.g., ``statepoint.100.h5``) file and summary file (e.g., -``summary.h5``) that resulted from running our previous example:: +or specify the name of known group structure (a list of which can be found at +:data:`openmc.mgxs.GROUP_STRUCTURES`). Once the above model has been run, the +resulting statepoint file will contain the needed flux and reaction rate tally +data so that a MGXS library file can be generated. Below is the postprocessing +script needed to generate the ``mgxs.h5`` library file given a statepoint file +(e.g., ``statepoint.100.h5``) file and summary file (e.g., ``summary.h5``) that +resulted from running our previous example:: import openmc @@ -628,10 +787,7 @@ a statepoint file (e.g., ``statepoint.100.h5``) file and summary file (e.g., geom = summary.geometry mats = summary.materials - statepoint_filename = 'statepoint.100.h5' - sp = openmc.StatePoint(statepoint_filename) - - groups = openmc.mgxs.EnergyGroups(openmc.mgxs.GROUP_STRUCTURES['CASMO-2']) + groups = openmc.mgxs.EnergyGroups('CASMO-2') mgxs_lib = openmc.mgxs.Library(geom) mgxs_lib.energy_groups = groups mgxs_lib.correction = None @@ -653,10 +809,10 @@ a statepoint file (e.g., ``statepoint.100.h5``) file and summary file (e.g., # Construct all tallies needed for the multi-group cross section library mgxs_lib.build_library() - mgxs_lib.load_from_statepoint(sp) + with openmc.StatePoint('statepoint.100.h5') as sp: + mgxs_lib.load_from_statepoint(sp) - names = [] - for mat in mgxs_lib.domains: names.append(mat.name) + names = [mat.name for mat in mgxs_lib.domains] # Create a MGXS File which can then be written to disk mgxs_file = mgxs_lib.create_mg_library(xs_type='macro', xsdata_names=names) @@ -665,8 +821,8 @@ a statepoint file (e.g., ``statepoint.100.h5``) file and summary file (e.g., mgxs_file.export_to_hdf5("mgxs.h5") Notably, the postprocessing script needs to match the same -:class:`openmc.mgxs.Library` settings that were used to generate the tallies, -but otherwise is able to discern the rest of the simulation details from the +:class:`openmc.mgxs.Library` settings that were used to generate the tallies but +is otherwise able to discern the rest of the simulation details from the statepoint and summary files. Once the postprocessing script is successfully run, the ``mgxs.h5`` file can be loaded by subsequent runs of OpenMC. @@ -701,11 +857,11 @@ multigroup library instead of defining their isotopic contents, as:: water_data = openmc.Macroscopic('Hot borated water') # Instantiate some Materials and register the appropriate Macroscopic objects - fuel= openmc.Material(name='UO2 (2.4%)') + fuel = openmc.Material(name='UO2 (2.4%)') fuel.set_density('macro', 1.0) fuel.add_macroscopic(fuel_data) - water= openmc.Material(name='Hot borated water') + water = openmc.Material(name='Hot borated water') water.set_density('macro', 1.0) water.add_macroscopic(water_data) diff --git a/docs/source/usersguide/variance_reduction.rst b/docs/source/usersguide/variance_reduction.rst index f8ee9c35c..3256fa35f 100644 --- a/docs/source/usersguide/variance_reduction.rst +++ b/docs/source/usersguide/variance_reduction.rst @@ -88,59 +88,66 @@ random ray mode can be found in the :ref:`Random Ray User Guide `. ray solver. A high level overview of the current workflow for generation of weight windows with FW-CADIS using random ray is given below. -1. Produce approximate multigroup cross section data (stored in a ``mgxs.h5`` - library). There is more information on generating multigroup cross sections - via OpenMC in the :ref:`multigroup materials ` user guide, and a - specific example of generating cross section data for use with random ray in - the :ref:`random ray MGXS guide `. +1. Begin by making a deepy copy of your continuous energy Python model and then + convert the copy to be multigroup and use the random ray transport solver. + The conversion process can largely be automated as described in more detail + in the :ref:`random ray quick start guide `, summarized below:: -2. Make a copy of your continuous energy Python input file. You'll edit the new - file to work in multigroup mode with random ray for producing weight windows. + # Define continuous energy model + ce_model = openmc.pwr_pin_cell() # example, replace with your model -3. Adjust the material definitions in your new multigroup Python file to utilize - the multigroup cross sections instead of nuclide-wise continuous energy data. - There is a specific example of making this conversion in the :ref:`random ray - MGXS guide `. + # Make a copy to convert to multigroup and random ray + model = copy.deepcopy(ce_model) -4. Configure OpenMC to run in random ray mode (by adding several standard random - ray input flags and settings to the :attr:`openmc.Settings.random_ray` - dictionary). More information can be found in the :ref:`Random Ray User - Guide `. + # Convert model to multigroup (will auto-generate MGXS library if needed) + model.convert_to_multigroup() -5. Add in a :class:`~openmc.WeightWindowGenerator` in a similar manner as for + # Convert model to random ray and initialize random ray parameters + # to reasonable defaults based on the specifics of the geometry + model.convert_to_random_ray() + + # (Optional) Overlay source region decomposition mesh to improve fidelity of the + # random ray solver. Adjust 'n' for fidelity vs runtime. + n = 10 + mesh = openmc.RegularMesh() + mesh.dimension = (n, n, n) + mesh.lower_left = model.geometry.bounding_box.lower_left + mesh.upper_right = model.geometry.bounding_box.upper_right + model.settings.random_ray['source_region_meshes'] = [(mesh, [model.geometry.root_universe])] + + # (Optional) Improve fidelity of the random ray solver by enabling linear sources + model.settings.random_ray['source_shape'] = 'linear' + + # (Optional) Increase the number of rays/batch, to reduce uncertainty + model.settings.particles = 500 + + If you need to improve the fidelity of the MGXS library, there is more + information on generating multigroup cross sections via OpenMC in the + :ref:`random ray MGXS guide `. + +2. Add in a :class:`~openmc.WeightWindowGenerator` in a similar manner as for MAGIC generation with Monte Carlo and set the :attr:`method` attribute set to ``"fw_cadis"``:: - # Define weight window spatial mesh - ww_mesh = openmc.RegularMesh() - ww_mesh.dimension = (10, 10, 10) - ww_mesh.lower_left = (0.0, 0.0, 0.0) - ww_mesh.upper_right = (100.0, 100.0, 100.0) - - # Create weight window object and adjust parameters + # Create weight window object and adjust parameters, using the same mesh + # we used for source region decomposition wwg = openmc.WeightWindowGenerator( method='fw_cadis', - mesh=ww_mesh, + mesh=mesh, max_realizations=settings.batches ) # Add generator to openmc.settings object settings.weight_window_generators = wwg - .. warning:: If using FW-CADIS weight window generation, ensure that the selected weight window mesh does not subdivide any source regions in the problem. This can - be ensured by assigning the weight window tally mesh to the root universe so - as to create source region boundaries that conform to the mesh, as in the - example below. + be ensured by using the same mesh for both source region subdivision (i.e., + assigning to ``model.settings.random_ray['source_region_meshes']``) and for + weight window generation. -:: - - root = model.geometry.root_universe - settings.random_ray['source_region_meshes'] = [(ww_mesh, [root])] - -6. When running your multigroup random ray input deck, OpenMC will automatically +3. When running your multigroup random ray input deck, OpenMC will automatically run a forward solve followed by an adjoint solve, with a ``weight_windows.h5`` file generated at the end. The ``weight_windows.h5`` file will contain FW-CADIS generated weight windows. This file can be used in diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index 39d87d663..656b0354a 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -58,6 +58,7 @@ public: SourceRegionHandle get_subdivided_source_region_handle( int64_t sr, int mesh_bin, Position r, double dist, Direction u); void finalize_discovered_source_regions(); + void apply_transport_stabilization(); int64_t n_source_regions() const { return source_regions_.n_source_regions(); @@ -71,6 +72,9 @@ public: // Static Data members static bool volume_normalized_flux_tallies_; static bool adjoint_; // If the user wants outputs based on the adjoint flux + static double + diagonal_stabilization_rho_; // Adjusts strength of diagonal stabilization + // for transport corrected MGXS data // Static variables to store source region meshes and domains static std::unordered_map>> @@ -127,6 +131,12 @@ public: std::unordered_map source_region_map_; + // If transport corrected MGXS data is being used, there may be negative + // in-group scattering cross sections that can result in instability in MOC + // and random ray if used naively. This flag enables a stabilization + // technique. + bool is_transport_stabilization_needed_ {false}; + protected: //---------------------------------------------------------------------------- // Methods diff --git a/openmc/model/model.py b/openmc/model/model.py index e8558f4ad..260059f4a 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1,9 +1,12 @@ from __future__ import annotations from collections.abc import Iterable, Sequence +import copy from functools import lru_cache from pathlib import Path import math from numbers import Integral, Real +import random +import re from tempfile import NamedTemporaryFile, TemporaryDirectory import warnings @@ -1439,3 +1442,482 @@ class Model: self.materials = openmc.Materials( self.geometry.get_all_materials().values() ) + + def _generate_infinite_medium_mgxs(self, groups, nparticles, mgxs_path, correction): + """Generate a MGXS library by running multiple OpenMC simulations, each + representing an infinite medium simulation of a single isolated + material. A discrete source is used to sample particles, with an equal + strength spread across each of the energy groups. This is a highly naive + method that ignores all spatial self shielding effects and all resonance + shielding effects between materials. + + Parameters + ---------- + groups : openmc.mgxs.EnergyGroups + Energy group structure for the MGXS. + mgxs_path : path-like + Filename for the MGXS HDF5 file. + """ + warnings.warn("The infinite medium method of generating MGXS may hang " + "if a material has a k-infinity > 1.0.") + mgxs_sets = [] + for material in self.materials: + openmc.reset_auto_ids() + model = openmc.Model() + + # Set materials on the model + model.materials = [material] + + # Settings + model.settings.batches = 100 + model.settings.particles = nparticles + model.settings.run_mode = 'fixed source' + + # Make a discrete source that is uniform over the bins of the group structure + n_groups = groups.num_groups + midpoints = [] + strengths = [] + for i in range(n_groups): + bounds = groups.get_group_bounds(i+1) + midpoints.append((bounds[0] + bounds[1]) / 2.0) + strengths.append(1.0) + + energy_distribution = openmc.stats.Discrete(x=midpoints, p=strengths) + model.settings.source = openmc.IndependentSource( + space=openmc.stats.Point(), energy=energy_distribution) + model.settings.output = {'summary': True, 'tallies': False} + + # Geometry + box = openmc.model.RectangularPrism( + 100000.0, 100000.0, boundary_type='reflective') + name = material.name + infinite_cell = openmc.Cell(name=name, fill=material, region=-box) + infinite_universe = openmc.Universe(name=name, cells=[infinite_cell]) + model.geometry.root_universe = infinite_universe + + # Add MGXS Tallies + + # Initialize MGXS library with a finished OpenMC geometry object + mgxs_lib = openmc.mgxs.Library(model.geometry) + + # Pick energy group structure + mgxs_lib.energy_groups = groups + + # Disable transport correction + mgxs_lib.correction = correction + + # Specify needed cross sections for random ray + if correction == 'P0': + mgxs_lib.mgxs_types = [ + 'nu-transport', 'absorption', 'nu-fission', 'fission', + 'consistent nu-scatter matrix', 'multiplicity matrix', 'chi' + ] + elif correction is None: + mgxs_lib.mgxs_types = [ + 'total', 'absorption', 'nu-fission', 'fission', + 'consistent nu-scatter matrix', 'multiplicity matrix', 'chi' + ] + + # Specify a "cell" domain type for the cross section tally filters + mgxs_lib.domain_type = "material" + + # Specify the cell domains over which to compute multi-group cross sections + mgxs_lib.domains = model.geometry.get_all_materials().values() + + # Do not compute cross sections on a nuclide-by-nuclide basis + mgxs_lib.by_nuclide = False + + # Check the library - if no errors are raised, then the library is satisfactory. + mgxs_lib.check_library_for_openmc_mgxs() + + # Construct all tallies needed for the multi-group cross section library + mgxs_lib.build_library() + + # Create a "tallies.xml" file for the MGXS Library + mgxs_lib.add_to_tallies_file(model.tallies, merge=True) + + # Run + statepoint_filename = model.run() + + # Load MGXS + with openmc.StatePoint(statepoint_filename) as sp: + mgxs_lib.load_from_statepoint(sp) + + # Create a MGXS File which can then be written to disk + mgxs_set = mgxs_lib.get_xsdata(domain=material, xsdata_name=name) + mgxs_sets.append(mgxs_set) + + # Write the file to disk + mgxs_file = openmc.MGXSLibrary(energy_groups=groups) + for mgxs_set in mgxs_sets: + mgxs_file.add_xsdata(mgxs_set) + mgxs_file.export_to_hdf5(mgxs_path) + + @staticmethod + def _create_stochastic_slab_geometry(materials, cell_thickness=1.0, num_repeats=100): + """Create a geometry representing a stochastic "sandwich" of materials in a + layered slab geometry. To reduce the impact of the order of materials in + the slab, the materials are applied to 'num_repeats' different randomly + positioned layers of 'cell_thickness' each. + + Parameters + ---------- + materials : list of openmc.Material + List of materials to assign. Each material will appear exactly num_repeats times, + then the ordering is randomly shuffled. + cell_thickness : float, optional + Thickness of each lattice cell in x (default 1.0 cm). + num_repeats : int, optional + Number of repeats for each material (default 100). + + Returns + ------- + geometry : openmc.Geometry + The constructed geometry. + box : openmc.stats.Box + A spatial sampling distribution covering the full slab domain. + """ + if not materials: + raise ValueError("At least one material must be provided.") + + num_materials = len(materials) + total_cells = num_materials * num_repeats + total_width = total_cells * cell_thickness + + # Generate an infinite cell/universe for each material + universes = [] + for i in range(num_materials): + cell = openmc.Cell(fill=materials[i]) + universes.append(openmc.Universe(cells=[cell])) + + # Make a list of randomized material idx assignments for the stochastic slab + assignments = list(range(num_materials)) * num_repeats + random.seed(42) + random.shuffle(assignments) + + # Create a list of the (randomized) universe assignments to be used + # when defining the problem lattice. + lattice_entries = [universes[m] for m in assignments] + + # Create the RectLattice for the 1D material variation in x. + lattice = openmc.RectLattice() + lattice.pitch = (cell_thickness, total_width, total_width) + lattice.lower_left = (0.0, 0.0, 0.0) + lattice.universes = [[lattice_entries]] + lattice.outer = universes[0] + + # Define the six outer surfaces with reflective boundary conditions + rpp = openmc.model.RectangularParallelepiped( + 0.0, total_width, 0.0, total_width, 0.0, total_width, + boundary_type='reflective' + ) + + # Create an outer cell that fills with the lattice. + outer_cell = openmc.Cell(fill=lattice, region=-rpp) + + # Build the geometry + geometry = openmc.Geometry([outer_cell]) + + # Define the spatial distribution that covers the full cubic domain + box = openmc.stats.Box(*outer_cell.bounding_box) + + return geometry, box + + def _generate_stochastic_slab_mgxs(self, groups, nparticles, mgxs_path, correction) -> None: + """Generate MGXS assuming a stochastic "sandwich" of materials in a layered + slab geometry. While geometry-specific spatial shielding effects are not + captured, this method can be useful when the geometry has materials only + found far from the source region that the "material_wise" method would + not be capable of generating cross sections for. Conversely, this method + will generate cross sections for all materials in the problem regardless + of type. If this is a fixed source problem, a discrete source is used to + sample particles, with an equal strength spread across each of the + energy groups. + + Parameters + ---------- + groups : openmc.mgxs.EnergyGroups + Energy group structure for the MGXS. + mgxs_path : path-like + Filename for the MGXS HDF5 file. + """ + openmc.reset_auto_ids() + model = openmc.Model() + model.materials = self.materials + + # Settings + model.settings.batches = 200 + model.settings.inactive = 100 + model.settings.particles = nparticles + model.settings.output = {'summary': True, 'tallies': False} + model.settings.run_mode = self.settings.run_mode + + # Stochastic slab geometry + model.geometry, spatial_distribution = Model._create_stochastic_slab_geometry( + model.materials) + + # Make a discrete source that is uniform over the bins of the group structure + n_groups = groups.num_groups + midpoints = [] + strengths = [] + for i in range(n_groups): + bounds = groups.get_group_bounds(i+1) + midpoints.append((bounds[0] + bounds[1]) / 2.0) + strengths.append(1.0) + + energy_distribution = openmc.stats.Discrete(x=midpoints, p=strengths) + model.settings.source = [openmc.IndependentSource( + space=spatial_distribution, energy=energy_distribution, strength=1.0)] + + model.settings.output = {'summary': True, 'tallies': False} + + # Add MGXS Tallies + + # Initialize MGXS library with a finished OpenMC geometry object + mgxs_lib = openmc.mgxs.Library(model.geometry) + + # Pick energy group structure + mgxs_lib.energy_groups = groups + + # Disable transport correction + mgxs_lib.correction = correction + + # Specify needed cross sections for random ray + if correction == 'P0': + mgxs_lib.mgxs_types = ['nu-transport', 'absorption', 'nu-fission', 'fission', + 'consistent nu-scatter matrix', 'multiplicity matrix', 'chi'] + elif correction is None: + mgxs_lib.mgxs_types = ['total', 'absorption', 'nu-fission', 'fission', + 'consistent nu-scatter matrix', 'multiplicity matrix', 'chi'] + + # Specify a "cell" domain type for the cross section tally filters + mgxs_lib.domain_type = "material" + + # Specify the cell domains over which to compute multi-group cross sections + mgxs_lib.domains = model.geometry.get_all_materials().values() + + # Do not compute cross sections on a nuclide-by-nuclide basis + mgxs_lib.by_nuclide = False + + # Check the library - if no errors are raised, then the library is satisfactory. + mgxs_lib.check_library_for_openmc_mgxs() + + # Construct all tallies needed for the multi-group cross section library + mgxs_lib.build_library() + + # Create a "tallies.xml" file for the MGXS Library + mgxs_lib.add_to_tallies_file(model.tallies, merge=True) + + # Run + statepoint_filename = model.run() + + # Load MGXS + with openmc.StatePoint(statepoint_filename) as sp: + mgxs_lib.load_from_statepoint(sp) + + names = [mat.name for mat in mgxs_lib.domains] + + # Create a MGXS File which can then be written to disk + mgxs_file = mgxs_lib.create_mg_library(xs_type='macro', xsdata_names=names) + mgxs_file.export_to_hdf5(mgxs_path) + + def _generate_material_wise_mgxs(self, groups, nparticles, mgxs_path, correction) -> None: + """Generate a material-wise MGXS library for the model by running the + original continuous energy OpenMC simulation of the full material + geometry and source, and tally MGXS data for each material. This method + accurately conserves reaction rates totaled over the entire simulation + domain. However, when the geometry has materials only found far from the + source region, it is possible the Monte Carlo solver may not be able to + score any tallies to these material types, thus resulting in zero cross + section values for these materials. For such cases, the "stochastic + slab" method may be more appropriate. + + Parameters + ---------- + groups : openmc.mgxs.EnergyGroups + Energy group structure for the MGXS. + mgxs_path : str + Filename for the MGXS HDF5 file. + """ + openmc.reset_auto_ids() + model = copy.deepcopy(self) + model.tallies = openmc.Tallies() + + # Settings + model.settings.batches = 200 + model.settings.inactive = 100 + model.settings.particles = nparticles + model.settings.output = {'summary': True, 'tallies': False} + + # Add MGXS Tallies + + # Initialize MGXS library with a finished OpenMC geometry object + mgxs_lib = openmc.mgxs.Library(model.geometry) + + # Pick energy group structure + mgxs_lib.energy_groups = groups + + # Disable transport correction + mgxs_lib.correction = correction + + # Specify needed cross sections for random ray + if correction == 'P0': + mgxs_lib.mgxs_types = [ + 'nu-transport', 'absorption', 'nu-fission', 'fission', + 'consistent nu-scatter matrix', 'multiplicity matrix', 'chi' + ] + elif correction is None: + mgxs_lib.mgxs_types = [ + 'total', 'absorption', 'nu-fission', 'fission', + 'consistent nu-scatter matrix', 'multiplicity matrix', 'chi' + ] + + # Specify a "cell" domain type for the cross section tally filters + mgxs_lib.domain_type = "material" + + # Specify the cell domains over which to compute multi-group cross sections + mgxs_lib.domains = model.geometry.get_all_materials().values() + + # Do not compute cross sections on a nuclide-by-nuclide basis + mgxs_lib.by_nuclide = False + + # Check the library - if no errors are raised, then the library is satisfactory. + mgxs_lib.check_library_for_openmc_mgxs() + + # Construct all tallies needed for the multi-group cross section library + mgxs_lib.build_library() + + # Create a "tallies.xml" file for the MGXS Library + mgxs_lib.add_to_tallies_file(model.tallies, merge=True) + + # Run + statepoint_filename = model.run() + + # Load MGXS + with openmc.StatePoint(statepoint_filename) as sp: + mgxs_lib.load_from_statepoint(sp) + + names = [mat.name for mat in mgxs_lib.domains] + + # Create a MGXS File which can then be written to disk + mgxs_file = mgxs_lib.create_mg_library( + xs_type='macro', xsdata_names=names) + mgxs_file.export_to_hdf5(mgxs_path) + + def convert_to_multigroup(self, method="material_wise", groups='CASMO-2', + nparticles=2000, overwrite_mgxs_library=False, + mgxs_path: PathLike = "mgxs.h5", correction=None): + """Convert all materials from continuous energy to multigroup. + + If no MGXS data library file is found, generate one using one or more + continuous energy Monte Carlo simulations. + + Parameters + ---------- + method : {"material_wise", "stochastic_slab", "infinite_medium"}, optional + Method to generate the MGXS. + groups : openmc.mgxs.EnergyGroups or str, optional + Energy group structure for the MGXS or the name of the group + structure (based on keys from openmc.mgxs.GROUP_STRUCTURES). + mgxs_path : str, optional + Filename of the mgxs.h5 library file. + correction : str, optional + Transport correction to apply to the MGXS. Options are None and + "P0". + """ + if isinstance(groups, str): + groups = openmc.mgxs.EnergyGroups(groups) + + # Make sure all materials have a name, and that the name is a valid HDF5 + # dataset name + for material in self.materials: + if material.name is None: + material.name = f"material {material.id}" + material.name = re.sub(r'[^a-zA-Z0-9]', '_', material.name) + + # If needed, generate the needed MGXS data library file + if not Path(mgxs_path).is_file() or overwrite_mgxs_library: + if method == "infinite_medium": + self._generate_infinite_medium_mgxs( + groups, nparticles, mgxs_path, correction) + elif method == "material_wise": + self._generate_material_wise_mgxs( + groups, nparticles, mgxs_path, correction) + elif method == "stochastic_slab": + self._generate_stochastic_slab_mgxs( + groups, nparticles, mgxs_path, correction) + else: + raise ValueError( + f'MGXS generation method "{method}" not recognized') + else: + print(f'Existing MGXS library file "{mgxs_path}" will be used') + + # Convert all continuous energy materials to multigroup + self.materials.cross_sections = mgxs_path + for material in self.materials: + material.set_density('macro', 1.0) + material._nuclides = [] + material._sab = [] + material.add_macroscopic(material.name) + + self.settings.energy_mode = 'multi-group' + + def convert_to_random_ray(self): + """Convert a multigroup model to use random ray. + + This method determines values for the needed settings and adds them to + the settings.random_ray dictionary so as to enable random ray mode. The + settings that are populated are: + + - 'ray_source' (openmc.IndependentSource): Where random ray starting + points are sampled from. + - 'distance_inactive' (float): The "dead zone" distance at the beginning + of the ray. + - 'distance_active' (float): The "active" distance of the ray + - 'particles' (int): Number of rays to simulate + + The method will determine reasonable defaults for each of the above + variables based on analysis of the model's geometry. The function will + have no effect if the random ray dictionary is already defined in the + model settings. + """ + # If the random ray dictionary is already set, don't overwrite it + if self.settings.random_ray: + warnings.warn("Random ray conversion skipped as " + "settings.random_ray dictionary is already set.") + return + + if self.settings.energy_mode != 'multi-group': + raise ValueError( + "Random ray conversion failed: energy mode must be " + "'multi-group'. Use convert_to_multigroup() first." + ) + + # Helper function for detecting infinity + def _replace_infinity(value): + if np.isinf(value): + return 1.0 if value > 0 else -1.0 + return value + + # Get a bounding box for sampling rays. We can utilize the geometry's bounding box + # though for 2D problems we need to detect the infinities and replace them with an + # arbitrary finite value. + bounding_box = self.geometry.bounding_box + lower_left = [_replace_infinity(v) for v in bounding_box.lower_left] + upper_right = [_replace_infinity(v) for v in bounding_box.upper_right] + uniform_dist_ray = openmc.stats.Box(lower_left, upper_right) + rr_source = openmc.IndependentSource(space=uniform_dist_ray) + self.settings.random_ray['ray_source'] = rr_source + + # For the dead zone and active length, a reasonable guess is the larger of either: + # 1) The maximum chord length through the geometry (as defined by its bounding box) + # 2) 30 cm + # Then, set the active length to be 5x longer than the dead zone length, for the sake of efficiency. + chord_length = np.array(upper_right) - np.array(lower_left) + max_length = max(np.linalg.norm(chord_length), 30.0) + + self.settings.random_ray['distance_inactive'] = max_length + self.settings.random_ray['distance_active'] = 5 * max_length + + # Take a wild guess as to how many rays are needed + self.settings.particles = 2 * int(max_length) diff --git a/openmc/settings.py b/openmc/settings.py index 17c24ba81..a2bf51a51 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -187,6 +187,17 @@ class Settings: or openmc.Universe. The mesh will be applied to the listed domains to subdivide source regions so as to improve accuracy and/or conform with tally meshes. + :diagonal_stabilization_rho: + The rho factor for use with diagonal stabilization. This technique is + applied when negative diagonal (in-group) elements are detected in + the scattering matrix of input MGXS data, which is a common feature + of transport corrected MGXS data. The default is 1.0, which ensures + no negative diagonal elements are present in the iteration matrix and + thus stabilizes the simulation. A value of 0.0 will disable diagonal + stabilization. Values between 0.0 and 1.0 will apply a degree of + stabilization, which may be desirable as stronger diagonal stabilization + also tends to dampen the convergence rate of the solver, thus requiring + more iterations to converge. .. versionadded:: 0.15.0 resonance_scattering : dict @@ -1177,6 +1188,10 @@ class Settings: elif key == 'sample_method': cv.check_value('sample method', value, ('prng', 'halton')) + elif key == 'diagonal_stabilization_rho': + cv.check_type('diagonal stabilization rho', value, Real) + cv.check_greater_than('diagonal stabilization rho', + value, 0.0, True) else: raise ValueError(f'Unable to set random ray to "{key}" which is ' 'unsupported by OpenMC') @@ -2018,7 +2033,7 @@ class Settings: if elem is not None: self.random_ray = {} for child in elem: - if child.tag in ('distance_inactive', 'distance_active'): + if child.tag in ('distance_inactive', 'distance_active', 'diagonal_stabilization_rho'): self.random_ray[child.tag] = float(child.text) elif child.tag == 'source': source = SourceBase.from_xml_element(child) diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 33651574f..ff8e5d232 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -30,6 +30,7 @@ RandomRayVolumeEstimator FlatSourceDomain::volume_estimator_ { RandomRayVolumeEstimator::HYBRID}; bool FlatSourceDomain::volume_normalized_flux_tallies_ {false}; bool FlatSourceDomain::adjoint_ {false}; +double FlatSourceDomain::diagonal_stabilization_rho_ {1.0}; std::unordered_map>> FlatSourceDomain::mesh_domain_map_; @@ -1106,6 +1107,11 @@ void FlatSourceDomain::flatten_xs() double sigma_s = m.get_xs(MgxsType::NU_SCATTER, g_in, &g_out, NULL, NULL, t, a); sigma_s_.push_back(sigma_s); + // For transport corrected XS data, diagonal elements may be negative. + // In this case, set a flag to enable transport stabilization for the + // simulation. + if (g_out == g_in && sigma_s < 0.0) + is_transport_stabilization_needed_ = true; } } else { sigma_t_.push_back(0); @@ -1427,4 +1433,53 @@ void FlatSourceDomain::finalize_discovered_source_regions() discovered_source_regions_.clear(); } +// This is the "diagonal stabilization" technique developed by Gunow et al. in: +// +// Geoffrey Gunow, Benoit Forget, Kord Smith, Stabilization of multi-group +// neutron transport with transport-corrected cross-sections, Annals of Nuclear +// Energy, Volume 126, 2019, Pages 211-219, ISSN 0306-4549, +// https://doi.org/10.1016/j.anucene.2018.10.036. +void FlatSourceDomain::apply_transport_stabilization() +{ + // Don't do anything if all in-group scattering + // cross sections are positive + if (!is_transport_stabilization_needed_) { + return; + } + + // Apply the stabilization factor to all source elements +#pragma omp parallel for + for (int64_t sr = 0; sr < n_source_regions(); sr++) { + int material = source_regions_.material(sr); + if (material == MATERIAL_VOID) { + continue; + } + for (int g = 0; g < negroups_; g++) { + // Only apply stabilization if the diagonal (in-group) scattering XS is + // negative + double sigma_s = + sigma_s_[material * negroups_ * negroups_ + g * negroups_ + g]; + if (sigma_s < 0.0) { + double sigma_t = sigma_t_[material * negroups_ + g]; + double phi_new = source_regions_.scalar_flux_new(sr, g); + double phi_old = source_regions_.scalar_flux_old(sr, g); + + // Equation 18 in the above Gunow et al. 2019 paper. For a default + // rho of 1.0, this ensures there are no negative diagonal elements + // in the iteration matrix. A lesser rho could be used (or exposed + // as a user input parameter) to reduce the negative impact on + // convergence rate though would need to be experimentally tested to see + // if it doesn't become unstable. rho = 1.0 is good as it gives the + // highest assurance of stability, and the impacts on convergence rate + // are pretty mild. + double D = diagonal_stabilization_rho_ * sigma_s / sigma_t; + + // Equation 16 in the above Gunow et al. 2019 paper + source_regions_.scalar_flux_new(sr, g) = + (phi_new - D * phi_old) / (1.0 - D); + } + } + } +} + } // namespace openmc diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index 36517bee4..60b00a5be 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -478,6 +478,9 @@ void RandomRaySimulation::simulate() // Add source to scalar flux, compute number of FSR hits int64_t n_hits = domain_->add_source_to_scalar_flux(); + // Apply transport stabilization factors + domain_->apply_transport_stabilization(); + if (settings::run_mode == RunMode::EIGENVALUE) { // Compute random ray k-eff k_eff_ = domain_->compute_k_eff(k_eff_); @@ -577,17 +580,23 @@ void RandomRaySimulation::print_results_random_ray( header("Simulation Statistics", 4); fmt::print( " Total Iterations = {}\n", settings::n_batches); - fmt::print(" Flat Source Regions (FSRs) = {}\n", n_source_regions); fmt::print( - " FSRs Containing External Sources = {}\n", n_external_source_regions); + " Number of Rays per Iteration = {}\n", settings::n_particles); + fmt::print(" Inactive Distance = {} cm\n", + RandomRay::distance_inactive_); + fmt::print(" Active Distance = {} cm\n", + RandomRay::distance_active_); + fmt::print(" Source Regions (SRs) = {}\n", n_source_regions); + fmt::print( + " SRs Containing External Sources = {}\n", n_external_source_regions); fmt::print(" Total Geometric Intersections = {:.4e}\n", static_cast(total_geometric_intersections)); fmt::print(" Avg per Iteration = {:.4e}\n", static_cast(total_geometric_intersections) / settings::n_batches); - fmt::print(" Avg per Iteration per FSR = {:.2f}\n", + fmt::print(" Avg per Iteration per SR = {:.2f}\n", static_cast(total_geometric_intersections) / static_cast(settings::n_batches) / n_source_regions); - fmt::print(" Avg FSR Miss Rate per Iteration = {:.4f}%\n", avg_miss_rate); + fmt::print(" Avg SR Miss Rate per Iteration = {:.4f}%\n", avg_miss_rate); fmt::print(" Energy Groups = {}\n", negroups); fmt::print( " Total Integrations = {:.4e}\n", total_integrations); @@ -613,6 +622,33 @@ void RandomRaySimulation::print_results_random_ray( std::string adjoint_true = (FlatSourceDomain::adjoint_) ? "ON" : "OFF"; fmt::print(" Adjoint Flux Mode = {}\n", adjoint_true); + std::string shape; + switch (RandomRay::source_shape_) { + case RandomRaySourceShape::FLAT: + shape = "Flat"; + break; + case RandomRaySourceShape::LINEAR: + shape = "Linear"; + break; + case RandomRaySourceShape::LINEAR_XY: + shape = "Linear XY"; + break; + default: + fatal_error("Invalid random ray source shape"); + } + fmt::print(" Source Shape = {}\n", shape); + std::string sample_method = + (RandomRay::sample_method_ == RandomRaySampleMethod::PRNG) ? "PRNG" + : "Halton"; + fmt::print(" Sample Method = {}\n", sample_method); + + if (domain_->is_transport_stabilization_needed_) { + fmt::print(" Transport XS Stabilization Used = YES (rho = {:.3f})\n", + FlatSourceDomain::diagonal_stabilization_rho_); + } else { + fmt::print(" Transport XS Stabilization Used = NO\n"); + } + header("Timing Statistics", 4); show_time("Total time for initialization", time_initialize.elapsed()); show_time("Reading cross sections", time_read_xs.elapsed(), 1); diff --git a/src/settings.cpp b/src/settings.cpp index 49b6590ec..53fcdec38 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -345,6 +345,15 @@ void get_run_parameters(pugi::xml_node node_base) } } } + if (check_for_node(random_ray_node, "diagonal_stabilization_rho")) { + FlatSourceDomain::diagonal_stabilization_rho_ = std::stod( + get_node_value(random_ray_node, "diagonal_stabilization_rho")); + if (FlatSourceDomain::diagonal_stabilization_rho_ < 0.0 || + FlatSourceDomain::diagonal_stabilization_rho_ > 1.0) { + fatal_error("Random ray diagonal stabilization rho factor must be " + "between 0 and 1"); + } + } } } diff --git a/tests/regression_tests/mgxs_library_ce_to_mg/test.py b/tests/regression_tests/mgxs_library_ce_to_mg/test.py index 4dff67323..48a715997 100644 --- a/tests/regression_tests/mgxs_library_ce_to_mg/test.py +++ b/tests/regression_tests/mgxs_library_ce_to_mg/test.py @@ -40,8 +40,8 @@ class MGXSTestHarness(PyAPITestHarness): # Build MG Inputs # Get data needed to execute Library calculations. - sp = openmc.StatePoint(self._sp_name) - self.mgxs_lib.load_from_statepoint(sp) + with openmc.StatePoint(self._sp_name) as sp: + self.mgxs_lib.load_from_statepoint(sp) self._model.mgxs_file, self._model.materials, \ self._model.geometry = self.mgxs_lib.create_mg_mode() @@ -55,11 +55,6 @@ class MGXSTestHarness(PyAPITestHarness): self._model.export_to_model_xml() self._model.mgxs_file.export_to_hdf5() - # Enforce closing statepoint and summary files so HDF5 - # does not throw an error during the next OpenMC execution - sp._f.close() - sp._summary._f.close() - # Re-run MG mode. if config['mpi']: mpi_args = [config['mpiexec'], '-n', config['mpi_np']] diff --git a/tests/regression_tests/mgxs_library_ce_to_mg_nuclides/test.py b/tests/regression_tests/mgxs_library_ce_to_mg_nuclides/test.py index ee0cdf0d9..a77ad2429 100644 --- a/tests/regression_tests/mgxs_library_ce_to_mg_nuclides/test.py +++ b/tests/regression_tests/mgxs_library_ce_to_mg_nuclides/test.py @@ -40,8 +40,8 @@ class MGXSTestHarness(PyAPITestHarness): # Build MG Inputs # Get data needed to execute Library calculations. - sp = openmc.StatePoint(self._sp_name) - self.mgxs_lib.load_from_statepoint(sp) + with openmc.StatePoint(self._sp_name) as sp: + self.mgxs_lib.load_from_statepoint(sp) self._model.mgxs_file, self._model.materials, \ self._model.geometry = self.mgxs_lib.create_mg_mode() @@ -55,11 +55,6 @@ class MGXSTestHarness(PyAPITestHarness): self._model.export_to_model_xml() self._model.mgxs_file.export_to_hdf5() - # Enforce closing statepoint and summary files so HDF5 - # does not throw an error during the next OpenMC execution - sp._f.close() - sp._summary._f.close() - # Re-run MG mode. if config['mpi']: mpi_args = [config['mpiexec'], '-n', config['mpi_np']] diff --git a/tests/regression_tests/random_ray_auto_convert/__init__.py b/tests/regression_tests/random_ray_auto_convert/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_auto_convert/infinite_medium/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert/infinite_medium/inputs_true.dat new file mode 100644 index 000000000..02be4ea7a --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert/infinite_medium/inputs_true.dat @@ -0,0 +1,64 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + + + -0.63 -0.63 -1 0.63 0.63 1 + + + true + + + multi-group + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 + 150.0 + + + + + + linear + + + 2 2 + -0.63 -0.63 + 0.63 0.63 + + + diff --git a/tests/regression_tests/random_ray_auto_convert/infinite_medium/results_true.dat b/tests/regression_tests/random_ray_auto_convert/infinite_medium/results_true.dat new file mode 100644 index 000000000..a0d7fc545 --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert/infinite_medium/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +7.796949E-01 1.055316E-02 diff --git a/tests/regression_tests/random_ray_auto_convert/material_wise/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert/material_wise/inputs_true.dat new file mode 100644 index 000000000..02be4ea7a --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert/material_wise/inputs_true.dat @@ -0,0 +1,64 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + + + -0.63 -0.63 -1 0.63 0.63 1 + + + true + + + multi-group + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 + 150.0 + + + + + + linear + + + 2 2 + -0.63 -0.63 + 0.63 0.63 + + + diff --git a/tests/regression_tests/random_ray_auto_convert/material_wise/results_true.dat b/tests/regression_tests/random_ray_auto_convert/material_wise/results_true.dat new file mode 100644 index 000000000..ebf510cfc --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert/material_wise/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +7.375068E-01 7.015839E-03 diff --git a/tests/regression_tests/random_ray_auto_convert/stochastic_slab/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert/stochastic_slab/inputs_true.dat new file mode 100644 index 000000000..02be4ea7a --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert/stochastic_slab/inputs_true.dat @@ -0,0 +1,64 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + + + -0.63 -0.63 -1 0.63 0.63 1 + + + true + + + multi-group + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 + 150.0 + + + + + + linear + + + 2 2 + -0.63 -0.63 + 0.63 0.63 + + + diff --git a/tests/regression_tests/random_ray_auto_convert/stochastic_slab/results_true.dat b/tests/regression_tests/random_ray_auto_convert/stochastic_slab/results_true.dat new file mode 100644 index 000000000..2f444fad3 --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert/stochastic_slab/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +7.499679E-01 8.107614E-03 diff --git a/tests/regression_tests/random_ray_auto_convert/test.py b/tests/regression_tests/random_ray_auto_convert/test.py new file mode 100644 index 000000000..fa7f2f17f --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert/test.py @@ -0,0 +1,54 @@ +import os + +import openmc +from openmc.examples import pwr_pin_cell +from openmc import RegularMesh +from openmc.utility_funcs import change_directory +import pytest + +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) + + +@pytest.mark.parametrize("method", ["material_wise", "stochastic_slab", "infinite_medium"]) +def test_random_ray_auto_convert(method): + with change_directory(method): + openmc.reset_auto_ids() + + # Start with a normal continuous energy model + model = pwr_pin_cell() + + # Convert to a multi-group model + model.convert_to_multigroup( + method=method, groups='CASMO-2', nparticles=30, + overwrite_mgxs_library=False, mgxs_path="mgxs.h5" + ) + + # Convert to a random ray model + model.convert_to_random_ray() + + # Set the number of particles + model.settings.particles = 100 + + # Overlay a basic 2x2 mesh + n = 2 + mesh = RegularMesh() + mesh.dimension = (n, n) + bbox = model.geometry.bounding_box + mesh.lower_left = (bbox.lower_left[0], bbox.lower_left[1]) + mesh.upper_right = (bbox.upper_right[0], bbox.upper_right[1]) + model.settings.random_ray['source_region_meshes'] = [ + (mesh, [model.geometry.root_universe])] + + # Set the source shape to linear + model.settings.random_ray['source_shape'] = 'linear' + + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/__init__.py b/tests/regression_tests/random_ray_diagonal_stabilization/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat b/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat new file mode 100644 index 000000000..4d4ed76dc --- /dev/null +++ b/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat @@ -0,0 +1,65 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 20 + 15 + + + -0.63 -0.63 -1 0.63 0.63 1 + + + true + + + multi-group + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 + 150.0 + + + + + + linear + 0.5 + + + 2 2 + -0.63 -0.63 + 0.63 0.63 + + + diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/results_true.dat b/tests/regression_tests/random_ray_diagonal_stabilization/results_true.dat new file mode 100644 index 000000000..c6ce6aab9 --- /dev/null +++ b/tests/regression_tests/random_ray_diagonal_stabilization/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +7.152917E-01 1.430362E-02 diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/test.py b/tests/regression_tests/random_ray_diagonal_stabilization/test.py new file mode 100644 index 000000000..c7a1c9f7c --- /dev/null +++ b/tests/regression_tests/random_ray_diagonal_stabilization/test.py @@ -0,0 +1,61 @@ +import os + +from openmc.examples import pwr_pin_cell +from openmc import RegularMesh + +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_diagonal_stabilization(): + # Start with a normal continuous energy model + model = pwr_pin_cell() + + # Convert to a multi-group model, with 70 group XS + # and transport correction enabled. This will generate + # MGXS data with some negatives on the diagonal, in order + # to trigger diagonal correction. + model.convert_to_multigroup( + method='material_wise', groups='CASMO-70', nparticles=30, + overwrite_mgxs_library=True, mgxs_path="mgxs.h5", correction='P0' + ) + + # Convert to a random ray model + model.convert_to_random_ray() + + # Set the number of particles + model.settings.particles = 100 + + # Overlay a basic 2x2 mesh + n = 2 + mesh = RegularMesh() + mesh.dimension = (n, n) + bbox = model.geometry.bounding_box + mesh.lower_left = (bbox.lower_left[0], bbox.lower_left[1]) + mesh.upper_right = (bbox.upper_right[0], bbox.upper_right[1]) + model.settings.random_ray['source_region_meshes'] = [ + (mesh, [model.geometry.root_universe])] + + # Set the source shape to linear + model.settings.random_ray['source_shape'] = 'linear' + + # Explicitly set the diagonal stabilization rho (default is otherwise 1.0). + # Note that if we set this to 0.0 (thus distabling stabilization), the + # problem should fail due to instability, so this is actually a good test + # problem. + model.settings.random_ray['diagonal_stabilization_rho'] = 0.5 + + # If rho was 0.0, the instability would cause failure after iteration 14, + # so we go a little past that. + model.settings.inactive = 15 + model.settings.batches = 20 + + harness = MGXSTestHarness('statepoint.20.h5', model) + harness.main() diff --git a/tools/ci/gha-install-dagmc.sh b/tools/ci/gha-install-dagmc.sh index 82759c9bc..8d0648a50 100755 --- a/tools/ci/gha-install-dagmc.sh +++ b/tools/ci/gha-install-dagmc.sh @@ -3,7 +3,7 @@ set -ex # MOAB Variables -MOAB_BRANCH='Version5.1.0' +MOAB_BRANCH='5.5.1' MOAB_REPO='https://bitbucket.org/fathomteam/moab/' MOAB_INSTALL_DIR=$HOME/MOAB/ @@ -19,7 +19,7 @@ cd $HOME mkdir MOAB && cd MOAB git clone -b $MOAB_BRANCH $MOAB_REPO mkdir build && cd build -cmake ../moab -DENABLE_HDF5=ON -DENABLE_NETCDF=ON -DBUILD_SHARED_LIBS=ON -DCMAKE_INSTALL_PREFIX=$MOAB_INSTALL_DIR -DENABLE_BLASLAPACK=OFF +cmake ../moab -DENABLE_HDF5=ON -DENABLE_NETCDF=ON -DBUILD_SHARED_LIBS=ON -DCMAKE_INSTALL_PREFIX=$MOAB_INSTALL_DIR make -j && make -j install rm -rf $HOME/MOAB/moab $HOME/MOAB/build From 07f5334616c2ca92ad0a04dfa8c900e080509f70 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Wed, 2 Apr 2025 00:40:07 -0500 Subject: [PATCH 303/671] Random Ray Point Source Locator (#3360) Co-authored-by: Paul Romano --- docs/source/methods/random_ray.rst | 3 +- docs/source/usersguide/random_ray.rst | 9 +- .../openmc/random_ray/flat_source_domain.h | 8 +- src/random_ray/flat_source_domain.cpp | 119 ++++++-- src/random_ray/random_ray_simulation.cpp | 23 +- .../__init__.py | 0 .../inputs_true.dat | 253 ++++++++++++++++++ .../results_true.dat | 9 + .../random_ray_point_source_locator/test.py | 44 +++ 9 files changed, 435 insertions(+), 33 deletions(-) create mode 100644 tests/regression_tests/random_ray_point_source_locator/__init__.py create mode 100644 tests/regression_tests/random_ray_point_source_locator/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_point_source_locator/results_true.dat create mode 100644 tests/regression_tests/random_ray_point_source_locator/test.py diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index eb22b0544..41f95f0a9 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -1052,7 +1052,8 @@ random ray and Monte Carlo, however. regions. Thus, in the OpenMC implementation of random ray, particle sources are restricted to being volumetric and isotropic, although different energy spectrums are supported. Fixed sources can be applied to specific materials, - cells, or universes. + cells, or universes. Point sources are "smeared" to fill the volume of the + source region that contains the point source coordinate. - **Inactive batches:** In Monte Carlo, use of a fixed source implies that all batches are active batches, as there is no longer a need to develop a fission diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index ce9821196..138ae910c 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -919,9 +919,12 @@ Monte Carlo solver. Currently, all of the following conditions must be met for the particle source to be valid in random ray mode: -- One or more domain ids must be specified that indicate which cells, universes, - or materials the source applies to. This implicitly limits the source type to - being volumetric. This is specified via the ``domains`` constraint placed on the +- Either a point source must be used, or a domain constraint must be specified + that indicates which cells, universes, or materials the source applies to. In + either case, this implicitly limits the source type to being volumetric, as + even in the point source case the source will be "smeared" throughout the + source region that contains the point source coordinate. A source domain is + specified via the ``domains`` constraint placed on the :class:`openmc.IndependentSource` Python class. - The source must be isotropic (default for a source) - The source must use a discrete (i.e., multigroup) energy distribution. The diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index 656b0354a..9fdd55875 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -131,6 +131,12 @@ public: std::unordered_map source_region_map_; + // Map that relates a SourceRegionKey to the external source index. This map + // is used to check if there are any point sources within a subdivided source + // region at the time it is discovered. + std::unordered_map + point_source_map_; + // If transport corrected MGXS data is being used, there may be negative // in-group scattering cross sections that can result in instability in MOC // and random ray if used naively. This flag enables a stabilization @@ -141,7 +147,7 @@ protected: //---------------------------------------------------------------------------- // Methods void apply_external_source_to_source_region( - Discrete* discrete, double strength_factor, int64_t sr); + Discrete* discrete, double strength_factor, SourceRegionHandle& srh); void apply_external_source_to_cell_instances(int32_t i_cell, Discrete* discrete, double strength_factor, int target_material_id, const vector& instances); diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index ff8e5d232..8ab30abf2 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -946,17 +946,16 @@ void FlatSourceDomain::output_to_vtk() const } void FlatSourceDomain::apply_external_source_to_source_region( - Discrete* discrete, double strength_factor, int64_t sr) + Discrete* discrete, double strength_factor, SourceRegionHandle& srh) { - source_regions_.external_source_present(sr) = 1; + srh.external_source_present() = 1; const auto& discrete_energies = discrete->x(); const auto& discrete_probs = discrete->prob(); for (int i = 0; i < discrete_energies.size(); i++) { int g = data::mg.get_group_index(discrete_energies[i]); - source_regions_.external_source(sr, g) += - discrete_probs[i] * strength_factor; + srh.external_source(g) += discrete_probs[i] * strength_factor; } } @@ -980,8 +979,9 @@ void FlatSourceDomain::apply_external_source_to_cell_instances(int32_t i_cell, if (target_material_id == C_NONE || cell_material_id == target_material_id) { int64_t source_region = source_region_offsets_[i_cell] + j; - apply_external_source_to_source_region( - discrete, strength_factor, source_region); + SourceRegionHandle srh = + source_regions_.get_source_region_handle(source_region); + apply_external_source_to_source_region(discrete, strength_factor, srh); } } } @@ -1023,34 +1023,88 @@ void FlatSourceDomain::convert_external_sources() { // Loop over external sources for (int es = 0; es < model::external_sources.size(); es++) { + + // Extract source information Source* s = model::external_sources[es].get(); IndependentSource* is = dynamic_cast(s); Discrete* energy = dynamic_cast(is->energy()); const std::unordered_set& domain_ids = is->domain_ids(); - double strength_factor = is->strength(); - if (is->domain_type() == Source::DomainType::MATERIAL) { - for (int32_t material_id : domain_ids) { - for (int i_cell = 0; i_cell < model::cells.size(); i_cell++) { - apply_external_source_to_cell_and_children( - i_cell, energy, strength_factor, material_id); + // If there is no domain constraint specified, then this must be a point + // source. In this case, we need to find the source region that contains the + // point source and apply or relate it to the external source. + if (is->domain_ids().size() == 0) { + + // Extract the point source coordinate and find the base source region at + // that point + auto sp = dynamic_cast(is->space()); + GeometryState gs; + gs.r() = sp->r(); + gs.r_last() = sp->r(); + gs.u() = {1.0, 0.0, 0.0}; + bool found = exhaustive_find_cell(gs); + if (!found) { + fatal_error(fmt::format("Could not find cell containing external " + "point source at {}", + sp->r())); + } + int i_cell = gs.lowest_coord().cell; + int64_t sr = source_region_offsets_[i_cell] + gs.cell_instance(); + + if (RandomRay::mesh_subdivision_enabled_) { + // If mesh subdivision is enabled, we need to determine which subdivided + // mesh bin the point source coordinate is in as well + int mesh_idx = source_regions_.mesh(sr); + int mesh_bin; + if (mesh_idx == C_NONE) { + mesh_bin = 0; + } else { + mesh_bin = model::meshes[mesh_idx]->get_bin(gs.r()); } + // With the source region and mesh bin known, we can use the + // accompanying SourceRegionKey as a key into a map that stores the + // corresponding external source index for the point source. Notably, we + // do not actually apply the external source to any source regions here, + // as if mesh subdivision is enabled, they haven't actually been + // discovered & initilized yet. When discovered, they will read from the + // point_source_map to determine if there are any point source terms + // that should be applied. + SourceRegionKey key {sr, mesh_bin}; + point_source_map_[key] = es; + } else { + // If we are not using mesh subdivision, we can apply the external + // source directly to the source region as we do for volumetric domain + // constraint sources. + SourceRegionHandle srh = source_regions_.get_source_region_handle(sr); + apply_external_source_to_source_region(energy, strength_factor, srh); } - } else if (is->domain_type() == Source::DomainType::CELL) { - for (int32_t cell_id : domain_ids) { - int32_t i_cell = model::cell_map[cell_id]; - apply_external_source_to_cell_and_children( - i_cell, energy, strength_factor, C_NONE); - } - } else if (is->domain_type() == Source::DomainType::UNIVERSE) { - for (int32_t universe_id : domain_ids) { - int32_t i_universe = model::universe_map[universe_id]; - Universe& universe = *model::universes[i_universe]; - for (int32_t i_cell : universe.cells_) { + + } else { + // If not a point source, then use the volumetric domain constraints to + // determine which source regions to apply the external source to. + if (is->domain_type() == Source::DomainType::MATERIAL) { + for (int32_t material_id : domain_ids) { + for (int i_cell = 0; i_cell < model::cells.size(); i_cell++) { + apply_external_source_to_cell_and_children( + i_cell, energy, strength_factor, material_id); + } + } + } else if (is->domain_type() == Source::DomainType::CELL) { + for (int32_t cell_id : domain_ids) { + int32_t i_cell = model::cell_map[cell_id]; apply_external_source_to_cell_and_children( i_cell, energy, strength_factor, C_NONE); } + } else if (is->domain_type() == Source::DomainType::UNIVERSE) { + for (int32_t universe_id : domain_ids) { + int32_t i_universe = model::universe_map[universe_id]; + Universe& universe = *model::universes[i_universe]; + for (int32_t i_cell : universe.cells_) { + apply_external_source_to_cell_and_children( + i_cell, energy, strength_factor, C_NONE); + } + } } } } // End loop over external sources @@ -1399,6 +1453,25 @@ SourceRegionHandle FlatSourceDomain::get_subdivided_source_region_handle( sr_key, {base_source_regions_.get_source_region_handle(sr), sr}); discovered_source_regions_.unlock(sr_key); SourceRegionHandle handle {*sr_ptr}; + + // Check if the new source region contains a point source and apply it if so + auto it2 = point_source_map_.find(sr_key); + if (it2 != point_source_map_.end()) { + int es = it2->second; + auto s = model::external_sources[es].get(); + auto is = dynamic_cast(s); + auto energy = dynamic_cast(is->energy()); + double strength_factor = is->strength(); + apply_external_source_to_source_region(energy, strength_factor, handle); + int material = handle.material(); + if (material != MATERIAL_VOID) { + for (int g = 0; g < negroups_; g++) { + double sigma_t = sigma_t_[material * negroups_ + g]; + handle.external_source(g) /= sigma_t; + } + } + } + return handle; } diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index 60b00a5be..b831efcd9 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -281,10 +281,21 @@ void validate_random_ray_inputs() "allowed in random ray mode."); } - // Validate that a domain ID was specified - if (is->domain_ids().size() == 0) { - fatal_error("Fixed sources must be specified by domain " - "id (cell, material, or universe) in random ray mode."); + // Validate that a domain ID was specified OR that it is a point source + auto sp = dynamic_cast(is->space()); + if (is->domain_ids().size() == 0 && !sp) { + fatal_error("Fixed sources must be point source or spatially " + "constrained by domain id (cell, material, or universe) in " + "random ray mode."); + } else if (is->domain_ids().size() > 0 && sp) { + // If both a domain constraint and a non-default point source location + // are specified, notify user that domain constraint takes precedence. + if (sp->r().x == 0.0 && sp->r().y == 0.0 && sp->r().z == 0.0) { + warning("Fixed source has both a domain constraint and a point " + "type spatial distribution. The domain constraint takes " + "precedence in random ray mode -- point source coordinate " + "will be ignored."); + } } // Check that a discrete energy distribution was used @@ -393,12 +404,12 @@ RandomRaySimulation::RandomRaySimulation() void RandomRaySimulation::apply_fixed_sources_and_mesh_domains() { + domain_->apply_meshes(); if (settings::run_mode == RunMode::FIXED_SOURCE) { // Transfer external source user inputs onto random ray source regions domain_->convert_external_sources(); domain_->count_external_source_regions(); } - domain_->apply_meshes(); } void RandomRaySimulation::prepare_fixed_sources_adjoint( @@ -517,6 +528,8 @@ void RandomRaySimulation::simulate() finalize_generation(); finalize_batch(); } // End random ray power iteration loop + + domain_->count_external_source_regions(); } void RandomRaySimulation::output_simulation_results() const diff --git a/tests/regression_tests/random_ray_point_source_locator/__init__.py b/tests/regression_tests/random_ray_point_source_locator/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_point_source_locator/inputs_true.dat b/tests/regression_tests/random_ray_point_source_locator/inputs_true.dat new file mode 100644 index 000000000..9c7cd4ab5 --- /dev/null +++ b/tests/regression_tests/random_ray_point_source_locator/inputs_true.dat @@ -0,0 +1,253 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 30 + 15 + + + 2.5 2.5 2.5 + + + 100.0 1.0 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + + + + + + + + 30 30 30 + 0.0 0.0 0.0 + 30.0 30.0 30.0 + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_point_source_locator/results_true.dat b/tests/regression_tests/random_ray_point_source_locator/results_true.dat new file mode 100644 index 000000000..1785dda57 --- /dev/null +++ b/tests/regression_tests/random_ray_point_source_locator/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +2.633900E+00 +2.948207E+00 +tally 2: +1.440463E-01 +3.294032E-03 +tally 3: +9.425207E-03 +1.089748E-05 diff --git a/tests/regression_tests/random_ray_point_source_locator/test.py b/tests/regression_tests/random_ray_point_source_locator/test.py new file mode 100644 index 000000000..fd3d8a18f --- /dev/null +++ b/tests/regression_tests/random_ray_point_source_locator/test.py @@ -0,0 +1,44 @@ +import os + +import openmc +from openmc.examples import random_ray_three_region_cube + +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_point_source_locator(): + model = random_ray_three_region_cube() + + # Overlay subdivided SR mesh to reduce resolution from 2.5cm -> 1cm + width = 30.0 + mesh = openmc.RegularMesh() + mesh.dimension = (30, 30, 30) + mesh.lower_left = (0.0, 0.0, 0.0) + mesh.upper_right = (width, width, width) + model.settings.random_ray['source_region_meshes'] = [ + (mesh, [model.geometry.root_universe]), + ] + + # Define a point source + strengths = [1.0] + midpoints = [100.0] + energy_distribution = openmc.stats.Discrete(x=midpoints, p=strengths) + spatial_distribution = openmc.stats.Point([2.5, 2.5, 2.5]) + source = openmc.IndependentSource( + energy=energy_distribution, space=spatial_distribution, strength=3.14) + model.settings.source = [source] + + # Settings + model.settings.inactive = 15 + model.settings.batches = 30 + + harness = MGXSTestHarness('statepoint.30.h5', model) + harness.main() From cdc254ccf4245282d5aff19bd769daf180ef1a37 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Thu, 10 Apr 2025 12:31:33 -0500 Subject: [PATCH 304/671] Fix for Issue Loading MGXS Data Files with LLVM 20 or Newer (#3368) Co-authored-by: Paul Romano --- src/mgxs.cpp | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/mgxs.cpp b/src/mgxs.cpp index eae3e5817..a88d2c196 100644 --- a/src/mgxs.cpp +++ b/src/mgxs.cpp @@ -99,7 +99,21 @@ void Mgxs::metadata_from_hdf5(hid_t xs_id, const vector& temperature, case TemperatureMethod::NEAREST: // Determine actual temperatures to read for (const auto& T : temperature) { - auto i_closest = xt::argmin(xt::abs(temps_available - T))[0]; + // 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; + } + } + double temp_actual = temps_available[i_closest]; if (std::fabs(temp_actual - T) < settings::temperature_tolerance) { if (std::find(temps_to_read.begin(), temps_to_read.end(), From bd95b52f4d711fd3e62ec80fed9a4f1d1778198f Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Fri, 11 Apr 2025 23:35:30 +0200 Subject: [PATCH 305/671] Add check for equal value bins in an EnergyFilter (#3372) --- openmc/filter.py | 4 ++-- tests/unit_tests/test_filters.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/openmc/filter.py b/openmc/filter.py index 35a7e7c7e..755777c5e 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -1238,7 +1238,7 @@ class RealFilter(Filter): cv.check_type('filter value', v1, Real) # Make sure that each tuple has values that are increasing - if v1 < v0: + if v1 <= v0: raise ValueError(f'Values {v0} and {v1} appear to be out of ' 'order') @@ -1384,7 +1384,7 @@ class EnergyFilter(RealFilter): ---------- values : Iterable of Real A list of values for which each successive pair constitutes a range of - energies in [eV] for a single bin + energies in [eV] for a single bin. Entries must be positive and ascending. filter_id : int Unique identifier for the filter diff --git a/tests/unit_tests/test_filters.py b/tests/unit_tests/test_filters.py index 55bb62075..c3b737192 100644 --- a/tests/unit_tests/test_filters.py +++ b/tests/unit_tests/test_filters.py @@ -294,3 +294,21 @@ def test_tabular_from_energyfilter(): tab = efilter.get_tabular(values=np.array([10, 10, 5]), interpolation='linear-linear') assert tab.interpolation == 'linear-linear' + + +def test_energy_filter(): + + # testing that bins descending value raises error + msg = "Values 1.0 and 0.5 appear to be out of order" + with raises(ValueError, match=msg): + openmc.EnergyFilter([0.0, 1.0, 0.5]) + + # testing that bins with same value raises error + msg = "Values 0.25 and 0.25 appear to be out of order" + with raises(ValueError, match=msg): + openmc.EnergyFilter([0.0, 0.25, 0.25]) + + # testing that negative bins values raises error + msg = 'Unable to set "filter value" to "-1.2" since it is less than "0.0"' + with raises(ValueError, match=msg): + openmc.EnergyFilter([-1.2, 0.25, 0.5]) From c1a4d43da81ea0e04f063ae450d44133e3d72b9c Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 11 Apr 2025 18:43:27 -0500 Subject: [PATCH 306/671] Add methods on Material class for waste disposal rating / classification (#3366) Co-authored-by: Ethan Peterson --- openmc/material.py | 100 ++++++- openmc/waste.py | 279 ++++++++++++++++++ tests/unit_tests/test_material.py | 6 + tests/unit_tests/test_waste_classification.py | 98 ++++++ 4 files changed, 475 insertions(+), 8 deletions(-) create mode 100644 openmc/waste.py create mode 100644 tests/unit_tests/test_waste_classification.py diff --git a/openmc/material.py b/openmc/material.py index f40ac805f..9677fbba3 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -18,6 +18,7 @@ import openmc.checkvalue as cv from ._xml import clean_indentation, reorder_attributes 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 @@ -30,6 +31,7 @@ DENSITY_UNITS = ('g/cm3', 'g/cc', 'kg/m3', 'atom/b-cm', 'atom/cm3', 'sum', # Smallest normalized floating point number _SMALLEST_NORMAL = sys.float_info.min +_BECQUEREL_PER_CURIE = 3.7e10 NuclideTuple = namedtuple('NuclideTuple', ['name', 'percent', 'percent_type']) @@ -1058,7 +1060,6 @@ class Material(IDManagerMixin): nuc_densities.append(nuc.percent) nuc_density_types.append(nuc.percent_type) - nucs = np.array(nucs) nuc_densities = np.array(nuc_densities) nuc_density_types = np.array(nuc_density_types) @@ -1137,17 +1138,16 @@ class Material(IDManagerMixin): def get_activity(self, units: str = 'Bq/cm3', by_nuclide: bool = False, volume: float | None = None) -> dict[str, float] | float: - """Returns the activity of the material or for each nuclide in the - material in units of [Bq], [Bq/g], [Bq/kg] or [Bq/cm3]. + """Returns the activity of the material or of each nuclide within. .. versionadded:: 0.13.1 Parameters ---------- - units : {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3'} + units : {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3', 'Ci', 'Ci/m3'} Specifies the type of activity to return, options include total - activity [Bq], specific [Bq/g, Bq/kg] or volumetric activity - [Bq/cm3]. Default is volumetric activity [Bq/cm3]. + activity [Bq,Ci], specific [Bq/g, Bq/kg] or volumetric activity + [Bq/cm3,Ci/m3]. Default is volumetric activity [Bq/cm3]. by_nuclide : bool Specifies if the activity should be returned for the material as a whole or per nuclide. Default is False. @@ -1165,17 +1165,24 @@ class Material(IDManagerMixin): of the material is returned as a float. """ - cv.check_value('units', units, {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3'}) + cv.check_value('units', units, {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3', 'Ci', 'Ci/m3'}) cv.check_type('by_nuclide', by_nuclide, bool) + if volume is None: + volume = self.volume + if units == 'Bq': - multiplier = volume if volume is not None else self.volume + multiplier = volume elif units == 'Bq/cm3': multiplier = 1 elif units == 'Bq/g': multiplier = 1.0 / self.get_mass_density() elif units == 'Bq/kg': multiplier = 1000.0 / self.get_mass_density() + elif units == 'Ci': + multiplier = volume / _BECQUEREL_PER_CURIE + elif units == 'Ci/m3': + multiplier = 1e6 / _BECQUEREL_PER_CURIE activity = {} for nuclide, atoms_per_bcm in self.get_nuclide_atom_densities().items(): @@ -1316,6 +1323,83 @@ class Material(IDManagerMixin): raise ValueError("Volume must be set in order to determine mass.") return volume*self.get_mass_density(nuclide) + def waste_classification(self, metal: bool = False) -> str: + """Classify the material for near-surface waste disposal. + + This method determines a waste classification for the material based on + the NRC regulations (10 CFR 61.55). Note that the NRC regulations do not + consider many long-lived radionuclides relevant to fusion systems; for + fusion applications, it is recommended to calculate a waste disposal + rating based on limits by Fetter et al. using the + :meth:`~openmc.Material.waste_disposal_rating` method. + + Parameters + ---------- + metal : bool, optional + Whether or not the material is in metal form. + + Returns + ------- + str + The waste disposal classification, which can be "Class A", "Class + B", "Class C", or "GTCC" (greater than class C). + + """ + return waste._waste_classification(self, metal=metal) + + def waste_disposal_rating( + self, + limits: str | dict[str, float] = 'Fetter', + metal: bool = False, + ) -> float: + """Return the waste disposal rating for the material. + + This method returns a waste disposal rating for the material based on a + set of specific activity limits. The waste disposal rating is a single + number that represents the sum of the ratios of the specific activity + for each radionuclide in the material against a nuclide-specific limit. + A value less than 1.0 indicates that the material "meets" the limits + whereas a value greater than 1.0 exceeds the limits. + + Note that the limits for NRC do not consider many long-lived + radionuclides relevant to fusion systems. A paper by `Fetter et al. + `_ applies the NRC + methodology to calculate specific activity limits for an expanded set of + radionuclides. + + Parameters + ---------- + limits : str or dict, optional + The name of a predefined set of specific activity limits or a + dictionary that contains specific activity limits for radionuclides, + where keys are nuclide names and values are activities in units of + [Ci/m3]. The predefined options are: + + - 'Fetter': Uses limits from Fetter et al. (1990) + - 'NRC_long': Uses the 10 CFR 61.55 limits for long-lived + radionuclides + - 'NRC_short_A': Uses the 10 CFR 61.55 class A limits for + short-lived radionuclides + - 'NRC_short_B': Uses the 10 CFR 61.55 class B limits for + short-lived radionuclides + - 'NRC_short_C': Uses the 10 CFR 61.55 class C limits for + short-lived radionuclides + metal : bool, optional + Whether or not the material is in metal form (only applicable for + NRC based limits) + + Returns + ------- + float + The waste disposal rating for the material. + + See also + -------- + Material.waste_classification() + + """ + return waste._waste_disposal_rating(self, limits, metal) + def clone(self, memo: dict | None = None) -> Material: """Create a copy of this material with a new unique ID. diff --git a/openmc/waste.py b/openmc/waste.py new file mode 100644 index 000000000..7dd45b8e3 --- /dev/null +++ b/openmc/waste.py @@ -0,0 +1,279 @@ +from __future__ import annotations + +import openmc +from openmc.data import half_life + + +def _waste_classification(mat: openmc.Material, metal: bool = True) -> str: + """Classify a material for near-surface waste disposal. + + This method determines a waste classification for a material based on the + NRC regulations (10 CFR 61.55). + + Parameters + ---------- + mat : openmc.Material + The material to classify. + metal : bool, optional + Whether or not the material is in metal form. This changes the + acceptable limits in Tables 1 and 2 for certain nuclides. + + Returns + ------- + str + The waste disposal classification, which can be "Class A", "Class B", + "Class C", or "GTCC" (greater than class C). + + """ + # Determine metrics based on Tables 1 and 2 using sum of fractions rule for + # mixture of radionuclides from §61.55(a)(7) + ratio1 = _waste_disposal_rating(mat, 'NRC_long', metal=metal) + ratio2 = [ + _waste_disposal_rating(mat, 'NRC_short_A', metal=metal), + _waste_disposal_rating(mat, 'NRC_short_B', metal=metal), + _waste_disposal_rating(mat, 'NRC_short_C', metal=metal), + ] + + # Determine which nuclides are present in Table 1 and Table 2 + table1_nuclides_present = (ratio1 > 0.0) + table2_nuclides_present = any(x > 0.0 for x in ratio2) + + # Helper function for classifying based on Table 2 + def classify_table2(col1, col2, col3): + if col1 < 1.0: + return "Class A" + elif col2 < 1.0: + return "Class B" + elif col3 < 1.0: + return "Class C" + else: + return "GTCC" + + if table1_nuclides_present and table2_nuclides_present: + # Classification based on §61.55(a)(5) + if ratio1 < 0.1: + return classify_table2(*ratio2) + elif ratio1 < 1.0: + return "Class C" if ratio2[2] < 1.0 else "GTCC" + else: + return "GTCC" + + elif table1_nuclides_present: + # Classification based on §61.55(a)(3) + if ratio1 < 0.1: + return "Class A" + elif ratio1 < 1.0: + return "Class C" + else: + return "GTCC" + + elif table2_nuclides_present: + # Classification based on §61.55(a)(4) + return classify_table2(*ratio2) + + else: + # Classification based on §61.55(a)(6) + return "Class A" + + +def _waste_disposal_rating( + mat: openmc.Material, + limits: str | dict[str, float] = 'Fetter', + metal: bool = False, + ) -> float: + """Return the waste disposal rating for a material. + + This method returns a waste disposal rating for the material based on a set + of specific activity limits. The waste disposal rating is a single number + that represents the sum of the ratios of the specific activity for each + radionuclide in the material against a nuclide-specific limit. A value less + than 1.0 indicates that the material "meets" the limits whereas a value + greater than 1.0 exceeds the limits. + + Parameters + ---------- + mat : openmc.Material + The material to classify. + limits : str or dict, optional + The name of a predefined set of specific activity limits or a dictionary + that contains specific activity limits for radionuclides, where keys are + nuclide names and values are activities in units of [Ci/m3]. The + predefined options are: + + - 'Fetter': Uses limits from Fetter et al. (1990) + - 'NRC_long': Uses the 10 CFR 61.55 limits for long-lived radionuclides + - 'NRC_short_A': Uses the 10 CFR 61.55 class A limits for short-lived + radionuclides + - 'NRC_short_B': Uses the 10 CFR 61.55 class B limits for short-lived + radionuclides + - 'NRC_short_C': Uses the 10 CFR 61.55 class C limits for short-lived + radionuclides + metal : bool, optional + Whether or not the material is in metal form (only applicable for NRC + based limits) + + Returns + ------- + float + The waste disposal rating for the material. + + """ + if limits == 'Fetter': + # Specific activity limits for radionuclides with half-lives between 5 + # years and 1e12 years from Table 2 in Fetter + limits = { + "Be10": 5.0e3, + "C14": 6.0e2, + "Al26": 9.0e-2, + "Si32": 6.0e2, + "Cl36": 1.0e1, + "Ar39": 2.0e4, + "Ar42": 2.0e4, + "K40": 2.0e0, + "Ca41": 1.0e4, + "Ti44": 2.0e2, + "Fe60": 1.0e-1, + "Co60": 3.0e8, + "Ni59": 9.0e2, + "Ni63": 7.0e5, + "Se79": 5.0e1, + "Kr81": 3.0e1, + "Sr90": 8.0e5, + "Nb91": 2.0e2, + "Nb92": 2.0e-1, + "Nb94": 2.0e-1, + "Mo93": 4.0e3, + "Tc97": 4.0e-1, + "Tc98": 1.0e-2, + "Tc99": 6.0e-2, + "Pd107": 9.0e2, + "Ag108_m1": 3.0e0, + "Sn121_m1": 7.0e5, + "Sn126": 1.0e-1, + "I129": 2.0e0, + "Cs137": 5.0e4, + "Ba133": 2.0e8, + "La137": 2.0e2, + "Sm151": 5.0e7, + "Eu150_m1": 3.0e3, + "Eu152": 3.0e5, + "Eu154": 5.0e6, + "Gd148": 2.0e5, + "Gd150": 2.0e3, + "Tb157": 5.0e3, + "Tb158": 4.0e0, + "Dy154": 1.0e3, + "Ho166_m1": 2.0e-1, + "Hf178_m1": 9.0e3, + "Hf182": 2.0e-1, + "Re186_m1": 2.0e1, + "Ir192_m1": 1.0e0, + "Pt193": 2.0e8, + "Hg194": 5.0e-1, + "Pb202": 6.0e-1, + "Pb210": 3.0e7, + "Bi207": 9.0e3, + "Bi208": 8.0e-2, + "Bi210_m1": 1.0e0, + "Po209": 3.0e3, + "Ra226": 1.0e-1, + "Ra228": 3.0e7, + "Ac227": 5.0e5, + "Th229": 2.0e0, + "Th230": 3.0e-1, + "Th232": 1.0e-1, + "Pa231": 7.0e-1, + "U232": 3.0e1, + "U233": 2.0e1, + "U234": 9.0e1, + "U235": 2.0e0, + "Np236": 1.0e0, + "Np237": 1.0e0, + "Pu238": 7.0e4, + "Pu239": 1.0e3, + "Pu240": 1.0e3, + "Pu241": 2.0e3, + "Pu242": 1.0e3, + "Pu244": 9.0e-1, + "Am241": 5.0e1, + "Am242_m1": 3.0e2, + "Am243": 2.0e0, + "Cm243": 6.0e2, + "Cm244": 5.0e5, + "Cm245": 5.0e0, + "Cm246": 8.0e2, + "Cm248": 8.0e2, + } + + elif limits == 'NRC_long': + # Specific activity limits for long-lived radionuclides from Table 1 in + # 10 CFR 61.55 in Ci/m3. + limits = { + 'C14': 8.0, + 'Tc99': 3.0, + 'I129': 0.08, + } + if metal: + limits['C14'] = 80.0 + limits['Ni59'] = 220.0 + limits['Nb94'] = 0.2 + + # Convert values in nCi/g to Ci/m3 + factor = (1e6 * mat.get_mass_density()) / 1e9 + limits.update({ + 'Pu241': 3500.0 * factor, + 'Cm242': 20000.0 * factor, + 'Np237': 100.0 * factor, + 'Pu238': 100.0 * factor, + 'Pu239': 100.0 * factor, + 'Pu240': 100.0 * factor, + 'Pu242': 100.0 * factor, + 'Pu244': 100.0 * factor, + 'Am241': 100.0 * factor, + 'Am243': 100.0 * factor, + 'Cm243': 100.0 * factor, + 'Cm244': 100.0 * factor, + 'Cm245': 100.0 * factor, + 'Cm246': 100.0 * factor, + 'Cm247': 100.0 * factor, + 'Cm248': 100.0 * factor, + 'Bk247': 100.0 * factor, + 'Cf249': 100.0 * factor, + 'Cf250': 100.0 * factor, + 'Cf251': 100.0 * factor, + }) + + elif limits == 'NRC_short_A': + # Get Class A specific activity limits for short-lived radionuclides + # from Table 2 in 10 CFR 61.55 + limits = { + 'H3': 40.0, + 'Co60': 700.0, + 'Ni63': 35.0 if metal else 3.5, + 'Sr90': 0.04, + 'Cs137': 1.0 + } + + # Add radionuclides with half-lives < 5 years to limits for class A + five_years = 60.0 * 60.0 * 24.0 * 365.25 * 5.0 + for nuc in mat.get_nuclides(): + if half_life(nuc) is not None and half_life(nuc) < five_years: + limits[nuc] = 700.0 + + elif limits == 'NRC_short_B': + # Get Class B specific activity limits for short-lived radionuclides + # from Table 2 in 10 CFR 61.55 + limits = {'Ni63': 700.0 if metal else 70.0, 'Sr90': 150.0, 'Cs137': 44.0} + + elif limits == 'NRC_short_C': + # Get Class C specific activity limits for short-lived radionuclides + # from Table 2 in 10 CFR 61.55 + limits = {'Ni63': 7000.0 if metal else 700.0, 'Sr90': 7000.0, 'Cs137': 4600.0} + + # Calculate the sum of the fractions of the activity of each radionuclide + # compared to the specified limits + ratio = 0.0 + for nuc, ci_m3 in mat.get_activity(units="Ci/m3", by_nuclide=True).items(): + if nuc in limits: + ratio += ci_m3 / limits[nuc] + return ratio diff --git a/tests/unit_tests/test_material.py b/tests/unit_tests/test_material.py index cb71cd09b..8ad579563 100644 --- a/tests/unit_tests/test_material.py +++ b/tests/unit_tests/test_material.py @@ -590,6 +590,12 @@ def test_get_activity(): # Test with volume specified as argument assert pytest.approx(m4.get_activity(units='Bq', volume=1.0)) == 355978108155965.94*3/2 + # Test units based on Ci + bq = m4.get_activity(units='Bq') + m3 = m4.volume * 1e-6 + assert (ci := m4.get_activity(units='Ci')) == pytest.approx(bq/3.7e10) + assert m4.get_activity(units='Ci/m3') == pytest.approx(ci/m3) + def test_get_decay_heat(): # Set chain file for testing diff --git a/tests/unit_tests/test_waste_classification.py b/tests/unit_tests/test_waste_classification.py new file mode 100644 index 000000000..dd692cc4b --- /dev/null +++ b/tests/unit_tests/test_waste_classification.py @@ -0,0 +1,98 @@ +import random + +import openmc +import pytest + + +@pytest.mark.parametrize("metal", [False, True]) +def test_waste_classification_long(metal): + """Test classification when determined by long-lived radionuclides""" + f = 10.0 if metal else 1.0 + limit = 8.0*f + mat = openmc.Material() + mat.add_nuclide('C14', 1e-9*f) + assert mat.get_activity('Ci/m3') < 0.1 * limit + assert mat.waste_classification(metal=metal) == 'Class A' + + mat = openmc.Material() + mat.add_nuclide('C14', 1e-8*f) + assert 0.1 * limit < mat.get_activity('Ci/m3') < limit + assert mat.waste_classification(metal=metal) == 'Class C' + + mat = openmc.Material() + mat.add_nuclide('C14', 1e-7*f) + assert mat.get_activity('Ci/m3') > limit + assert mat.waste_classification(metal=metal) == 'GTCC' + + +@pytest.mark.parametrize("metal", [False, True]) +def test_waste_classification_short(metal): + """Test classification when determined by short-lived radionuclides""" + f = 10.0 if metal else 1.0 + col1, col2, col3 = 3.5*f, 70.0*f, 700.0*f + + mat = openmc.Material() + mat.add_nuclide('Ni63', 1e-10*f) + assert mat.get_activity('Ci/m3') < col1 + assert mat.waste_classification(metal=metal) == 'Class A' + + mat = openmc.Material() + mat.add_nuclide('Ni63', 1e-10*10*f) + assert col1 < mat.get_activity('Ci/m3') < col2 + assert mat.waste_classification(metal=metal) == 'Class B' + + mat = openmc.Material() + mat.add_nuclide('Ni63', 1e-10*200*f) + assert col2 < mat.get_activity('Ci/m3') < col3 + assert mat.waste_classification(metal=metal) == 'Class C' + + mat = openmc.Material() + mat.add_nuclide('Ni63', 1e-10*2000*f) + assert mat.get_activity('Ci/m3') > col3 + assert mat.waste_classification(metal=metal) == 'GTCC' + + +def test_waste_classification_mix(): + """Test classification when determined by a mix of radionuclides""" + # Check example from 10 CFR 61.55 with mix of Sr90 and Cs137 + mat = openmc.Material() + mat.add_nuclide('Sr90', 2.425e-9) + mat.add_nuclide('Cs137', 1.115e-9) + + # In example, activity of Sr90 is 50.0 Ci/m3 and Cs137 is 22.0 Ci/m3 + activity = mat.get_activity(units='Ci/m3', by_nuclide=True) + assert activity['Sr90'] == pytest.approx(50.0, 0.01) + assert activity['Cs137'] == pytest.approx(22.0, 0.01) + + # According to example, the waste should be class B + assert mat.waste_classification() == 'Class B' + + +def test_waste_rating_fetter(): + """Test waste classification using the Fetter limits""" + # For Tc99, Fetter has a more strict limit. Here, we create a material with + # Tc99 at 1 Ci/m3 which exceeds Fetter but not NRC + density = 3.5561e-7 + mat = openmc.Material() + mat.add_nuclide('Tc99', density) + assert mat.get_activity('Ci/m3') == pytest.approx(1.0, 1e-3) + assert mat.waste_disposal_rating(limits='NRC_short_C') < 1.0 + assert mat.waste_disposal_rating(limits='Fetter') > 1.0 + + # With a lower density, it should be Class C under Fetter limits and Class A + # under NRC limits + mat = openmc.Material() + mat.add_nuclide('Tc99', 5.0e-2*density) + assert mat.waste_disposal_rating(limits='NRC_short_A') < 1.0 + assert mat.waste_disposal_rating(limits='Fetter') < 1.0 + + +def test_waste_disposal_rating(): + """Test waste_disposal_rating method""" + mat = openmc.Material() + mat.add_nuclide('K40', random.random()) + + # Check for correct classification based on actual activity + ci_m3 = mat.get_activity('Ci/m3') + assert mat.waste_disposal_rating(limits={'K40': 2*ci_m3}) < 1.0 + assert mat.waste_disposal_rating(limits={'K40': 0.5*ci_m3}) > 1.0 From 47ca2916aa85a2909a5f6e0a3442ab7c581894b1 Mon Sep 17 00:00:00 2001 From: Joffrey Dorville <54550047+JoffreyDorville@users.noreply.github.com> Date: Mon, 14 Apr 2025 09:07:48 -0500 Subject: [PATCH 307/671] Kinetics parameters using Iterated Fission Probability (#3133) Co-authored-by: Paul Romano --- CMakeLists.txt | 1 + docs/source/usersguide/index.rst | 1 + docs/source/usersguide/kinetics.rst | 110 ++++++++++ docs/source/usersguide/tallies.rst | 18 ++ include/openmc/bank.h | 8 + include/openmc/constants.h | 8 +- include/openmc/ifp.h | 188 +++++++++++++++++ include/openmc/particle_data.h | 7 + include/openmc/settings.h | 15 +- openmc/lib/tally.py | 4 +- openmc/settings.py | 29 +++ src/bank.cpp | 28 +++ src/eigenvalue.cpp | 82 +++++++- src/finalize.cpp | 3 +- src/ifp.cpp | 216 ++++++++++++++++++++ src/output.cpp | 3 + src/particle.cpp | 6 +- src/physics.cpp | 5 + src/reaction.cpp | 3 + src/settings.cpp | 17 ++ src/simulation.cpp | 6 + src/tallies/tally.cpp | 63 ++++++ src/tallies/tally_scoring.cpp | 51 +++++ tests/regression_tests/ifp/__init__.py | 0 tests/regression_tests/ifp/inputs_true.dat | 33 +++ tests/regression_tests/ifp/results_true.dat | 9 + tests/regression_tests/ifp/test.py | 45 ++++ tests/unit_tests/test_ifp.py | 49 +++++ 28 files changed, 1002 insertions(+), 6 deletions(-) create mode 100644 docs/source/usersguide/kinetics.rst create mode 100644 include/openmc/ifp.h create mode 100644 src/ifp.cpp create mode 100644 tests/regression_tests/ifp/__init__.py create mode 100644 tests/regression_tests/ifp/inputs_true.dat create mode 100644 tests/regression_tests/ifp/results_true.dat create mode 100644 tests/regression_tests/ifp/test.py create mode 100644 tests/unit_tests/test_ifp.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 461abe508..8cb03a1a1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -343,6 +343,7 @@ list(APPEND libopenmc_SOURCES src/geometry.cpp src/geometry_aux.cpp src/hdf5_interface.cpp + src/ifp.cpp src/initialize.cpp src/lattice.cpp src/material.cpp diff --git a/docs/source/usersguide/index.rst b/docs/source/usersguide/index.rst index aef9b1a1c..5f8e0197e 100644 --- a/docs/source/usersguide/index.rst +++ b/docs/source/usersguide/index.rst @@ -22,6 +22,7 @@ essential aspects of using OpenMC to perform simulations. plots depletion decay_sources + kinetics scripts processing parallel diff --git a/docs/source/usersguide/kinetics.rst b/docs/source/usersguide/kinetics.rst new file mode 100644 index 000000000..bdf26d341 --- /dev/null +++ b/docs/source/usersguide/kinetics.rst @@ -0,0 +1,110 @@ +.. _kinetics: + +=================== +Kinetics parameters +=================== + +OpenMC has the capability to estimate the following adjoint-weighted effective +generation time :math:`\Lambda_{\text{eff}}` and the effective delayed neutron +fraction :math:`\beta_{\text{eff}}`. These parameters are calculated using the +iterated fission probability (IFP) method [Hurwitz_1964]_ based on a similar +approach as in `Serpent 2 `_. The +implementation in OpenMC is limited to eigenvalue calculations and is described +in more details in [Dorville_2025]_. + +---------------------------------- +Iterated Fission Probability (IFP) +---------------------------------- + +With IFP, additional information needs to be recorded during the simulation +compared to a typical eigenvalue calculation. OpenMC stores an additional +set of values (neutron lifetime or delayed neutron group number for +:math:`\Lambda_{\text{eff}}` or :math:`\beta_{\text{eff}}`, respectively) +for every fission neutron simulated. Each set of values corresponds to +the values that are associated to the :math:`N_{\text{gen}}` direct ancestors +of any given fission neutron. + +:math:`N_{\text{gen}}` is referred to as the number of generations in the +IFP method and corresponds to the number of generations between the birth of +a fission neutron and the time its score is added to the IFP tally. By default, +OpenMC considers 10 generations but this value can be modified by the user via +the ``ifp_n_generation`` settings in the Python API:: + + settings.ifp_n_generation = 5 + +``ifp_n_generation`` should be greater than 0, but should also be lower than +or equal to the number of inactive batches declared for the calculation. +The respect of these constraints is verified by OpenMC before any calculation. + +OpenMC will automatically detect the type of data that needs to be stored based +on the tally scores selected by the user. This guarantees that only information +of interest are stored during a simulation and avoids using extra memory when +only one parameter is needed. The following table shows the tally scores that +are needed to compute kinetics parameters in OpenMC: + +.. table:: **OpenMC tally scores needed to calculate adjoint-weighted kinetics parameters** + :align: center + + =============================== ============================ ========================== ======== + OpenMC tally score \\ Parameter :math:`\Lambda_{\text{eff}}` :math:`\beta_{\text{eff}}` Both + =============================== ============================ ========================== ======== + ``ifp-time-numerator`` X X + ``ifp-beta-numerator`` X X + ``ifp-denominator`` X X X + =============================== ============================ ========================== ======== + +| + +.. note:: Because the memory footprint of additional data is generally non-negligible + with IFP, it is recommended to choose the value for ``ifp_n_generation`` carefully. + For example, using one generation for both kinetics parameters corresponds to store + one additional integer (for the delayed neutron group number used with + :math:`\beta_{\text{eff}}`) and one floating point value (for the neutron lifetime + used with :math:`\Lambda_{\text{eff}}`) for every fission neutron simulated once the + asymptotic regime is reached. + +----------------------------- +Obtaining kinetics parameters +----------------------------- + +Here is an example showing how to declare the three available IFP scores in a +single tally:: + + tally = openmc.Tally(name="ifp-scores") + tally.scores = [ + "ifp-time-numerator", + "ifp-beta-numerator", + "ifp-denominator" + ] + +The effective generation time :math:`\Lambda_{\text{eff}}` is calculated +by dividing the result of the ``ifp-time-numerator`` score by the one obtained +for ``ifp-denominator`` and by the :math:`k_{\text{eff}}` of the simulation: + +.. math:: + :label: lambda_eff + + \Lambda_{\text{eff}} = \frac{S_{\text{ifp-time-numerator}}}{S_{\text{ifp-denominator}} \times k_{\text{eff}}} + +The effective delayed neutron fraction :math:`\beta_{\text{eff}}` is calculated +by dividing the result of the ``ifp-beta-numerator`` score by the one obtained +for ``ifp-denominator``: + +.. math:: + :label: beta_eff + + \beta_{\text{eff}} = \frac{S_{\text{ifp-beta-numerator}}}{S_{\text{ifp-denominator}}} + +.. only:: html + + .. rubric:: References + +.. [Hurwitz_1964] H. Hurwitz Jr., "Naval Reactors Physics Handbook", volume 1, p. 864. + Radkowsky, A. (Ed.), Naval Reactors, Division of Reactor Development, U.S. + Atomic Energy Commission (1964). + +.. [Dorville_2025] J. Dorville, L. Labrie-Cleary, and P. K. Romano, "Implementation + of the Iterated Fission Probability Method in OpenMC to Compute Adjoint-Weighted + Kinetics Parameters", International Conference on Mathematics and Computational + Methods Applied to Nuclear Science and Engineering (M&C 2025), Denver, April 27-30, + 2025 (to be presented). diff --git a/docs/source/usersguide/tallies.rst b/docs/source/usersguide/tallies.rst index 02328af58..e3b4e508b 100644 --- a/docs/source/usersguide/tallies.rst +++ b/docs/source/usersguide/tallies.rst @@ -322,6 +322,24 @@ The following tables show all valid scores: | |particle. Note that this score can only be combined| | |with a cell filter and an energy filter. | +----------------------+---------------------------------------------------+ + |ifp-time-numerator |Adjoint-weighted lifetime of neutron produced by | + | |fission in units of seconds per source particle. | + | |This score is used to compute kinetics parameters | + | |using the iterated fission probability (IFP) | + | |method. | + +----------------------+---------------------------------------------------+ + |ifp-beta-numerator |Adjoint-weighted number of delayed fission events | + | |in units of number of delayed fission event per | + | |source particle. This score is used to compute | + | |kinetics parameters using the iterated fission | + | |probability (IFP) method. | + +----------------------+---------------------------------------------------+ + |ifp-denominator |Weights corresponding to the number of fission | + | |events in units of number of fission event per | + | |source particle. This score is used to compute | + | |kinetics parameters using the iterated fission | + | |probability (IFP) method. | + +----------------------+---------------------------------------------------+ .. _usersguide_tally_normalization: diff --git a/include/openmc/bank.h b/include/openmc/bank.h index 95386514d..fd8fbd73e 100644 --- a/include/openmc/bank.h +++ b/include/openmc/bank.h @@ -22,6 +22,14 @@ extern SharedArray surf_source_bank; extern SharedArray fission_bank; +extern vector> ifp_source_delayed_group_bank; + +extern vector> ifp_source_lifetime_bank; + +extern vector> ifp_fission_delayed_group_bank; + +extern vector> ifp_fission_lifetime_bank; + extern vector progeny_per_particle; } // namespace simulation diff --git a/include/openmc/constants.h b/include/openmc/constants.h index 2a7ce1990..252194528 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -312,7 +312,10 @@ enum TallyScore { SCORE_FISS_Q_PROMPT = -14, // prompt fission Q-value SCORE_FISS_Q_RECOV = -15, // recoverable fission Q-value SCORE_DECAY_RATE = -16, // delayed neutron precursor decay rate - SCORE_PULSE_HEIGHT = -17 // pulse-height + SCORE_PULSE_HEIGHT = -17, // pulse-height + SCORE_IFP_TIME_NUM = -18, // IFP lifetime numerator + SCORE_IFP_BETA_NUM = -19, // IFP delayed fraction numerator + SCORE_IFP_DENOM = -20 // IFP common denominator }; // Global tally parameters @@ -322,6 +325,9 @@ enum class GlobalTally { K_COLLISION, K_ABSORPTION, K_TRACKLENGTH, LEAKAGE }; // Miscellaneous constexpr int C_NONE {-1}; +// Default value of generation for IFP +constexpr int DEFAULT_IFP_N_GENERATION {10}; + // Interpolation rules enum class Interpolation { histogram = 1, diff --git a/include/openmc/ifp.h b/include/openmc/ifp.h new file mode 100644 index 000000000..633a262d5 --- /dev/null +++ b/include/openmc/ifp.h @@ -0,0 +1,188 @@ +#ifndef OPENMC_IFP_H +#define OPENMC_IFP_H + +#include "openmc/message_passing.h" +#include "openmc/particle.h" +#include "openmc/particle_data.h" +#include "openmc/settings.h" + +namespace openmc { + +//! Check the value of the IFP parameter for beta effective or both. +//! +//! \return true if "BetaEffective" or "Both", false otherwise. +bool is_beta_effective_or_both(); + +//! Check the value of the IFP parameter for generation time or both. +//! +//! \return true if "GenerationTime" or "Both", false otherwise. +bool is_generation_time_or_both(); + +//! Resize IFP vectors +//! +//! \param[in,out] delayed_groups List of delayed group numbers +//! \param[in,out] lifetimes List of lifetimes +//! \param[in] n Dimension to resize vectors +template +void resize_ifp_data(vector& delayed_groups, vector& lifetimes, int64_t n) +{ + if (is_beta_effective_or_both()) { + delayed_groups.resize(n); + } + if (is_generation_time_or_both()) { + lifetimes.resize(n); + } +} + +//! Update a list of values by adding a new value if the size +//! of the list can accomodate the new value or by shifting all +//! values to the left (removing the first value of the list +//! and adding the new value at the end of the list). +//! +//! \param[in] value Value to add to the list +//! \param[in] data Initial version of the list +//! \return Updated list +template +vector _ifp(const T& value, const vector& data) +{ + vector updated; + size_t source_idx = data.size(); + + if (source_idx < settings::ifp_n_generation) { + updated.resize(source_idx + 1); + for (size_t i = 0; i < source_idx; i++) { + updated[i] = data[i]; + } + updated[source_idx] = value; + } else if (source_idx == settings::ifp_n_generation) { + updated.resize(source_idx); + for (size_t i = 0; i < source_idx - 1; i++) { + updated[i] = data[i + 1]; + } + updated[source_idx - 1] = value; + } + return updated; +} + +//! \brief Iterated Fission Probability (IFP) method. +//! +//! Add the IFP information in the IFP banks using the same index +//! as the one used to append the fission site to the fission bank. +//! Multithreading protection is guaranteed by the index returned by the +//! thread_safe_append call in physics.cpp. +//! +//! Needs to be done after the delayed group is found. +//! +//! \param[in] p Particle +//! \param[in] site Fission site +//! \param[in] idx Bank index from the thread_safe_append call in physics.cpp +void ifp(const Particle& p, const SourceSite& site, int64_t idx); + +//! Resize the IFP banks used in the simulation +void resize_simulation_ifp_banks(); + +//! Retrieve IFP data from the IFP fission banks. +//! +//! \param[in] i_bank Index in the fission banks +//! \param[in,out] delayed_groups Delayed group numbers +//! \param[in,out] lifetimes Lifetimes lists +void copy_ifp_data_from_fission_banks( + int i_bank, vector& delayed_groups, vector& lifetimes); + +#ifdef OPENMC_MPI + +//! Deserialization information for transfer of IFP data using MPI +struct DeserializationInfo { + int64_t index_local; //!< local index + int64_t n; //!< number of sites sent +}; + +//! Broadcast the number of generation determined by the size of the first +//! element on the first processor. +//! +//! \param[in] n_generation Number of generations +//! \param[in] delayed_groups List of delayed group numbers lists +//! \param[in] lifetimes List of lifetimes lists +void broadcast_ifp_n_generation(int& n_generation, + const vector>& delayed_groups, + const vector>& lifetimes); + +//! Send IFP data using MPI. +//! +//! \param[in] idx Index of the first site +//! \param[in] n Number of sites to send +//! \param[in] n_generation Number of generations +//! \param[in] neighbor Index of the neighboring processor +//! \param[in] requests MPI requests +//! \param[in] delayed_groups List of delayed group numbers lists +//! \param[out] send_delayed_groups Delayed group numbers buffer +//! \param[in] lifetimes List of lifetimes lists +//! \param[out] send_lifetimes Lifetimes buffer +void send_ifp_info(int64_t idx, int64_t n, int n_generation, int neighbor, + vector& requests, const vector>& delayed_groups, + vector& send_delayed_groups, const vector>& lifetimes, + vector& send_lifetimes); + +//! Receive IFP data using MPI. +//! +//! \param[in] idx Index of the first site +//! \param[in] n Number of sites to receive +//! \param[in] n_generation Number of generations +//! \param[in] neighbor Index of the neighboring processor +//! \param[in] requests MPI requests +//! \param[in] delayed_groups List of delayed group numbers +//! \param[in] lifetimes List of lifetimes +//! \param[out] deserialization Information to deserialize the received data +void receive_ifp_data(int64_t idx, int64_t n, int n_generation, int neighbor, + vector& requests, vector& delayed_groups, + vector& lifetimes, vector& deserialization); + +//! Copy partial IFP data from local lists to source banks. +//! +//! \param[in] idx Index of the first site +//! \param[in] n Number of sites to copy +//! \param[in] i_bank Index in the IFP source banks +//! \param[in] delayed_groups List of delayed group numbers lists +//! \param[in] lifetimes List of lifetimes lists +void copy_partial_ifp_data_to_source_banks(int64_t idx, int n, int64_t i_bank, + const vector>& delayed_groups, + const vector>& lifetimes); + +//! Deserialize IFP information received using MPI and store it in +//! the IFP source banks. +//! +//! \param[in] n_generation Number of generations +//! \param[out] deserialization Information to deserialize the received data +//! \param[in] delayed_groups List of delayed group numbers +//! \param[in] lifetimes List of lifetimes +void deserialize_ifp_info(int n_generation, + const vector& deserialization, + const vector& delayed_groups, const vector& lifetimes); + +#endif + +//! Copy IFP temporary vectors to source banks. +//! +//! \param[in] delayed_groups List of delayed group numbers lists +//! \param[in] lifetimes List of lifetimes lists +void copy_complete_ifp_data_to_source_banks( + const vector>& delayed_groups, + const vector>& lifetimes); + +//! Allocate temporary vectors for IFP data. +//! +//! \param[in,out] delayed_groups List of delayed group numbers lists +//! \param[in,out] lifetimes List of delayed group numbers lists +void allocate_temporary_vector_ifp( + vector>& delayed_groups, vector>& lifetimes); + +//! Copy local IFP data to IFP fission banks. +//! +//! \param[in] delayed_groups_ptr Pointer to delayed group numbers +//! \param[in] lifetimes_ptr Pointer to lifetimes +void copy_ifp_data_to_fission_banks( + const vector* delayed_groups_ptr, const vector* lifetimes_ptr); + +} // namespace openmc + +#endif // OPENMC_IFP_H diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index 15ae57893..1570b780b 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -454,6 +454,9 @@ private: int cell_born_ {-1}; + // Iterated Fission Probability + double lifetime_ {0.0}; //!< neutron lifetime [s] + int n_collision_ {0}; bool write_track_ {false}; @@ -560,6 +563,10 @@ public: double& time_last() { return time_last_; } const double& time_last() const { return time_last_; } + // Particle lifetime + double& lifetime() { return lifetime_; } + const double& lifetime() const { return lifetime_; } + // What event took place, described in greater detail below TallyEvent& event() { return event_; } const TallyEvent& event() const { return event_; } diff --git a/include/openmc/settings.h b/include/openmc/settings.h index f3aff08b0..9017b2d08 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -24,6 +24,14 @@ enum class SSWCellType { To, }; +// Type of IFP parameters +enum class IFPParameter { + None, + Both, + BetaEffective, + GenerationTime, +}; + //============================================================================== // Global variable declarations //============================================================================== @@ -42,7 +50,8 @@ extern bool delayed_photon_scaling; //!< Scale fission photon yield to include delayed extern "C" bool entropy_on; //!< calculate Shannon entropy? extern "C" bool - event_based; //!< use event-based mode (instead of history-based) + event_based; //!< use event-based mode (instead of history-based) +extern bool ifp_on; //!< Use IFP for kinetics parameters? extern bool legendre_to_tabular; //!< convert Legendre distributions to tabular? extern bool material_cell_offsets; //!< create material cells offsets? extern "C" bool output_summary; //!< write summary.h5? @@ -112,6 +121,10 @@ extern array energy_cutoff; //!< Energy cutoff in [eV] for each particle type extern array time_cutoff; //!< Time cutoff in [s] for each particle type +extern int + ifp_n_generation; //!< Number of generation for Iterated Fission Probability +extern IFPParameter + ifp_parameter; //!< Parameter to calculate for Iterated Fission Probability extern int legendre_to_tabular_points; //!< number of points to convert Legendres extern int max_order; //!< Maximum Legendre order for multigroup data diff --git a/openmc/lib/tally.py b/openmc/lib/tally.py index d0b34aedc..c17b16597 100644 --- a/openmc/lib/tally.py +++ b/openmc/lib/tally.py @@ -104,7 +104,9 @@ _SCORES = { -5: 'absorption', -6: 'fission', -7: 'nu-fission', -8: 'kappa-fission', -9: 'current', -10: 'events', -11: 'delayed-nu-fission', -12: 'prompt-nu-fission', -13: 'inverse-velocity', -14: 'fission-q-prompt', - -15: 'fission-q-recoverable', -16: 'decay-rate', -17: 'pulse-height' + -15: 'fission-q-recoverable', -16: 'decay-rate', -17: 'pulse-height', + -18: 'ifp-time-numerator', -19: 'ifp-beta-numerator', + -20: 'ifp-denominator', } _ESTIMATORS = { 0: 'analog', 1: 'tracklength', 2: 'collision' diff --git a/openmc/settings.py b/openmc/settings.py index a2bf51a51..6fb36f3fc 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -86,6 +86,9 @@ class Settings: .. versionadded:: 0.12 generations_per_batch : int Number of generations per batch + ifp_n_generation : int + Number of generations to consider for the Iterated Fission Probability + method. max_lost_particles : int Maximum number of lost particles @@ -375,6 +378,9 @@ class Settings: self._output = None + # Iterated Fission Probability + self._ifp_n_generation = None + # Output options self._statepoint = {} self._sourcepoint = {} @@ -826,6 +832,17 @@ class Settings: cv.check_less_than('verbosity', verbosity, 10, True) self._verbosity = verbosity + @property + def ifp_n_generation(self) -> int: + return self._ifp_n_generation + + @ifp_n_generation.setter + def ifp_n_generation(self, ifp_n_generation: int): + if ifp_n_generation is not None: + cv.check_type("number of generations", ifp_n_generation, Integral) + cv.check_greater_than("number of generations", ifp_n_generation, 0) + self._ifp_n_generation = ifp_n_generation + @property def tabular_legendre(self) -> dict: return self._tabular_legendre @@ -1455,6 +1472,11 @@ class Settings: element = ET.SubElement(root, "no_reduce") element.text = str(self._no_reduce).lower() + def _create_ifp_n_generation_subelement(self, root): + if self._ifp_n_generation is not None: + element = ET.SubElement(root, "ifp_n_generation") + element.text = str(self._ifp_n_generation) + def _create_tabular_legendre_subelements(self, root): if self.tabular_legendre: element = ET.SubElement(root, "tabular_legendre") @@ -1888,6 +1910,11 @@ class Settings: if text is not None: self.verbosity = int(text) + def _ifp_n_generation_from_xml_element(self, root): + text = get_text(root, 'ifp_n_generation') + if text is not None: + self.ifp_n_generation = int(text) + def _tabular_legendre_from_xml_element(self, root): elem = root.find('tabular_legendre') if elem is not None: @@ -2116,6 +2143,7 @@ class Settings: self._create_trigger_subelement(element) self._create_no_reduce_subelement(element) self._create_verbosity_subelement(element) + self._create_ifp_n_generation_subelement(element) self._create_tabular_legendre_subelements(element) self._create_temperature_subelements(element) self._create_trace_subelement(element) @@ -2225,6 +2253,7 @@ class Settings: settings._trigger_from_xml_element(elem) settings._no_reduce_from_xml_element(elem) settings._verbosity_from_xml_element(elem) + settings._ifp_n_generation_from_xml_element(elem) settings._tabular_legendre_from_xml_element(elem) settings._temperature_from_xml_element(elem) settings._trace_from_xml_element(elem) diff --git a/src/bank.cpp b/src/bank.cpp index 8d00d5440..9955939f6 100644 --- a/src/bank.cpp +++ b/src/bank.cpp @@ -1,6 +1,7 @@ #include "openmc/bank.h" #include "openmc/capi.h" #include "openmc/error.h" +#include "openmc/ifp.h" #include "openmc/message_passing.h" #include "openmc/simulation.h" #include "openmc/vector.h" @@ -26,6 +27,14 @@ SharedArray surf_source_bank; // function. SharedArray fission_bank; +vector> ifp_source_delayed_group_bank; + +vector> ifp_source_lifetime_bank; + +vector> ifp_fission_delayed_group_bank; + +vector> ifp_fission_lifetime_bank; + // Each entry in this vector corresponds to the number of progeny produced // this generation for the particle located at that index. This vector is // used to efficiently sort the fission bank after each iteration. @@ -43,6 +52,10 @@ void free_memory_bank() simulation::surf_source_bank.clear(); simulation::fission_bank.clear(); simulation::progeny_per_particle.clear(); + simulation::ifp_source_delayed_group_bank.clear(); + simulation::ifp_source_lifetime_bank.clear(); + simulation::ifp_fission_delayed_group_bank.clear(); + simulation::ifp_fission_lifetime_bank.clear(); } void init_fission_bank(int64_t max) @@ -82,6 +95,8 @@ void sort_fission_bank() // over provisioned, so we can use that as scratch space. SourceSite* sorted_bank; vector sorted_bank_holder; + vector> sorted_ifp_delayed_group_bank; + vector> sorted_ifp_lifetime_bank; // If there is not enough space, allocate a temporary vector and point to it if (simulation::fission_bank.size() > @@ -92,6 +107,11 @@ void sort_fission_bank() sorted_bank = &simulation::fission_bank[simulation::fission_bank.size()]; } + if (settings::ifp_on) { + allocate_temporary_vector_ifp( + sorted_ifp_delayed_group_bank, sorted_ifp_lifetime_bank); + } + // Use parent and progeny indices to sort fission bank for (int64_t i = 0; i < simulation::fission_bank.size(); i++) { const auto& site = simulation::fission_bank[i]; @@ -102,11 +122,19 @@ void sort_fission_bank() "shared fission bank size."); } sorted_bank[idx] = site; + if (settings::ifp_on) { + copy_ifp_data_from_fission_banks( + i, sorted_ifp_delayed_group_bank[idx], sorted_ifp_lifetime_bank[idx]); + } } // Copy sorted bank into the fission bank std::copy(sorted_bank, sorted_bank + simulation::fission_bank.size(), simulation::fission_bank.data()); + if (settings::ifp_on) { + copy_ifp_data_to_fission_banks( + sorted_ifp_delayed_group_bank.data(), sorted_ifp_lifetime_bank.data()); + } } //============================================================================== diff --git a/src/eigenvalue.cpp b/src/eigenvalue.cpp index 8669d76f9..2685bbe98 100644 --- a/src/eigenvalue.cpp +++ b/src/eigenvalue.cpp @@ -11,6 +11,7 @@ #include "openmc/constants.h" #include "openmc/error.h" #include "openmc/hdf5_interface.h" +#include "openmc/ifp.h" #include "openmc/math_functions.h" #include "openmc/mesh.h" #include "openmc/message_passing.h" @@ -153,8 +154,17 @@ void synchronize_bank() // Allocate temporary source bank -- we don't really know how many fission // sites were created, so overallocate by a factor of 3 int64_t index_temp = 0; + vector temp_sites(3 * simulation::work_per_rank); + // Temporary banks for IFP + vector> temp_delayed_groups; + vector> temp_lifetimes; + if (settings::ifp_on) { + resize_ifp_data( + temp_delayed_groups, temp_lifetimes, 3 * simulation::work_per_rank); + } + for (int64_t i = 0; i < simulation::fission_bank.size(); i++) { const auto& site = simulation::fission_bank[i]; @@ -165,6 +175,10 @@ void synchronize_bank() if (total < settings::n_particles) { for (int64_t j = 1; j <= settings::n_particles / total; ++j) { temp_sites[index_temp] = site; + if (settings::ifp_on) { + copy_ifp_data_from_fission_banks( + i, temp_delayed_groups[index_temp], temp_lifetimes[index_temp]); + } ++index_temp; } } @@ -172,6 +186,10 @@ void synchronize_bank() // Randomly sample sites needed if (prn(&seed) < p_sample) { temp_sites[index_temp] = site; + if (settings::ifp_on) { + copy_ifp_data_from_fission_banks( + i, temp_delayed_groups[index_temp], temp_lifetimes[index_temp]); + } ++index_temp; } } @@ -188,6 +206,8 @@ void synchronize_bank() MPI_Exscan(&index_temp, &start, 1, MPI_INT64_T, MPI_SUM, mpi::intracomm); finish = start + index_temp; + // TODO: protect for MPI_Exscan at rank 0 + // Allocate space for bank_position if this hasn't been done yet int64_t bank_position[mpi::n_procs]; MPI_Allgather( @@ -211,9 +231,15 @@ void synchronize_bank() // If we have too few sites, repeat sites from the very end of the // fission bank sites_needed = settings::n_particles - finish; + // TODO: sites_needed > simulation::fission_bank.size() or other test to + // make sure we don't need info from other proc for (int i = 0; i < sites_needed; ++i) { int i_bank = simulation::fission_bank.size() - sites_needed + i; temp_sites[index_temp] = simulation::fission_bank[i_bank]; + if (settings::ifp_on) { + copy_ifp_data_from_fission_banks(i_bank, + temp_delayed_groups[index_temp], temp_lifetimes[index_temp]); + } ++index_temp; } } @@ -229,15 +255,32 @@ void synchronize_bank() // ========================================================================== // SEND BANK SITES TO NEIGHBORS + // IFP number of generation + int ifp_n_generation; + if (settings::ifp_on) { + broadcast_ifp_n_generation( + ifp_n_generation, temp_delayed_groups, temp_lifetimes); + } + int64_t index_local = 0; vector requests; + // IFP send buffers + vector send_delayed_groups; + vector send_lifetimes; + if (start < settings::n_particles) { // Determine the index of the processor which has the first part of the // source_bank for the local processor int neighbor = upper_bound_index( simulation::work_index.begin(), simulation::work_index.end(), start); + // Resize IFP send buffers + if (settings::ifp_on && mpi::n_procs > 1) { + resize_ifp_data(send_delayed_groups, send_lifetimes, + ifp_n_generation * 3 * simulation::work_per_rank); + } + while (start < finish) { // Determine the number of sites to send int64_t n = @@ -250,6 +293,13 @@ void synchronize_bank() MPI_Isend(&temp_sites[index_local], static_cast(n), mpi::source_site, neighbor, mpi::rank, mpi::intracomm, &requests.back()); + + if (settings::ifp_on) { + // Send IFP data + send_ifp_info(index_local, n, ifp_n_generation, neighbor, requests, + temp_delayed_groups, send_delayed_groups, temp_lifetimes, + send_lifetimes); + } } // Increment all indices @@ -271,6 +321,11 @@ void synchronize_bank() start = simulation::work_index[mpi::rank]; index_local = 0; + // IFP receive buffers + vector recv_delayed_groups; + vector recv_lifetimes; + vector deserialization_info; + // Determine what process has the source sites that will need to be stored at // the beginning of this processor's source bank. @@ -282,6 +337,12 @@ void synchronize_bank() upper_bound_index(bank_position, bank_position + mpi::n_procs, start); } + // Resize IFP receive buffers + if (settings::ifp_on && mpi::n_procs > 1) { + resize_ifp_data(recv_delayed_groups, recv_lifetimes, + ifp_n_generation * simulation::work_per_rank); + } + while (start < simulation::work_index[mpi::rank + 1]) { // Determine how many sites need to be received int64_t n; @@ -301,13 +362,24 @@ void synchronize_bank() MPI_Irecv(&simulation::source_bank[index_local], static_cast(n), mpi::source_site, neighbor, neighbor, mpi::intracomm, &requests.back()); + if (settings::ifp_on) { + // Receive IFP data + receive_ifp_data(index_local, n, ifp_n_generation, neighbor, requests, + recv_delayed_groups, recv_lifetimes, deserialization_info); + } + } else { - // If the source sites are on this procesor, we can simply copy them + // If the source sites are on this processor, we can simply copy them // from the temp_sites bank index_temp = start - bank_position[mpi::rank]; std::copy(&temp_sites[index_temp], &temp_sites[index_temp + n], &simulation::source_bank[index_local]); + + if (settings::ifp_on) { + copy_partial_ifp_data_to_source_banks( + index_temp, n, index_local, temp_delayed_groups, temp_lifetimes); + } } // Increment all indices @@ -323,9 +395,17 @@ void synchronize_bank() int n_request = requests.size(); MPI_Waitall(n_request, requests.data(), MPI_STATUSES_IGNORE); + if (settings::ifp_on) { + deserialize_ifp_info(ifp_n_generation, deserialization_info, + recv_delayed_groups, recv_lifetimes); + } + #else std::copy(temp_sites.data(), temp_sites.data() + settings::n_particles, simulation::source_bank.begin()); + if (settings::ifp_on) { + copy_complete_ifp_data_to_source_banks(temp_delayed_groups, temp_lifetimes); + } #endif simulation::time_bank_sendrecv.stop(); diff --git a/src/finalize.cpp b/src/finalize.cpp index ed3d0e7f4..54aa1d1d9 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -171,8 +171,9 @@ int openmc_finalize() // Free all MPI types #ifdef OPENMC_MPI - if (mpi::source_site != MPI_DATATYPE_NULL) + if (mpi::source_site != MPI_DATATYPE_NULL) { MPI_Type_free(&mpi::source_site); + } #endif openmc_reset_random_ray(); diff --git a/src/ifp.cpp b/src/ifp.cpp new file mode 100644 index 000000000..1f81f26f6 --- /dev/null +++ b/src/ifp.cpp @@ -0,0 +1,216 @@ +#include "openmc/ifp.h" + +#include "openmc/bank.h" +#include "openmc/message_passing.h" +#include "openmc/particle.h" +#include "openmc/particle_data.h" +#include "openmc/settings.h" +#include "openmc/simulation.h" +#include "openmc/vector.h" + +namespace openmc { + +bool is_beta_effective_or_both() +{ + if (settings::ifp_parameter == IFPParameter::BetaEffective || + settings::ifp_parameter == IFPParameter::Both) { + return true; + } + return false; +} + +bool is_generation_time_or_both() +{ + if (settings::ifp_parameter == IFPParameter::GenerationTime || + settings::ifp_parameter == IFPParameter::Both) { + return true; + } + return false; +} + +void ifp(const Particle& p, const SourceSite& site, int64_t idx) +{ + if (is_beta_effective_or_both()) { + const auto& delayed_groups = + simulation::ifp_source_delayed_group_bank[p.current_work() - 1]; + simulation::ifp_fission_delayed_group_bank[idx] = + _ifp(site.delayed_group, delayed_groups); + } + if (is_generation_time_or_both()) { + const auto& lifetimes = + simulation::ifp_source_lifetime_bank[p.current_work() - 1]; + simulation::ifp_fission_lifetime_bank[idx] = _ifp(p.lifetime(), lifetimes); + } +} + +void resize_simulation_ifp_banks() +{ + resize_ifp_data(simulation::ifp_source_delayed_group_bank, + simulation::ifp_source_lifetime_bank, simulation::work_per_rank); + resize_ifp_data(simulation::ifp_fission_delayed_group_bank, + simulation::ifp_fission_lifetime_bank, 3 * simulation::work_per_rank); +} + +void copy_ifp_data_from_fission_banks( + int i_bank, vector& delayed_groups, vector& lifetimes) +{ + if (is_beta_effective_or_both()) { + delayed_groups = simulation::ifp_fission_delayed_group_bank[i_bank]; + } + if (is_generation_time_or_both()) { + lifetimes = simulation::ifp_fission_lifetime_bank[i_bank]; + } +} + +#ifdef OPENMC_MPI + +void broadcast_ifp_n_generation(int& n_generation, + const vector>& delayed_groups, + const vector>& lifetimes) +{ + if (mpi::rank == 0) { + if (is_beta_effective_or_both()) { + n_generation = static_cast(delayed_groups[0].size()); + } else { + n_generation = static_cast(lifetimes[0].size()); + } + } + MPI_Bcast(&n_generation, 1, MPI_INT, 0, mpi::intracomm); +} + +void send_ifp_info(int64_t idx, int64_t n, int n_generation, int neighbor, + vector& requests, const vector>& delayed_groups, + vector& send_delayed_groups, const vector>& lifetimes, + vector& send_lifetimes) +{ + // Copy data in send buffers + for (int i = idx; i < idx + n; i++) { + if (is_beta_effective_or_both()) { + std::copy(delayed_groups[i].begin(), delayed_groups[i].end(), + send_delayed_groups.begin() + i * n_generation); + } + if (is_generation_time_or_both()) { + std::copy(lifetimes[i].begin(), lifetimes[i].end(), + send_lifetimes.begin() + i * n_generation); + } + } + // Send delayed groups + if (is_beta_effective_or_both()) { + requests.emplace_back(); + MPI_Isend(&send_delayed_groups[n_generation * idx], + n_generation * static_cast(n), MPI_INT, neighbor, mpi::rank, + mpi::intracomm, &requests.back()); + } + // Send lifetimes + if (is_generation_time_or_both()) { + requests.emplace_back(); + MPI_Isend(&send_lifetimes[n_generation * idx], + n_generation * static_cast(n), MPI_DOUBLE, neighbor, mpi::rank, + mpi::intracomm, &requests.back()); + } +} + +void receive_ifp_data(int64_t idx, int64_t n, int n_generation, int neighbor, + vector& requests, vector& delayed_groups, + vector& lifetimes, vector& deserialization) +{ + // Receive delayed groups + if (is_beta_effective_or_both()) { + requests.emplace_back(); + MPI_Irecv(&delayed_groups[n_generation * idx], + n_generation * static_cast(n), MPI_INT, neighbor, neighbor, + mpi::intracomm, &requests.back()); + } + // Receive lifetimes + if (is_generation_time_or_both()) { + requests.emplace_back(); + MPI_Irecv(&lifetimes[n_generation * idx], + n_generation * static_cast(n), MPI_DOUBLE, neighbor, neighbor, + mpi::intracomm, &requests.back()); + } + // Deserialization info to reconstruct data later + DeserializationInfo info = {idx, n}; + deserialization.push_back(info); +} + +void copy_partial_ifp_data_to_source_banks(int64_t idx, int n, int64_t i_bank, + const vector>& delayed_groups, + const vector>& lifetimes) +{ + if (is_beta_effective_or_both()) { + std::copy(&delayed_groups[idx], &delayed_groups[idx + n], + &simulation::ifp_source_delayed_group_bank[i_bank]); + } + if (is_generation_time_or_both()) { + std::copy(&lifetimes[idx], &lifetimes[idx + n], + &simulation::ifp_source_lifetime_bank[i_bank]); + } +} + +void deserialize_ifp_info(int n_generation, + const vector& deserialization, + const vector& delayed_groups, const vector& lifetimes) +{ + for (auto info : deserialization) { + int64_t index_local = info.index_local; + int64_t n = info.n; + + for (int i = index_local; i < index_local + n; i++) { + if (is_beta_effective_or_both()) { + vector delayed_groups_received( + delayed_groups.begin() + n_generation * i, + delayed_groups.begin() + n_generation * (i + 1)); + simulation::ifp_source_delayed_group_bank[i] = delayed_groups_received; + } + if (is_generation_time_or_both()) { + vector lifetimes_received(lifetimes.begin() + n_generation * i, + lifetimes.begin() + n_generation * (i + 1)); + simulation::ifp_source_lifetime_bank[i] = lifetimes_received; + } + } + } +} + +#endif + +void copy_complete_ifp_data_to_source_banks( + const vector>& delayed_groups, + const vector>& lifetimes) +{ + if (is_beta_effective_or_both()) { + std::copy(delayed_groups.data(), + delayed_groups.data() + settings::n_particles, + simulation::ifp_source_delayed_group_bank.begin()); + } + if (is_generation_time_or_both()) { + std::copy(lifetimes.data(), lifetimes.data() + settings::n_particles, + simulation::ifp_source_lifetime_bank.begin()); + } +} + +void allocate_temporary_vector_ifp( + vector>& delayed_groups, vector>& lifetimes) +{ + if (is_beta_effective_or_both()) { + delayed_groups.resize(simulation::fission_bank.size()); + } + if (is_generation_time_or_both()) { + lifetimes.resize(simulation::fission_bank.size()); + } +} + +void copy_ifp_data_to_fission_banks(const vector* const delayed_groups_ptr, + const vector* lifetimes_ptr) +{ + if (is_beta_effective_or_both()) { + std::copy(delayed_groups_ptr, + delayed_groups_ptr + simulation::fission_bank.size(), + simulation::ifp_fission_delayed_group_bank.data()); + } + if (is_generation_time_or_both()) { + std::copy(lifetimes_ptr, lifetimes_ptr + simulation::fission_bank.size(), + simulation::ifp_fission_lifetime_bank.data()); + } +} + +} // namespace openmc diff --git a/src/output.cpp b/src/output.cpp index e20868efb..baaf682a1 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -593,6 +593,9 @@ const std::unordered_map score_names = { {SCORE_FISS_Q_RECOV, "Recoverable fission power"}, {SCORE_CURRENT, "Current"}, {SCORE_PULSE_HEIGHT, "pulse-height"}, + {SCORE_IFP_TIME_NUM, "IFP lifetime numerator"}, + {SCORE_IFP_BETA_NUM, "IFP delayed fraction numerator"}, + {SCORE_IFP_DENOM, "IFP common denominator"}, }; //! Create an ASCII output file showing all tally results. diff --git a/src/particle.cpp b/src/particle.cpp index c51011d6b..324276d15 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -116,6 +116,7 @@ void Particle::from_source(const SourceSite* src) n_collision() = 0; fission() = false; zero_flux_derivs(); + lifetime() = 0.0; // Copy attributes from source bank site type() = src->particle; @@ -235,7 +236,9 @@ void Particle::event_advance() for (int j = 0; j < n_coord(); ++j) { coord(j).r += distance * coord(j).u; } - this->time() += distance / this->speed(); + double dt = distance / this->speed(); + this->time() += dt; + this->lifetime() += dt; // Kill particle if its time exceeds the cutoff bool hit_time_boundary = false; @@ -243,6 +246,7 @@ void Particle::event_advance() if (time() > time_cutoff) { double dt = time() - time_cutoff; time() = time_cutoff; + lifetime() = time_cutoff; double push_back_distance = speed() * dt; this->move_distance(-push_back_distance); diff --git a/src/physics.cpp b/src/physics.cpp index 72c04a5ca..a8e5b9e81 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -8,6 +8,7 @@ #include "openmc/eigenvalue.h" #include "openmc/endf.h" #include "openmc/error.h" +#include "openmc/ifp.h" #include "openmc/material.h" #include "openmc/math_functions.h" #include "openmc/message_passing.h" @@ -233,6 +234,10 @@ void create_fission_sites(Particle& p, int i_nuclide, const Reaction& rx) // Break out of loop as no more sites can be added to fission bank break; } + // Iterated Fission Probability (IFP) method + if (settings::ifp_on) { + ifp(p, site, idx); + } } else { p.secondary_bank().push_back(site); } diff --git a/src/reaction.cpp b/src/reaction.cpp index 971473438..d96790c6d 100644 --- a/src/reaction.cpp +++ b/src/reaction.cpp @@ -202,6 +202,9 @@ std::unordered_map REACTION_NAME_MAP { {SCORE_FISS_Q_PROMPT, "fission-q-prompt"}, {SCORE_FISS_Q_RECOV, "fission-q-recoverable"}, {SCORE_PULSE_HEIGHT, "pulse-height"}, + {SCORE_IFP_TIME_NUM, "ifp-time-numerator"}, + {SCORE_IFP_BETA_NUM, "ifp-beta-numerator"}, + {SCORE_IFP_DENOM, "ifp-denominator"}, // Normal ENDF-based reactions {TOTAL_XS, "(n,total)"}, {ELASTIC, "(n,elastic)"}, diff --git a/src/settings.cpp b/src/settings.cpp index 53fcdec38..c135fa339 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -52,6 +52,7 @@ bool create_fission_neutrons {true}; bool delayed_photon_scaling {true}; bool entropy_on {false}; bool event_based {false}; +bool ifp_on {false}; bool legendre_to_tabular {true}; bool material_cell_offsets {true}; bool output_summary {true}; @@ -106,6 +107,8 @@ int max_particle_events {1000000}; ElectronTreatment electron_treatment {ElectronTreatment::TTB}; array energy_cutoff {0.0, 1000.0, 0.0, 0.0}; array time_cutoff {INFTY, INFTY, INFTY, INFTY}; +int ifp_n_generation {-1}; +IFPParameter ifp_parameter {IFPParameter::None}; int legendre_to_tabular_points {C_NONE}; int max_order {0}; int n_log_bins {8000}; @@ -1059,6 +1062,20 @@ void read_settings_xml(pugi::xml_node root) temperature_range[1] = range.at(1); } + // Check for user value for the number of generation of the Iterated Fission + // Probability (IFP) method + if (check_for_node(root, "ifp_n_generation")) { + ifp_n_generation = std::stoi(get_node_value(root, "ifp_n_generation")); + if (ifp_n_generation <= 0) { + fatal_error("'ifp_n_generation' must be greater than 0."); + } + // Avoid tallying 0 if IFP logs are not complete when active cycles start + if (ifp_n_generation > n_inactive) { + fatal_error("'ifp_n_generation' must be lower than or equal to the " + "number of inactive cycles."); + } + } + // Check for tabular_legendre options if (check_for_node(root, "tabular_legendre")) { // Get pointer to tabular_legendre node diff --git a/src/simulation.cpp b/src/simulation.cpp index 74a7dbe63..e4521c06d 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -7,6 +7,7 @@ #include "openmc/error.h" #include "openmc/event.h" #include "openmc/geometry_aux.h" +#include "openmc/ifp.h" #include "openmc/material.h" #include "openmc/mcpl_interface.h" #include "openmc/message_passing.h" @@ -335,6 +336,11 @@ void allocate_banks() // Allocate fission bank init_fission_bank(3 * simulation::work_per_rank); + + // Allocate IFP bank + if (settings::ifp_on) { + resize_simulation_ifp_banks(); + } } if (settings::surf_source_write) { diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index 96d684f71..35805b20d 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -180,6 +180,64 @@ Tally::Tally(pugi::xml_node node) fatal_error(fmt::format("No scores specified on tally {}.", id_)); } + // Set IFP if needed + if (!settings::ifp_on) { + // Determine if this tally has an IFP score + bool has_ifp_score = false; + for (int score : scores_) { + if (score == SCORE_IFP_TIME_NUM || score == SCORE_IFP_BETA_NUM || + score == SCORE_IFP_DENOM) { + has_ifp_score = true; + break; + } + } + + // Check for errors + if (has_ifp_score) { + if (settings::run_mode == RunMode::EIGENVALUE) { + if (settings::ifp_n_generation < 0) { + settings::ifp_n_generation = DEFAULT_IFP_N_GENERATION; + warning(fmt::format( + "{} generations will be used for IFP (default value). It can be " + "changed using the 'ifp_n_generation' settings.", + settings::ifp_n_generation)); + } + if (settings::ifp_n_generation > settings::n_inactive) { + fatal_error("'ifp_n_generation' must be lower than or equal to the " + "number of inactive cycles."); + } + settings::ifp_on = true; + } else { + fatal_error( + "Iterated Fission Probability can only be used in an eigenvalue " + "calculation."); + } + } + } + + // Set IFP parameters if needed + if (settings::ifp_on) { + for (int score : scores_) { + switch (score) { + case SCORE_IFP_TIME_NUM: + if (settings::ifp_parameter == IFPParameter::None) { + settings::ifp_parameter = IFPParameter::GenerationTime; + } else if (settings::ifp_parameter == IFPParameter::BetaEffective) { + settings::ifp_parameter = IFPParameter::Both; + } + break; + case SCORE_IFP_BETA_NUM: + case SCORE_IFP_DENOM: + if (settings::ifp_parameter == IFPParameter::None) { + settings::ifp_parameter = IFPParameter::BetaEffective; + } else if (settings::ifp_parameter == IFPParameter::GenerationTime) { + settings::ifp_parameter = IFPParameter::Both; + } + break; + } + } + } + // Check if tally is compatible with particle type if (!settings::photon_transport) { for (int score : scores_) { @@ -585,7 +643,12 @@ void Tally::set_scores(const vector& scores) } } } + break; + case SCORE_IFP_TIME_NUM: + case SCORE_IFP_BETA_NUM: + case SCORE_IFP_DENOM: + estimator_ = TallyEstimator::COLLISION; break; } diff --git a/src/tallies/tally_scoring.cpp b/src/tallies/tally_scoring.cpp index 02cb48567..5c6386fb4 100644 --- a/src/tallies/tally_scoring.cpp +++ b/src/tallies/tally_scoring.cpp @@ -4,6 +4,7 @@ #include "openmc/capi.h" #include "openmc/constants.h" #include "openmc/error.h" +#include "openmc/ifp.h" #include "openmc/material.h" #include "openmc/mgxs_interface.h" #include "openmc/nuclide.h" @@ -890,6 +891,56 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, score_fission_q(p, score_bin, tally, flux, i_nuclide, atom_density); break; + case SCORE_IFP_TIME_NUM: + if (settings::ifp_on) { + if ((p.type() == Type::neutron) && (p.fission())) { + if (is_generation_time_or_both()) { + const auto& lifetimes = + simulation::ifp_source_lifetime_bank[p.current_work() - 1]; + if (lifetimes.size() == settings::ifp_n_generation) { + score = lifetimes[0] * p.wgt_last(); + } + } + } + } + break; + + case SCORE_IFP_BETA_NUM: + if (settings::ifp_on) { + if ((p.type() == Type::neutron) && (p.fission())) { + if (is_beta_effective_or_both()) { + const auto& delayed_groups = + simulation::ifp_source_delayed_group_bank[p.current_work() - 1]; + if (delayed_groups.size() == settings::ifp_n_generation) { + if (delayed_groups[0] > 0) { + score = p.wgt_last(); + } + } + } + } + } + break; + + case SCORE_IFP_DENOM: + if (settings::ifp_on) { + if ((p.type() == Type::neutron) && (p.fission())) { + int ifp_data_size; + if (is_beta_effective_or_both()) { + ifp_data_size = static_cast( + simulation::ifp_source_delayed_group_bank[p.current_work() - 1] + .size()); + } else { + ifp_data_size = static_cast( + simulation::ifp_source_lifetime_bank[p.current_work() - 1] + .size()); + } + if (ifp_data_size == settings::ifp_n_generation) { + score = p.wgt_last(); + } + } + } + break; + case N_2N: case N_3N: case N_4N: diff --git a/tests/regression_tests/ifp/__init__.py b/tests/regression_tests/ifp/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/ifp/inputs_true.dat b/tests/regression_tests/ifp/inputs_true.dat new file mode 100644 index 000000000..a3a3f1d77 --- /dev/null +++ b/tests/regression_tests/ifp/inputs_true.dat @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + eigenvalue + 1000 + 20 + 5 + + + -10.0 -10.0 -10.0 10.0 10.0 10.0 + + + true + + + 5 + + + + ifp-time-numerator ifp-beta-numerator ifp-denominator + + + diff --git a/tests/regression_tests/ifp/results_true.dat b/tests/regression_tests/ifp/results_true.dat new file mode 100644 index 000000000..a74e2bd78 --- /dev/null +++ b/tests/regression_tests/ifp/results_true.dat @@ -0,0 +1,9 @@ +k-combined: +1.007452E+00 5.705278E-03 +tally 1: +8.996235E-08 +5.461421E-16 +4.800000E-02 +4.680000E-04 +1.512000E+01 +1.526063E+01 diff --git a/tests/regression_tests/ifp/test.py b/tests/regression_tests/ifp/test.py new file mode 100644 index 000000000..6969a54c4 --- /dev/null +++ b/tests/regression_tests/ifp/test.py @@ -0,0 +1,45 @@ +"""Test the Iterated Fission Probability (IFP) method to compute adjoint-weighted +kinetics parameters using dedicated tallies.""" + +import openmc +import pytest + +from tests.testing_harness import PyAPITestHarness + + +@pytest.fixture() +def ifp_model(): + model = openmc.Model() + + # Material + material = openmc.Material(name="core") + material.add_nuclide("U235", 1.0) + material.set_density('g/cm3', 16.0) + + # Geometry + radius = 10.0 + sphere = openmc.Sphere(r=radius, boundary_type="vacuum") + cell = openmc.Cell(region=-sphere, fill=material) + model.geometry = openmc.Geometry([cell]) + + # Settings + model.settings.particles = 1000 + model.settings.batches = 20 + model.settings.inactive = 5 + model.settings.ifp_n_generation = 5 + + space = openmc.stats.Box(*cell.bounding_box) + model.settings.source = openmc.IndependentSource( + space=space, constraints={'fissionable': True}) + + # Tally IFP scores + tally = openmc.Tally(name="ifp-scores") + tally.scores = ["ifp-time-numerator", "ifp-beta-numerator", "ifp-denominator"] + model.tallies = [tally] + + return model + + +def test_iterated_fission_probability(ifp_model): + harness = PyAPITestHarness("statepoint.20.h5", model=ifp_model) + harness.main() diff --git a/tests/unit_tests/test_ifp.py b/tests/unit_tests/test_ifp.py new file mode 100644 index 000000000..8d0fd9801 --- /dev/null +++ b/tests/unit_tests/test_ifp.py @@ -0,0 +1,49 @@ +"""Test the Iterated Fission Probability (IFP) method to compute adjoint-weighted +kinetics parameters using dedicated tallies.""" + +import pytest +import openmc + + +def test_xml_serialization(run_in_tmpdir): + """Check that a simple use case can be written and read in XML.""" + parameter = 5 + settings = openmc.Settings() + settings.ifp_n_generation = parameter + settings.export_to_xml() + + read_settings = openmc.Settings.from_xml() + assert read_settings.ifp_n_generation == parameter + + +@pytest.fixture(scope="module") +def geometry(): + openmc.reset_auto_ids() + material = openmc.Material() + material.add_nuclide("U235", 1.0) + sphere = openmc.Sphere(r=1.0, boundary_type="vacuum") + cell = openmc.Cell(region=-sphere, fill=material) + return openmc.Geometry([cell]) + + +@pytest.mark.parametrize( + "options, error", + [ + ({"ifp_n_generation": 0}, ValueError), + ({"ifp_n_generation": -1}, ValueError), + ({"run_mode": "fixed source"}, RuntimeError), + ({"inactive": 5, "ifp_n_generation": 6}, RuntimeError), + ({"inactive": 9}, RuntimeError) + ], +) +def test_exceptions(options, error, run_in_tmpdir, geometry): + """Test settings configuration that should return an error.""" + with pytest.raises(error): + settings = openmc.Settings(**options) + settings.particles = 100 + settings.batches = 15 + tally = openmc.Tally(name="ifp-scores") + tally.scores = ["ifp-time-numerator", "ifp-beta-numerator", "ifp-denominator"] + tallies = openmc.Tallies([tally]) + model = openmc.Model(geometry=geometry, settings=settings, tallies=tallies) + model.run() From a1134409863361c553014150949695407dea8287 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Tue, 22 Apr 2025 12:17:04 -0500 Subject: [PATCH 308/671] Small Fixes to Allow Random Ray to Work with DAGMC (#3374) Co-authored-by: Patrick Shriwise --- openmc/model/model.py | 121 +++++++++++++++-------- src/random_ray/random_ray.cpp | 2 +- src/random_ray/random_ray_simulation.cpp | 4 +- 3 files changed, 84 insertions(+), 43 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index 260059f4a..9ff574ec6 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -263,7 +263,7 @@ class Model: return model def init_lib(self, threads=None, geometry_debug=False, restart_file=None, - tracks=False, output=True, event_based=None, intracomm=None): + tracks=False, output=True, event_based=None, intracomm=None, directory=None): """Initializes the model in memory via the C API .. versionadded:: 0.13.0 @@ -291,6 +291,8 @@ class Model: the Settings will be used. intracomm : mpi4py.MPI.Intracomm or None, optional MPI intracommunicator + directory : str or None, optional + Directory to write XML files to. Defaults to None. """ import openmc.lib @@ -304,7 +306,8 @@ class Model: args = _process_CLI_arguments( volume=False, geometry_debug=geometry_debug, restart_file=restart_file, threads=threads, tracks=tracks, - event_based=event_based) + event_based=event_based, path_input=directory) + # Args adds the openmc_exec command in the first entry; remove it args = args[1:] @@ -318,7 +321,10 @@ class Model: self._intracomm = DummyCommunicator() if self._intracomm.rank == 0: - self.export_to_xml() + if directory is not None: + self.export_to_xml(directory=directory) + else: + self.export_to_xml() self._intracomm.barrier() # We cannot pass DummyCommunicator to openmc.lib.init so pass instead @@ -1443,7 +1449,7 @@ class Model: self.geometry.get_all_materials().values() ) - def _generate_infinite_medium_mgxs(self, groups, nparticles, mgxs_path, correction): + def _generate_infinite_medium_mgxs(self, groups, nparticles, mgxs_path, correction, directory): """Generate a MGXS library by running multiple OpenMC simulations, each representing an infinite medium simulation of a single isolated material. A discrete source is used to sample particles, with an equal @@ -1455,8 +1461,15 @@ class Model: ---------- groups : openmc.mgxs.EnergyGroups Energy group structure for the MGXS. - mgxs_path : path-like + nparticles : int + Number of particles to simulate per batch when generating MGXS. + mgxs_path : str Filename for the MGXS HDF5 file. + correction : str + Transport correction to apply to the MGXS. Options are None and + "P0". + directory : str + Directory to run the simulation in, so as to contain XML files. """ warnings.warn("The infinite medium method of generating MGXS may hang " "if a material has a k-infinity > 1.0.") @@ -1537,7 +1550,7 @@ class Model: mgxs_lib.add_to_tallies_file(model.tallies, merge=True) # Run - statepoint_filename = model.run() + statepoint_filename = model.run(cwd=directory) # Load MGXS with openmc.StatePoint(statepoint_filename) as sp: @@ -1623,7 +1636,7 @@ class Model: return geometry, box - def _generate_stochastic_slab_mgxs(self, groups, nparticles, mgxs_path, correction) -> None: + def _generate_stochastic_slab_mgxs(self, groups, nparticles, mgxs_path, correction, directory) -> None: """Generate MGXS assuming a stochastic "sandwich" of materials in a layered slab geometry. While geometry-specific spatial shielding effects are not captured, this method can be useful when the geometry has materials only @@ -1638,8 +1651,15 @@ class Model: ---------- groups : openmc.mgxs.EnergyGroups Energy group structure for the MGXS. - mgxs_path : path-like + nparticles : int + Number of particles to simulate per batch when generating MGXS. + mgxs_path : str Filename for the MGXS HDF5 file. + correction : str + Transport correction to apply to the MGXS. Options are None and + "P0". + directory : str + Directory to run the simulation in, so as to contain XML files. """ openmc.reset_auto_ids() model = openmc.Model() @@ -1709,7 +1729,7 @@ class Model: mgxs_lib.add_to_tallies_file(model.tallies, merge=True) # Run - statepoint_filename = model.run() + statepoint_filename = model.run(cwd=directory) # Load MGXS with openmc.StatePoint(statepoint_filename) as sp: @@ -1721,7 +1741,7 @@ class Model: mgxs_file = mgxs_lib.create_mg_library(xs_type='macro', xsdata_names=names) mgxs_file.export_to_hdf5(mgxs_path) - def _generate_material_wise_mgxs(self, groups, nparticles, mgxs_path, correction) -> None: + def _generate_material_wise_mgxs(self, groups, nparticles, mgxs_path, correction, directory) -> None: """Generate a material-wise MGXS library for the model by running the original continuous energy OpenMC simulation of the full material geometry and source, and tally MGXS data for each material. This method @@ -1736,8 +1756,15 @@ class Model: ---------- groups : openmc.mgxs.EnergyGroups Energy group structure for the MGXS. + nparticles : int + Number of particles to simulate per batch when generating MGXS. mgxs_path : str Filename for the MGXS HDF5 file. + correction : str + Transport correction to apply to the MGXS. Options are None and + "P0". + directory : str + Directory to run the simulation in, so as to contain XML files. """ openmc.reset_auto_ids() model = copy.deepcopy(self) @@ -1791,7 +1818,7 @@ class Model: mgxs_lib.add_to_tallies_file(model.tallies, merge=True) # Run - statepoint_filename = model.run() + statepoint_filename = model.run(cwd=directory) # Load MGXS with openmc.StatePoint(statepoint_filename) as sp: @@ -1828,39 +1855,53 @@ class Model: if isinstance(groups, str): groups = openmc.mgxs.EnergyGroups(groups) - # Make sure all materials have a name, and that the name is a valid HDF5 - # dataset name - for material in self.materials: - if material.name is None: - material.name = f"material {material.id}" - material.name = re.sub(r'[^a-zA-Z0-9]', '_', material.name) + # Do all work (including MGXS generation) in a temporary directory + # to avoid polluting the working directory with residual XML files + with TemporaryDirectory() as tmpdir: - # If needed, generate the needed MGXS data library file - if not Path(mgxs_path).is_file() or overwrite_mgxs_library: - if method == "infinite_medium": - self._generate_infinite_medium_mgxs( - groups, nparticles, mgxs_path, correction) - elif method == "material_wise": - self._generate_material_wise_mgxs( - groups, nparticles, mgxs_path, correction) - elif method == "stochastic_slab": - self._generate_stochastic_slab_mgxs( - groups, nparticles, mgxs_path, correction) + # Determine if there are DAGMC universes in the model. If so, we need to synchronize + # the dagmc materials with cells. + # TODO: Can this be done without having to init/finalize? + for univ in self.geometry.get_all_universes().values(): + if isinstance(univ, openmc.DAGMCUniverse): + self.init_lib(directory=tmpdir) + self.sync_dagmc_universes() + self.finalize_lib() + break + + # Make sure all materials have a name, and that the name is a valid HDF5 + # dataset name + for material in self.materials: + if material.name is None: + material.name = f"material {material.id}" + material.name = re.sub(r'[^a-zA-Z0-9]', '_', material.name) + + # If needed, generate the needed MGXS data library file + if not Path(mgxs_path).is_file() or overwrite_mgxs_library: + if method == "infinite_medium": + self._generate_infinite_medium_mgxs( + groups, nparticles, mgxs_path, correction, tmpdir) + elif method == "material_wise": + self._generate_material_wise_mgxs( + groups, nparticles, mgxs_path, correction, tmpdir) + elif method == "stochastic_slab": + self._generate_stochastic_slab_mgxs( + groups, nparticles, mgxs_path, correction, tmpdir) + else: + raise ValueError( + f'MGXS generation method "{method}" not recognized') else: - raise ValueError( - f'MGXS generation method "{method}" not recognized') - else: - print(f'Existing MGXS library file "{mgxs_path}" will be used') + print(f'Existing MGXS library file "{mgxs_path}" will be used') - # Convert all continuous energy materials to multigroup - self.materials.cross_sections = mgxs_path - for material in self.materials: - material.set_density('macro', 1.0) - material._nuclides = [] - material._sab = [] - material.add_macroscopic(material.name) + # Convert all continuous energy materials to multigroup + self.materials.cross_sections = mgxs_path + for material in self.materials: + material.set_density('macro', 1.0) + material._nuclides = [] + material._sab = [] + material.add_macroscopic(material.name) - self.settings.energy_mode = 'multi-group' + self.settings.energy_mode = 'multi-group' def convert_to_random_ray(self): """Convert a multigroup model to use random ray. diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp index 9a9d1394a..532840f1f 100644 --- a/src/random_ray/random_ray.cpp +++ b/src/random_ray/random_ray.cpp @@ -277,7 +277,7 @@ void RandomRay::event_advance_ray() boundary() = distance_to_boundary(*this); double distance = boundary().distance; - if (distance <= 0.0) { + if (distance < 0.0) { mark_as_lost("Negative transport distance detected for particle " + std::to_string(id())); return; diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index b831efcd9..40a08c3af 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -196,8 +196,8 @@ void validate_random_ray_inputs() "supported in random ray mode."); } if (material.get_xsdata().size() > 1) { - fatal_error("Non-isothermal MGXS detected. Only isothermal XS data sets " - "supported in random ray mode."); + warning("Non-isothermal MGXS detected. Only isothermal XS data sets " + "supported in random ray mode. Using lowest temperature."); } for (int g = 0; g < data::mg.num_energy_groups_; g++) { if (material.exists_in_model) { From 5dd6ff6527b073aa214806e01f82f24447593fec Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Tue, 22 Apr 2025 23:00:44 -0500 Subject: [PATCH 309/671] Fix negative distances from bins_crossed for CylindricalMesh (#3370) --- include/openmc/mesh.h | 5 --- src/mesh.cpp | 37 +++++++++---------- .../filter_mesh/inputs_true.dat | 4 +- tests/regression_tests/filter_mesh/test.py | 3 +- tests/unit_tests/test_cylindrical_mesh.py | 11 ++++-- tests/unit_tests/test_spherical_mesh.py | 2 - 6 files changed, 29 insertions(+), 33 deletions(-) diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index 8faf45f1b..5a727c2b6 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -138,9 +138,6 @@ public: //! Perform any preparation needed to support point location within the mesh virtual void prepare_for_point_location() {}; - //! Update a position to the local coordinates of the mesh - virtual void local_coords(Position& r) const {}; - //! Return a position in the local coordinates of the mesh virtual Position local_coords(const Position& r) const { return r; }; @@ -427,8 +424,6 @@ public: PeriodicStructuredMesh() = default; PeriodicStructuredMesh(pugi::xml_node node) : StructuredMesh {node} {}; - void local_coords(Position& r) const override { r -= origin_; }; - Position local_coords(const Position& r) const override { return r - origin_; diff --git a/src/mesh.cpp b/src/mesh.cpp index 101da7468..8280177ac 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -915,6 +915,11 @@ void StructuredMesh::raytrace_mesh( if (total_distance == 0.0 && settings::solver_type != SolverType::RANDOM_RAY) return; + // keep a copy of the original global position to pass to get_indices, + // which performs its own transformation to local coordinates + Position global_r = r0; + Position local_r = local_coords(r0); + const int n = n_dimension_; // Flag if position is inside the mesh @@ -925,7 +930,7 @@ void StructuredMesh::raytrace_mesh( // Calculate index of current cell. Offset the position a tiny bit in // direction of flight - MeshIndex ijk = get_indices(r0 + TINY_BIT * u, in_mesh); + MeshIndex ijk = get_indices(global_r + TINY_BIT * u, in_mesh); // if track is very short, assume that it is completely inside one cell. // Only the current cell will score and no surfaces @@ -936,16 +941,10 @@ void StructuredMesh::raytrace_mesh( return; } - // translate start and end positions, - // this needs to come after the get_indices call because it does its own - // translation - local_coords(r0); - local_coords(r1); - // Calculate initial distances to next surfaces in all three dimensions std::array distances; for (int k = 0; k < n; ++k) { - distances[k] = distance_to_grid_boundary(ijk, k, r0, u, 0.0); + distances[k] = distance_to_grid_boundary(ijk, k, local_r, u, 0.0); } // Loop until r = r1 is eventually reached @@ -975,7 +974,7 @@ void StructuredMesh::raytrace_mesh( // The two other directions are still valid! ijk[k] = distances[k].next_index; distances[k] = - distance_to_grid_boundary(ijk, k, r0, u, traveled_distance); + distance_to_grid_boundary(ijk, k, local_r, u, traveled_distance); // Check if we have left the interior of the mesh in_mesh = ((ijk[k] >= 1) && (ijk[k] <= shape_[k])); @@ -1005,10 +1004,10 @@ void StructuredMesh::raytrace_mesh( // Calculate the new cell index and update all distances to next // surfaces. - ijk = get_indices(r0 + (traveled_distance + TINY_BIT) * u, in_mesh); + ijk = get_indices(global_r + (traveled_distance + TINY_BIT) * u, in_mesh); for (int k = 0; k < n; ++k) { distances[k] = - distance_to_grid_boundary(ijk, k, r0, u, traveled_distance); + distance_to_grid_boundary(ijk, k, local_r, u, traveled_distance); } // If inside the mesh, Tally inward current @@ -1482,7 +1481,7 @@ std::string CylindricalMesh::get_mesh_type() const StructuredMesh::MeshIndex CylindricalMesh::get_indices( Position r, bool& in_mesh) const { - local_coords(r); + r = local_coords(r); Position mapped_r; mapped_r[0] = std::hypot(r.x, r.y); @@ -1630,23 +1629,21 @@ StructuredMesh::MeshDistance CylindricalMesh::distance_to_grid_boundary( const MeshIndex& ijk, int i, const Position& r0, const Direction& u, double l) const { - Position r = r0 - origin_; - if (i == 0) { return std::min( - MeshDistance(ijk[i] + 1, true, find_r_crossing(r, u, l, ijk[i])), - MeshDistance(ijk[i] - 1, false, find_r_crossing(r, u, l, ijk[i] - 1))); + MeshDistance(ijk[i] + 1, true, find_r_crossing(r0, u, l, ijk[i])), + MeshDistance(ijk[i] - 1, false, find_r_crossing(r0, u, l, ijk[i] - 1))); } else if (i == 1) { return std::min(MeshDistance(sanitize_phi(ijk[i] + 1), true, - find_phi_crossing(r, u, l, ijk[i])), + find_phi_crossing(r0, u, l, ijk[i])), MeshDistance(sanitize_phi(ijk[i] - 1), false, - find_phi_crossing(r, u, l, ijk[i] - 1))); + find_phi_crossing(r0, u, l, ijk[i] - 1))); } else { - return find_z_crossing(r, u, l, ijk[i]); + return find_z_crossing(r0, u, l, ijk[i]); } } @@ -1762,7 +1759,7 @@ std::string SphericalMesh::get_mesh_type() const StructuredMesh::MeshIndex SphericalMesh::get_indices( Position r, bool& in_mesh) const { - local_coords(r); + r = local_coords(r); Position mapped_r; mapped_r[0] = r.norm(); diff --git a/tests/regression_tests/filter_mesh/inputs_true.dat b/tests/regression_tests/filter_mesh/inputs_true.dat index 0667c0341..4995749bb 100644 --- a/tests/regression_tests/filter_mesh/inputs_true.dat +++ b/tests/regression_tests/filter_mesh/inputs_true.dat @@ -54,8 +54,8 @@ 0.0 0.4411764705882353 0.8823529411764706 1.3235294117647058 1.7647058823529411 2.2058823529411766 2.6470588235294117 3.0882352941176467 3.5294117647058822 3.9705882352941178 4.411764705882353 4.852941176470588 5.294117647058823 5.735294117647059 6.1764705882352935 6.617647058823529 7.0588235294117645 7.5 0.0 0.3490658503988659 0.6981317007977318 1.0471975511965976 1.3962634015954636 1.7453292519943295 2.0943951023931953 2.443460952792061 2.792526803190927 3.141592653589793 3.490658503988659 3.839724354387525 4.1887902047863905 4.537856055185257 4.886921905584122 5.235987755982989 5.585053606381854 5.93411945678072 6.283185307179586 - -7.5 -6.5625 -5.625 -4.6875 -3.75 -2.8125 -1.875 -0.9375 0.0 0.9375 1.875 2.8125 3.75 4.6875 5.625 6.5625 7.5 - 0.0 0.0 0.0 + 0.0 0.9375 1.875 2.8125 3.75 4.6875 5.625 6.5625 7.5 8.4375 9.375 10.3125 11.25 12.1875 13.125 14.0625 15.0 + 0.0 0.0 -7.5 0.0 0.4411764705882353 0.8823529411764706 1.3235294117647058 1.7647058823529411 2.2058823529411766 2.6470588235294117 3.0882352941176467 3.5294117647058822 3.9705882352941178 4.411764705882353 4.852941176470588 5.294117647058823 5.735294117647059 6.1764705882352935 6.617647058823529 7.0588235294117645 7.5 diff --git a/tests/regression_tests/filter_mesh/test.py b/tests/regression_tests/filter_mesh/test.py index 6214e0486..165ba2a0c 100644 --- a/tests/regression_tests/filter_mesh/test.py +++ b/tests/regression_tests/filter_mesh/test.py @@ -61,9 +61,10 @@ def model(): np.testing.assert_allclose(recti_mesh.volumes, recti_mesh_exp_vols) cyl_mesh = openmc.CylindricalMesh( + origin=(0, 0, -7.5), r_grid=np.linspace(0, 7.5, 18), phi_grid=np.linspace(0, 2*pi, 19), - z_grid=np.linspace(-7.5, 7.5, 17), + z_grid=np.linspace(0, 15, 17), ) dr = 0.5 * np.diff(np.linspace(0, 7.5, 18)**2) dp = np.full(cyl_mesh.dimension[1], 2*pi / 18) diff --git a/tests/unit_tests/test_cylindrical_mesh.py b/tests/unit_tests/test_cylindrical_mesh.py index b408bc0e9..645269825 100644 --- a/tests/unit_tests/test_cylindrical_mesh.py +++ b/tests/unit_tests/test_cylindrical_mesh.py @@ -49,6 +49,7 @@ def model(): return openmc.Model(geometry=geom, settings=settings, tallies=tallies) + def test_origin_read_write_to_xml(run_in_tmpdir, model): """Tests that the origin attribute can be written and read back to XML """ @@ -63,8 +64,9 @@ def test_origin_read_write_to_xml(run_in_tmpdir, model): np.testing.assert_equal(new_mesh.origin, mesh.origin) estimators = ('tracklength', 'collision') -origins = set(permutations((-geom_size, 0, 0))) -origins |= set(permutations((geom_size, 0, 0))) +offset = geom_size + 0.001 +origins = set(permutations((-offset , 0, 0))) +origins |= set(permutations((offset, 0, 0))) test_cases = product(estimators, origins) @@ -74,8 +76,9 @@ def label(p): if isinstance(p, str): return f'estimator:{p}' + @pytest.mark.parametrize('estimator,origin', test_cases, ids=label) -def test_offset_mesh(run_in_tmpdir, model, estimator, origin): +def test_offset_mesh(model, estimator, origin): """Tests that the mesh has been moved based on tally results """ mesh = model.tallies[0].filters[0].mesh @@ -103,6 +106,7 @@ def test_offset_mesh(run_in_tmpdir, model, estimator, origin): else: mean[i, j, k] != 0.0 + @pytest.fixture() def void_coincident_geom_model(): """A model with many geometric boundaries coincident with mesh boundaries @@ -155,6 +159,7 @@ def _check_void_cylindrical_tally(statepoint_filename): d_r = mesh.r_grid[1] - mesh.r_grid[0] assert neutron_flux == pytest.approx(d_r) + def test_void_geom_pnt_src(run_in_tmpdir, void_coincident_geom_model): src = openmc.IndependentSource() src.space = openmc.stats.Point() diff --git a/tests/unit_tests/test_spherical_mesh.py b/tests/unit_tests/test_spherical_mesh.py index 0b579be35..a95a4151a 100644 --- a/tests/unit_tests/test_spherical_mesh.py +++ b/tests/unit_tests/test_spherical_mesh.py @@ -63,8 +63,6 @@ def test_origin_read_write_to_xml(run_in_tmpdir, model): np.testing.assert_equal(new_mesh.origin, mesh.origin) estimators = ('tracklength', 'collision') -# TODO: determine why this is needed for spherical mesh -# but not cylindrical mesh offset = geom_size + 0.001 origins = set(permutations((-offset, 0, 0))) From 820648daeee2d3323ce2125f7ad67c4e23a8cd2e Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Wed, 23 Apr 2025 08:44:46 +0200 Subject: [PATCH 310/671] using reduce chain level to remove need for reduce chain (#3377) Co-authored-by: Jon Shimwell Co-authored-by: Paul Romano --- openmc/deplete/coupled_operator.py | 13 +++---------- openmc/deplete/independent_operator.py | 20 ++++---------------- openmc/deplete/openmc_operator.py | 16 +++++++--------- tests/unit_tests/test_model.py | 4 ++-- 4 files changed, 16 insertions(+), 37 deletions(-) diff --git a/openmc/deplete/coupled_operator.py b/openmc/deplete/coupled_operator.py index 17af935f4..7ecfd5a08 100644 --- a/openmc/deplete/coupled_operator.py +++ b/openmc/deplete/coupled_operator.py @@ -154,15 +154,9 @@ class CoupledOperator(OpenMCOperator): options. .. versionadded:: 0.12.1 - reduce_chain : bool, optional - If True, use :meth:`openmc.deplete.Chain.reduce` to reduce the - depletion chain up to ``reduce_chain_level``. - - .. versionadded:: 0.12 reduce_chain_level : int, optional - Depth of the search when reducing the depletion chain. Only used - if ``reduce_chain`` evaluates to true. The default value of - ``None`` implies no limit on the depth. + Depth of the search when reducing the depletion chain. The default + value of ``None`` implies no limit on the depth. .. versionadded:: 0.12 diff_volume_method : str @@ -214,7 +208,7 @@ class CoupledOperator(OpenMCOperator): normalization_mode="fission-q", fission_q=None, fission_yield_mode="constant", fission_yield_opts=None, reaction_rate_mode="direct", reaction_rate_opts=None, - reduce_chain=False, reduce_chain_level=None): + reduce_chain_level=None): # check for old call to constructor if isinstance(model, openmc.Geometry): @@ -270,7 +264,6 @@ class CoupledOperator(OpenMCOperator): diff_volume_method=diff_volume_method, fission_q=fission_q, helper_kwargs=helper_kwargs, - reduce_chain=reduce_chain, reduce_chain_level=reduce_chain_level) def _differentiate_burnable_mats(self): diff --git a/openmc/deplete/independent_operator.py b/openmc/deplete/independent_operator.py index 333ead0f8..b3fe9a78f 100644 --- a/openmc/deplete/independent_operator.py +++ b/openmc/deplete/independent_operator.py @@ -66,13 +66,9 @@ class IndependentOperator(OpenMCOperator): Dictionary of nuclides and their fission Q values [eV]. If not given, values will be pulled from the ``chain_file``. Only applicable if ``"normalization_mode" == "fission-q"``. - reduce_chain : bool, optional - If True, use :meth:`openmc.deplete.Chain.reduce` to reduce the depletion - chain up to ``reduce_chain_level``. reduce_chain_level : int, optional - Depth of the search when reducing the depletion chain. Only used if - ``reduce_chain`` evaluates to true. The default value of ``None`` - implies no limit on the depth. + Depth of the search when reducing the depletion chain. The default + value of ``None`` implies no limit on the depth. fission_yield_opts : dict of str to option, optional Optional arguments to pass to the :class:`openmc.deplete.helpers.FissionYieldHelper` object. Will be @@ -119,7 +115,6 @@ class IndependentOperator(OpenMCOperator): normalization_mode='fission-q', fission_q=None, prev_results=None, - reduce_chain=False, reduce_chain_level=None, fission_yield_opts=None): # Validate micro-xs parameters @@ -157,7 +152,6 @@ class IndependentOperator(OpenMCOperator): prev_results=prev_results, fission_q=fission_q, helper_kwargs=helper_kwargs, - reduce_chain=reduce_chain, reduce_chain_level=reduce_chain_level) @classmethod @@ -170,7 +164,6 @@ class IndependentOperator(OpenMCOperator): normalization_mode='fission-q', fission_q=None, prev_results=None, - reduce_chain=False, reduce_chain_level=None, fission_yield_opts=None): """ @@ -206,13 +199,9 @@ class IndependentOperator(OpenMCOperator): applicable if ``"normalization_mode" == "fission-q"``. prev_results : Results, optional Results from a previous depletion calculation. - reduce_chain : bool, optional - If True, use :meth:`openmc.deplete.Chain.reduce` to reduce the - depletion chain up to ``reduce_chain_level``. Default is False. reduce_chain_level : int, optional - Depth of the search when reducing the depletion chain. Only used - if ``reduce_chain`` evaluates to true. The default value of - ``None`` implies no limit on the depth. + Depth of the search when reducing the depletion chain. The default + value of ``None`` implies no limit on the depth. fission_yield_opts : dict of str to option, optional Optional arguments to pass to the :class:`openmc.deplete.helpers.FissionYieldHelper` class. Will be @@ -232,7 +221,6 @@ class IndependentOperator(OpenMCOperator): normalization_mode=normalization_mode, fission_q=fission_q, prev_results=prev_results, - reduce_chain=reduce_chain, reduce_chain_level=reduce_chain_level, fission_yield_opts=fission_yield_opts) diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py index 64cb0a7e5..4ef867372 100644 --- a/openmc/deplete/openmc_operator.py +++ b/openmc/deplete/openmc_operator.py @@ -11,7 +11,7 @@ from warnings import warn import numpy as np import openmc -from openmc.checkvalue import check_value +from openmc.checkvalue import check_value, check_type, check_greater_than from openmc.exceptions import DataError from openmc.mpi import comm from .abc import TransportOperator, OperatorResult @@ -49,13 +49,9 @@ class OpenMCOperator(TransportOperator): Dictionary of nuclides and their fission Q values [eV]. helper_kwargs : dict Keyword arguments for helper classes - reduce_chain : bool, optional - If True, use :meth:`openmc.deplete.Chain.reduce()` to reduce the - depletion chain up to ``reduce_chain_level``. reduce_chain_level : int, optional - Depth of the search when reducing the depletion chain. Only used - if ``reduce_chain`` evaluates to true. The default value of - ``None`` implies no limit on the depth. + Depth of the search when reducing the depletion chain. The default + value of ``None`` implies no limit on the depth. diff_volume_method : str Specifies how the volumes of the new materials should be found. Default @@ -107,7 +103,6 @@ class OpenMCOperator(TransportOperator): diff_volume_method='divide equally', fission_q=None, helper_kwargs=None, - reduce_chain=False, reduce_chain_level=None): # If chain file was not specified, try to get it from global config @@ -126,10 +121,13 @@ class OpenMCOperator(TransportOperator): check_value('diff volume method', diff_volume_method, {'divide equally', 'match cell'}) + if reduce_chain_level: + check_type('reduce_chain_level', reduce_chain_level, int) + check_greater_than('reduce_chain_level', reduce_chain_level, 0) self.diff_volume_method = diff_volume_method # Reduce the chain to only those nuclides present - if reduce_chain: + if reduce_chain_level is not None: init_nuclides = set() for material in self.materials: if not material.depletable: diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index ee5d8895c..aa844e2f2 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -448,7 +448,7 @@ def test_deplete(run_in_tmpdir, pin_model_attributes, mpi_intracomm): # In this test we first run without pre-initializing the shared library # data and then compare. Then we repeat with the C API already initialized # and make sure we get the same answer - test_model.deplete([1e6], 'predictor', final_step=False, + test_model.deplete(timesteps=[1e6], method='predictor', final_step=False, operator_kwargs=op_kwargs, power=1., output=False) # Get the new Xe136 and U235 atom densities @@ -482,7 +482,7 @@ def test_deplete(run_in_tmpdir, pin_model_attributes, mpi_intracomm): # Now we can re-run with the pre-initialized API test_model.init_lib(output=False, intracomm=mpi_intracomm) - test_model.deplete([1e6], 'predictor', final_step=False, + test_model.deplete(timesteps=[1e6], method='predictor', final_step=False, operator_kwargs=op_kwargs, power=1., output=False) # Get the new Xe136 and U235 atom densities From c17908f24ccc9ae3ae47686cacd8faa78f879d9c Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 2 May 2025 18:38:10 -0500 Subject: [PATCH 311/671] Install MCPL using same build type as OpenMC in CI (#3388) --- CMakeLists.txt | 7 +++++++ cmake/OpenMCConfig.cmake.in | 2 +- tools/ci/gha-install-mcpl.sh | 7 ------- tools/ci/gha-install.sh | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) delete mode 100755 tools/ci/gha-install-mcpl.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index 8cb03a1a1..d79746209 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -194,6 +194,13 @@ endif() #=============================================================================== if (OPENMC_USE_MCPL) + if (NOT DEFINED MCPL_DIR) + execute_process( + COMMAND mcpl-config --show cmakedir + OUTPUT_VARIABLE MCPL_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + endif() find_package(MCPL REQUIRED) message(STATUS "Found MCPL: ${MCPL_DIR} (found version \"${MCPL_VERSION}\")") endif() diff --git a/cmake/OpenMCConfig.cmake.in b/cmake/OpenMCConfig.cmake.in index 5cab4790a..7416013fb 100644 --- a/cmake/OpenMCConfig.cmake.in +++ b/cmake/OpenMCConfig.cmake.in @@ -30,7 +30,7 @@ if(@OPENMC_USE_OPENMP@) endif() if(@OPENMC_USE_MCPL@) - find_package(MCPL REQUIRED) + find_package(MCPL REQUIRED HINTS @MCPL_DIR@) endif() if(@OPENMC_USE_UWUW@ AND NOT ${DAGMC_BUILD_UWUW}) diff --git a/tools/ci/gha-install-mcpl.sh b/tools/ci/gha-install-mcpl.sh deleted file mode 100755 index 9b8609398..000000000 --- a/tools/ci/gha-install-mcpl.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -set -ex -cd $HOME -git clone https://github.com/mctools/mcpl -cd mcpl -mkdir build && cd build -cmake .. && make 2>/dev/null && sudo make install diff --git a/tools/ci/gha-install.sh b/tools/ci/gha-install.sh index d8a3a7600..74c3947f1 100755 --- a/tools/ci/gha-install.sh +++ b/tools/ci/gha-install.sh @@ -29,7 +29,7 @@ if [[ $LIBMESH = 'y' ]]; then fi # Install MCPL -./tools/ci/gha-install-mcpl.sh +pip install mcpl # For MPI configurations, make sure mpi4py and h5py are built against the # correct version of MPI From 4138bc34e08777d2a0c7c91748c00aacf048e136 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 2 May 2025 19:07:43 -0500 Subject: [PATCH 312/671] Add option to determine waste disposal rating by nuclide (#3376) --- openmc/material.py | 22 ++++++++++------ openmc/waste.py | 25 ++++++++++++------- tests/unit_tests/test_waste_classification.py | 4 +++ 3 files changed, 35 insertions(+), 16 deletions(-) diff --git a/openmc/material.py b/openmc/material.py index 9677fbba3..770722f74 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -1348,10 +1348,11 @@ class Material(IDManagerMixin): return waste._waste_classification(self, metal=metal) def waste_disposal_rating( - self, - limits: str | dict[str, float] = 'Fetter', - metal: bool = False, - ) -> float: + self, + limits: str | dict[str, float] = 'Fetter', + metal: bool = False, + by_nuclide: bool = False, + ) -> float | dict[str, float]: """Return the waste disposal rating for the material. This method returns a waste disposal rating for the material based on a @@ -1387,18 +1388,25 @@ class Material(IDManagerMixin): metal : bool, optional Whether or not the material is in metal form (only applicable for NRC based limits) + by_nuclide : bool, optional + Whether to return the waste disposal rating for each nuclide in the + material. If True, a dictionary is returned where the keys are the + nuclide names and the values are the waste disposal ratings for each + nuclide. If False, a single float value is returned that represents + the overall waste disposal rating for the material. Returns ------- - float - The waste disposal rating for the material. + float or dict + The waste disposal rating for the material or its constituent + nuclides. See also -------- Material.waste_classification() """ - return waste._waste_disposal_rating(self, limits, metal) + return waste._waste_disposal_rating(self, limits, metal, by_nuclide) def clone(self, memo: dict | None = None) -> Material: """Create a copy of this material with a new unique ID. diff --git a/openmc/waste.py b/openmc/waste.py index 7dd45b8e3..80cfc0adc 100644 --- a/openmc/waste.py +++ b/openmc/waste.py @@ -77,10 +77,11 @@ def _waste_classification(mat: openmc.Material, metal: bool = True) -> str: def _waste_disposal_rating( - mat: openmc.Material, - limits: str | dict[str, float] = 'Fetter', - metal: bool = False, - ) -> float: + mat: openmc.Material, + limits: str | dict[str, float] = 'Fetter', + metal: bool = False, + by_nuclide: bool = False, +) -> float | dict[str, float]: """Return the waste disposal rating for a material. This method returns a waste disposal rating for the material based on a set @@ -111,11 +112,17 @@ def _waste_disposal_rating( metal : bool, optional Whether or not the material is in metal form (only applicable for NRC based limits) + by_nuclide : bool, optional + Whether to return the waste disposal rating for each nuclide in the + material. If True, a dictionary is returned where the keys are the + nuclide names and the values are the waste disposal ratings for each + nuclide. If False, a single float value is returned that represents the + overall waste disposal rating for the material. Returns ------- - float - The waste disposal rating for the material. + float or dict + The waste disposal rating for the material or its constituent nuclides. """ if limits == 'Fetter': @@ -272,8 +279,8 @@ def _waste_disposal_rating( # Calculate the sum of the fractions of the activity of each radionuclide # compared to the specified limits - ratio = 0.0 + ratio = {} for nuc, ci_m3 in mat.get_activity(units="Ci/m3", by_nuclide=True).items(): if nuc in limits: - ratio += ci_m3 / limits[nuc] - return ratio + ratio[nuc] = ci_m3 / limits[nuc] + return ratio if by_nuclide else sum(ratio.values()) diff --git a/tests/unit_tests/test_waste_classification.py b/tests/unit_tests/test_waste_classification.py index dd692cc4b..072590df9 100644 --- a/tests/unit_tests/test_waste_classification.py +++ b/tests/unit_tests/test_waste_classification.py @@ -96,3 +96,7 @@ def test_waste_disposal_rating(): ci_m3 = mat.get_activity('Ci/m3') assert mat.waste_disposal_rating(limits={'K40': 2*ci_m3}) < 1.0 assert mat.waste_disposal_rating(limits={'K40': 0.5*ci_m3}) > 1.0 + + wdr = mat.waste_disposal_rating(limits={'K40': 4*ci_m3}, by_nuclide=True) + assert isinstance(wdr, dict) + assert wdr['K40'] == pytest.approx(1/4) From d0354dbd71531d9c6c88d2c613d571a270453e71 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Sat, 3 May 2025 06:38:23 +0200 Subject: [PATCH 313/671] corrected tally name in D1S example (#3383) Co-authored-by: Paul Romano --- docs/source/usersguide/decay_sources.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/usersguide/decay_sources.rst b/docs/source/usersguide/decay_sources.rst index a228f8e66..d5a078135 100644 --- a/docs/source/usersguide/decay_sources.rst +++ b/docs/source/usersguide/decay_sources.rst @@ -88,5 +88,5 @@ relevant tallies. This can be done with the aid of the dose_tally = sp.get_tally(name='dose tally') # Apply time correction factors - tally = d1s.apply_time_correction(tally, factors, time_index) + tally = d1s.apply_time_correction(dose_tally, factors, time_index) From a921280fa9c3f63eb10132ad37bacd612ab82157 Mon Sep 17 00:00:00 2001 From: Hunter Belanger Date: Sat, 3 May 2025 05:10:18 +0000 Subject: [PATCH 314/671] Fix extremely large yields from Bremsstrahlung (#3386) Co-authored-by: Paul Romano --- src/material.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/material.cpp b/src/material.cpp index a1937aca4..2bd504e4d 100644 --- a/src/material.cpp +++ b/src/material.cpp @@ -640,7 +640,7 @@ void Material::init_bremsstrahlung() // Allocate arrays for TTB data ttb->pdf = xt::zeros({n_e, n_e}); ttb->cdf = xt::zeros({n_e, n_e}); - ttb->yield = xt::empty({n_e}); + ttb->yield = xt::zeros({n_e}); // Allocate temporary arrays xt::xtensor stopping_power_collision({n_e}, 0.0); From 512df2f4ffb4ad529af2f69b614685ab1ad4d372 Mon Sep 17 00:00:00 2001 From: Amanda Lund Date: Sat, 3 May 2025 00:46:14 -0500 Subject: [PATCH 315/671] Skip atomic relaxation if binding energy is larger than photon energy (#3391) Co-authored-by: Paul Romano --- src/photon.cpp | 5 +- .../results_true.dat | 58 +++++++++---------- 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/src/photon.cpp b/src/photon.cpp index 4b2fb4197..dae87ea4e 100644 --- a/src/photon.cpp +++ b/src/photon.cpp @@ -770,8 +770,9 @@ void PhotonInteraction::pair_production(double alpha, double* E_electron, void PhotonInteraction::atomic_relaxation(int i_shell, Particle& p) const { - // Return if no atomic relaxation data is present - if (!has_atomic_relaxation_) + // Return if no atomic relaxation data is present or if the binding energy is + // larger than the incident particle energy + if (!has_atomic_relaxation_ || shells_[i_shell].binding_energy > p.E()) return; // Stack for unprocessed holes left by transitioning electrons diff --git a/tests/regression_tests/photon_production_fission/results_true.dat b/tests/regression_tests/photon_production_fission/results_true.dat index 1aa08b147..7e20342af 100644 --- a/tests/regression_tests/photon_production_fission/results_true.dat +++ b/tests/regression_tests/photon_production_fission/results_true.dat @@ -1,12 +1,12 @@ k-combined: -2.272421E+00 4.418825E-02 +2.270911E+00 4.568134E-02 tally 1: -2.665301E+00 -2.369839E+00 +2.663476E+00 +2.366726E+00 0.000000E+00 0.000000E+00 -2.665301E+00 -2.369839E+00 +2.663476E+00 +2.366726E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -18,52 +18,52 @@ tally 1: 0.000000E+00 0.000000E+00 tally 2: -2.637532E+00 -2.320051E+00 -4.231377E+08 -5.971498E+16 +2.636709E+00 +2.318646E+00 +4.230052E+08 +5.967879E+16 0.000000E+00 0.000000E+00 -2.637532E+00 -2.320051E+00 -4.231377E+08 -5.971498E+16 +2.636709E+00 +2.318646E+00 +4.230052E+08 +5.967879E+16 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.448745E+06 -2.005297E+12 +2.455009E+06 +2.015314E+12 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.448745E+06 -2.005297E+12 +2.455009E+06 +2.015314E+12 0.000000E+00 0.000000E+00 tally 3: -2.660000E+00 -2.358558E+00 -4.231377E+08 -5.971498E+16 +2.660004E+00 +2.358564E+00 +4.230052E+08 +5.967879E+16 0.000000E+00 0.000000E+00 -2.660000E+00 -2.358558E+00 -4.231377E+08 -5.971498E+16 +2.660004E+00 +2.358564E+00 +4.230052E+08 +5.967879E+16 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.448745E+06 -2.005297E+12 +2.455009E+06 +2.015314E+12 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.448745E+06 -2.005297E+12 +2.455009E+06 +2.015314E+12 0.000000E+00 0.000000E+00 From 1e7d8324eefaa6b0995f16a40b99416fdf3cf179 Mon Sep 17 00:00:00 2001 From: Gregoire Biot Date: Sat, 3 May 2025 08:19:10 -0400 Subject: [PATCH 316/671] Figure of Merit implementation (#3363) Co-authored-by: Paul Romano --- docs/source/methods/tallies.rst | 27 +++++++++++++++++++++++++++ openmc/tallies.py | 18 ++++++++++++++++++ tests/unit_tests/test_tallies.py | 18 +++++++++++++++++- 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/docs/source/methods/tallies.rst b/docs/source/methods/tallies.rst index 57e05d84f..79a63fbdd 100644 --- a/docs/source/methods/tallies.rst +++ b/docs/source/methods/tallies.rst @@ -387,6 +387,33 @@ of this is that the longer you run a simulation, the better you know your results. Therefore, by running a simulation long enough, it is possible to reduce the stochastic uncertainty to arbitrarily low levels. +Figure of Merit ++++++++++++++++ + +The figure of merit (FOM) is an indicator that accounts for both the statistical +uncertainty and the execution time and represents how much information is +obtained per unit time in the simulation. The FOM is defined as + +.. math:: + :label: figure_of_merit + + FOM = \frac{1}{r^2 t}, + +where :math:`t` is the total execution time and :math:`r` is the relative error +defined as + +.. math:: + :label: relative_error + + r = \frac{s_\bar{X}}{\bar{x}}. + +Based on this definition, one can see that a higher FOM is desirable. The FOM is +useful as a comparative tool. For example, if a variance reduction technique is +being applied to a simulation, the FOM with variance reduction can be compared +to the FOM without variance reduction to ascertain whether the reduction in +variance outweighs the potential increase in execution time (e.g., due to +particle splitting). + Confidence Intervals ++++++++++++++++++++ diff --git a/openmc/tallies.py b/openmc/tallies.py index ebb313551..e990a95f7 100644 --- a/openmc/tallies.py +++ b/openmc/tallies.py @@ -95,6 +95,10 @@ class Tally(IDManagerMixin): An array containing the sample mean for each bin std_dev : numpy.ndarray An array containing the sample standard deviation for each bin + figure_of_merit : numpy.ndarray + An array containing the figure of merit for each bin + + .. versionadded:: 0.15.3 derived : bool Whether or not the tally is derived from one or more other tallies sparse : bool @@ -127,6 +131,7 @@ class Tally(IDManagerMixin): self._sum_sq = None self._mean = None self._std_dev = None + self._simulation_time = None self._with_batch_statistics = False self._derived = False self._sparse = False @@ -385,6 +390,9 @@ class Tally(IDManagerMixin): self._sum = sps.lil_matrix(self._sum.flatten(), self._sum.shape) self._sum_sq = sps.lil_matrix(self._sum_sq.flatten(), self._sum_sq.shape) + # Read simulation time (needed for figure of merit) + self._simulation_time = f["runtime"]["simulation"][()] + # Indicate that Tally results have been read self._results_read = True @@ -462,6 +470,16 @@ class Tally(IDManagerMixin): else: return self._std_dev + @property + def figure_of_merit(self): + mean = self.mean + std_dev = self.std_dev + fom = np.zeros_like(mean) + nonzero = np.abs(mean) > 0 + fom[nonzero] = 1.0 / ( + (std_dev[nonzero] / mean[nonzero])**2 * self._simulation_time) + return fom + @property def with_batch_statistics(self): return self._with_batch_statistics diff --git a/tests/unit_tests/test_tallies.py b/tests/unit_tests/test_tallies.py index ccc6ff343..c38f067d5 100644 --- a/tests/unit_tests/test_tallies.py +++ b/tests/unit_tests/test_tallies.py @@ -1,5 +1,5 @@ import numpy as np - +import pytest import openmc @@ -96,6 +96,22 @@ def test_tally_equivalence(): assert tally_a == tally_b +def test_figure_of_merit(sphere_model, run_in_tmpdir): + # Run model with a few simple tally scores + tally = openmc.Tally() + tally.scores = ['total', 'absorption', 'scatter'] + sphere_model.tallies = [tally] + sp_path = sphere_model.run(apply_tally_results=True) + + # Get execution time and relative error + with openmc.StatePoint(sp_path) as sp: + time = sp.runtime['simulation'] + rel_err = tally.std_dev / tally.mean + + # Check that figure of merit is calculated correctly + assert tally.figure_of_merit == pytest.approx(1 / (rel_err**2 * time)) + + def test_tally_application(sphere_model, run_in_tmpdir): # Create a tally with most possible gizmos tally = openmc.Tally(name='test tally') From 57dc71f5303a53fc74794384d2a211b1fab57810 Mon Sep 17 00:00:00 2001 From: Amanda Lund Date: Mon, 5 May 2025 13:05:48 -0500 Subject: [PATCH 317/671] Map Compton subshell data to atomic relaxation data (#3392) Co-authored-by: Paul Romano --- include/openmc/photon.h | 6 ++- src/photon.cpp | 30 ++++++++--- src/physics.cpp | 10 ++-- .../photon_production/results_true.dat | 52 +++++++++---------- .../weightwindows/results_true.dat | 2 +- 5 files changed, 59 insertions(+), 41 deletions(-) diff --git a/include/openmc/photon.h b/include/openmc/photon.h index 1fee6c9f5..f6f28a4df 100644 --- a/include/openmc/photon.h +++ b/include/openmc/photon.h @@ -33,7 +33,6 @@ public: int index_subshell; //!< index in SUBSHELLS int threshold; - double n_electrons; double binding_energy; vector transitions; }; @@ -90,6 +89,11 @@ public: xt::xtensor binding_energy_; xt::xtensor 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_; + // Stopping power data double I_; // mean excitation energy xt::xtensor n_electrons_; diff --git a/src/photon.cpp b/src/photon.cpp index dae87ea4e..4926e3eae 100644 --- a/src/photon.cpp +++ b/src/photon.cpp @@ -165,7 +165,6 @@ PhotonInteraction::PhotonInteraction(hid_t group) if (attribute_exists(tgroup, "binding_energy")) { has_atomic_relaxation_ = true; read_attribute(tgroup, "binding_energy", shell.binding_energy); - read_attribute(tgroup, "num_electrons", shell.n_electrons); } // Read subshell cross section @@ -233,6 +232,28 @@ PhotonInteraction::PhotonInteraction(hid_t group) } close_group(rgroup); + // Map Compton subshell data to atomic relaxation data by finding the + // subshell with the equivalent binding energy + if (has_atomic_relaxation_) { + auto is_close = [](double a, double b) { + return std::abs(a - b) / a < FP_REL_PRECISION; + }; + subshell_map_ = xt::full_like(binding_energy_, -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)) { + subshell_map_[i] = i; + } else { + for (int j = 0; j < n_shell; ++j) { + if (is_close(E_b, shells_[j].binding_energy)) { + subshell_map_[i] = j; + break; + } + } + } + } + } + // Create Compton profile CDF auto n_profile = data::compton_profile_pz.size(); auto n_shell_compton = profile_pdf_.shape(0); @@ -423,13 +444,6 @@ void PhotonInteraction::compton_scatter(double alpha, bool doppler, double E_out; this->compton_doppler(alpha, *mu, &E_out, i_shell, seed); *alpha_out = E_out / MASS_ELECTRON_EV; - - // It's possible for the Compton profile data to have more shells than - // there are in the ENDF data. Make sure the shell index doesn't end up - // out of bounds. - if (*i_shell >= shells_.size()) { - *i_shell = -1; - } } else { *i_shell = -1; } diff --git a/src/physics.cpp b/src/physics.cpp index a8e5b9e81..3c06e543d 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -340,11 +340,11 @@ void sample_photon_reaction(Particle& p) p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron); } - // TODO: Compton subshell data does not match atomic relaxation data - // Allow electrons to fill orbital and produce auger electrons - // and fluorescent photons - if (i_shell >= 0) { - element.atomic_relaxation(i_shell, p); + // Allow electrons to fill orbital and produce Auger electrons and + // fluorescent photons. Since Compton subshell data does not match atomic + // relaxation data, use the mapping between the data to find the subshell + if (i_shell >= 0 && element.subshell_map_[i_shell] >= 0) { + element.atomic_relaxation(element.subshell_map_[i_shell], p); } phi += PI; diff --git a/tests/regression_tests/photon_production/results_true.dat b/tests/regression_tests/photon_production/results_true.dat index 398deeed8..a911fed3b 100644 --- a/tests/regression_tests/photon_production/results_true.dat +++ b/tests/regression_tests/photon_production/results_true.dat @@ -1,8 +1,8 @@ tally 1: 8.610000E-01 7.413210E-01 -9.493000E-01 -9.011705E-01 +9.491000E-01 +9.007908E-01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -16,12 +16,12 @@ tally 2: 1.573004E+00 4.296434E-04 1.845934E-07 -2.337049E-01 -5.461796E-02 +2.350021E-01 +5.522600E-02 0.000000E+00 0.000000E+00 -2.337049E-01 -5.461796E-02 +2.350021E-01 +5.522600E-02 0.000000E+00 0.000000E+00 0.000000E+00 @@ -53,16 +53,16 @@ tally 3: 4.104374E+12 4.296582E-04 1.846062E-07 -2.286000E-01 -5.225796E-02 -7.054033E+03 -4.975938E+07 +2.297000E-01 +5.276209E-02 +7.055982E+03 +4.978688E+07 0.000000E+00 0.000000E+00 -2.286000E-01 -5.225796E-02 -7.054033E+03 -4.975938E+07 +2.297000E-01 +5.276209E-02 +7.055982E+03 +4.978688E+07 0.000000E+00 0.000000E+00 0.000000E+00 @@ -73,8 +73,8 @@ tally 3: 0.000000E+00 0.000000E+00 0.000000E+00 -1.764573E+05 -3.113718E+10 +1.774451E+05 +3.148675E+10 0.000000E+00 0.000000E+00 0.000000E+00 @@ -102,16 +102,16 @@ tally 4: 4.104374E+12 0.000000E+00 0.000000E+00 -2.286000E-01 -5.225796E-02 -7.054033E+03 -4.975938E+07 +2.297000E-01 +5.276209E-02 +7.055982E+03 +4.978688E+07 0.000000E+00 0.000000E+00 -2.286000E-01 -5.225796E-02 -7.054033E+03 -4.975938E+07 +2.297000E-01 +5.276209E-02 +7.055982E+03 +4.978688E+07 0.000000E+00 0.000000E+00 0.000000E+00 @@ -122,8 +122,8 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -1.764573E+05 -3.113718E+10 +1.774451E+05 +3.148675E+10 0.000000E+00 0.000000E+00 0.000000E+00 diff --git a/tests/regression_tests/weightwindows/results_true.dat b/tests/regression_tests/weightwindows/results_true.dat index 6c05af31a..6bb992cd4 100644 --- a/tests/regression_tests/weightwindows/results_true.dat +++ b/tests/regression_tests/weightwindows/results_true.dat @@ -1 +1 @@ -ebc761815175b25fc95a226174928c226a3ab5dbf3b2a2abf09e079a0b87dee1dee74aa9b9eaec35acd58b8c481d264be7e9b3f052905bcc14ccd76f36b01549 \ No newline at end of file +952c20fefac374d3b9fd28627fda7d5ae262f8cd7c01a33f0381526204a2287c18e56259a599dc6fdc1b59a2d2866fdfeedea371154c8fa7a69dc5c445b08c2d \ No newline at end of file From 9942269a910cf2f02bd028656be9f3744e1bc7dd Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Mon, 5 May 2025 12:55:14 -0600 Subject: [PATCH 318/671] Updates to VTK data checks (#3371) Co-authored-by: Paul Romano --- docs/source/_images/sphere-mesh-vtk.png | Bin 0 -> 73644 bytes docs/source/pythonapi/base.rst | 19 ++- docs/source/usersguide/processing.rst | 79 +++++++++++ openmc/mesh.py | 124 +++++++++++++----- tests/unit_tests/mesh_to_vtk/test.py | 6 +- tests/unit_tests/mesh_to_vtk/test_vtk_dims.py | 2 +- tests/unit_tests/test_mesh.py | 3 + 7 files changed, 195 insertions(+), 38 deletions(-) create mode 100644 docs/source/_images/sphere-mesh-vtk.png diff --git a/docs/source/_images/sphere-mesh-vtk.png b/docs/source/_images/sphere-mesh-vtk.png new file mode 100644 index 0000000000000000000000000000000000000000..73b412bdd3779cff6573d81642b91e083fd7c784 GIT binary patch literal 73644 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

    h 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

    Xvgr!z$Pq0D}n~Bl-=;bz?MVH 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

    Uh3_-mm;=L=cv@S4~E2# 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 Muc4z}q-q)PUuriayZ`_I literal 0 HcmV?d00001 diff --git a/docs/source/pythonapi/base.rst b/docs/source/pythonapi/base.rst index e5b5b17c3..f3eb7ce83 100644 --- a/docs/source/pythonapi/base.rst +++ b/docs/source/pythonapi/base.rst @@ -147,17 +147,26 @@ Constructing Tallies openmc.ZernikeRadialFilter openmc.ParentNuclideFilter openmc.ParticleFilter - openmc.RegularMesh - openmc.RectilinearMesh - openmc.CylindricalMesh - openmc.SphericalMesh - openmc.UnstructuredMesh openmc.MeshMaterialVolumes openmc.Trigger openmc.TallyDerivative openmc.Tally openmc.Tallies +Meshes +------ + +.. autosummary:: + :toctree: generated + :nosignatures: + :template: myclassinherit.rst + + openmc.RegularMesh + openmc.RectilinearMesh + openmc.CylindricalMesh + openmc.SphericalMesh + openmc.UnstructuredMesh + Geometry Plotting ----------------- diff --git a/docs/source/usersguide/processing.rst b/docs/source/usersguide/processing.rst index 10944b5e2..8b5ae53fa 100644 --- a/docs/source/usersguide/processing.rst +++ b/docs/source/usersguide/processing.rst @@ -91,3 +91,82 @@ from a statepoint file, the ``openmc.statepoint`` module can be used. An `example notebook`_ demontrates how to analyze and plot source information. .. _example notebook: https://nbviewer.jupyter.org/github/openmc-dev/openmc-notebooks/blob/main/post-processing.ipynb + +------------------------ +VTK Mesh File Generation +------------------------ + +VTK files of OpenMC meshes can be created using the +:meth:`openmc.Mesh.write_data_to_vtk` method. Data can be applied to the +elements of the resulting mesh from mesh filter objects. This data can be +provided either as a flat array or, in the case of structured meshes +(:class:`~openmc.RegularMesh`, :class:`~openmc.RectilinearMesh`, +:class:`~openmc.CylindricalMesh`, or :class:`SphericalMesh`), the data can be +shaped with dimensions that match the dimensions of the mesh itself. + + +.. image:: ../_images/sphere-mesh-vtk.png + :width: 400px + :align: center + :alt: OpenMC spherical mesh exported to VTK + + +For all mesh types, if a flat data array is provided to the mesh, it is expected +that the data is ordered in the same ordering as the :attr:`openmc.Mesh.indices` +for that mesh object. When providing data directly from a tally, as shown below, +a flat array for a given dataset can be passed directly to this method. + +:: + + # create model above + + # create a mesh tally + mesh = openmc.RegularMesh() + mesh.dimension = [10, 20, 30] + mesh.lower_left = [-5, -10, -15] + mesh.upper_right = [5, 10, 15] + mesh_filter = openmc.MeshFilter(mesh) + tally = openmc.Tally() + tally.filters = [mesh_filter] + tally.scores = ['flux'] + + model.tallies = [tally] + model.run(apply_tally_results=True) + + # provide the data as-is to the method + mesh.write_data_to_vtk('flux.vtk', {'flux-mean': tally.mean}) + +The :class:`~openmc.Tally` object also provides a way to expand the dimensions +of the mesh filter into a meaningful form where indexing the mesh filter +dimensions results in intuitive slicing of structured meshes by setting +``expand_dims=True`` when using :meth:`openmc.Tally.get_reshaped_data`. This +reshaping does cause flat indexing of the data to change, however. As noted +above, provided datasets are allowed to be shaped so long as such datasets have +shapes that match the mesh dimensions. The ability to pass datasets in this way +is useful when additional filters are applied to a tally. The example below +demonstrates such a case for tally with both a :class:`~openmc.MeshFilter` and +:class:`~openmc.EnergyFilter` applied. + +:: + + # create model above + + # create a mesh tally with energy filter + mesh = openmc.RegularMesh() + mesh.dimension = [10, 20, 30] + mesh.lower_left = [-5, -10, -15] + mesh.upper_right = [5, 10, 15] + mesh_filter = openmc.MeshFilter(mesh) + energy_filter = openmc.EnergyFilter([0.0, 1.0, 20.0e6]) + tally = openmc.Tally() + tally.filters = [mesh_filter, energy_filter] + tally.scores = ['flux'] + + model.tallies = [tally] + model.run(apply_tally_results=True) + + # get the data with mesh dimensions expanded, squeeze out length-one dimensions (nuclides, scores) + flux = tally.get_reshaped_data(expand_dims=True).squeeze() # shape: (10, 20, 30, 2) + + # write the lowest energy group to a VTK file + mesh.write_data_to_vtk('flux-group1.vtk', datasets={'flux-mean': flux[..., 0]}) diff --git a/openmc/mesh.py b/openmc/mesh.py index 08025f374..27187b5dd 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -576,11 +576,16 @@ class StructuredMesh(MeshBase): filename : str Name of the VTK file to write. datasets : dict - Dictionary whose keys are the data labels - and values are the data sets. + Dictionary whose keys are the data labels and values are the data + sets. 1D datasets are expected to be extracted directly from + statepoint data without reordering/reshaping. Multidimensional + datasets are expected to have the same dimensions as the mesh itself + with structured indexing in "C" ordering. See the "expand_dims" flag + of :meth:`~openmc.Tally.get_reshaped_data` on reshaping tally data when using + :class:`~openmc.MeshFilter`'s. volume_normalization : bool, optional - Whether or not to normalize the data by - the volume of the mesh elements. + Whether or not to normalize the data by the volume of the mesh + elements. curvilinear : bool Whether or not to write curvilinear elements. Only applies to ``SphericalMesh`` and ``CylindricalMesh``. @@ -594,14 +599,27 @@ class StructuredMesh(MeshBase): ------- vtk.StructuredGrid or vtk.UnstructuredGrid a VTK grid object representing the mesh + + Examples + -------- + 1D data from a tally with only a mesh filter and heating score: + + # pass the tally mean property of shape (N, 1, 1) directly to this + # method; dimensions of size 1 will automatically removed + >>> heating = tally.mean + >>> mesh.write_data_to_vtk({'heating': heating}) + + Multidimensional data from a tally with only a mesh + + # retrieve a data array with the mesh filter expanded into three + # dimensions, ijk; additional dimensions of size one will + # automatically be removed + >>> heating = tally.get_reshaped_data(expand_dims=True) + >>> mesh.write_data_to_vtk({'heating': heating}) """ import vtk from vtk.util import numpy_support as nps - # check that the data sets are appropriately sized - if datasets is not None: - self._check_vtk_datasets(datasets) - # write linear elements using a structured grid if not curvilinear or isinstance(self, (RegularMesh, RectilinearMesh)): vtk_grid = self._create_vtk_structured_grid() @@ -612,22 +630,27 @@ class StructuredMesh(MeshBase): writer = vtk.vtkUnstructuredGridWriter() if datasets is not None: - # maintain a list of the datasets as added - # to the VTK arrays to ensure they persist - # in memory until the file is written + # maintain a list of the datasets as added to the VTK arrays to + # ensure they persist in memory until the file is written datasets_out = [] for label, dataset in datasets.items(): - dataset = np.asarray(dataset).flatten() + dataset = self._reshape_vtk_dataset(dataset) + self._check_vtk_dataset(label, dataset) + # If the array data is 3D, assume is in C ordering and transpose + # before flattening to match the ordering expected by the VTK + # array based on the way mesh indices are ordered in the Python + # API + # TODO: update to "C" ordering throughout + if dataset.ndim == 3: + dataset = dataset.T.ravel() datasets_out.append(dataset) if volume_normalization: - dataset /= self.volumes.T.flatten() + dataset /= self.volumes.T.ravel() dataset_array = vtk.vtkDoubleArray() dataset_array.SetName(label) - dataset_array.SetArray(nps.numpy_to_vtk(dataset), - dataset.size, - True) + dataset_array.SetArray(nps.numpy_to_vtk(dataset), dataset.size, True) vtk_grid.GetCellData().AddArray(dataset_array) writer.SetFileName(str(filename)) @@ -754,28 +777,69 @@ class StructuredMesh(MeshBase): return vtk_grid - def _check_vtk_datasets(self, datasets: dict): - """Perform some basic checks that the datasets are valid for this mesh + @staticmethod + def _reshape_vtk_dataset(dataset): + """Reshape a dataset to be compatible with VTK output + + This method performs the following operations on a dataset: + 1. Convert to numpy array if not already + 2. Remove any trailing dimensions of size 1 + 3. Squeeze out any extra dimensions of size 1 beyond the first 3 Parameters ---------- - datasets : dict - Dictionary whose keys are the data labels - and values are the data sets. + dataset : array-like + The dataset to reshape + + Returns + ------- + numpy.ndarray + The reshaped dataset + """ + reshaped_data = np.asarray(dataset) + + # detect flat array with extra dims + if all(d == 1 for d in reshaped_data.shape[1:]): + reshaped_data = reshaped_data.squeeze() + + # remove any higher dimensions with size 1 + if reshaped_data.ndim > 3 and all(d == 1 for d in reshaped_data.shape[3:]): + reshaped_data = reshaped_data.reshape(reshaped_data.shape[:3]) + + if np.shares_memory(reshaped_data, dataset): + return np.copy(reshaped_data) + else: + return reshaped_data + + def _check_vtk_dataset(self, label: str, dataset: np.ndarray): + """Perform some basic checks that a dataset is valid for this Mesh + + Parameters + ---------- + label : str + The label for the dataset being checked + dataset : numpy.ndarray + The dataset array to check against this mesh's dimensions """ - for label, dataset in datasets.items(): - errmsg = ( + cv.check_type('data label', label, str) + + if dataset.size != self.num_mesh_cells: + raise ValueError( f"The size of the dataset '{label}' ({dataset.size}) should be" f" equal to the number of mesh cells ({self.num_mesh_cells})" ) - if isinstance(dataset, np.ndarray): - if not dataset.size == self.num_mesh_cells: - raise ValueError(errmsg) - else: - if len(dataset) == self.num_mesh_cells: - raise ValueError(errmsg) - cv.check_type('data label', label, str) + + # accept a flat array as-is, assuming it is in the correct order + if dataset.ndim == 1: + return + + if dataset.shape != self.dimension: + raise ValueError( + f'Cannot apply multidimensional dataset "{label}" with ' + f"shape {dataset.shape} to mesh {self.id} " + f"with dimensions {self.dimension}" + ) class RegularMesh(StructuredMesh): diff --git a/tests/unit_tests/mesh_to_vtk/test.py b/tests/unit_tests/mesh_to_vtk/test.py index 3ee6c0298..f00aa4626 100644 --- a/tests/unit_tests/mesh_to_vtk/test.py +++ b/tests/unit_tests/mesh_to_vtk/test.py @@ -49,7 +49,10 @@ sphere_mesh = openmc.SphericalMesh( def mesh_data(mesh_dims): data = 100 * np.arange(np.prod(mesh_dims), dtype=float) - return data.reshape(*mesh_dims) + # data is returned reshaped with order 'F' to ensure that + # the resulting data is interpreted correctly by the + # write_data_to_vtk method + return data.reshape(*mesh_dims, order='F') test_data = ((reg_mesh, False, 'regular'), (rect_mesh, False, 'rectilinear'), @@ -83,7 +86,6 @@ def test_mesh_write_vtk(mesh_params, run_in_tmpdir): # check data writing def test_mesh_write_vtk_data(run_in_tmpdir): - data = {'ascending_data': mesh_data(cyl_mesh.dimension)} filename_expected = full_path('cyl-data.vtk') filename_actual = full_path('cyl-data-actual.vtk') diff --git a/tests/unit_tests/mesh_to_vtk/test_vtk_dims.py b/tests/unit_tests/mesh_to_vtk/test_vtk_dims.py index 0b6acf5aa..8166ba68c 100644 --- a/tests/unit_tests/mesh_to_vtk/test_vtk_dims.py +++ b/tests/unit_tests/mesh_to_vtk/test_vtk_dims.py @@ -90,7 +90,7 @@ def test_write_data_to_vtk(mesh, tmpdir): # kji (i changing fastest) orering is expected for input data # by using the volumes transposed as the data here, we can ensure the # normalization is happening correctly - data = mesh.volumes.T + data = mesh.volumes # RUN mesh.write_data_to_vtk(filename=filename, datasets={"label1": data, "label2": data}) diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index f1f5e9f67..49556fce3 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -480,6 +480,9 @@ def test_umesh(run_in_tmpdir, simple_umesh, export_type): mean = np.array([arr.GetTuple1(i) for i in range(ref_data.size)]) np.testing.assert_almost_equal(mean, ref_data) + # attempt to apply a dataset with an improper size to a VTK write + with pytest.raises(ValueError, match='Cannot apply dataset "mean"') as e: + simple_umesh.write_data_to_vtk(datasets={'mean': ref_data[:-2]}, filename=filename) def test_mesh_get_homogenized_materials(): """Test the get_homogenized_materials method""" From dc619eac17d45ae42361c973bd4e643b5bb31684 Mon Sep 17 00:00:00 2001 From: Gregoire Biot Date: Mon, 5 May 2025 16:33:12 -0400 Subject: [PATCH 319/671] Filter weight implementation (#3345) Co-authored-by: Grego01-biot Co-authored-by: Patrick Shriwise Co-authored-by: Paul Romano --- CMakeLists.txt | 1 + docs/source/pythonapi/base.rst | 1 + docs/source/pythonapi/capi.rst | 1 + include/openmc/tallies/filter.h | 1 + include/openmc/tallies/filter_weight.h | 51 +++++++++++++++++++++ openmc/filter.py | 25 +++++++++- openmc/lib/filter.py | 4 ++ src/hdf5_interface.cpp | 2 +- src/tallies/filter.cpp | 3 ++ src/tallies/filter_weight.cpp | 63 ++++++++++++++++++++++++++ tests/unit_tests/test_filter_weight.py | 44 ++++++++++++++++++ tests/unit_tests/test_filters.py | 21 +++++++++ 12 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 include/openmc/tallies/filter_weight.h create mode 100644 src/tallies/filter_weight.cpp create mode 100644 tests/unit_tests/test_filter_weight.py diff --git a/CMakeLists.txt b/CMakeLists.txt index d79746209..4dff35418 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -425,6 +425,7 @@ list(APPEND libopenmc_SOURCES src/tallies/filter_surface.cpp src/tallies/filter_time.cpp src/tallies/filter_universe.cpp + src/tallies/filter_weight.cpp src/tallies/filter_zernike.cpp src/tallies/tally.cpp src/tallies/tally_scoring.cpp diff --git a/docs/source/pythonapi/base.rst b/docs/source/pythonapi/base.rst index f3eb7ce83..1d8a93d47 100644 --- a/docs/source/pythonapi/base.rst +++ b/docs/source/pythonapi/base.rst @@ -143,6 +143,7 @@ Constructing Tallies openmc.SpatialLegendreFilter openmc.SphericalHarmonicsFilter openmc.TimeFilter + openmc.WeightFilter openmc.ZernikeFilter openmc.ZernikeRadialFilter openmc.ParentNuclideFilter diff --git a/docs/source/pythonapi/capi.rst b/docs/source/pythonapi/capi.rst index c8e0e874d..63fc5d11b 100644 --- a/docs/source/pythonapi/capi.rst +++ b/docs/source/pythonapi/capi.rst @@ -91,6 +91,7 @@ Classes Tally UniverseFilter UnstructuredMesh + WeightFilter WeightWindows ZernikeFilter ZernikeRadialFilter diff --git a/include/openmc/tallies/filter.h b/include/openmc/tallies/filter.h index 3e982d0cf..ee635b183 100644 --- a/include/openmc/tallies/filter.h +++ b/include/openmc/tallies/filter.h @@ -44,6 +44,7 @@ enum class FilterType { SURFACE, TIME, UNIVERSE, + WEIGHT, ZERNIKE, ZERNIKE_RADIAL }; diff --git a/include/openmc/tallies/filter_weight.h b/include/openmc/tallies/filter_weight.h new file mode 100644 index 000000000..1fe9d75d3 --- /dev/null +++ b/include/openmc/tallies/filter_weight.h @@ -0,0 +1,51 @@ +#ifndef OPENMC_TALLIES_FILTER_WEIGHT_H +#define OPENMC_TALLIES_FILTER_WEIGHT_H + +#include + +#include "openmc/span.h" +#include "openmc/tallies/filter.h" +#include "openmc/vector.h" + +namespace openmc { + +//============================================================================== +//! Bins the weights of the particles. +//============================================================================== + +class WeightFilter : public Filter { +public: + //---------------------------------------------------------------------------- + // Constructors, destructors + + ~WeightFilter() = default; + + //---------------------------------------------------------------------------- + // Methods + + std::string type_str() const override { return "weight"; } + FilterType type() const override { return FilterType::WEIGHT; } + + 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 + vector bins_; +}; + +} // namespace openmc +#endif // OPENMC_TALLIES_FILTER_WEIGHT_H diff --git a/openmc/filter.py b/openmc/filter.py index 755777c5e..4d37f1055 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -25,7 +25,7 @@ _FILTER_TYPES = ( 'energyout', 'mu', 'musurface', 'polar', 'azimuthal', 'distribcell', 'delayedgroup', 'energyfunction', 'cellfrom', 'materialfrom', 'legendre', 'spatiallegendre', 'sphericalharmonics', 'zernike', 'zernikeradial', 'particle', 'cellinstance', - 'collision', 'time', 'parentnuclide' + 'collision', 'time', 'parentnuclide', 'weight' ) _CURRENT_NAMES = ( @@ -2328,3 +2328,26 @@ class EnergyFunctionFilter(Filter): {self.short_name.lower(): filter_bins})]) return df + + +class WeightFilter(RealFilter): + """Bins tally events based on the incoming particle weight. + + Parameters + ---------- + Values : Iterable of float + A list or iterable of the weight boundaries, as float values. + filter_id : int + Unique identifier for the filter + + Attributes + ---------- + id : int + Unique identifier for the filter + bins : numpy.ndarray + An array of integer values representing the weights by which to filter + num_bins : int + The number of filter bins + values : numpy.ndarray + Array of weight boundaries + """ diff --git a/openmc/lib/filter.py b/openmc/lib/filter.py index b6086c567..3a76b52a1 100644 --- a/openmc/lib/filter.py +++ b/openmc/lib/filter.py @@ -610,6 +610,10 @@ class UniverseFilter(Filter): filter_type = 'universe' +class WeightFilter(Filter): + filter_type = 'weight' + + class ZernikeFilter(Filter): filter_type = 'zernike' diff --git a/src/hdf5_interface.cpp b/src/hdf5_interface.cpp index 27b750b93..e90aa7490 100644 --- a/src/hdf5_interface.cpp +++ b/src/hdf5_interface.cpp @@ -92,7 +92,7 @@ void get_shape_attr(hid_t obj_id, const char* name, hsize_t* dims) H5Aclose(attr); } -hid_t create_group(hid_t parent_id, char const* name) +hid_t create_group(hid_t parent_id, const char* name) { hid_t out = H5Gcreate(parent_id, name, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); if (out < 0) { diff --git a/src/tallies/filter.cpp b/src/tallies/filter.cpp index 6430d9ae1..17e57a987 100644 --- a/src/tallies/filter.cpp +++ b/src/tallies/filter.cpp @@ -36,6 +36,7 @@ #include "openmc/tallies/filter_surface.h" #include "openmc/tallies/filter_time.h" #include "openmc/tallies/filter_universe.h" +#include "openmc/tallies/filter_weight.h" #include "openmc/tallies/filter_zernike.h" #include "openmc/xml_interface.h" @@ -154,6 +155,8 @@ Filter* Filter::create(const std::string& type, int32_t id) return Filter::create(id); } else if (type == "universe") { return Filter::create(id); + } else if (type == "weight") { + return Filter::create(id); } else if (type == "zernike") { return Filter::create(id); } else if (type == "zernikeradial") { diff --git a/src/tallies/filter_weight.cpp b/src/tallies/filter_weight.cpp new file mode 100644 index 000000000..31f4bd1bf --- /dev/null +++ b/src/tallies/filter_weight.cpp @@ -0,0 +1,63 @@ +#include "openmc/tallies/filter_weight.h" + +#include // for is_sorted +#include // for runtime_error + +#include + +#include "openmc/search.h" +#include "openmc/xml_interface.h" + +namespace openmc { + +//============================================================================== +// WeightFilter implementation +//============================================================================== + +void WeightFilter::from_xml(pugi::xml_node node) +{ + auto bins = get_node_array(node, "bins"); + this->set_bins(bins); +} + +void WeightFilter::set_bins(span bins) +{ + if (!std::is_sorted(bins.begin(), bins.end())) { + throw std::runtime_error {"Weight bins must be monotonically increasing."}; + } + + // Clear existing bins + bins_.clear(); + bins_.reserve(bins.size()); + + // Copy bins + bins_.insert(bins_.end(), bins.begin(), bins.end()); + n_bins_ = bins_.size() - 1; +} + +void WeightFilter::get_all_bins( + const Particle& p, TallyEstimator estimator, FilterMatch& match) const +{ + // Get particle weight + double wgt = p.wgt_last(); + + // Bin the weight + if (wgt >= bins_.front() && wgt <= bins_.back()) { + auto bin = lower_bound_index(bins_.begin(), bins_.end(), wgt); + match.bins_.push_back(bin); + match.weights_.push_back(1.0); + } +} + +void WeightFilter::to_statepoint(hid_t filter_group) const +{ + Filter::to_statepoint(filter_group); + write_dataset(filter_group, "bins", bins_); +} + +std::string WeightFilter::text_label(int bin) const +{ + return fmt::format("Weight [{}, {}]", bins_[bin], bins_[bin + 1]); +} + +} // namespace openmc diff --git a/tests/unit_tests/test_filter_weight.py b/tests/unit_tests/test_filter_weight.py new file mode 100644 index 000000000..878929ee0 --- /dev/null +++ b/tests/unit_tests/test_filter_weight.py @@ -0,0 +1,44 @@ +import openmc +import numpy as np + + +def test_weightfilter(run_in_tmpdir): + steel = openmc.Material(name='Stainless Steel') + steel.set_density('g/cm3', 8.00) + steel.add_nuclide('Fe56', 1.0) + + sphere = openmc.Sphere(r=50.0, boundary_type='vacuum') + cell = openmc.Cell(region=-sphere, fill=steel) + model = openmc.Model() + model.geometry = openmc.Geometry([cell]) + model.settings.particles = 100 + model.settings.batches = 10 + + model.settings.source = openmc.IndependentSource( + energy=openmc.stats.delta_function(14e6), + ) + model.settings.run_mode = "fixed source" + + radius = list(range(1, 50)) + sphere_mesh = openmc.SphericalMesh(radius) + mesh_filter = openmc.MeshFilter(sphere_mesh) + weight_filter = openmc.WeightFilter( + [0.999, 0.9999, 0.99999, 0.999999, 1.0, 1.000001 ,1.00001, 1.0001, 1.001] + ) + + tally = openmc.Tally() + tally.filters = [mesh_filter, weight_filter] + tally.estimator = 'analog' + tally.scores = ['flux'] + model.tallies = openmc.Tallies([tally]) + + # Run OpenMC + model.run(apply_tally_results=True) + + # Get current binned by mu + neutron_flux = tally.mean.reshape(48, 8) + + # All contributions should show up in the fourth bin + assert np.all(neutron_flux[:, 3] != 0.0) + neutron_flux[:, 3] = 0.0 + assert np.all(neutron_flux == 0.0) diff --git a/tests/unit_tests/test_filters.py b/tests/unit_tests/test_filters.py index c3b737192..60a60e3f5 100644 --- a/tests/unit_tests/test_filters.py +++ b/tests/unit_tests/test_filters.py @@ -312,3 +312,24 @@ def test_energy_filter(): msg = 'Unable to set "filter value" to "-1.2" since it is less than "0.0"' with raises(ValueError, match=msg): openmc.EnergyFilter([-1.2, 0.25, 0.5]) + + +def test_weight(): + f = openmc.WeightFilter([0.01, 0.1, 1.0, 10.0]) + expected_bins = [[0.01, 0.1], [0.1, 1.0], [1.0, 10.0]] + + assert np.allclose(f.bins, expected_bins) + assert len(f.bins) == 3 + + # Make sure __repr__ works + repr(f) + + # to_xml_element() + elem = f.to_xml_element() + assert elem.tag == 'filter' + assert elem.attrib['type'] == 'weight' + + # from_xml_element() + new_f = openmc.Filter.from_xml_element(elem) + assert new_f.id == f.id + assert np.allclose(new_f.bins, f.bins) From e4f55a57b69c87f16e73710bcf51f7427b2cb6a7 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 5 May 2025 15:59:11 -0500 Subject: [PATCH 320/671] Fix weight modification for uniform source sampling (#3395) Co-authored-by: Patrick Shriwise --- openmc/deplete/d1s.py | 4 +++- openmc/source.py | 2 +- src/settings.cpp | 14 +++++++------- src/source.cpp | 15 ++++++++++----- src/tallies/tally.cpp | 7 ++----- tests/unit_tests/test_uniform_source_sampling.py | 15 +++++++++++---- 6 files changed, 34 insertions(+), 23 deletions(-) diff --git a/openmc/deplete/d1s.py b/openmc/deplete/d1s.py index 8f6bffa86..0a1ed74b1 100644 --- a/openmc/deplete/d1s.py +++ b/openmc/deplete/d1s.py @@ -141,7 +141,9 @@ def apply_time_correction( time_correction_factors : dict Time correction factors as returned by :func:`time_correction_factors` index : int, optional - Index to use for the correction factors + Index of the time of interest. If N timesteps are provided in + :func:`time_correction_factors`, there are N + 1 times to select from. + The default is -1 which corresponds to the final time. sum_nuclides : bool Whether to sum over the parent nuclides diff --git a/openmc/source.py b/openmc/source.py index 878f52c3b..87e734e9a 100644 --- a/openmc/source.py +++ b/openmc/source.py @@ -110,7 +110,7 @@ class SourceBase(ABC): cv.check_value('rejection strategy', value, ('resample', 'kill')) self._constraints['rejection_strategy'] = value else: - raise ValueError('Unknown key in constraints dictionary: {key}') + raise ValueError(f'Unknown key in constraints dictionary: {key}') @abstractmethod def populate_xml_element(self, element): diff --git a/src/settings.cpp b/src/settings.cpp index c135fa339..c8230a10f 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -620,13 +620,6 @@ void read_settings_xml(pugi::xml_node root) model::external_sources.push_back(make_unique(path)); } - // Build probability mass function for sampling external sources - vector source_strengths; - for (auto& s : model::external_sources) { - source_strengths.push_back(s->strength()); - } - model::external_sources_probability.assign(source_strengths); - // If no source specified, default to isotropic point source at origin with // Watt spectrum. No default source is needed in random ray mode. if (model::external_sources.empty() && @@ -639,6 +632,13 @@ void read_settings_xml(pugi::xml_node root) UPtrDist {new Discrete(T, p, 1)})); } + // Build probability mass function for sampling external sources + vector source_strengths; + for (auto& s : model::external_sources) { + source_strengths.push_back(s->strength()); + } + model::external_sources_probability.assign(source_strengths); + // Check if we want to write out source if (check_for_node(root, "write_initial_source")) { write_initial_source = get_node_value_bool(root, "write_initial_source"); diff --git a/src/source.cpp b/src/source.cpp index c1b4ce260..8e6eb4f11 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -613,9 +613,10 @@ SourceSite sample_external_source(uint64_t* seed) { // Sample from among multiple source distributions int i = 0; - if (model::external_sources.size() > 1) { + int n_sources = model::external_sources.size(); + if (n_sources > 1) { if (settings::uniform_source_sampling) { - i = prn(seed) * model::external_sources.size(); + i = prn(seed) * n_sources; } else { i = model::external_sources_probability.sample(seed); } @@ -624,9 +625,13 @@ SourceSite sample_external_source(uint64_t* seed) // Sample source site from i-th source distribution SourceSite site {model::external_sources[i]->sample_with_constraints(seed)}; - // Set particle creation weight - if (settings::uniform_source_sampling) { - site.wgt *= model::external_sources[i]->strength(); + // For uniform source sampling, multiply the weight by the ratio of the actual + // probability of sampling source i to the biased probability of sampling + // source i, which is (strength_i / total_strength) / (1 / n) + if (n_sources > 1 && settings::uniform_source_sampling) { + double total_strength = model::external_sources_probability.integral(); + site.wgt *= + model::external_sources[i]->strength() * n_sources / total_strength; } // If running in MG, convert site.E to group diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index 35805b20d..54840b518 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -815,11 +815,8 @@ void Tally::accumulate() if (mpi::master || !settings::reduce_tallies) { // Calculate total source strength for normalization double total_source = 0.0; - if (settings::run_mode == RunMode::FIXED_SOURCE && - !settings::uniform_source_sampling) { - for (const auto& s : model::external_sources) { - total_source += s->strength(); - } + if (settings::run_mode == RunMode::FIXED_SOURCE) { + total_source = model::external_sources_probability.integral(); } else { total_source = 1.0; } diff --git a/tests/unit_tests/test_uniform_source_sampling.py b/tests/unit_tests/test_uniform_source_sampling.py index 7f805e37d..0d1930328 100644 --- a/tests/unit_tests/test_uniform_source_sampling.py +++ b/tests/unit_tests/test_uniform_source_sampling.py @@ -14,10 +14,15 @@ def sphere_model(): model.settings.particles = 100 model.settings.batches = 1 - model.settings.source = openmc.IndependentSource( + src1 = openmc.IndependentSource( energy=openmc.stats.delta_function(1.0e3), - strength=100.0 + strength=75.0 ) + src2 = openmc.IndependentSource( + energy=openmc.stats.delta_function(1.0e3), + strength=25.0 + ) + model.settings.source = [src1, src2] model.settings.run_mode = "fixed source" model.settings.surf_source_write = { "max_particles": 100, @@ -42,11 +47,13 @@ def test_source_weight(run_in_tmpdir, sphere_model): sphere_model.settings.uniform_source_sampling = True sphere_model.run() particles = openmc.ParticleList.from_hdf5('surface_source.h5') - strength = sphere_model.settings.source[0].strength - assert set(p.wgt for p in particles) == {strength} + assert set(p.wgt for p in particles) == {0.5, 1.5} def test_tally_mean(run_in_tmpdir, sphere_model): + # Use only one source + sphere_model.settings.source.pop() + # Run without uniform source sampling sphere_model.settings.uniform_source_sampling = False sp_file = sphere_model.run() From f9dca9a458fec7ec8384b88d349170ddf581ce3d Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Tue, 6 May 2025 03:04:47 +0300 Subject: [PATCH 321/671] Fixing an incorrect computation of CDF of bremsstrahlung photons (#3396) Co-authored-by: Paul Romano --- src/material.cpp | 9 +-- .../photon_production/results_true.dat | 24 ++++---- .../results_true.dat | 58 +++++++++---------- .../photon_source/results_true.dat | 4 +- .../weightwindows/results_true.dat | 2 +- 5 files changed, 49 insertions(+), 48 deletions(-) diff --git a/src/material.cpp b/src/material.cpp index 2bd504e4d..b8846d7cb 100644 --- a/src/material.cpp +++ b/src/material.cpp @@ -777,14 +777,15 @@ void Material::init_bremsstrahlung() // Loop over photon energies double c = 0.0; for (int i = 0; i < j; ++i) { - // Integrate the CDF from the PDF using the trapezoidal rule in log-log - // space + // Integrate the CDF from the PDF using the fact that the PDF is linear + // in log-log space double w_l = std::log(data::ttb_e_grid(i)); double w_r = std::log(data::ttb_e_grid(i + 1)); double x_l = std::log(ttb->pdf(j, i)); double x_r = std::log(ttb->pdf(j, i + 1)); - - c += 0.5 * (w_r - w_l) * (std::exp(w_l + x_l) + std::exp(w_r + x_r)); + double beta = (x_r - x_l) / (w_r - w_l); + double a = beta + 1.0; + c += std::exp(w_l + x_l) / a * std::expm1(a * (w_r - w_l)); ttb->cdf(j, i + 1) = c; } diff --git a/tests/regression_tests/photon_production/results_true.dat b/tests/regression_tests/photon_production/results_true.dat index a911fed3b..874e32b85 100644 --- a/tests/regression_tests/photon_production/results_true.dat +++ b/tests/regression_tests/photon_production/results_true.dat @@ -16,12 +16,12 @@ tally 2: 1.573004E+00 4.296434E-04 1.845934E-07 -2.350021E-01 -5.522600E-02 +2.350047E-01 +5.522722E-02 0.000000E+00 0.000000E+00 -2.350021E-01 -5.522600E-02 +2.350047E-01 +5.522722E-02 0.000000E+00 0.000000E+00 0.000000E+00 @@ -73,8 +73,8 @@ tally 3: 0.000000E+00 0.000000E+00 0.000000E+00 -1.774451E+05 -3.148675E+10 +1.774484E+05 +3.148794E+10 0.000000E+00 0.000000E+00 0.000000E+00 @@ -85,8 +85,8 @@ tally 3: 0.000000E+00 0.000000E+00 0.000000E+00 -7.691658E+03 -5.916160E+07 +7.692488E+03 +5.917437E+07 0.000000E+00 0.000000E+00 tally 4: @@ -122,8 +122,8 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -1.774451E+05 -3.148675E+10 +1.774484E+05 +3.148794E+10 0.000000E+00 0.000000E+00 0.000000E+00 @@ -134,7 +134,7 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -7.691658E+03 -5.916160E+07 +7.692488E+03 +5.917437E+07 0.000000E+00 0.000000E+00 diff --git a/tests/regression_tests/photon_production_fission/results_true.dat b/tests/regression_tests/photon_production_fission/results_true.dat index 7e20342af..cdbb1255c 100644 --- a/tests/regression_tests/photon_production_fission/results_true.dat +++ b/tests/regression_tests/photon_production_fission/results_true.dat @@ -1,12 +1,12 @@ k-combined: -2.270911E+00 4.568134E-02 +2.278476E+00 6.220292E-02 tally 1: -2.663476E+00 -2.366726E+00 +2.664071E+00 +2.369250E+00 0.000000E+00 0.000000E+00 -2.663476E+00 -2.366726E+00 +2.664071E+00 +2.369250E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -18,52 +18,52 @@ tally 1: 0.000000E+00 0.000000E+00 tally 2: -2.636709E+00 -2.318646E+00 -4.230052E+08 -5.967879E+16 +2.640755E+00 +2.326201E+00 +4.245217E+08 +6.012128E+16 0.000000E+00 0.000000E+00 -2.636709E+00 -2.318646E+00 -4.230052E+08 -5.967879E+16 +2.640755E+00 +2.326201E+00 +4.245217E+08 +6.012128E+16 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.455009E+06 -2.015314E+12 +2.479234E+06 +2.052367E+12 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.455009E+06 -2.015314E+12 +2.479234E+06 +2.052367E+12 0.000000E+00 0.000000E+00 tally 3: -2.660004E+00 -2.358564E+00 -4.230052E+08 -5.967879E+16 +2.657846E+00 +2.354717E+00 +4.245217E+08 +6.012128E+16 0.000000E+00 0.000000E+00 -2.660004E+00 -2.358564E+00 -4.230052E+08 -5.967879E+16 +2.657846E+00 +2.354717E+00 +4.245217E+08 +6.012128E+16 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.455009E+06 -2.015314E+12 +2.479234E+06 +2.052367E+12 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.455009E+06 -2.015314E+12 +2.479234E+06 +2.052367E+12 0.000000E+00 0.000000E+00 diff --git a/tests/regression_tests/photon_source/results_true.dat b/tests/regression_tests/photon_source/results_true.dat index fcc75cbca..8d934afc6 100644 --- a/tests/regression_tests/photon_source/results_true.dat +++ b/tests/regression_tests/photon_source/results_true.dat @@ -1,5 +1,5 @@ tally 1: -2.263938E+02 -5.125417E+04 +2.263761E+02 +5.124615E+04 0.000000E+00 0.000000E+00 diff --git a/tests/regression_tests/weightwindows/results_true.dat b/tests/regression_tests/weightwindows/results_true.dat index 6bb992cd4..671be1d7b 100644 --- a/tests/regression_tests/weightwindows/results_true.dat +++ b/tests/regression_tests/weightwindows/results_true.dat @@ -1 +1 @@ -952c20fefac374d3b9fd28627fda7d5ae262f8cd7c01a33f0381526204a2287c18e56259a599dc6fdc1b59a2d2866fdfeedea371154c8fa7a69dc5c445b08c2d \ No newline at end of file +a5880ad9262e8aba90801783891ee74618144101401f06fb46e954e851a3c517ab28d5f0d6e1b2b364844f08d363cba35ab23b63fc81012ea8a6a328755d56c4 \ No newline at end of file From c1c5c0b93ecc93815afb3554a2812fb22b50d9c4 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Wed, 7 May 2025 18:51:40 -0600 Subject: [PATCH 322/671] Apply resolve paths to path values in `config` (#3400) --- docs/source/usersguide/data.rst | 3 ++- openmc/config.py | 2 +- tests/unit_tests/test_config.py | 7 +++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/source/usersguide/data.rst b/docs/source/usersguide/data.rst index 2a9cd36db..8b2938556 100644 --- a/docs/source/usersguide/data.rst +++ b/docs/source/usersguide/data.rst @@ -30,7 +30,8 @@ responsible for specifying one or more of the following: Each of the above files can specified in several ways. In the Python API, a :ref:`runtime configuration variable ` :data:`openmc.config` can be used to specify any of the above and is initialized -using a set of environment variables. +using a set of environment variables. Data configuration paths set in +:data:`openmc.config` will be expanded to absolute paths. .. _usersguide_data_runtime: diff --git a/openmc/config.py b/openmc/config.py index ab53ab61b..56d8a4072 100644 --- a/openmc/config.py +++ b/openmc/config.py @@ -60,7 +60,7 @@ class _Config(MutableMapping): return repr(self._mapping) def _set_path(self, key, value): - self._mapping[key] = p = Path(value) + self._mapping[key] = p = Path(value).resolve() if not p.exists(): warnings.warn(f"'{value}' does not exist.") diff --git a/tests/unit_tests/test_config.py b/tests/unit_tests/test_config.py index 9d3f53a74..4e87de4b7 100644 --- a/tests/unit_tests/test_config.py +++ b/tests/unit_tests/test_config.py @@ -1,5 +1,6 @@ from collections.abc import Mapping import os +from pathlib import Path import openmc import pytest @@ -34,6 +35,12 @@ def test_config_basics(): with pytest.raises(KeyError): openmc.config['🐖'] = '/like/to/eat/bacon' + # ensure relative paths are expanded into absolute + # paths + chain_path = Path('./chain.xml') + openmc.config['chain_file'] = chain_path + assert openmc.config['chain_file'] == chain_path.resolve() + def test_config_patch(): openmc.config['cross_sections'] = '/path/to/cross_sections.xml' From ba834be5c29f4e2a5218cf6377bf536b0358a12c Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Thu, 8 May 2025 08:10:33 +0200 Subject: [PATCH 323/671] added type hints to model file (#3399) Co-authored-by: Paul Romano --- openmc/model/model.py | 223 ++++++++++++++++++++++++++++++------------ 1 file changed, 162 insertions(+), 61 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index 9ff574ec6..c19c8ac9f 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -67,8 +67,14 @@ class Model: """ - def __init__(self, geometry=None, materials=None, settings=None, - tallies=None, plots=None): + def __init__( + self, + geometry: openmc.Geometry | None = None, + materials: openmc.Materials = None, + settings: openmc.Settings | None = None, + tallies: openmc.Tallies | None = None, + plots: openmc.Plots | None = None, + ): self.geometry = openmc.Geometry() if geometry is None else geometry self.materials = openmc.Materials() if materials is None else materials self.settings = openmc.Settings() if settings is None else settings @@ -192,24 +198,29 @@ class Model: return result @classmethod - def from_xml(cls, geometry='geometry.xml', materials='materials.xml', - settings='settings.xml', tallies='tallies.xml', - plots='plots.xml') -> Model: + def from_xml( + cls, + geometry: PathLike = "geometry.xml", + materials: PathLike = "materials.xml", + settings: PathLike = "settings.xml", + tallies: PathLike = "tallies.xml", + plots: PathLike = "plots.xml", + ) -> Model: """Create model from existing XML files Parameters ---------- - geometry : str + geometry : PathLike Path to geometry.xml file - materials : str + materials : PathLike Path to materials.xml file - settings : str + settings : PathLike Path to settings.xml file - tallies : str + tallies : PathLike Path to tallies.xml file .. versionadded:: 0.13.0 - plots : str + plots : PathLike Path to plots.xml file .. versionadded:: 0.13.0 @@ -229,14 +240,14 @@ class Model: return cls(geometry, materials, settings, tallies, plots) @classmethod - def from_model_xml(cls, path='model.xml'): + def from_model_xml(cls, path: PathLike = "model.xml") -> Model: """Create model from single XML file .. versionadded:: 0.13.3 Parameters ---------- - path : str or PathLike + path : PathLike Path to model.xml file """ parser = ET.XMLParser(huge_tree=True) @@ -262,8 +273,17 @@ class Model: return model - def init_lib(self, threads=None, geometry_debug=False, restart_file=None, - tracks=False, output=True, event_based=None, intracomm=None, directory=None): + def init_lib( + self, + threads: int | None = None, + geometry_debug: bool = False, + restart_file: PathLike | None = None, + tracks: bool = False, + output: bool = True, + event_based: bool | None = None, + intracomm=None, + directory: PathLike | None = None, + ): """Initializes the model in memory via the C API .. versionadded:: 0.13.0 @@ -278,7 +298,7 @@ class Model: variable). geometry_debug : bool, optional Turn on geometry debugging during simulation. Defaults to False. - restart_file : str, optional + restart_file : PathLike, optional Path to restart file to use tracks : bool, optional Enables the writing of particles tracks. The number of particle @@ -291,7 +311,7 @@ class Model: the Settings will be used. intracomm : mpi4py.MPI.Intracomm or None, optional MPI intracommunicator - directory : str or None, optional + directory : PathLike or None, optional Directory to write XML files to. Defaults to None. """ @@ -365,9 +385,15 @@ class Model: openmc.lib.finalize() - def deplete(self, timesteps, method='cecm', final_step=True, - operator_kwargs=None, directory='.', output=True, - **integrator_kwargs): + def deplete( + self, + method: str = "cecm", + final_step: bool = True, + operator_kwargs: dict | None = None, + directory: PathLike = ".", + output: bool = True, + **integrator_kwargs, + ): """Deplete model using specified timesteps/power .. versionchanged:: 0.13.0 @@ -376,10 +402,12 @@ class Model: Parameters ---------- - timesteps : iterable of float - Array of timesteps in units of [s]. Note that values are not - cumulative. - method : str, optional + timesteps : iterable of float or iterable of tuple + Array of timesteps. Note that values are not cumulative. The units are + specified by the `timestep_units` argument when `timesteps` is an + iterable of float. Alternatively, units can be specified for each step + by passing an iterable of (value, unit) tuples. + method : str Integration method used for depletion (e.g., 'cecm', 'predictor'). Defaults to 'cecm'. final_step : bool, optional @@ -388,14 +416,14 @@ class Model: operator_kwargs : dict Keyword arguments passed to the depletion operator initializer (e.g., :func:`openmc.deplete.Operator`) - directory : str, optional + directory : PathLike, optional Directory to write XML files to. If it doesn't exist already, it will be created. Defaults to the current working directory output : bool Capture OpenMC output from standard out integrator_kwargs : dict - Remaining keyword arguments passed to the depletion Integrator - initializer (e.g., :func:`openmc.deplete.integrator.cecm`). + Remaining keyword arguments passed to the depletion integrator + (e.g., :class:`openmc.deplete.CECMIntegrator`). """ @@ -426,8 +454,7 @@ class Model: check_value('method', method, dep.integrators.integrator_by_name.keys()) integrator_class = dep.integrators.integrator_by_name[method] - integrator = integrator_class(depletion_operator, timesteps, - **integrator_kwargs) + integrator = integrator_class(depletion_operator, **integrator_kwargs) # Now perform the depletion with openmc.lib.quiet_dll(output): @@ -456,7 +483,7 @@ class Model: Parameters ---------- - directory : str + directory : PathLike Directory to write XML files to. If it doesn't exist already, it will be created. remove_surfs : bool @@ -570,7 +597,7 @@ class Model: fh.write(ET.tostring(plots_element, encoding="unicode")) fh.write("\n") - def import_properties(self, filename): + def import_properties(self, filename: PathLike): """Import physical properties .. versionchanged:: 0.13.0 @@ -578,7 +605,7 @@ class Model: Parameters ---------- - filename : str + filename : PathLike Path to properties HDF5 file See Also @@ -631,11 +658,22 @@ class Model: C_mat = openmc.lib.materials[mat_id] C_mat.set_density(atom_density, 'atom/b-cm') - def run(self, particles=None, threads=None, geometry_debug=False, - restart_file=None, tracks=False, output=True, cwd='.', - openmc_exec='openmc', mpi_args=None, event_based=None, - export_model_xml=True, apply_tally_results=False, - **export_kwargs): + def run( + self, + particles: int | None = None, + threads: int | None = None, + geometry_debug: bool = False, + restart_file: PathLike | None = None, + tracks: bool = False, + output: bool = True, + cwd: PathLike = ".", + openmc_exec: PathLike = "openmc", + mpi_args: Iterable[str] = None, + event_based: bool | None = None, + export_model_xml: bool = True, + apply_tally_results: bool = False, + **export_kwargs, + ) -> Path: """Run OpenMC If the C API has been initialized, then the C API is used, otherwise, @@ -767,10 +805,17 @@ class Model: return last_statepoint - def calculate_volumes(self, threads=None, output=True, cwd='.', - openmc_exec='openmc', mpi_args=None, - apply_volumes=True, export_model_xml=True, - **export_kwargs): + def calculate_volumes( + self, + threads: int | None = None, + output: bool = True, + cwd: PathLike = ".", + openmc_exec: PathLike = "openmc", + mpi_args: list[str] | None = None, + apply_volumes: bool = True, + export_model_xml: bool = True, + **export_kwargs, + ): """Runs an OpenMC stochastic volume calculation and, if requested, applies volumes to the model @@ -1116,8 +1161,14 @@ class Model: """ self.tallies.add_results(statepoint) - def plot_geometry(self, output=True, cwd='.', openmc_exec='openmc', - export_model_xml=True, **export_kwargs): + def plot_geometry( + self, + output: bool = True, + cwd: PathLike = ".", + openmc_exec: PathLike = "openmc", + export_model_xml: bool = True, + **export_kwargs, + ): """Creates plot images as specified by the Model.plots attribute .. versionadded:: 0.13.0 @@ -1126,10 +1177,10 @@ class Model: ---------- output : bool, optional Capture OpenMC output from standard out - cwd : str, optional + cwd : PathLike, optional Path to working directory to run in. Defaults to the current working directory. - openmc_exec : str, optional + openmc_exec : PathLike, optional Path to OpenMC executable. Defaults to 'openmc'. This only applies to the case when not using the C API. export_model_xml : bool, optional @@ -1159,8 +1210,14 @@ class Model: openmc.plot_geometry(output=output, openmc_exec=openmc_exec, path_input=path_input) - def _change_py_lib_attribs(self, names_or_ids, value, obj_type, - attrib_name, density_units='atom/b-cm'): + def _change_py_lib_attribs( + self, + names_or_ids: Iterable[str] | Iterable[int], + value: float | Iterable[float], + obj_type: str, + attrib_name: str, + density_units: str = "atom/b-cm", + ): # Method to do the same work whether it is a cell or material and # a temperature or volume check_type('names_or_ids', names_or_ids, Iterable, (Integral, str)) @@ -1239,7 +1296,9 @@ class Model: else: setattr(lib_obj, attrib_name, value) - def rotate_cells(self, names_or_ids, vector): + def rotate_cells( + self, names_or_ids: Iterable[str] | Iterable[int], vector: Iterable[float] + ): """Rotate the identified cell(s) by the specified rotation vector. The rotation is only applied to cells filled with a universe. @@ -1261,7 +1320,9 @@ class Model: self._change_py_lib_attribs(names_or_ids, vector, 'cell', 'rotation') - def translate_cells(self, names_or_ids, vector): + def translate_cells( + self, names_or_ids: Iterable[str] | Iterable[int], vector: Iterable[float] + ): """Translate the identified cell(s) by the specified translation vector. The translation is only applied to cells filled with a universe. @@ -1284,7 +1345,12 @@ class Model: self._change_py_lib_attribs(names_or_ids, vector, 'cell', 'translation') - def update_densities(self, names_or_ids, density, density_units='atom/b-cm'): + def update_densities( + self, + names_or_ids: Iterable[str] | Iterable[int], + density: float, + density_units: str = "atom/b-cm", + ): """Update the density of a given set of materials to a new value .. note:: If applying this change to a name that is not unique, then @@ -1307,7 +1373,9 @@ class Model: self._change_py_lib_attribs(names_or_ids, density, 'material', 'density', density_units) - def update_cell_temperatures(self, names_or_ids, temperature): + def update_cell_temperatures( + self, names_or_ids: Iterable[str] | Iterable[int], temperature: float + ): """Update the temperature of a set of cells to the given value .. note:: If applying this change to a name that is not unique, then @@ -1328,7 +1396,9 @@ class Model: self._change_py_lib_attribs(names_or_ids, temperature, 'cell', 'temperature') - def update_material_volumes(self, names_or_ids, volume): + def update_material_volumes( + self, names_or_ids: Iterable[str] | Iterable[int], volume: float + ): """Update the volume of a set of materials to the given value .. note:: If applying this change to a name that is not unique, then @@ -1449,7 +1519,14 @@ class Model: self.geometry.get_all_materials().values() ) - def _generate_infinite_medium_mgxs(self, groups, nparticles, mgxs_path, correction, directory): + def _generate_infinite_medium_mgxs( + self, + groups: openmc.mgxs.EnergyGroups, + nparticles: int, + mgxs_path: PathLike, + correction: str | None, + directory: PathLike, + ): """Generate a MGXS library by running multiple OpenMC simulations, each representing an infinite medium simulation of a single isolated material. A discrete source is used to sample particles, with an equal @@ -1567,7 +1644,11 @@ class Model: mgxs_file.export_to_hdf5(mgxs_path) @staticmethod - def _create_stochastic_slab_geometry(materials, cell_thickness=1.0, num_repeats=100): + def _create_stochastic_slab_geometry( + materials: Sequence[openmc.Material], + cell_thickness: float = 1.0, + num_repeats: int = 100, + ) -> tuple[openmc.Geometry, openmc.stats.Box]: """Create a geometry representing a stochastic "sandwich" of materials in a layered slab geometry. To reduce the impact of the order of materials in the slab, the materials are applied to 'num_repeats' different randomly @@ -1636,7 +1717,14 @@ class Model: return geometry, box - def _generate_stochastic_slab_mgxs(self, groups, nparticles, mgxs_path, correction, directory) -> None: + def _generate_stochastic_slab_mgxs( + self, + groups: openmc.mgxs.EnergyGroups, + nparticles: int, + mgxs_path: PathLike, + correction: str | None, + directory: PathLike, + ) -> None: """Generate MGXS assuming a stochastic "sandwich" of materials in a layered slab geometry. While geometry-specific spatial shielding effects are not captured, this method can be useful when the geometry has materials only @@ -1741,7 +1829,14 @@ class Model: mgxs_file = mgxs_lib.create_mg_library(xs_type='macro', xsdata_names=names) mgxs_file.export_to_hdf5(mgxs_path) - def _generate_material_wise_mgxs(self, groups, nparticles, mgxs_path, correction, directory) -> None: + def _generate_material_wise_mgxs( + self, + groups: openmc.mgxs.EnergyGroups, + nparticles: int, + mgxs_path: PathLike, + correction: str | None, + directory: PathLike, + ) -> None: """Generate a material-wise MGXS library for the model by running the original continuous energy OpenMC simulation of the full material geometry and source, and tally MGXS data for each material. This method @@ -1758,12 +1853,12 @@ class Model: Energy group structure for the MGXS. nparticles : int Number of particles to simulate per batch when generating MGXS. - mgxs_path : str + mgxs_path : PathLike Filename for the MGXS HDF5 file. correction : str Transport correction to apply to the MGXS. Options are None and "P0". - directory : str + directory : PathLike Directory to run the simulation in, so as to contain XML files. """ openmc.reset_auto_ids() @@ -1831,9 +1926,15 @@ class Model: xs_type='macro', xsdata_names=names) mgxs_file.export_to_hdf5(mgxs_path) - def convert_to_multigroup(self, method="material_wise", groups='CASMO-2', - nparticles=2000, overwrite_mgxs_library=False, - mgxs_path: PathLike = "mgxs.h5", correction=None): + def convert_to_multigroup( + self, + method: str = "material_wise", + groups: str = "CASMO-2", + nparticles: int = 2000, + overwrite_mgxs_library: bool = False, + mgxs_path: PathLike = "mgxs.h5", + correction: str | None = None, + ): """Convert all materials from continuous energy to multigroup. If no MGXS data library file is found, generate one using one or more @@ -1868,7 +1969,7 @@ class Model: self.sync_dagmc_universes() self.finalize_lib() break - + # Make sure all materials have a name, and that the name is a valid HDF5 # dataset name for material in self.materials: From f615441f069cea5a835c1302fa84f7f5a9bdf538 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Fri, 9 May 2025 22:10:25 -0500 Subject: [PATCH 324/671] Random Ray Misc Memory Error Fixes (#3405) Co-authored-by: Hunter Belanger --- src/random_ray/flat_source_domain.cpp | 8 +++++--- src/random_ray/random_ray.cpp | 21 +++++++++++++-------- src/random_ray/source_region.cpp | 6 +++++- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 8ab30abf2..cd073f350 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -206,9 +206,11 @@ void FlatSourceDomain::set_flux_to_flux_plus_source( int material = source_regions_.material(sr); if (material == MATERIAL_VOID) { source_regions_.scalar_flux_new(sr, g) /= volume; - source_regions_.scalar_flux_new(sr, g) += - 0.5f * source_regions_.external_source(sr, g) * - source_regions_.volume_sq(sr); + if (settings::run_mode == RunMode::FIXED_SOURCE) { + source_regions_.scalar_flux_new(sr, g) += + 0.5f * source_regions_.external_source(sr, g) * + source_regions_.volume_sq(sr); + } } else { double sigma_t = sigma_t_[source_regions_.material(sr) * negroups_ + g]; source_regions_.scalar_flux_new(sr, g) /= (sigma_t * volume); diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp index 532840f1f..27819591c 100644 --- a/src/random_ray/random_ray.cpp +++ b/src/random_ray/random_ray.cpp @@ -518,8 +518,10 @@ void RandomRay::attenuate_flux_flat_source_void( } // Add source to incoming angular flux, assuming void region - for (int g = 0; g < negroups_; g++) { - angular_flux_[g] += srh.external_source(g) * distance; + if (settings::run_mode == RunMode::FIXED_SOURCE) { + for (int g = 0; g < negroups_; g++) { + angular_flux_[g] += srh.external_source(g) * distance; + } } } @@ -688,7 +690,10 @@ void RandomRay::attenuate_flux_linear_source_void( // transport through a void region is greatly simplified. Here we // compute the updated flux moments. for (int g = 0; g < negroups_; g++) { - float spatial_source = srh.external_source(g); + float spatial_source = 0.f; + if (settings::run_mode == RunMode::FIXED_SOURCE) { + spatial_source = srh.external_source(g); + } float new_delta_psi = (angular_flux_[g] - spatial_source) * distance; float h1 = 0.5f; h1 = h1 * angular_flux_[g]; @@ -750,8 +755,10 @@ void RandomRay::attenuate_flux_linear_source_void( } // Add source to incoming angular flux, assuming void region - for (int g = 0; g < negroups_; g++) { - angular_flux_[g] += srh.external_source(g) * distance; + if (settings::run_mode == RunMode::FIXED_SOURCE) { + for (int g = 0; g < negroups_; g++) { + angular_flux_[g] += srh.external_source(g) * distance; + } } } @@ -782,9 +789,7 @@ void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain) fatal_error("Unknown sample method for random ray transport."); } - site.E = lower_bound_index( - data::mg.rev_energy_bins_.begin(), data::mg.rev_energy_bins_.end(), site.E); - site.E = negroups_ - site.E - 1.; + site.E = 0.0; this->from_source(&site); // Locate ray diff --git a/src/random_ray/source_region.cpp b/src/random_ray/source_region.cpp index 6c0df0157..69541f7f8 100644 --- a/src/random_ray/source_region.cpp +++ b/src/random_ray/source_region.cpp @@ -217,7 +217,11 @@ SourceRegionHandle SourceRegionContainer::get_source_region_handle(int64_t sr) handle.scalar_flux_old_ = &scalar_flux_old(sr, 0); handle.scalar_flux_new_ = &scalar_flux_new(sr, 0); handle.source_ = &source(sr, 0); - handle.external_source_ = &external_source(sr, 0); + if (settings::run_mode == RunMode::FIXED_SOURCE) { + handle.external_source_ = &external_source(sr, 0); + } else { + handle.external_source_ = nullptr; + } handle.scalar_flux_final_ = &scalar_flux_final(sr, 0); handle.tally_task_ = &tally_task(sr, 0); From 7382b5d1c8b00342335b39159a00667ca8768b05 Mon Sep 17 00:00:00 2001 From: Lorenzo Chierici Date: Mon, 12 May 2025 22:48:21 +0200 Subject: [PATCH 325/671] External transfer rates source term (#3088) Co-authored-by: Paul Romano --- docs/source/pythonapi/deplete.rst | 5 +- openmc/deplete/abc.py | 110 ++++-- openmc/deplete/chain.py | 70 +++- openmc/deplete/integrators.py | 56 +-- openmc/deplete/pool.py | 59 ++- openmc/deplete/transfer_rates.py | 355 ++++++++++++++---- .../ref_depletion_with_ext_source.h5 | Bin 0 -> 37328 bytes .../ref_depletion_with_feed.h5 | Bin 37328 -> 37328 bytes .../ref_depletion_with_removal.h5 | Bin 37328 -> 37328 bytes .../ref_depletion_with_transfer.h5 | Bin 37328 -> 37328 bytes .../ref_no_depletion_only_feed.h5 | Bin 37328 -> 37328 bytes .../ref_no_depletion_only_removal.h5 | Bin 37328 -> 37328 bytes .../ref_no_depletion_with_ext_source.h5 | Bin 0 -> 37328 bytes .../ref_no_depletion_with_transfer.h5 | Bin 37328 -> 37328 bytes .../deplete_with_transfer_rates/test.py | 45 ++- .../test_deplete_external_source_rates.py | 170 +++++++++ .../unit_tests/test_deplete_transfer_rates.py | 65 ++-- 17 files changed, 751 insertions(+), 184 deletions(-) create mode 100644 tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_ext_source.h5 create mode 100644 tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_with_ext_source.h5 create mode 100644 tests/unit_tests/test_deplete_external_source_rates.py diff --git a/docs/source/pythonapi/deplete.rst b/docs/source/pythonapi/deplete.rst index 3967f1a18..f112cf8cc 100644 --- a/docs/source/pythonapi/deplete.rst +++ b/docs/source/pythonapi/deplete.rst @@ -206,14 +206,15 @@ total system energy. The :class:`openmc.deplete.IndependentOperator` uses inner classes subclassed from those listed above to perform similar calculations. -The following classes are used to define transfer rates to model continuous -removal or feed of nuclides during depletion. +The following classes are used to define external source rates or transfer rates +to model continuous removal or feed of nuclides during depletion. .. autosummary:: :toctree: generated :nosignatures: :template: myclass.rst + transfer_rates.ExternalSourceRates transfer_rates.TransferRates Intermediate Classes diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index ab087b98e..c304b8dc6 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -29,7 +29,7 @@ from .results import Results, _SECONDS_PER_MINUTE, _SECONDS_PER_HOUR, \ _SECONDS_PER_DAY, _SECONDS_PER_JULIAN_YEAR from .pool import deplete from .reaction_rates import ReactionRates -from .transfer_rates import TransferRates +from .transfer_rates import TransferRates, ExternalSourceRates __all__ = [ @@ -607,9 +607,14 @@ class Integrator(ABC): next time step. Expected to be of the same shape as ``n0`` transfer_rates : openmc.deplete.TransferRates - Instance of TransferRates class to perform continuous transfer during depletion + Transfer rates for the depletion system used to model continuous + removal/feed between materials. .. versionadded:: 0.14.0 + external_source_rates : openmc.deplete.ExternalSourceRates + External source rates for the depletion system. + + .. versionadded:: 0.15.3 """ @@ -686,6 +691,7 @@ class Integrator(ABC): self.source_rates = np.asarray(source_rates) self.transfer_rates = None + self.external_source_rates = None if isinstance(solver, str): # Delay importing of cram module, which requires this file @@ -731,11 +737,11 @@ class Integrator(ABC): self._solver = func - def _timed_deplete(self, n, rates, dt, matrix_func=None): + def _timed_deplete(self, n, rates, dt, i=None, matrix_func=None): start = time.time() results = deplete( - self._solver, self.chain, n, rates, dt, matrix_func, - self.transfer_rates) + self._solver, self.chain, n, rates, dt, i, matrix_func, + self.transfer_rates, self.external_source_rates) return time.time() - start, results @abstractmethod @@ -885,13 +891,14 @@ class Integrator(ABC): self.operator.finalize() def add_transfer_rate( - self, - material: Union[str, int, Material], - components: Sequence[str], - transfer_rate: float, - transfer_rate_units: str = '1/s', - destination_material: Optional[Union[str, int, Material]] = None - ): + self, + material: str | int | Material, + components: Sequence[str], + transfer_rate: float, + transfer_rate_units: str = '1/s', + timesteps: Sequence[int] | None = None, + destination_material: str | int | Material | None = None + ): """Add transfer rates to depletable material. Parameters @@ -905,18 +912,79 @@ class Integrator(ABC): transfer_rate : float Rate at which elements are transferred. A positive or negative values set removal of feed rates, respectively. - destination_material : openmc.Material or str or int, Optional - Destination material to where nuclides get fed. transfer_rate_units : {'1/s', '1/min', '1/h', '1/d', '1/a'} Units for values specified in the transfer_rate argument. 's' means seconds, 'min' means minutes, 'h' means hours, 'a' means Julian years. + timesteps : list of int, optional + List of timestep indices where to set external source rates. + Defaults to None, which means the external source rate is set for + all timesteps. + destination_material : openmc.Material or str or int, Optional + Destination material to where nuclides get fed. """ if self.transfer_rates is None: - self.transfer_rates = TransferRates(self.operator, self.operator.model) + if hasattr(self.operator, 'model'): + materials = self.operator.model.materials + elif hasattr(self.operator, 'materials'): + materials = self.operator.materials + self.transfer_rates = TransferRates( + self.operator, materials, len(self.timesteps)) + + if self.external_source_rates is not None and destination_material: + raise ValueError('Currently is not possible to set a transfer rate ' + 'with destination matrial in combination with ' + 'external source rates.') + + self.transfer_rates.set_transfer_rate( + material, components, transfer_rate, transfer_rate_units, + timesteps, destination_material) + + def add_external_source_rate( + self, + material: str | int | Material, + composition: dict[str, float], + rate: float, + rate_units: str = 'g/s', + timesteps: Sequence[int] | None = None + ): + """Add external source rates to depletable material. + + Parameters + ---------- + material : openmc.Material or str or int + Depletable material + composition : dict of str to float + External source rate composition vector, where key can be an element + or a nuclide and value the corresponding weight percent. + rate : float + External source rate in units of mass per time. A positive or + negative value corresponds to a feed or removal rate, respectively. + units : {'g/s', 'g/min', 'g/h', 'g/d', 'g/a'} + Units for values specified in the `rate` argument. 's' for seconds, + 'min' for minutes, 'h' for hours, 'a' for Julian years. + timesteps : list of int, optional + List of timestep indices where to set external source rates. + Defaults to None, which means the external source rate is set for + all timesteps. + + """ + if self.external_source_rates is None: + if hasattr(self.operator, 'model'): + materials = self.operator.model.materials + elif hasattr(self.operator, 'materials'): + materials = self.operator.materials + self.external_source_rates = ExternalSourceRates( + self.operator, materials, len(self.timesteps)) + + if self.transfer_rates is not None and self.transfer_rates.index_transfer: + raise ValueError('Currently is not possible to set an external ' + 'source rate in combination with transfer rates ' + 'with destination matrial.') + + self.external_source_rates.set_external_source_rate( + material, composition, rate, rate_units, timesteps) - self.transfer_rates.set_transfer_rate(material, components, transfer_rate, - transfer_rate_units, destination_material) @add_params class SIIntegrator(Integrator): @@ -1047,10 +1115,10 @@ class SIIntegrator(Integrator): return inherited def integrate( - self, - output: bool = True, - path: PathLike = "depletion_results.h5" - ): + self, + output: bool = True, + path: PathLike = "depletion_results.h5" + ): """Perform the entire depletion process across all steps Parameters diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index 4998a2c3d..9f234683a 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -703,7 +703,7 @@ class Chain: # Return CSC representation instead of DOK return matrix.tocsc() - def form_rr_term(self, tr_rates, mats): + def form_rr_term(self, tr_rates, current_timestep, mats): """Function to form the transfer rate term matrices. .. versionadded:: 0.14.0 @@ -712,6 +712,8 @@ class Chain: ---------- tr_rates : openmc.deplete.TransferRates Instance of openmc.deplete.TransferRates + current_timestep : int + Current timestep index mats : string or two-tuple of strings Two cases are possible: @@ -740,32 +742,74 @@ class Chain: for i, nuc in enumerate(self.nuclides): elm = re.split(r'\d+', nuc.name)[0] - # Build transfer terms matrices + # Build transfer terms (nuclide transfer only) if isinstance(mats, str): mat = mats - components = tr_rates.get_components(mat) + components = tr_rates.get_components(mat, current_timestep) + if not components: + break if elm in components: - matrix[i, i] = sum(tr_rates.get_transfer_rate(mat, elm)) + 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_transfer_rate(mat, nuc.name)) + matrix[i, i] = sum( + tr_rates.get_external_rate(mat, nuc.name, current_timestep)) else: matrix[i, i] = 0.0 - #Build transfer terms matrices + + # Build transfer terms (transfer from one material into another) elif isinstance(mats, tuple): dest_mat, mat = mats - if dest_mat in tr_rates.get_destination_material(mat, elm): - dest_mat_idx = tr_rates.get_destination_material(mat, elm).index(dest_mat) - matrix[i, i] = tr_rates.get_transfer_rate(mat, elm)[dest_mat_idx] - elif dest_mat in tr_rates.get_destination_material(mat, nuc.name): - dest_mat_idx = tr_rates.get_destination_material(mat, nuc.name).index(dest_mat) - matrix[i, i] = tr_rates.get_transfer_rate(mat, nuc.name)[dest_mat_idx] + 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 - #Nothing else is allowed # Return CSC instead of DOK return matrix.tocsc() + def form_ext_source_term(self, ext_source_rates, current_timestep, mat): + """Function to form the external source rate term vectors. + + .. versionadded:: 0.15.3 + + Parameters + ---------- + ext_source_rates : openmc.deplete.ExternalSourceRates + Instance of openmc.deplete.ExternalSourceRates + current_timestep : int + Current timestep index + mat : string + Material id + + Returns + ------- + scipy.sparse.csc_matrix + Sparse vector representing external source term. + + """ + if not ext_source_rates.get_components(mat, current_timestep): + return + # Use DOK as intermediate representation + n = len(self) + vector = sp.dok_matrix((n, 1)) + + for i, nuc in enumerate(self.nuclides): + # Build source term vector + if nuc.name in ext_source_rates.get_components(mat, current_timestep): + 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() + def get_branch_ratios(self, reaction="(n,gamma)"): """Return a dictionary with reaction branching ratios diff --git a/openmc/deplete/integrators.py b/openmc/deplete/integrators.py index 50810c88a..000cb2c41 100644 --- a/openmc/deplete/integrators.py +++ b/openmc/deplete/integrators.py @@ -40,8 +40,8 @@ class PredictorIntegrator(Integrator): Time in [s] for the entire depletion interval source_rate : float Power in [W] or source rate in [neutron/sec] - _i : int or None - Iteration index. Not used + _i : int, optional + Current iteration count. Not used Returns ------- @@ -54,7 +54,7 @@ class PredictorIntegrator(Integrator): with predictor """ - proc_time, n_end = self._timed_deplete(n, rates, dt) + proc_time, n_end = self._timed_deplete(n, rates, dt, _i) return proc_time, [n_end], [] @@ -106,12 +106,12 @@ class CECMIntegrator(Integrator): Eigenvalue and reaction rates from transport simulations """ # deplete across first half of interval - time0, n_middle = self._timed_deplete(n, rates, dt / 2) + time0, n_middle = self._timed_deplete(n, rates, dt / 2, _i) res_middle = self.operator(n_middle, source_rate) # deplete across entire interval with BOS concentrations, # MOS reaction rates - time1, n_end = self._timed_deplete(n, res_middle.rates, dt) + time1, n_end = self._timed_deplete(n, res_middle.rates, dt, _i) return time0 + time1, [n_middle, n_end], [res_middle] @@ -172,26 +172,26 @@ class CF4Integrator(Integrator): """ # Step 1: deplete with matrix 1/2*A(y0) time1, n_eos1 = self._timed_deplete( - n_bos, bos_rates, dt, matrix_func=cf4_f1) + n_bos, bos_rates, dt, _i, matrix_func=cf4_f1) res1 = self.operator(n_eos1, source_rate) # Step 2: deplete with matrix 1/2*A(y1) time2, n_eos2 = self._timed_deplete( - n_bos, res1.rates, dt, matrix_func=cf4_f1) + n_bos, res1.rates, dt, _i, matrix_func=cf4_f1) res2 = self.operator(n_eos2, source_rate) # Step 3: deplete with matrix -1/2*A(y0)+A(y2) list_rates = list(zip(bos_rates, res2.rates)) time3, n_eos3 = self._timed_deplete( - n_eos1, list_rates, dt, matrix_func=cf4_f2) + n_eos1, list_rates, dt, _i, matrix_func=cf4_f2) res3 = self.operator(n_eos3, source_rate) # Step 4: deplete with two matrix exponentials list_rates = list(zip(bos_rates, res1.rates, res2.rates, res3.rates)) time4, n_inter = self._timed_deplete( - n_bos, list_rates, dt, matrix_func=cf4_f3) + n_bos, list_rates, dt, _i, matrix_func=cf4_f3) time5, n_eos5 = self._timed_deplete( - n_inter, list_rates, dt, matrix_func=cf4_f4) + n_inter, list_rates, dt, _i, matrix_func=cf4_f4) return (time1 + time2 + time3 + time4 + time5, [n_eos1, n_eos2, n_eos3, n_eos5], @@ -249,17 +249,17 @@ class CELIIntegrator(Integrator): simulation """ # deplete to end using BOS rates - proc_time, n_ce = self._timed_deplete(n_bos, rates, dt) + proc_time, n_ce = self._timed_deplete(n_bos, rates, dt, _i) res_ce = self.operator(n_ce, source_rate) # deplete using two matrix exponentials list_rates = list(zip(rates, res_ce.rates)) time_le1, n_inter = self._timed_deplete( - n_bos, list_rates, dt, matrix_func=celi_f1) + n_bos, list_rates, dt, _i, matrix_func=celi_f1) time_le2, n_end = self._timed_deplete( - n_inter, list_rates, dt, matrix_func=celi_f2) + n_inter, list_rates, dt, _i, matrix_func=celi_f2) return proc_time + time_le1 + time_le1, [n_ce, n_end], [res_ce] @@ -316,20 +316,20 @@ class EPCRK4Integrator(Integrator): """ # Step 1: deplete with matrix A(y0) / 2 - time1, n1 = self._timed_deplete(n, rates, dt, matrix_func=rk4_f1) + time1, n1 = self._timed_deplete(n, rates, dt, _i, matrix_func=rk4_f1) res1 = self.operator(n1, source_rate) # Step 2: deplete with matrix A(y1) / 2 - time2, n2 = self._timed_deplete(n, res1.rates, dt, matrix_func=rk4_f1) + time2, n2 = self._timed_deplete(n, res1.rates, dt, _i, matrix_func=rk4_f1) res2 = self.operator(n2, source_rate) # Step 3: deplete with matrix A(y2) - time3, n3 = self._timed_deplete(n, res2.rates, dt) + time3, n3 = self._timed_deplete(n, res2.rates, dt, _i) res3 = self.operator(n3, source_rate) # Step 4: deplete with matrix built from weighted rates list_rates = list(zip(rates, res1.rates, res2.rates, res3.rates)) - time4, n4 = self._timed_deplete(n, list_rates, dt, matrix_func=rk4_f4) + time4, n4 = self._timed_deplete(n, list_rates, dt, _i, matrix_func=rk4_f4) return (time1 + time2 + time3 + time4, [n1, n2, n3, n4], [res1, res2, res3]) @@ -414,9 +414,9 @@ class LEQIIntegrator(Integrator): self._prev_rates, bos_res.rates, repeat(prev_dt), repeat(dt))) time1, n_inter = self._timed_deplete( - n_bos, le_inputs, dt, matrix_func=leqi_f1) + n_bos, le_inputs, dt, i, matrix_func=leqi_f1) time2, n_eos0 = self._timed_deplete( - n_inter, le_inputs, dt, matrix_func=leqi_f2) + n_inter, le_inputs, dt, i, matrix_func=leqi_f2) res_inter = self.operator(n_eos0, source_rate) @@ -425,9 +425,9 @@ class LEQIIntegrator(Integrator): repeat(prev_dt), repeat(dt))) time3, n_inter = self._timed_deplete( - n_bos, qi_inputs, dt, matrix_func=leqi_f3) + n_bos, qi_inputs, dt, i, matrix_func=leqi_f3) time4, n_eos1 = self._timed_deplete( - n_inter, qi_inputs, dt, matrix_func=leqi_f4) + n_inter, qi_inputs, dt, i, matrix_func=leqi_f4) # store updated rates self._prev_rates = copy.deepcopy(bos_res.rates) @@ -478,7 +478,7 @@ class SICELIIntegrator(SIIntegrator): Eigenvalue and reaction rates from intermediate transport simulations """ - proc_time, n_eos = self._timed_deplete(n_bos, bos_rates, dt) + proc_time, n_eos = self._timed_deplete(n_bos, bos_rates, dt, _i) n_inter = copy.deepcopy(n_eos) # Begin iteration @@ -494,9 +494,9 @@ class SICELIIntegrator(SIIntegrator): list_rates = list(zip(bos_rates, res_bar.rates)) time1, n_inter = self._timed_deplete( - n_bos, list_rates, dt, matrix_func=celi_f1) + n_bos, list_rates, dt, _i, matrix_func=celi_f1) time2, n_inter = self._timed_deplete( - n_inter, list_rates, dt, matrix_func=celi_f2) + n_inter, list_rates, dt, _i, matrix_func=celi_f2) proc_time += time1 + time2 # end iteration @@ -560,9 +560,9 @@ class SILEQIIntegrator(SIIntegrator): inputs = list(zip(self._prev_rates, bos_rates, repeat(prev_dt), repeat(dt))) proc_time, n_inter = self._timed_deplete( - n_bos, inputs, dt, matrix_func=leqi_f1) + n_bos, inputs, dt, i, matrix_func=leqi_f1) time1, n_eos = self._timed_deplete( - n_inter, inputs, dt, matrix_func=leqi_f2) + n_inter, inputs, dt, i, matrix_func=leqi_f2) proc_time += time1 n_inter = copy.deepcopy(n_eos) @@ -580,9 +580,9 @@ class SILEQIIntegrator(SIIntegrator): inputs = list(zip(self._prev_rates, bos_rates, res_bar.rates, repeat(prev_dt), repeat(dt))) time1, n_inter = self._timed_deplete( - n_bos, inputs, dt, matrix_func=leqi_f3) + n_bos, inputs, dt, i, matrix_func=leqi_f3) time2, n_inter = self._timed_deplete( - n_inter, inputs, dt, matrix_func=leqi_f4) + n_inter, inputs, dt, i, matrix_func=leqi_f4) proc_time += time1 + time2 return proc_time, [n_eos, n_inter], [res_bar] diff --git a/openmc/deplete/pool.py b/openmc/deplete/pool.py index 27ecaa4dd..03b050af3 100644 --- a/openmc/deplete/pool.py +++ b/openmc/deplete/pool.py @@ -5,7 +5,7 @@ Provided to avoid some circular imports from itertools import repeat, starmap from multiprocessing import Pool -from scipy.sparse import bmat +from scipy.sparse import bmat, hstack, vstack, csc_matrix import numpy as np from openmc.mpi import comm @@ -40,8 +40,8 @@ def _distribute(items): return items[j:j + chunk_size] j += chunk_size -def deplete(func, chain, n, rates, dt, matrix_func=None, transfer_rates=None, - *matrix_args): +def deplete(func, chain, n, rates, dt, current_timestep=None, matrix_func=None, + transfer_rates=None, external_source_rates=None, *matrix_args): """Deplete materials using given reaction rates for a specified time Parameters @@ -58,15 +58,21 @@ def deplete(func, chain, n, rates, dt, matrix_func=None, transfer_rates=None, Reaction rates (from transport operator) dt : float Time in [s] to deplete for + current_timestep : int + Current timestep index maxtrix_func : callable, optional Function to form the depletion matrix after calling ``matrix_func(chain, rates, fission_yields)``, where ``fission_yields = {parent: {product: yield_frac}}`` Expected to return the depletion matrix required by ``func`` transfer_rates : openmc.deplete.TransferRates, Optional - Object to perform continuous reprocessing. + Transfer rates for continuous removal/feed. .. versionadded:: 0.14.0 + external_source_rates : openmc.deplete.ExternalSourceRates, Optional + External source rates for continuous removal/feed. + + .. versionadded:: 0.15.3 matrix_args: Any, optional Additional arguments passed to matrix_func @@ -93,15 +99,17 @@ def deplete(func, chain, n, rates, dt, matrix_func=None, transfer_rates=None, matrices = map(matrix_func, repeat(chain), rates, fission_yields, *matrix_args) - if transfer_rates is not None: + if (transfer_rates is not None and + current_timestep in transfer_rates.external_timesteps): # Calculate transfer rate terms as diagonal matrices transfers = map(chain.form_rr_term, repeat(transfer_rates), - transfer_rates.local_mats) + repeat(current_timestep), transfer_rates.local_mats) + # Subtract transfer rate terms from Bateman matrices matrices = [matrix - transfer for (matrix, transfer) in zip(matrices, transfers)] - if len(transfer_rates.index_transfer) > 0: + if current_timestep in transfer_rates.index_transfer: # Gather all on comm.rank 0 matrices = comm.gather(matrices) n = comm.gather(n) @@ -112,10 +120,12 @@ def deplete(func, chain, n, rates, dt, matrix_func=None, transfer_rates=None, n = [n_elm for n_mat in n for n_elm in n_mat] # Calculate transfer rate terms as diagonal matrices - transfer_pair = { - mat_pair: chain.form_rr_term(transfer_rates, mat_pair) - for mat_pair in transfer_rates.index_transfer - } + transfer_pair = {} + for mat_pair in transfer_rates.index_transfer[current_timestep]: + transfer_matrix = chain.form_rr_term(transfer_rates, + current_timestep, + mat_pair) + transfer_pair[mat_pair] = transfer_matrix # Combine all matrices together in a single matrix of matrices # to be solved in one go @@ -129,7 +139,7 @@ def deplete(func, chain, n, rates, dt, matrix_func=None, transfer_rates=None, if row == col: # Fill the diagonals with the Bateman matrices cols.append(matrices[row]) - elif mat_pair in transfer_rates.index_transfer: + elif mat_pair in transfer_rates.index_transfer[current_timestep]: # Fill the off-diagonals with the transfer pair matrices cols.append(transfer_pair[mat_pair]) else: @@ -155,6 +165,25 @@ def deplete(func, chain, n, rates, dt, matrix_func=None, transfer_rates=None, return n_result + if (external_source_rates is not None and + current_timestep in external_source_rates.external_timesteps): + # Calculate external source term vectors + sources = map(chain.form_ext_source_term, repeat(external_source_rates), + repeat(current_timestep), external_source_rates.local_mats) + + # stack vector column at the end of the matrix + matrices = [ + hstack([matrix, source]) + for matrix, source in zip(matrices, sources) + ] + + # Add a last row of zeroes to the matrices and append 1 to the last row + # of the nuclide vectors + for i, matrix in enumerate(matrices): + if not np.equal(*matrix.shape): + matrices[i] = vstack([matrix, csc_matrix([0]*matrix.shape[1])]) + n[i] = np.append(n[i], 1.0) + inputs = zip(matrices, n, repeat(dt)) if USE_MULTIPROCESSING: @@ -163,4 +192,10 @@ def deplete(func, chain, n, rates, dt, matrix_func=None, transfer_rates=None, else: n_result = list(starmap(func, inputs)) + # Remove extra value at the end of the nuclide vectors + if (external_source_rates is not None and + current_timestep in external_source_rates.external_timesteps): + external_source_rates.reformat_nuclide_vectors(n) + external_source_rates.reformat_nuclide_vectors(n_result) + return n_result diff --git a/openmc/deplete/transfer_rates.py b/openmc/deplete/transfer_rates.py index 01c9b2e35..4c28c7d15 100644 --- a/openmc/deplete/transfer_rates.py +++ b/openmc/deplete/transfer_rates.py @@ -1,31 +1,31 @@ +from collections import defaultdict from numbers import Real import re +from typing import Sequence + +import numpy as np from openmc.checkvalue import check_type, check_value from openmc import Material -from openmc.data import ELEMENT_SYMBOL +from openmc.data import ELEMENT_SYMBOL, isotopes, AVOGADRO, atomic_mass +from .results import _SECONDS_PER_MINUTE, _SECONDS_PER_HOUR, \ + _SECONDS_PER_DAY, _SECONDS_PER_JULIAN_YEAR -class TransferRates: - """Class for defining continuous removals and feeds. - Molten Salt Reactors (MSRs) benefit from continuous reprocessing, - which removes fission products and feeds fresh fuel into the system. MSRs - inspired the development of this class. +class ExternalRates: + """External rates class for defining addition terms of depletion equation. - An instance of this class can be passed directly to an instance of one of - the :class:`openmc.deplete.Integrator` classes. - - .. versionadded:: 0.14.0 + .. versionadded:: 0.15.3 Parameters ---------- operator : openmc.TransportOperator Depletion operator - model : openmc.Model - OpenMC model containing materials and geometry. If using - :class:`openmc.deplete.CoupledOperator`, the model must also contain - a :class:`opnemc.Settings` object. + materials : openmc.Materials + OpenMC materials. + number_of_timesteps : int + Total number of depletion timesteps Attributes ---------- @@ -33,22 +33,25 @@ class TransferRates: All burnable material IDs. local_mats : list of str All burnable material IDs being managed by a single process - transfer_rates : dict of str to dict - Container of transfer rates, components (elements and/or nuclides) and - destination material - index_transfer : Set of pair of str - Pair of strings needed to build final matrix (destination_material, mat) + number_of_timesteps : int + Total number of depletion timesteps + external_rates : dict of str to dict + Container of timesteps, external rates, components (elements and/or + nuclides) and optionally destination material + external_timesteps : list of int + Container of all timesteps indeces with an external rate defined. """ - def __init__(self, operator, model): + def __init__(self, operator, materials, number_of_timesteps): - self.materials = model.materials + self.materials = materials self.burnable_mats = operator.burnable_mats self.local_mats = operator.local_mats + self.number_of_timesteps = number_of_timesteps - #initialize transfer rates container dict - self.transfer_rates = {mat: {} for mat in self.burnable_mats} - self.index_transfer = set() + # initialize transfer rates container dict + self.external_rates = {mat: defaultdict(list) for mat in self.burnable_mats} + self.external_timesteps = [] def _get_material_id(self, val): """Helper method for getting material id from Material obj or name. @@ -71,7 +74,7 @@ class TransferRates: check_value('Material ID', str(val), self.burnable_mats) else: check_value('Material name', val, - [mat.name for mat in self.materials if mat.depletable]) + [mat.name for mat in self.materials if mat.depletable]) val = [mat.id for mat in self.materials if mat.name == val][0] elif isinstance(val, int): @@ -79,7 +82,13 @@ class TransferRates: return str(val) - def get_transfer_rate(self, material, component): + def get_external_rate( + self, + material: str | int | Material, + component: str, + timestep: int, + destination_material: str | int | Material | None = None + ): """Return transfer rate for given material and element. Parameters @@ -88,62 +97,114 @@ class TransferRates: Depletable material component : str Element or nuclide to get transfer rate value + timestep : int + Current timestep index + destination_material : openmc.Material or str or int, Optional + Destination material to where nuclides get fed Returns ------- - transfer_rate : list of floats - Transfer rate values + external_rate : list of floats + External rate values """ material_id = self._get_material_id(material) check_type('component', component, str) - return [i[0] for i in self.transfer_rates[material_id][component]] - - def get_destination_material(self, material, component): - """Return destination material for given material and - component, if defined. - - Parameters - ---------- - material : openmc.Material or str or int - Depletable material - component : str - Element or nuclide that gets transferred to another material. - - Returns - ------- - destination_material_id : list of str - Depletable material ID to where the element or nuclide gets - transferred - - """ - material_id = self._get_material_id(material) - check_type('component', component, str) - if component in self.transfer_rates[material_id]: - return [i[1] for i in self.transfer_rates[material_id][component]] + if destination_material is not None: + dest_mat_id = self._get_material_id(destination_material) + return [i[1] for i in self.external_rates[material_id][component] + if timestep in i[0] and dest_mat_id == i[2]] else: - return [] + return [i[1] for i in self.external_rates[material_id][component] + if timestep in i[0]] - def get_components(self, material): - """Extract removing elements and/or nuclides for a given material + def get_components(self, material, timestep, destination_material=None): + """Extract removing elements and/or nuclides for a given material at a + given timestep Parameters ---------- material : openmc.Material or str or int Depletable material + timestep : int + Current timestep index + destination_material : openmc.Material or str or int, Optional + Destination material to where nuclides get fed Returns ------- - elements : list - List of elements and nuclides where transfer rates exist + components : list + List of elements or nuclides with external rates set at a given + timestep """ material_id = self._get_material_id(material) - if material_id in self.transfer_rates: - return self.transfer_rates[material_id].keys() + if destination_material is not None: + dest_mat_id = self._get_material_id(destination_material) + else: + dest_mat_id = None + + all_components = [] + if material_id in self.external_rates: + mat_components = self.external_rates[material_id] + + for component in mat_components: + if dest_mat_id: + # check for both timestep and destination material ids + if np.isin(timestep, [val[0] for val in mat_components[component]]) and \ + np.isin(dest_mat_id, [val[2] for val in mat_components[component]]): + all_components.append(component) + else: + # check only for timesteps + if np.isin(timestep, [val[0] for val in mat_components[component]]): + all_components.append(component) + return all_components + + +class TransferRates(ExternalRates): + """Class for defining continuous removals and feeds. + + Molten Salt Reactors (MSRs) benefit from continuous reprocessing, + which removes fission products and feeds fresh fuel into the system. MSRs + inspired the development of this class. + + An instance of this class can be passed directly to an instance of one of + the :class:`openmc.deplete.Integrator` classes. + + .. versionadded:: 0.14.0 + + Parameters + ---------- + operator : openmc.TransportOperator + Depletion operator + materials : openmc.Materials + OpenMC materials. + number_of_timesteps : int + Total number of depletion timesteps + + Attributes + ---------- + burnable_mats : list of str + All burnable material IDs. + local_mats : list of str + All burnable material IDs being managed by a single process + external_rates : dict of str to dict + Container of timesteps, transfer rates, components (elements and/or + nuclides) and destination material + external_timesteps : list of int + Container of all timesteps indeces with an external rate defined. + index_transfer : Set of pair of str + Pair of strings needed to build final matrix (destination_material, mat) + """ + + def __init__(self, operator, materials, number_of_timesteps): + super().__init__(operator, materials, number_of_timesteps) + self.index_transfer = defaultdict(list) + self.chain_nuclides = [nuc.name for nuc in operator.chain.nuclides] def set_transfer_rate(self, material, components, transfer_rate, - transfer_rate_units='1/s', destination_material=None): + transfer_rate_units='1/s', timesteps=None, + destination_material=None): """Set element and/or nuclide transfer rates in a depletable material. Parameters @@ -157,11 +218,14 @@ class TransferRates: transfer_rate : float Rate at which elements and/or nuclides are transferred. A positive or negative value corresponds to a removal or feed rate, respectively. - destination_material : openmc.Material or str or int, Optional - Destination material to where nuclides get fed. transfer_rate_units : {'1/s', '1/min', '1/h', '1/d', '1/a'} Units for values specified in the transfer_rate argument. 's' for seconds, 'min' for minutes, 'h' for hours, 'a' for Julian years. + timesteps : list of int, Optional + List of timestep indeces where to set transfer rates. + Default to None means the transfer rate is set for all timesteps. + destination_material : openmc.Material or str or int, Optional + Destination material to where nuclides get fed. """ material_id = self._get_material_id(material) @@ -183,19 +247,25 @@ class TransferRates: if transfer_rate_units in ('1/s', '1/sec'): unit_conv = 1 elif transfer_rate_units in ('1/min', '1/minute'): - unit_conv = 60 + unit_conv = _SECONDS_PER_MINUTE elif transfer_rate_units in ('1/h', '1/hr', '1/hour'): - unit_conv = 60*60 + unit_conv = _SECONDS_PER_HOUR elif transfer_rate_units in ('1/d', '1/day'): - unit_conv = 24*60*60 + unit_conv = _SECONDS_PER_DAY elif transfer_rate_units in ('1/a', '1/year'): - unit_conv = 365.25*24*60*60 + unit_conv = _SECONDS_PER_JULIAN_YEAR else: - raise ValueError('Invalid transfer rate unit ' - f'"{transfer_rate_units}"') + raise ValueError(f'Invalid transfer rate unit "{transfer_rate_units}"') + + if timesteps is not None: + for timestep in timesteps: + check_value('timestep', timestep, range(self.number_of_timesteps)) + timesteps = np.array(timesteps) + else: + timesteps = np.arange(self.number_of_timesteps) for component in components: - current_components = self.transfer_rates[material_id].keys() + current_components = self.external_rates[material_id].keys() split_component = re.split(r'\d+', component) element = split_component[0] if element not in ELEMENT_SYMBOL.values(): @@ -219,11 +289,144 @@ class TransferRates: f'where element {element} already has ' 'a transfer rate.') - if component in self.transfer_rates[material_id]: - self.transfer_rates[material_id][component].append( - (transfer_rate / unit_conv, destination_material_id)) - else: - self.transfer_rates[material_id][component] = [ - (transfer_rate / unit_conv, destination_material_id)] + self.external_rates[material_id][component].append( + (timesteps, transfer_rate/unit_conv, destination_material_id)) + if destination_material_id is not None: - self.index_transfer.add((destination_material_id, material_id)) + for timestep in timesteps: + self.index_transfer[timestep].append( + (destination_material_id, material_id)) + + self.external_timesteps = np.unique(np.concatenate( + [self.external_timesteps, timesteps])) + + +class ExternalSourceRates(ExternalRates): + """Class for defining external source rates. + + An instance of this class can be passed directly to an instance of one of + the :class:`openmc.deplete.Integrator` classes. + + .. versionadded:: 0.15.3 + + Parameters + ---------- + operator : openmc.TransportOperator + Depletion operator + materials : openmc.Materials + OpenMC materials. + number_of_timesteps : int + Total number of depletion timesteps + + Attributes + ---------- + burnable_mats : list of str + All burnable material IDs. + local_mats : list of str + All burnable material IDs being managed by a single process + external_timesteps : list of int + Container of all timesteps indeces with an external rate defined. + external_rates : dict of str to dict + Container of timesteps external source rates, and components + (elements and/or nuclides) + """ + + def reformat_nuclide_vectors(self, vectors): + """Remove last element of nuclide vector that was added for handling + external source rates by the depletion solver. + + Parameters + ---------- + vectors : list of array + List of nuclides vector to reformat + + """ + for mat_index, i in enumerate(self.local_mats): + if self.external_rates[i]: + vectors[mat_index] = vectors[mat_index][:-1] + + def set_external_source_rate( + self, + material: str | int | Material, + composition: dict[str, float], + rate: float, + rate_units: str = 'g/s', + timesteps: Sequence[int] | None = None + ): + """Set element and/or nuclide composition vector external source rates + to a depletable material. + + Parameters + ---------- + material : openmc.Material or str or int + Depletable material + composition : dict of str to float + External source rate composition vector, where key can be an element + or a nuclide and value the corresponding weight percent. + rate : float + External source rate in units of mass per time. A positive or + negative value corresponds to a feed or removal rate, respectively. + units : {'g/s', 'g/min', 'g/h', 'g/d', 'g/a'} + Units for values specified in the `rate` argument. 's' for seconds, + 'min' for minutes, 'h' for hours, 'a' for Julian years. + timesteps : list of int, optional + List of timestep indices where to set external source rates. Default + to None, which means the external source rate is set for all + timesteps. + + """ + + material_id = self._get_material_id(material) + check_type('rate', rate, Real) + check_type('composition', composition, dict, str) + + if rate_units in ('g/s', 'g/sec'): + unit_conv = 1 + elif rate_units in ('g/min', 'g/minute'): + unit_conv = _SECONDS_PER_MINUTE + elif rate_units in ('g/h', 'g/hr', 'g/hour'): + unit_conv = _SECONDS_PER_HOUR + elif rate_units in ('g/d', 'g/day'): + unit_conv = _SECONDS_PER_DAY + elif rate_units in ('g/a', 'g/year'): + unit_conv = _SECONDS_PER_JULIAN_YEAR + else: + raise ValueError(f'Invalid external source rate unit "{rate_units}"') + + if timesteps is not None: + for timestep in timesteps: + check_value('timestep', timestep, range(self.number_of_timesteps)) + timesteps = np.asarray(timesteps) + else: + timesteps = np.arange(self.number_of_timesteps) + + components = composition.keys() + percents = composition.values() + norm_percents = [float(i) / sum(percents) for i in percents] + + atoms_per_nuc = {} + for component, percent in zip(components, norm_percents): + split_component = re.split(r'\d+', component) + element = split_component[0] + if element not in ELEMENT_SYMBOL.values(): + raise ValueError(f'{component} is not a valid nuclide or element.') + + if len(split_component) == 1: + if not isotopes(component): + raise ValueError(f'Cannot add element {component} ' + 'as it is not naturally abundant. ' + 'Specify a nuclide vector instead.') + for nuc, frac in isotopes(component): + atoms_per_nuc[nuc] = (rate / atomic_mass(nuc) * AVOGADRO * + frac * percent / unit_conv) + + else: + atoms_per_nuc[component] = (rate / atomic_mass(component) * + AVOGADRO * percent / unit_conv) + + for nuc, val in atoms_per_nuc.items(): + self.external_rates[material_id][nuc].append((timesteps, val, None)) + + self.external_timesteps = np.unique(np.concatenate( + [self.external_timesteps, timesteps] + )) diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_ext_source.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_ext_source.h5 new file mode 100644 index 0000000000000000000000000000000000000000..aa09e1bcf4e352f94798f2cc266718b5f77fcec1 GIT binary patch literal 37328 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 delta 693 zcmcbxnCZe|rVXd;7#TL7wv%TBQm^gj1F6Z~j{kw2ZYO>aXSOqlGuvgUC!@?{$G-Ub zZIjMO#I5AFV}JsCzP}w+i{8Nbjt>ml!{a8{!sIPqYq9+MAOV+uv2vD|`KhIF{_f=q zAA~(rg!4UT6u4|LHndY)@AJ>@>mS>D=M3+!(^0qku~2fA_6Ns$yNhK9Vr3uxu@$sP zYJa%ys;$ZYd+`cA3UC8Xy`0Q)pp4lb=73vnE8JqT7r-4<#(!kp(aS1u^%MP%Z@v_? zAI{JI^StFlg&~~3)nlHyR>xUO(^j?3eJ$VqozFzLkET!FI6=OC`mEK9jrZulbv;gT zmHu|h#BN^BtSN^W|Jk0o(;8qOu3@()@niL@e~xzT+>dqI&iuD6=2-ghbN*G^DWCW6 zt(QTBMM8Z_Ra#Xa9#tDI9hB<|Z#b*vCdpUNupP1r&||wBXb{ diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_removal.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_removal.h5 index 4b24bed52ad30a270df65704cecf8aef59078557..271af24103cda8f8c4886da551422a824dcde360 100644 GIT binary patch delta 720 zcmcbxnCZe|rVXd;7@0Powv%TBQm^gj1F6Z~j{kw2ZYO>aXSOqlGuvgUC!_3S$G-Ub z`blRb;#Tt8F+hO|-`|d^MQ`AI!v_ZK;c*jeVe-PSwOIarkbukYUOCIl{M1r7fAR8# z55gWQ!ue)13S71r8`}L}=kw3*>mS=a=M3+!(^0qky-;$M_6Ns$yBTE%Vr3uxvHfSB z)c$bYRok%t_u>_L6yOFdcsZHnKpC?=%mF*xR=CAvFMvD9ng7VTqnB0S>QDF|-+U=( zKb-IX=XuMA3PU*m?j1EDPz77M!#20J_n|UnY3QEOd`t6%^;ry)v>$QDbKiW@R zHu+S)l5Tw95&LVOWXh8J4V)i-%?pV7q2jzS|7oOSos~0-Rkiu-Zz|4;nzpKKCa0XH zPnMjJUa#TK<#D4A5yZl)U3P48Lfz{@F?$ zH(nv%Fo^RFe%OS1TEZ2iO+31{_^BzJzm#RxO@U1x?VT4* LUNupP1r)>paXSOqlGuvgUC!@?{$G-Ub zU6am8#I5AFV}JrDzP}w+i{8NbE)NXa!{a8{!sM-AYq9+MAOV+uy>gb9`KhIF{=Vf4 zAA~(rg!8>-6u4|LHnh`P@AJ>@>mS?u=M3+!(^0qku~2fA_6Ns$yDMb}Vr3uxu@$jM zYJa%ys;$}od+`cA3UC9?yqwH(pp4lb=74){E8JqT7r-4<#eZbo(aS1u_0#;1Z@v_? zAI{JE^StFlg&~~3>ynn>^fG5CUx$IA>jm4vW*!T;f^%Xq{q{||aQ=FM_1eCzAMF#C zPCnJIq<^@FkP;p+E|1{FE&dT|ZWwrV2Zz|5pnzpKKCa0WM zOqQIGUT^5m<#D4A5yXP4U3P48Lfz{@Jn| zH(?*JrcAgLsoqrpqj?rSKpQy)n=1-F!rp9Q#pUwCd0i zxc=&VFURQ>MsWVhITbwGu>nv%Fo;VGe%OS1TEZ2yO+31{_^BzJ-^nuTrog6;_FYRR LubQaD0t#XPxH8vc diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_transfer.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_transfer.h5 index 51173f778b945f5c666bd9a8ed9ace75f47eb0ec..25365f6f3a486e7cc6ffa3ce844d4ed3fa17a95c 100644 GIT binary patch delta 747 zcmcbxnCZe|rVXd;7@0Powv%TBQm^gj1F6Z~j{kw2ZYO>aXSOqlGuvgUC!_3S$G-Ub znn`CQ;#Tt8F+hPC-`|d^MQ`AIod*W(;c*jeVe)dXwOIarkbujtT{+9k{M1r7fA#W( z55gWQ!ubv}3S71r8``n1_xWe{^^fg=bB6cV>8RWNS}3_n`-5Y>-Q2PRv9b^U*#0+9 zYJa%ys%^^ud+`cA3UCAVznsi+pp4lb=72SBE8JqT7r-54$A4tq(aS1u^=ti)Z@v_? zAI?ws^StFlg&~}O?UI(@^fG5CUx$HV!V9*8%{&%x1(U>L`t6%^;ruNE>$QDbKiW$y zn|!KYIYqL_{>mqrvgCdP=lfst0-}DXI4{V58tGVP<^0dG+I;pm6=y|FTh%s`Q%=*? zFN}Km^|q7b6BRc^s6l{FfSF{d#K&8>4(#Mfvfj7Ezoh^ zJvnfKzTxvU{%g7!d~kQHYY!LjzWo}`UoVu~79}td9uV%6H%^eRSKe)LTyU`-TvY;- zPRfT!JG-#7kbTw*|JmB}?}#v$(6rkf@axu!eU5h5#G9{8-1^UU!k!BcORitDz1ih? zCPfMne=FouU#G=p=3n$G%*2jC9K&N)0GkkJIr z-*Q~&^`-5uaQ?N71A)5=Ea7~~OTuLjZomy>@MwKGX-dW?`+quQC{jF delta 747 zcmcbxnCZe|rVXd;7#TL7wv%TBQm^gj1F6Z~j{kw2ZYO>aXSOqlGuvgUC!@?{$G-Ub zZIjMO#I5AFV}JsCzP}w+i{8NbP7e&)!{a8{!sLx#Yq9+MAOV+uwsMx2`KhIF{@&#a zAA~(rg!A2J6u4|LHndY;@AJ>@>mS>D=M3+!(^0qku~2fA_6Ns$yGvyUVr3uxu@$sP zYJa%ys;%+=d+`cA3UC8Xy`0Q)pp4lb=75`SE8JqT7r-4<#(!kp(aS1u_5J?GH(v_c z59eq8dEWA&!Vu2ia!E^YdYLnnufxF5@`CMPGmiya!DX?Se)}d}IDfssdTrmm zC!gw9PMKS1f8~=*S#rOD^WCp`0Z~6xoR{Z6jdZNDa%QrsHlO`X#aTad^^4A~US^?dzuUT8Iwe*<{c4K~%u6}<9%?si`r-6R;OhNN3v`^f zP7a))Z}=*W|C(+FAKV>V+QS9BZ@-4~4+-VAMF~uV2ZYt+jT7YS6?a=47hJ3dS5?fU zlky?b&Mqb`WS{lIf41iQJ0i>_H0`zq{JOPbpQGJ#@#bq2xBjzTwCBRZlIz!OuXK5y zNs&UtpTN-%vElm=!GBgNIa7VsLb$D*YDrrIjNzdb&5)j;rnCOy0k{KVat==jWHf>E zR~;96eQCQZoPRLmK;W(dOE~}SMd7jsH{b>`c(uNqG$rGc{af|Pt0pQl1A`v`ms{(o diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_only_feed.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_only_feed.h5 index d53adc7d8413c904289119fcb922be662b396f96..d080b447093018f62b3eced413db81fa606d3338 100644 GIT binary patch delta 77 zcmcbxnCZe|rVXd;7@0Powv%TBQm^gj1F6Z~j{kw2ZYO>aXSOqlGuvgUC!_3U$G+8! Z%msdjCnvhfPrM+sSz*E&MiwC79spTR99sYY delta 77 zcmcbxnCZe|rVXd;7#TL7wv%TBQm^gj1F6Z~j{kw2ZYO>aXSOqlGuvgUC!@?}$G+8! Z%zKV6nw;n=KkaXSOqlGuvgUC!_3U$G+8! Z%!S%VCnvhfPrM+sSz*E&MiwC79spL192)=t delta 77 zcmcbxnCZe|rVXd;7#TL7wv%TBQm^gj1F6Z~j{kw2ZYO>aXSOqlGuvgUC!@?}$G+8! Z%u{q0O-^)`pLjuNv%-Wmj4VLDJpfd>91s8i diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_with_ext_source.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_with_ext_source.h5 new file mode 100644 index 0000000000000000000000000000000000000000..f3b1b171ebcab9d99fb7b73f5b8273ac72187971 GIT binary patch literal 37328 zcmeHPO>7%Q6rQz{)@=i%rIb)nv=vA-|4y5>0o2x|PDw8`f`WWN;56Abmhwk_N)Q~X z3Q*tx=`AM|2_dBiP!2sHaq$7998)Bu9>Xoia;TI`Rc80SFB?xhcD#vG(>@|;Ck?^OOLkx&h)IXJ}Eef}(5rxw)FpW_3E%SkS0V|3tXjecJX zw>PtXH%<+6+2UNA29urKSB(F8G$OhaG{=j?-QK8sRVSvs}8i zE8orjiuvHzO{z1P*ii}9q`mu!H!_+Z8dS{*O{1WP409Y}2Gc=%IDNnulue4|)eb#A zp)Te7y|K@SJr-9_YQf#Q0{LEVSPSSG-4Sneynk$z1syHn%YbjNf0_B#!aO3rIaa<+ zQ$#SL0j!boVMhRb%ko0uPwnhKSbr-Y9Oe@KnT9}><@@iR&p4`4P4KvMro4h6C}@%9 z)8*xelS(Zx4`8Rs3#_y71nGm^64w)ZsJ)%1OP?%1?}O^KU3V9EkwsH%7{vi)jROzL zewnvE5HRBac$DVr1FQs3z(=%4Eh!Gf_*$ye?d_xq^Ps~uu2aKJ#d6bJ|LyUpmi)iR z;regl)ud9v!W^|t=PwDBAMEw~n7bx_F_zz?DDpXKyV|SAT^Z^hAJAFKZub;VkydTH zRNH+c&h~tH@!YN7|2bugODT@85|<3XrCJgJTU@$r@Eq}Cv*Hr?WX6?h`2@S7^69V1 zeIMPpcT@AJbiQxqE0y-w>8A*0m(iZ!gs!tB`qKEYApkzj@xs8Dc~08NmQ&9a%O$zq zxgr3;`Ydr>HSr4Xi|q;W)b6pN0e5hG;G#F;B4_niWXoGQ4b@}Q!NMJ#wfgz)h}Ydi zx6iuAyj7Yi>9NJDJAB?GUIj4AR|N!Y@oJj=2*VK{HY;9%XJ)(!`4Nq0jcjnJCmPS7 z$Huel0h&m71`m<490E3;DfT0btrgGA>vhPFXgq6RgF`*hcm_Q-o)ui0NO%Shk+K{D zHlFpl#iMG)Gc(@}`4Nq0usYNejc3qf<5}_rk`SH&DJjb#VB^`s^TnfT#j~k{G|wE* z7xE(-&+v0{s3#iFpvT6usY7J9@T}NRq*fXM8_(L=k1(oMJX_}LLJQ9q@*^70_~+9N z-v@yn8_$+sB4mVT@DM4>Azy40eWkqVWuRY&Az5qebIbWy-A3W!qWZiuS=>YSZ`^*%}EpT28IWteDr_+>bXT^{+uk&fvH^XrWa%P#vft&65#ygBJ&cDPJ zASPug1Z;6-+Tba848JO+tC}|O!Hf&l@&R`F@9G7|+~DB7*am7q{s(!~cd*fT&)e1? z*Ke}=eYW>Y8uhb$et#?;3sfYO8)%Nl8mG?&M?rTWqO1P29KtsXQJMtbYw^eiT~1?Q z-kSG+O*~MqgbU=V$A>mX@z}P-3uLr!*lK)y)ws;*!)h}sQfht==*KKa^4 zr$688p}O)sCK~7wPqotSj%WqC)5~A>2CsDYkMq#6y#Mx*QlmrTBR$?HzEm)E_P_py`r7kH>v!Vi&!6plReyfmz?gWfAw?@G^50eGB^Mf9!=tr$5P}ZTZN;DA zkH7)->et`G>yHnAO6!7e^4ceAxZ299*NX@I4S1lw0X$~E2=VaM)hQly{>=Y901naD zA6xu7T>mN_OjbT=TPT6|0n$8Y*QxT8C%HRmj=SBr%gfb!ub)3V zCH@a)s!GF~zo!8h2Od^ZbLWR*35-LD_=Y@Yl|vQ}l$szQ2nYg#fFK|U2m*qDARq_` z0)oJnAYj`^y4c87X~hihCo#TSHCM3_<#{n4rMa&}UG|q%o+uF)1Ox#=KoAfF1OY)n z5D)|e0YN|z5Ck@WfbD)l^E!1F_;+Q76itcqH&~i7uS~C6P9~J(M xYPB>SfAiT-KW9Gd%3SL^aQ?f03z=)javzW1ZS7bqDJ@%u0M08)p4dOX{{cmRix2<+ literal 0 HcmV?d00001 diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_with_transfer.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_with_transfer.h5 index 469149f6742736085bebd22499c4eb2a45f519ed..b0ba997407f2471adcb32897dd08378bfe312814 100644 GIT binary patch delta 77 zcmcbxnCZe|rVXd;7@0Powv%TBQm^gj1F6Z~j{kw2ZYO>aXSOqlGuvgUC!_3U$G+8! Z%nw!-O-^)`pLjuNv%-Wmj4VLDJpg559Z>)P delta 77 zcmcbxnCZe|rVXd;7#TL7wv%TBQm^gj1F6Z~j{kw2ZYO>aXSOqlGuvgUC!@?}$G+8! Z%nfS`CnvhfPrM+sSz*E&MiwC79spV!9Ebn_ diff --git a/tests/regression_tests/deplete_with_transfer_rates/test.py b/tests/regression_tests/deplete_with_transfer_rates/test.py index 10d60866a..4ad009d22 100644 --- a/tests/regression_tests/deplete_with_transfer_rates/test.py +++ b/tests/regression_tests/deplete_with_transfer_rates/test.py @@ -1,8 +1,7 @@ -""" TransferRates depletion test suite """ +""" ExternalRates depletion test suite """ from pathlib import Path import shutil -import sys import numpy as np import pytest @@ -11,7 +10,7 @@ import openmc.deplete from openmc.deplete import CoupledOperator from tests.regression_tests import config, assert_reaction_rates_equal, \ - assert_atoms_equal, assert_same_mats + assert_atoms_equal @pytest.fixture @@ -44,9 +43,11 @@ def model(): settings.particles = 100 settings.inactive = 0 settings.batches = 10 + settings.seed = 1 return openmc.Model(geometry, materials, settings) + @pytest.mark.parametrize("rate, dest_mat, power, ref_result", [ (1e-5, None, 0.0, 'no_depletion_only_removal'), (-1e-5, None, 0.0, 'no_depletion_only_feed'), @@ -83,6 +84,40 @@ def test_transfer_rates(run_in_tmpdir, model, rate, dest_mat, power, ref_result) res_ref = openmc.deplete.Results(path_reference) res_test = openmc.deplete.Results(path_test) - assert_same_mats(res_ref, res_test) - assert_atoms_equal(res_ref, res_test, 1e-6) + assert_atoms_equal(res_ref, res_test) + assert_reaction_rates_equal(res_ref, res_test) + + +@pytest.mark.parametrize("rate, power, ref_result", [ + (1e-1, 0.0, 'no_depletion_with_ext_source'), + (1e-1, 174., 'depletion_with_ext_source'), +]) +def test_external_source_rates(run_in_tmpdir, model, rate, power, ref_result): + """Tests external_rates depletion class with external source rates""" + + chain_file = Path(__file__).parents[2] / 'chain_simple.xml' + + external_source_vector = {'U': 1} + + op = CoupledOperator(model, chain_file) + op.round_number = True + integrator = openmc.deplete.PredictorIntegrator( + op, [1], power, timestep_units='d') + integrator.add_external_source_rate('f', external_source_vector, rate) + integrator.integrate() + + # Get path to test and reference results + path_test = op.output_dir / 'depletion_results.h5' + path_reference = Path(__file__).with_name(f'ref_{ref_result}.h5') + + # If updating results, do so and return + if config['update']: + shutil.copyfile(str(path_test), str(path_reference)) + return + + # Load the reference/test results + res_ref = openmc.deplete.Results(path_reference) + res_test = openmc.deplete.Results(path_test) + + assert_atoms_equal(res_ref, res_test) assert_reaction_rates_equal(res_ref, res_test) diff --git a/tests/unit_tests/test_deplete_external_source_rates.py b/tests/unit_tests/test_deplete_external_source_rates.py new file mode 100644 index 000000000..a8cf9dde5 --- /dev/null +++ b/tests/unit_tests/test_deplete_external_source_rates.py @@ -0,0 +1,170 @@ +""" Tests for ExternalSourceRates class """ + +from pathlib import Path + +import pytest +import numpy as np +import re + +import openmc +from openmc.data import AVOGADRO, atomic_mass +from openmc.deplete import CoupledOperator +from openmc.deplete.transfer_rates import ExternalSourceRates +from openmc.deplete.abc import (_SECONDS_PER_MINUTE, _SECONDS_PER_HOUR, + _SECONDS_PER_DAY, _SECONDS_PER_JULIAN_YEAR) + +CHAIN_PATH = Path(__file__).parents[1] / "chain_simple.xml" + +@pytest.fixture +def model(): + openmc.reset_auto_ids() + f = openmc.Material(name="f") + f.add_element("U", 1, enrichment=4.25) + f.add_element("O", 2) + f.set_density("g/cm3", 10.4) + + w = openmc.Material(name="w") + w.add_element("O", 1) + w.add_element("H", 2) + w.set_density("g/cm3", 1.0) + w.depletable = True + + # material just to test multiple destination material + h = openmc.Material(name="h") + h.add_element("He", 1) + h.set_density("g/cm3", 1.78e-4) + + radii = [0.42, 0.45] + f.volume = np.pi * radii[0] ** 2 + w.volume = np.pi * (radii[1]**2 - radii[0]**2) + h.volume = 1 + materials = openmc.Materials([f, w, h]) + + surf_f = openmc.Sphere(r=radii[0]) + surf_w = openmc.Sphere(r=radii[1], boundary_type='vacuum') + surf_h = openmc.Sphere(x0=10, r=1, boundary_type='vacuum') + cell_f = openmc.Cell(fill=f, region=-surf_f) + cell_w = openmc.Cell(fill=w, region=+surf_f & -surf_w) + cell_h = openmc.Cell(fill=h, region=-surf_h) + geometry = openmc.Geometry([cell_f, cell_w, cell_h]) + + settings = openmc.Settings() + settings.particles = 1000 + settings.inactive = 10 + settings.batches = 50 + + return openmc.Model(geometry, materials, settings) + + +@pytest.mark.parametrize( +"case_name, external_source_vectors, external_source_rate, timesteps", [ + ('elements', [{'U': 0.9, 'Xe': 0.1}], 1, None), + ('nuclides', [{'I135': 0.1, 'Gd156': 0.3, 'Gd157': 0.6}], 1, None), + ('nuclides_elements', [{'I135': 0.01, 'Gd156': 0.1, 'Gd157': 0.01, 'U': 0.8, + 'Xe': 0.08}], 1, None), + ('elements_nuclides', [{'U': 0.78, 'Xe': 0.1, 'I135': 0.01, 'Gd156': 0.1, + 'Gd157': 0.01}], 1, None), + ('multiple_vectors', [{'U': 1.}, {'Xe': 1}], 1, None), + ('timesteps', [{'U': 0.9, 'Xe': 0.1}], 1, [1]), + ('rates_invalid_1', [{'Gb': 1.}], 1, None), + ('rates_invalid_2', [{'Pu': 1.}], 1, None) + ]) +def test_get_set(model, case_name, external_source_vectors, external_source_rate, + timesteps): + """Tests the get/set methods""" + + op = CoupledOperator(model, CHAIN_PATH) + number_of_timesteps = 2 + transfer = ExternalSourceRates(op, model.materials, number_of_timesteps) + + if timesteps is None: + timesteps = np.arange(number_of_timesteps) + + # Test by Openmc material, material name and material id + material= [m for m in model.materials if m.depletable][0] + + for material_input in [material, material.name, material.id]: + for external_source_vector in external_source_vectors: + if case_name == 'rates_invalid_1': + with pytest.raises(ValueError, match='Gb is not a valid ' + 'nuclide or element.'): + transfer.set_external_source_rate(material_input, + external_source_vector, + external_source_rate) + elif case_name == 'rates_invalid_2': + with pytest.raises(ValueError, match='Cannot add element Pu'): + transfer.set_external_source_rate(material_input, + external_source_vector, + external_source_rate) + else: + transfer.set_external_source_rate(material_input, + external_source_vector, + external_source_rate, + timesteps=timesteps) + for component, percent in external_source_vector.items(): + split_component = re.split(r'\d+', component) + if len(split_component) == 1: + for nuc, frac in openmc.data.isotopes(component): + val = external_source_rate * percent * frac * \ + AVOGADRO / atomic_mass(nuc) + assert transfer.get_external_rate( + material_input, nuc, timesteps)[0] == pytest.approx(val) + else: + val = external_source_rate * percent * AVOGADRO / atomic_mass(component) + assert transfer.get_external_rate( + material_input, component, timesteps)[0] == pytest.approx(val) + + assert np.all(transfer.external_timesteps == timesteps) + + +@pytest.mark.parametrize("units, unit_conv", [ + ('g/s', 1), + ('g/sec', 1), + ('g/min', _SECONDS_PER_MINUTE), + ('g/minute', _SECONDS_PER_MINUTE), + ('g/h', _SECONDS_PER_HOUR), + ('g/hr', _SECONDS_PER_HOUR), + ('g/hour', _SECONDS_PER_HOUR), + ('g/d', _SECONDS_PER_DAY), + ('g/day', _SECONDS_PER_DAY), + ('g/a', _SECONDS_PER_JULIAN_YEAR), + ('g/year', _SECONDS_PER_JULIAN_YEAR), + ]) +def test_units(units, unit_conv, model): + """ Units testing""" + # create external rate Xe + components = ['Xe135', 'U235'] + external_source_rate = 1.0 + number_of_timesteps = 2 + op = CoupledOperator(model, CHAIN_PATH) + transfer = ExternalSourceRates(op, model.materials, number_of_timesteps) + timesteps = np.arange(number_of_timesteps) + + for component in components: + rate = external_source_rate * unit_conv * atomic_mass(component) / AVOGADRO + transfer.set_external_source_rate('f', {component: 1}, rate, rate_units=units) + assert transfer.get_external_rate( + 'f', component, timesteps)[0] == pytest.approx(external_source_rate) + + +def test_external_source(run_in_tmpdir, model): + """Tests external source depletion class without neither reaction rates nor + decay but only external source rates""" + # create transfer rate for U + vector = {'U235': 1} + external_source = 10 # grams + op = CoupledOperator(model, CHAIN_PATH) + integrator = openmc.deplete.PredictorIntegrator( + op, [1, 1], 0.0, timestep_units = 'd') + integrator.add_external_source_rate('f', vector, external_source/(24*3600)) + integrator.integrate() + + # Get number of U238 atoms from results + results = openmc.deplete.Results('depletion_results.h5') + _, atoms = results.get_atoms(model.materials[0], "U235") + + # Ensure number of atoms equal external source + assert atoms[1] - atoms[0] == pytest.approx( + external_source * AVOGADRO / atomic_mass('U235')) + assert atoms[2] - atoms[1] == pytest.approx( + external_source * AVOGADRO / atomic_mass('U235')) diff --git a/tests/unit_tests/test_deplete_transfer_rates.py b/tests/unit_tests/test_deplete_transfer_rates.py index 140777cd6..a3228e9fb 100644 --- a/tests/unit_tests/test_deplete_transfer_rates.py +++ b/tests/unit_tests/test_deplete_transfer_rates.py @@ -16,6 +16,7 @@ CHAIN_PATH = Path(__file__).parents[1] / "chain_simple.xml" @pytest.fixture def model(): + openmc.reset_auto_ids() f = openmc.Material(name="f") f.add_element("U", 1, percent_type="ao", enrichment=4.25) f.add_element("O", 2) @@ -54,32 +55,35 @@ def model(): return openmc.Model(geometry, materials, settings) -@pytest.mark.parametrize("case_name, transfer_rates", [ - ('elements', {'U': 0.01, 'Xe': 0.1}), - ('nuclides', {'I135': 0.01, 'Gd156': 0.1, 'Gd157': 0.01}), +@pytest.mark.parametrize("case_name, transfer_rates, timesteps", [ + ('elements', {'U': 0.01, 'Xe': 0.1}, None), + ('nuclides', {'I135': 0.01, 'Gd156': 0.1, 'Gd157': 0.01}, None), ('nuclides_elements', {'I135': 0.01, 'Gd156': 0.1, 'Gd157': 0.01, 'U': 0.01, - 'Xe': 0.1}), + 'Xe': 0.1}, None), ('elements_nuclides', {'U': 0.01, 'Xe': 0.1, 'I135': 0.01, 'Gd156': 0.1, - 'Gd157': 0.01}), + 'Gd157': 0.01}, None), ('multiple_transfer', {'U': 0.01, 'Xe': 0.1, 'I135': 0.01, 'Gd156': 0.1, - 'Gd157': 0.01}), - ('rates_invalid_1', {'Gd': 0.01, 'Gd157': 0.01, 'Gd156': 0.01}), - ('rates_invalid_2', {'Gd156': 0.01, 'Gd157': 0.01, 'Gd': 0.01}), - ('rates_invalid_3', {'Gb156': 0.01}), - ('rates_invalid_4', {'Gb': 0.01}) + 'Gd157': 0.01}, None), + ('timesteps', {'U': 0.01, 'Xe': 0.1}, [1]), + ('rates_invalid_1', {'Gd': 0.01, 'Gd157': 0.01, 'Gd156': 0.01}, None), + ('rates_invalid_2', {'Gd156': 0.01, 'Gd157': 0.01, 'Gd': 0.01}, None), + ('rates_invalid_3', {'Gb156': 0.01}, None), + ('rates_invalid_4', {'Gb': 0.01}, None) ]) -def test_get_set(model, case_name, transfer_rates): +def test_get_set(model, case_name, transfer_rates, timesteps): """Tests the get/set methods""" - - openmc.reset_auto_ids() op = CoupledOperator(model, CHAIN_PATH) - transfer = TransferRates(op, model) + number_of_timesteps = 2 + transfer = TransferRates(op, model.materials, number_of_timesteps) + + if timesteps is None: + timesteps = np.arange(number_of_timesteps) # Test by Openmc material, material name and material id material, dest_material, dest_material2 = [m for m in model.materials if m.depletable] for material_input in [material, material.name, material.id]: - for dest_material_input in [dest_material, dest_material.name, + for dest_material_input in [None, dest_material, dest_material.name, dest_material.id]: if case_name == 'rates_invalid_1': with pytest.raises(ValueError, match='Cannot add transfer ' @@ -117,14 +121,21 @@ def test_get_set(model, case_name, transfer_rates): for component, transfer_rate in transfer_rates.items(): transfer.set_transfer_rate(material_input, [component], transfer_rate, + timesteps=timesteps, destination_material=\ dest_material_input) - assert transfer.get_transfer_rate( - material_input, component)[0] == transfer_rate - assert transfer.get_destination_material( - material_input, component)[0] == str(dest_material.id) - assert transfer.get_components(material_input) == \ - transfer_rates.keys() + assert transfer.get_external_rate( + material_input, component, timesteps, + dest_material_input)[0] == transfer_rate + assert np.all(transfer.external_timesteps == timesteps) + + if timesteps is not None: + for timestep in timesteps: + assert transfer.get_components(material_input, timestep, + dest_material_input) == list(transfer_rates.keys()) + else: + assert transfer.get_components(material_input, timesteps, + dest_material_input) == list(transfer_rates.keys()) if case_name == 'multiple_transfer': for dest2_material_input in [dest_material2, dest_material2.name, @@ -135,10 +146,8 @@ def test_get_set(model, case_name, transfer_rates): destination_material=\ dest2_material_input) for id, dest_mat in zip([0,1],[dest_material,dest_material2]): - assert transfer.get_transfer_rate( - material_input, component)[id] == transfer_rate - assert transfer.get_destination_material( - material_input, component)[id] == str(dest_mat.id) + assert transfer.get_external_rate( + material_input, component, timesteps)[0] == transfer_rate @pytest.mark.parametrize("transfer_rate_units, unit_conv", [ ('1/s', 1), @@ -158,13 +167,15 @@ def test_units(transfer_rate_units, unit_conv, model): # create transfer rate Xe components = ['Xe', 'U235'] transfer_rate = 1e-5 + number_of_timesteps = 2 op = CoupledOperator(model, CHAIN_PATH) - transfer = TransferRates(op, model) + transfer = TransferRates(op, model.materials, number_of_timesteps) for component in components: transfer.set_transfer_rate('f', [component], transfer_rate * unit_conv, transfer_rate_units=transfer_rate_units) - assert transfer.get_transfer_rate('f', component)[0] == transfer_rate + for timestep in range(transfer.number_of_timesteps): + assert transfer.get_external_rate('f', component, timestep)[0] == transfer_rate def test_transfer(run_in_tmpdir, model): From 757617be50acb2a92120fb06991b5491b79bf4d4 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Fri, 16 May 2025 16:55:41 +0300 Subject: [PATCH 326/671] added test for dagmc geometry plot (#3375) Co-authored-by: Paul Romano --- tests/unit_tests/dagmc/test_model.py | 1 + tests/unit_tests/dagmc/test_plot.py | 32 ++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 tests/unit_tests/dagmc/test_plot.py diff --git a/tests/unit_tests/dagmc/test_model.py b/tests/unit_tests/dagmc/test_model.py index 9fdfbcebc..0de4f6092 100644 --- a/tests/unit_tests/dagmc/test_model.py +++ b/tests/unit_tests/dagmc/test_model.py @@ -4,6 +4,7 @@ import lxml.etree as ET import numpy as np import pytest import openmc +import openmc.lib from openmc.utility_funcs import change_directory pytestmark = pytest.mark.skipif( diff --git a/tests/unit_tests/dagmc/test_plot.py b/tests/unit_tests/dagmc/test_plot.py new file mode 100644 index 000000000..83f6df767 --- /dev/null +++ b/tests/unit_tests/dagmc/test_plot.py @@ -0,0 +1,32 @@ +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_plotting_dagmc_geometry(request): + """Test plotting a DAGMC geometry with OpenMC. This is different to CSG + geometry plotting as the path to the DAGMC file needs handling.""" + + dag_universe = openmc.DAGMCUniverse(request.path.parent / 'dagmc.h5m') + csg_with_dag_inside = dag_universe.bounded_universe() + model = openmc.Model() + model.geometry = openmc.Geometry(csg_with_dag_inside) + + for mat_name in dag_universe.material_names: + material = openmc.Material(name=mat_name) + material.add_nuclide("Fe56", 1.0) + material.set_density("g/cm3", 7.0) + model.materials.append(material) + + # putting the source at the center of the bounding box of the DAGMC + model.settings.source = openmc.IndependentSource( + space=openmc.stats.Point(dag_universe.bounding_box.center) + ) + model.settings.batches = 10 + model.settings.particles = 50 + + model.plot() From a7d1ceba3f9fe5e394a49cdf10b724094f39af11 Mon Sep 17 00:00:00 2001 From: Alberto P <4104972+alberto743@users.noreply.github.com> Date: Sun, 18 May 2025 19:28:00 +0200 Subject: [PATCH 327/671] small typo - spelling of Debian (#3411) --- docs/source/usersguide/install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/usersguide/install.rst b/docs/source/usersguide/install.rst index d294770e0..7a8cb8020 100644 --- a/docs/source/usersguide/install.rst +++ b/docs/source/usersguide/install.rst @@ -221,7 +221,7 @@ Prerequisites OpenMC's built-in plotting capabilities use the libpng library to produce compressed PNG files. In the absence of this library, OpenMC will fallback to writing PPM files, which are uncompressed and only supported by select - image viewers. libpng can be installed on Ddebian derivates with:: + image viewers. libpng can be installed on Debian derivates with:: sudo apt install libpng-dev From cb95c784b755719d9e5897bb550611eb0a8e542a Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Tue, 27 May 2025 19:04:16 +0300 Subject: [PATCH 328/671] Fix bug where the same mesh is written multiple times to settings.xml (#3418) --- openmc/settings.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openmc/settings.py b/openmc/settings.py index 6fb36f3fc..4cacefbed 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -1626,9 +1626,12 @@ class Settings: if mesh_memo is not None and wwg.mesh.id in mesh_memo: continue - root.append(wwg.mesh.to_xml_element()) - if mesh_memo is not None: - mesh_memo.add(wwg.mesh.id) + # See if a element already exists -- if not, add it + path = f"./mesh[@id='{wwg.mesh.id}']" + if root.find(path) is None: + root.append(wwg.mesh.to_xml_element()) + if mesh_memo is not None: + mesh_memo.add(wwg.mesh.id) def _create_weight_windows_file_element(self, root): if self.weight_windows_file is not None: From ace73ab5de619d33241fcc778551c843b682ced1 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Tue, 3 Jun 2025 21:02:20 +0300 Subject: [PATCH 329/671] Fixed a bug in charged particle energy deposition. (#3416) Co-authored-by: Jonathan Shimwell Co-authored-by: Paul Romano --- docs/source/methods/energy_deposition.rst | 79 ++++++++++++++++--- include/openmc/material.h | 5 ++ src/material.cpp | 11 ++- src/tallies/tally_scoring.cpp | 67 +++++++++------- .../photon_production/results_true.dat | 8 +- tests/unit_tests/test_data_multipole.py | 4 +- tests/unit_tests/test_nuclide_heating.py | 39 +++++++++ 7 files changed, 166 insertions(+), 47 deletions(-) create mode 100644 tests/unit_tests/test_nuclide_heating.py diff --git a/docs/source/methods/energy_deposition.rst b/docs/source/methods/energy_deposition.rst index 4675aee3f..0babd7124 100644 --- a/docs/source/methods/energy_deposition.rst +++ b/docs/source/methods/energy_deposition.rst @@ -25,19 +25,36 @@ KERMA (Kinetic Energy Release in Materials) [Mack97]_ coefficients for reaction :math:`\times` cross-section (e.g., eV-barn) and can be used much like a reaction cross section for the purpose of tallying energy deposition. -KERMA coefficients can be computed using the energy-balance method with -a nuclear data processing code like NJOY, which performs the following -iteration over all reactions :math:`r` for all isotopes :math:`i` -requested +KERMA coefficients can be computed using the energy-balance method with a +nuclear data processing code like NJOY, which estimates the KERMA coefficients +using the following equation: .. math:: - k_{i, r}(E) = \left(E + Q_{i, r} - \bar{E}_{i, r, n} + k_{i, r}(E) = \left(E + Q_{i, r} - \sum\limits_x \bar{E}_{i, r, x} + \right)\sigma_{i, r}(E), + +where the summation is over each secondary particle type :math:`x`. This +equation states that the energy deposited is equal to the energy of the incident +particle plus the reaction :math:`Q` value less the energy of secondary +particles that are transported away from the reaction site. For neutron +interactions, the energy-balance KERMA coefficient is + +.. math:: + + k_{i, r}(E) = \left(E + Q_{i, r} - \sum\limits_x \bar{E}_{i, r, n} - \bar{E}_{i, r, \gamma}\right)\sigma_{i, r}(E), -removing the energy of neutral particles (neutrons and photons) that are -transported away from the reaction site :math:`\bar{E}`, and the reaction -:math:`Q` value. +where :math:`\bar{E}_{i, r, n}` is the average energy of secondary neutrons and +:math:`\bar{E}_{i, r, \gamma}` is the average energy of secondary photons. For +photon and charged particle interactions, the :math:`Q` value is zero and thus +the KERMA coefficient is + +.. math:: + :label: energy-balance-photon + + k_{i, r}(E) = \left(E - \sum\limits_x \bar{E}_{i, r, x} + \right)\sigma_{i, r}(E). ------- Fission @@ -120,7 +137,7 @@ run with :math:`N918` reflecting fission heating computed from NJOY. This modified heating data is stored as the MT=901 reaction and will be scored if ``heating-local`` is included in :attr:`openmc.Tally.scores`. -Coupled neutron-photon transport +Coupled Neutron-Photon Transport -------------------------------- Here, OpenMC instructs ``heatr`` to assume that energy from photons is not @@ -138,6 +155,50 @@ Let :math:`N301` represent the total heating number returned from this This modified heating data is stored as the MT=301 reaction and will be scored if ``heating`` is included in :attr:`openmc.Tally.scores`. +Photons and Charged Particles +----------------------------- + +In OpenMC, energy deposition from photons or charged particles is scored using +the energy balance method based on Equation :eq:`energy-balance-photon`. Special +consideration is given to electrons and positrons as described below. + ++++++++++++++++++ +Charged Particles ++++++++++++++++++ + +OpenMC tracks photons interaction by interaction so the energy deposited in each +collision is easily attributed back to the nuclide and reaction for which the +photon interacted with. Charged particles (electrons and photons) aren't tracked +in the same way. For charged particles, OpenMC assumes that all their energy +(less the energy of bremsstrahlung radiation) is deposited in the material in +which they were born. In this way it is harder to trace how much energy should +be attributed in each nuclide. + +According to the CSDA approximation (see :ref:`ttb`) the energy deposited by a +charged particle with kinetic energy :math:`T` in the :math:`i`-th element can +be calculated as: + +.. math:: + + E_{i} = \int_{0}^{R(T)} w_{i}S_{\text{col,i}} dx + +where :math:`R(T)` is the CSDA range of the charged particle, +:math:`S_{\text{col},i}` is the collision stopping power of the charged particle +in the :math:`i`-th element and :math:`w_i` is the mass fraction of the +:math:`i`-th element. According to the Bethe formula the collision stopping +power of the :math:`i`-th element is proportional to :math:`Z_i/A_i`, so the +fractional collision stopping power from the :math:`i`-th element is: + +.. math:: + + \frac{w_{i}S_{\text{col},i}(T)}{S_{\text{col}}(T)} = + \frac{\frac{w_{i}Z_{i}}{A_{i}}}{\sum_{i}\frac{w_{i}Z_{i}}{A_{i}}} = + \frac{\gamma_i Z_{i}}{\sum_{i}\gamma_i Z_{i}}. + +where :math:`\gamma_i` is the atomic fraction of the :math:`i`-th element. +Therefore, the energy deposited by charged particles should be attributed to +a given element according to its fractional charge density. + ---------- References ---------- diff --git a/include/openmc/material.h b/include/openmc/material.h index 31fab2ae2..fe587a86f 100644 --- a/include/openmc/material.h +++ b/include/openmc/material.h @@ -107,6 +107,10 @@ public: //! \return Density in [g/cm^3] double density_gpcc() const { return density_gpcc_; } + //! Get charge density in [e/b-cm] + //! \return Charge density in [e/b-cm] + double charge_density() const { return charge_density_; }; + //! Get name //! \return Material name const std::string& name() const { return name_; } @@ -177,6 +181,7 @@ public: xt::xtensor 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] double volume_ {-1.0}; //!< Volume in [cm^3] vector p0_; //!< Indicate which nuclides are to be treated with //!< iso-in-lab scattering diff --git a/src/material.cpp b/src/material.cpp index b8846d7cb..32384ddf1 100644 --- a/src/material.cpp +++ b/src/material.cpp @@ -454,12 +454,15 @@ void Material::normalize_density() // Calculate nuclide atom densities atom_density_ *= density_; - // Calculate density in g/cm^3. + // Calculate density in [g/cm^3] and charge density in [e/b-cm] density_gpcc_ = 0.0; + charge_density_ = 0.0; for (int i = 0; i < nuclide_.size(); ++i) { int i_nuc = nuclide_[i]; double awr = settings::run_CE ? data::nuclides[i_nuc]->awr_ : 1.0; + int z = settings::run_CE ? data::nuclides[i_nuc]->Z_ : 0.0; density_gpcc_ += atom_density_(i) * awr * MASS_NEUTRON / N_AVOGADRO; + charge_density_ += atom_density_(i) * z; } } @@ -982,12 +985,15 @@ void Material::set_density(double density, const std::string& units) // Recalculate nuclide atom densities based on given density atom_density_ *= density; - // Calculate density in g/cm^3. + // Calculate density in g/cm^3 and charge density in [e/b-cm] density_gpcc_ = 0.0; + charge_density_ = 0.0; for (int i = 0; i < nuclide_.size(); ++i) { int i_nuc = nuclide_[i]; double awr = data::nuclides[i_nuc]->awr_; + int z = settings::run_CE ? data::nuclides[i_nuc]->Z_ : 0.0; density_gpcc_ += atom_density_(i) * awr * MASS_NEUTRON / N_AVOGADRO; + charge_density_ += atom_density_(i) * z; } } else if (units == "g/cm3" || units == "g/cc") { // Determine factor by which to change densities @@ -998,6 +1004,7 @@ void Material::set_density(double density, const std::string& units) density_gpcc_ = density; density_ *= f; atom_density_ *= f; + charge_density_ *= f; } else { throw std::invalid_argument { "Invalid units '" + std::string(units.data()) + "' specified."}; diff --git a/src/tallies/tally_scoring.cpp b/src/tallies/tally_scoring.cpp index 5c6386fb4..62b002907 100644 --- a/src/tallies/tally_scoring.cpp +++ b/src/tallies/tally_scoring.cpp @@ -325,6 +325,39 @@ double score_neutron_heating(const Particle& p, const Tally& tally, double flux, return score; } +//! Helper function to obtain particle heating [eV] + +double score_particle_heating(const Particle& p, const Tally& tally, + double flux, int rxn_bin, int i_nuclide, double atom_density) +{ + if (p.type() == ParticleType::neutron) + return score_neutron_heating( + p, tally, flux, rxn_bin, i_nuclide, atom_density); + if (i_nuclide == -1 || i_nuclide == p.event_nuclide() || + p.event_nuclide() == -1) { + // Get the pre-collision energy of the particle. + auto E = p.E_last(); + // The energy deposited is the difference between the pre-collision + // and post-collision energy... + double score = E - p.E(); + // ...less the energy of any secondary particles since they will be + // transported individually later + score -= p.bank_second_E(); + score *= p.wgt_last(); + + // if no event_nuclide (charged particle) scale energy deposition by + // fractional charge density + if (i_nuclide != -1 && p.event_nuclide() == -1) { + const auto& mat {model::materials[p.material()]}; + int z = data::nuclides[i_nuclide]->Z_; + auto i = mat->mat_nuclide_index_[i_nuclide]; + score *= (z * mat->atom_density_[i] / mat->charge_density()); + } + return score; + } + return 0.0; +} + //! Helper function for nu-fission tallies with energyout filters. // //! In this case, we may need to score to multiple bins if there were multiple @@ -1007,23 +1040,8 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, break; case HEATING: - if (p.type() == Type::neutron) { - score = score_neutron_heating( - p, tally, flux, HEATING, i_nuclide, atom_density); - } else { - if (i_nuclide == -1 || i_nuclide == p.event_nuclide()) { - // The energy deposited is the difference between the pre-collision - // and post-collision energy... - score = E - p.E(); - // ...less the energy of any secondary particles since they will be - // transported individually later - score -= p.bank_second_E(); - - score *= p.wgt_last(); - } else { - score = 0.0; - } - } + score = score_particle_heating( + p, tally, flux, HEATING, i_nuclide, atom_density); break; default: @@ -1539,19 +1557,8 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index, break; case HEATING: - if (p.type() == Type::neutron) { - score = score_neutron_heating( - p, tally, flux, HEATING, i_nuclide, atom_density); - } else { - // The energy deposited is the difference between the pre-collision and - // post-collision energy... - score = E - p.E(); - // ...less the energy of any secondary particles since they will be - // transported individually later - score -= p.bank_second_E(); - - score *= p.wgt_last(); - } + score = score_particle_heating( + p, tally, flux, HEATING, i_nuclide, atom_density); break; default: diff --git a/tests/regression_tests/photon_production/results_true.dat b/tests/regression_tests/photon_production/results_true.dat index 874e32b85..c16524689 100644 --- a/tests/regression_tests/photon_production/results_true.dat +++ b/tests/regression_tests/photon_production/results_true.dat @@ -67,8 +67,8 @@ tally 3: 0.000000E+00 0.000000E+00 0.000000E+00 -0.000000E+00 -0.000000E+00 +1.774484E+05 +3.148794E+10 0.000000E+00 0.000000E+00 0.000000E+00 @@ -79,8 +79,8 @@ tally 3: 0.000000E+00 0.000000E+00 0.000000E+00 -0.000000E+00 -0.000000E+00 +7.692488E+03 +5.917437E+07 0.000000E+00 0.000000E+00 0.000000E+00 diff --git a/tests/unit_tests/test_data_multipole.py b/tests/unit_tests/test_data_multipole.py index 4c2ba96d6..bc7136a97 100644 --- a/tests/unit_tests/test_data_multipole.py +++ b/tests/unit_tests/test_data_multipole.py @@ -52,7 +52,7 @@ def test_from_endf(): pytest.importorskip('vectfit') endf_data = os.environ['OPENMC_ENDF_DATA'] endf_file = os.path.join(endf_data, 'neutrons', 'n-001_H_001.endf') - return openmc.data.WindowedMultipole.from_endf( + assert openmc.data.WindowedMultipole.from_endf( endf_file, log=True, wmp_options={"n_win": 400, "n_cf": 3}) @@ -60,5 +60,5 @@ def test_from_endf_search(): pytest.importorskip('vectfit') endf_data = os.environ['OPENMC_ENDF_DATA'] endf_file = os.path.join(endf_data, 'neutrons', 'n-095_Am_244.endf') - return openmc.data.WindowedMultipole.from_endf( + assert openmc.data.WindowedMultipole.from_endf( endf_file, log=True, wmp_options={"search": True, 'rtol':1e-2}) diff --git a/tests/unit_tests/test_nuclide_heating.py b/tests/unit_tests/test_nuclide_heating.py new file mode 100644 index 000000000..e00d57b4f --- /dev/null +++ b/tests/unit_tests/test_nuclide_heating.py @@ -0,0 +1,39 @@ +import openmc +from pytest import approx + + +def test_nuclide_heating(run_in_tmpdir): + mat = openmc.Material() + mat.add_nuclide("Li6", 0.5) + mat.add_nuclide("Li7", 0.5) + mat.set_density("g/cm3", 1.0) + + sphere = openmc.Sphere(r=20, boundary_type="reflective") + inside_sphere = openmc.Cell(fill=mat, region=-sphere) + model = openmc.Model() + model.geometry = openmc.Geometry([inside_sphere]) + + model.settings.particles = 1000 + model.settings.batches = 1 + model.settings.photon_transport = True + model.settings.electron_treatment = "ttb" + model.settings.cutoff = {"energy_photon": 1000} + model.settings.run_mode = "fixed source" + model.settings.source = openmc.IndependentSource( + energy=openmc.stats.delta_function(10.0e6), + particle="photon" + ) + + # Create two tallies, one with heating by nuclide and one with total heating + tally1 = openmc.Tally() + tally1.scores = ["heating"] + tally1.nuclides = mat.get_nuclides() + tally2 = openmc.Tally() + tally2.scores = ["heating"] + model.tallies = [tally1, tally2] + + # Run the model + model.run(apply_tally_results=True) + + # Make sure the heating results are consistent + assert tally1.mean.sum() == approx(tally2.mean.sum()) From e14bb884eb97f80fecaa3c509dce10964c10247a Mon Sep 17 00:00:00 2001 From: Lewis Gross <43077972+lewisgross1296@users.noreply.github.com> Date: Wed, 4 Jun 2025 18:40:55 -0500 Subject: [PATCH 330/671] Update _get_start_data to always grab the beginning of timestep time (#3414) Co-authored-by: Paul Wilson Co-authored-by: Paul Romano --- openmc/deplete/abc.py | 29 +++++++++++++++-- tests/unit_tests/test_deplete_continue.py | 38 +++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index c304b8dc6..4b43dd221 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -818,10 +818,35 @@ class Integrator(ABC): rates *= source_rate / res.source_rate return bos_conc, OperatorResult(k, rates) - def _get_start_data(self): + def _get_start_data(self) -> tuple[float, int]: + """ + This function fetches the starting state of a depletion simulation in + terms of the simulation physical time at which to start and the index at + which the depletion simulation should start. When no previous results + exist, the time and index are both zero. When previous results do exist, + it returns the time corresponding to beginning the previous results last + timestep and the index as N-1 where N is the number of previous + StepResults found in the previous Results (as expected from 0-based + indexing). + + Note that the openmc.deplete.Results.time object is a list of float with + [t,t+dt] where t is the beginning of timestep time and t+dt is the end + of timestep time. If the previous results correspond to a simulation + that finished to completeion, it will contain a results in the form of + [t,t], but if a simulation doesn't finish all the given timesteps, it is + the t that is the desired start time, not t+dt. Thus, it is always safe + to take time[0]. + + Returns + ------- + start_time : float + Time at which depletion simulation should start in [s] + index : int + Index at which depletion simulation should start + """ if self.operator.prev_res is None: return 0.0, 0 - return (self.operator.prev_res[-1].time[-1], + return (self.operator.prev_res[-1].time[0], len(self.operator.prev_res) - 1) def integrate( diff --git a/tests/unit_tests/test_deplete_continue.py b/tests/unit_tests/test_deplete_continue.py index 53c4c56d2..637c9d5e4 100644 --- a/tests/unit_tests/test_deplete_continue.py +++ b/tests/unit_tests/test_deplete_continue.py @@ -4,6 +4,7 @@ These tests run in two steps: first a normal run and then a continue run using t """ import pytest +import numpy as np import openmc.deplete from tests import dummy_operator @@ -26,6 +27,43 @@ def test_continue(run_in_tmpdir): bundle.solver(operator, [1.0, 2.0, 3.0, 4.0], [1.0, 2.0, 3.0, 4.0], continue_timesteps=True).integrate() + final_res = openmc.deplete.Results(operator.output_dir / "depletion_results.h5") + + assert np.array_equal( + np.diff(final_res.get_times(time_units="s")), + [1.0, 2.0, 3.0, 4.0] + ) + + +def test_continue_continue(run_in_tmpdir): + """Test to ensure that a continue run can be continued""" + # set up the problem + bundle = dummy_operator.SCHEMES['predictor'] + operator = dummy_operator.DummyOperator() + + # initial depletion + bundle.solver(operator, [1.0, 2.0], [1.0, 2.0]).integrate() + + # set up continue run + prev_res = openmc.deplete.Results(operator.output_dir / "depletion_results.h5") + operator = dummy_operator.DummyOperator(prev_res) + + # first continue run + bundle.solver(operator, [1.0, 2.0, 3.0, 4.0], [1.0, 2.0, 3.0, 4.0], + continue_timesteps=True).integrate() + + prev_res = openmc.deplete.Results(operator.output_dir / "depletion_results.h5") + # second continue run + bundle.solver(operator, [1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [1.0, 2.0, 3.0, 4.0, 5.0, 6.0], + continue_timesteps=True).integrate() + + final_res = openmc.deplete.Results(operator.output_dir / "depletion_results.h5") + + assert np.array_equal( + np.diff(final_res.get_times(time_units="s")), + [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] + ) + def test_mismatched_initial_times(run_in_tmpdir): """Test to ensure that a continue run with different initial steps is properly caught""" From dadc4fe4184e66848a60a052c00d0b4cfeebb247 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Thu, 5 Jun 2025 03:07:59 +0300 Subject: [PATCH 331/671] Fix no serialization of periodic_surface_id bug (#3421) Co-authored-by: Paul Romano --- include/openmc/boundary_condition.h | 4 +++ openmc/summary.py | 12 +++++++++ src/surface.cpp | 13 +++++++++ tests/unit_tests/test_summary.py | 41 +++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+) create mode 100644 tests/unit_tests/test_summary.py diff --git a/include/openmc/boundary_condition.h b/include/openmc/boundary_condition.h index cd8988162..af40131f1 100644 --- a/include/openmc/boundary_condition.h +++ b/include/openmc/boundary_condition.h @@ -111,6 +111,10 @@ public: std::string type() const override { return "periodic"; } + int i_surf() const { return i_surf_; } + + int j_surf() const { return j_surf_; } + protected: int i_surf_; int j_surf_; diff --git a/openmc/summary.py b/openmc/summary.py index 6423f8ce1..ca5cfadd7 100644 --- a/openmc/summary.py +++ b/openmc/summary.py @@ -127,11 +127,23 @@ class Summary: self._fast_materials[material.id] = material def _read_surfaces(self): + periodic_surface_ids = set() for group in self._f['geometry/surfaces'].values(): surface = openmc.Surface.from_hdf5(group) # surface may be None for DAGMC surfaces if surface: self._fast_surfaces[surface.id] = surface + if surface.boundary_type == "periodic": + periodic_surface_ids.add(surface.id) + + # Assign periodic surfaces when information is in file + for surface_id in periodic_surface_ids: + group = self._f[f'geometry/surfaces/surface {surface_id}'] + surface = self._fast_surfaces[surface_id] + if 'periodic_surface_id' in group: + periodic_surface_id = int(group['periodic_surface_id'][()]) + surface.periodic_surface = self._fast_surfaces[periodic_surface_id] + def _read_cells(self): diff --git a/src/surface.cpp b/src/surface.cpp index 0002275c3..ea19514a3 100644 --- a/src/surface.cpp +++ b/src/surface.cpp @@ -172,6 +172,19 @@ void Surface::to_hdf5(hid_t group_id) const if (bc_) { write_string(surf_group, "boundary_type", bc_->type(), false); bc_->to_hdf5(surf_group); + + // write periodic surface ID + if (bc_->type() == "periodic") { + auto pbc = dynamic_cast(bc_.get()); + Surface& surf1 {*model::surfaces[pbc->i_surf()]}; + Surface& surf2 {*model::surfaces[pbc->j_surf()]}; + + if (id_ == surf1.id_) { + write_dataset(surf_group, "periodic_surface_id", surf2.id_); + } else { + write_dataset(surf_group, "periodic_surface_id", surf1.id_); + } + } } else { write_string(surf_group, "boundary_type", "transmission", false); } diff --git a/tests/unit_tests/test_summary.py b/tests/unit_tests/test_summary.py new file mode 100644 index 000000000..315ac0ba5 --- /dev/null +++ b/tests/unit_tests/test_summary.py @@ -0,0 +1,41 @@ +import openmc + + +def test_periodic_surface_roundtrip(run_in_tmpdir): + # Create a simple model with periodic surfaces + mat = openmc.Material() + mat.add_nuclide('H1', 1.0) + mat.set_density('g/cm3', 1.0) + cyl = openmc.ZCylinder(r=1.0) + x0 = openmc.XPlane(-5.0, boundary_type='periodic') + y0 = openmc.YPlane(-5.0, boundary_type='periodic') + z0 = openmc.ZPlane(-5.0, boundary_type='periodic') + x1 = openmc.XPlane(5.0, boundary_type='periodic') + y1 = openmc.YPlane(5.0, boundary_type='periodic') + z1 = openmc.ZPlane(5.0, boundary_type='periodic') + x0.periodic_surface = x1 + y0.periodic_surface = y1 + z0.periodic_surface = z1 + cell1 = openmc.Cell(fill=mat, region=-cyl) + cell2 = openmc.Cell(fill=mat, region=+cyl & +x0 & -x1 & +y0 & -y1 & +z0 & -z1) + model = openmc.Model() + model.geometry = openmc.Geometry([cell1, cell2]) + model.settings.particles = 100 + model.settings.batches = 1 + model.settings.run_mode = 'fixed source' + model.settings.source = openmc.IndependentSource( + energy=openmc.stats.delta_function(1.0e4) + ) + + # Run model + model.run() + + # Load summary data and check periodic surfaces + summary = openmc.Summary('summary.h5') + surfs = summary.geometry.get_all_surfaces() + for s in [x0, y0, z0, x1, y1, z1]: + assert surfs[s.id].boundary_type == 'periodic' + pairs = [(x0, x1), (y0, y1), (z0, z1)] + for s0, s1 in pairs: + assert surfs[s0.id].periodic_surface == surfs[s1.id] + assert surfs[s1.id].periodic_surface == surfs[s0.id] From 4943fa363074e511cd0fa797a8d4c02af92c2443 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 5 Jun 2025 10:45:49 -0500 Subject: [PATCH 332/671] Avoid negative heating values during pair production and bremsstrahlung (#3426) Co-authored-by: GuySten --- docs/source/methods/energy_deposition.rst | 8 +++-- include/openmc/particle_data.h | 3 +- src/bremsstrahlung.cpp | 6 ++++ src/tallies/tally_scoring.cpp | 29 ++++++++++++++---- .../photon_production/results_true.dat | 28 ++++++++--------- .../results_true.dat | 16 +++++----- tests/unit_tests/test_photon_heating.py | 30 +++++++++++++++++++ 7 files changed, 88 insertions(+), 32 deletions(-) create mode 100644 tests/unit_tests/test_photon_heating.py diff --git a/docs/source/methods/energy_deposition.rst b/docs/source/methods/energy_deposition.rst index 0babd7124..c43ee64ac 100644 --- a/docs/source/methods/energy_deposition.rst +++ b/docs/source/methods/energy_deposition.rst @@ -47,15 +47,17 @@ interactions, the energy-balance KERMA coefficient is where :math:`\bar{E}_{i, r, n}` is the average energy of secondary neutrons and :math:`\bar{E}_{i, r, \gamma}` is the average energy of secondary photons. For -photon and charged particle interactions, the :math:`Q` value is zero and thus -the KERMA coefficient is +photon and charged particle interactions the KERMA coefficient is .. math:: :label: energy-balance-photon - k_{i, r}(E) = \left(E - \sum\limits_x \bar{E}_{i, r, x} + k_{i, r}(E) = \left(E + Q_{i, r} - \sum\limits_x \bar{E}_{i, r, x} \right)\sigma_{i, r}(E). +where the :math:`Q` value is zero for all interactions except for pair +production and positron annihilation. + ------- Fission ------- diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index 1570b780b..a07a99ffb 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -573,7 +573,8 @@ public: bool& fission() { return fission_; } // true if implicit fission int& event_nuclide() { return event_nuclide_; } // index of collision nuclide const int& event_nuclide() const { return event_nuclide_; } - int& event_mt() { return event_mt_; } // MT number of collision + int& event_mt() { return event_mt_; } // MT number of collision + const int& event_mt() const { return event_mt_; } int& delayed_group() { return delayed_group_; } // delayed group const int& parent_nuclide() const { return parent_nuclide_; } int& parent_nuclide() { return parent_nuclide_; } // Parent nuclide diff --git a/src/bremsstrahlung.cpp b/src/bremsstrahlung.cpp index d22d6392a..a2320e0b4 100644 --- a/src/bremsstrahlung.cpp +++ b/src/bremsstrahlung.cpp @@ -112,6 +112,12 @@ void thick_target_bremsstrahlung(Particle& p, double* E_lost) std::pow(a * (c - c_l) / (std::exp(w_l) * p_l) + 1.0, 1.0 / a); if (w > settings::energy_cutoff[photon]) { + // If the energy of the secondary photon is larger than the remaining + // energy of the primary particle, adjust it to the remaining energy + if (*E_lost + w > p.E()) { + w = p.E() - *E_lost; + } + // Create secondary photon p.create_secondary(p.wgt(), p.u(), w, ParticleType::photon); *E_lost += w; diff --git a/src/tallies/tally_scoring.cpp b/src/tallies/tally_scoring.cpp index 62b002907..71b103cfb 100644 --- a/src/tallies/tally_scoring.cpp +++ b/src/tallies/tally_scoring.cpp @@ -325,6 +325,20 @@ double score_neutron_heating(const Particle& p, const Tally& tally, double flux, return score; } +//! Helper function to obtain reaction Q value for photons and charged particles +double get_reaction_q_value(const Particle& p) +{ + if (p.type() == ParticleType::photon && p.event_mt() == PAIR_PROD) { + // pair production + return -2 * MASS_ELECTRON_EV; + } else if (p.type() == ParticleType::positron) { + // positron annihilation + return 2 * MASS_ELECTRON_EV; + } else { + return 0.0; + } +} + //! Helper function to obtain particle heating [eV] double score_particle_heating(const Particle& p, const Tally& tally, @@ -335,14 +349,17 @@ double score_particle_heating(const Particle& p, const Tally& tally, p, tally, flux, rxn_bin, i_nuclide, atom_density); if (i_nuclide == -1 || i_nuclide == p.event_nuclide() || p.event_nuclide() == -1) { + // For pair production and positron annihilation, we need to account for the + // reaction Q value + double Q = get_reaction_q_value(p); + // Get the pre-collision energy of the particle. auto E = p.E_last(); - // The energy deposited is the difference between the pre-collision - // and post-collision energy... - double score = E - p.E(); - // ...less the energy of any secondary particles since they will be - // transported individually later - score -= p.bank_second_E(); + + // The energy deposited is the sum of the incident energy and the reaction + // Q-value less the energy of any outgoing particles + double score = E + Q - p.E() - p.bank_second_E(); + score *= p.wgt_last(); // if no event_nuclide (charged particle) scale energy deposition by diff --git a/tests/regression_tests/photon_production/results_true.dat b/tests/regression_tests/photon_production/results_true.dat index c16524689..413f6f0ca 100644 --- a/tests/regression_tests/photon_production/results_true.dat +++ b/tests/regression_tests/photon_production/results_true.dat @@ -55,14 +55,14 @@ tally 3: 1.846062E-07 2.297000E-01 5.276209E-02 -7.055982E+03 -4.978688E+07 +4.196651E+00 +1.761188E+01 0.000000E+00 0.000000E+00 2.297000E-01 5.276209E-02 -7.055982E+03 -4.978688E+07 +4.196651E+00 +1.761188E+01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -79,14 +79,14 @@ tally 3: 0.000000E+00 0.000000E+00 0.000000E+00 -7.692488E+03 -5.917437E+07 +1.474427E+04 +2.173936E+08 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -7.692488E+03 -5.917437E+07 +1.474427E+04 +2.173936E+08 0.000000E+00 0.000000E+00 tally 4: @@ -104,14 +104,14 @@ tally 4: 0.000000E+00 2.297000E-01 5.276209E-02 -7.055982E+03 -4.978688E+07 +4.196651E+00 +1.761188E+01 0.000000E+00 0.000000E+00 2.297000E-01 5.276209E-02 -7.055982E+03 -4.978688E+07 +4.196651E+00 +1.761188E+01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -134,7 +134,7 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -7.692488E+03 -5.917437E+07 +1.474427E+04 +2.173936E+08 0.000000E+00 0.000000E+00 diff --git a/tests/regression_tests/photon_production_fission/results_true.dat b/tests/regression_tests/photon_production_fission/results_true.dat index cdbb1255c..b17b3621a 100644 --- a/tests/regression_tests/photon_production_fission/results_true.dat +++ b/tests/regression_tests/photon_production_fission/results_true.dat @@ -32,14 +32,14 @@ tally 2: 0.000000E+00 0.000000E+00 0.000000E+00 -2.479234E+06 -2.052367E+12 +1.675918E+05 +9.365733E+09 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.479234E+06 -2.052367E+12 +1.675918E+05 +9.365733E+09 0.000000E+00 0.000000E+00 tally 3: @@ -57,13 +57,13 @@ tally 3: 0.000000E+00 0.000000E+00 0.000000E+00 -2.479234E+06 -2.052367E+12 +1.675918E+05 +9.365733E+09 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.479234E+06 -2.052367E+12 +1.675918E+05 +9.365733E+09 0.000000E+00 0.000000E+00 diff --git a/tests/unit_tests/test_photon_heating.py b/tests/unit_tests/test_photon_heating.py new file mode 100644 index 000000000..05473f5c6 --- /dev/null +++ b/tests/unit_tests/test_photon_heating.py @@ -0,0 +1,30 @@ +import openmc + + +def test_negative_positron_heating(): + m = openmc.Material() + m.add_element('Li', 1.0) + m.set_density('g/cm3', 10.0) + + surf = openmc.Sphere(r=100.0, boundary_type='reflective') + cell = openmc.Cell(fill=m, region=-surf) + model = openmc.Model() + model.geometry = openmc.Geometry([cell]) + model.settings.run_mode = 'fixed source' + model.settings.source = openmc.IndependentSource( + space=openmc.stats.Point(), + energy=openmc.stats.Discrete([5.0e6], [1.0]), + particle='photon', + ) + model.settings.particles = 7 + model.settings.batches = 1 + model.settings.electron_treatment = 'led' + model.settings.seed = 513836 + + tally = openmc.Tally() + tally.filters = [openmc.ParticleFilter(['photon', 'electron', 'positron'])] + tally.scores = ['heating'] + model.tallies = openmc.Tallies([tally]) + model.run(apply_tally_results=True) + + assert (tally.mean >= 0.0).all(), "Negative heating detected" From 67aa0def9deabae1cc012214d5b96367c5743dcf Mon Sep 17 00:00:00 2001 From: John Tramm Date: Fri, 6 Jun 2025 14:51:39 -0500 Subject: [PATCH 333/671] Random Ray External Source Plotting Fix (#3430) --- src/random_ray/flat_source_domain.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index cd073f350..367c63af4 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -919,10 +919,17 @@ void FlatSourceDomain::output_to_vtk() const std::fprintf(plot, "LOOKUP_TABLE default\n"); for (int i = 0; i < Nx * Ny * Nz; i++) { int64_t fsr = voxel_indices[i]; + int mat = source_regions_.material(fsr); float total_external = 0.0f; if (fsr >= 0) { for (int g = 0; g < negroups_; g++) { - total_external += source_regions_.external_source(fsr, g); + // External sources are already divided by sigma_t, so we need to + // multiply it back to get the true external source. + double sigma_t = 1.0; + if (mat != MATERIAL_VOID) { + sigma_t = sigma_t_[mat * negroups_ + g]; + } + total_external += source_regions_.external_source(fsr, g) * sigma_t; } } total_external = convert_to_big_endian(total_external); From 7fda3eb84695f6b147705e3b0fd4a65a49e0735b Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 6 Jun 2025 16:30:50 -0500 Subject: [PATCH 334/671] Implement a new MeshMaterialFilter (#3406) Co-authored-by: Jonathan Shimwell Co-authored-by: Patrick Shriwise --- CMakeLists.txt | 1 + docs/source/pythonapi/base.rst | 1 + include/openmc/tallies/filter.h | 1 + include/openmc/tallies/filter_mesh.h | 6 +- include/openmc/tallies/filter_meshmaterial.h | 114 +++++++++++ openmc/deplete/microxs.py | 9 +- openmc/filter.py | 190 ++++++++++++++++++- openmc/lib/filter.py | 20 +- openmc/tallies.py | 2 +- src/tallies/filter.cpp | 3 + src/tallies/filter_meshmaterial.cpp | 185 ++++++++++++++++++ tests/unit_tests/test_filter_meshmaterial.py | 65 +++++++ tests/unit_tests/test_filters.py | 33 ++++ 13 files changed, 614 insertions(+), 16 deletions(-) create mode 100644 include/openmc/tallies/filter_meshmaterial.h create mode 100644 src/tallies/filter_meshmaterial.cpp create mode 100644 tests/unit_tests/test_filter_meshmaterial.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 4dff35418..6665682dd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -414,6 +414,7 @@ list(APPEND libopenmc_SOURCES src/tallies/filter_materialfrom.cpp src/tallies/filter_mesh.cpp src/tallies/filter_meshborn.cpp + src/tallies/filter_meshmaterial.cpp src/tallies/filter_meshsurface.cpp src/tallies/filter_mu.cpp src/tallies/filter_musurface.cpp diff --git a/docs/source/pythonapi/base.rst b/docs/source/pythonapi/base.rst index 1d8a93d47..d0e7d2439 100644 --- a/docs/source/pythonapi/base.rst +++ b/docs/source/pythonapi/base.rst @@ -129,6 +129,7 @@ Constructing Tallies openmc.SurfaceFilter openmc.MeshFilter openmc.MeshBornFilter + openmc.MeshMaterialFilter openmc.MeshSurfaceFilter openmc.EnergyFilter openmc.EnergyoutFilter diff --git a/include/openmc/tallies/filter.h b/include/openmc/tallies/filter.h index ee635b183..65098597a 100644 --- a/include/openmc/tallies/filter.h +++ b/include/openmc/tallies/filter.h @@ -33,6 +33,7 @@ enum class FilterType { MATERIALFROM, MESH, MESHBORN, + MESH_MATERIAL, MESH_SURFACE, MU, MUSURFACE, diff --git a/include/openmc/tallies/filter_mesh.h b/include/openmc/tallies/filter_mesh.h index 2f48b6d4d..4c9460d2e 100644 --- a/include/openmc/tallies/filter_mesh.h +++ b/include/openmc/tallies/filter_mesh.h @@ -9,9 +9,9 @@ namespace openmc { //============================================================================== -//! Indexes the location of particle events to a regular mesh. For tracklength -//! tallies, it will produce multiple valid bins and the bin weight will -//! correspond to the fraction of the track length that lies in that bin. +//! Indexes the location of particle events to a mesh. For tracklength tallies, +//! it will produce multiple valid bins and the bin weight will correspond to +//! the fraction of the track length that lies in that bin. //============================================================================== class MeshFilter : public Filter { diff --git a/include/openmc/tallies/filter_meshmaterial.h b/include/openmc/tallies/filter_meshmaterial.h new file mode 100644 index 000000000..42a4edcf0 --- /dev/null +++ b/include/openmc/tallies/filter_meshmaterial.h @@ -0,0 +1,114 @@ +#ifndef OPENMC_TALLIES_FILTER_MESHMATERIAL_H +#define OPENMC_TALLIES_FILTER_MESHMATERIAL_H + +#include +#include +#include +#include + +#include "openmc/position.h" +#include "openmc/random_ray/source_region.h" +#include "openmc/span.h" +#include "openmc/tallies/filter.h" +#include "openmc/vector.h" + +namespace openmc { + +//============================================================================== +//! Helper structs that define a combination of a mesh element index and a +//! material index and a functor for hashing to place in an unordered_map +//============================================================================== + +struct ElementMat { + //! Check for equality + bool operator==(const ElementMat& other) const + { + return index_element == other.index_element && index_mat == other.index_mat; + } + + int32_t index_element; + int32_t index_mat; +}; + +struct ElementMatHash { + std::size_t operator()(const ElementMat& k) const + { + size_t seed = 0; + hash_combine(seed, k.index_element); + hash_combine(seed, k.index_mat); + return seed; + } +}; + +//============================================================================== +//! Indexes the location of particle events to combinations of mesh element +//! index and material. For tracklength tallies, it will produce multiple valid +//! bins and the bin weight will correspond to the fraction of the track length +//! that lies in that bin. +//============================================================================== + +class MeshMaterialFilter : public Filter { +public: + //---------------------------------------------------------------------------- + // Constructors, destructors + + ~MeshMaterialFilter() = default; + + //---------------------------------------------------------------------------- + // Methods + + std::string type_str() const override { return "meshmaterial"; } + FilterType type() const override { return FilterType::MESH_MATERIAL; } + + 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 + + int32_t mesh() const { return mesh_; } + + void set_mesh(int32_t mesh); + + //! Set the bins based on a flat vector of alternating element index and + //! material IDs + void set_bins(span bins); + + //! Set the bins based on a vector of (element, material index) pairs + void set_bins(vector&& bins); + + virtual void set_translation(const Position& translation); + + virtual void set_translation(const double translation[3]); + + virtual const Position& translation() const { return translation_; } + + virtual bool translated() const { return translated_; } + +private: + //---------------------------------------------------------------------------- + // Data members + + int32_t mesh_; //!< Index of the mesh + bool translated_ {false}; //!< Whether or not the filter is translated + Position translation_ {0.0, 0.0, 0.0}; //!< Filter translation + + //! The indices of the mesh element-material combinations binned by this + //! filter. + vector bins_; + + //! The set of materials used in this filter + std::unordered_set materials_; + + //! A map from mesh element-material indices to filter bin indices. + std::unordered_map map_; +}; + +} // namespace openmc +#endif // OPENMC_TALLIES_FILTER_MESHMATERIAL_H diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index bbe9b2a43..686d3bc38 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -56,8 +56,8 @@ def get_microxs_and_flux( ---------- model : openmc.Model OpenMC model object. Must contain geometry, materials, and settings. - domains : list of openmc.Material or openmc.Cell or openmc.Universe, or openmc.MeshBase - Domains in which to tally reaction rates. + domains : list of openmc.Material or openmc.Cell or openmc.Universe, or openmc.MeshBase, or openmc.Filter + Domains in which to tally reaction rates, or a spatial tally filter. nuclides : list of str Nuclides to get cross sections for. If not specified, all burnable nuclides from the depletion chain file are used. @@ -103,7 +103,10 @@ def get_microxs_and_flux( energy_filter = openmc.EnergyFilter.from_group_structure(energies) else: energy_filter = openmc.EnergyFilter(energies) - if isinstance(domains, openmc.MeshBase): + + if isinstance(domains, openmc.Filter): + domain_filter = domains + elif isinstance(domains, openmc.MeshBase): domain_filter = openmc.MeshFilter(domains) elif isinstance(domains[0], openmc.Material): domain_filter = openmc.MaterialFilter(domains) diff --git a/openmc/filter.py b/openmc/filter.py index 4d37f1055..f29c54085 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -25,7 +25,8 @@ _FILTER_TYPES = ( 'energyout', 'mu', 'musurface', 'polar', 'azimuthal', 'distribcell', 'delayedgroup', 'energyfunction', 'cellfrom', 'materialfrom', 'legendre', 'spatiallegendre', 'sphericalharmonics', 'zernike', 'zernikeradial', 'particle', 'cellinstance', - 'collision', 'time', 'parentnuclide', 'weight' + 'collision', 'time', 'parentnuclide', 'weight', 'meshborn', 'meshsurface', + 'meshmaterial', ) _CURRENT_NAMES = ( @@ -900,8 +901,6 @@ class MeshFilter(Filter): @property def shape(self): - if isinstance(self, MeshSurfaceFilter): - return (self.num_bins,) return self.mesh.dimension @property @@ -992,8 +991,11 @@ class MeshFilter(Filter): XML element containing filter data """ - element = super().to_xml_element() - element[0].text = str(self.mesh.id) + element = ET.Element('filter') + element.set('id', str(self.id)) + element.set('type', self.short_name.lower()) + subelement = ET.SubElement(element, 'bins') + subelement.text = str(self.mesh.id) if self.translation is not None: element.set('translation', ' '.join(map(str, self.translation))) return element @@ -1039,6 +1041,181 @@ class MeshBornFilter(MeshFilter): """ +class MeshMaterialFilter(MeshFilter): + """Filter events by combinations of mesh elements and materials. + + .. versionadded:: 0.15.3 + + Parameters + ---------- + mesh : openmc.MeshBase + The mesh object that events will be tallied onto + bins : iterable of 2-tuples or numpy.ndarray + Combinations of (mesh element, material) to tally, given as 2-tuples. + The first value in the tuple represents the index of the mesh element, + and the second value indicates the material (either a + :class:`openmc.Material` instance of the ID). + filter_id : int + Unique identifier for the filter + + """ + def __init__(self, mesh: openmc.MeshBase, bins, filter_id=None): + self.mesh = mesh + self.bins = bins + self.id = filter_id + self._translation = None + + @classmethod + def from_volumes(cls, mesh: openmc.MeshBase, volumes: openmc.MeshMaterialVolumes): + """Construct a MeshMaterialFilter from a MeshMaterialVolumes object. + + Parameters + ---------- + mesh : openmc.MeshBase + The mesh object that events will be tallied onto + volumes : openmc.MeshMaterialVolumes + The mesh material volumes to use for the filter + + Returns + ------- + MeshMaterialFilter + A new MeshMaterialFilter instance + + """ + bins = list(zip(*np.where(volumes._materials > -1))) + return cls(mesh, bins) + + def __hash__(self): + data = (type(self).__name__, self.mesh.id, tuple(self.bins.ravel())) + return hash(data) + + def __repr__(self): + string = type(self).__name__ + '\n' + string += '{: <16}=\t{}\n'.format('\tID', self.id) + string += '{: <16}=\t{}\n'.format('\tMesh ID', self.mesh.id) + string += '{: <16}=\n{}\n'.format('\tBins', self.bins) + string += '{: <16}=\t{}\n'.format('\tTranslation', self.translation) + return string + + @property + def shape(self): + return (self.num_bins,) + + @property + def mesh(self): + return self._mesh + + @mesh.setter + def mesh(self, mesh): + cv.check_type('filter mesh', mesh, openmc.MeshBase) + self._mesh = mesh + + @Filter.bins.setter + def bins(self, bins): + pairs = np.empty((len(bins), 2), dtype=int) + for i, (elem, mat) in enumerate(bins): + cv.check_type('element', elem, Integral) + cv.check_type('material', mat, (Integral, openmc.Material)) + pairs[i, 0] = elem + pairs[i, 1] = mat if isinstance(mat, Integral) else mat.id + self._bins = pairs + + def to_xml_element(self): + """Return XML element representing the filter. + + Returns + ------- + element : lxml.etree._Element + XML element containing filter data + + """ + element = ET.Element('filter') + element.set('id', str(self.id)) + element.set('type', self.short_name.lower()) + element.set('mesh', str(self.mesh.id)) + + if self.translation is not None: + element.set('translation', ' '.join(map(str, self.translation))) + + subelement = ET.SubElement(element, 'bins') + subelement.text = ' '.join(str(i) for i in self.bins.ravel()) + + return element + + @classmethod + def from_xml_element(cls, elem: ET.Element, **kwargs) -> MeshMaterialFilter: + filter_id = int(elem.get('id')) + mesh_id = int(elem.get('mesh')) + mesh_obj = kwargs['meshes'][mesh_id] + bins = [int(x) for x in get_text(elem, 'bins').split()] + bins = list(zip(bins[::2], bins[1::2])) + out = cls(mesh_obj, bins, filter_id=filter_id) + + translation = elem.get('translation') + if translation: + out.translation = [float(x) for x in translation.split()] + return out + + @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") + + if 'meshes' not in kwargs: + raise ValueError(cls.__name__ + " requires a 'meshes' keyword " + "argument.") + + mesh_id = group['mesh'][()] + mesh_obj = kwargs['meshes'][mesh_id] + bins = group['bins'][()] + filter_id = int(group.name.split('/')[-1].lstrip('filter ')) + out = cls(mesh_obj, bins, filter_id=filter_id) + + translation = group.get('translation') + if translation: + out.translation = translation[()] + + return out + + def get_pandas_dataframe(self, data_size, stride, **kwargs): + """Builds a Pandas DataFrame for the Filter's bins. + + This method constructs a Pandas DataFrame object for the filter with + columns annotated by filter bin information. This is a helper method for + :meth:`Tally.get_pandas_dataframe`. + + Parameters + ---------- + data_size : int + The total number of bins in the tally corresponding to this filter + stride : int + Stride in memory for the filter + + Returns + ------- + pandas.DataFrame + A Pandas DataFrame with a multi-index column for the cell instance. + 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() + + """ + # Repeat and tile bins as necessary to account for other filters. + bins = np.repeat(self.bins, stride, axis=0) + tile_factor = data_size // len(bins) + bins = np.tile(bins, (tile_factor, 1)) + + columns = pd.MultiIndex.from_product([[self.short_name.lower()], + ['element', 'material']]) + return pd.DataFrame(bins, columns=columns) + + class MeshSurfaceFilter(MeshFilter): """Filter events by surface crossings on a mesh. @@ -1065,6 +1242,9 @@ class MeshSurfaceFilter(MeshFilter): The number of filter bins """ + @property + def shape(self): + return (self.num_bins,) @MeshFilter.mesh.setter def mesh(self, mesh): diff --git a/openmc/lib/filter.py b/openmc/lib/filter.py index 3a76b52a1..55b681d89 100644 --- a/openmc/lib/filter.py +++ b/openmc/lib/filter.py @@ -21,10 +21,10 @@ __all__ = [ 'CellInstanceFilter', 'CollisionFilter', 'DistribcellFilter', 'DelayedGroupFilter', 'EnergyFilter', 'EnergyoutFilter', 'EnergyFunctionFilter', 'LegendreFilter', 'MaterialFilter', 'MaterialFromFilter', 'MeshFilter', 'MeshBornFilter', - 'MeshSurfaceFilter', 'MuFilter', 'MuSurfaceFilter', 'ParentNuclideFilter', - 'ParticleFilter', 'PolarFilter', 'SphericalHarmonicsFilter', - 'SpatialLegendreFilter', 'SurfaceFilter', 'UniverseFilter', 'ZernikeFilter', - 'ZernikeRadialFilter', 'filters' + 'MeshMaterialFilter', 'MeshSurfaceFilter', 'MuFilter', 'MuSurfaceFilter', + 'ParentNuclideFilter', 'ParticleFilter', 'PolarFilter', 'SphericalHarmonicsFilter', + 'SpatialLegendreFilter', 'SurfaceFilter', 'TimeFilter', 'UniverseFilter', + 'WeightFilter', 'ZernikeFilter', 'ZernikeRadialFilter', 'filters' ] # Tally functions @@ -480,6 +480,10 @@ class MeshBornFilter(Filter): _dll.openmc_meshborn_filter_set_translation(self._index, (c_double*3)(*translation)) +class MeshMaterialFilter(Filter): + filter_type = 'meshmaterial' + + class MeshSurfaceFilter(Filter): """MeshSurface filter stored internally. @@ -606,6 +610,10 @@ class SurfaceFilter(Filter): filter_type = 'surface' +class TimeFilter(Filter): + filter_type = 'time' + + class UniverseFilter(Filter): filter_type = 'universe' @@ -643,6 +651,7 @@ _FILTER_TYPE_MAP = { 'cellborn': CellbornFilter, 'cellfrom': CellfromFilter, 'cellinstance': CellInstanceFilter, + 'collision': CollisionFilter, 'delayedgroup': DelayedGroupFilter, 'distribcell': DistribcellFilter, 'energy': EnergyFilter, @@ -653,6 +662,7 @@ _FILTER_TYPE_MAP = { 'materialfrom': MaterialFromFilter, 'mesh': MeshFilter, 'meshborn': MeshBornFilter, + 'meshmaterial': MeshMaterialFilter, 'meshsurface': MeshSurfaceFilter, 'mu': MuFilter, 'musurface': MuSurfaceFilter, @@ -662,7 +672,9 @@ _FILTER_TYPE_MAP = { 'sphericalharmonics': SphericalHarmonicsFilter, 'spatiallegendre': SpatialLegendreFilter, 'surface': SurfaceFilter, + 'time': TimeFilter, 'universe': UniverseFilter, + 'weight': WeightFilter, 'zernike': ZernikeFilter, 'zernikeradial': ZernikeRadialFilter } diff --git a/openmc/tallies.py b/openmc/tallies.py index e990a95f7..06de0205c 100644 --- a/openmc/tallies.py +++ b/openmc/tallies.py @@ -1575,7 +1575,7 @@ class Tally(IDManagerMixin): for i, f in enumerate(self.filters): if expand_dims: # Mesh filter indices are backwards so we need to flip them - if isinstance(f, openmc.MeshFilter): + if type(f) in {openmc.MeshFilter, openmc.MeshBornFilter}: fshape = f.shape[::-1] new_shape += fshape idx0, idx1 = i, i + len(fshape) - 1 diff --git a/src/tallies/filter.cpp b/src/tallies/filter.cpp index 17e57a987..79817981d 100644 --- a/src/tallies/filter.cpp +++ b/src/tallies/filter.cpp @@ -25,6 +25,7 @@ #include "openmc/tallies/filter_materialfrom.h" #include "openmc/tallies/filter_mesh.h" #include "openmc/tallies/filter_meshborn.h" +#include "openmc/tallies/filter_meshmaterial.h" #include "openmc/tallies/filter_meshsurface.h" #include "openmc/tallies/filter_mu.h" #include "openmc/tallies/filter_musurface.h" @@ -133,6 +134,8 @@ Filter* Filter::create(const std::string& type, int32_t id) return Filter::create(id); } else if (type == "meshborn") { return Filter::create(id); + } else if (type == "meshmaterial") { + return Filter::create(id); } else if (type == "meshsurface") { return Filter::create(id); } else if (type == "mu") { diff --git a/src/tallies/filter_meshmaterial.cpp b/src/tallies/filter_meshmaterial.cpp new file mode 100644 index 000000000..6e1f30380 --- /dev/null +++ b/src/tallies/filter_meshmaterial.cpp @@ -0,0 +1,185 @@ +#include "openmc/tallies/filter_meshmaterial.h" + +#include // for move + +#include + +#include "openmc/capi.h" +#include "openmc/constants.h" +#include "openmc/container_util.h" +#include "openmc/error.h" +#include "openmc/material.h" +#include "openmc/mesh.h" +#include "openmc/xml_interface.h" + +namespace openmc { + +void MeshMaterialFilter::from_xml(pugi::xml_node node) +{ + // Get mesh ID + auto mesh = get_node_array(node, "mesh"); + if (mesh.size() != 1) { + fatal_error( + "Only one mesh can be specified per " + type_str() + " mesh filter."); + } + + auto id = mesh[0]; + auto search = model::mesh_map.find(id); + if (search == model::mesh_map.end()) { + fatal_error( + fmt::format("Could not find mesh {} specified on tally filter.", id)); + } + set_mesh(search->second); + + // Get pairs of (element index, material) and set the bins + auto bins = get_node_array(node, "bins"); + this->set_bins(bins); + + if (check_for_node(node, "translation")) { + set_translation(get_node_array(node, "translation")); + } +} + +void MeshMaterialFilter::set_bins(span bins) +{ + if (bins.size() % 2 != 0) { + fatal_error( + fmt::format("Size of mesh material bins is not even: {}", bins.size())); + } + + // Create a vector of ElementMat pairs from the flat vector of bins + vector element_mats; + for (int64_t i = 0; i < bins.size() / 2; ++i) { + int32_t element = bins[2 * i]; + int32_t mat_id = bins[2 * i + 1]; + auto search = model::material_map.find(mat_id); + if (search == model::material_map.end()) { + fatal_error(fmt::format( + "Could not find material {} specified on tally filter.", mat_id)); + } + int32_t mat_index = search->second; + element_mats.push_back({element, mat_index}); + } + + this->set_bins(std::move(element_mats)); +} + +void MeshMaterialFilter::set_bins(vector&& bins) +{ + // Swap internal bins_ with the provided vector to avoid copying + bins_.swap(bins); + + // Clear and update the mapping and vector of materials + materials_.clear(); + map_.clear(); + for (std::size_t i = 0; i < bins_.size(); ++i) { + const auto& x = bins_[i]; + assert(x.index_mat >= 0); + assert(x.index_mat < model::materials.size()); + materials_.insert(x.index_mat); + map_[x] = i; + } + + n_bins_ = bins_.size(); +} + +void MeshMaterialFilter::set_mesh(int32_t mesh) +{ + // perform any additional perparation for mesh tallies here + mesh_ = mesh; + model::meshes[mesh_]->prepare_for_point_location(); +} + +void MeshMaterialFilter::set_translation(const Position& translation) +{ + translated_ = true; + translation_ = translation; +} + +void MeshMaterialFilter::set_translation(const double translation[3]) +{ + this->set_translation({translation[0], translation[1], translation[2]}); +} + +void MeshMaterialFilter::get_all_bins( + const Particle& p, TallyEstimator estimator, FilterMatch& match) const +{ + // If current material is not in any bins, don't bother checking + if (!contains(materials_, p.material())) { + return; + } + + Position last_r = p.r_last(); + Position r = p.r(); + Position u = p.u(); + + // apply translation if present + if (translated_) { + last_r -= translation(); + r -= translation(); + } + + if (estimator != TallyEstimator::TRACKLENGTH) { + int32_t index_element = model::meshes[mesh_]->get_bin(r); + if (index_element >= 0) { + auto search = map_.find({index_element, p.material()}); + if (search != map_.end()) { + match.bins_.push_back(search->second); + match.weights_.push_back(1.0); + } + } + } else { + // First determine which elements the particle crosses (may or may not + // actually match bins so we have to adjust bins_/weight_ after) + int32_t n_start = match.bins_.size(); + model::meshes[mesh_]->bins_crossed( + last_r, r, u, match.bins_, match.weights_); + int32_t n_end = match.bins_.size(); + + // Go through bins and weights and check which ones are actually a match + // based on the (element, material) pair. For matches, overwrite the bin. + int i = 0; + for (int j = n_start; j < n_end; ++j) { + int32_t index_element = match.bins_[j]; + double weight = match.weights_[j]; + auto search = map_.find({index_element, p.material()}); + if (search != map_.end()) { + match.bins_[n_start + i] = search->second; + match.weights_[n_start + i] = weight; + ++i; + } + } + + // Resize the vectors to remove the unmatched bins + match.bins_.resize(n_start + i); + } +} + +void MeshMaterialFilter::to_statepoint(hid_t filter_group) const +{ + Filter::to_statepoint(filter_group); + write_dataset(filter_group, "mesh", model::meshes[mesh_]->id_); + + size_t n = bins_.size(); + xt::xtensor data({n, 2}); + for (int64_t i = 0; i < n; ++i) { + const auto& x = bins_[i]; + data(i, 0) = x.index_element; + data(i, 1) = model::materials[x.index_mat]->id_; + } + write_dataset(filter_group, "bins", data); + + if (translated_) { + write_dataset(filter_group, "translation", translation_); + } +} + +std::string MeshMaterialFilter::text_label(int bin) const +{ + auto& x = bins_[bin]; + auto& mesh = *model::meshes.at(mesh_); + return fmt::format("Mesh {}, {}, Material {}", mesh.id(), + mesh.bin_label(x.index_element), model::materials[x.index_mat]->id_); +} + +} // namespace openmc diff --git a/tests/unit_tests/test_filter_meshmaterial.py b/tests/unit_tests/test_filter_meshmaterial.py new file mode 100644 index 000000000..120533209 --- /dev/null +++ b/tests/unit_tests/test_filter_meshmaterial.py @@ -0,0 +1,65 @@ +import numpy as np +import openmc +from pytest import approx + + +def test_filter_mesh_material(run_in_tmpdir): + # Create four identical materials + openmc.reset_auto_ids() + materials = [] + for i in range(4): + mat = openmc.Material() + mat.add_nuclide('Fe56', 1.0) + materials.append(mat) + + # Create a slab model with four cells + z_values = [-10., -5., 0., 5., 10.] + planes = [openmc.ZPlane(z) for z in z_values] + planes[0].boundary_type = 'vacuum' + planes[-1].boundary_type = 'vacuum' + regions = [+left & -right for left, right in zip(planes[:-1], planes[1:])] + cells = [openmc.Cell(fill=m, region=r) for r, m in zip(regions, materials)] + model = openmc.Model() + model.geometry = openmc.Geometry(cells) + model.settings.particles = 1_000 + model.settings.batches = 5 + model.settings.run_mode = 'fixed source' + + # Create a mesh that does not align with all planar surfaces + mesh = openmc.RegularMesh() + mesh.lower_left = (-1., -1., -10.) + mesh.upper_right = (1., 1., 10.) + mesh.dimension = (1, 1, 5) + + # Determine material volumes in each mesh element and use result to create a + # MeshMaterialFilter with corresponding bins + vols = mesh.material_volumes(model) + mmf = openmc.MeshMaterialFilter.from_volumes(mesh, vols) + expected_bins = [(0, 1), (1, 1), (1, 2), (2, 2), (2, 3), (3, 3), (3, 4), (4, 4)] + np.testing.assert_equal(mmf.bins, expected_bins) + + # Create two tallies, one with a mesh filter and one with mesh-material + mesh_tally = openmc.Tally() + mesh_tally.filters = [openmc.MeshFilter(mesh)] + mesh_tally.scores = ['flux'] + mesh_material_tally = openmc.Tally() + mesh_material_tally.filters = [mmf] + mesh_material_tally.scores = ['flux'] + model.tallies = [mesh_tally, mesh_material_tally] + + # Run model to get results on the two tallies + model.run(apply_tally_results=True) + + # The sum of the flux in each mesh-material combination within a single mesh + # element should be equal to the flux in that mesh element + mesh_mean = mesh_tally.mean.ravel() + meshmat_mean = mesh_material_tally.mean.ravel() + assert mesh_mean[0] == approx(meshmat_mean[0]) + assert mesh_mean[1] == approx(meshmat_mean[1] + meshmat_mean[2]) + assert mesh_mean[2] == approx(meshmat_mean[3] + meshmat_mean[4]) + assert mesh_mean[3] == approx(meshmat_mean[5] + meshmat_mean[6]) + assert mesh_mean[4] == approx(meshmat_mean[7]) + assert mesh_tally.mean.sum() == approx(mesh_material_tally.mean.sum()) + + # Make sure get_pandas_dataframe method works + mesh_material_tally.get_pandas_dataframe() diff --git a/tests/unit_tests/test_filters.py b/tests/unit_tests/test_filters.py index 60a60e3f5..8c56a310e 100644 --- a/tests/unit_tests/test_filters.py +++ b/tests/unit_tests/test_filters.py @@ -333,3 +333,36 @@ def test_weight(): new_f = openmc.Filter.from_xml_element(elem) assert new_f.id == f.id assert np.allclose(new_f.bins, f.bins) + + +def test_mesh_material(): + mat1 = openmc.Material() + mat2 = openmc.Material() + + mesh = openmc.RegularMesh() + mesh.lower_left = (-1., -1., -1.) + mesh.upper_right = (1., 1., 1.) + mesh.dimension = (2, 4, 1) + bins = [(0, mat1), (0, mat2), (6, mat1), (7, mat2)] + f = openmc.MeshMaterialFilter(mesh, bins) + + expected_bins = [(0, mat1.id), (0, mat2.id), (6, mat1.id), (7, mat2.id)] + assert np.allclose(f.bins, expected_bins) + assert f.mesh == mesh + assert f.shape == (4,) + + # to_xml_element() + elem = f.to_xml_element() + assert elem.tag == 'filter' + assert elem.attrib['type'] == 'meshmaterial' + + # from_xml_element() + new_f = openmc.Filter.from_xml_element(elem, meshes={mesh.id: mesh}) + assert isinstance(new_f, openmc.MeshMaterialFilter) + assert new_f.id == f.id + assert new_f.mesh == f.mesh + assert np.allclose(new_f.bins, expected_bins) + + # Test hash and str + hash(f) + str(f) From 56b44aa4e8ed8e8c55a8f4a7ec0c347fe9390449 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Tue, 10 Jun 2025 09:13:08 -0500 Subject: [PATCH 335/671] Random Ray Missed Cell Policy Change for Adjoint Mode (#3434) --- src/random_ray/flat_source_domain.cpp | 2 +- .../flat/results_true.dat | 340 +++++++++--------- .../linear/results_true.dat | 254 ++++++------- 3 files changed, 298 insertions(+), 298 deletions(-) diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 367c63af4..9df6513ae 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -298,7 +298,7 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() // in the cell we will use the previous iteration's flux estimate. This // injects a small degree of correlation into the simulation, but this // is going to be trivial when the miss rate is a few percent or less. - if (source_regions_.external_source_present(sr) && !adjoint_) { + if (source_regions_.external_source_present(sr)) { set_flux_to_old_flux(sr, g); } else { set_flux_to_source(sr, g); diff --git a/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/results_true.dat b/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/results_true.dat index e14b7e199..0354501d1 100644 --- a/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/results_true.dat +++ b/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/results_true.dat @@ -36,15 +36,15 @@ Lower Bounds 1.73e-01 2.44e-01 3.63e-01 -1.23e-01 +1.22e-01 1.71e-01 1.66e-01 1.69e-01 -1.71e-01 +1.70e-01 1.67e-01 1.71e-01 1.64e-01 -1.56e-01 +1.55e-01 1.67e-01 1.64e-01 1.68e-01 @@ -96,7 +96,7 @@ Lower Bounds 1.80e-01 2.59e-01 2.97e-01 -1.07e-01 +1.06e-01 1.68e-01 1.60e-01 1.58e-01 @@ -156,7 +156,7 @@ Lower Bounds 1.49e-01 2.14e-01 1.76e-01 -6.71e-02 +6.70e-02 1.72e-01 1.66e-01 1.74e-01 @@ -192,7 +192,7 @@ Lower Bounds 2.37e-01 2.51e-01 2.64e-01 -2.43e-01 +2.42e-01 2.39e-01 2.32e-01 2.32e-01 @@ -209,12 +209,12 @@ Lower Bounds 2.86e-01 3.17e-01 2.60e-01 -2.39e-01 +2.38e-01 1.88e-01 1.97e-01 1.84e-01 1.50e-01 -9.99e-02 +9.98e-02 3.88e-02 1.90e-02 2.18e-01 @@ -340,7 +340,7 @@ Lower Bounds 1.65e-01 1.63e-01 1.59e-01 -1.58e-01 +1.57e-01 1.59e-01 1.54e-01 1.54e-01 @@ -416,7 +416,7 @@ Lower Bounds 2.65e-01 2.54e-01 2.58e-01 -2.81e-01 +2.80e-01 2.53e-01 2.33e-01 2.40e-01 @@ -503,7 +503,7 @@ Lower Bounds 3.36e-01 1.29e-01 1.57e-01 -1.68e-01 +1.67e-01 1.66e-01 1.63e-01 1.63e-01 @@ -513,7 +513,7 @@ Lower Bounds 1.56e-01 1.59e-01 1.65e-01 -1.60e-01 +1.59e-01 2.43e-01 3.30e-01 1.08e-01 @@ -543,7 +543,7 @@ Lower Bounds 1.54e-01 1.56e-01 1.67e-01 -1.70e-01 +1.69e-01 2.42e-01 3.48e-01 1.32e-01 @@ -661,7 +661,7 @@ Lower Bounds 2.34e-01 2.20e-01 1.88e-01 -1.78e-01 +1.77e-01 1.34e-01 1.25e-01 1.07e-01 @@ -676,7 +676,7 @@ Lower Bounds 8.09e-02 8.37e-02 5.69e-02 -6.46e-02 +6.45e-02 3.15e-02 3.86e-02 3.51e-02 @@ -770,14 +770,14 @@ Lower Bounds 1.53e-01 1.64e-01 2.46e-01 -3.03e-01 +3.02e-01 1.11e-01 1.63e-01 1.59e-01 1.68e-01 -1.56e-01 +1.59e-01 1.52e-01 -1.55e-01 +1.54e-01 1.56e-01 1.49e-01 1.49e-01 @@ -850,7 +850,7 @@ Lower Bounds 1.73e-01 1.65e-01 1.70e-01 -7.83e-02 +1.66e-01 1.73e-01 1.78e-01 1.65e-01 @@ -877,7 +877,7 @@ Lower Bounds 2.14e-01 8.26e-02 2.65e-02 -3.07e-01 +3.06e-01 2.51e-01 2.10e-01 2.95e-01 @@ -1091,7 +1091,7 @@ Lower Bounds 2.30e-01 2.33e-01 2.43e-01 -2.46e-01 +2.45e-01 2.48e-01 2.46e-01 2.21e-01 @@ -1124,7 +1124,7 @@ Lower Bounds 1.07e-01 8.75e-02 1.17e-01 -5.68e-02 +5.67e-02 7.16e-02 5.22e-02 3.31e-02 @@ -1317,7 +1317,7 @@ Lower Bounds 2.20e-01 2.27e-01 2.41e-01 -2.35e-01 +2.34e-01 2.13e-01 2.04e-01 2.13e-01 @@ -1359,14 +1359,14 @@ Lower Bounds 8.92e-03 1.61e-01 1.59e-01 -1.57e-01 +1.56e-01 1.59e-01 1.51e-01 1.49e-01 1.54e-01 1.51e-01 1.53e-01 -1.49e-01 +1.48e-01 1.52e-01 1.48e-01 2.27e-01 @@ -1478,12 +1478,12 @@ Lower Bounds 1.68e-01 5.02e-02 1.53e-01 -7.09e-02 +1.48e-01 1.47e-01 1.48e-01 1.43e-01 1.46e-01 -6.84e-02 +1.42e-01 1.27e-01 1.24e-01 1.31e-01 @@ -1523,7 +1523,7 @@ Lower Bounds 7.50e-02 2.64e-02 1.58e-01 -1.56e-01 +1.55e-01 1.50e-01 1.45e-01 1.50e-01 @@ -1575,7 +1575,7 @@ Lower Bounds 6.95e-02 7.10e-02 4.92e-02 -4.74e-02 +4.73e-02 3.52e-02 4.69e-02 4.03e-02 @@ -1669,7 +1669,7 @@ Lower Bounds 1.34e-01 1.36e-01 1.50e-01 -1.96e-01 +1.95e-01 1.29e-01 4.48e-02 1.49e-01 @@ -1784,7 +1784,7 @@ Lower Bounds 1.53e-01 1.76e-01 1.13e-01 -9.77e-02 +9.76e-02 8.62e-02 1.49e-01 1.29e-01 @@ -1898,7 +1898,7 @@ Lower Bounds 1.53e-01 5.25e-02 1.45e-01 -1.41e-01 +1.44e-01 1.44e-01 1.36e-01 1.36e-01 @@ -1928,13 +1928,13 @@ Lower Bounds 7.71e-02 3.01e-02 1.38e-01 -1.12e-01 +1.35e-01 1.38e-01 1.41e-01 1.44e-01 1.46e-01 1.34e-01 -1.11e-01 +1.28e-01 1.24e-01 1.20e-01 1.22e-01 @@ -1955,7 +1955,7 @@ Lower Bounds 1.21e-01 1.25e-01 1.84e-01 -9.19e-02 +9.18e-02 2.31e-02 1.48e-01 1.32e-01 @@ -2039,7 +2039,7 @@ Lower Bounds 1.55e-01 1.46e-01 1.44e-01 -1.48e-01 +1.47e-01 1.42e-01 1.44e-01 1.40e-01 @@ -2067,7 +2067,7 @@ Lower Bounds 1.43e-01 1.44e-01 1.46e-01 -1.54e-01 +1.53e-01 1.50e-01 1.51e-01 1.54e-01 @@ -2214,7 +2214,7 @@ Lower Bounds 1.83e-02 2.39e-01 1.91e-01 -1.79e-01 +1.80e-01 2.07e-01 1.97e-01 2.03e-01 @@ -2238,7 +2238,7 @@ Lower Bounds 1.30e-01 1.06e-01 9.60e-02 -7.07e-02 +7.06e-02 4.00e-02 1.71e-02 7.45e-03 @@ -2343,7 +2343,7 @@ Lower Bounds 1.32e-01 1.41e-01 1.41e-01 -1.40e-01 +1.39e-01 2.04e-01 1.15e-01 3.10e-02 @@ -2471,7 +2471,7 @@ Lower Bounds 3.06e-02 3.68e-02 4.09e-02 -5.27e-02 +5.26e-02 4.71e-02 3.30e-02 4.67e-02 @@ -2493,7 +2493,7 @@ Lower Bounds 1.53e-01 1.51e-01 1.54e-01 -1.45e-01 +1.44e-01 1.43e-01 3.50e-02 1.38e-02 @@ -2533,7 +2533,7 @@ Lower Bounds 1.54e-01 1.59e-01 1.65e-01 -5.07e-02 +1.63e-01 1.43e-01 1.52e-01 1.45e-01 @@ -2548,7 +2548,7 @@ Lower Bounds 1.55e-01 1.59e-01 1.59e-01 -1.53e-01 +1.54e-01 1.47e-01 1.53e-01 1.51e-01 @@ -2648,7 +2648,7 @@ Lower Bounds 5.02e-02 1.48e-02 1.33e-01 -5.34e-02 +1.31e-01 1.34e-01 1.36e-01 1.35e-01 @@ -2678,7 +2678,7 @@ Lower Bounds 2.21e-02 7.42e-03 1.30e-01 -7.03e-02 +7.02e-02 8.47e-02 1.05e-01 8.11e-02 @@ -2726,7 +2726,7 @@ Lower Bounds 2.10e-01 2.47e-01 2.20e-01 -2.21e-01 +2.20e-01 2.29e-01 2.22e-01 2.06e-01 @@ -2776,7 +2776,7 @@ Lower Bounds 2.54e-01 2.18e-01 2.16e-01 -2.24e-01 +2.23e-01 1.99e-01 1.77e-01 1.57e-01 @@ -3026,7 +3026,7 @@ Lower Bounds 1.80e-01 2.09e-01 2.09e-01 -1.83e-01 +1.82e-01 2.00e-01 1.82e-01 1.57e-01 @@ -3120,7 +3120,7 @@ Lower Bounds 8.59e-02 7.51e-02 7.34e-02 -6.11e-02 +6.10e-02 6.19e-02 4.42e-02 3.60e-02 @@ -3164,7 +3164,7 @@ Lower Bounds 1.09e-01 8.20e-02 7.49e-02 -7.94e-02 +7.93e-02 7.32e-02 5.71e-02 3.93e-02 @@ -3188,7 +3188,7 @@ Lower Bounds 1.79e-02 8.68e-03 8.76e-02 -6.94e-02 +6.93e-02 1.01e-01 9.79e-02 5.28e-02 @@ -3236,7 +3236,7 @@ Lower Bounds 5.48e-02 7.05e-02 7.64e-02 -6.19e-02 +6.18e-02 5.28e-02 6.84e-02 5.17e-02 @@ -3404,7 +3404,7 @@ Upper Bounds 7.61e-01 8.32e-01 8.60e-01 -8.42e-01 +8.41e-01 8.42e-01 8.32e-01 8.34e-01 @@ -3412,15 +3412,15 @@ Upper Bounds 8.65e-01 1.22e+00 1.82e+00 -6.13e-01 +6.12e-01 8.56e-01 -8.32e-01 +8.31e-01 8.43e-01 -8.53e-01 +8.52e-01 8.33e-01 8.57e-01 8.19e-01 -7.78e-01 +7.77e-01 8.33e-01 8.19e-01 8.41e-01 @@ -3472,8 +3472,8 @@ Upper Bounds 8.98e-01 1.29e+00 1.49e+00 -5.33e-01 -8.40e-01 +5.32e-01 +8.39e-01 7.98e-01 7.91e-01 8.03e-01 @@ -3526,7 +3526,7 @@ Upper Bounds 7.71e-01 7.75e-01 7.58e-01 -7.64e-01 +7.63e-01 7.06e-01 7.64e-01 7.43e-01 @@ -3556,7 +3556,7 @@ Upper Bounds 8.72e-01 7.91e-01 8.39e-01 -7.95e-01 +7.94e-01 7.87e-01 7.88e-01 7.38e-01 @@ -3589,7 +3589,7 @@ Upper Bounds 9.40e-01 9.83e-01 9.19e-01 -7.52e-01 +7.51e-01 4.99e-01 1.94e-01 9.48e-02 @@ -3615,7 +3615,7 @@ Upper Bounds 8.13e-01 7.97e-01 8.76e-01 -8.45e-01 +8.44e-01 7.95e-01 8.18e-01 8.84e-01 @@ -3649,7 +3649,7 @@ Upper Bounds 8.01e-01 8.48e-01 8.52e-01 -8.71e-01 +8.70e-01 1.31e+00 1.65e+00 5.73e-01 @@ -3713,10 +3713,10 @@ Upper Bounds 1.17e+00 1.50e+00 5.74e-01 -8.24e-01 +8.23e-01 8.15e-01 7.95e-01 -7.88e-01 +7.87e-01 7.97e-01 7.70e-01 7.69e-01 @@ -3761,7 +3761,7 @@ Upper Bounds 8.45e-01 8.73e-01 8.56e-01 -8.16e-01 +8.15e-01 8.44e-01 8.36e-01 7.99e-01 @@ -3774,11 +3774,11 @@ Upper Bounds 6.73e-01 2.04e-01 8.93e-01 -8.59e-01 +8.60e-01 8.64e-01 8.51e-01 8.70e-01 -8.91e-01 +8.90e-01 8.41e-01 7.94e-01 7.85e-01 @@ -3856,10 +3856,10 @@ Upper Bounds 8.14e-01 7.91e-01 8.00e-01 -7.95e-01 +7.94e-01 8.24e-01 8.16e-01 -8.87e-01 +8.86e-01 1.22e+00 1.96e+00 8.18e-01 @@ -3879,7 +3879,7 @@ Upper Bounds 1.68e+00 6.47e-01 7.87e-01 -8.38e-01 +8.37e-01 8.29e-01 8.15e-01 8.16e-01 @@ -3889,13 +3889,13 @@ Upper Bounds 7.82e-01 7.95e-01 8.24e-01 -7.98e-01 +7.97e-01 1.22e+00 1.65e+00 5.42e-01 8.09e-01 8.09e-01 -8.05e-01 +8.04e-01 7.72e-01 8.14e-01 8.20e-01 @@ -3919,7 +3919,7 @@ Upper Bounds 7.72e-01 7.80e-01 8.33e-01 -8.48e-01 +8.47e-01 1.21e+00 1.74e+00 6.61e-01 @@ -3934,7 +3934,7 @@ Upper Bounds 7.56e-01 7.70e-01 7.83e-01 -7.97e-01 +7.96e-01 1.13e+00 1.26e+00 4.66e-01 @@ -3943,7 +3943,7 @@ Upper Bounds 7.98e-01 7.75e-01 7.87e-01 -7.92e-01 +7.91e-01 7.70e-01 7.35e-01 7.31e-01 @@ -3959,7 +3959,7 @@ Upper Bounds 8.01e-01 7.99e-01 7.90e-01 -7.89e-01 +7.88e-01 7.83e-01 7.33e-01 7.48e-01 @@ -3975,7 +3975,7 @@ Upper Bounds 8.32e-01 7.85e-01 7.86e-01 -7.51e-01 +7.50e-01 7.01e-01 7.38e-01 7.30e-01 @@ -4037,13 +4037,13 @@ Upper Bounds 1.17e+00 1.10e+00 9.39e-01 -8.88e-01 +8.87e-01 6.70e-01 6.23e-01 5.35e-01 2.34e-01 6.47e-02 -6.45e-01 +6.44e-01 4.31e-01 7.11e-01 6.80e-01 @@ -4063,7 +4063,7 @@ Upper Bounds 8.31e-01 8.52e-01 8.43e-01 -8.06e-01 +8.05e-01 7.88e-01 7.57e-01 7.58e-01 @@ -4083,7 +4083,7 @@ Upper Bounds 7.76e-01 7.95e-01 8.30e-01 -7.96e-01 +7.95e-01 8.37e-01 1.18e+00 1.62e+00 @@ -4105,7 +4105,7 @@ Upper Bounds 5.36e-01 8.08e-01 8.15e-01 -8.00e-01 +7.99e-01 8.03e-01 7.68e-01 7.86e-01 @@ -4122,7 +4122,7 @@ Upper Bounds 7.72e-01 8.04e-01 8.28e-01 -7.75e-01 +7.74e-01 7.64e-01 7.80e-01 7.54e-01 @@ -4151,12 +4151,12 @@ Upper Bounds 8.13e-01 7.95e-01 8.40e-01 -7.80e-01 +7.97e-01 7.59e-01 -7.73e-01 +7.72e-01 7.80e-01 7.46e-01 -7.45e-01 +7.44e-01 7.55e-01 7.27e-01 7.44e-01 @@ -4226,8 +4226,8 @@ Upper Bounds 8.65e-01 8.27e-01 8.48e-01 -3.91e-01 -8.66e-01 +8.31e-01 +8.65e-01 8.92e-01 8.23e-01 7.50e-01 @@ -4265,7 +4265,7 @@ Upper Bounds 9.09e-01 6.43e-01 6.18e-01 -5.19e-01 +5.18e-01 2.26e-01 8.71e-02 5.78e-01 @@ -4367,7 +4367,7 @@ Upper Bounds 7.08e-01 7.78e-01 7.45e-01 -7.62e-01 +7.61e-01 7.55e-01 7.92e-01 1.16e+00 @@ -4376,7 +4376,7 @@ Upper Bounds 7.40e-01 7.79e-01 7.94e-01 -7.60e-01 +7.59e-01 7.99e-01 7.16e-01 7.14e-01 @@ -4420,7 +4420,7 @@ Upper Bounds 2.76e-01 8.13e-01 8.09e-01 -7.87e-01 +7.86e-01 7.66e-01 7.26e-01 7.62e-01 @@ -4531,7 +4531,7 @@ Upper Bounds 7.86e-01 7.98e-01 7.43e-01 -7.69e-01 +7.68e-01 7.51e-01 7.70e-01 8.08e-01 @@ -4583,7 +4583,7 @@ Upper Bounds 1.19e+00 1.33e+00 5.12e-01 -7.76e-01 +7.75e-01 7.68e-01 7.84e-01 7.62e-01 @@ -4631,14 +4631,14 @@ Upper Bounds 7.78e-01 7.65e-01 7.86e-01 -7.10e-01 +7.09e-01 7.60e-01 7.43e-01 7.27e-01 6.62e-01 6.51e-01 6.35e-01 -7.04e-01 +7.03e-01 7.07e-01 1.06e+00 7.73e-01 @@ -4648,7 +4648,7 @@ Upper Bounds 7.63e-01 7.80e-01 7.34e-01 -7.23e-01 +7.24e-01 7.02e-01 6.87e-01 7.13e-01 @@ -4724,7 +4724,7 @@ Upper Bounds 4.81e-01 4.96e-01 3.78e-01 -5.26e-01 +5.25e-01 3.10e-01 2.83e-01 1.99e-01 @@ -4735,14 +4735,14 @@ Upper Bounds 4.46e-02 8.04e-01 7.95e-01 -7.83e-01 +7.82e-01 7.95e-01 7.54e-01 7.47e-01 7.72e-01 7.56e-01 7.67e-01 -7.43e-01 +7.42e-01 7.58e-01 7.40e-01 1.14e+00 @@ -4779,7 +4779,7 @@ Upper Bounds 1.24e+00 5.06e-01 7.87e-01 -7.41e-01 +7.40e-01 7.55e-01 7.36e-01 7.30e-01 @@ -4821,7 +4821,7 @@ Upper Bounds 7.08e-01 7.57e-01 1.07e+00 -7.06e-01 +7.05e-01 2.16e-01 7.82e-01 7.85e-01 @@ -4854,12 +4854,12 @@ Upper Bounds 8.40e-01 2.51e-01 7.67e-01 -3.55e-01 +7.41e-01 7.36e-01 7.41e-01 7.16e-01 -7.29e-01 -3.42e-01 +7.28e-01 +7.11e-01 6.33e-01 6.18e-01 6.57e-01 @@ -4874,7 +4874,7 @@ Upper Bounds 7.36e-01 7.09e-01 6.77e-01 -6.93e-01 +6.94e-01 6.91e-01 6.86e-01 6.77e-01 @@ -4899,8 +4899,8 @@ Upper Bounds 3.75e-01 1.32e-01 7.90e-01 -7.78e-01 -7.50e-01 +7.77e-01 +7.49e-01 7.25e-01 7.49e-01 7.50e-01 @@ -4964,7 +4964,7 @@ Upper Bounds 7.66e-01 7.81e-01 7.83e-01 -7.80e-01 +7.79e-01 7.68e-01 7.20e-01 7.09e-01 @@ -5045,7 +5045,7 @@ Upper Bounds 6.71e-01 6.81e-01 7.49e-01 -9.78e-01 +9.77e-01 6.44e-01 2.24e-01 7.44e-01 @@ -5148,7 +5148,7 @@ Upper Bounds 8.35e-01 8.39e-01 9.59e-01 -9.14e-01 +9.15e-01 7.74e-01 5.71e-01 1.72e-01 @@ -5191,7 +5191,7 @@ Upper Bounds 7.60e-01 7.46e-01 7.71e-01 -7.12e-01 +7.11e-01 6.80e-01 6.82e-01 7.51e-01 @@ -5229,7 +5229,7 @@ Upper Bounds 8.27e-01 2.75e-01 7.59e-01 -7.20e-01 +7.19e-01 7.47e-01 7.32e-01 7.40e-01 @@ -5256,7 +5256,7 @@ Upper Bounds 7.13e-01 7.47e-01 9.68e-01 -6.86e-01 +6.85e-01 2.05e-01 7.59e-01 7.40e-01 @@ -5266,22 +5266,22 @@ Upper Bounds 6.98e-01 7.04e-01 6.61e-01 -6.80e-01 -6.80e-01 +6.79e-01 +6.79e-01 6.86e-01 7.42e-01 1.01e+00 7.65e-01 2.63e-01 7.27e-01 -7.07e-01 +7.22e-01 7.21e-01 6.82e-01 6.82e-01 6.79e-01 6.66e-01 6.58e-01 -6.32e-01 +6.31e-01 6.61e-01 6.87e-01 6.88e-01 @@ -5304,13 +5304,13 @@ Upper Bounds 3.86e-01 1.50e-01 6.91e-01 -5.58e-01 +6.77e-01 6.91e-01 7.07e-01 7.18e-01 7.31e-01 6.70e-01 -5.57e-01 +6.39e-01 6.18e-01 6.02e-01 6.11e-01 @@ -5349,7 +5349,7 @@ Upper Bounds 4.84e-01 1.41e-01 7.40e-01 -6.74e-01 +6.73e-01 6.74e-01 7.15e-01 7.22e-01 @@ -5415,16 +5415,16 @@ Upper Bounds 7.76e-01 7.30e-01 7.18e-01 -7.38e-01 +7.37e-01 7.09e-01 -7.21e-01 +7.20e-01 7.00e-01 7.72e-01 9.68e-01 6.90e-01 2.52e-01 7.57e-01 -7.65e-01 +7.64e-01 7.45e-01 7.47e-01 7.46e-01 @@ -5433,9 +5433,9 @@ Upper Bounds 7.52e-01 7.26e-01 6.82e-01 -7.10e-01 +7.09e-01 7.86e-01 -1.05e+00 +1.04e+00 7.77e-01 2.28e-01 7.74e-01 @@ -5443,7 +5443,7 @@ Upper Bounds 7.17e-01 7.21e-01 7.29e-01 -7.68e-01 +7.67e-01 7.52e-01 7.57e-01 7.68e-01 @@ -5504,7 +5504,7 @@ Upper Bounds 7.08e-01 7.09e-01 7.14e-01 -6.80e-01 +6.79e-01 6.85e-01 6.58e-01 6.83e-01 @@ -5519,7 +5519,7 @@ Upper Bounds 7.21e-01 7.00e-01 6.83e-01 -6.44e-01 +6.43e-01 6.62e-01 6.42e-01 6.10e-01 @@ -5566,7 +5566,7 @@ Upper Bounds 6.70e-01 6.87e-01 6.53e-01 -6.04e-01 +6.03e-01 5.84e-01 5.54e-01 5.65e-01 @@ -5574,7 +5574,7 @@ Upper Bounds 4.40e-01 1.48e-01 7.67e-01 -6.96e-01 +6.97e-01 7.26e-01 7.26e-01 6.91e-01 @@ -5590,7 +5590,7 @@ Upper Bounds 9.16e-02 1.20e+00 9.57e-01 -8.97e-01 +8.98e-01 1.03e+00 9.83e-01 1.01e+00 @@ -5655,7 +5655,7 @@ Upper Bounds 7.63e-01 7.61e-01 7.53e-01 -7.70e-01 +7.69e-01 7.59e-01 7.54e-01 7.39e-01 @@ -5667,18 +5667,18 @@ Upper Bounds 7.39e-01 7.32e-01 7.64e-01 -7.36e-01 +7.35e-01 7.69e-01 7.88e-01 7.27e-01 7.35e-01 7.32e-01 7.27e-01 -7.50e-01 +7.49e-01 9.97e-01 7.08e-01 2.42e-01 -7.31e-01 +7.30e-01 7.41e-01 7.80e-01 7.20e-01 @@ -5719,7 +5719,7 @@ Upper Bounds 6.58e-01 7.03e-01 7.03e-01 -6.98e-01 +6.97e-01 1.02e+00 5.77e-01 1.55e-01 @@ -5766,12 +5766,12 @@ Upper Bounds 6.04e-01 6.27e-01 8.72e-01 -4.59e-01 +4.58e-01 1.30e-01 6.94e-01 6.76e-01 6.98e-01 -7.17e-01 +7.16e-01 7.24e-01 6.61e-01 6.58e-01 @@ -5815,7 +5815,7 @@ Upper Bounds 7.13e-02 9.78e-01 8.94e-01 -9.01e-01 +9.02e-01 9.75e-01 9.93e-01 9.72e-01 @@ -5869,7 +5869,7 @@ Upper Bounds 7.65e-01 7.57e-01 7.69e-01 -7.23e-01 +7.22e-01 7.14e-01 1.75e-01 6.92e-02 @@ -5901,7 +5901,7 @@ Upper Bounds 7.12e-01 8.00e-01 1.03e+00 -5.17e-01 +5.16e-01 1.38e-01 7.80e-01 7.93e-01 @@ -5909,7 +5909,7 @@ Upper Bounds 7.70e-01 7.94e-01 8.27e-01 -2.53e-01 +8.15e-01 7.14e-01 7.62e-01 7.24e-01 @@ -5924,7 +5924,7 @@ Upper Bounds 7.75e-01 7.94e-01 7.94e-01 -7.67e-01 +7.68e-01 7.35e-01 7.64e-01 7.55e-01 @@ -5940,7 +5940,7 @@ Upper Bounds 7.62e-01 7.55e-01 7.49e-01 -7.14e-01 +7.15e-01 7.33e-01 7.29e-01 6.88e-01 @@ -5953,10 +5953,10 @@ Upper Bounds 7.63e-01 7.78e-01 7.67e-01 -7.39e-01 +7.38e-01 7.39e-01 7.04e-01 -6.89e-01 +6.90e-01 7.02e-01 7.00e-01 6.50e-01 @@ -5967,7 +5967,7 @@ Upper Bounds 7.31e-01 7.02e-01 7.48e-01 -7.26e-01 +7.25e-01 7.23e-01 7.48e-01 7.26e-01 @@ -6005,7 +6005,7 @@ Upper Bounds 5.90e-01 6.17e-01 5.70e-01 -8.00e-01 +7.99e-01 3.80e-01 1.34e-01 6.68e-01 @@ -6024,7 +6024,7 @@ Upper Bounds 2.51e-01 7.41e-02 6.65e-01 -2.67e-01 +6.54e-01 6.72e-01 6.79e-01 6.77e-01 @@ -6036,11 +6036,11 @@ Upper Bounds 5.40e-01 5.07e-01 5.77e-01 -2.23e-01 +2.24e-01 6.42e-02 8.66e-01 8.00e-01 -8.00e-01 +8.01e-01 8.48e-01 8.27e-01 8.32e-01 @@ -6048,13 +6048,13 @@ Upper Bounds 9.02e-01 9.05e-01 7.29e-01 -6.76e-01 +6.75e-01 5.88e-01 3.48e-01 1.10e-01 3.71e-02 6.49e-01 -3.52e-01 +3.51e-01 4.24e-01 5.24e-01 4.05e-01 @@ -6103,7 +6103,7 @@ Upper Bounds 1.23e+00 1.10e+00 1.10e+00 -1.15e+00 +1.14e+00 1.11e+00 1.03e+00 9.91e-01 @@ -6147,7 +6147,7 @@ Upper Bounds 1.03e+00 1.06e+00 1.10e+00 -1.04e+00 +1.05e+00 1.13e+00 1.27e+00 1.09e+00 @@ -6184,7 +6184,7 @@ Upper Bounds 8.41e-01 9.51e-01 9.08e-01 -7.46e-01 +7.45e-01 7.77e-01 3.05e-01 9.78e-02 @@ -6218,7 +6218,7 @@ Upper Bounds 8.06e-01 2.24e-01 7.49e-02 -9.70e-01 +9.69e-01 1.03e+00 1.10e+00 1.01e+00 @@ -6229,7 +6229,7 @@ Upper Bounds 8.30e-01 8.02e-01 8.09e-01 -8.10e-01 +8.11e-01 7.80e-01 2.46e-01 7.63e-02 @@ -6250,7 +6250,7 @@ Upper Bounds 7.72e-02 7.36e-01 7.62e-01 -8.87e-01 +8.86e-01 8.71e-01 9.21e-01 9.57e-01 @@ -6287,7 +6287,7 @@ Upper Bounds 2.20e-01 2.69e-01 4.39e-01 -2.89e-01 +2.88e-01 1.35e-01 1.30e-01 7.47e-02 @@ -6335,7 +6335,7 @@ Upper Bounds 7.01e-01 6.46e-01 5.40e-01 -4.50e-01 +4.49e-01 2.04e-01 7.38e-02 1.18e+00 @@ -6345,7 +6345,7 @@ Upper Bounds 9.38e-01 1.19e+00 9.74e-01 -1.02e+00 +1.01e+00 9.23e-01 8.23e-01 8.89e-01 @@ -6362,7 +6362,7 @@ Upper Bounds 1.36e+00 1.18e+00 1.09e+00 -8.67e-01 +8.66e-01 6.32e-01 4.57e-01 3.13e-01 @@ -6402,7 +6402,7 @@ Upper Bounds 9.01e-01 1.04e+00 1.05e+00 -9.13e-01 +9.12e-01 1.00e+00 9.12e-01 7.83e-01 @@ -6515,7 +6515,7 @@ Upper Bounds 1.43e-01 8.60e-02 5.91e-02 -4.19e-02 +4.18e-02 2.09e-02 1.01e-02 6.92e-02 @@ -6639,7 +6639,7 @@ Upper Bounds 3.97e-02 3.67e-02 1.54e-01 -2.67e-01 +2.66e-01 2.58e-01 2.66e-01 2.93e-01 diff --git a/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/results_true.dat b/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/results_true.dat index 3afdc56ca..c1066ae82 100644 --- a/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/results_true.dat +++ b/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/results_true.dat @@ -60,7 +60,7 @@ Lower Bounds 1.68e-01 1.70e-01 1.75e-01 -1.73e-01 +1.72e-01 1.73e-01 1.65e-01 1.84e-01 @@ -281,7 +281,7 @@ Lower Bounds 1.86e-01 1.81e-01 1.83e-01 -1.73e-01 +1.74e-01 1.67e-01 1.72e-01 1.75e-01 @@ -722,7 +722,7 @@ Lower Bounds 1.63e-01 1.58e-01 1.70e-01 -1.70e-01 +1.69e-01 1.84e-01 2.44e-01 2.71e-01 @@ -775,7 +775,7 @@ Lower Bounds 1.69e-01 1.67e-01 1.76e-01 -1.59e-01 +1.64e-01 1.59e-01 1.58e-01 1.64e-01 @@ -835,7 +835,7 @@ Lower Bounds 1.76e-01 1.78e-01 1.68e-01 -1.78e-01 +1.79e-01 1.73e-01 1.69e-01 1.62e-01 @@ -850,11 +850,11 @@ Lower Bounds 1.88e-01 1.77e-01 1.78e-01 -8.59e-02 +1.76e-01 1.82e-01 1.90e-01 1.76e-01 -1.63e-01 +1.62e-01 1.69e-01 1.69e-01 1.69e-01 @@ -1201,7 +1201,7 @@ Lower Bounds 1.57e-01 1.58e-01 1.58e-01 -1.57e-01 +1.56e-01 1.58e-01 1.66e-01 2.50e-01 @@ -1223,7 +1223,7 @@ Lower Bounds 2.18e-01 9.03e-02 1.68e-01 -1.70e-01 +1.69e-01 1.64e-01 1.56e-01 1.58e-01 @@ -1382,7 +1382,7 @@ Lower Bounds 1.52e-01 1.59e-01 1.52e-01 -1.49e-01 +1.48e-01 1.55e-01 2.25e-01 2.01e-01 @@ -1478,12 +1478,12 @@ Lower Bounds 1.63e-01 4.89e-02 1.64e-01 -6.61e-02 +1.55e-01 1.57e-01 1.53e-01 1.48e-01 1.53e-01 -7.58e-02 +1.52e-01 1.40e-01 1.38e-01 1.40e-01 @@ -1539,7 +1539,7 @@ Lower Bounds 2.68e-02 2.40e-01 2.25e-01 -2.21e-01 +2.22e-01 2.27e-01 2.31e-01 2.20e-01 @@ -1572,7 +1572,7 @@ Lower Bounds 8.97e-02 4.80e-02 4.87e-02 -6.44e-02 +6.43e-02 6.38e-02 4.70e-02 4.40e-02 @@ -1581,7 +1581,7 @@ Lower Bounds 4.03e-02 1.52e-02 8.66e-03 -6.15e-03 +6.14e-03 1.56e-01 1.57e-01 1.54e-01 @@ -1637,7 +1637,7 @@ Lower Bounds 1.53e-01 1.47e-01 1.42e-01 -1.57e-01 +1.56e-01 1.66e-01 2.10e-01 1.46e-01 @@ -1792,7 +1792,7 @@ Lower Bounds 4.29e-02 1.08e-02 4.27e-03 -7.46e-02 +7.45e-02 7.65e-02 5.07e-02 4.76e-02 @@ -1898,7 +1898,7 @@ Lower Bounds 1.50e-01 5.21e-02 1.53e-01 -1.49e-01 +1.53e-01 1.52e-01 1.45e-01 1.45e-01 @@ -1928,13 +1928,13 @@ Lower Bounds 7.19e-02 2.90e-02 1.48e-01 -1.19e-01 +1.44e-01 1.48e-01 1.48e-01 1.49e-01 1.54e-01 1.45e-01 -1.21e-01 +1.38e-01 1.29e-01 1.30e-01 1.31e-01 @@ -1970,7 +1970,7 @@ Lower Bounds 1.18e-01 1.31e-01 1.83e-01 -9.56e-02 +9.55e-02 2.75e-02 1.63e-01 1.47e-01 @@ -2058,7 +2058,7 @@ Lower Bounds 1.58e-01 1.43e-01 1.49e-01 -1.60e-01 +1.59e-01 2.21e-01 1.56e-01 4.57e-02 @@ -2208,7 +2208,7 @@ Lower Bounds 1.36e-01 1.33e-01 1.26e-01 -1.10e-01 +1.11e-01 1.31e-01 5.72e-02 1.80e-02 @@ -2245,7 +2245,7 @@ Lower Bounds 7.21e-02 6.08e-02 6.40e-02 -5.54e-02 +5.53e-02 6.28e-02 5.59e-02 4.39e-02 @@ -2405,7 +2405,7 @@ Lower Bounds 1.23e-01 1.24e-01 1.55e-01 -8.14e-02 +8.15e-02 2.87e-02 1.44e-01 1.47e-01 @@ -2416,7 +2416,7 @@ Lower Bounds 1.42e-01 1.37e-01 1.27e-01 -1.28e-01 +1.29e-01 1.17e-01 1.21e-01 1.63e-01 @@ -2479,12 +2479,12 @@ Lower Bounds 2.61e-02 2.32e-02 1.25e-02 -9.63e-03 +9.62e-03 6.12e-03 3.33e-03 1.57e-01 1.68e-01 -1.77e-01 +1.76e-01 1.71e-01 1.70e-01 1.76e-01 @@ -2533,7 +2533,7 @@ Lower Bounds 1.65e-01 1.69e-01 1.74e-01 -6.32e-02 +1.78e-01 1.61e-01 1.64e-01 1.57e-01 @@ -2547,8 +2547,8 @@ Lower Bounds 1.61e-01 1.66e-01 1.68e-01 -1.70e-01 -1.63e-01 +1.71e-01 +1.64e-01 1.61e-01 1.62e-01 1.60e-01 @@ -2647,8 +2647,8 @@ Lower Bounds 1.45e-01 4.24e-02 1.27e-02 -1.42e-01 -6.20e-02 +1.43e-01 +1.40e-01 1.44e-01 1.45e-01 1.42e-01 @@ -2660,7 +2660,7 @@ Lower Bounds 1.15e-01 1.07e-01 1.24e-01 -3.83e-02 +3.91e-02 1.19e-02 1.85e-01 1.70e-01 @@ -2678,7 +2678,7 @@ Lower Bounds 2.05e-02 7.17e-03 1.27e-01 -6.47e-02 +6.46e-02 8.27e-02 1.04e-01 7.86e-02 @@ -3043,7 +3043,7 @@ Lower Bounds 1.57e-01 1.80e-01 1.80e-01 -1.24e-01 +1.23e-01 1.22e-01 8.18e-02 7.30e-02 @@ -3055,7 +3055,7 @@ Lower Bounds 1.13e-01 1.37e-01 1.61e-01 -2.21e-01 +2.20e-01 1.71e-01 1.51e-01 1.16e-01 @@ -3187,14 +3187,14 @@ Lower Bounds 2.64e-02 1.76e-02 8.34e-03 -8.72e-02 +8.71e-02 6.82e-02 1.03e-01 9.93e-02 4.68e-02 8.00e-02 6.54e-02 -5.74e-02 +5.73e-02 5.88e-02 4.17e-02 6.05e-02 @@ -3252,7 +3252,7 @@ Lower Bounds 7.34e-02 7.91e-02 4.60e-02 -5.73e-02 +5.72e-02 6.01e-02 5.26e-02 5.01e-02 @@ -3399,7 +3399,7 @@ Upper Bounds 2.14e+00 7.89e-01 8.70e-01 -9.12e-01 +9.11e-01 9.06e-01 8.15e-01 8.92e-01 @@ -3436,7 +3436,7 @@ Upper Bounds 8.38e-01 8.52e-01 8.74e-01 -8.63e-01 +8.62e-01 8.63e-01 8.27e-01 9.20e-01 @@ -3509,8 +3509,8 @@ Upper Bounds 9.09e-01 8.56e-01 8.36e-01 -8.27e-01 -8.09e-01 +8.26e-01 +8.10e-01 7.69e-01 7.73e-01 7.71e-01 @@ -3642,14 +3642,14 @@ Upper Bounds 8.80e-01 8.68e-01 8.83e-01 -8.81e-01 +8.82e-01 8.83e-01 8.68e-01 8.89e-01 8.61e-01 9.00e-01 9.04e-01 -9.07e-01 +9.06e-01 1.33e+00 1.66e+00 5.75e-01 @@ -3657,7 +3657,7 @@ Upper Bounds 9.28e-01 9.04e-01 9.15e-01 -8.67e-01 +8.68e-01 8.37e-01 8.58e-01 8.74e-01 @@ -3673,7 +3673,7 @@ Upper Bounds 8.97e-01 8.66e-01 8.29e-01 -8.49e-01 +8.48e-01 8.85e-01 8.47e-01 8.39e-01 @@ -3734,7 +3734,7 @@ Upper Bounds 8.72e-01 8.86e-01 8.25e-01 -8.06e-01 +8.05e-01 7.70e-01 7.66e-01 7.77e-01 @@ -4084,7 +4084,7 @@ Upper Bounds 8.23e-01 8.70e-01 8.45e-01 -8.81e-01 +8.80e-01 1.24e+00 1.66e+00 6.51e-01 @@ -4098,7 +4098,7 @@ Upper Bounds 8.14e-01 7.91e-01 8.49e-01 -8.48e-01 +8.47e-01 9.21e-01 1.22e+00 1.35e+00 @@ -4124,7 +4124,7 @@ Upper Bounds 8.56e-01 8.12e-01 7.90e-01 -8.10e-01 +8.09e-01 7.88e-01 8.08e-01 8.07e-01 @@ -4141,7 +4141,7 @@ Upper Bounds 7.98e-01 8.23e-01 8.40e-01 -8.12e-01 +8.11e-01 8.01e-01 8.15e-01 8.78e-01 @@ -4149,14 +4149,14 @@ Upper Bounds 1.49e+00 5.48e-01 8.43e-01 -8.37e-01 +8.36e-01 8.81e-01 -7.97e-01 +8.18e-01 7.95e-01 7.90e-01 8.20e-01 7.94e-01 -7.75e-01 +7.74e-01 7.96e-01 7.56e-01 8.00e-01 @@ -4176,10 +4176,10 @@ Upper Bounds 7.46e-01 7.89e-01 1.17e+00 -9.25e-01 +9.24e-01 3.31e-01 8.47e-01 -8.26e-01 +8.25e-01 8.14e-01 8.34e-01 8.09e-01 @@ -4211,7 +4211,7 @@ Upper Bounds 8.80e-01 8.88e-01 8.39e-01 -8.92e-01 +8.93e-01 8.67e-01 8.43e-01 8.12e-01 @@ -4226,11 +4226,11 @@ Upper Bounds 9.42e-01 8.84e-01 8.89e-01 -4.30e-01 +8.81e-01 9.12e-01 9.52e-01 8.80e-01 -8.13e-01 +8.12e-01 8.44e-01 8.47e-01 8.47e-01 @@ -4284,7 +4284,7 @@ Upper Bounds 1.28e-01 6.06e-02 8.53e-01 -8.42e-01 +8.41e-01 8.16e-01 8.39e-01 8.62e-01 @@ -4293,7 +4293,7 @@ Upper Bounds 7.57e-01 7.84e-01 8.17e-01 -7.86e-01 +7.85e-01 8.91e-01 1.29e+00 1.58e+00 @@ -4384,7 +4384,7 @@ Upper Bounds 7.51e-01 7.69e-01 7.83e-01 -7.95e-01 +7.94e-01 1.09e+00 1.03e+00 3.79e-01 @@ -4439,7 +4439,7 @@ Upper Bounds 8.29e-01 8.34e-01 8.10e-01 -8.16e-01 +8.15e-01 8.13e-01 7.91e-01 7.58e-01 @@ -4500,7 +4500,7 @@ Upper Bounds 5.38e-01 4.35e-01 5.87e-01 -2.74e-01 +2.73e-01 3.52e-01 2.53e-01 1.54e-01 @@ -4577,7 +4577,7 @@ Upper Bounds 7.84e-01 7.91e-01 7.91e-01 -7.83e-01 +7.82e-01 7.90e-01 8.30e-01 1.25e+00 @@ -4590,7 +4590,7 @@ Upper Bounds 7.98e-01 7.82e-01 7.55e-01 -7.52e-01 +7.51e-01 8.14e-01 8.19e-01 7.83e-01 @@ -4599,7 +4599,7 @@ Upper Bounds 1.09e+00 4.51e-01 8.40e-01 -8.48e-01 +8.47e-01 8.18e-01 7.81e-01 7.89e-01 @@ -4623,7 +4623,7 @@ Upper Bounds 7.25e-01 7.18e-01 7.55e-01 -7.87e-01 +7.86e-01 8.22e-01 1.16e+00 5.88e-01 @@ -4658,7 +4658,7 @@ Upper Bounds 1.05e+00 6.00e-01 1.91e-01 -8.27e-01 +8.26e-01 7.94e-01 8.07e-01 8.05e-01 @@ -4758,7 +4758,7 @@ Upper Bounds 7.61e-01 7.94e-01 7.58e-01 -7.43e-01 +7.42e-01 7.76e-01 1.12e+00 1.01e+00 @@ -4784,12 +4784,12 @@ Upper Bounds 7.60e-01 7.73e-01 8.04e-01 -7.87e-01 +7.86e-01 7.68e-01 7.69e-01 7.72e-01 7.79e-01 -8.20e-01 +8.19e-01 1.12e+00 7.80e-01 3.07e-01 @@ -4817,7 +4817,7 @@ Upper Bounds 7.52e-01 7.32e-01 7.17e-01 -7.61e-01 +7.60e-01 7.74e-01 8.04e-01 1.13e+00 @@ -4854,12 +4854,12 @@ Upper Bounds 8.15e-01 2.44e-01 8.21e-01 -3.30e-01 +7.75e-01 7.87e-01 7.66e-01 7.39e-01 7.65e-01 -3.79e-01 +7.62e-01 6.99e-01 6.88e-01 6.98e-01 @@ -4962,10 +4962,10 @@ Upper Bounds 7.87e-01 7.72e-01 8.00e-01 -8.25e-01 -8.31e-01 +8.24e-01 +8.30e-01 8.27e-01 -7.89e-01 +7.88e-01 7.35e-01 7.02e-01 6.79e-01 @@ -4991,13 +4991,13 @@ Upper Bounds 7.91e-01 7.84e-01 8.12e-01 -8.14e-01 +8.13e-01 7.93e-01 7.86e-01 7.80e-01 7.88e-01 7.62e-01 -7.26e-01 +7.25e-01 7.48e-01 8.00e-01 1.08e+00 @@ -5013,7 +5013,7 @@ Upper Bounds 7.67e-01 7.34e-01 7.08e-01 -7.83e-01 +7.82e-01 8.28e-01 1.05e+00 7.28e-01 @@ -5043,7 +5043,7 @@ Upper Bounds 7.26e-01 7.36e-01 7.31e-01 -7.41e-01 +7.40e-01 7.92e-01 1.07e+00 6.18e-01 @@ -5089,7 +5089,7 @@ Upper Bounds 6.75e-01 6.66e-01 7.38e-01 -7.32e-01 +7.31e-01 9.88e-01 5.41e-01 1.68e-01 @@ -5219,7 +5219,7 @@ Upper Bounds 7.96e-01 8.02e-01 7.57e-01 -7.80e-01 +7.79e-01 7.99e-01 7.43e-01 7.26e-01 @@ -5251,7 +5251,7 @@ Upper Bounds 7.54e-01 7.53e-01 7.66e-01 -7.41e-01 +7.40e-01 7.01e-01 7.50e-01 8.07e-01 @@ -5274,9 +5274,9 @@ Upper Bounds 7.50e-01 2.61e-01 7.64e-01 -7.45e-01 +7.65e-01 7.60e-01 -7.26e-01 +7.25e-01 7.24e-01 7.17e-01 6.99e-01 @@ -5294,7 +5294,7 @@ Upper Bounds 7.14e-01 7.38e-01 7.21e-01 -6.81e-01 +6.80e-01 6.69e-01 6.84e-01 6.84e-01 @@ -5304,13 +5304,13 @@ Upper Bounds 3.59e-01 1.45e-01 7.38e-01 -5.93e-01 +7.18e-01 7.38e-01 7.39e-01 7.46e-01 7.70e-01 7.25e-01 -6.07e-01 +6.90e-01 6.46e-01 6.49e-01 6.56e-01 @@ -5318,7 +5318,7 @@ Upper Bounds 9.65e-01 3.98e-01 1.02e-01 -7.63e-01 +7.64e-01 7.31e-01 7.32e-01 7.05e-01 @@ -5369,7 +5369,7 @@ Upper Bounds 1.11e+00 1.11e+00 1.08e+00 -9.96e-01 +9.95e-01 1.02e+00 9.59e-01 1.01e+00 @@ -5434,7 +5434,7 @@ Upper Bounds 7.89e-01 7.16e-01 7.47e-01 -7.98e-01 +7.97e-01 1.11e+00 7.82e-01 2.29e-01 @@ -5447,7 +5447,7 @@ Upper Bounds 7.75e-01 7.99e-01 8.00e-01 -7.27e-01 +7.26e-01 7.00e-01 7.95e-01 1.12e+00 @@ -5584,7 +5584,7 @@ Upper Bounds 6.82e-01 6.66e-01 6.30e-01 -5.52e-01 +5.53e-01 6.56e-01 2.86e-01 9.02e-02 @@ -5603,7 +5603,7 @@ Upper Bounds 5.90e-01 1.70e-01 6.04e-02 -9.35e-01 +9.34e-01 7.67e-01 7.48e-01 8.36e-01 @@ -5688,7 +5688,7 @@ Upper Bounds 7.74e-01 7.69e-01 7.48e-01 -7.27e-01 +7.26e-01 7.70e-01 1.04e+00 5.38e-01 @@ -5698,7 +5698,7 @@ Upper Bounds 7.98e-01 7.72e-01 8.10e-01 -7.95e-01 +7.96e-01 7.80e-01 7.69e-01 7.86e-01 @@ -5776,7 +5776,7 @@ Upper Bounds 7.07e-01 6.87e-01 6.74e-01 -6.34e-01 +6.35e-01 6.34e-01 6.14e-01 6.22e-01 @@ -5792,10 +5792,10 @@ Upper Bounds 7.08e-01 6.86e-01 6.37e-01 -6.42e-01 +6.43e-01 5.84e-01 6.05e-01 -8.13e-01 +8.14e-01 2.50e-01 7.65e-02 7.35e-01 @@ -5805,7 +5805,7 @@ Upper Bounds 7.24e-01 7.52e-01 7.60e-01 -7.36e-01 +7.35e-01 6.72e-01 6.44e-01 6.12e-01 @@ -5860,7 +5860,7 @@ Upper Bounds 1.67e-02 7.86e-01 8.39e-01 -8.83e-01 +8.82e-01 8.55e-01 8.50e-01 8.79e-01 @@ -5901,7 +5901,7 @@ Upper Bounds 7.49e-01 8.31e-01 1.07e+00 -4.89e-01 +4.88e-01 1.30e-01 8.26e-01 8.49e-01 @@ -5909,7 +5909,7 @@ Upper Bounds 8.27e-01 8.44e-01 8.70e-01 -3.16e-01 +8.88e-01 8.04e-01 8.19e-01 7.85e-01 @@ -5923,8 +5923,8 @@ Upper Bounds 8.03e-01 8.28e-01 8.39e-01 -8.52e-01 -8.17e-01 +8.53e-01 +8.18e-01 8.07e-01 8.12e-01 8.02e-01 @@ -6023,8 +6023,8 @@ Upper Bounds 7.24e-01 2.12e-01 6.37e-02 -7.12e-01 -3.10e-01 +7.13e-01 +6.99e-01 7.21e-01 7.24e-01 7.11e-01 @@ -6036,7 +6036,7 @@ Upper Bounds 5.74e-01 5.37e-01 6.18e-01 -1.92e-01 +1.95e-01 5.93e-02 9.23e-01 8.49e-01 @@ -6053,8 +6053,8 @@ Upper Bounds 3.58e-01 1.02e-01 3.58e-02 -6.34e-01 -3.24e-01 +6.33e-01 +3.23e-01 4.14e-01 5.18e-01 3.93e-01 @@ -6067,7 +6067,7 @@ Upper Bounds 2.08e-01 1.27e-01 3.00e-02 -9.84e-03 +9.83e-03 2.53e-01 1.24e-01 1.72e-01 @@ -6211,7 +6211,7 @@ Upper Bounds 1.03e+00 1.15e+00 1.11e+00 -9.64e-01 +9.63e-01 1.01e+00 9.05e-01 8.67e-01 @@ -6229,8 +6229,8 @@ Upper Bounds 8.93e-01 8.70e-01 8.58e-01 -8.56e-01 -8.00e-01 +8.57e-01 +8.01e-01 2.31e-01 7.13e-02 9.12e-01 @@ -6249,7 +6249,7 @@ Upper Bounds 2.01e-01 7.22e-02 7.80e-01 -8.00e-01 +7.99e-01 9.64e-01 9.33e-01 9.70e-01 @@ -6277,7 +6277,7 @@ Upper Bounds 5.08e-01 3.32e-01 9.24e-02 -4.15e-02 +4.14e-02 3.42e-01 2.04e-01 2.56e-01 @@ -6414,12 +6414,12 @@ Upper Bounds 1.17e-01 7.24e-02 5.31e-01 -7.17e-01 +7.16e-01 8.03e-01 7.87e-01 9.01e-01 9.00e-01 -6.18e-01 +6.17e-01 6.08e-01 4.09e-01 3.65e-01 @@ -6545,7 +6545,7 @@ Upper Bounds 2.87e-01 1.88e-01 1.85e-01 -1.80e-01 +1.79e-01 1.07e-01 3.77e-02 5.65e-01 @@ -6578,13 +6578,13 @@ Upper Bounds 1.41e-01 5.88e-02 6.26e-02 -4.47e-01 +4.46e-01 3.47e-01 5.27e-01 6.11e-01 3.91e-01 4.22e-01 -4.77e-01 +4.76e-01 4.54e-01 4.31e-01 3.19e-01 @@ -6727,14 +6727,14 @@ Upper Bounds 5.73e-02 5.81e-02 1.92e-02 -6.10e-03 +6.09e-03 2.72e-02 4.77e-02 4.74e-02 3.34e-02 5.41e-02 7.93e-02 -6.70e-02 +6.69e-02 7.67e-02 9.31e-02 5.17e-02 From f796fa04e06a074163f9071436724a06fd691931 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Tue, 10 Jun 2025 09:01:28 -0600 Subject: [PATCH 336/671] Adding fix and tests for spherical mesh as spatial distribution (#3428) Co-authored-by: Paul Wilson Co-authored-by: Paul Romano --- openmc/mesh.py | 126 +++++++++++++++++----- src/mesh.cpp | 3 +- tests/unit_tests/test_mesh_from_domain.py | 32 +++++- tests/unit_tests/test_source_mesh.py | 45 ++++++++ 4 files changed, 176 insertions(+), 30 deletions(-) diff --git a/openmc/mesh.py b/openmc/mesh.py index 27187b5dd..b046769fd 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -5,6 +5,7 @@ from collections.abc import Iterable, Sequence, Mapping from functools import wraps from math import pi, sqrt, atan2 from numbers import Integral, Real +from typing import Protocol import h5py import lxml.etree as ET @@ -842,6 +843,11 @@ class StructuredMesh(MeshBase): ) +class HasBoundingBox(Protocol): + """Object that has a ``bounding_box`` attribute.""" + bounding_box: openmc.BoundingBox + + class RegularMesh(StructuredMesh): """A regular Cartesian mesh in one, two, or three dimensions @@ -1088,17 +1094,16 @@ class RegularMesh(StructuredMesh): @classmethod def from_domain( cls, - domain: 'openmc.Cell' | 'openmc.Region' | 'openmc.Universe' | 'openmc.Geometry', + domain: HasBoundingBox, dimension: Sequence[int] = (10, 10, 10), mesh_id: int | None = None, name: str = '' ): - """Create mesh from an existing openmc cell, region, universe or - geometry by making use of the objects bounding box property. + """Create RegularMesh from a domain using its bounding box. Parameters ---------- - domain : {openmc.Cell, openmc.Region, openmc.Universe, openmc.Geometry} + domain : HasBoundingBox 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 @@ -1115,11 +1120,8 @@ class RegularMesh(StructuredMesh): RegularMesh instance """ - cv.check_type( - "domain", - domain, - (openmc.Cell, openmc.Region, openmc.Universe, openmc.Geometry), - ) + if not hasattr(domain, 'bounding_box'): + raise TypeError("Domain must have a bounding_box property") mesh = cls(mesh_id=mesh_id, name=name) mesh.lower_left = domain.bounding_box[0] @@ -1787,17 +1789,18 @@ class CylindricalMesh(StructuredMesh): @classmethod def from_domain( cls, - domain: 'openmc.Cell' | 'openmc.Region' | 'openmc.Universe' | 'openmc.Geometry', + domain: HasBoundingBox, dimension: Sequence[int] = (10, 10, 10), mesh_id: int | None = None, phi_grid_bounds: Sequence[float] = (0.0, 2*pi), - name: str = '' + name: str = '', + enclose_domain: bool = False ): - """Creates a regular CylindricalMesh from an existing openmc domain. + """Create CylindricalMesh from a domain using its bounding box. Parameters ---------- - domain : openmc.Cell or openmc.Region or openmc.Universe or openmc.Geometry + domain : HasBoundingBox 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. @@ -1811,6 +1814,9 @@ class CylindricalMesh(StructuredMesh): is (0, 2π), i.e., the full phi range. name : str Name of the mesh + enclose_domain : bool + If True, the mesh will encompass the bounding box of the domain. If + False, the mesh will be inscribed within the domain's bounding box. Returns ------- @@ -1818,25 +1824,20 @@ class CylindricalMesh(StructuredMesh): CylindricalMesh instance """ - cv.check_type( - "domain", - domain, - (openmc.Cell, openmc.Region, openmc.Universe, openmc.Geometry), - ) + 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 - max_bounding_box_radius = max( - [ - cached_bb[0][0], - cached_bb[0][1], - cached_bb[1][0], - cached_bb[1][1], - ] - ) + + if enclose_domain: + outer_radius = 0.5 * np.linalg.norm(cached_bb.width[:2]) + else: + outer_radius = 0.5 * min(cached_bb.width[:2]) + r_grid = np.linspace( 0, - max_bounding_box_radius, + outer_radius, num=dimension[0]+1 ) phi_grid = np.linspace( @@ -2167,6 +2168,77 @@ class SphericalMesh(StructuredMesh): return mesh + @classmethod + def from_domain( + cls, + domain: HasBoundingBox, + dimension: Sequence[int] = (10, 10, 10), + mesh_id: int | None = None, + phi_grid_bounds: Sequence[float] = (0.0, 2*pi), + theta_grid_bounds: Sequence[float] = (0.0, pi), + name: str = '', + enclose_domain: bool = False + ): + """Create SphericalMesh from a domain using its bounding box. + + Parameters + ---------- + domain : HasBoundingBox + 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. + 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 + phi and theta, and in absolute space for r. + mesh_id : int + Unique identifier for the mesh + phi_grid_bounds : numpy.ndarray + Mesh bounds points along the phi-axis in radians. The default value + is (0, 2π), i.e., the full phi range. + theta_grid_bounds : numpy.ndarray + Mesh bounds points along the theta-axis in radians. The default value + is (0, π), i.e., the full theta range. + name : str + Name of the mesh + enclose_domain : bool + If True, the mesh will encompass the bounding box of the domain. If + False, the mesh will be inscribed within the domain's bounding box. + + Returns + ------- + openmc.SphericalMesh + 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 enclose_domain: + outer_radius = 0.5 * np.linalg.norm(cached_bb.width) + else: + outer_radius = 0.5 * min(cached_bb.width) + + r_grid = np.linspace(0, outer_radius, num=dimension[0] + 1) + theta_grid = np.linspace( + theta_grid_bounds[0], + theta_grid_bounds[1], + num=dimension[1]+1 + ) + phi_grid = np.linspace( + phi_grid_bounds[0], + phi_grid_bounds[1], + num=dimension[2]+1 + ) + origin = np.array([ + cached_bb.center[0], cached_bb.center[1], cached_bb.center[2]]) + + return cls(r_grid=r_grid, phi_grid=phi_grid, theta_grid=theta_grid, + origin=origin, mesh_id=mesh_id, name=name) + def to_xml_element(self): """Return XML representation of the mesh diff --git a/src/mesh.cpp b/src/mesh.cpp index 8280177ac..d61058525 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -1794,7 +1794,8 @@ Position SphericalMesh::sample_element( double phi_min = this->phi(ijk[2] - 1); double phi_max = this->phi(ijk[2]); - double cos_theta = uniform_distribution(theta_min, theta_max, seed); + double cos_theta = + uniform_distribution(std::cos(theta_min), std::cos(theta_max), seed); double sin_theta = std::sin(std::acos(cos_theta)); double phi = uniform_distribution(phi_min, phi_max, seed); double r_min_cub = std::pow(r_min, 3); diff --git a/tests/unit_tests/test_mesh_from_domain.py b/tests/unit_tests/test_mesh_from_domain.py index 0cbe413e8..0e3858913 100644 --- a/tests/unit_tests/test_mesh_from_domain.py +++ b/tests/unit_tests/test_mesh_from_domain.py @@ -40,7 +40,7 @@ def test_cylindrical_mesh_from_cell(): assert isinstance(mesh, openmc.CylindricalMesh) assert np.array_equal(mesh.dimension, (1, 1, 1)) - assert np.array_equal(mesh.r_grid, [0., 150.]) + assert np.array_equal(mesh.r_grid, [0., 50.]) assert np.array_equal(mesh.origin, [100., 0., 10.]) # Cell is not centralized on Z, X or Y axis @@ -49,7 +49,7 @@ def test_cylindrical_mesh_from_cell(): mesh = openmc.CylindricalMesh.from_domain(domain=cell, dimension=[1, 1, 1]) assert isinstance(mesh, openmc.CylindricalMesh) - assert np.array_equal(mesh.r_grid, [0., 220.]) + assert np.array_equal(mesh.r_grid, [0., 50.]) assert np.array_equal(mesh.origin, [100., 170., 10.]) @@ -87,6 +87,34 @@ def test_cylindrical_mesh_from_region(): assert np.array_equal(mesh.origin, (0.0, 0.0, -30.)) +def test_spherical_mesh_from_domain(): + """Tests a SphericalMesh can be made from a Region and the specified + dimensions are propagated through. Cell is not centralized""" + sphere = openmc.Sphere(r=5, x0=2, y0=3, z0=4) + region = -sphere + + geometry = openmc.Geometry(openmc.Universe(cells=[openmc.Cell(region=region)])) + + region_mesh = openmc.SphericalMesh.from_domain( + domain=region, dimension=(4, 3, 4)) + universe_mesh = openmc.SphericalMesh.from_domain( + domain=geometry.root_universe, dimension=(4, 3, 4)) + geometry_mesh = openmc.SphericalMesh.from_domain( + domain=geometry, dimension=(4, 3, 4)) + + + for mesh in (region_mesh, universe_mesh, geometry_mesh): + assert isinstance(mesh, openmc.SphericalMesh) + assert np.array_equal(mesh.dimension, (4, 3, 4)) + assert np.array_equal(mesh.r_grid, [0., 1.25, 2.5, 3.75, 5.0]) + assert np.array_equal(mesh.theta_grid, [0., np.pi/3., 2*np.pi/3., np.pi]) + assert np.array_equal(mesh.phi_grid, [0., np.pi/2., np.pi, 3*np.pi/2., 2*np.pi]) + assert np.array_equal(mesh.origin, (2.0, 3.0, 4.0)) + + for p in mesh.centroids.reshape(-1, 3): + assert p in mesh.bounding_box + + def test_reg_mesh_from_universe(): """Tests a RegularMesh can be made from a Universe and the default dimensions are propagated through. Universe is centralized""" diff --git a/tests/unit_tests/test_source_mesh.py b/tests/unit_tests/test_source_mesh.py index 43bb1678c..d023c1335 100644 --- a/tests/unit_tests/test_source_mesh.py +++ b/tests/unit_tests/test_source_mesh.py @@ -396,3 +396,48 @@ def test_mesh_source_file(run_in_tmpdir): assert site.u == source_particle.u assert site.time == source_particle.time assert site.r in bbox + + +@pytest.mark.parametrize("mesh_type", ('rectangular', 'cylindrical', 'spherical')) +def test_mesh_spatial(run_in_tmpdir, mesh_type): + """Test that a spherical mesh source works as expected.""" + model = openmc.Model() + + # Set up geometry, a box that is shifted in x, y, and z + box = openmc.model.RectangularParallelepiped(5.0, 25.0, -20.0, 20.0, -30.0, 30.0, boundary_type='vacuum') + mat = openmc.Material() + mat.add_nuclide('H1', 1.0) + model.geometry = openmc.Geometry([openmc.Cell(fill=mat, region=-box)]) + + # Create a mesh of each type in turn + if mesh_type == 'rectangular': + mesh = openmc.RegularMesh.from_domain(model.geometry, (10, 2, 2)) + elif mesh_type == 'cylindrical': + mesh = openmc.CylindricalMesh.from_domain(model.geometry, (10, 2, 2)) + assert max(mesh.r_grid) == 10.0, "Cylindrical mesh radius exceeds geometry bounds" + assert mesh.origin[0] == 15.0, "Cylindrical mesh origin x-coordinate is incorrect" + elif mesh_type == 'spherical': + mesh = openmc.SphericalMesh.from_domain(model.geometry, (10, 2, 2)) + assert max(mesh.r_grid) == 10.0, "Spherical mesh radius exceeds geometry bounds" + assert mesh.origin[0] == 15.0, "Spherical mesh origin x-coordinate is incorrect" + + # Create a mesh source with a single particle + ind_source = openmc.IndependentSource(space=openmc.stats.MeshSpatial(mesh, np.prod(mesh.dimension)*[1.0])) + model.settings.source = ind_source + + model.settings.particles = 100 + model.settings.batches = 10 + model.settings.run_mode = 'fixed source' + + model.export_to_model_xml() + + openmc.lib.init() + openmc.lib.simulation_init() + sites = openmc.lib.sample_external_source(10) + openmc.lib.simulation_finalize() + openmc.lib.finalize() + + # Check that the sites are within the spherical mesh bounds + bbox = mesh.bounding_box + for site in sites: + assert site.r in bbox From f571be87c5e46587ce898dd5fa944512283b6ea3 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 10 Jun 2025 22:51:51 -0500 Subject: [PATCH 337/671] Add user setting for source rejection fraction (#3433) --- docs/source/io_formats/settings.rst | 12 ++++++- include/openmc/settings.h | 2 ++ include/openmc/source.h | 5 ++- openmc/settings.py | 28 ++++++++++++++++ src/error.cpp | 25 ++++++++------- src/finalize.cpp | 1 + src/settings.cpp | 7 ++++ src/source.cpp | 50 +++++++++++++++-------------- tests/unit_tests/test_settings.py | 5 +-- tests/unit_tests/test_source.py | 33 +++++++++++++------ 10 files changed, 118 insertions(+), 50 deletions(-) diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index cef9bf911..26673faac 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -487,7 +487,7 @@ found in the :ref:`random ray user guide `. :type: The type of the domain. Can be ``material``, ``cell``, or ``universe``. - + :diagonal_stabilization_rho: The rho factor for use with diagonal stabilization. This technique is applied when negative diagonal (in-group) elements are detected in @@ -917,6 +917,16 @@ variable and whose sub-elements/attributes are as follows: :dist: This sub-element of a ``pair`` element provides information on the corresponding univariate distribution. +--------------------------------------- +```` Element +--------------------------------------- + +The ```` element specifies the minimum fraction of +external source sites that must be accepted when applying rejection sampling +based on constraints. + + *Default*: 0.05 + ------------------------- ```` Element ------------------------- diff --git a/include/openmc/settings.h b/include/openmc/settings.h index 9017b2d08..069d94c3b 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -145,6 +145,8 @@ extern std::unordered_set statepoint_batch; //!< Batches when state should be written extern std::unordered_set source_write_surf_id; //!< Surface ids where sources will be written +extern double source_rejection_fraction; //!< Minimum fraction of source sites + //!< that must be accepted extern int max_history_splits; //!< maximum number of particle splits for weight windows diff --git a/include/openmc/source.h b/include/openmc/source.h index 6733eaeff..e82c42269 100644 --- a/include/openmc/source.h +++ b/include/openmc/source.h @@ -21,10 +21,9 @@ namespace openmc { // Constants //============================================================================== -// Maximum number of external source spatial resamples to encounter before an -// error is thrown. +// Minimum number of external source sites rejected before checking againts the +// source_rejection_fraction constexpr int EXTSRC_REJECT_THRESHOLD {10000}; -constexpr double EXTSRC_REJECT_FRACTION {0.05}; //============================================================================== // Global variables diff --git a/openmc/settings.py b/openmc/settings.py index 4cacefbed..bd3a89e11 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -222,6 +222,10 @@ class Settings: Number of random numbers allocated for each source particle history source : Iterable of openmc.SourceBase Distribution of source sites in space, angle, and energy + source_rejection_fraction : float + Minimum fraction of source sites that must be accepted when applying + rejection sampling based on constraints. If not specified, the default + value is 0.05. sourcepoint : dict Options for writing source points. Acceptable keys are: @@ -357,6 +361,7 @@ class Settings: # Source subelement self._source = cv.CheckedList(SourceBase, 'source distributions') + self._source_rejection_fraction = None self._confidence_intervals = None self._electron_treatment = None @@ -1224,6 +1229,17 @@ class Settings: cv.check_type('use decay photons', value, bool) self._use_decay_photons = value + @property + def source_rejection_fraction(self) -> float: + return self._source_rejection_fraction + + @source_rejection_fraction.setter + def source_rejection_fraction(self, source_rejection_fraction: float): + cv.check_type('source_rejection_fraction', source_rejection_fraction, Real) + cv.check_greater_than('source_rejection_fraction', source_rejection_fraction, 0) + cv.check_less_than('source_rejection_fraction', source_rejection_fraction, 1) + self._source_rejection_fraction = source_rejection_fraction + def _create_run_mode_subelement(self, root): elem = ET.SubElement(root, "run_mode") elem.text = self._run_mode.value @@ -1685,6 +1701,11 @@ class Settings: subelement = ET.SubElement(element, key) subelement.text = str(value) + def _create_source_rejection_fraction_subelement(self, root): + if self._source_rejection_fraction is not None: + element = ET.SubElement(root, "source_rejection_fraction") + element.text = str(self._source_rejection_fraction) + def _eigenvalue_from_xml_element(self, root): elem = root.find('eigenvalue') if elem is not None: @@ -2104,6 +2125,11 @@ class Settings: if text is not None: self.use_decay_photons = text in ('true', '1') + def _source_rejection_fraction_from_xml_element(self, root): + text = get_text(root, 'source_rejection_fraction') + if text is not None: + self.source_rejection_fraction = float(text) + def to_xml_element(self, mesh_memo=None): """Create a 'settings' element to be written to an XML file. @@ -2171,6 +2197,7 @@ class Settings: self._create_max_tracks_subelement(element) self._create_random_ray_subelement(element, mesh_memo) self._create_use_decay_photons_subelement(element) + self._create_source_rejection_fraction_subelement(element) # Clean the indentation in the file to be user-readable clean_indentation(element) @@ -2279,6 +2306,7 @@ class Settings: settings._max_tracks_from_xml_element(elem) settings._random_ray_from_xml_element(elem) settings._use_decay_photons_from_xml_element(elem) + settings._source_rejection_fraction_from_xml_element(elem) return settings diff --git a/src/error.cpp b/src/error.cpp index 566950a97..f99f5935f 100644 --- a/src/error.cpp +++ b/src/error.cpp @@ -110,23 +110,26 @@ void write_message(const std::string& message, int level) void fatal_error(const std::string& message, int err) { +#pragma omp critical(FatalError) + { #ifdef _POSIX_VERSION - // Make output red if user is in a terminal - if (isatty(STDERR_FILENO)) { - std::cerr << "\033[0;31m"; - } + // Make output red if user is in a terminal + if (isatty(STDERR_FILENO)) { + std::cerr << "\033[0;31m"; + } #endif - // Write error message - std::cerr << " ERROR: "; - output(message, std::cerr, 8); + // Write error message + std::cerr << " ERROR: "; + output(message, std::cerr, 8); #ifdef _POSIX_VERSION - // Reset color for terminal - if (isatty(STDERR_FILENO)) { - std::cerr << "\033[0m"; - } + // Reset color for terminal + if (isatty(STDERR_FILENO)) { + std::cerr << "\033[0m"; + } #endif + } #ifdef OPENMC_MPI MPI_Abort(mpi::intracomm, err); diff --git a/src/finalize.cpp b/src/finalize.cpp index 54aa1d1d9..9cbebc878 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -119,6 +119,7 @@ int openmc_finalize() settings::run_CE = true; settings::run_mode = RunMode::UNSET; settings::source_latest = false; + settings::source_rejection_fraction = 0.05; settings::source_separate = false; settings::source_write = true; settings::ssw_cell_id = C_NONE; diff --git a/src/settings.cpp b/src/settings.cpp index c8230a10f..9afe74361 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -124,6 +124,7 @@ RunMode run_mode {RunMode::UNSET}; SolverType solver_type {SolverType::MONTE_CARLO}; std::unordered_set sourcepoint_batch; std::unordered_set statepoint_batch; +double source_rejection_fraction {0.05}; std::unordered_set source_write_surf_id; int64_t ssw_max_particles; int64_t ssw_max_files; @@ -644,6 +645,12 @@ void read_settings_xml(pugi::xml_node root) write_initial_source = get_node_value_bool(root, "write_initial_source"); } + // Get relative number of lost particles + if (check_for_node(root, "source_rejection_fraction")) { + source_rejection_fraction = + std::stod(get_node_value(root, "source_rejection_fraction")); + } + // Survival biasing if (check_for_node(root, "survival_biasing")) { survival_biasing = get_node_value_bool(root, "survival_biasing"); diff --git a/src/source.cpp b/src/source.cpp index 8e6eb4f11..6ee0b5603 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -157,11 +157,28 @@ void Source::read_constraints(pugi::xml_node node) } } +void check_rejection_fraction(int64_t n_reject, int64_t n_accept) +{ + // Don't check unless we've hit a minimum number of total sites rejected + if (n_reject < EXTSRC_REJECT_THRESHOLD) + return; + + // Compute fraction of accepted sites and compare against minimum + double fraction = static_cast(n_accept) / n_reject; + if (fraction <= settings::source_rejection_fraction) { + fatal_error(fmt::format( + "Too few source sites satisfied the constraints (minimum source " + "rejection fraction = {}). Please check your source definition or " + "set a lower value of Settings.source_rejection_fraction.", + settings::source_rejection_fraction)); + } +} + SourceSite Source::sample_with_constraints(uint64_t* seed) const { bool accepted = false; - static int n_reject = 0; - static int n_accept = 0; + static int64_t n_reject = 0; + static int64_t n_accept = 0; SourceSite site; while (!accepted) { @@ -176,13 +193,9 @@ 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; - if (n_reject >= EXTSRC_REJECT_THRESHOLD && - static_cast(n_accept) / n_reject <= - EXTSRC_REJECT_FRACTION) { - fatal_error("More than 95% of external source sites sampled were " - "rejected. Please check your source definition."); - } + check_rejection_fraction(n_reject, n_accept); // For the "kill" strategy, accept particle but set weight to 0 so that // it is terminated immediately @@ -337,8 +350,8 @@ SourceSite IndependentSource::sample(uint64_t* seed) const // Repeat sampling source location until a good site has been accepted bool accepted = false; - static int n_reject = 0; - static int n_accept = 0; + static int64_t n_reject = 0; + static int64_t n_accept = 0; while (!accepted) { @@ -351,12 +364,7 @@ SourceSite IndependentSource::sample(uint64_t* seed) const // Check for rejection if (!accepted) { ++n_reject; - if (n_reject >= EXTSRC_REJECT_THRESHOLD && - static_cast(n_accept) / n_reject <= EXTSRC_REJECT_FRACTION) { - fatal_error("More than 95% of external source sites sampled were " - "rejected. Please check your external source's spatial " - "definition."); - } + check_rejection_fraction(n_reject, n_accept); } } @@ -381,18 +389,12 @@ SourceSite IndependentSource::sample(uint64_t* seed) const site.E = energy_->sample(seed); // Resample if energy falls above maximum particle energy - if (site.E < data::energy_max[p] and + if (site.E < data::energy_max[p] && (satisfies_energy_constraints(site.E))) break; n_reject++; - if (n_reject >= EXTSRC_REJECT_THRESHOLD && - static_cast(n_accept) / n_reject <= EXTSRC_REJECT_FRACTION) { - fatal_error( - "More than 95% of external source sites sampled were " - "rejected. Please check your external source energy spectrum " - "definition."); - } + check_rejection_fraction(n_reject, n_accept); } // Sample particle creation time diff --git a/tests/unit_tests/test_settings.py b/tests/unit_tests/test_settings.py index 397d70434..7f202bcdc 100644 --- a/tests/unit_tests/test_settings.py +++ b/tests/unit_tests/test_settings.py @@ -66,8 +66,8 @@ def test_export_to_xml(run_in_tmpdir): space=openmc.stats.Box((-1., -1., -1.), (1., 1., 1.)) ) } - s.max_particle_events = 100 + s.source_rejection_fraction = 0.01 # Make sure exporting XML works s.export_to_xml() @@ -130,7 +130,7 @@ def test_export_to_xml(run_in_tmpdir): assert s.log_grid_bins == 2000 assert not s.photon_transport assert s.electron_treatment == 'led' - assert s.write_initial_source == True + assert s.write_initial_source assert len(s.volume_calculations) == 1 vol = s.volume_calculations[0] assert vol.domain_type == 'cell' @@ -144,3 +144,4 @@ def test_export_to_xml(run_in_tmpdir): assert s.random_ray['distance_active'] == 100.0 assert s.random_ray['ray_source'].space.lower_left == [-1., -1., -1.] assert s.random_ray['ray_source'].space.upper_right == [1., 1., 1.] + assert s.source_rejection_fraction == 0.01 diff --git a/tests/unit_tests/test_source.py b/tests/unit_tests/test_source.py index c88fbcbe6..bb8a1b785 100644 --- a/tests/unit_tests/test_source.py +++ b/tests/unit_tests/test_source.py @@ -264,21 +264,36 @@ def test_constraints_file(sphere_box_model, run_in_tmpdir): @pytest.mark.skipif(config['mpi'], reason='Not compatible with MPI') -def test_rejection_limit(sphere_box_model, run_in_tmpdir): - model, cell1 = sphere_box_model[:2] +def test_rejection_fraction(run_in_tmpdir): + mat = openmc.Material() + mat.add_nuclide('H1', 1.0) + w = 0.25 + rpp1 = openmc.model.RectangularParallelepiped( + -w/2, w/2, -w/2, w/2, -w/2, w/2) + rpp2 = openmc.model.RectangularParallelepiped( + -0.5, 0.5, -0.5, 0.5, -0.5, 0.5, boundary_type='vacuum') + cell1 = openmc.Cell(fill=mat, region=-rpp1) + cell2 = openmc.Cell(region=+rpp1 & -rpp2) + model = openmc.Model() + model.geometry = openmc.Geometry([cell1, cell2]) - # Define a point source that will get rejected 100% of the time + # Create a box source over a 1 cm³ volume that is constrained to the source + # cell of volume (0.25 cm)³ = 0.0125 cm³, which means the default rejection + # fraction of 0.05 won't work + model.settings.particles = 1000 + model.settings.batches = 1 + model.settings.run_mode = 'fixed source' model.settings.source = openmc.IndependentSource( - space=openmc.stats.Point((-3., 0., 0.)), + space=openmc.stats.Box(*(-rpp2).bounding_box), constraints={'domains': [cell1]} ) - - # Confirm that OpenMC doesn't run in an infinite loop. Note that this may - # work when running with MPI since it won't necessarily capture the error - # message correctly - with pytest.raises(RuntimeError, match="rejected"): + with pytest.raises(RuntimeError, match='Too few source sites'): model.run(openmc_exec=config['exe']) + # With a source rejection fraction below 0.0125, the simulation should run + model.settings.source_rejection_fraction = 0.005 + model.run(openmc_exec=config['exe']) + def test_exceptions(): From 2eeba89992ee8832147957ec0e330aa363bf189e Mon Sep 17 00:00:00 2001 From: John Tramm Date: Wed, 11 Jun 2025 12:41:38 -0500 Subject: [PATCH 338/671] Apply Max Number of Events Check to Random Rays (#3438) Co-authored-by: Jonathan Shimwell --- src/random_ray/random_ray.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp index 27819591c..36eab0628 100644 --- a/src/random_ray/random_ray.cpp +++ b/src/random_ray/random_ray.cpp @@ -265,6 +265,12 @@ uint64_t RandomRay::transport_history_based_single_ray() if (!alive()) break; event_cross_surface(); + // If ray has too many events, display warning and kill it + if (n_event() >= settings::max_particle_events) { + warning("Ray " + std::to_string(id()) + + " underwent maximum number of events, terminating ray."); + wgt() = 0.0; + } } return n_event(); From e678b1a057e4dfe079ee27fd73886a8b2249f0c5 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Thu, 12 Jun 2025 01:54:04 +0300 Subject: [PATCH 339/671] Fix raytrace infinite loop. (#3423) Co-authored-by: Paul Romano --- src/mesh.cpp | 9 +++++++-- tests/unit_tests/test_mesh.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/mesh.cpp b/src/mesh.cpp index d61058525..becbbd3fd 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -989,7 +989,7 @@ void StructuredMesh::raytrace_mesh( // For all directions outside the mesh, find the distance that we need // to travel to reach the next surface. Use the largest distance, as // only this will cross all outer surfaces. - int k_max {0}; + int k_max {-1}; for (int k = 0; k < n; ++k) { if ((ijk[k] < 1 || ijk[k] > shape_[k]) && (distances[k].distance > traveled_distance)) { @@ -997,6 +997,10 @@ void StructuredMesh::raytrace_mesh( k_max = k; } } + // Assure some distance is traveled + if (k_max == -1) { + traveled_distance += TINY_BIT; + } // If r1 is not inside the mesh, exit here if (traveled_distance >= total_distance) @@ -1011,7 +1015,7 @@ void StructuredMesh::raytrace_mesh( } // If inside the mesh, Tally inward current - if (in_mesh) + if (in_mesh && k_max >= 0) tally.surface(ijk, k_max, !distances[k_max].max_surface, true); } } @@ -1207,6 +1211,7 @@ StructuredMesh::MeshDistance RegularMesh::distance_to_grid_boundary( d.next_index--; d.distance = (negative_grid_boundary(ijk, i) - r0[i]) / u[i]; } + return d; } diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index 49556fce3..3c8e988c0 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -618,3 +618,36 @@ def test_mesh_material_volumes_serialize(): assert new_volumes.by_element(1) == [(None, 1.0)] assert new_volumes.by_element(2) == [(2, 0.5), (1, 0.5)] assert new_volumes.by_element(3) == [(2, 1.0)] + + +def test_raytrace_mesh_infinite_loop(): + # Create a model with one large spherical cell + sphere = openmc.Sphere(r=100, boundary_type='vacuum') + cell = openmc.Cell(region=-sphere) + model = openmc.Model() + model.geometry = openmc.Geometry([cell]) + + # Create a regular mesh and associated tally + mesh_surface = openmc.RegularMesh() + mesh_surface.lower_left = (-30, -30, 30) + mesh_surface.upper_right = (30, 30, 60) + mesh_surface.dimension = (1, 1, 1) + reg_filter = openmc.MeshSurfaceFilter(mesh_surface) + mesh_surface_tally = openmc.Tally() + mesh_surface_tally.filters = [reg_filter] + mesh_surface_tally.scores = ['current'] + model.tallies = [mesh_surface_tally] + + # Define a source such that the z position is on a mesh boundary with a very + # small directional cosine in the z direction + polar = openmc.stats.delta_function(1.75e-7) + azimuthal = openmc.stats.Uniform(0.0, 2.0*pi) + model.settings.source = openmc.IndependentSource( + angle=openmc.stats.PolarAzimuthal(polar, azimuthal) + ) + model.settings.run_mode = 'fixed source' + model.settings.particles = 10 + model.settings.batches = 1 + + # Run the model; this should not cause an infinite loop + model.run() From 6c9c69628cbab69ecd46dc50c2585ab9fd5251f6 Mon Sep 17 00:00:00 2001 From: April Novak Date: Thu, 12 Jun 2025 11:49:59 -0500 Subject: [PATCH 340/671] update units for flux (#3441) --- docs/source/methods/random_ray.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index 41f95f0a9..5e17316aa 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -109,9 +109,7 @@ terms on the right hand side. In Equation :eq:`transport`, :math:`\psi` is the angular neutron flux. This parameter represents the total distance traveled by all neutrons in a particular direction inside of a control volume per second, and is often given in units of -:math:`1/(\text{cm}^{2} \text{s})`. As OpenMC does not support time dependence -in the random ray solver mode, we consider the steady state equation, where the -units of flux become :math:`1/\text{cm}^{2}`. The angular direction unit vector, +:math:`1/(\text{cm}^{2} \text{s})`. The angular direction unit vector, :math:`\mathbf{\Omega}`, represents the direction of travel for the neutron. The spatial position vector, :math:`\mathbf{r}`, represents the location within the simulation. The neutron energy, :math:`E`, or speed in continuous space, is From f81962c960929edc055198527d544db36f150747 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Thu, 12 Jun 2025 23:02:35 +0200 Subject: [PATCH 341/671] Allowing chain_file to be chain object to save reloading time (#3436) Co-authored-by: Paul Romano --- openmc/deplete/abc.py | 11 +++--- openmc/deplete/chain.py | 38 ++++++++++++++++++- openmc/deplete/coupled_operator.py | 6 +-- openmc/deplete/d1s.py | 15 ++++---- openmc/deplete/independent_operator.py | 12 +++--- openmc/deplete/microxs.py | 35 +++++------------ openmc/deplete/openmc_operator.py | 6 +-- tests/unit_tests/test_d1s.py | 3 +- .../test_deplete_coupled_operator.py | 6 ++- .../test_deplete_independent_operator.py | 5 ++- 10 files changed, 81 insertions(+), 56 deletions(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 4b43dd221..e6d4c1512 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -24,7 +24,7 @@ from openmc.mpi import comm from openmc.utility_funcs import change_directory from openmc import Material from .stepresult import StepResult -from .chain import Chain +from .chain import _get_chain from .results import Results, _SECONDS_PER_MINUTE, _SECONDS_PER_HOUR, \ _SECONDS_PER_DAY, _SECONDS_PER_JULIAN_YEAR from .pool import deplete @@ -126,8 +126,8 @@ class TransportOperator(ABC): Parameters ---------- - chain_file : str - Path to the depletion chain XML file + chain_file : PathLike or Chain + Path to the depletion chain XML file or instance of openmc.deplete.Chain. fission_q : dict, optional Dictionary of nuclides and their fission Q values [eV]. If not given, values will be pulled from the ``chain_file``. @@ -145,11 +145,12 @@ class TransportOperator(ABC): The depletion chain information necessary to form matrices and tallies. """ - def __init__(self, chain_file, fission_q=None, prev_results=None): + def __init__(self, chain_file=None, fission_q=None, prev_results=None): self.output_dir = '.' # Read depletion chain - self.chain = Chain.from_xml(chain_file, fission_q) + self.chain = _get_chain(chain_file, fission_q) + if prev_results is None: self.prev_res = None else: diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index 9f234683a..07db48799 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -17,8 +17,9 @@ from typing import List import lxml.etree as ET import scipy.sparse as sp -from openmc.checkvalue import check_type, check_greater_than +from openmc.checkvalue import check_type, check_greater_than, PathLike from openmc.data import gnds_name, zam +from openmc.exceptions import DataError from .nuclide import FissionYieldDistribution, Nuclide import openmc.data @@ -1246,3 +1247,38 @@ class Chain: found.update(isotopes) return found + + +def _get_chain( + chain_file: PathLike | Chain | None = None, + fission_q: dict | None = None +) -> Chain: + """Get a depletion chain from a file or the runtime configuration. + + Parameters + ---------- + chain_file : PathLike or Chain, optional + Path to depletion chain XML file, a Chain instance, or None to use + the file specified in ``openmc.config['chain_file']``. + fission_q : dict, optional + Dictionary of nuclides and their fission Q values [eV]. If not given, + values will be pulled from the ``chain_file``. + + Returns + ------- + Chain + Depletion chain instance. + """ + if isinstance(chain_file, Chain): + return chain_file + elif isinstance(chain_file, PathLike | None): + if chain_file is None: + chain_file = openmc.config.get('chain_file') + if 'chain_file' not in openmc.config: + raise DataError( + "No depletion chain specified and could not find depletion " + "chain in openmc.config['chain_file']" + ) + return Chain.from_xml(chain_file, fission_q) + else: + raise TypeError("chain_file must be path-like, a Chain, or None") diff --git a/openmc/deplete/coupled_operator.py b/openmc/deplete/coupled_operator.py index 7ecfd5a08..34bb28b49 100644 --- a/openmc/deplete/coupled_operator.py +++ b/openmc/deplete/coupled_operator.py @@ -102,9 +102,9 @@ class CoupledOperator(OpenMCOperator): ---------- model : openmc.model.Model OpenMC model object - chain_file : str, optional - Path to the depletion chain XML file. Defaults to - ``openmc.config['chain_file']``. + chain_file : PathLike or Chain, optional + Path to the depletion chain XML file or instance of openmc.deplete.Chain. + Defaults to ``openmc.config['chain_file']``. prev_results : Results, optional Results from a previous depletion calculation. If this argument is specified, the depletion calculation will start from the latest state diff --git a/openmc/deplete/d1s.py b/openmc/deplete/d1s.py index 0a1ed74b1..e9f8bfe79 100644 --- a/openmc/deplete/d1s.py +++ b/openmc/deplete/d1s.py @@ -14,19 +14,20 @@ import numpy as np import openmc from openmc.data import half_life from .abc import _normalize_timesteps -from .chain import Chain +from .chain import Chain, _get_chain +from ..checkvalue import PathLike -def get_radionuclides(model: openmc.Model, chain_file: str | None = None) -> list[str]: +def get_radionuclides(model: openmc.Model, chain_file: PathLike | Chain | None = None) -> list[str]: """Determine all radionuclides that can be produced during D1S. Parameters ---------- model : openmc.Model Model that should be used for determining what nuclides are present - chain_file : str, optional - Which chain file to use for inspecting decay data. If None is passed, - defaults to ``openmc.config['chain_file']`` + chain_file : PathLike | Chain + Path to the depletion chain XML file or instance of openmc.deplete.Chain. + Used for inspecting decay data. Defaults to ``openmc.config['chain_file']`` Returns ------- @@ -39,9 +40,7 @@ def get_radionuclides(model: openmc.Model, chain_file: str | None = None) -> lis for nuc in mat.get_nuclides()} # Load chain file - if chain_file is None: - chain_file = openmc.config['chain_file'] - chain = Chain.from_xml(chain_file) + chain = _get_chain(chain_file) radionuclides = set() for nuclide in chain.nuclides: diff --git a/openmc/deplete/independent_operator.py b/openmc/deplete/independent_operator.py index b3fe9a78f..3eb50b05e 100644 --- a/openmc/deplete/independent_operator.py +++ b/openmc/deplete/independent_operator.py @@ -50,9 +50,9 @@ class IndependentOperator(OpenMCOperator): Cross sections in [b] for each domain. If the :class:`~openmc.deplete.MicroXS` object is empty, a decay-only calculation will be run. - chain_file : str - Path to the depletion chain XML file. Defaults to - ``openmc.config['chain_file']``. + chain_file : PathLike or Chain, optional + Path to the depletion chain XML file or instance of openmc.deplete.Chain. + Defaults to ``openmc.config['chain_file']``. keff : 2-tuple of float, optional keff eigenvalue and uncertainty from transport calculation. prev_results : Results, optional @@ -179,9 +179,9 @@ class IndependentOperator(OpenMCOperator): micro_xs : MicroXS Cross sections in [b]. If the :class:`~openmc.deplete.MicroXS` object is empty, a decay-only calculation will be run. - chain_file : str, optional - Path to the depletion chain XML file. Defaults to - ``openmc.config['chain_file']``. + chain_file : PathLike or Chain, optional + Path to the depletion chain XML file or instance of + openmc.deplete.Chain. Defaults to ``openmc.config['chain_file']``. nuc_units : {'atom/cm3', 'atom/b-cm'}, optional Units for nuclide concentration. keff : 2-tuple of float, optional diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index 686d3bc38..d478db11b 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -12,13 +12,12 @@ import pandas as pd import numpy as np from openmc.checkvalue import check_type, check_value, check_iterable_type, PathLike -from openmc.exceptions import DataError from openmc.utility_funcs import change_directory from openmc import StatePoint from openmc.mgxs import GROUP_STRUCTURES from openmc.data import REACTION_MT import openmc -from .chain import Chain, REACTIONS +from .chain import Chain, REACTIONS, _get_chain from .coupled_operator import _find_cross_sections, _get_nuclides_with_data import openmc.lib from openmc.mpi import comm @@ -28,24 +27,13 @@ _valid_rxns.append('fission') _valid_rxns.append('damage-energy') -def _resolve_chain_file_path(chain_file: str | None): - if chain_file is None: - chain_file = openmc.config.get('chain_file') - if 'chain_file' not in openmc.config: - raise DataError( - "No depletion chain specified and could not find depletion " - "chain in openmc.config['chain_file']" - ) - return chain_file - - def get_microxs_and_flux( model: openmc.Model, domains, nuclides: Iterable[str] | None = None, reactions: Iterable[str] | None = None, energies: Iterable[float] | str | None = None, - chain_file: PathLike | None = None, + chain_file: PathLike | Chain | None = None, run_kwargs=None ) -> tuple[list[np.ndarray], list[MicroXS]]: """Generate a microscopic cross sections and flux from a Model @@ -66,9 +54,9 @@ def get_microxs_and_flux( reactions listed in the depletion chain file are used. energies : iterable of float or str Energy group boundaries in [eV] or the name of the group structure - chain_file : str, optional - Path to the depletion chain XML file that will be used in depletion - simulation. Used to determine cross sections for materials not + 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 present in the inital composition. Defaults to ``openmc.config['chain_file']``. run_kwargs : dict, optional @@ -86,8 +74,7 @@ def get_microxs_and_flux( original_tallies = model.tallies # Determine what reactions and nuclides are available in chain - chain_file = _resolve_chain_file_path(chain_file) - chain = Chain.from_xml(chain_file) + chain = _get_chain(chain_file) if reactions is None: reactions = chain.reactions if not nuclides: @@ -245,9 +232,9 @@ class MicroXS: Energy group boundaries in [eV] or the name of the group structure multi_group_flux : iterable of float Energy-dependent multigroup flux values - chain_file : str, optional - Path to the depletion chain XML file that will be used in depletion - simulation. Defaults to ``openmc.config['chain_file']``. + chain_file : PathLike or Chain, optional + Path to the depletion chain XML file or an instance of + openmc.deplete.Chain. Defaults to ``openmc.config['chain_file']``. temperature : int, optional Temperature for cross section evaluation in [K]. nuclides : list of str, optional @@ -278,9 +265,7 @@ class MicroXS: if len(multigroup_flux) != len(energies) - 1: raise ValueError('Length of flux array should be len(energies)-1') - chain_file_path = _resolve_chain_file_path(chain_file) - chain = Chain.from_xml(chain_file_path) - + chain = _get_chain(chain_file) cross_sections = _find_cross_sections(model=None) nuclides_with_data = _get_nuclides_with_data(cross_sections) diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py index 4ef867372..2d7694093 100644 --- a/openmc/deplete/openmc_operator.py +++ b/openmc/deplete/openmc_operator.py @@ -36,9 +36,9 @@ class OpenMCOperator(TransportOperator): cross_sections : str or list of MicroXS Path to continuous energy cross section library, or list of objects containing cross sections. - chain_file : str, optional - Path to the depletion chain XML file. Defaults to - openmc.config['chain_file']. + chain_file : PathLike or Chain, optional + Path to the depletion chain XML file or instance of openmc.deplete.Chain. + Defaults to ``openmc.config['chain_file']``. prev_results : Results, optional Results from a previous depletion calculation. If this argument is specified, the depletion calculation will start from the latest state diff --git a/tests/unit_tests/test_d1s.py b/tests/unit_tests/test_d1s.py index 7c92dd6d9..115759489 100644 --- a/tests/unit_tests/test_d1s.py +++ b/tests/unit_tests/test_d1s.py @@ -22,7 +22,8 @@ def model(): def test_get_radionuclides(model): # Check that radionuclides are correct and are unstable - nuclides = d1s.get_radionuclides(model, CHAIN_PATH) + chain = openmc.deplete.Chain.from_xml(CHAIN_PATH) + nuclides = d1s.get_radionuclides(model, chain) assert sorted(nuclides) == [ 'Co58', 'Co60', 'Co61', 'Co62', 'Co64', 'Fe55', 'Fe59', 'Fe61', 'Ni57', 'Ni59', 'Ni63', 'Ni65' diff --git a/tests/unit_tests/test_deplete_coupled_operator.py b/tests/unit_tests/test_deplete_coupled_operator.py index ba291e07d..119a15923 100644 --- a/tests/unit_tests/test_deplete_coupled_operator.py +++ b/tests/unit_tests/test_deplete_coupled_operator.py @@ -5,7 +5,7 @@ from pathlib import Path import pytest -from openmc.deplete import CoupledOperator +from openmc.deplete import CoupledOperator, Chain import openmc import numpy as np @@ -106,11 +106,13 @@ def test_diff_volume_method_match_cell(model_with_volumes): def test_diff_volume_method_divide_equally(model_with_volumes): """Tests the volumes assigned to the materials are divided equally""" + chain = Chain.from_xml(CHAIN_PATH) + operator = openmc.deplete.CoupledOperator( model=model_with_volumes, diff_burnable_mats=True, diff_volume_method='divide equally', - chain_file=CHAIN_PATH + chain_file=chain ) all_cells = list(operator.model.geometry.get_all_cells().values()) diff --git a/tests/unit_tests/test_deplete_independent_operator.py b/tests/unit_tests/test_deplete_independent_operator.py index c765d0650..aca83399a 100644 --- a/tests/unit_tests/test_deplete_independent_operator.py +++ b/tests/unit_tests/test_deplete_independent_operator.py @@ -7,7 +7,7 @@ from pathlib import Path import pytest from openmc import Material -from openmc.deplete import IndependentOperator, MicroXS +from openmc.deplete import IndependentOperator, MicroXS, Chain CHAIN_PATH = Path(__file__).parents[1] / "chain_simple.xml" ONE_GROUP_XS = Path(__file__).parents[1] / "micro_xs_simple.csv" @@ -25,8 +25,9 @@ def test_operator_init(): 'O17': 1.7588724018066158e+19} flux = 1.0 micro_xs = MicroXS.from_csv(ONE_GROUP_XS) + chain = Chain.from_xml(CHAIN_PATH) IndependentOperator.from_nuclides( - volume, nuclides, flux, micro_xs, CHAIN_PATH, nuc_units='atom/cm3') + volume, nuclides, flux, micro_xs, chain, nuc_units='atom/cm3') fuel = Material(name="uo2") fuel.add_element("U", 1, percent_type="ao", enrichment=4.25) From aeb1052c1907e4d12890fcbbc2bb53ee3c737aa3 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Thu, 12 Jun 2025 19:54:35 -0500 Subject: [PATCH 342/671] Fix Resetting of Auto IDs When Generating MGXS (#3437) --- openmc/model/model.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index c19c8ac9f..7e679d10a 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1552,7 +1552,6 @@ class Model: "if a material has a k-infinity > 1.0.") mgxs_sets = [] for material in self.materials: - openmc.reset_auto_ids() model = openmc.Model() # Set materials on the model @@ -1749,7 +1748,6 @@ class Model: directory : str Directory to run the simulation in, so as to contain XML files. """ - openmc.reset_auto_ids() model = openmc.Model() model.materials = self.materials @@ -1861,7 +1859,6 @@ class Model: directory : PathLike Directory to run the simulation in, so as to contain XML files. """ - openmc.reset_auto_ids() model = copy.deepcopy(self) model.tallies = openmc.Tallies() @@ -1973,7 +1970,7 @@ class Model: # Make sure all materials have a name, and that the name is a valid HDF5 # dataset name for material in self.materials: - if material.name is None: + if not material.name or not material.name.strip(): material.name = f"material {material.id}" material.name = re.sub(r'[^a-zA-Z0-9]', '_', material.name) From b11eb02655ab0112ce6001546c9311692d08c9b2 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 13 Jun 2025 15:38:16 -0500 Subject: [PATCH 343/671] Change Dockerfile from debian:bookworm-slim to ubuntu:24.04 (#3442) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 1ae615a50..9745f1512 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,7 @@ ARG compile_cores=1 ARG build_dagmc=off ARG build_libmesh=off -FROM debian:bookworm-slim AS dependencies +FROM ubuntu:24.04 AS dependencies ARG compile_cores ARG build_dagmc From 23eab2c89bb6537cf3bf264215b38c6013a518e0 Mon Sep 17 00:00:00 2001 From: Jan Malec Date: Sat, 14 Jun 2025 08:39:31 +0200 Subject: [PATCH 344/671] Allow specifying number of equiprobable angles for thermal scattering data generation (#3346) Co-authored-by: Paul Romano --- openmc/data/njoy.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openmc/data/njoy.py b/openmc/data/njoy.py index e025cff0d..4c538bd6e 100644 --- a/openmc/data/njoy.py +++ b/openmc/data/njoy.py @@ -201,12 +201,12 @@ acer / %%%%%%%%%%%%%%%%%%%%%%%% Write out in ACE format %%%%%%%%%%%%%%%%%%%%%%%% _THERMAL_TEMPLATE_THERMR = """ thermr / %%%%%%%%%%%%%%%% Add thermal scattering data (free gas) %%%%%%%%%%%%%%% 0 {nthermr1_in} {nthermr1} -0 {mat} 12 {num_temp} 1 0 {iform} 1 221 1/ +0 {mat} {nbin} {num_temp} 1 0 {iform} 1 221 1/ {temps} {error} {energy_max} thermr / %%%%%%%%%%%%%%%% Add thermal scattering data (bound) %%%%%%%%%%%%%%%%%% {nthermal_endf} {nthermr2_in} {nthermr2} -{mat_thermal} {mat} 16 {num_temp} {inelastic} {elastic} {iform} {natom} 222 1/ +{mat_thermal} {mat} {nbin} {num_temp} {inelastic} {elastic} {iform} {natom} 222 1/ {temps} {error} {energy_max} """ @@ -495,7 +495,7 @@ def make_ace(filename, temperatures=None, acer=True, xsdir=None, def make_ace_thermal(filename, filename_thermal, temperatures=None, ace=None, xsdir=None, output_dir=None, error=0.001, iwt=2, evaluation=None, evaluation_thermal=None, - table_name=None, zaids=None, nmix=None, **kwargs): + table_name=None, zaids=None, nmix=None, nbin=16, **kwargs): """Generate thermal scattering ACE file from ENDF files Parameters @@ -532,6 +532,8 @@ def make_ace_thermal(filename, filename_thermal, temperatures=None, ZAIDs that the thermal scattering data applies to nmix : int, optional Number of atom types in mixed moderator + nbin : int, optional + Number of equi-probable angles **kwargs Keyword arguments passed to :func:`openmc.data.njoy.run` From 7a1cafa6c17d456dabdd4a2aa33fd5e586e98794 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Wed, 18 Jun 2025 00:36:35 +0200 Subject: [PATCH 345/671] adding plot function to DAGMCUnvierse (#3451) Co-authored-by: Paul Romano --- openmc/dagmc.py | 16 ++++++++++++++++ tests/unit_tests/dagmc/test_plot.py | 14 +++++++++++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/openmc/dagmc.py b/openmc/dagmc.py index 8ab0aaf69..3f20fb0ac 100644 --- a/openmc/dagmc.py +++ b/openmc/dagmc.py @@ -13,6 +13,7 @@ from .checkvalue import check_type, check_value from .surface import _BOUNDARY_TYPES from .bounding_box import BoundingBox from .utility_funcs import input_path +from .plots import add_plot_params class DAGMCUniverse(openmc.UniverseBase): @@ -566,6 +567,21 @@ class DAGMCUniverse(openmc.UniverseBase): fill = mats_per_id[dag_cell.fill.id] if dag_cell.fill else None self.add_cell(openmc.DAGMCCell(cell_id=dag_cell_id, fill=fill)) + @add_plot_params + def plot(self, *args, **kwargs): + """Display a slice plot of the DAGMCUniverse. + """ + model = openmc.Model() + model.geometry = openmc.Geometry(self) + + for mat_name in self.material_names: + material = openmc.Material(name=mat_name) + # Placeholder nuclide to ensure material is not empty + material.add_nuclide('H1', 1.0) + model.materials.append(material) + + return model.plot(*args, **kwargs) + class DAGMCCell(openmc.Cell): """A cell class for DAGMC-based geometries. diff --git a/tests/unit_tests/dagmc/test_plot.py b/tests/unit_tests/dagmc/test_plot.py index 83f6df767..d768ba508 100644 --- a/tests/unit_tests/dagmc/test_plot.py +++ b/tests/unit_tests/dagmc/test_plot.py @@ -7,9 +7,9 @@ pytestmark = pytest.mark.skipif( not openmc.lib._dagmc_enabled(), reason="DAGMC CAD geometry is not enabled." ) -def test_plotting_dagmc_geometry(request): - """Test plotting a DAGMC geometry with OpenMC. This is different to CSG - geometry plotting as the path to the DAGMC file needs handling.""" +def test_plotting_dagmc_model(request): + """Test plotting a DAGMC model with OpenMC. This is different to CSG + model plotting as the path to the DAGMC file needs handling.""" dag_universe = openmc.DAGMCUniverse(request.path.parent / 'dagmc.h5m') csg_with_dag_inside = dag_universe.bounded_universe() @@ -30,3 +30,11 @@ def test_plotting_dagmc_geometry(request): model.settings.particles = 50 model.plot() + + +def test_plotting_dagmc_universe(request): + """Test plotting a DAGMCUniverse with OpenMC. This is different to plotting + UniverseBase as the materials are not defined withing the DAGMCUniverse.""" + + dag_universe = openmc.DAGMCUniverse(request.path.parent / 'dagmc.h5m') + dag_universe.plot() From f31130d36798aef0ca7bed6a73e24a9281c6c51c Mon Sep 17 00:00:00 2001 From: John Tramm Date: Wed, 18 Jun 2025 06:34:56 -0500 Subject: [PATCH 346/671] Prevent Adjoint Sources from Trending towards Infinity (#3449) --- include/openmc/constants.h | 5 ++++ src/random_ray/flat_source_domain.cpp | 43 +++++++++++++++++++++------ 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/include/openmc/constants.h b/include/openmc/constants.h index 252194528..ae7056079 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -63,6 +63,11 @@ constexpr int MAX_SAMPLE {100000}; // source region in the random ray solver constexpr double MIN_HITS_PER_BATCH {1.5}; +// The minimum flux value to be considered non-zero when computing adjoint +// sources. Positive values below this cutoff will be treated as zero, so as to +// prevent extremely large adjoint source terms from being generated. +constexpr double ZERO_FLUX_CUTOFF {1e-22}; + // ============================================================================ // MATH AND PHYSICAL CONSTANTS diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 9df6513ae..7404a0b29 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -304,6 +304,13 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() set_flux_to_source(sr, g); } } + // Halt if NaN implosion is detected + if (!std::isfinite(source_regions_.scalar_flux_new(sr, g))) { + fatal_error("A source region scalar flux is not finite. " + "This indicates a numerical instability in the " + "simulation. Consider increasing ray density or adjusting " + "the source region mesh."); + } } } @@ -1154,9 +1161,9 @@ void FlatSourceDomain::flatten_xs() m.get_xs(MgxsType::TOTAL, g_out, NULL, NULL, NULL, t, a); sigma_t_.push_back(sigma_t); - double nu_Sigma_f = + double nu_sigma_f = m.get_xs(MgxsType::NU_FISSION, g_out, NULL, NULL, NULL, t, a); - nu_sigma_f_.push_back(nu_Sigma_f); + nu_sigma_f_.push_back(nu_sigma_f); double sigma_f = m.get_xs(MgxsType::FISSION, g_out, NULL, NULL, NULL, t, a); @@ -1164,6 +1171,11 @@ void FlatSourceDomain::flatten_xs() double chi = m.get_xs(MgxsType::CHI_PROMPT, g_out, &g_out, NULL, NULL, t, a); + if (!std::isfinite(chi)) { + // MGXS interface may return NaN in some cases, such as when material + // is fissionable but has very small sigma_f. + chi = 0.0; + } chi_.push_back(chi); for (int g_in = 0; g_in < negroups_; g_in++) { @@ -1191,17 +1203,30 @@ void FlatSourceDomain::flatten_xs() void FlatSourceDomain::set_adjoint_sources(const vector& forward_flux) { - // Set the external source to 1/forward_flux. If the forward flux is negative - // or zero, set the adjoint source to zero, as this is likely a very small - // source region that we don't need to bother trying to vector particles - // towards. Flux negativity in random ray is not related to the flux being - // small in magnitude, but rather due to the source region being physically - // small in volume and thus having a noisy flux estimate. + // Set the adjoint external source to 1/forward_flux. If the forward flux is + // negative, zero, or extremely close to zero, set the adjoint source to zero, + // as this is likely a very small source region that we don't need to bother + // trying to vector particles towards. In the case of flux "being extremely + // close to zero", we define this as being a fixed fraction of the maximum + // forward flux, below which we assume the flux would be physically + // undetectable. + + // First, find the maximum forward flux value + double max_flux = 0.0; +#pragma omp parallel for reduction(max : max_flux) + for (int64_t se = 0; se < n_source_elements(); se++) { + double flux = forward_flux[se]; + if (flux > max_flux) { + max_flux = flux; + } + } + + // Then, compute the adjoint source for each source region #pragma omp parallel for for (int64_t sr = 0; sr < n_source_regions(); sr++) { for (int g = 0; g < negroups_; g++) { double flux = forward_flux[sr * negroups_ + g]; - if (flux <= 0.0) { + if (flux <= ZERO_FLUX_CUTOFF * max_flux) { source_regions_.external_source(sr, g) = 0.0; } else { source_regions_.external_source(sr, g) = 1.0 / flux; From 1c5aa6559be235abeb1c7a4c5d596d5ec617947d Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Wed, 18 Jun 2025 18:10:03 +0300 Subject: [PATCH 347/671] fixing expansion of elemental Ta bug (#3443) Co-authored-by: Paul Romano --- openmc/data/data.py | 2 +- openmc/element.py | 9 +++++---- tests/unit_tests/test_element.py | 7 +++++++ 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/openmc/data/data.py b/openmc/data/data.py index 408adf2e4..8ccc8d263 100644 --- a/openmc/data/data.py +++ b/openmc/data/data.py @@ -94,7 +94,7 @@ NATURAL_ABUNDANCE = { 'Yb174': 0.32025, 'Yb176': 0.12995, 'Lu175': 0.97401, 'Lu176': 0.02599, 'Hf174': 0.0016, 'Hf176': 0.0526, 'Hf177': 0.186, 'Hf178': 0.2728, 'Hf179': 0.1362, - 'Hf180': 0.3508, 'Ta180': 0.0001201, 'Ta181': 0.9998799, + 'Hf180': 0.3508, 'Ta180_m1': 0.0001201, 'Ta181': 0.9998799, 'W180': 0.0012, 'W182': 0.265, 'W183': 0.1431, 'W184': 0.3064, 'W186': 0.2843, 'Re185': 0.374, 'Re187': 0.626, 'Os184': 0.0002, 'Os186': 0.0159, diff --git a/openmc/element.py b/openmc/element.py index 082bee522..f1d8f5f24 100644 --- a/openmc/element.py +++ b/openmc/element.py @@ -144,8 +144,7 @@ class Element(str): root = tree.getroot() for child in root.findall('library'): nuclide = child.attrib['materials'] - if re.match(r'{}\d+'.format(self), nuclide) and \ - '_m' not in nuclide: + if re.match(r'{}\d+'.format(self), nuclide): library_nuclides.add(nuclide) # Get a set of the mutual and absent nuclides. Convert to lists @@ -185,8 +184,10 @@ class Element(str): for nuclide in absent_nuclides: if nuclide in ['O17', 'O18'] and 'O16' in mutual_nuclides: abundances['O16'] += NATURAL_ABUNDANCE[nuclide] - elif nuclide == 'Ta180' and 'Ta181' in mutual_nuclides: - abundances['Ta181'] += NATURAL_ABUNDANCE[nuclide] + elif nuclide == 'Ta180_m1' and 'Ta180' in library_nuclides: + abundances['Ta180'] = NATURAL_ABUNDANCE[nuclide] + elif nuclide == 'Ta180_m1' and 'Ta181' in mutual_nuclides: + abundances['Ta181'] += NATURAL_ABUNDANCE[nuclide] elif nuclide == 'W180' and 'W182' in mutual_nuclides: abundances['W182'] += NATURAL_ABUNDANCE[nuclide] else: diff --git a/tests/unit_tests/test_element.py b/tests/unit_tests/test_element.py index d3555701e..d91cfaf6e 100644 --- a/tests/unit_tests/test_element.py +++ b/tests/unit_tests/test_element.py @@ -44,6 +44,13 @@ def test_expand_no_isotopes(): element.expand(100.0, 'ao') +def test_expand_ta(): + ref = {'Ta180': 0.01201, 'Ta181': 99.98799} + element = openmc.Element('Ta') + for isotope in element.expand(100.0, 'ao'): + assert isotope[1] == approx(ref[isotope[0]]) + + def test_expand_exceptions(): """ Test that correct exceptions are raised for invalid input """ From 3e32aed2da312a04fb4db2c22648d66d78a0e745 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Wed, 18 Jun 2025 20:34:07 +0200 Subject: [PATCH 348/671] Fixing crash when calling Geometry.plot when DAGMCUniverse in geometry (#3455) Co-authored-by: Paul Romano --- openmc/dagmc.py | 11 +---------- openmc/geometry.py | 15 ++++++++++++++- openmc/model/model.py | 14 ++++++++++++++ openmc/universe.py | 8 -------- tests/unit_tests/dagmc/test_plot.py | 21 +++++++++++++++++++++ 5 files changed, 50 insertions(+), 19 deletions(-) diff --git a/openmc/dagmc.py b/openmc/dagmc.py index 3f20fb0ac..2486f7516 100644 --- a/openmc/dagmc.py +++ b/openmc/dagmc.py @@ -571,16 +571,7 @@ class DAGMCUniverse(openmc.UniverseBase): def plot(self, *args, **kwargs): """Display a slice plot of the DAGMCUniverse. """ - model = openmc.Model() - model.geometry = openmc.Geometry(self) - - for mat_name in self.material_names: - material = openmc.Material(name=mat_name) - # Placeholder nuclide to ensure material is not empty - material.add_nuclide('H1', 1.0) - model.materials.append(material) - - return model.plot(*args, **kwargs) + return openmc.Geometry(self).plot(*args, **kwargs) class DAGMCCell(openmc.Cell): diff --git a/openmc/geometry.py b/openmc/geometry.py index c069d5796..9b2af7b4a 100644 --- a/openmc/geometry.py +++ b/openmc/geometry.py @@ -754,4 +754,17 @@ class Geometry: .. versionadded:: 0.14.0 """ - return self.root_universe.plot(*args, **kwargs) + model = openmc.Model() + model.geometry = self + model.materials = self.get_all_materials().values() + + # Add placeholder materials for DAGMCUniverses + universes = self.get_all_universes() + for universe in universes.values(): + if isinstance(universe, openmc.DAGMCUniverse): + for name in universe.material_names: + mat_dag = openmc.Material(name=name) + mat_dag.add_nuclide('H1', 1.0) + model.materials.append(mat_dag) + + return model.plot(*args, **kwargs) diff --git a/openmc/model/model.py b/openmc/model/model.py index 7e679d10a..55f309bfb 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -986,7 +986,16 @@ class Model: y_min = (origin[y] - 0.5*width[1]) * axis_scaling_factor[axis_units] y_max = (origin[y] + 0.5*width[1]) * axis_scaling_factor[axis_units] + # Determine whether any materials contains macroscopic data and if so, + # set energy mode accordingly + _energy_mode = self.settings._energy_mode + for mat in self.geometry.get_all_materials().values(): + if mat._macroscopic is not None: + self.settings.energy_mode = 'multi-group' + break + with TemporaryDirectory() as tmpdir: + _plot_seed = self.settings.plot_seed if seed is not None: self.settings.plot_seed = seed @@ -1007,6 +1016,11 @@ class Model: # Run OpenMC in geometry plotting mode self.plot_geometry(False, cwd=tmpdir, openmc_exec=openmc_exec) + # Undo changes to model + self.plots.pop() + self.settings._plot_seed = _plot_seed + self.settings._energy_mode = _energy_mode + # Read image from file img_path = Path(tmpdir) / f'plot_{plot.id}.png' if not img_path.is_file(): diff --git a/openmc/universe.py b/openmc/universe.py index 02b794dee..0e64693ba 100644 --- a/openmc/universe.py +++ b/openmc/universe.py @@ -337,14 +337,6 @@ class UniverseBase(ABC, IDManagerMixin): """ model = openmc.Model() model.geometry = openmc.Geometry(self) - - # Determine whether any materials contains macroscopic data and if - # so, set energy mode accordingly - for mat in self.get_all_materials().values(): - if mat._macroscopic is not None: - model.settings.energy_mode = 'multi-group' - break - return model.plot(*args, **kwargs) def get_nuclides(self): diff --git a/tests/unit_tests/dagmc/test_plot.py b/tests/unit_tests/dagmc/test_plot.py index d768ba508..afc2c395e 100644 --- a/tests/unit_tests/dagmc/test_plot.py +++ b/tests/unit_tests/dagmc/test_plot.py @@ -38,3 +38,24 @@ def test_plotting_dagmc_universe(request): dag_universe = openmc.DAGMCUniverse(request.path.parent / 'dagmc.h5m') dag_universe.plot() + + +def test_plotting_geometry_filled_with_dagmc_universe(request): + """Test plotting a geometry with OpenMC. This is an edge case when plotting + geometry as often geometry objects don't include a DAGMCUniverse. The + inclusion of a DAGMCUniverse requires special handling for the materials.""" + + dag_universe = openmc.DAGMCUniverse(request.path.parent / 'dagmc.h5m', auto_geom_ids=True) + + sphere1 = openmc.Sphere(r=50.0) + sphere2 = openmc.Sphere(r=60.0, boundary_type='vacuum') + + # Adding a material to the CSG Universe to check all materials are accounted for + csg_material = openmc.Material(name='csg_material') + csg_material.add_nuclide("H1", 1.0) + + cell1 = openmc.Cell(fill=dag_universe, region=-sphere1) + cell2 = openmc.Cell(fill=csg_material, region=+sphere1 & -sphere2) + + geometry = openmc.Geometry([cell1, cell2]) + geometry.plot() From 3d16d4b1007fbee2f48e823d08f2ae0ab29aea73 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Thu, 19 Jun 2025 22:43:45 +0200 Subject: [PATCH 349/671] Adding checks to geometry.plot to avoid material name overlaps (#3458) Co-authored-by: Patrick Shriwise --- openmc/geometry.py | 30 +++++++++++++++++++++-------- tests/unit_tests/dagmc/test_plot.py | 11 +++++++++-- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/openmc/geometry.py b/openmc/geometry.py index 9b2af7b4a..f56b9b6fe 100644 --- a/openmc/geometry.py +++ b/openmc/geometry.py @@ -758,13 +758,27 @@ class Geometry: model.geometry = self model.materials = self.get_all_materials().values() - # Add placeholder materials for DAGMCUniverses - universes = self.get_all_universes() - for universe in universes.values(): - if isinstance(universe, openmc.DAGMCUniverse): - for name in universe.material_names: - mat_dag = openmc.Material(name=name) - mat_dag.add_nuclide('H1', 1.0) - model.materials.append(mat_dag) + # collect all the material names from the geometry + all_material_names = {m.name for m in model.materials if m.name is not None} + + # makes a placeholder material for each material name if it isn't + # already present on the model. These materials are otherwise missing + # from the geometry and are needed for plotting. + for universe in model.geometry.get_all_universes().values(): + if not isinstance(universe, openmc.DAGMCUniverse): + continue + for name in universe.material_names: + # if this name is already present in the model, skip it + # (this can happen if the same material name is used in multiple + # universes) + if name in all_material_names: + continue + # if the material name is not present on the model, + # create a placeholder material with the same name + # and add it to the model + mat_dag = openmc.Material(name=name) + mat_dag.add_nuclide('H1', 1.0) + model.materials.append(mat_dag) + all_material_names.add(name) return model.plot(*args, **kwargs) diff --git a/tests/unit_tests/dagmc/test_plot.py b/tests/unit_tests/dagmc/test_plot.py index afc2c395e..62022b258 100644 --- a/tests/unit_tests/dagmc/test_plot.py +++ b/tests/unit_tests/dagmc/test_plot.py @@ -48,14 +48,21 @@ def test_plotting_geometry_filled_with_dagmc_universe(request): dag_universe = openmc.DAGMCUniverse(request.path.parent / 'dagmc.h5m', auto_geom_ids=True) sphere1 = openmc.Sphere(r=50.0) - sphere2 = openmc.Sphere(r=60.0, boundary_type='vacuum') + sphere2 = openmc.Sphere(r=60.0) + sphere2 = openmc.Sphere(r=70.0, boundary_type='vacuum') - # Adding a material to the CSG Universe to check all materials are accounted for + # Adding a material to the CSG Universe to check universe materials are accounted for csg_material = openmc.Material(name='csg_material') csg_material.add_nuclide("H1", 1.0) + # Adding a material with the same name as a dagmc material to check that + # the plot can handel two materials with the same name from different universes + csg_material = openmc.Material(name=dag_universe.material_names[0]) + csg_material.add_nuclide("H1", 1.0) + cell1 = openmc.Cell(fill=dag_universe, region=-sphere1) cell2 = openmc.Cell(fill=csg_material, region=+sphere1 & -sphere2) geometry = openmc.Geometry([cell1, cell2]) + geometry.plot() From eaef13b9bc4606e9f70253b5a044064975d98b09 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Mon, 23 Jun 2025 15:53:27 -0500 Subject: [PATCH 350/671] Weight Window Birth Scaling (#3459) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Paul Romano --- include/openmc/particle_data.h | 5 +++++ src/simulation.cpp | 4 ++++ src/weight_windows.cpp | 16 ++++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index a07a99ffb..82b101c22 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -435,6 +435,7 @@ private: double wgt_ {1.0}; double wgt_born_ {1.0}; + double wgt_ww_born_ {-1.0}; double mu_; double time_ {0.0}; double time_last_ {0.0}; @@ -545,6 +546,10 @@ public: double& wgt_born() { return wgt_born_; } double wgt_born() const { return wgt_born_; } + // Weight window value at birth + double& wgt_ww_born() { return wgt_ww_born_; } + const double& wgt_ww_born() const { return wgt_ww_born_; } + // Statistic weight of particle at last collision double& wgt_last() { return wgt_last_; } const double& wgt_last() const { return wgt_last_; } diff --git a/src/simulation.cpp b/src/simulation.cpp index e4521c06d..f8d4e5470 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -614,6 +614,10 @@ void initialize_history(Particle& p, int64_t index_source) // Set particle track. p.write_track() = check_track_criteria(p); + // Set the particle's initial weight window value. + p.wgt_ww_born() = -1.0; + apply_weight_windows(p); + // Display message if high verbosity or trace is on if (settings::verbosity >= 9 || p.trace()) { write_message("Simulating Particle {}", p.id()); diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index 0efa5a6ae..50f3fb2ed 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -73,10 +73,26 @@ void apply_weight_windows(Particle& 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(); From 7c6aaf7204163464d13e14a4547b48269a70c3d1 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Mon, 23 Jun 2025 17:20:36 -0500 Subject: [PATCH 351/671] Fix Weight Window Infinite Loop Bug (#3457) Co-authored-by: Paul Romano --- include/openmc/particle_data.h | 2 +- src/particle.cpp | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index 82b101c22..be7fa7483 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -47,7 +47,7 @@ struct SourceSite { double time {0.0}; double wgt {1.0}; int delayed_group {0}; - int surf_id {0}; + int surf_id {SURFACE_NONE}; ParticleType particle; // Extra attributes that don't show up in source written to file diff --git a/src/particle.cpp b/src/particle.cpp index 324276d15..6cfdf2175 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -104,6 +104,14 @@ void Particle::split(double wgt) bank.u = u(); bank.E = settings::run_CE ? E() : g(); bank.time = time(); + + // Convert signed index to a singed surface ID + if (surface() == SURFACE_NONE) { + bank.surf_id = SURFACE_NONE; + } else { + int surf_id = model::surfaces[surface_index()]->id_; + bank.surf_id = (surface() > 0) ? surf_id : -surf_id; + } } void Particle::from_source(const SourceSite* src) @@ -140,6 +148,12 @@ void Particle::from_source(const SourceSite* src) time() = src->time; time_last() = src->time; parent_nuclide() = src->parent_nuclide; + + // Convert signed surface ID to signed index + if (src->surf_id != SURFACE_NONE) { + int index_plus_one = model::surface_map[std::abs(src->surf_id)] + 1; + surface() = (src->surf_id > 0) ? index_plus_one : -index_plus_one; + } } void Particle::event_calculate_xs() From 3a8894c3f86a4e7c013eb5bd6e795aadead8ef7d Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 24 Jun 2025 04:29:16 -0500 Subject: [PATCH 352/671] Fix Dockerfile DAGMC build (#3463) --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 9745f1512..a163a2810 100644 --- a/Dockerfile +++ b/Dockerfile @@ -96,6 +96,7 @@ RUN if [ "$build_dagmc" = "on" ]; then \ # Install addition packages required for DAGMC apt-get -y install libeigen3-dev libnetcdf-dev libtbb-dev libglfw3-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} \ From 2a0299aec7e83fd978cb584ba8051c7ec6fb8866 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Tue, 24 Jun 2025 15:20:26 -0500 Subject: [PATCH 353/671] Limit Random Ray Weight Window Generation to Final Batch (#3464) --- src/simulation.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/simulation.cpp b/src/simulation.cpp index f8d4e5470..d9a4fd131 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -400,8 +400,11 @@ void finalize_batch() simulation::time_tallies.stop(); // update weight windows if needed - for (const auto& wwg : variance_reduction::weight_windows_generators) { - wwg->update(); + if (settings::solver_type != SolverType::RANDOM_RAY || + simulation::current_batch == settings::n_batches) { + for (const auto& wwg : variance_reduction::weight_windows_generators) { + wwg->update(); + } } // Reset global tally results From 15dfe7e8b96a762086a0f4cf3c90d7f2cc31e833 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Wed, 25 Jun 2025 08:46:40 -0500 Subject: [PATCH 354/671] Parallelization of Weight Window Update (#3467) --- src/weight_windows.cpp | 189 ++++++++++++++++++++++++++--------------- 1 file changed, 120 insertions(+), 69 deletions(-) diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index 50f3fb2ed..26762ad18 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -6,11 +6,11 @@ #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/xstrided_view.hpp" #include "xtensor/xview.hpp" #include "openmc/error.h" @@ -506,8 +506,18 @@ void WeightWindows::update_weights(const Tally* tally, const std::string& value, /////////////////////////// this->check_tally_update_compatibility(tally); - lower_ww_.fill(-1); - upper_ww_.fill(-1); + // Dimensions of weight window arrays + 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) + for (int e = 0; e < e_bins; e++) { + for (int64_t m = 0; m < mesh_bins; m++) { + lower_ww_(e, m) = -1.0; + upper_ww_(e, m) = -1.0; + } + } // determine which value to use const std::set allowed_values = {"mean", "rel_err"}; @@ -605,10 +615,12 @@ void WeightWindows::update_weights(const Tally* tally, const std::string& value, } // down-select data based on particle and score - auto sum = xt::view(transposed_view, particle_idx, xt::all(), xt::all(), - score_index, static_cast(TallyResult::SUM)); - auto sum_sq = xt::view(transposed_view, particle_idx, xt::all(), xt::all(), - score_index, static_cast(TallyResult::SUM_SQ)); + 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)}); int n = tally->n_realizations_; ////////////////////////////////////////////// @@ -626,78 +638,117 @@ void WeightWindows::update_weights(const Tally* tally, const std::string& value, auto& new_bounds = this->lower_ww_; auto& rel_err = this->upper_ww_; - // noalias avoids memory allocation here - xt::noalias(new_bounds) = sum / n; - - xt::noalias(rel_err) = - xt::sqrt(((sum_sq / n) - xt::square(new_bounds)) / (n - 1)) / new_bounds; - xt::filter(rel_err, sum <= 0.0).fill(INFTY); - - if (value == "rel_err") - xt::noalias(new_bounds) = 1 / rel_err; - // get mesh volumes auto mesh_vols = this->mesh()->volumes(); - int e_bins = new_bounds.shape()[0]; - - if (method == WeightWindowUpdateMethod::MAGIC) { - // If we are computing weight windows with forward fluxes derived from a - // Monte Carlo or forward random ray solve, we use the MAGIC algorithm. - for (int e = 0; e < e_bins; e++) { - // select all - auto group_view = xt::view(new_bounds, e); - - // divide by volume of mesh elements - for (int i = 0; i < group_view.size(); i++) { - group_view[i] /= mesh_vols[i]; + // Calculate mean (new_bounds) and relative error +#pragma omp parallel for collapse(2) schedule(static) + for (int e = 0; e < e_bins; e++) { + for (int64_t m = 0; m < mesh_bins; m++) { + // Calculate mean + new_bounds(e, m) = sum(e, m) / n; + // Calculate relative error + if (sum(e, m) > 0.0) { + double mean_val = new_bounds(e, m); + double variance = (sum_sq(e, m) / n - mean_val * mean_val) / (n - 1); + rel_err(e, m) = std::sqrt(variance) / mean_val; + } else { + rel_err(e, m) = INFTY; } - - double group_max = - *std::max_element(group_view.begin(), group_view.end()); - // normalize values in this energy group by the maximum value for this - // group - if (group_max > 0.0) - group_view /= 2.0 * group_max; - } - } else { - // If we are computing weight windows with adjoint fluxes derived from an - // adjoint random ray solve, we use the FW-CADIS algorithm. - for (int e = 0; e < e_bins; e++) { - // select all - auto group_view = xt::view(new_bounds, e); - - // divide by volume of mesh elements - for (int i = 0; i < group_view.size(); i++) { - group_view[i] /= mesh_vols[i]; + if (value == "rel_err") { + new_bounds(e, m) = 1.0 / rel_err(e, m); } } - - // We take the inverse, but are careful not to divide by zero e.g. if some - // mesh bins are not reachable in the physical geometry. - xt::noalias(new_bounds) = - xt::where(xt::not_equal(new_bounds, 0.0), 1.0 / new_bounds, 0.0); - auto max_val = xt::amax(new_bounds)(); - xt::noalias(new_bounds) = new_bounds / (2.0 * max_val); - - // For bins that were missed, we use the minimum weight window value. This - // shouldn't matter except for plotting. - auto min_val = xt::amin(new_bounds)(); - xt::noalias(new_bounds) = - xt::where(xt::not_equal(new_bounds, 0.0), new_bounds, min_val); } - // make sure that values where the mean is zero are set s.t. the weight window - // value will be ignored - xt::filter(new_bounds, sum <= 0.0).fill(-1.0); + // Divide by volume of mesh elements +#pragma omp parallel for collapse(2) schedule(static) + for (int e = 0; e < e_bins; e++) { + for (int64_t m = 0; m < mesh_bins; m++) { + new_bounds(e, m) /= mesh_vols[m]; + } + } - // make sure the weight windows are ignored for any locations where the - // relative error is higher than the specified relative error threshold - xt::filter(new_bounds, rel_err > threshold).fill(-1.0); + if (method == WeightWindowUpdateMethod::MAGIC) { + // For MAGIC, weight windows are proportional to the forward fluxes. + // We normalize weight windows independently for each energy group. - // update the bounds of this weight window class - // noalias avoids additional memory allocation - xt::noalias(upper_ww_) = ratio * lower_ww_; + // Find group maximum and normalize (per energy group) + for (int e = 0; e < e_bins; e++) { + double group_max = 0.0; + + // Find maximum value across all elements in this energy group +#pragma omp parallel for schedule(static) reduction(max : group_max) + for (int64_t m = 0; m < mesh_bins; m++) { + if (new_bounds(e, m) > group_max) { + group_max = new_bounds(e, m); + } + } + + // Normalize values in this energy group by the maximum value + if (group_max > 0.0) { + double norm_factor = 1.0 / (2.0 * group_max); +#pragma omp parallel for schedule(static) + for (int64_t m = 0; m < mesh_bins; m++) { + new_bounds(e, m) *= norm_factor; + } + } + } + } else { + // For FW-CADIS, weight windows are inversely proportional to the adjoint + // fluxes. We normalize the weight windows across all energy groups. +#pragma omp parallel for collapse(2) schedule(static) + for (int e = 0; e < e_bins; e++) { + for (int64_t m = 0; m < mesh_bins; m++) { + // Take the inverse, but are careful not to divide by zero + if (new_bounds(e, m) != 0.0) { + new_bounds(e, m) = 1.0 / new_bounds(e, m); + } else { + new_bounds(e, m) = 0.0; + } + } + } + + // Find the maximum value across all elements + double max_val = 0.0; +#pragma omp parallel for collapse(2) schedule(static) reduction(max : max_val) + for (int e = 0; e < e_bins; e++) { + for (int64_t m = 0; m < mesh_bins; m++) { + if (new_bounds(e, m) > max_val) { + max_val = new_bounds(e, m); + } + } + } + + // Parallel normalization + if (max_val > 0.0) { + double norm_factor = 1.0 / (2.0 * max_val); +#pragma omp parallel for collapse(2) schedule(static) + for (int e = 0; e < e_bins; e++) { + for (int64_t m = 0; m < mesh_bins; m++) { + new_bounds(e, m) *= norm_factor; + } + } + } + } + + // Final processing +#pragma omp parallel for collapse(2) schedule(static) + for (int e = 0; e < e_bins; e++) { + for (int64_t m = 0; m < mesh_bins; m++) { + // Values where the mean is zero should be ignored + if (sum(e, m) <= 0.0) { + new_bounds(e, m) = -1.0; + } + // Values where the relative error is higher than the threshold should be + // ignored + else if (rel_err(e, m) > threshold) { + new_bounds(e, m) = -1.0; + } + // Set the upper bounds + upper_ww_(e, m) = ratio * lower_ww_(e, m); + } + } } void WeightWindows::check_tally_update_compatibility(const Tally* tally) From a6db05ac8b5f250e41d858d1f803fae811bf125f Mon Sep 17 00:00:00 2001 From: John Tramm Date: Wed, 25 Jun 2025 08:47:23 -0500 Subject: [PATCH 355/671] Optimize Mapping of Random Ray Source Regions to Tallies (#3465) --- .../openmc/random_ray/flat_source_domain.h | 2 +- src/random_ray/flat_source_domain.cpp | 19 +++++++++++++------ src/random_ray/random_ray_simulation.cpp | 5 +++-- src/random_ray/source_region.cpp | 8 -------- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index 9fdd55875..78351fcc5 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -34,7 +34,7 @@ public: int64_t add_source_to_scalar_flux(); virtual void batch_reset(); - void convert_source_regions_to_tallies(); + void convert_source_regions_to_tallies(int64_t start_sr_id); void reset_tally_volumes(); void random_ray_tally(); virtual void accumulate_iteration_flux(); diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 7404a0b29..41e93ec94 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -421,7 +421,12 @@ double FlatSourceDomain::compute_k_eff(double k_eff_old) const // be passed back to the caller to alert them that this function doesn't // need to be called for the remainder of the simulation. -void FlatSourceDomain::convert_source_regions_to_tallies() +// It takes as an argument the starting index in the source region array, +// and it will operate from that index until the end of the array. This +// is useful as it can be called for both explicit user source regions or +// when a source region mesh is overlaid. + +void FlatSourceDomain::convert_source_regions_to_tallies(int64_t start_sr_id) { openmc::simulation::time_tallies.start(); @@ -430,7 +435,7 @@ void FlatSourceDomain::convert_source_regions_to_tallies() // Attempt to generate mapping for all source regions #pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions(); sr++) { + for (int64_t sr = start_sr_id; sr < n_source_regions(); sr++) { // If this source region has not been hit by a ray yet, then // we aren't going to be able to map it, so skip it. @@ -468,7 +473,7 @@ void FlatSourceDomain::convert_source_regions_to_tallies() // Loop over all active tallies. This logic is essentially identical // to what happens when scanning for applicable tallies during // MC transport. - for (auto i_tally : model::active_tallies) { + for (int i_tally = 0; i_tally < model::tallies.size(); i_tally++) { Tally& tally {*model::tallies[i_tally]}; // Initialize an iterator over valid filter bin combinations. @@ -1525,6 +1530,9 @@ void FlatSourceDomain::finalize_discovered_source_regions() // order due to shared memory threading. std::sort(keys.begin(), keys.end()); + // Remember the index of the first new source region + int64_t start_sr_id = source_regions_.n_source_regions(); + // Append the source regions in the sorted key order. for (const auto& key : keys) { const SourceRegion& sr = discovered_source_regions_[key]; @@ -1532,9 +1540,8 @@ void FlatSourceDomain::finalize_discovered_source_regions() source_regions_.push_back(sr); } - // If any new source regions were discovered, we need to update the - // tally mapping between source regions and tally bins. - mapped_all_tallies_ = false; + // Map all new source regions to tallies + convert_source_regions_to_tallies(start_sr_id); } discovered_source_regions_.clear(); diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index 40a08c3af..388a778b8 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -508,8 +508,9 @@ void RandomRaySimulation::simulate() domain_->accumulate_iteration_flux(); // Generate mapping between source regions and tallies - if (!domain_->mapped_all_tallies_) { - domain_->convert_source_regions_to_tallies(); + if (!domain_->mapped_all_tallies_ && + !RandomRay::mesh_subdivision_enabled_) { + domain_->convert_source_regions_to_tallies(0); } // Use above mapping to contribute FSR flux data to appropriate diff --git a/src/random_ray/source_region.cpp b/src/random_ray/source_region.cpp index 69541f7f8..1205b995a 100644 --- a/src/random_ray/source_region.cpp +++ b/src/random_ray/source_region.cpp @@ -259,15 +259,11 @@ void SourceRegionContainer::adjoint_reset() MomentMatrix {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}); std::fill(mom_matrix_t_.begin(), mom_matrix_t_.end(), MomentMatrix {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}); - for (auto& task_set : volume_task_) { - task_set.clear(); - } std::fill(scalar_flux_old_.begin(), scalar_flux_old_.end(), 0.0); std::fill(scalar_flux_new_.begin(), scalar_flux_new_.end(), 0.0); std::fill(scalar_flux_final_.begin(), scalar_flux_final_.end(), 0.0); std::fill(source_.begin(), source_.end(), 0.0f); std::fill(external_source_.begin(), external_source_.end(), 0.0f); - std::fill(source_gradients_.begin(), source_gradients_.end(), MomentArray {0.0, 0.0, 0.0}); std::fill(flux_moments_old_.begin(), flux_moments_old_.end(), @@ -276,10 +272,6 @@ void SourceRegionContainer::adjoint_reset() MomentArray {0.0, 0.0, 0.0}); std::fill(flux_moments_t_.begin(), flux_moments_t_.end(), MomentArray {0.0, 0.0, 0.0}); - - for (auto& task_set : tally_task_) { - task_set.clear(); - } } } // namespace openmc From 01fa8056d130d0f0271d4c5c25867dae5f91b3c1 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 25 Jun 2025 13:44:53 -0500 Subject: [PATCH 356/671] Introduce WeightWindowsList class that enables export to HDF5 (#3456) --- docs/source/pythonapi/base.rst | 9 +- docs/source/usersguide/variance_reduction.rst | 2 +- openmc/settings.py | 14 +- openmc/weight_windows.py | 495 +++++++++++------- .../weightwindows/inputs_true.dat | 4 +- .../unit_tests/weightwindows/test_ww_list.py | 23 + 6 files changed, 337 insertions(+), 210 deletions(-) create mode 100644 tests/unit_tests/weightwindows/test_ww_list.py diff --git a/docs/source/pythonapi/base.rst b/docs/source/pythonapi/base.rst index d0e7d2439..2a9d0876c 100644 --- a/docs/source/pythonapi/base.rst +++ b/docs/source/pythonapi/base.rst @@ -37,7 +37,6 @@ Simulation Settings openmc.read_source_file openmc.write_source_file - openmc.wwinp_to_wws Material Specification ---------------------- @@ -259,8 +258,16 @@ Variance Reduction :template: myclass openmc.WeightWindows + openmc.WeightWindowsList openmc.WeightWindowGenerator + +.. autosummary:: + :toctree: generated + :nosignatures: + :template: myfunction.rst + openmc.hdf5_to_wws + openmc.wwinp_to_wws Coarse Mesh Finite Difference Acceleration diff --git a/docs/source/usersguide/variance_reduction.rst b/docs/source/usersguide/variance_reduction.rst index 3256fa35f..369e33e2d 100644 --- a/docs/source/usersguide/variance_reduction.rst +++ b/docs/source/usersguide/variance_reduction.rst @@ -162,7 +162,7 @@ solver, the Python input just needs to load the h5 file:: settings.weight_window_checkpoints = {'collision': True, 'surface': True} settings.survival_biasing = False - settings.weight_windows = openmc.hdf5_to_wws('weight_windows.h5') + settings.weight_windows = openmc.WeightWindowsList.from_hdf5('weight_windows.h5') settings.weight_windows_on = True The :class:`~openmc.WeightWindowGenerator` instance is not needed to load an diff --git a/openmc/settings.py b/openmc/settings.py index bd3a89e11..36eaec6cb 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -16,7 +16,7 @@ from .mesh import _read_meshes, RegularMesh, MeshBase from .source import SourceBase, MeshSource, IndependentSource from .utility_funcs import input_path from .volume import VolumeCalculation -from .weight_windows import WeightWindows, WeightWindowGenerator +from .weight_windows import WeightWindows, WeightWindowGenerator, WeightWindowsList class RunMode(Enum): @@ -313,7 +313,7 @@ class Settings: described in :ref:`verbosity`. volume_calculations : VolumeCalculation or iterable of VolumeCalculation Stochastic volume calculation specifications - weight_windows : WeightWindows or iterable of WeightWindows + weight_windows : WeightWindowsList Weight windows to use for variance reduction .. versionadded:: 0.13 @@ -424,7 +424,7 @@ class Settings: self._max_particles_in_flight = None self._max_particle_events = None self._write_initial_source = None - self._weight_windows = cv.CheckedList(WeightWindows, 'weight windows') + self._weight_windows = WeightWindowsList() self._weight_window_generators = cv.CheckedList(WeightWindowGenerator, 'weight window generators') self._weight_windows_on = None self._weight_windows_file = None @@ -1095,14 +1095,14 @@ class Settings: self._write_initial_source = value @property - def weight_windows(self) -> list[WeightWindows]: + def weight_windows(self) -> WeightWindowsList: return self._weight_windows @weight_windows.setter - def weight_windows(self, value: WeightWindows | Iterable[WeightWindows]): - if not isinstance(value, MutableSequence): + def weight_windows(self, value: WeightWindows | Sequence[WeightWindows]): + if not isinstance(value, Sequence): value = [value] - self._weight_windows = cv.CheckedList(WeightWindows, 'weight windows', value) + self._weight_windows = WeightWindowsList(value) @property def weight_windows_on(self) -> bool: diff --git a/openmc/weight_windows.py b/openmc/weight_windows.py index d52b81925..1b44747d5 100644 --- a/openmc/weight_windows.py +++ b/openmc/weight_windows.py @@ -1,6 +1,8 @@ from __future__ import annotations from numbers import Real, Integral from collections.abc import Iterable, Sequence +from pathlib import Path +from typing import Self import warnings import lxml.etree as ET @@ -14,6 +16,7 @@ import openmc.checkvalue as cv from openmc.checkvalue import PathLike from ._xml import get_text, clean_indentation from .mixin import IDManagerMixin +from .utility_funcs import change_directory class WeightWindows(IDManagerMixin): @@ -90,7 +93,7 @@ class WeightWindows(IDManagerMixin): survival_ratio : float Ratio of the survival weight to the lower weight window bound for rouletting - max_lower_bound_ratio: float + max_lower_bound_ratio : float Maximum allowed ratio of a particle's weight to the weight window's lower bound. (Default: 1.0) max_split : int @@ -114,7 +117,7 @@ class WeightWindows(IDManagerMixin): upper_bound_ratio: float | None = None, energy_bounds: Iterable[Real] | None = None, particle_type: str = 'neutron', - survival_ratio: float = 3, + survival_ratio: float = 3.0, max_lower_bound_ratio: float | None = None, max_split: int = 10, weight_cutoff: float = 1.e-38, @@ -354,7 +357,7 @@ class WeightWindows(IDManagerMixin): return element @classmethod - def from_xml_element(cls, elem: ET.Element, meshes: dict[int, MeshBase]) -> WeightWindows: + def from_xml_element(cls, elem: ET.Element, meshes: dict[int, MeshBase]) -> Self: """Generate weight window settings from an XML element Parameters @@ -408,7 +411,7 @@ class WeightWindows(IDManagerMixin): ) @classmethod - def from_hdf5(cls, group: h5py.Group, meshes: dict[int, MeshBase]) -> WeightWindows: + def from_hdf5(cls, group: h5py.Group, meshes: dict[int, MeshBase]) -> Self: """Create weight windows from HDF5 group Parameters @@ -458,7 +461,7 @@ class WeightWindows(IDManagerMixin): ) -def wwinp_to_wws(path: PathLike) -> list[WeightWindows]: +def wwinp_to_wws(path: PathLike) -> WeightWindowsList: """Create WeightWindows instances from a wwinp file .. versionadded:: 0.13.1 @@ -470,190 +473,13 @@ def wwinp_to_wws(path: PathLike) -> list[WeightWindows]: Returns ------- - list of openmc.WeightWindows + WeightWindowsList """ - - with open(path) as wwinp: - # BLOCK 1 - header = wwinp.readline().split(None, 4) - # read file type, time-dependence, number of - # particles, mesh type and problem identifier - _if, iv, ni, nr = [int(x) for x in header[:4]] - - # header value checks - if _if != 1: - raise ValueError(f'Found incorrect file type, if: {_if}') - - if iv > 1: - # read number of time bins for each particle, 'nt(1...ni)' - nt = np.fromstring(wwinp.readline(), sep=' ', dtype=int) - - # raise error if time bins are present for now - raise ValueError('Time-dependent weight windows ' - 'are not yet supported') - else: - nt = ni * [1] - - # read number of energy bins for each particle, 'ne(1...ni)' - ne = np.fromstring(wwinp.readline(), sep=' ', dtype=int) - - # read coarse mesh dimensions and lower left corner - mesh_description = np.fromstring(wwinp.readline(), sep=' ') - nfx, nfy, nfz = mesh_description[:3].astype(int) - xyz0 = mesh_description[3:] - - # read cylindrical and spherical mesh vectors if present - if nr == 16: - # read number of coarse bins - line_arr = np.fromstring(wwinp.readline(), sep=' ') - ncx, ncy, ncz = line_arr[:3].astype(int) - # read polar vector (x1, y1, z1) - xyz1 = line_arr[3:] - # read azimuthal vector (x2, y2, z2) - line_arr = np.fromstring(wwinp.readline(), sep=' ') - xyz2 = line_arr[:3] - - # Get polar and azimuthal axes - polar_axis = xyz1 - xyz0 - azimuthal_axis = xyz2 - xyz0 - - # Check for polar axis other than (0, 0, 1) - norm = np.linalg.norm(polar_axis) - if not np.isclose(polar_axis[2]/norm, 1.0): - raise NotImplementedError('Polar axis not aligned to z-axis not supported') - - # Check for azimuthal axis other than (1, 0, 0) - norm = np.linalg.norm(azimuthal_axis) - if not np.isclose(azimuthal_axis[0]/norm, 1.0): - raise NotImplementedError('Azimuthal axis not aligned to x-axis not supported') - - # read geometry type - nwg = int(line_arr[-1]) - - elif nr == 10: - # read rectilinear data: - # number of coarse mesh bins and mesh type - ncx, ncy, ncz, nwg = \ - np.fromstring(wwinp.readline(), sep=' ').astype(int) - else: - raise RuntimeError(f'Invalid mesh description (nr) found: {nr}') - - # read BLOCK 2 and BLOCK 3 data into a single array - ww_data = np.fromstring(wwinp.read(), sep=' ') - - # extract mesh data from the ww_data array - start_idx = 0 - - # first values in the mesh definition arrays are the first - # coordinate of the grid - end_idx = start_idx + 1 + 3 * ncx - i0, i_vals = ww_data[start_idx], ww_data[start_idx+1:end_idx] - start_idx = end_idx - - end_idx = start_idx + 1 + 3 * ncy - j0, j_vals = ww_data[start_idx], ww_data[start_idx+1:end_idx] - start_idx = end_idx - - end_idx = start_idx + 1 + 3 * ncz - k0, k_vals = ww_data[start_idx], ww_data[start_idx+1:end_idx] - start_idx = end_idx - - # mesh consistency checks - if nr == 16 and nwg == 1 or nr == 10 and nwg != 1: - raise ValueError(f'Mesh description in header ({nr}) ' - f'does not match the mesh type ({nwg})') - - if nr == 10 and (xyz0 != (i0, j0, k0)).any(): - raise ValueError(f'Mesh origin in the header ({xyz0}) ' - f' does not match the origin in the mesh ' - f' description ({i0, j0, k0})') - - # create openmc mesh object - grids = [] - mesh_definition = [(i0, i_vals, nfx), (j0, j_vals, nfy), (k0, k_vals, nfz)] - for grid0, grid_vals, n_pnts in mesh_definition: - # file spec checks for the mesh definition - if (grid_vals[2::3] != 1.0).any(): - raise ValueError('One or more mesh ratio value, qx, ' - 'is not equal to one') - - s = int(grid_vals[::3].sum()) - if s != n_pnts: - raise ValueError(f'Sum of the fine bin entries, {s}, does ' - f'not match the number of fine bins, {n_pnts}') - - # extend the grid based on the next coarse bin endpoint, px - # and the number of fine bins in the coarse bin, sx - intervals = grid_vals.reshape(-1, 3) - coords = [grid0] - for sx, px, qx in intervals: - coords += np.linspace(coords[-1], px, int(sx + 1)).tolist()[1:] - - grids.append(np.array(coords)) - - if nwg == 1: - mesh = RectilinearMesh() - mesh.x_grid, mesh.y_grid, mesh.z_grid = grids - elif nwg == 2: - mesh = CylindricalMesh( - r_grid=grids[0], - z_grid=grids[1], - phi_grid=grids[2], - origin = xyz0, - ) - elif nwg == 3: - mesh = SphericalMesh( - r_grid=grids[0], - theta_grid=grids[1], - phi_grid=grids[2], - origin = xyz0 - ) - - # extract weight window values from array - wws = [] - for ne_i, nt_i, particle_type in zip(ne, nt, ('neutron', 'photon')): - # no information to read for this particle if - # either the energy bins or time bins are empty - if ne_i == 0 or nt_i == 0: - continue - - if iv > 1: - # time bins are parsed but unused for now - end_idx = start_idx + nt_i - time_bounds = ww_data[start_idx:end_idx] - np.insert(time_bounds, (0,), (0.0,)) - start_idx = end_idx - - # read energy boundaries - end_idx = start_idx + ne_i - energy_bounds = np.insert(ww_data[start_idx:end_idx], (0,), (0.0,)) - # convert from MeV to eV - energy_bounds *= 1e6 - start_idx = end_idx - - # read weight window values - end_idx = start_idx + (nfx * nfy * nfz) * nt_i * ne_i - - # read values and reshape according to ordering - # slowest to fastest: t, e, z, y, x - # reorder with transpose since our ordering is x, y, z, e, t - ww_shape = (nt_i, ne_i, nfz, nfy, nfx) - ww_values = ww_data[start_idx:end_idx].reshape(ww_shape).T - # Only use first time bin since we don't support time dependent weight - # windows yet. - ww_values = ww_values[:, :, :, :, 0] - start_idx = end_idx - - # create a weight window object - ww = WeightWindows(id=None, - mesh=mesh, - lower_ww_bounds=ww_values, - upper_bound_ratio=5.0, - energy_bounds=energy_bounds, - particle_type=particle_type) - wws.append(ww) - - return wws + warnings.warn( + "This function is deprecated in favor of 'WeightWindowsList.from_wwinp'", + FutureWarning + ) + return WeightWindowsList.from_wwinp(path) class WeightWindowGenerator: @@ -886,7 +712,7 @@ class WeightWindowGenerator: return element @classmethod - def from_xml_element(cls, elem: ET.Element, meshes: dict) -> WeightWindowGenerator: + def from_xml_element(cls, elem: ET.Element, meshes: dict) -> Self: """ Create a weight window generation object from an XML element @@ -929,8 +755,8 @@ class WeightWindowGenerator: return wwg -def hdf5_to_wws(path='weight_windows.h5'): - """Create WeightWindows instances from a weight windows HDF5 file +def hdf5_to_wws(path='weight_windows.h5') -> WeightWindowsList: + """Create a WeightWindowsList from a weight windows HDF5 file .. versionadded:: 0.14.0 @@ -941,13 +767,284 @@ def hdf5_to_wws(path='weight_windows.h5'): Returns ------- - list of openmc.WeightWindows + WeightWindowsList """ + warnings.warn( + "This function is deprecated in favor of 'WeightWindowsList.from_hdf5'", + FutureWarning + ) + return WeightWindowsList.from_hdf5(path) - with h5py.File(path) as h5_file: - # read in all of the meshes in the mesh node - meshes = {} - for mesh_group in h5_file['meshes']: - mesh = MeshBase.from_hdf5(h5_file['meshes'][mesh_group]) - meshes[mesh.id] = mesh - return [WeightWindows.from_hdf5(ww, meshes) for ww in h5_file['weight_windows'].values()] + +class WeightWindowsList(list): + """A list of WeightWindows objects. + + .. versionadded:: 0.15.3 + + Parameters + ---------- + iterable : iterable of openmc.WeightWindows + An iterable of WeightWindows objects to initialize the list with + + """ + def __init__(self, iterable: Iterable[WeightWindows] = ()): + super().__init__(iterable) + + @classmethod + def from_hdf5(cls, path: PathLike = 'weight_windows.h5') -> Self: + """Create WeightWindowsList from a weight windows HDF5 file. + + Parameters + ---------- + path : PathLike + Path to the weight windows hdf5 file + + Returns + ------- + WeightWindowsList + A list of WeightWindows objects read from the file + """ + + with h5py.File(path) as h5_file: + # read in all of the meshes in the mesh node + meshes = {} + for mesh_group in h5_file['meshes']: + mesh = MeshBase.from_hdf5(h5_file['meshes'][mesh_group]) + meshes[mesh.id] = mesh + wws = [ + WeightWindows.from_hdf5(ww, meshes) + for ww in h5_file['weight_windows'].values() + ] + + return cls(wws) + + @classmethod + def from_wwinp(cls, path: PathLike) -> Self: + """Create WeightWindowsList from a wwinp file. + + Parameters + ---------- + path : PathLike + Path to the wwinp file + + Returns + ------- + WeightWindowsList + A list of WeightWindows objects read from the file + """ + + with open(path) as wwinp: + # BLOCK 1 + header = wwinp.readline().split(None, 4) + # read file type, time-dependence, number of + # particles, mesh type and problem identifier + _if, iv, ni, nr = [int(x) for x in header[:4]] + + # header value checks + if _if != 1: + raise ValueError(f'Found incorrect file type, if: {_if}') + + if iv > 1: + # read number of time bins for each particle, 'nt(1...ni)' + nt = np.fromstring(wwinp.readline(), sep=' ', dtype=int) + + # raise error if time bins are present for now + raise ValueError('Time-dependent weight windows ' + 'are not yet supported') + else: + nt = ni * [1] + + # read number of energy bins for each particle, 'ne(1...ni)' + ne = np.fromstring(wwinp.readline(), sep=' ', dtype=int) + + # read coarse mesh dimensions and lower left corner + mesh_description = np.fromstring(wwinp.readline(), sep=' ') + nfx, nfy, nfz = mesh_description[:3].astype(int) + xyz0 = mesh_description[3:] + + # read cylindrical and spherical mesh vectors if present + if nr == 16: + # read number of coarse bins + line_arr = np.fromstring(wwinp.readline(), sep=' ') + ncx, ncy, ncz = line_arr[:3].astype(int) + # read polar vector (x1, y1, z1) + xyz1 = line_arr[3:] + # read azimuthal vector (x2, y2, z2) + line_arr = np.fromstring(wwinp.readline(), sep=' ') + xyz2 = line_arr[:3] + + # Get polar and azimuthal axes + polar_axis = xyz1 - xyz0 + azimuthal_axis = xyz2 - xyz0 + + # Check for polar axis other than (0, 0, 1) + norm = np.linalg.norm(polar_axis) + if not np.isclose(polar_axis[2]/norm, 1.0): + raise NotImplementedError('Polar axis not aligned to z-axis not supported') + + # Check for azimuthal axis other than (1, 0, 0) + norm = np.linalg.norm(azimuthal_axis) + if not np.isclose(azimuthal_axis[0]/norm, 1.0): + raise NotImplementedError('Azimuthal axis not aligned to x-axis not supported') + + # read geometry type + nwg = int(line_arr[-1]) + + elif nr == 10: + # read rectilinear data: + # number of coarse mesh bins and mesh type + ncx, ncy, ncz, nwg = \ + np.fromstring(wwinp.readline(), sep=' ').astype(int) + else: + raise RuntimeError(f'Invalid mesh description (nr) found: {nr}') + + # read BLOCK 2 and BLOCK 3 data into a single array + ww_data = np.fromstring(wwinp.read(), sep=' ') + + # extract mesh data from the ww_data array + start_idx = 0 + + # first values in the mesh definition arrays are the first + # coordinate of the grid + end_idx = start_idx + 1 + 3 * ncx + i0, i_vals = ww_data[start_idx], ww_data[start_idx+1:end_idx] + start_idx = end_idx + + end_idx = start_idx + 1 + 3 * ncy + j0, j_vals = ww_data[start_idx], ww_data[start_idx+1:end_idx] + start_idx = end_idx + + end_idx = start_idx + 1 + 3 * ncz + k0, k_vals = ww_data[start_idx], ww_data[start_idx+1:end_idx] + start_idx = end_idx + + # mesh consistency checks + if nr == 16 and nwg == 1 or nr == 10 and nwg != 1: + raise ValueError(f'Mesh description in header ({nr}) ' + f'does not match the mesh type ({nwg})') + + if nr == 10 and (xyz0 != (i0, j0, k0)).any(): + raise ValueError(f'Mesh origin in the header ({xyz0}) ' + f' does not match the origin in the mesh ' + f' description ({i0, j0, k0})') + + # create openmc mesh object + grids = [] + mesh_definition = [(i0, i_vals, nfx), (j0, j_vals, nfy), (k0, k_vals, nfz)] + for grid0, grid_vals, n_pnts in mesh_definition: + # file spec checks for the mesh definition + if (grid_vals[2::3] != 1.0).any(): + raise ValueError('One or more mesh ratio value, qx, ' + 'is not equal to one') + + s = int(grid_vals[::3].sum()) + if s != n_pnts: + raise ValueError(f'Sum of the fine bin entries, {s}, does ' + f'not match the number of fine bins, {n_pnts}') + + # extend the grid based on the next coarse bin endpoint, px + # and the number of fine bins in the coarse bin, sx + intervals = grid_vals.reshape(-1, 3) + coords = [grid0] + for sx, px, qx in intervals: + coords += np.linspace(coords[-1], px, int(sx + 1)).tolist()[1:] + + grids.append(np.array(coords)) + + if nwg == 1: + mesh = RectilinearMesh() + mesh.x_grid, mesh.y_grid, mesh.z_grid = grids + elif nwg == 2: + mesh = CylindricalMesh( + r_grid=grids[0], + z_grid=grids[1], + phi_grid=grids[2], + origin = xyz0, + ) + elif nwg == 3: + mesh = SphericalMesh( + r_grid=grids[0], + theta_grid=grids[1], + phi_grid=grids[2], + origin = xyz0 + ) + + # extract weight window values from array + wws = cls() + for ne_i, nt_i, particle_type in zip(ne, nt, ('neutron', 'photon')): + # no information to read for this particle if + # either the energy bins or time bins are empty + if ne_i == 0 or nt_i == 0: + continue + + if iv > 1: + # time bins are parsed but unused for now + end_idx = start_idx + nt_i + time_bounds = ww_data[start_idx:end_idx] + np.insert(time_bounds, (0,), (0.0,)) + start_idx = end_idx + + # read energy boundaries + end_idx = start_idx + ne_i + energy_bounds = np.insert(ww_data[start_idx:end_idx], (0,), (0.0,)) + # convert from MeV to eV + energy_bounds *= 1e6 + start_idx = end_idx + + # read weight window values + end_idx = start_idx + (nfx * nfy * nfz) * nt_i * ne_i + + # read values and reshape according to ordering + # slowest to fastest: t, e, z, y, x + # reorder with transpose since our ordering is x, y, z, e, t + ww_shape = (nt_i, ne_i, nfz, nfy, nfx) + ww_values = ww_data[start_idx:end_idx].reshape(ww_shape).T + # Only use first time bin since we don't support time dependent weight + # windows yet. + ww_values = ww_values[:, :, :, :, 0] + start_idx = end_idx + + # create a weight window object + ww = WeightWindows(id=None, + mesh=mesh, + lower_ww_bounds=ww_values, + upper_bound_ratio=5.0, + energy_bounds=energy_bounds, + particle_type=particle_type) + wws.append(ww) + + return wws + + def export_to_hdf5(self, path: PathLike = 'weight_windows.h5', **init_kwargs): + """Write weight windows to an HDF5 file. + + Parameters + ---------- + path : PathLike + Path to the file to write weight windows to + **init_kwargs + Keyword arguments passed to :func:`openmc.lib.init` + + """ + import openmc.lib + cv.check_type('path', path, PathLike) + + # Create a temporary model with the weight windows + model = openmc.Model() + sph = openmc.Sphere(boundary_type='vacuum') + cell = openmc.Cell(region=-sph) + model.geometry = openmc.Geometry([cell]) + model.settings.weight_windows = self + model.settings.particles = 100 + model.settings.batches = 1 + + # Get absolute path before moving to temporary directory + path = Path(path).resolve() + + with change_directory(tmpdir=True): + # Write the model to an XML file + model.export_to_model_xml() + + # Load the model with openmc.lib and then export it to an HDF5 file + with openmc.lib.run_in_memory(**init_kwargs): + openmc.lib.export_weight_windows(path) diff --git a/tests/regression_tests/weightwindows/inputs_true.dat b/tests/regression_tests/weightwindows/inputs_true.dat index dcc41ae84..91fec4af9 100644 --- a/tests/regression_tests/weightwindows/inputs_true.dat +++ b/tests/regression_tests/weightwindows/inputs_true.dat @@ -49,7 +49,7 @@ 0.0 0.5 20000000.0 -1.0 0.007673145137236567 6.542700644645627e-07 0.0017541380096893788 0.0007245451610090619 7.476545089278482e-06 2.253382683081525e-06 2.1040609012865134e-05 0.08285899874014539 -1.0 1.0262431550731535e-13 3.3164825349503764e-16 0.0064803314120165335 3.5981031766416143e-06 -1.0 1.6205064883026195e-11 1.7770136411255912e-05 0.0001670700632870335 4.323138781337963e-05 -1.0 -1.0 6.922231352729155e-05 0.06550426960512012 -1.0 2.5639656866250453e-06 1.4508262073256659e-13 -1.0 -1.0 0.0009127050763987511 3.8523247550378815e-12 -1.0 0.0008175033526488423 -1.0 0.015148978428271613 2.5157355790201773e-07 2.1612150425221703e-06 1.3181562352445092e-10 2.9282097947816224e-05 2.762955748645507e-05 2.936268747135548e-14 1.6859219614058024e-19 3.937160538176268e-07 2.6011787393916806e-06 0.001570829313580841 -1.0 1.7041669224797384e-09 1.6421491993751715e-11 0.4899999304038166 6.948292975998466e-07 -1.0 -1.0 0.08657573566388353 0.006639379668946672 1.681305446488884e-11 2.2487617828938326e-05 4.109066205029878e-05 0.08288806458368345 -1.0 0.007385384405891666 -1.0 -1.0 1.008394964750947e-06 6.655926357459275e-08 0.0016279794552427574 1.4825219199133353e-12 -1.0 1.93847081892941e-05 1.284759166582202e-05 5.1161740516205715e-05 2.5447281241730366e-07 3.853097455417877e-06 -1.0 8.887234042637684e-05 0.01621821782442112 -1.0 1.8747824667478637e-14 5.97653814110781e-06 -1.0 -1.0 1.2296747260635405e-06 -1.0 -1.0 7.754700849337902e-06 7.091263906557291e-05 3.2799045883372075e-06 0.00020573181450240786 5.6949597066784976e-05 1.271128669411295e-07 0.0010019432401508087 -1.0 -1.0 6.848642863465585e-07 5.783974359937857e-06 2.380955631371226e-06 4.756872959630257e-05 2.1360401784344975e-18 0.00046029617115127556 1.0092905083158804e-11 0.08095280568305474 3.89841667138598e-08 4.354946981329799e-05 -1.0 0.5 0.0015447823995470042 -1.0 -1.0 0.001959411999677113 0.48573842213878143 -1.0 7.962899789920809e-06 5.619835883512353e-14 0.0835648710074336 2.7207433165796702e-11 0.00027464244062887683 4.076762579823379e-17 2.124999182004655e-05 8.037131813528501e-07 8.846493399850663e-06 1.080711295768551e-05 -1.0 -1.0 0.0010737081832502356 2.679340942769232e-06 2.1648918040467363e-05 0.0002668579837809081 1.3455341306162382e-05 2.5629068330446215e-05 1.3228013765525015e-05 -1.0 -1.0 7.002861620919232e-08 1.5172912057312398e-14 -1.0 7.989579285134734e-06 2.639268952045579e-10 -1.0 0.013330619853379314 0.00021534568699157805 2.4469456008493614e-09 8.211982683649991e-09 1.951356547084204e-15 0.00016419031150495913 5.985108617124989e-06 1.28584557092134e-05 1.5476769237571295e-14 5.5379154134462336e-08 0.0016637126543321873 2.499931357628383e-07 8.020767115181574e-07 2.7697233171399416e-13 -1.0 0.007197662162700777 -1.0 0.0833931797381589 2.8226276944532696e-05 3.353295403842037e-05 5.504813693036933e-12 0.007136750029575816 0.08636569925236791 -1.0 -1.0 6.315154082213375e-07 0.4877856021170283 -1.0 3.7051518220083886e-06 -1.0 0.0017950613169359904 7.84289689494925e-06 9.041149893307806e-07 -1.0 8.48176145671274e-10 9.03108551826469e-05 3.634644995026692e-05 2.0295213621716706e-09 1.0305672698745657e-11 1.5550455959178486e-06 0.01664109232689093 8.533030345699353e-17 0.0009252532821720597 -1.0 7.695031155172816e-14 0.0007151459162818186 -1.0 -1.0 5.182269672663266e-10 3.0160758454323657e-07 -1.0 0.06539005235506369 3.452537179033895e-05 -1.0 -1.0 3.855884234344845e-06 0.00017699858357744463 2.594951085377588e-06 1.1333627604823673e-09 -1.0 2.4306375693722205e-06 0.0063790654507599135 2.2060992580622125e-11 -1.0 -1.0 0.0848872967381878 3.932389299316285e-06 1.2911827198500372e-08 1.957092814065688e-05 0.0007213722022489493 0.0018327593437608 1.6699254277734109e-06 0.007188410694198128 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -10.0 0.07673145137236567 6.5427006446456266e-06 0.017541380096893787 0.0072454516100906195 7.476545089278483e-05 2.253382683081525e-05 0.00021040609012865135 0.828589987401454 -10.0 1.0262431550731535e-12 3.3164825349503764e-15 0.06480331412016534 3.5981031766416144e-05 -10.0 1.6205064883026195e-10 0.0001777013641125591 0.001670700632870335 0.0004323138781337963 -10.0 -10.0 0.0006922231352729155 0.6550426960512012 -10.0 2.5639656866250452e-05 1.4508262073256658e-12 -10.0 -10.0 0.009127050763987512 3.852324755037882e-11 -10.0 0.008175033526488424 -10.0 0.15148978428271614 2.515735579020177e-06 2.1612150425221703e-05 1.3181562352445092e-09 0.00029282097947816227 0.0002762955748645507 2.9362687471355483e-13 1.6859219614058023e-18 3.937160538176268e-06 2.6011787393916804e-05 0.015708293135808408 -10.0 1.7041669224797384e-08 1.6421491993751715e-10 4.899999304038166 6.948292975998466e-06 -10.0 -10.0 0.8657573566388354 0.06639379668946672 1.681305446488884e-10 0.00022487617828938325 0.0004109066205029878 0.8288806458368345 -10.0 0.07385384405891665 -10.0 -10.0 1.0083949647509469e-05 6.655926357459275e-07 0.016279794552427576 1.4825219199133352e-11 -10.0 0.000193847081892941 0.0001284759166582202 0.0005116174051620571 2.5447281241730365e-06 3.853097455417877e-05 -10.0 0.0008887234042637684 0.16218217824421122 -10.0 1.8747824667478637e-13 5.97653814110781e-05 -10.0 -10.0 1.2296747260635405e-05 -10.0 -10.0 7.754700849337902e-05 0.0007091263906557291 3.2799045883372076e-05 0.0020573181450240785 0.0005694959706678497 1.271128669411295e-06 0.010019432401508087 -10.0 -10.0 6.848642863465585e-06 5.7839743599378564e-05 2.380955631371226e-05 0.0004756872959630257 2.1360401784344976e-17 0.004602961711512756 1.0092905083158804e-10 0.8095280568305474 3.89841667138598e-07 0.00043549469813297995 -10.0 5.0 0.015447823995470043 -10.0 -10.0 0.01959411999677113 4.8573842213878144 -10.0 7.96289978992081e-05 5.619835883512353e-13 0.8356487100743359 2.7207433165796704e-10 0.002746424406288768 4.076762579823379e-16 0.00021249991820046548 8.037131813528501e-06 8.846493399850663e-05 0.0001080711295768551 -10.0 -10.0 0.010737081832502356 2.679340942769232e-05 0.00021648918040467362 0.0026685798378090807 0.00013455341306162382 0.00025629068330446217 0.00013228013765525016 -10.0 -10.0 7.002861620919232e-07 1.51729120573124e-13 -10.0 7.989579285134734e-05 2.639268952045579e-09 -10.0 0.13330619853379314 0.0021534568699157807 2.4469456008493615e-08 8.211982683649991e-08 1.951356547084204e-14 0.0016419031150495913 5.985108617124989e-05 0.000128584557092134 1.5476769237571296e-13 5.537915413446234e-07 0.016637126543321872 2.499931357628383e-06 8.020767115181574e-06 2.7697233171399415e-12 -10.0 0.07197662162700777 -10.0 0.8339317973815891 0.000282262769445327 0.00033532954038420373 5.504813693036933e-11 0.07136750029575815 0.8636569925236791 -10.0 -10.0 6.3151540822133754e-06 4.877856021170283 -10.0 3.7051518220083885e-05 -10.0 0.017950613169359905 7.84289689494925e-05 9.041149893307805e-06 -10.0 8.48176145671274e-09 0.000903108551826469 0.0003634644995026692 2.0295213621716707e-08 1.0305672698745658e-10 1.5550455959178486e-05 0.1664109232689093 8.533030345699353e-16 0.009252532821720597 -10.0 7.695031155172816e-13 0.007151459162818186 -10.0 -10.0 5.182269672663266e-09 3.0160758454323656e-06 -10.0 0.6539005235506369 0.0003452537179033895 -10.0 -10.0 3.855884234344845e-05 0.0017699858357744464 2.594951085377588e-05 1.1333627604823672e-08 -10.0 2.4306375693722206e-05 0.06379065450759913 2.2060992580622125e-10 -10.0 -10.0 0.848872967381878 3.9323892993162844e-05 1.2911827198500372e-07 0.0001957092814065688 0.0072137220224894934 0.018327593437608 1.669925427773411e-05 0.07188410694198127 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 - 3 + 3.0 1.5 10 1e-38 @@ -65,7 +65,7 @@ 0.0 0.5 20000000.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -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.06827568359008944 0.00017329528628152923 0.018541112907970003 0.011113024641218336 8.76041935303527e-05 8.168081568291806e-05 0.00046693784586018913 0.4952432605379829 2.2512327083641865e-10 4.6258592060517866e-07 2.7044817176109556e-05 0.054902816619122045 0.00014126153603265097 8.304845400451923e-08 2.2950716170641463e-05 0.0001335621430311088 0.003432919201293737 0.0005197081842709918 5.726152701849347e-16 -1.0 0.0013332554437480327 0.4346984808472855 7.691527781829036e-17 0.00011042047857307253 3.0571050855707084e-06 -1.0 4.986698316296507e-06 0.011128424196387618 1.3106335518133802e-05 5.447493368067179e-10 0.011440381012864086 4.670673837764874e-08 0.12364069326350118 0.00012558879474108074 3.136829920007009e-05 2.34750482787598e-05 0.001537590300149361 0.0017693279949094218 6.717625881826167e-05 4.587143797469485e-08 2.6211239246796104e-05 0.00030002326388383244 0.01729816946709781 4.5757238372058234e-11 6.92296299242814e-05 3.792209399652396e-06 -1.0 9.256616392487858e-05 8.541606727879512e-07 -1.0 0.49317081643076643 0.05500108683629957 1.298062961617927e-05 0.0006870228272606287 0.0008231482263927564 0.5 3.419181071650816e-06 0.06433819434431046 6.574706543646946e-16 4.0404626147344e-13 0.0003782322077390033 0.00012637534695610975 0.017602971074583987 1.0044390728858277e-05 -1.0 0.0008787411098754915 0.00023686818807101741 0.0009304497896205121 6.949587369646497e-05 0.00012918433677593975 2.0707648529746894e-06 0.0023295401596005313 0.11819165666519223 -1.0 2.1606550732170693e-05 0.00022057970761624768 -1.0 1.0325207105027347e-08 0.0002029396545382813 2.1234980618763222e-13 -1.0 0.00024175998138228048 0.001178616843705956 0.0001399807456616496 0.0038811025614225057 0.0011076796092102186 0.00010814452764860558 0.010955637485319249 -1.0 9.382800796426366e-17 5.098747893974248e-05 0.00027052054298385255 0.00021305826462238593 0.000808100901275472 4.233410073487772e-06 0.0051513292132259495 8.616027294555462e-07 0.4873897581604287 2.9093345975075226e-05 0.000469589568463265 -1.0 -1.0 0.017802624844035688 1.3116807133030035e-15 2.617878055106949e-16 0.01884234453311008 -1.0 2.027381220364608e-16 0.0004299195570308035 1.4628847959717225e-05 0.4899952354672477 2.774067049392004e-07 0.004441328419270369 4.279460329782799e-09 0.0012297303081805393 0.0002792896409558745 0.0006934466049804711 8.370896020975689e-05 3.8270536025799805e-15 -1.0 0.01208261392046878 0.0001497958445994315 0.0015016500045374082 0.005384625561469684 0.00025519632243117644 0.0011028020390507682 0.0002362355168421952 -1.0 -1.0 8.862760180332873e-05 1.3806577785135177e-06 -1.0 0.0001319677450153934 2.6866894852481906e-05 -1.0 0.11510120013926417 0.002672923773363389 1.2257360246712472e-05 8.544159282151438e-05 2.777791741571174e-05 0.0014957033281168012 0.0003257646134837451 0.0008919466185572302 1.2907171695294846e-13 4.6842350882367666e-05 0.01721806295736377 0.00025106741008066317 0.00022234471595443313 2.101790478097517e-09 5.752120832330886e-11 0.06428782790571179 3.2478301680021854e-08 0.4903495510314233 0.001146714816952061 0.0004777967757842694 1.0882011018684852e-06 0.05708498338352203 0.4981497862127628 -1.0 3.2790914744011534e-06 0.00010077009726691265 -1.0 2.210707325798542e-07 4.3166052799897375e-05 -1.0 0.01825795827020865 0.0003182108781955478 3.8561696337086434e-05 1.0995322456277382e-06 9.53380658669839e-06 0.001550036616828061 0.0012648333883989995 5.272021975060514e-06 1.9257248009114363e-05 0.00016757997590048827 0.11891251244906934 5.515522196186723e-06 0.01102706140784316 9.904481358675478e-13 3.520674728719278e-05 0.011294708566804167 7.239087311622819e-08 -1.0 3.5740607788414942e-06 0.00035983547999740665 1.0588257623722672e-07 0.42372713683725016 0.0011273340972061078 8.582011716859219e-15 1.1514579117272292e-10 0.00043130977711323417 0.0027847329307350423 0.00017287873387910357 6.401055165502384e-06 1.722191123879782e-08 0.00020848309220162036 0.05639071613735579 1.8064550990294753e-05 1.289814511028015e-06 1.078687446476881e-14 0.4897735047310072 0.0002981891705231209 0.00016079768514218546 9.332371598523186e-05 0.011724714376848187 0.019040707071165303 0.00014242053195529864 0.06684484191345574 -1.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.6827568359008944 0.0017329528628152924 0.18541112907970003 0.11113024641218336 0.0008760419353035269 0.0008168081568291806 0.004669378458601891 4.952432605379829 2.2512327083641864e-09 4.625859206051786e-06 0.00027044817176109554 0.5490281661912204 0.0014126153603265096 8.304845400451923e-07 0.00022950716170641464 0.001335621430311088 0.03432919201293737 0.0051970818427099184 5.726152701849347e-15 -10.0 0.013332554437480326 4.346984808472855 7.6915277818290365e-16 0.0011042047857307254 3.0571050855707086e-05 -10.0 4.986698316296507e-05 0.11128424196387618 0.000131063355181338 5.447493368067179e-09 0.11440381012864086 4.670673837764874e-07 1.2364069326350118 0.0012558879474108074 0.0003136829920007009 0.000234750482787598 0.01537590300149361 0.01769327994909422 0.0006717625881826168 4.587143797469485e-07 0.000262112392467961 0.0030002326388383245 0.1729816946709781 4.5757238372058235e-10 0.0006922962992428139 3.792209399652396e-05 -10.0 0.0009256616392487858 8.541606727879512e-06 -10.0 4.931708164307665 0.5500108683629957 0.00012980629616179268 0.006870228272606287 0.008231482263927564 5.0 3.419181071650816e-05 0.6433819434431045 6.574706543646946e-15 4.0404626147344005e-12 0.003782322077390033 0.0012637534695610975 0.17602971074583987 0.00010044390728858278 -10.0 0.008787411098754914 0.002368681880710174 0.00930449789620512 0.0006949587369646498 0.0012918433677593976 2.0707648529746893e-05 0.023295401596005315 1.1819165666519222 -10.0 0.00021606550732170693 0.0022057970761624767 -10.0 1.0325207105027347e-07 0.0020293965453828133 2.123498061876322e-12 -10.0 0.0024175998138228046 0.01178616843705956 0.0013998074566164962 0.03881102561422506 0.011076796092102187 0.0010814452764860557 0.10955637485319249 -10.0 9.382800796426367e-16 0.0005098747893974248 0.0027052054298385255 0.0021305826462238594 0.00808100901275472 4.233410073487772e-05 0.0515132921322595 8.61602729455546e-06 4.873897581604287 0.0002909334597507523 0.0046958956846326495 -10.0 -10.0 0.1780262484403569 1.3116807133030034e-14 2.6178780551069492e-15 0.1884234453311008 -10.0 2.027381220364608e-15 0.004299195570308035 0.00014628847959717226 4.899952354672477 2.774067049392004e-06 0.04441328419270369 4.279460329782799e-08 0.012297303081805393 0.002792896409558745 0.006934466049804711 0.0008370896020975689 3.8270536025799805e-14 -10.0 0.1208261392046878 0.001497958445994315 0.015016500045374082 0.05384625561469684 0.0025519632243117644 0.011028020390507681 0.002362355168421952 -10.0 -10.0 0.0008862760180332873 1.3806577785135176e-05 -10.0 0.001319677450153934 0.00026866894852481903 -10.0 1.1510120013926417 0.026729237733633893 0.00012257360246712473 0.0008544159282151438 0.0002777791741571174 0.014957033281168012 0.0032576461348374514 0.008919466185572301 1.2907171695294846e-12 0.00046842350882367664 0.17218062957363772 0.0025106741008066318 0.0022234471595443312 2.101790478097517e-08 5.752120832330886e-10 0.6428782790571179 3.2478301680021856e-07 4.903495510314233 0.01146714816952061 0.004777967757842694 1.0882011018684853e-05 0.5708498338352204 4.981497862127628 -10.0 3.279091474401153e-05 0.0010077009726691265 -10.0 2.210707325798542e-06 0.00043166052799897375 -10.0 0.1825795827020865 0.0031821087819554777 0.00038561696337086434 1.0995322456277382e-05 9.53380658669839e-05 0.01550036616828061 0.012648333883989995 5.272021975060514e-05 0.00019257248009114364 0.0016757997590048828 1.1891251244906933 5.515522196186723e-05 0.1102706140784316 9.904481358675478e-12 0.0003520674728719278 0.11294708566804167 7.239087311622819e-07 -10.0 3.574060778841494e-05 0.0035983547999740664 1.0588257623722672e-06 4.237271368372502 0.011273340972061077 8.582011716859219e-14 1.151457911727229e-09 0.004313097771132342 0.027847329307350423 0.0017287873387910357 6.401055165502385e-05 1.722191123879782e-07 0.0020848309220162036 0.5639071613735579 0.00018064550990294753 1.289814511028015e-05 1.078687446476881e-13 4.897735047310072 0.002981891705231209 0.0016079768514218546 0.0009332371598523186 0.11724714376848187 0.19040707071165303 0.0014242053195529865 0.6684484191345574 -10.0 - 3 + 3.0 1.5 10 1e-38 diff --git a/tests/unit_tests/weightwindows/test_ww_list.py b/tests/unit_tests/weightwindows/test_ww_list.py new file mode 100644 index 000000000..d148f382a --- /dev/null +++ b/tests/unit_tests/weightwindows/test_ww_list.py @@ -0,0 +1,23 @@ +import openmc + + +def test_ww_roundtrip(request, run_in_tmpdir): + # Load weight windows from a wwinp file + wwinp_file = request.path.with_name('wwinp_n') + wws = openmc.WeightWindowsList.from_wwinp(wwinp_file) + + # Roundtrip them, writing to HDF5 and reading back in + wws.export_to_hdf5('ww.h5') + wws_new = openmc.WeightWindowsList.from_hdf5('ww.h5') + + # Check that the new weight windows are the same as the original + assert len(wws) == len(wws_new) + for ww, ww_new in zip(wws, wws_new): + assert ww.particle_type == ww_new.particle_type + assert (ww.lower_ww_bounds == ww_new.lower_ww_bounds).all() + assert (ww.upper_ww_bounds == ww_new.upper_ww_bounds).all() + assert ww.survival_ratio == ww_new.survival_ratio + assert ww.num_energy_bins == ww_new.num_energy_bins + assert ww.max_split == ww_new.max_split + assert ww.weight_cutoff == ww_new.weight_cutoff + assert ww.mesh.id == ww_new.mesh.id From 5c10214465107277b4f046b58ff9d00e34ba4ce4 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Thu, 26 Jun 2025 03:29:45 +0200 Subject: [PATCH 357/671] added openmc.Material.mean_free_path function (#3469) --- openmc/material.py | 30 ++++++++++++++++++++++++++++++ tests/unit_tests/test_material.py | 13 +++++++++++++ 2 files changed, 43 insertions(+) diff --git a/openmc/material.py b/openmc/material.py index 770722f74..c6765178e 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -1718,6 +1718,36 @@ class Material(IDManagerMixin): return mat + def mean_free_path(self, energy: float) -> float: + """Calculate the mean free path of neutrons in the material at a given + energy. + + .. versionadded:: 0.15.3 + + Parameters + ---------- + energy : float + Neutron energy in eV + + Returns + ------- + float + Mean free path in cm + + """ + from openmc.plotter import _calculate_cexs_elem_mat + + energy_grid, cexs = _calculate_cexs_elem_mat( + this=self, + types=["total"], + ) + total_cexs = cexs[0] + + interpolated_cexs = float(np.interp(energy, energy_grid, total_cexs)) + + return 1.0 / interpolated_cexs + + class Materials(cv.CheckedList): """Collection of Materials used for an OpenMC simulation. diff --git a/tests/unit_tests/test_material.py b/tests/unit_tests/test_material.py index 8ad579563..db5f4ce32 100644 --- a/tests/unit_tests/test_material.py +++ b/tests/unit_tests/test_material.py @@ -710,3 +710,16 @@ def test_avoid_subnormal(run_in_tmpdir): # When read back in, the density should be zero mats = openmc.Materials.from_xml() assert mats[0].get_nuclide_atom_densities()['H2'] == 0.0 + + +def test_mean_free_path(): + + mat1 = openmc.Material() + mat1.add_nuclide('Si28', 1.0) + mat1.set_density('g/cm3', 2.32) + assert mat1.mean_free_path(energy=14e6) == pytest.approx(11.41, abs=1e-2) + + mat2 = openmc.Material() + mat2.add_nuclide('Pb208', 1.0) + mat2.set_density('g/cm3', 11.34) + assert mat2.mean_free_path(energy=14e6) == pytest.approx(5.65, abs=1e-2) From c116288ea389f01420600fe31f000ff7276990dc Mon Sep 17 00:00:00 2001 From: John Tramm Date: Thu, 26 Jun 2025 18:50:26 -0500 Subject: [PATCH 358/671] Updated Docs to Not Give Specific Python Version Requirement (#3473) --- docs/source/usersguide/install.rst | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/source/usersguide/install.rst b/docs/source/usersguide/install.rst index 7a8cb8020..2621109f7 100644 --- a/docs/source/usersguide/install.rst +++ b/docs/source/usersguide/install.rst @@ -515,10 +515,13 @@ to install the Python package in :ref:`"editable" mode `. Prerequisites ------------- -The Python API works with Python 3.8+. In addition to Python itself, the API -relies on a number of third-party packages. All prerequisites can be installed -using Conda_ (recommended), pip_, or through the package manager in most Linux -distributions. +In addition to Python itself, the OpenMC Python API relies on a number of +third-party packages. All prerequisites can be installed using Conda_ +(recommended), pip_, or through the package manager in most Linux distributions. +The current required Python version and up-to-date list of package dependencies +can be found in the `pyproject.toml `_ +file in the root directory of the OpenMC repository. An overview of these +dependencies is provided below. .. admonition:: Required :class: error From 25d64c9b26b0bf4d4b0aad4829ea8ec225226905 Mon Sep 17 00:00:00 2001 From: Ahnaf Tahmid Chowdhury Date: Fri, 27 Jun 2025 06:49:55 +0600 Subject: [PATCH 359/671] Refactor and Harden Configuration Management (#3461) Co-authored-by: Paul Romano --- openmc/config.py | 230 +++++++++++++++++++++++--------- tests/unit_tests/test_config.py | 115 +++++++++++----- 2 files changed, 247 insertions(+), 98 deletions(-) diff --git a/openmc/config.py b/openmc/config.py index 56d8a4072..d61fb2d31 100644 --- a/openmc/config.py +++ b/openmc/config.py @@ -1,8 +1,23 @@ +"""Module for handling global configuration in OpenMC. + +This module exports a single object, `config`, that can be used to control +various settings, primarily paths to data files. It acts like a dictionary but +with special behaviors. + +Examples +-------- +>>> import openmc +>>> openmc.config['cross_sections'] = '/path/to/my/cross_sections.xml' +>>> print(openmc.config) +{'resolve_paths': True, 'cross_sections': PosixPath('/path/to/my/cross_sections.xml')} + +""" from collections.abc import MutableMapping from contextlib import contextmanager import os from pathlib import Path import warnings +from typing import Any, Dict, Iterator from openmc.data import DataLibrary from openmc.data.decay import _DECAY_ENERGY, _DECAY_PHOTON_ENERGY @@ -11,104 +26,195 @@ __all__ = ["config"] class _Config(MutableMapping): - def __init__(self, data=()): - self._mapping = {'resolve_paths': True} + """A configuration dictionary for OpenMC with special handling for path-like values. + + This class enforces valid configuration keys and synchronizes path-related + settings with their corresponding environment variables. + + Attributes + ---------- + cross_sections : pathlib.Path + Path to a cross_sections.xml file. Also sets/unsets the + OPENMC_CROSS_SECTIONS environment variable. + mg_cross_sections : pathlib.Path + Path to a multi-group cross_sections.h5 file. Also sets/unsets + the OPENMC_MG_CROSS_SECTIONS environment variable. + chain_file : pathlib.Path + Path to a depletion chain XML file. Also sets/unsets the + OPENMC_CHAIN_FILE environment variable. Setting or deleting this + clears internal decay data caches. + resolve_paths : bool + If True (default), all paths assigned are resolved to absolute + paths. If False, paths are stored as they are provided. + + """ + _PATH_KEYS: Dict[str, str] = { + 'cross_sections': 'OPENMC_CROSS_SECTIONS', + 'mg_cross_sections': 'OPENMC_MG_CROSS_SECTIONS', + 'chain_file': 'OPENMC_CHAIN_FILE' + } + + def __init__(self, data: dict = ()): + self._mapping: Dict[str, Any] = {'resolve_paths': True} self.update(data) - def __getitem__(self, key): + def __getitem__(self, key: str) -> Any: return self._mapping[key] - def __delitem__(self, key): - del self._mapping[key] - if key == 'cross_sections': - del os.environ['OPENMC_CROSS_SECTIONS'] - elif key == 'mg_cross_sections': - del os.environ['OPENMC_MG_CROSS_SECTIONS'] - elif key == 'chain_file': - del os.environ['OPENMC_CHAIN_FILE'] - # Reset photon source data since it relies on chain file - _DECAY_PHOTON_ENERGY.clear() + def __delitem__(self, key: str): + """Delete a configuration key. - def __setitem__(self, key, value): - if key == 'cross_sections': - # Force environment variable to match - self._set_path(key, value) - os.environ['OPENMC_CROSS_SECTIONS'] = str(value) - elif key == 'mg_cross_sections': - self._set_path(key, value) - os.environ['OPENMC_MG_CROSS_SECTIONS'] = str(value) - elif key == 'chain_file': - self._set_path(key, value) - os.environ['OPENMC_CHAIN_FILE'] = str(value) - # Reset photon source data since it relies on chain file + This also deletes the corresponding environment variable if the key is a + path-like key, and clears decay data caches if 'chain_file' is deleted. + 'resolve_paths' cannot be deleted. + + """ + if key == 'resolve_paths': + raise KeyError("'resolve_paths' cannot be deleted.") + del self._mapping[key] + if key in self._PATH_KEYS: + env_var = self._PATH_KEYS[key] + if env_var in os.environ: + del os.environ[env_var] + if key == 'chain_file': _DECAY_PHOTON_ENERGY.clear() _DECAY_ENERGY.clear() + + def __setitem__(self, key: str, value: Any): + """Set a configuration key and its corresponding value. + + For path-like keys, this method performs several actions: + 1. Resolves the path to an absolute path if `resolve_paths` is True. + 2. Stores the `pathlib.Path` object. + 3. Sets the corresponding environment variable (e.g., OPENMC_CROSS_SECTIONS). + 4. For 'chain_file', clears internal decay data caches. + 5. Issues a `UserWarning` if the final path does not exist. + + """ + if key in self._PATH_KEYS: + p = Path(value) + # Use .get() for robustness, defaulting to True + if self._mapping.get('resolve_paths', True): + stored_path = p.resolve(strict=False) + else: + stored_path = p + + self._mapping[key] = stored_path + os.environ[self._PATH_KEYS[key]] = str(stored_path) + + if key == 'chain_file': + _DECAY_PHOTON_ENERGY.clear() + _DECAY_ENERGY.clear() + + if not stored_path.exists(): + warnings.warn(f"Path '{stored_path}' does not exist.", UserWarning) + elif key == 'resolve_paths': + if not isinstance(value, bool): + raise TypeError("'resolve_paths' must be a boolean.") self._mapping[key] = value else: - raise KeyError(f'Unrecognized config key: {key}. Acceptable keys ' - 'are "cross_sections", "mg_cross_sections", ' - '"chain_file", and "resolve_paths".') + valid_keys = list(self._PATH_KEYS.keys()) + ['resolve_paths'] + raise KeyError( + f"Unrecognized config key: {key}. Acceptable keys are: " + f"{', '.join(repr(k) for k in valid_keys)}." + ) - def __iter__(self): + def __iter__(self) -> Iterator[str]: return iter(self._mapping) - def __len__(self): + def __len__(self) -> int: return len(self._mapping) - def __repr__(self): + def __repr__(self) -> str: return repr(self._mapping) - def _set_path(self, key, value): - self._mapping[key] = p = Path(value).resolve() - if not p.exists(): - warnings.warn(f"'{value}' does not exist.") + def clear(self): + """Clear all configuration keys except for 'resolve_paths'. + + This ensures that the path resolution behavior is not accidentally reset + when clearing the configuration. + + """ + # Create a copy of keys to iterate over for safe deletion + keys_to_delete = [k for k in self._mapping if k != 'resolve_paths'] + for key in keys_to_delete: + del self[key] @contextmanager - def patch(self, key, value): - """Temporarily change a value in the configuration. + def patch(self, key: str, value: Any): + """Context manager to temporarily change a configuration value. + + After the `with` block, the configuration is restored to its original + state. Parameters ---------- key : str - Key to change - value : object - New value + The key of the configuration value to change. + value + The new temporary value. + + Examples + -------- + >>> openmc.config['cross_sections'] = 'endf71.xml' + >>> with openmc.config.patch('cross_sections', 'fendl32.xml'): + ... # Code in this block sees the new value + ... print(f"Inside with block: {openmc.config['cross_sections']}") + >>> # Outside the block, the value is restored + >>> print(f"Outside with block: {openmc.config['cross_sections']}") + Inside with block: fendl32.xml + Outside with block: endf71.xml + """ previous_value = self.get(key) self[key] = value - yield - if previous_value is None: - del self[key] - else: - self[key] = previous_value + try: + yield + finally: + if previous_value is None: + del self[key] + else: + self[key] = previous_value -def _default_config(): - """Return default configuration""" + +def _default_config() -> _Config: + """Create a configuration initialized from environment variables. + + This function checks for OPENMC_CROSS_SECTIONS, OPENMC_MG_CROSS_SECTIONS, + and OPENMC_CHAIN_FILE environment variables. It also has logic to find + a chain file within a `cross_sections.xml` file if one is not + explicitly set. + + Returns + ------- + _Config + A new configuration object. + + """ config = _Config() - - # Set cross sections using environment variable if "OPENMC_CROSS_SECTIONS" in os.environ: config['cross_sections'] = os.environ["OPENMC_CROSS_SECTIONS"] if "OPENMC_MG_CROSS_SECTIONS" in os.environ: config['mg_cross_sections'] = os.environ["OPENMC_MG_CROSS_SECTIONS"] - - # Set depletion chain chain_file = os.environ.get("OPENMC_CHAIN_FILE") - if (chain_file is None and - config.get('cross_sections') is not None and - config['cross_sections'].exists() - ): - # Check for depletion chain in cross_sections.xml - data = DataLibrary.from_xml(config['cross_sections']) - for lib in reversed(data.libraries): - if lib['type'] == 'depletion_chain': - chain_file = lib['path'] - break + xs_path = config.get('cross_sections') + if chain_file is None and xs_path is not None and xs_path.exists(): + try: + data = DataLibrary.from_xml(xs_path) + except Exception: + # Let this pass silently if cross_sections.xml can't be parsed + # or if a dependency like lxml is not available. + pass + else: + for lib in reversed(data.libraries): + if lib['type'] == 'depletion_chain': + chain_file = xs_path.parent / lib['path'] + break if chain_file is not None: config['chain_file'] = chain_file - return config +# Global configuration dictionary for OpenMC settings. config = _default_config() diff --git a/tests/unit_tests/test_config.py b/tests/unit_tests/test_config.py index 4e87de4b7..242c59b5e 100644 --- a/tests/unit_tests/test_config.py +++ b/tests/unit_tests/test_config.py @@ -3,58 +3,101 @@ import os from pathlib import Path import openmc +from openmc.config import _default_config +from openmc.data import decay import pytest -@pytest.fixture(autouse=True, scope='module') -def reset_config(): - config = dict(openmc.config) +@pytest.fixture(autouse=True, scope='function') +def reset_config_and_env(): + """A fixture to ensure each test has a clean config, env, and CWD.""" + original_env = dict(os.environ) + original_cwd = os.getcwd() + + # Reset environment variables that affect config + for key in ['OPENMC_CROSS_SECTIONS', 'OPENMC_MG_CROSS_SECTIONS', 'OPENMC_CHAIN_FILE']: + if key in os.environ: + del os.environ[key] + + # Re-initialize the global config object + openmc.config = _default_config() + try: yield finally: - openmc.config.clear() - openmc.config.update(config) + # Restore environment and CWD + os.environ.clear() + os.environ.update(original_env) + os.chdir(original_cwd) + + # Restore config one last time for safety between modules + openmc.config = _default_config() def test_config_basics(): assert isinstance(openmc.config, Mapping) - for key, value in openmc.config.items(): - assert isinstance(key, str) - if key == 'resolve_paths': - assert isinstance(value, bool) - else: - assert isinstance(value, os.PathLike) - - # Set and delete - openmc.config['cross_sections'] = '/path/to/cross_sections.xml' + with pytest.warns(UserWarning): + openmc.config['cross_sections'] = '/path/to/cross_sections.xml' del openmc.config['cross_sections'] assert 'cross_sections' not in openmc.config assert 'OPENMC_CROSS_SECTIONS' not in os.environ - - # Can't use any key - with pytest.raises(KeyError): - openmc.config['🐖'] = '/like/to/eat/bacon' - - # ensure relative paths are expanded into absolute - # paths - chain_path = Path('./chain.xml') - openmc.config['chain_file'] = chain_path - assert openmc.config['chain_file'] == chain_path.resolve() + with pytest.raises(KeyError, match="Unrecognized config key: nuke"): + openmc.config['nuke'] = '/like/to/eat/bacon' + with pytest.raises(TypeError): + openmc.config['resolve_paths'] = 'not a bool' -def test_config_patch(): - openmc.config['cross_sections'] = '/path/to/cross_sections.xml' - with openmc.config.patch('cross_sections', '/path/to/other.xml'): - assert str(openmc.config['cross_sections']) == '/path/to/other.xml' - assert str(openmc.config['cross_sections']) == '/path/to/cross_sections.xml' +def test_config_path_resolution(tmp_path): + """Test path resolution logic.""" + os.chdir(tmp_path) + relative_path = Path("some/file.xml") + absolute_path = relative_path.resolve() + + # Test with resolve_paths = True (default) + with pytest.warns(UserWarning): + openmc.config['cross_sections'] = relative_path + assert openmc.config['cross_sections'] == absolute_path + assert openmc.config['cross_sections'].is_absolute() + + # Test with resolve_paths = False + with openmc.config.patch('resolve_paths', False): + with pytest.warns(UserWarning): + openmc.config['chain_file'] = relative_path + assert openmc.config['chain_file'] == relative_path + assert not openmc.config['chain_file'].is_absolute() + + assert openmc.config['resolve_paths'] is True -def test_config_set_envvar(): - openmc.config['cross_sections'] = '/path/to/cross_sections.xml' - assert os.environ['OPENMC_CROSS_SECTIONS'] == '/path/to/cross_sections.xml' +def test_config_patch(tmp_path): + file_a = tmp_path / "a.xml"; file_a.touch() + file_b = tmp_path / "b.xml"; file_b.touch() + openmc.config['cross_sections'] = file_a + with openmc.config.patch('cross_sections', file_b): + assert openmc.config['cross_sections'] == file_b.resolve() + assert openmc.config['cross_sections'] == file_a.resolve() - openmc.config['mg_cross_sections'] = '/path/to/mg_cross_sections.h5' - assert os.environ['OPENMC_MG_CROSS_SECTIONS'] == '/path/to/mg_cross_sections.h5' +def test_config_set_envvar(tmp_path): + """Test that setting config also sets environment variables correctly.""" + os.chdir(tmp_path) + relative_path = Path("relative.xml") + with pytest.warns(UserWarning): + openmc.config['cross_sections'] = relative_path + expected_path = str(relative_path.resolve()) + assert os.environ['OPENMC_CROSS_SECTIONS'] == expected_path - openmc.config['chain_file'] = '/path/to/chain_file.xml' - assert os.environ['OPENMC_CHAIN_FILE'] == '/path/to/chain_file.xml' + +def test_config_warning_nonexistent_path(tmp_path): + """Test that a warning is issued for a path that does not exist.""" + bad_path = tmp_path / "a/path/that/does/not/exist.xml" + with pytest.warns(UserWarning, match=f"Path '{bad_path}' does not exist."): + openmc.config['chain_file'] = bad_path + + +def test_config_chain_side_effect(tmp_path): + """Test that modifying chain_file clears decay data caches.""" + chain_file = tmp_path / "chain.xml"; chain_file.touch() + decay._DECAY_ENERGY['U235'] = (1.0, 2.0) + decay._DECAY_PHOTON_ENERGY['PU239'] = {} + openmc.config['chain_file'] = chain_file + assert not decay._DECAY_ENERGY and not decay._DECAY_PHOTON_ENERGY From b939f9003b65b8b7c1070256f51638c25e8d8c30 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Mon, 30 Jun 2025 03:33:22 -0500 Subject: [PATCH 360/671] Stabilize Adjoint Source (#3476) --- src/random_ray/flat_source_domain.cpp | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 41e93ec94..4a5b43036 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -1242,6 +1242,30 @@ void FlatSourceDomain::set_adjoint_sources(const vector& forward_flux) } } + // "Small" source regions in OpenMC are defined as those that are hit by + // MIN_HITS_PER_BATCH rays or fewer each batch. These regions typically have + // very small volumes combined with a low aspect ratio, and are often + // generated when applying a source region mesh that clips the edge of a + // curved surface. As perhaps only a few rays will visit these regions over + // the entire forward simulation, the forward flux estimates are extremely + // noisy and unreliable. In some cases, the noise may make the forward fluxes + // extremely low, leading to unphysically large adjoint source terms, + // resulting in weight windows that aggressively try to drive particles + // towards these regions. To fix this, we simply filter out any "small" source + // regions from consideration. If a source region is "small", we + // set its adjoint source to zero. This adds negligible bias to the adjoint + // flux solution, as the true total adjoint source contribution from small + // regions is likely to be negligible. +#pragma omp parallel for + for (int64_t sr = 0; sr < n_source_regions(); sr++) { + if (source_regions_.is_small(sr)) { + for (int g = 0; g < negroups_; g++) { + source_regions_.external_source(sr, g) = 0.0; + } + source_regions_.external_source_present(sr) = 0; + } + } + // Divide the fixed source term by sigma t (to save time when applying each // iteration) #pragma omp parallel for From dab8af5672d7367996fd78a40b0c75d852efe0db Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 1 Jul 2025 05:43:28 -0500 Subject: [PATCH 361/671] Support flux collapse method in `get_microxs_and_flux` (#3466) Co-authored-by: Jonathan Shimwell --- openmc/deplete/microxs.py | 134 +++++++++++++----- openmc/filter_expansion.py | 2 +- tests/regression_tests/microxs/test.py | 71 +++++----- .../microxs/test_reference_materials.csv | 25 ---- .../test_reference_materials_direct.csv | 7 + .../microxs/test_reference_materials_flux.csv | 7 + .../microxs/test_reference_mesh.csv | 25 ---- .../microxs/test_reference_mesh_direct.csv | 7 + .../microxs/test_reference_mesh_flux.csv | 7 + 9 files changed, 163 insertions(+), 122 deletions(-) delete mode 100644 tests/regression_tests/microxs/test_reference_materials.csv create mode 100644 tests/regression_tests/microxs/test_reference_materials_direct.csv create mode 100644 tests/regression_tests/microxs/test_reference_materials_flux.csv delete mode 100644 tests/regression_tests/microxs/test_reference_mesh.csv create mode 100644 tests/regression_tests/microxs/test_reference_mesh_direct.csv create mode 100644 tests/regression_tests/microxs/test_reference_mesh_flux.csv diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index d478db11b..cbc8b1c88 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -5,8 +5,10 @@ IndependentOperator class for depletion. """ from __future__ import annotations -from collections.abc import Iterable, Sequence +from collections.abc import Sequence +import shutil from tempfile import TemporaryDirectory +from typing import Union, TypeAlias import pandas as pd import numpy as np @@ -27,19 +29,39 @@ _valid_rxns.append('fission') _valid_rxns.append('damage-energy') +# TODO: Replace with type statement when support is Python 3.12+ +DomainTypes: TypeAlias = Union[ + Sequence[openmc.Material], + Sequence[openmc.Cell], + Sequence[openmc.Universe], + openmc.MeshBase, + openmc.Filter +] + + def get_microxs_and_flux( - model: openmc.Model, - domains, - nuclides: Iterable[str] | None = None, - reactions: Iterable[str] | None = None, - energies: Iterable[float] | str | None = None, - chain_file: PathLike | Chain | None = None, - run_kwargs=None - ) -> tuple[list[np.ndarray], list[MicroXS]]: - """Generate a microscopic cross sections and flux from a Model + model: openmc.Model, + domains: DomainTypes, + nuclides: Sequence[str] | None = None, + reactions: Sequence[str] | None = None, + energies: Sequence[float] | str | None = None, + reaction_rate_mode: str = 'direct', + chain_file: PathLike | Chain | None = None, + path_statepoint: PathLike | None = None, + run_kwargs=None +) -> tuple[list[np.ndarray], list[MicroXS]]: + """Generate microscopic cross sections and fluxes for multiple domains. + + This function runs a neutron transport solve to obtain the flux and reaction + rates in the specified domains and computes multigroup microscopic cross + sections that can be used in depletion calculations with the + :class:`~openmc.deplete.IndependentOperator` class. .. versionadded:: 0.14.0 + .. versionchanged:: 0.15.3 + Added `reaction_rate_mode` and `path_statepoint` arguments. + Parameters ---------- model : openmc.Model @@ -53,12 +75,22 @@ def get_microxs_and_flux( Reactions to get cross sections for. If not specified, all neutron reactions listed in the depletion chain file are used. energies : iterable of float or str - Energy group boundaries in [eV] or the name of the group structure + 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). 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 present in the inital composition. Defaults to ``openmc.config['chain_file']``. + path_statepoint : path-like, optional + Path to write the statepoint file from the neutron transport solve to. + By default, The statepoint file is written to a temporary directory and + is not kept. run_kwargs : dict, optional Keyword arguments passed to :meth:`openmc.Model.run` @@ -69,7 +101,13 @@ def get_microxs_and_flux( list of MicroXS Cross section data in [b] for each domain + See Also + -------- + openmc.deplete.IndependentOperator + """ + check_value('reaction_rate_mode', reaction_rate_mode, {'direct', 'flux'}) + # Save any original tallies on the model original_tallies = model.tallies @@ -85,8 +123,8 @@ def get_microxs_and_flux( # Set up the reaction rate and flux tallies if energies is None: - energy_filter = openmc.EnergyFilter([0.0, 100.0e6]) - elif isinstance(energies, str): + energies = [0.0, 100.0e6] + if isinstance(energies, str): energy_filter = openmc.EnergyFilter.from_group_structure(energies) else: energy_filter = openmc.EnergyFilter(energies) @@ -104,16 +142,18 @@ def get_microxs_and_flux( else: raise ValueError(f"Unsupported domain type: {type(domains[0])}") - rr_tally = openmc.Tally(name='MicroXS RR') - rr_tally.filters = [domain_filter, energy_filter] - rr_tally.nuclides = nuclides - rr_tally.multiply_density = False - rr_tally.scores = reactions - flux_tally = openmc.Tally(name='MicroXS flux') flux_tally.filters = [domain_filter, energy_filter] flux_tally.scores = ['flux'] - model.tallies = openmc.Tallies([rr_tally, flux_tally]) + model.tallies = [flux_tally] + + if reaction_rate_mode == 'direct': + rr_tally = openmc.Tally(name='MicroXS RR') + rr_tally.filters = [domain_filter, energy_filter] + rr_tally.nuclides = nuclides + rr_tally.multiply_density = False + rr_tally.scores = reactions + model.tallies.append(rr_tally) if openmc.lib.is_initialized: openmc.lib.finalize() @@ -134,33 +174,55 @@ def get_microxs_and_flux( statepoint_path = model.run(**run_kwargs) if comm.rank == 0: + # Move the statepoint file if it is being saved to a specific path + if path_statepoint is not None: + shutil.move(statepoint_path, path_statepoint) + statepoint_path = path_statepoint + with StatePoint(statepoint_path) as sp: - rr_tally = sp.tallies[rr_tally.id] - rr_tally._read_results() + if reaction_rate_mode == 'direct': + rr_tally = sp.tallies[rr_tally.id] + rr_tally._read_results() flux_tally = sp.tallies[flux_tally.id] flux_tally._read_results() - rr_tally = comm.bcast(rr_tally) + # Get flux values and make energy groups last dimension flux_tally = comm.bcast(flux_tally) - # Get reaction rates and flux values - reaction_rates = rr_tally.get_reshaped_data() # (domains, groups, nuclides, reactions) flux = flux_tally.get_reshaped_data() # (domains, groups, 1, 1) - - # Make energy groups last dimension - reaction_rates = np.moveaxis(reaction_rates, 1, -1) # (domains, nuclides, reactions, groups) flux = np.moveaxis(flux, 1, -1) # (domains, 1, 1, groups) - # Divide RR by flux to get microscopic cross sections - xs = np.empty_like(reaction_rates) # (domains, nuclides, reactions, groups) - d, _, _, g = np.nonzero(flux) - xs[d, ..., g] = reaction_rates[d, ..., g] / flux[d, :, :, g] + # Create list where each item corresponds to one domain + fluxes = list(flux.squeeze((1, 2))) + + if reaction_rate_mode == 'direct': + # Get reaction rates + rr_tally = comm.bcast(rr_tally) + 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) + + # 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) + 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( + energies=energies, + multigroup_flux=flux_i, + chain_file=chain_file, + nuclides=nuclides, + reactions=reactions + ) for flux_i in fluxes] # Reset tallies model.tallies = original_tallies - # Create lists where each item corresponds to one domain - fluxes = list(flux.squeeze((1, 2))) - micros = [MicroXS(xs_i, nuclides, reactions) for xs_i in xs] return fluxes, micros @@ -230,7 +292,7 @@ class MicroXS: ---------- energies : iterable of float or str Energy group boundaries in [eV] or the name of the group structure - multi_group_flux : iterable of float + multigroup_flux : iterable of float Energy-dependent multigroup flux values chain_file : PathLike or Chain, optional Path to the depletion chain XML file or an instance of diff --git a/openmc/filter_expansion.py b/openmc/filter_expansion.py index f8e677578..cdb2f20e0 100644 --- a/openmc/filter_expansion.py +++ b/openmc/filter_expansion.py @@ -3,7 +3,7 @@ from numbers import Integral, Real import lxml.etree as ET import openmc.checkvalue as cv -from . import Filter +from .filter import Filter class ExpansionFilter(Filter): diff --git a/tests/regression_tests/microxs/test.py b/tests/regression_tests/microxs/test.py index a35150a1b..70833bb39 100644 --- a/tests/regression_tests/microxs/test.py +++ b/tests/regression_tests/microxs/test.py @@ -13,55 +13,56 @@ CHAIN_FILE = Path(__file__).parents[2] / "chain_simple.xml" @pytest.fixture(scope="module") def model(): fuel = openmc.Material(name="uo2") - fuel.add_element("U", 1, percent_type="ao", enrichment=4.25) - fuel.add_element("O", 2) + fuel.add_nuclide("U235", 1.0) + fuel.add_nuclide("O16", 2.0) fuel.set_density("g/cc", 10.4) - clad = openmc.Material(name="clad") - clad.add_element("Zr", 1) - clad.set_density("g/cc", 6) - - water = openmc.Material(name="water") - water.add_element("O", 1) - water.add_element("H", 2) - water.set_density("g/cc", 1.0) - water.add_s_alpha_beta("c_H_in_H2O") - - radii = [0.42, 0.45] - fuel.volume = np.pi * radii[0] ** 2 - - materials = openmc.Materials([fuel, clad, water]) - - pin_surfaces = [openmc.ZCylinder(r=r) for r in radii] - pin_univ = openmc.model.pin(pin_surfaces, materials) - bound_box = openmc.model.RectangularPrism(1.24, 1.24, boundary_type="reflective") - root_cell = openmc.Cell(fill=pin_univ, region=-bound_box) - geometry = openmc.Geometry([root_cell]) + sphere = openmc.Sphere(r=10.0, boundary_type='vacuum') + cell = openmc.Cell(region=-sphere, fill=fuel) + geometry = openmc.Geometry([cell]) settings = openmc.Settings() settings.particles = 1000 settings.inactive = 5 settings.batches = 10 - return openmc.Model(geometry, materials, settings) + return openmc.Model(geometry, settings=settings) -@pytest.mark.parametrize("domain_type", ["materials", "mesh"]) -def test_from_model(model, domain_type): +@pytest.mark.parametrize( + "domain_type, rr_mode", + [ + ("materials", "direct"), + ("materials", "flux"), + ("mesh", "direct"), + ("mesh", "flux"), + ] +) +def test_from_model(model, domain_type, rr_mode): if domain_type == 'materials': - domains = model.materials[:1] + domains = list(model.geometry.get_all_materials().values()) elif domain_type == 'mesh': mesh = openmc.RegularMesh() - mesh.lower_left = (-0.62, -0.62) - mesh.upper_right = (0.62, 0.62) - mesh.dimension = (3, 3) + mesh.lower_left = (-10., -10.) + mesh.upper_right = (10., 10.) + mesh.dimension = (1, 1) domains = mesh - nuclides = ['U234', 'U235', 'U238', 'U236', 'O16', 'O17', 'I135', 'Xe135', - 'Xe136', 'Cs135', 'Gd157', 'Gd156'] - _, test_xs = get_microxs_and_flux(model, domains, nuclides, chain_file=CHAIN_FILE) + nuclides = ['U235', 'O16', 'Xe135'] + kwargs = { + 'reaction_rate_mode': rr_mode, + 'chain_file': CHAIN_FILE, + 'path_statepoint': 'neutron_transport.h5', + } + if rr_mode == 'flux': + kwargs['energies'] = 'CASMO-40' + _, test_xs = get_microxs_and_flux(model, domains, nuclides, **kwargs) if config['update']: - test_xs[0].to_csv(f'test_reference_{domain_type}.csv') - - ref_xs = MicroXS.from_csv(f'test_reference_{domain_type}.csv') + test_xs[0].to_csv(f'test_reference_{domain_type}_{rr_mode}.csv') + # Make sure results match reference results + ref_xs = MicroXS.from_csv(f'test_reference_{domain_type}_{rr_mode}.csv') np.testing.assert_allclose(test_xs[0].data, ref_xs.data, rtol=1e-11) + + # Make sure statepoint file was saved + assert Path('neutron_transport.h5').exists() + Path('neutron_transport.h5').unlink() diff --git a/tests/regression_tests/microxs/test_reference_materials.csv b/tests/regression_tests/microxs/test_reference_materials.csv deleted file mode 100644 index 3e679f980..000000000 --- a/tests/regression_tests/microxs/test_reference_materials.csv +++ /dev/null @@ -1,25 +0,0 @@ -nuclides,reactions,groups,xs -U234,"(n,gamma)",1,21.418670317831076 -U234,fission,1,0.5014588470882162 -U235,"(n,gamma)",1,10.343944102483215 -U235,fission,1,47.46718472611895 -U238,"(n,gamma)",1,0.8741166723597229 -U238,fission,1,0.10829568455139067 -U236,"(n,gamma)",1,9.08348678468935 -U236,fission,1,0.3325287927011424 -O16,"(n,gamma)",1,7.548646353912426e-05 -O16,fission,1,0.0 -O17,"(n,gamma)",1,0.00040184862213103105 -O17,fission,1,0.0 -I135,"(n,gamma)",1,6.691256508942912 -I135,fission,1,0.0 -Xe135,"(n,gamma)",1,223998.6418566729 -Xe135,fission,1,0.0 -Xe136,"(n,gamma)",1,0.022934362666193503 -Xe136,fission,1,0.0 -Cs135,"(n,gamma)",1,2.2845395222353204 -Cs135,fission,1,0.0 -Gd157,"(n,gamma)",1,12582.07962003624 -Gd157,fission,1,0.0 -Gd156,"(n,gamma)",1,2.942112751533234 -Gd156,fission,1,0.0 diff --git a/tests/regression_tests/microxs/test_reference_materials_direct.csv b/tests/regression_tests/microxs/test_reference_materials_direct.csv new file mode 100644 index 000000000..3259d0151 --- /dev/null +++ b/tests/regression_tests/microxs/test_reference_materials_direct.csv @@ -0,0 +1,7 @@ +nuclides,reactions,groups,xs +U235,"(n,gamma)",1,0.14765501510184456 +U235,fission,1,1.2517200956290817 +O16,"(n,gamma)",1,0.00010872314985710938 +O16,fission,1,0.0 +Xe135,"(n,gamma)",1,0.014333667335215764 +Xe135,fission,1,0.0 diff --git a/tests/regression_tests/microxs/test_reference_materials_flux.csv b/tests/regression_tests/microxs/test_reference_materials_flux.csv new file mode 100644 index 000000000..5023e0d0e --- /dev/null +++ b/tests/regression_tests/microxs/test_reference_materials_flux.csv @@ -0,0 +1,7 @@ +nuclides,reactions,groups,xs +U235,"(n,gamma)",1,0.15018326758942505 +U235,fission,1,1.2603151141390958 +O16,"(n,gamma)",1,0.00012159621938463765 +O16,fission,1,0.0 +Xe135,"(n,gamma)",1,0.015180177095633546 +Xe135,fission,1,0.0 diff --git a/tests/regression_tests/microxs/test_reference_mesh.csv b/tests/regression_tests/microxs/test_reference_mesh.csv deleted file mode 100644 index 1882372b1..000000000 --- a/tests/regression_tests/microxs/test_reference_mesh.csv +++ /dev/null @@ -1,25 +0,0 @@ -nuclides,reactions,groups,xs -U234,"(n,gamma)",1,27.027171724208227 -U234,fission,1,0.04333740860093498 -U235,"(n,gamma)",1,12.683875995776193 -U235,fission,1,4.2596665957162605 -U238,"(n,gamma)",1,4.479719141496804 -U238,fission,1,0.009460665924409056 -U236,"(n,gamma)",1,8.469286849810802 -U236,fission,1,0.027373590840715795 -O16,"(n,gamma)",1,7.478160204479271e-05 -O16,fission,1,0.0 -O17,"(n,gamma)",1,0.0004743959164164789 -O17,fission,1,0.0 -I135,"(n,gamma)",1,8.33959524761822 -I135,fission,1,0.0 -Xe135,"(n,gamma)",1,282068.447252079 -Xe135,fission,1,0.0 -Xe136,"(n,gamma)",1,0.02888928065916194 -Xe136,fission,1,0.0 -Cs135,"(n,gamma)",1,2.5863526577468408 -Cs135,fission,1,0.0 -Gd157,"(n,gamma)",1,16518.24083307153 -Gd157,fission,1,0.0 -Gd156,"(n,gamma)",1,2.838514589417232 -Gd156,fission,1,0.0 diff --git a/tests/regression_tests/microxs/test_reference_mesh_direct.csv b/tests/regression_tests/microxs/test_reference_mesh_direct.csv new file mode 100644 index 000000000..bd07a3237 --- /dev/null +++ b/tests/regression_tests/microxs/test_reference_mesh_direct.csv @@ -0,0 +1,7 @@ +nuclides,reactions,groups,xs +U235,"(n,gamma)",1,0.14765501510184456 +U235,fission,1,1.2517200956290815 +O16,"(n,gamma)",1,0.00010872314985710936 +O16,fission,1,0.0 +Xe135,"(n,gamma)",1,0.014333667335215761 +Xe135,fission,1,0.0 diff --git a/tests/regression_tests/microxs/test_reference_mesh_flux.csv b/tests/regression_tests/microxs/test_reference_mesh_flux.csv new file mode 100644 index 000000000..d946ada80 --- /dev/null +++ b/tests/regression_tests/microxs/test_reference_mesh_flux.csv @@ -0,0 +1,7 @@ +nuclides,reactions,groups,xs +U235,"(n,gamma)",1,0.15018326758942507 +U235,fission,1,1.2603151141390958 +O16,"(n,gamma)",1,0.00012159621938463766 +O16,fission,1,0.0 +Xe135,"(n,gamma)",1,0.015180177095633546 +Xe135,fission,1,0.0 From ecfb666db735544dd6a76b88c02f57049111cbf2 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 1 Jul 2025 09:03:28 -0500 Subject: [PATCH 362/671] Allow spatial constraints on element sources within MeshSource (#3431) --- include/openmc/source.h | 29 +++++++--- src/source.cpp | 53 ++++++++++-------- tests/unit_tests/test_source_mesh.py | 81 ++++++++++++++-------------- 3 files changed, 94 insertions(+), 69 deletions(-) diff --git a/include/openmc/source.h b/include/openmc/source.h index e82c42269..d58195f10 100644 --- a/include/openmc/source.h +++ b/include/openmc/source.h @@ -139,6 +139,9 @@ public: DomainType domain_type() const { return domain_type_; } const std::unordered_set& domain_ids() const { return domain_ids_; } + // Setter for spatial distribution + void set_space(UPtrSpace space) { space_ = std::move(space); } + protected: // Indicates whether derived class already handles constraints bool constraints_applied() const override { return true; } @@ -205,6 +208,23 @@ typedef unique_ptr create_compiled_source_t(std::string parameters); //! Mesh-based source with different distributions for each element //============================================================================== +// Helper class to sample spatial position on a single mesh element +class MeshElementSpatial : public SpatialDistribution { +public: + MeshElementSpatial(int32_t mesh_index, int elem_index) + : mesh_index_(mesh_index), elem_index_(elem_index) + {} + + //! Sample a position from the distribution + //! \param seed Pseudorandom number seed pointer + //! \return Sampled position + Position sample(uint64_t* seed) const override; + +private: + int32_t mesh_index_ {C_NONE}; //!< Index in global meshes array + int elem_index_; //! Index of mesh element +}; + class MeshSource : public Source { public: // Constructors @@ -219,18 +239,15 @@ public: double strength() const override { return space_->total_strength(); } // Accessors - const std::unique_ptr& source(int32_t i) const + const unique_ptr& source(int32_t i) const { return sources_.size() == 1 ? sources_[0] : sources_[i]; } -protected: - bool constraints_applied() const override { return true; } - private: // Data members - unique_ptr space_; //!< Mesh spatial - vector> sources_; //!< Source distributions + unique_ptr space_; //!< Mesh spatial + vector> sources_; //!< Source distributions }; //============================================================================== diff --git a/src/source.cpp b/src/source.cpp index 6ee0b5603..65873f5a7 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -239,7 +239,9 @@ bool Source::satisfies_spatial_constraints(Position r) const if (!domain_ids_.empty()) { if (domain_type_ == DomainType::MATERIAL) { auto mat_index = geom_state.material(); - if (mat_index != MATERIAL_VOID) { + if (mat_index == MATERIAL_VOID) { + accepted = false; + } else { accepted = contains(domain_ids_, model::materials[mat_index]->id()); } } else { @@ -525,6 +527,15 @@ CompiledSourceWrapper::~CompiledSourceWrapper() #endif } +//============================================================================== +// MeshElementSpatial implementation +//============================================================================== + +Position MeshElementSpatial::sample(uint64_t* seed) const +{ + return model::meshes[mesh_index_]->sample_element(elem_index_, seed); +} + //============================================================================== // MeshSource implementation //============================================================================== @@ -539,10 +550,23 @@ MeshSource::MeshSource(pugi::xml_node node) : Source(node) // read all source distributions and populate strengths vector for MeshSpatial // object for (auto source_node : node.children("source")) { - sources_.emplace_back(Source::create(source_node)); + auto src = Source::create(source_node); + if (auto ptr = dynamic_cast(src.get())) { + src.release(); + sources_.emplace_back(ptr); + } else { + fatal_error( + "The source assigned to each element must be an IndependentSource."); + } strengths.push_back(sources_.back()->strength()); } + // Set spatial distributions for each mesh element + for (int elem_index = 0; elem_index < sources_.size(); ++elem_index) { + sources_[elem_index]->set_space( + std::make_unique(mesh_idx, elem_index)); + } + // the number of source distributions should either be one or equal to the // number of mesh elements if (sources_.size() > 1 && sources_.size() != mesh->n_bins()) { @@ -556,29 +580,12 @@ MeshSource::MeshSource(pugi::xml_node node) : Source(node) SourceSite MeshSource::sample(uint64_t* seed) const { - // Sample the CDF defined in initialization above + // Sample a mesh element based on the relative strengths int32_t element = space_->sample_element_index(seed); - // Sample position and apply rejection on spatial domains - Position r; - do { - r = space_->mesh()->sample_element(element, seed); - } while (!this->satisfies_spatial_constraints(r)); - - SourceSite site; - while (true) { - // Sample source for the chosen element and replace the position - site = source(element)->sample_with_constraints(seed); - site.r = r; - - // Apply other rejections - if (satisfies_energy_constraints(site.E) && - satisfies_time_constraints(site.time)) { - break; - } - } - - return site; + // Sample the distribution for the specific mesh element; note that the + // spatial distribution has been set for each element using MeshElementSpatial + return source(element)->sample_with_constraints(seed); } //============================================================================== diff --git a/tests/unit_tests/test_source_mesh.py b/tests/unit_tests/test_source_mesh.py index d023c1335..2550cb87e 100644 --- a/tests/unit_tests/test_source_mesh.py +++ b/tests/unit_tests/test_source_mesh.py @@ -1,5 +1,7 @@ from itertools import product from pathlib import Path +from math import sqrt +import random import pytest import numpy as np @@ -334,13 +336,10 @@ def test_umesh_source_independent(run_in_tmpdir, request, void_model, library): n_elements = 12_000 model.settings.source = openmc.MeshSource(uscd_mesh, n_elements*[ind_source]) model.export_to_model_xml() - try: - openmc.lib.init() + with openmc.lib.run_in_memory(): openmc.lib.simulation_init() sites = openmc.lib.sample_external_source(10) openmc.lib.statepoint_write('statepoint.h5') - finally: - openmc.lib.finalize() with openmc.StatePoint('statepoint.h5') as sp: uscd_mesh = sp.meshes[uscd_mesh.id] @@ -351,51 +350,53 @@ def test_umesh_source_independent(run_in_tmpdir, request, void_model, library): assert site.r in bounding_box -def test_mesh_source_file(run_in_tmpdir): - # Creating a source file with a single particle - source_particle = openmc.SourceParticle(time=10.0) - openmc.write_source_file([source_particle], 'source.h5') - file_source = openmc.FileSource('source.h5') +def test_mesh_source_constraints(run_in_tmpdir): + """Test application of constraints to underlying mesh element sources""" + # Create simple model with two cells + m1 = openmc.Material() + m1.add_nuclide('H1', 1.0) + m2 = m1.clone() + sph = openmc.Sphere(r=100, boundary_type='vacuum') + box1 = openmc.model.RectangularParallelepiped(-1, 0, -1, 1, -1, 1) + box2 = openmc.model.RectangularParallelepiped(0, 2, -1, 1, -1, 1) + cell1 = openmc.Cell(fill=m1, region=-box1) + cell2 = openmc.Cell(fill=m2, region=-box2) + outer = openmc.Cell(region=-sph & (+box1 | +box2)) model = openmc.Model() + model.geometry = openmc.Geometry([cell1, cell2, outer]) - rect_prism = openmc.model.RectangularParallelepiped( - -5.0, 5.0, -5.0, 5.0, -5.0, 5.0, boundary_type='vacuum') - - mat = openmc.Material() - mat.add_nuclide('H1', 1.0) - - model.geometry = openmc.Geometry([openmc.Cell(fill=mat, region=-rect_prism)]) - model.settings.particles = 1000 - model.settings.batches = 10 - model.settings.run_mode = 'fixed source' - + # Define a mesh covering the two cells: the first mesh element contains + # cell1 (-1 < x < 0) and the second element contains cells2 (0 < x < 2) mesh = openmc.RegularMesh() - mesh.lower_left = (-1, -2, -3) - mesh.upper_right = (2, 3, 4) - mesh.dimension = (1, 1, 1) + mesh.lower_left = (-3., -1., -1.) + mesh.upper_right = (3., 1., 1.) + mesh.dimension = (2, 1, 1) - model.settings.source = openmc.MeshSource(mesh, [file_source]) + # Define a mesh source with a randomly chosen probability + p = random.random() + src1 = openmc.IndependentSource(strength=p, constraints={'domains': [cell1]}) + src2 = openmc.IndependentSource(strength=1 - p, constraints={'domains': [cell2]}) + model.settings.source = openmc.MeshSource(mesh, [src1, src2]) + # Finish settings and export + model.settings.particles = 100 + model.settings.batches = 1 model.export_to_model_xml() - openmc.lib.init() - openmc.lib.simulation_init() - sites = openmc.lib.sample_external_source(10) - openmc.lib.simulation_finalize() - openmc.lib.finalize() + with openmc.lib.run_in_memory(): + # Sample sites from the source + sites = openmc.lib.sample_external_source(N := 1000) - # The mesh bounds do not contain the point of the lone source site in the - # file source, so it should not appear in the set of source sites produced - # from the mesh source. Additionally, the source should be located within - # the mesh - bbox = mesh.bounding_box - for site in sites: - assert site.r != (0, 0, 0) - assert site.E == source_particle.E - assert site.u == source_particle.u - assert site.time == source_particle.time - assert site.r in bbox + # Check that all sites are either in cell1 or cell2 + xs = np.array([s.r[0] for s in sites]) + assert (xs >= -1.0).all() + assert (xs <= 2.0).all() + + # Check that the correct percentage of the sites are in cell1 + sigma = sqrt(p*(1- p)/N) + frac = xs[(-1.0 <= xs) & (xs <= 0.0)].size / N + assert frac == pytest.approx(p, abs=5*sigma) @pytest.mark.parametrize("mesh_type", ('rectangular', 'cylindrical', 'spherical')) From eb74d497d2cfa784d9d628a79fda9f4c509b87d4 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 1 Jul 2025 12:43:45 -0500 Subject: [PATCH 363/671] Introduce `openmc.lib.TemporarySession` context manager (#3475) --- docs/source/pythonapi/capi.rst | 1 + openmc/deplete/chain.py | 57 ++++++++++++++++------ openmc/deplete/microxs.py | 51 ++++++++------------ openmc/lib/core.py | 65 ++++++++++++++++++++++++++ openmc/lib/nuclide.py | 6 +++ openmc/mesh.py | 37 +++++++-------- openmc/weight_windows.py | 10 ++-- src/nuclide.cpp | 3 +- tests/unit_tests/test_deplete_chain.py | 6 +-- 9 files changed, 160 insertions(+), 76 deletions(-) diff --git a/docs/source/pythonapi/capi.rst b/docs/source/pythonapi/capi.rst index 63fc5d11b..67eca0094 100644 --- a/docs/source/pythonapi/capi.rst +++ b/docs/source/pythonapi/capi.rst @@ -89,6 +89,7 @@ Classes SphericalMesh SurfaceFilter Tally + TemporarySession UniverseFilter UnstructuredMesh WeightFilter diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index 07db48799..8e24f716b 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -11,6 +11,7 @@ import re from collections import defaultdict, namedtuple from collections.abc import Mapping, Iterable from numbers import Real, Integral +from pathlib import Path from warnings import warn from typing import List @@ -278,7 +279,6 @@ class Chain: """Number of nuclides in chain.""" return len(self.nuclides) - @property def stable_nuclides(self) -> List[Nuclide]: """List of stable nuclides available in the chain""" @@ -298,6 +298,7 @@ class Chain: Nuclide to add """ + _invalidate_chain_cache(self) self.nuclide_dict[nuclide.name] = len(self.nuclides) self.nuclides.append(nuclide) @@ -463,7 +464,6 @@ class Chain: nuclide.add_reaction('fission', None, q_value, 1.0) fissionable = True - if fissionable: if parent in fpy_data: fpy = fpy_data[parent] @@ -558,6 +558,9 @@ class Chain: nuc = Nuclide.from_xml(nuclide_elem, root, this_q) chain.add_nuclide(nuc) + # Store path of XML file (used for handling cache invalidation) + chain._xml_path = str(Path(filename).resolve()) + return chain def export_to_xml(self, filename): @@ -888,7 +891,7 @@ class Chain: -------- :meth:`get_branch_ratios` """ - + _invalidate_chain_cache(self) # Store some useful information through the validation stage sums = {} @@ -1027,6 +1030,7 @@ class Chain: @fission_yields.setter def fission_yields(self, yields): + _invalidate_chain_cache(self) if yields is not None: if isinstance(yields, Mapping): yields = [yields] @@ -1249,6 +1253,10 @@ class Chain: return found +# A global cache for Chain objects +_CHAIN_CACHE = {} + + def _get_chain( chain_file: PathLike | Chain | None = None, fission_q: dict | None = None @@ -1269,16 +1277,39 @@ def _get_chain( Chain Depletion chain instance. """ + # If chain_file is already a Chain, return it directly if isinstance(chain_file, Chain): return chain_file - elif isinstance(chain_file, PathLike | None): - if chain_file is None: - chain_file = openmc.config.get('chain_file') - if 'chain_file' not in openmc.config: - raise DataError( - "No depletion chain specified and could not find depletion " - "chain in openmc.config['chain_file']" - ) - return Chain.from_xml(chain_file, fission_q) - else: + + # Resolve chain_file based on config if None + if chain_file is None: + chain_file = openmc.config.get('chain_file') + if 'chain_file' not in openmc.config: + raise DataError( + "No depletion chain specified and could not find depletion " + "chain in openmc.config['chain_file']" + ) + elif not isinstance(chain_file, PathLike): raise TypeError("chain_file must be path-like, a Chain, or None") + + # Determine the key for the cache, which consists of the absolute path, the + # file modification time, the file size, and the fission Q values. + chain_path = Path(chain_file).resolve() + stat_result = chain_path.stat() + fq_tuple = tuple(sorted(fission_q.items())) if fission_q else () + key = (chain_path, stat_result.st_mtime, stat_result.st_size, fq_tuple) + + # Check the global cache. If not cached, load the chain from XML and store + global _CHAIN_CACHE + if key not in _CHAIN_CACHE: + _CHAIN_CACHE[key] = Chain.from_xml(chain_path, fission_q) + return _CHAIN_CACHE[key] + + +def _invalidate_chain_cache(chain): + """Invalidate the cache for a specific Chain (when it is modifed).""" + if hasattr(chain, '_xml_path'): + # Remove all entries with the same path as self._xml_path + for key in list(_CHAIN_CACHE.keys()): + if str(key[0]) == chain._xml_path: + del _CHAIN_CACHE[key] diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index cbc8b1c88..df78f8a26 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -14,7 +14,6 @@ import pandas as pd import numpy as np from openmc.checkvalue import check_type, check_value, check_iterable_type, PathLike -from openmc.utility_funcs import change_directory from openmc import StatePoint from openmc.mgxs import GROUP_STRUCTURES from openmc.data import REACTION_MT @@ -286,6 +285,10 @@ class MicroXS: sections available. MicroXS entry will be 0 if the nuclide cross section is not found. + It is recommended to make repeated calls to this method within a context + manager using the :class:`openmc.lib.TemporarySession` class to avoid + re-initializing OpenMC and loading cross sections each time. + .. versionadded:: 0.15.0 Parameters @@ -349,37 +352,23 @@ class MicroXS: # Create 3D array for microscopic cross sections microxs_arr = np.zeros((len(nuclides), len(mts), 1)) - # Create a material with all nuclides - mat_all_nucs = openmc.Material() - for nuc in nuclides: - if nuc in nuclides_with_data: - mat_all_nucs.add_nuclide(nuc, 1.0) - mat_all_nucs.set_density("atom/b-cm", 1.0) + def compute_microxs(): + # For each nuclide and reaction, compute the flux-averaged xs + for nuc_index, nuc in enumerate(nuclides): + if nuc not in nuclides_with_data: + continue + lib_nuc = openmc.lib.load_nuclide(nuc) + for mt_index, mt in enumerate(mts): + microxs_arr[nuc_index, mt_index, 0] = lib_nuc.collapse_rate( + mt, temperature, energies, multigroup_flux + ) - # Create simple model containing the above material - surf1 = openmc.Sphere(boundary_type="vacuum") - surf1_cell = openmc.Cell(fill=mat_all_nucs, region=-surf1) - model = openmc.Model() - model.geometry = openmc.Geometry([surf1_cell]) - model.settings = openmc.Settings( - particles=1, batches=1, output={'summary': False}) - - with change_directory(tmpdir=True): - # Export model within temporary directory - model.export_to_model_xml() - - with openmc.lib.run_in_memory(**init_kwargs): - # For each nuclide and reaction, compute the flux-averaged - # cross section - for nuc_index, nuc in enumerate(nuclides): - if nuc not in nuclides_with_data: - continue - lib_nuc = openmc.lib.nuclides[nuc] - for mt_index, mt in enumerate(mts): - xs = lib_nuc.collapse_rate( - mt, temperature, energies, multigroup_flux - ) - microxs_arr[nuc_index, mt_index, 0] = xs + # Compute microscopic cross sections within a temporary session + if not openmc.lib.is_initialized: + with openmc.lib.TemporarySession(**init_kwargs): + compute_microxs() + else: + compute_microxs() return cls(microxs_arr, nuclides, reactions) diff --git a/openmc/lib/core.py b/openmc/lib/core.py index 577913dcb..eac0e1784 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -4,7 +4,9 @@ from ctypes import (c_bool, c_int, c_int32, c_int64, c_double, c_char_p, c_uint64, c_size_t) import sys import os +from pathlib import Path from random import getrandbits +from tempfile import TemporaryDirectory import numpy as np from numpy.ctypeslib import as_array @@ -617,6 +619,69 @@ def run_in_memory(**kwargs): finalize() +class TemporarySession: + """Context manager for running via openmc.lib in a temporary directory. + + This class is useful for accessing functionality from openmc.lib without + polluting your current working directory with OpenMC files. It is used + internally as a persistent session to avoid loading cross sections multiple + times. + + Parameters + ---------- + model : openmc.Model, optional + OpenMC model to use for the session. If None, a minimal working model is + created. + **init_kwargs + Keyword arguments to pass to :func:`openmc.lib.init`. + + Attributes + ---------- + model : openmc.Model + The OpenMC model used for the session. + + """ + def __init__(self, model=None, **init_kwargs): + self.init_kwargs = init_kwargs + if model is None: + surf = openmc.Sphere(boundary_type="vacuum") + cell = openmc.Cell(region=-surf) + model = openmc.Model() + model.geometry = openmc.Geometry([cell]) + model.settings = openmc.Settings( + particles=1, batches=1, output={'summary': False}) + self.model = model + + def __enter__(self): + """Initialize the OpenMC library in a temporary directory.""" + # Make sure OpenMC is not already initialized + if openmc.lib.is_initialized: + raise RuntimeError("openmc.lib is already initialized.") + + # Store original working directory + self.orig_dir = Path.cwd() + + # Set up temporary directory + self.tmp_dir = TemporaryDirectory() + working_dir = Path(self.tmp_dir.name) + working_dir.mkdir(parents=True, exist_ok=True) + os.chdir(working_dir) + + # Export model and initialize OpenMC + self.model.export_to_model_xml() + openmc.lib.init(**self.init_kwargs) + + return self + + def __exit__(self, exc_type, exc_value, traceback): + """Finalize the OpenMC library and clean up temporary directory.""" + try: + openmc.lib.finalize() + finally: + os.chdir(self.orig_dir) + self.tmp_dir.cleanup() + + class _DLLGlobal: """Data descriptor that exposes global variables from libopenmc.""" def __init__(self, ctype, name): diff --git a/openmc/lib/nuclide.py b/openmc/lib/nuclide.py index 8078882cf..ef1287cf3 100644 --- a/openmc/lib/nuclide.py +++ b/openmc/lib/nuclide.py @@ -40,8 +40,14 @@ def load_nuclide(name): name : str Name of the nuclide, e.g. 'U235' + Returns + ------- + Nuclide + The class:`Nuclide` that was just loaded. + """ _dll.openmc_load_nuclide(name.encode(), None, 0) + return nuclides[name] class Nuclide(_FortranObject): diff --git a/openmc/mesh.py b/openmc/mesh.py index b046769fd..56fe2bd1c 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -400,31 +400,30 @@ class MeshBase(IDManagerMixin, ABC): """ import openmc.lib - with change_directory(tmpdir=True): - # In order to get mesh into model, we temporarily replace the - # tallies with a single mesh tally using the current mesh - original_tallies = model.tallies - new_tally = openmc.Tally() - new_tally.filters = [openmc.MeshFilter(self)] - new_tally.scores = ['flux'] - model.tallies = [new_tally] + # In order to get mesh into model, we temporarily replace the + # tallies with a single mesh tally using the current mesh + original_tallies = model.tallies + new_tally = openmc.Tally() + new_tally.filters = [openmc.MeshFilter(self)] + new_tally.scores = ['flux'] + model.tallies = [new_tally] - # Export model to XML - model.export_to_model_xml() + # Set default arguments + kwargs.setdefault('output', True) + if 'args' in kwargs: + kwargs['args'] = ['-c'] + kwargs['args'] + kwargs.setdefault('args', ['-c']) - # Get material volume fractions - kwargs.setdefault('output', True) - if 'args' in kwargs: - kwargs['args'] = ['-c'] + kwargs['args'] - kwargs.setdefault('args', ['-c']) - openmc.lib.init(**kwargs) + with openmc.lib.TemporarySession(model, **kwargs): + # Get mesh from single tally mesh = openmc.lib.tallies[new_tally.id].filters[0].mesh + + # Compute material volumes volumes = mesh.material_volumes( n_samples, max_materials, output=kwargs['output']) - openmc.lib.finalize() - # Restore original tallies - model.tallies = original_tallies + # Restore original tallies + model.tallies = original_tallies return volumes diff --git a/openmc/weight_windows.py b/openmc/weight_windows.py index 1b44747d5..90856ebab 100644 --- a/openmc/weight_windows.py +++ b/openmc/weight_windows.py @@ -1041,10 +1041,6 @@ class WeightWindowsList(list): # Get absolute path before moving to temporary directory path = Path(path).resolve() - with change_directory(tmpdir=True): - # Write the model to an XML file - model.export_to_model_xml() - - # Load the model with openmc.lib and then export it to an HDF5 file - with openmc.lib.run_in_memory(**init_kwargs): - openmc.lib.export_weight_windows(path) + # Load the model with openmc.lib and then export it to an HDF5 file + with openmc.lib.TemporarySession(model, **init_kwargs): + openmc.lib.export_weight_windows(path) diff --git a/src/nuclide.cpp b/src/nuclide.cpp index eaac2f756..7cb84640d 100644 --- a/src/nuclide.cpp +++ b/src/nuclide.cpp @@ -1114,7 +1114,7 @@ extern "C" size_t nuclides_size() extern "C" int openmc_load_nuclide(const char* name, const double* temps, int n) { if (data::nuclide_map.find(name) == data::nuclide_map.end() || - data::nuclide_map.at(name) >= data::elements.size()) { + data::nuclide_map.at(name) >= data::nuclides.size()) { LibraryKey key {Library::Type::neutron, name}; const auto& it = data::library_map.find(key); if (it == data::library_map.end()) { @@ -1215,7 +1215,6 @@ extern "C" int openmc_nuclide_collapse_rate(int index, int MT, *xs = data::nuclides[index]->collapse_rate( MT, temperature, {energy, energy + n + 1}, {flux, flux + n}); } catch (const std::out_of_range& e) { - fmt::print("Caught error\n"); set_errmsg(e.what()); return OPENMC_E_OUT_OF_BOUNDS; } diff --git a/tests/unit_tests/test_deplete_chain.py b/tests/unit_tests/test_deplete_chain.py index 13267cb05..bd5cc2263 100644 --- a/tests/unit_tests/test_deplete_chain.py +++ b/tests/unit_tests/test_deplete_chain.py @@ -310,8 +310,7 @@ def test_capture_branch_infer_ground(): # Create nuclide to be added into the chain xe136m = nuclide.Nuclide("Xe136_m1") - chain.nuclides.append(xe136m) - chain.nuclide_dict[xe136m.name] = len(chain.nuclides) - 1 + chain.add_nuclide(xe136m) chain.set_branch_ratios(infer_br, "(n,gamma)") @@ -327,8 +326,7 @@ def test_capture_branch_no_rxn(): u5m = nuclide.Nuclide("U235_m1") - chain.nuclides.append(u5m) - chain.nuclide_dict[u5m.name] = len(chain.nuclides) - 1 + chain.add_nuclide(u5m) with pytest.raises(AttributeError, match="U234"): chain.set_branch_ratios(u4br) From 6636b7e32b3347c5846aa26c434138eda6f0ee84 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Thu, 3 Jul 2025 06:52:39 +0300 Subject: [PATCH 364/671] fix zam parsing (#3484) --- openmc/data/data.py | 2 +- tests/unit_tests/test_data_misc.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openmc/data/data.py b/openmc/data/data.py index 8ccc8d263..2142a5dc9 100644 --- a/openmc/data/data.py +++ b/openmc/data/data.py @@ -607,7 +607,7 @@ def zam(name): """ try: - symbol, A, state = _GNDS_NAME_RE.match(name).groups() + symbol, A, state = _GNDS_NAME_RE.fullmatch(name).groups() except AttributeError: raise ValueError(f"'{name}' does not appear to be a nuclide name in " "GNDS format") diff --git a/tests/unit_tests/test_data_misc.py b/tests/unit_tests/test_data_misc.py index 226d848a5..b65c704af 100644 --- a/tests/unit_tests/test_data_misc.py +++ b/tests/unit_tests/test_data_misc.py @@ -131,7 +131,8 @@ def test_zam(): assert openmc.data.zam('Am242_m10') == (95, 242, 10) with pytest.raises(ValueError): openmc.data.zam('garbage') - + with pytest.raises(ValueError): + openmc.data.zam('Am242-m1') def test_half_life(): assert openmc.data.half_life('H2') is None From efcf8f649e7be1d709a308c587d67c8bd58db6b8 Mon Sep 17 00:00:00 2001 From: Micah Gale Date: Thu, 3 Jul 2025 03:32:43 -0500 Subject: [PATCH 365/671] Added citation metadata file (#3409) --- CITATION.cff | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 CITATION.cff diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 000000000..c1bc1443f --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,33 @@ +preferred-citation: + authors: + - family-names: Romano + given-names: Paul K. + orcid: "https://orcid.org/0000-0002-1147-045X" + - final-names: Horelik + given-names: Nicholas E. + - family-names: Herman + given-names: Bryan R. + - family-names: Nelson + given-names: Adam G. + - family-names: Forget + given-names: Benoit + orcid: "https://orcid.org/0000-0003-1459-7672" + - family-names: Smith + given-names: Kord + contact: + - family-names: Romano + given-names: Paul K. + orcid: "https://orcid.org/0000-0002-1147-045X" + date-published: 2015-08 + doi: 10.1016/j.anucene.2014.07.048 + issn: 1873-2100 + volume: 82 + journal: Annals of Nuclear Energy + publisher: + name: Elsevier + start: 90 + title: "OpenMC: A state-of-the-art Monte Carlo code for research and development" + type: article + url: "https://www.sciencedirect.com/science/article/pii/S030645491400379X" + volume: 10 + From b6c6ac078b391b3647510360529dcac0ac72c93a Mon Sep 17 00:00:00 2001 From: Nathan Glaser <123763273+nglaser3@users.noreply.github.com> Date: Thu, 3 Jul 2025 09:49:25 -0500 Subject: [PATCH 366/671] Add flag to CMakeLists to use submodules instead of searching (#3480) Co-authored-by: Paul Romano --- CMakeLists.txt | 38 +++++++++++++++++++++++------- docs/source/usersguide/install.rst | 5 ++++ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6665682dd..48f3bfc39 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_MCPL "Enable MCPL" OFF) option(OPENMC_USE_UWUW "Enable UWUW" OFF) +option(OPENMC_FORCE_VENDORED_LIBS "Explicitly use submodules defined in 'vendor'" 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_MCPL ${OPENMC_USE_MCPL}") message(STATUS "OPENMC_USE_UWUW ${OPENMC_USE_UWUW}") +message(STATUS "OPENMC_FORCE_VENDORED_LIBS ${OPENMC_FORCE_VENDORED_LIBS}") # Warnings for deprecated options foreach(OLD_OPT IN ITEMS "openmp" "profile" "coverage" "dagmc" "libmesh") @@ -256,31 +258,47 @@ endif() # pugixml library #=============================================================================== -find_package_write_status(pugixml) -if (NOT pugixml_FOUND) +if(OPENMC_FORCE_VENDORED_LIBS) add_subdirectory(vendor/pugixml) set_target_properties(pugixml PROPERTIES CXX_STANDARD 14 CXX_EXTENSIONS OFF) +else() + find_package_write_status(pugixml) + if (NOT pugixml_FOUND) + add_subdirectory(vendor/pugixml) + set_target_properties(pugixml PROPERTIES CXX_STANDARD 14 CXX_EXTENSIONS OFF) + endif() endif() #=============================================================================== # {fmt} library #=============================================================================== -find_package_write_status(fmt) -if (NOT fmt_FOUND) +if(OPENMC_FORCE_VENDORED_LIBS) set(FMT_INSTALL ON CACHE BOOL "Generate the install target.") add_subdirectory(vendor/fmt) +else() + find_package_write_status(fmt) + if (NOT fmt_FOUND) + set(FMT_INSTALL ON CACHE BOOL "Generate the install target.") + add_subdirectory(vendor/fmt) + endif() endif() #=============================================================================== # xtensor header-only library #=============================================================================== -find_package_write_status(xtensor) -if (NOT xtensor_FOUND) +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() #=============================================================================== @@ -288,9 +306,13 @@ endif() #=============================================================================== if(OPENMC_BUILD_TESTS) - find_package_write_status(Catch2) - if (NOT Catch2_FOUND) + if (OPENMC_FORCE_VENDORED_LIBS) add_subdirectory(vendor/Catch2) + else() + find_package_write_status(Catch2) + if (NOT Catch2_FOUND) + add_subdirectory(vendor/Catch2) + endif() endif() endif() diff --git a/docs/source/usersguide/install.rst b/docs/source/usersguide/install.rst index 2621109f7..063c985f4 100644 --- a/docs/source/usersguide/install.rst +++ b/docs/source/usersguide/install.rst @@ -380,6 +380,11 @@ OPENMC_USE_MPI options, please see the `FindMPI.cmake documentation `_. +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 + modules. + To set any of these options (e.g., turning on profiling), the following form should be used: From dd8d621f0b7d99db02e7b9dc855e514bac756096 Mon Sep 17 00:00:00 2001 From: April Novak Date: Thu, 3 Jul 2025 09:10:12 -0600 Subject: [PATCH 367/671] Only show warning if in restart mode. Refs #3477 (#3478) --- src/state_point.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/state_point.cpp b/src/state_point.cpp index 3b822715c..12dfeb82a 100644 --- a/src/state_point.cpp +++ b/src/state_point.cpp @@ -436,7 +436,8 @@ extern "C" int openmc_statepoint_load(const char* filename) // Read batch number to restart at read_dataset(file_id, "current_batch", simulation::restart_batch); - if (simulation::restart_batch >= settings::n_max_batches) { + if (settings::restart_run && + simulation::restart_batch >= settings::n_max_batches) { warning(fmt::format( "The number of batches specified for simulation ({}) is smaller " "than or equal to the number of batches in the restart statepoint file " From d700d395de02d95190eab35514f25f9ea29d516f Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 3 Jul 2025 17:59:40 -0500 Subject: [PATCH 368/671] Update conda install instructions for macOS Apple silicon (#3488) --- CITATION.cff | 11 ++++++----- docs/source/quickinstall.rst | 7 +++++++ docs/source/usersguide/install.rst | 7 +++++++ 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index c1bc1443f..19b4213a1 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -9,6 +9,7 @@ preferred-citation: given-names: Bryan R. - family-names: Nelson given-names: Adam G. + orcid: "https://orcid.org/0000-0002-3614-0676" - family-names: Forget given-names: Benoit orcid: "https://orcid.org/0000-0003-1459-7672" @@ -18,16 +19,16 @@ preferred-citation: - family-names: Romano given-names: Paul K. orcid: "https://orcid.org/0000-0002-1147-045X" - date-published: 2015-08 doi: 10.1016/j.anucene.2014.07.048 - issn: 1873-2100 + issn: 0306-4549 volume: 82 journal: Annals of Nuclear Energy publisher: name: Elsevier start: 90 + end: 97 + year: 2015 + month: 8 title: "OpenMC: A state-of-the-art Monte Carlo code for research and development" type: article - url: "https://www.sciencedirect.com/science/article/pii/S030645491400379X" - volume: 10 - + url: "https://doi.org/10.1016/j.anucene.2014.07.048" diff --git a/docs/source/quickinstall.rst b/docs/source/quickinstall.rst index 21526242f..47c257dfe 100644 --- a/docs/source/quickinstall.rst +++ b/docs/source/quickinstall.rst @@ -35,6 +35,13 @@ you wish) with OpenMC installed. conda create --name openmc-env openmc conda activate openmc-env +If you are installing on macOS with an Apple silicon ARM-based processor, you +will also need to specify the `--platform` option: + +.. code-block:: sh + + conda create --name openmc-env --platform osx-arm64 openmc + You are now in a conda environment called `openmc-env` that has OpenMC installed. diff --git a/docs/source/usersguide/install.rst b/docs/source/usersguide/install.rst index 063c985f4..775e38109 100644 --- a/docs/source/usersguide/install.rst +++ b/docs/source/usersguide/install.rst @@ -35,6 +35,13 @@ you wish) with OpenMC installed. conda create --name openmc-env openmc conda activate openmc-env +If you are installing on macOS with an Apple silicon ARM-based processor, you +will also need to specify the `--platform` option: + +.. code-block:: sh + + conda create --name openmc-env --platform osx-arm64 openmc + You are now in a conda environment called `openmc-env` that has OpenMC installed. From 58ee8d825dfc6d5bcb98a0156a46e023a84e9aea Mon Sep 17 00:00:00 2001 From: Shikhar Kumar Date: Tue, 15 Jul 2025 16:53:52 -0400 Subject: [PATCH 369/671] Update OSX install instructions to point to x64 platform (#3501) --- docs/source/quickinstall.rst | 2 +- docs/source/usersguide/install.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/quickinstall.rst b/docs/source/quickinstall.rst index 47c257dfe..0f887940e 100644 --- a/docs/source/quickinstall.rst +++ b/docs/source/quickinstall.rst @@ -40,7 +40,7 @@ will also need to specify the `--platform` option: .. code-block:: sh - conda create --name openmc-env --platform osx-arm64 openmc + conda create --name openmc-env --platform osx-64 openmc You are now in a conda environment called `openmc-env` that has OpenMC installed. diff --git a/docs/source/usersguide/install.rst b/docs/source/usersguide/install.rst index 775e38109..cec3a541d 100644 --- a/docs/source/usersguide/install.rst +++ b/docs/source/usersguide/install.rst @@ -40,7 +40,7 @@ will also need to specify the `--platform` option: .. code-block:: sh - conda create --name openmc-env --platform osx-arm64 openmc + conda create --name openmc-env --platform osx-64 openmc You are now in a conda environment called `openmc-env` that has OpenMC installed. From 6372c29cfad804ce49ac79179659eff9d9cddfcd Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Wed, 16 Jul 2025 08:31:49 -0500 Subject: [PATCH 370/671] Provide a way to get ID maps from plot parameters on the Model class (#3481) Co-authored-by: Jonathan Shimwell Co-authored-by: Paul Romano --- openmc/lib/plot.py | 1 + openmc/model/model.py | 173 ++++++++++++++++------- openmc/plots.py | 13 +- tests/unit_tests/test_model.py | 243 +++++++++++++++++++++++++++++++++ 4 files changed, 378 insertions(+), 52 deletions(-) diff --git a/openmc/lib/plot.py b/openmc/lib/plot.py index d98636676..f97348b20 100644 --- a/openmc/lib/plot.py +++ b/openmc/lib/plot.py @@ -90,6 +90,7 @@ class _PlotBase(Structure): def __init__(self): self.level_ = -1 + self.basis_ = 1 self.color_overlaps_ = False @property diff --git a/openmc/model/model.py b/openmc/model/model.py index 55f309bfb..93e029a33 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -20,7 +20,7 @@ from openmc.dummy_comm import DummyCommunicator from openmc.executor import _process_CLI_arguments from openmc.checkvalue import check_type, check_value, PathLike from openmc.exceptions import InvalidIDError -from openmc.plots import add_plot_params +from openmc.plots import add_plot_params, _BASIS_INDICES from openmc.utility_funcs import change_directory @@ -902,6 +902,111 @@ class Model: openmc.lib.materials[domain_id].volume = \ vol_calc.volumes[domain_id].n + + def _set_plot_defaults( + self, + origin: Sequence[float] | None, + width: Sequence[float] | None, + pixels: int | Sequence[int], + basis: str + ): + x, y, _ = _BASIS_INDICES[basis] + + bb = self.bounding_box + # checks to see if bounding box contains -inf or inf values + if np.isinf(bb.extent[basis]).any(): + if origin is None: + origin = (0, 0, 0) + if width is None: + width = (10, 10) + else: + if origin is None: + # if nan values in the bb.center they get replaced with 0.0 + # this happens when the bounding_box contains inf values + with warnings.catch_warnings(): + warnings.simplefilter("ignore", RuntimeWarning) + origin = np.nan_to_num(bb.center) + if width is None: + bb_width = bb.width + width = (bb_width[x], bb_width[y]) + + if isinstance(pixels, int): + aspect_ratio = width[0] / width[1] + pixels_y = math.sqrt(pixels / aspect_ratio) + pixels = (int(pixels / pixels_y), int(pixels_y)) + + return origin, width, pixels + + def id_map( + self, + origin: Sequence[float] | None = None, + width: Sequence[float] | None = None, + pixels: int | Sequence[int] = 40000, + basis: str = 'xy', + **init_kwargs + ) -> np.ndarray: + """Generate an ID map for domains based on the plot parameters + + If the model is not yet initialized, it will be initialized with + openmc.lib. If the model is initialized, the model will remain + initialized after this method call exits. + + .. versionadded:: 0.15.3 + + Parameters + ---------- + origin : Sequence[float], optional + Origin of the plot. If unspecified, this argument defaults to the + center of the bounding box if the bounding box does not contain inf + values for the provided basis, otherwise (0.0, 0.0, 0.0). + width : Sequence[float], optional + Width of the plot. If unspecified, this argument defaults to the + width of the bounding box if the bounding box does not contain inf + values for the provided basis, otherwise (10.0, 10.0). + pixels : int | Sequence[int], optional + If an iterable of ints is provided then this directly sets the + number of pixels to use in each basis direction. If a single int is + provided then this sets the total number of pixels in the plot and + the number of pixels in each basis direction is calculated from this + total and the image aspect ratio based on the width argument. + basis : {'xy', 'yz', 'xz'}, optional + Basis of the plot. + **init_kwargs + Keyword arguments passed to :meth:`Model.init_lib`. + + Returns + ------- + id_map : numpy.ndarray + A NumPy array with shape (vertical pixels, horizontal pixels, 3) of + OpenMC property IDs with dtype int32. The last dimension of the + array contains cell IDs, cell instances, and material IDs (in that + order). + """ + import openmc.lib + + origin, width, pixels = self._set_plot_defaults( + origin, width, pixels, basis) + + # initialize the openmc.lib.plot._PlotBase object + plot_obj = openmc.lib.plot._PlotBase() + plot_obj.origin = origin + plot_obj.width = width[0] + plot_obj.height = width[1] + plot_obj.h_res = pixels[0] + plot_obj.v_res = pixels[1] + plot_obj.basis = basis + + if self.is_initialized: + return openmc.lib.id_map(plot_obj) + else: + # Silence output by default. Also set arguments to start in volume + # calculation mode to avoid loading cross sections + init_kwargs.setdefault('output', False) + init_kwargs.setdefault('args', ['-c']) + + with openmc.lib.TemporarySession(self, **init_kwargs): + return openmc.lib.id_map(plot_obj) + @add_plot_params def plot( self, @@ -945,39 +1050,13 @@ class Model: source_kwargs = {} source_kwargs.setdefault('marker', 'x') + # Set indices using basis and create axis labels + x, y, z = _BASIS_INDICES[basis] + xlabel, ylabel = f'{basis[0]} [{axis_units}]', f'{basis[1]} [{axis_units}]' + # Determine extents of plot - if basis == 'xy': - x, y, z = 0, 1, 2 - xlabel, ylabel = f'x [{axis_units}]', f'y [{axis_units}]' - elif basis == 'yz': - x, y, z = 1, 2, 0 - xlabel, ylabel = f'y [{axis_units}]', f'z [{axis_units}]' - elif basis == 'xz': - x, y, z = 0, 2, 1 - xlabel, ylabel = f'x [{axis_units}]', f'z [{axis_units}]' - - bb = self.bounding_box - # checks to see if bounding box contains -inf or inf values - if np.isinf(bb.extent[basis]).any(): - if origin is None: - origin = (0, 0, 0) - if width is None: - width = (10, 10) - else: - if origin is None: - # if nan values in the bb.center they get replaced with 0.0 - # this happens when the bounding_box contains inf values - with warnings.catch_warnings(): - warnings.simplefilter("ignore", RuntimeWarning) - origin = np.nan_to_num(bb.center) - if width is None: - bb_width = bb.width - width = (bb_width[x], bb_width[y]) - - if isinstance(pixels, int): - aspect_ratio = width[0] / width[1] - pixels_y = math.sqrt(pixels / aspect_ratio) - pixels = (int(pixels / pixels_y), int(pixels_y)) + origin, width, pixels = self._set_plot_defaults( + origin, width, pixels, basis) axis_scaling_factor = {'km': 0.00001, 'm': 0.01, 'cm': 1, 'mm': 10} @@ -1124,10 +1203,10 @@ class Model: return axes def sample_external_source( - self, - n_samples: int = 1000, - prn_seed: int | None = None, - **init_kwargs + self, + n_samples: int = 1000, + prn_seed: int | None = None, + **init_kwargs ) -> openmc.ParticleList: """Sample external source and return source particles. @@ -1150,17 +1229,17 @@ class Model: """ import openmc.lib - # Silence output by default. Also set arguments to start in volume - # calculation mode to avoid loading cross sections - init_kwargs.setdefault('output', False) - init_kwargs.setdefault('args', ['-c']) + if self.is_initialized: + return openmc.lib.sample_external_source( + n_samples=n_samples, prn_seed=prn_seed + ) + else: + # Silence output by default. Also set arguments to start in volume + # calculation mode to avoid loading cross sections + init_kwargs.setdefault('output', False) + init_kwargs.setdefault('args', ['-c']) - with change_directory(tmpdir=True): - # Export model within temporary directory - self.export_to_model_xml() - - # Sample external source sites - with openmc.lib.run_in_memory(**init_kwargs): + with openmc.lib.TemporarySession(self, **init_kwargs): return openmc.lib.sample_external_source( n_samples=n_samples, prn_seed=prn_seed ) diff --git a/openmc/plots.py b/openmc/plots.py index 40fcf0c78..9e097e2b9 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -15,6 +15,8 @@ from .mixin import IDManagerMixin _BASES = {'xy', 'xz', 'yz'} +_BASIS_INDICES = {'xy': (0, 1, 2), 'xz': (0, 2, 1), 'yz': (1, 2, 0)} + _SVG_COLORS = { 'aliceblue': (240, 248, 255), 'antiquewhite': (250, 235, 215), @@ -178,11 +180,12 @@ _PLOT_PARAMS = """ ascertain the plot width. Defaults to (10, 10) if the bounding box contains inf values. pixels : Iterable of int or int - If iterable of ints provided then this directly sets the number of - pixels to use in each basis direction. If int provided then this - sets the total number of pixels in the plot and the number of - pixels in each basis direction is calculated from this total and - the image aspect ratio. + If an iterable of ints is provided then this directly sets the + number of pixels to use in each basis direction. If a single int + is provided then this sets the total number of pixels in the plot + and the number of pixels in each basis direction is calculated + from this total and the image aspect ratio based on the width + argument. basis : {'xy', 'xz', 'yz'} The basis directions for the plot color_by : {'cell', 'material'} diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index aa844e2f2..3797188ba 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -649,3 +649,246 @@ def test_model_plot(): # ensure that all of the data in the image data is either white or red test_mask = (image_data == white) | (image_data == red) assert np.all(test_mask), "Colors other than white or red found in overlap plot image" + + +def test_model_id_map_initialization(run_in_tmpdir): + model = openmc.examples.pwr_assembly() + model.init_lib(output=False) + + id_map = model.id_map( + pixels=(100, 100), + basis='xy', + origin=(0, 0, 0), + width=(10, 10), + ) + + assert id_map.shape == (100, 100, 3) + assert id_map.dtype == np.int32 + + max_cell_id = max(model.geometry.get_all_cells().keys()) + max_material_id = max(model.geometry.get_all_materials().keys()) + + # add some spot checks for the id_map + # Check that the array contains valid cell/material IDs (not all -2) + # The -2 values indicate outside the geometry + assert not np.all(id_map == -2), "All values are -2, indicating no valid geometry found" + + # Check that we have valid cell IDs (first dimension) + valid_cell_ids = id_map[:, :, 0] + assert np.any(valid_cell_ids >= 0), "No valid cell IDs found in the id_map" + + # Check that we have valid material IDs (third dimension) + valid_material_ids = id_map[:, :, 2] + assert np.any(valid_material_ids >= 0), "No valid material IDs found in the id_map" + + # Check that the middle dimension (cell instances) is consistent + # Cell instances should be >= 0 when cell IDs are valid + cell_instances = id_map[:, :, 1] + valid_cells = valid_cell_ids >= 0 + if np.any(valid_cells): + assert np.all(cell_instances[valid_cells] >= 0), "Invalid cell instances found for valid cells" + + # Check that the array contains reasonable ranges of values + # Cell IDs should be within the expected range for the assembly + if np.any(valid_cell_ids >= 0): + max_map_cell_id = np.max(valid_cell_ids) + assert max_map_cell_id <= max_cell_id, \ + f"Cell ID {max_map_cell_id} in the map is greater than the maximum cell ID {max_cell_id}" + + # Material IDs should be within the expected range + if np.any(valid_material_ids >= 0): + max_map_material_id = np.max(valid_material_ids) + assert max_map_material_id <= max_material_id, \ + f"Material ID {max_map_material_id} in the map is greater than the maximum material ID {max_material_id}" + + # Test id_map with pixels outside the model geometry + # Use a plot that's far from the model center to ensure we get -2 values + outside_id_map = model.id_map( + pixels=(50, 50), + basis='xy', + origin=(1000, 1000, 0), # Far from the model center + width=(10, 10), + ) + + assert outside_id_map.shape == (50, 50, 3) + assert outside_id_map.dtype == np.int32 + + # All values should be -2 (outside geometry) for this plot + assert np.all(outside_id_map == -2), "Expected all values to be -2 for plot outside model geometry" + + # Verify that the outside plot has the correct structure + assert np.all(outside_id_map[:, :, 0] == -2), "Cell IDs should all be -2 outside geometry" + assert np.all(outside_id_map[:, :, 1] == -2), "Cell instances should all be -2 outside geometry" + assert np.all(outside_id_map[:, :, 2] == -2), "Material IDs should all be -2 outside geometry" + + # if the model is already initialized, it should not be finalized + # after calling this method + model.id_map( + pixels=(100, 100), + basis='xy', + origin=(0, 0, 0), + width=(10, 10), + ) + assert model.is_initialized + + # if the model is not initialized, it should be finalized + # before exiting this method + model.finalize_lib() + model.id_map( + pixels=(100, 100), + basis='xy', + origin=(0, 0, 0), + width=(10, 10), + ) + assert not model.is_initialized + + +def test_id_map_aligned_model(): + """Test id_map with a 2x2 lattice where pixel boundaries align to cell boundaries""" + # Create materials -- identical compositions, different IDs + mat1 = openmc.Material(material_id=1, name='Material 1') + mat1.set_density('g/cm3', 1.0) + mat1.add_element('H', 1.0) + + mat2 = openmc.Material(material_id=2, name='Material 2') + mat2.set_density('g/cm3', 1.0) + mat2.add_element('H', 1.0) + + mat3 = openmc.Material(material_id=3, name='Material 3') + mat3.set_density('g/cm3', 1.0) + mat3.add_element('H', 1.0) + + mat4 = openmc.Material(material_id=4, name='Material 4') + mat4.set_density('g/cm3', 1.0) + mat4.add_element('H', 1.0) + + outer_mat = openmc.Material(material_id=5, name='Material 5') + outer_mat.set_density('g/cm3', 1.0) + outer_mat.add_element('H', 1.0) + + inner_materials = [mat1, mat2, mat3, mat4] + + # Create square surface that fits inside the lattice cell + # Lattice cell is 1 cm x 1 cm, so square will be 0.6 cm x 0.6 cm centered on the origin + square = openmc.model.RectangularPrism(0.6, 0.6, boundary_type='transmission') + + # Create cells for this universe + inner_cell = openmc.Cell(cell_id=10, region=-square, name='inner_cell') + inner_cell.fill = inner_materials + + outer_cell = openmc.Cell(cell_id=20, region=+square, name='outer_cell') + outer_cell.fill = outer_mat + + # Create universe + universe = openmc.Universe(universe_id=100, cells=[inner_cell, outer_cell]) + + # Create 2x2 lattice + lattice = openmc.RectLattice(lattice_id=1) + lattice.lower_left = [-1.0, -1.0] + lattice.pitch = [1.0, 1.0] + lattice.universes = [[universe, universe], [universe, universe]] + + # Create outer boundary + outer_boundary = openmc.model.RectangularPrism(2.0, 2.0, boundary_type='vacuum') + + # Create root cell + root_cell = openmc.Cell(cell_id=1, name='root', fill=lattice, region=-outer_boundary) + + # Create geometry + geometry = openmc.Geometry([root_cell]) + + # Create settings + settings = openmc.Settings() + settings.particles = 1000 + settings.batches = 10 + + # Create model + model = openmc.Model(settings=settings, geometry=geometry) + + # Generate id_map with pixel boundaries aligned to cell boundaries + # The model is 2 cm x 2 cm, so we'll use 200x200 pixels to get 0.01 cm resolution + # This allows us to align pixels with the squares inside each lattice cell + id_map = model.id_map( + pixels=(200, 200), + basis='xy', + origin=(0.0, 0.0, 0.0), # Align with lattice lower_left + width=(2.0, 2.0), # Align with lattice size + ) + + # Verify id_map properties + assert id_map.shape == (200, 200, 3) + assert id_map.dtype == np.int32 + + cell_id_map = id_map[:, :, 0] + material_ids_map = id_map[:, :, 2] + + # Check that we have valid cell IDs (not all -2) + assert np.any(cell_id_map >= 0), "No valid cell IDs found in the id_map" + + # Check that we have valid material IDs + assert np.any(material_ids_map >= 0), "No valid material IDs found in the id_map" + + # Check that the expected cell IDs are present + expected_cell_ids = [10, 20] # Root cell, inner cell, outer cell + found_cell_ids = np.unique(cell_id_map[cell_id_map >= 0]) + for cell_id in expected_cell_ids: + assert cell_id in found_cell_ids, f"Expected cell ID {cell_id} not found in id_map" + + # Check that the expected material IDs are present + expected_material_ids = [1, 2, 3, 4, 5] # All materials defined above + found_material_ids = np.unique(material_ids_map[material_ids_map >= 0]) + for mat_id in expected_material_ids: + assert mat_id in found_material_ids, f"Expected material ID {mat_id} not found in id_map" + + # Test specific regions to verify lattice structure + # Check center of each lattice cell (should be inner cells) + # Lattice cell centers are at (-0.5, -0.5), (0.5, -0.5), (-0.5, 0.5), (0.5, 0.5) + # With 200x200 pixels over 2x2 units, each pixel is 0.01 units + + # Bottom-left lattice cell center (should be inner cell 10) + bl_cell, bl_instance, bl_material = id_map[-50, 50] + assert bl_cell == 10, f"Expected cell ID 10 at bottom-left center, got {bl_cell}" + assert bl_instance == 0, f"Expected cell instance 0 at bottom-left center, got {bl_instance}" + assert bl_material == 1, f"Expected material ID 1 at bottom-left center, got {bl_material}" + + # Bottom-right lattice cell center (should be inner cell 10) + br_cell, br_instance, br_material = id_map[-50, 150] + assert br_cell == 10, f"Expected cell ID 10 at bottom-right center, got {br_cell}" + assert br_instance == 1, f"Expected cell instance 1 at bottom-right center, got {br_instance}" + assert br_material == 2, f"Expected material ID 2 at bottom-right center, got {br_material}" + + # Top-left lattice cell center (should be inner cell 10) + tl_cell, tl_instance, tl_material = id_map[-150, 50] + assert tl_cell == 10, f"Expected cell ID 10 at top-left center, got {tl_cell}" + assert tl_instance == 2, f"Expected cell instance 2 at top-left center, got {tl_instance}" + assert tl_material == 3, f"Expected material ID 3 at top-left center, got {tl_material}" + + # Top-right lattice cell center (should be inner cell 10) + tr_cell, tr_instance, tr_material = id_map[-150, 150] + assert tr_cell == 10, f"Expected cell ID 10 at top-right center, got {tr_cell}" + assert tr_instance == 3, f"Expected cell instance 3 at top-right center, got {tr_instance}" + assert tr_material == 4, f"Expected material ID 4 at top-right center, got {tr_material}" + + # Check that the model is properly finalized after id_map call + assert not model.is_initialized, "Model should be finalized after id_map call" + + # Check that the values at the corners are correctly set as the outer cell and material + bl_cell, bl_instance, bl_material = id_map[-1, 0] + assert bl_cell == 20, f"Expected cell ID 20 at bottom-left corner, got {bl_cell}" + assert bl_instance == 0, f"Expected cell instance 0 at bottom-left corner, got {bl_instance}" + assert bl_material == 5, f"Expected material ID 5 at bottom-left corner, got {bl_material}" + + br_cell, br_instance, br_material = id_map[-1, -1] + assert br_cell == 20, f"Expected cell ID 20 at bottom-right corner, got {br_cell}" + assert br_instance == 1, f"Expected cell instance 1 at bottom-right corner, got {br_instance}" + assert br_material == 5, f"Expected material ID 5 at bottom-right corner, got {br_material}" + + tl_cell, tl_instance, tl_material = id_map[0, 0] + assert tl_cell == 20, f"Expected cell ID 20 at top-left corner, got {tl_cell}" + assert tl_instance == 2, f"Expected cell instance 2 at top-left corner, got {tl_instance}" + assert tl_material == 5, f"Expected material ID 5 at top-left corner, got {tl_material}" + + tr_cell, tr_instance, tr_material = id_map[0, -1] + assert tr_cell == 20, f"Expected cell ID 20 at top-right corner, got {tr_cell}" + assert tr_instance == 3, f"Expected cell instance 3 at top-right corner, got {tr_instance}" + assert tr_material == 5, f"Expected material ID 5 at top-right corner, got {tr_material}" From 24f78e6e924f5163e5e45dd307f8819d78073bfe Mon Sep 17 00:00:00 2001 From: Perry <100789850+yrrepy@users.noreply.github.com> Date: Wed, 16 Jul 2025 17:40:02 -0700 Subject: [PATCH 371/671] Use auto-chunking for StepResult HDF5 writing (#3498) --- openmc/deplete/stepresult.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openmc/deplete/stepresult.py b/openmc/deplete/stepresult.py index 9cf33898f..7c86b8645 100644 --- a/openmc/deplete/stepresult.py +++ b/openmc/deplete/stepresult.py @@ -328,13 +328,13 @@ class StepResult: handle.create_dataset("number", (1, n_stages, n_mats, n_nuc_number), maxshape=(None, n_stages, n_mats, n_nuc_number), - chunks=(1, 1, n_mats, n_nuc_number), + chunks=True, dtype='float64') if n_nuc_rxn > 0 and n_rxn > 0: handle.create_dataset("reaction rates", (1, n_stages, n_mats, n_nuc_rxn, n_rxn), maxshape=(None, n_stages, n_mats, n_nuc_rxn, n_rxn), - chunks=(1, 1, n_mats, n_nuc_rxn, n_rxn), + chunks=True, dtype='float64') handle.create_dataset("eigenvalues", (1, n_stages, 2), From 5318ea6e2b46000f3805d29edfbae1530694951d Mon Sep 17 00:00:00 2001 From: Ahnaf Tahmid Chowdhury Date: Thu, 17 Jul 2025 08:46:39 +0600 Subject: [PATCH 372/671] Make MCPL a Runtime Optional Dependency (#3429) Co-authored-by: Paul Romano --- CMakeLists.txt | 23 - cmake/OpenMCConfig.cmake.in | 4 - docs/source/usersguide/install.rst | 4 - include/openmc/mcpl_interface.h | 22 +- openmc/lib/__init__.py | 3 - src/mcpl_interface.cpp | 537 ++++++++++++++---- src/settings.cpp | 12 - .../source_mcpl_file/settings.xml | 9 +- .../regression_tests/source_mcpl_file/test.py | 27 +- 9 files changed, 438 insertions(+), 203 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 48f3bfc39..2b863eb08 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,7 +36,6 @@ option(OPENMC_ENABLE_COVERAGE "Compile with coverage analysis flags" option(OPENMC_USE_DAGMC "Enable support for DAGMC (CAD) geometry" OFF) option(OPENMC_USE_LIBMESH "Enable support for libMesh unstructured mesh tallies" OFF) option(OPENMC_USE_MPI "Enable MPI" OFF) -option(OPENMC_USE_MCPL "Enable MCPL" OFF) option(OPENMC_USE_UWUW "Enable UWUW" OFF) option(OPENMC_FORCE_VENDORED_LIBS "Explicitly use submodules defined in 'vendor'" OFF) @@ -47,7 +46,6 @@ message(STATUS "OPENMC_ENABLE_COVERAGE ${OPENMC_ENABLE_COVERAGE}") message(STATUS "OPENMC_USE_DAGMC ${OPENMC_USE_DAGMC}") message(STATUS "OPENMC_USE_LIBMESH ${OPENMC_USE_LIBMESH}") message(STATUS "OPENMC_USE_MPI ${OPENMC_USE_MPI}") -message(STATUS "OPENMC_USE_MCPL ${OPENMC_USE_MCPL}") message(STATUS "OPENMC_USE_UWUW ${OPENMC_USE_UWUW}") message(STATUS "OPENMC_FORCE_VENDORED_LIBS ${OPENMC_FORCE_VENDORED_LIBS}") @@ -191,22 +189,6 @@ if(${HDF5_VERSION} VERSION_GREATER_EQUAL 1.12.0) list(APPEND cxxflags -DH5Oget_info_by_idx_vers=1 -DH5O_info_t_vers=1) endif() -#=============================================================================== -# MCPL -#=============================================================================== - -if (OPENMC_USE_MCPL) - if (NOT DEFINED MCPL_DIR) - execute_process( - COMMAND mcpl-config --show cmakedir - OUTPUT_VARIABLE MCPL_DIR - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - endif() - find_package(MCPL REQUIRED) - message(STATUS "Found MCPL: ${MCPL_DIR} (found version \"${MCPL_VERSION}\")") -endif() - #=============================================================================== # Set compile/link flags based on which compiler is being used #=============================================================================== @@ -557,11 +539,6 @@ if (OPENMC_BUILD_TESTS) add_subdirectory(tests/cpp_unit_tests) endif() -if (OPENMC_USE_MCPL) - target_compile_definitions(libopenmc PUBLIC OPENMC_MCPL) - target_link_libraries(libopenmc MCPL::mcpl) -endif() - #=============================================================================== # Log build info that this executable can report later #=============================================================================== diff --git a/cmake/OpenMCConfig.cmake.in b/cmake/OpenMCConfig.cmake.in index 7416013fb..3fe0c1bcd 100644 --- a/cmake/OpenMCConfig.cmake.in +++ b/cmake/OpenMCConfig.cmake.in @@ -29,10 +29,6 @@ if(@OPENMC_USE_OPENMP@) find_package(OpenMP REQUIRED) endif() -if(@OPENMC_USE_MCPL@) - find_package(MCPL REQUIRED HINTS @MCPL_DIR@) -endif() - if(@OPENMC_USE_UWUW@ AND NOT ${DAGMC_BUILD_UWUW}) message(FATAL_ERROR "UWUW is enabled in OpenMC but the DAGMC installation discovered was not configured with UWUW.") endif() diff --git a/docs/source/usersguide/install.rst b/docs/source/usersguide/install.rst index cec3a541d..64d480179 100644 --- a/docs/source/usersguide/install.rst +++ b/docs/source/usersguide/install.rst @@ -375,10 +375,6 @@ OPENMC_USE_DAGMC should also be defined as `DAGMC_ROOT` in the CMake configuration command. (Default: off) -OPENMC_USE_MCPL - Turns on support for reading MCPL_ source files and writing MCPL source points - and surface sources. (Default: off) - OPENMC_USE_LIBMESH Enables the use of unstructured mesh tallies with libMesh_. (Default: off) diff --git a/include/openmc/mcpl_interface.h b/include/openmc/mcpl_interface.h index e5c182280..f7323e10a 100644 --- a/include/openmc/mcpl_interface.h +++ b/include/openmc/mcpl_interface.h @@ -9,12 +9,6 @@ namespace openmc { -//============================================================================== -// Constants -//============================================================================== - -extern "C" const bool MCPL_ENABLED; - //============================================================================== // Functions //============================================================================== @@ -29,14 +23,18 @@ vector mcpl_source_sites(std::string path); // //! \param[in] filename Path to MCPL file //! \param[in] source_bank Vector of SourceSites to write to file for this -//! MPI rank. Note that this can't be const due to -//! it being used as work space by MPI. -//! \param[in] bank_indx Pointer to vector of site index ranges over all -//! MPI ranks. This can be computed by calling -//! calculate_parallel_index_vector on -//! source_bank.size(). +//! MPI rank. +//! \param[in] bank_index Pointer to vector of site index ranges over all +//! MPI ranks. void write_mcpl_source_point(const char* filename, span source_bank, const vector& bank_index); + +//! Check if MCPL functionality is available +bool is_mcpl_interface_available(); + +//! Initialize the MCPL interface +void initialize_mcpl_interface_if_needed(); + } // namespace openmc #endif // OPENMC_MCPL_INTERFACE_H diff --git a/openmc/lib/__init__.py b/openmc/lib/__init__.py index 15642b42b..d2a794eb1 100644 --- a/openmc/lib/__init__.py +++ b/openmc/lib/__init__.py @@ -46,9 +46,6 @@ def _coord_levels(): def _libmesh_enabled(): return c_bool.in_dll(_dll, "LIBMESH_ENABLED").value -def _mcpl_enabled(): - return c_bool.in_dll(_dll, "MCPL_ENABLED").value - def _uwuw_enabled(): return c_bool.in_dll(_dll, "UWUW_ENABLED").value diff --git a/src/mcpl_interface.cpp b/src/mcpl_interface.cpp index 83ef63320..13915c3b8 100644 --- a/src/mcpl_interface.cpp +++ b/src/mcpl_interface.cpp @@ -11,32 +11,292 @@ #include -#ifdef OPENMC_MCPL -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#else +#include #endif +// WARNING: These declarations MUST EXACTLY MATCH the structure and function +// signatures of the libmcpl being loaded at runtime. Any discrepancy will +// likely lead to crashes or incorrect behavior. This is a maintenance risk. +// MCPL 2.2.0 + +#pragma pack(push, 1) +struct mcpl_particle_repr_t { + double ekin; + double polarisation[3]; + double position[3]; + double direction[3]; + double time; + double weight; + int32_t pdgcode; + uint32_t userflags; +}; +#pragma pack(pop) + +// Opaque struct definitions replicating the MCPL C-API to ensure ABI +// compatibility without including mcpl.h. These must be kept in sync. +struct mcpl_file_t { + void* internal; +}; +struct mcpl_outfile_t { + void* internal; +}; + +// Function pointer types for the dynamically loaded MCPL library +using mcpl_open_file_fpt = mcpl_file_t* (*)(const char* filename); +using mcpl_hdr_nparticles_fpt = uint64_t (*)(mcpl_file_t* file_handle); +using mcpl_read_fpt = const mcpl_particle_repr_t* (*)(mcpl_file_t* file_handle); +using mcpl_close_file_fpt = void (*)(mcpl_file_t* file_handle); + +using mcpl_create_outfile_fpt = mcpl_outfile_t* (*)(const char* filename); +using mcpl_hdr_set_srcname_fpt = void (*)( + mcpl_outfile_t* outfile_handle, const char* srcname); +using mcpl_add_particle_fpt = void (*)( + mcpl_outfile_t* outfile_handle, const mcpl_particle_repr_t* particle); +using mcpl_close_outfile_fpt = void (*)(mcpl_outfile_t* outfile_handle); + namespace openmc { -//============================================================================== -// Constants -//============================================================================== - -#ifdef OPENMC_MCPL -const bool MCPL_ENABLED = true; +#ifdef _WIN32 +using LibraryHandleType = HMODULE; #else -const bool MCPL_ENABLED = false; +using LibraryHandleType = void*; #endif -//============================================================================== -// Functions -//============================================================================== +std::string get_last_library_error() +{ +#ifdef _WIN32 + DWORD error_code = GetLastError(); + if (error_code == 0) + return "No error reported by system."; // More accurate than "No error." + LPSTR message_buffer = nullptr; + size_t size = + FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, error_code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPSTR)&message_buffer, 0, NULL); + std::string message(message_buffer, size); + LocalFree(message_buffer); + while ( + !message.empty() && (message.back() == '\n' || message.back() == '\r')) { + message.pop_back(); + } + return message; +#else + const char* err = dlerror(); + return err ? std::string(err) : "No error reported by dlerror."; +#endif +} -#ifdef OPENMC_MCPL -SourceSite mcpl_particle_to_site(const mcpl_particle_t* particle) +struct McplApi { + mcpl_open_file_fpt open_file; + mcpl_hdr_nparticles_fpt hdr_nparticles; + mcpl_read_fpt read; + mcpl_close_file_fpt close_file; + mcpl_create_outfile_fpt create_outfile; + mcpl_hdr_set_srcname_fpt hdr_set_srcname; + mcpl_add_particle_fpt add_particle; + mcpl_close_outfile_fpt close_outfile; + + explicit McplApi(LibraryHandleType lib_handle) + { + if (!lib_handle) + throw std::runtime_error( + "MCPL library handle is null during API binding."); + + auto load_symbol_platform = [lib_handle](const char* name) { + void* sym = nullptr; +#ifdef _WIN32 + sym = (void*)GetProcAddress(lib_handle, name); +#else + sym = dlsym(lib_handle, name); +#endif + if (!sym) { + throw std::runtime_error( + fmt::format("Failed to load MCPL symbol '{}': {}", name, + get_last_library_error())); + } + return sym; + }; + + open_file = reinterpret_cast( + load_symbol_platform("mcpl_open_file")); + hdr_nparticles = reinterpret_cast( + load_symbol_platform("mcpl_hdr_nparticles")); + read = reinterpret_cast(load_symbol_platform("mcpl_read")); + close_file = reinterpret_cast( + load_symbol_platform("mcpl_close_file")); + create_outfile = reinterpret_cast( + load_symbol_platform("mcpl_create_outfile")); + hdr_set_srcname = reinterpret_cast( + load_symbol_platform("mcpl_hdr_set_srcname")); + add_particle = reinterpret_cast( + load_symbol_platform("mcpl_add_particle")); + close_outfile = reinterpret_cast( + load_symbol_platform("mcpl_close_outfile")); + } +}; + +static LibraryHandleType g_mcpl_lib_handle = nullptr; +static std::unique_ptr g_mcpl_api; +static bool g_mcpl_init_attempted = false; +static bool g_mcpl_successfully_loaded = false; +static std::string g_mcpl_load_error_msg; +static std::once_flag g_mcpl_init_flag; + +void append_error(std::string& existing_msg, const std::string& new_error) +{ + if (!existing_msg.empty()) { + existing_msg += "; "; + } + existing_msg += new_error; +} + +void initialize_mcpl_interface_impl() +{ + g_mcpl_init_attempted = true; + g_mcpl_load_error_msg.clear(); + + // Try mcpl-config + if (!g_mcpl_lib_handle) { + FILE* pipe = nullptr; +#ifdef _WIN32 + pipe = _popen("mcpl-config --show libpath", "r"); +#else + pipe = popen("mcpl-config --show libpath 2>/dev/null", "r"); +#endif + if (pipe) { + char buffer[512]; + if (fgets(buffer, sizeof(buffer), pipe) != nullptr) { + std::string shlibpath = buffer; + // Remove trailing whitespace + while (!shlibpath.empty() && + std::isspace(static_cast(shlibpath.back()))) { + shlibpath.pop_back(); + } + + if (!shlibpath.empty()) { +#ifdef _WIN32 + g_mcpl_lib_handle = LoadLibraryA(shlibpath.c_str()); +#else + g_mcpl_lib_handle = dlopen(shlibpath.c_str(), RTLD_LAZY); +#endif + if (!g_mcpl_lib_handle) { + append_error( + g_mcpl_load_error_msg, fmt::format("From mcpl-config ({}): {}", + shlibpath, get_last_library_error())); + } + } + } +#ifdef _WIN32 + _pclose(pipe); +#else + pclose(pipe); +#endif + } else { // pipe failed to open + append_error(g_mcpl_load_error_msg, + "mcpl-config command not found or failed to execute"); + } + } + + // Try standard library names + if (!g_mcpl_lib_handle) { +#ifdef _WIN32 + const char* standard_names[] = {"mcpl.dll", "libmcpl.dll"}; +#else + const char* standard_names[] = {"libmcpl.so", "libmcpl.dylib"}; +#endif + for (const char* name : standard_names) { +#ifdef _WIN32 + g_mcpl_lib_handle = LoadLibraryA(name); +#else + g_mcpl_lib_handle = dlopen(name, RTLD_LAZY); +#endif + if (g_mcpl_lib_handle) + break; + } + if (!g_mcpl_lib_handle) { + append_error( + g_mcpl_load_error_msg, fmt::format("Using standard names (e.g. {}): {}", + standard_names[0], get_last_library_error())); + } + } + + if (!g_mcpl_lib_handle) { + if (mpi::master) { + warning(fmt::format("MCPL library could not be loaded. MCPL-dependent " + "features will be unavailable. Load attempts: {}", + g_mcpl_load_error_msg.empty() + ? "No specific error during load attempts." + : g_mcpl_load_error_msg)); + } + g_mcpl_successfully_loaded = false; + return; + } + + try { + g_mcpl_api = std::make_unique(g_mcpl_lib_handle); + g_mcpl_successfully_loaded = true; + // Do not call dlclose/FreeLibrary at exit. Leaking the handle is safer + // and standard practice for libraries used for the application's lifetime. + } catch (const std::runtime_error& e) { + append_error(g_mcpl_load_error_msg, + fmt::format( + "MCPL library loaded, but failed to bind symbols: {}", e.what())); + if (mpi::master) { + warning(g_mcpl_load_error_msg); + } +#ifdef _WIN32 + FreeLibrary(g_mcpl_lib_handle); +#else + dlclose(g_mcpl_lib_handle); +#endif + g_mcpl_lib_handle = nullptr; + g_mcpl_successfully_loaded = false; + } +} + +void initialize_mcpl_interface_if_needed() +{ + std::call_once(g_mcpl_init_flag, initialize_mcpl_interface_impl); +} + +bool is_mcpl_interface_available() +{ + initialize_mcpl_interface_if_needed(); + return g_mcpl_successfully_loaded; +} + +inline void ensure_mcpl_ready_or_fatal() +{ + initialize_mcpl_interface_if_needed(); + if (!g_mcpl_successfully_loaded) { + fatal_error("MCPL functionality is required, but the MCPL library is not " + "available or failed to initialize. Please ensure MCPL is " + "installed and its library can be found (e.g., via PATH on " + "Windows, LD_LIBRARY_PATH on Linux, or DYLD_LIBRARY_PATH on " + "macOS). You can often install MCPL with 'pip install mcpl' or " + "'conda install mcpl'."); + } +} + +SourceSite mcpl_particle_to_site(const mcpl_particle_repr_t* particle_repr) { SourceSite site; - - switch (particle->pdgcode) { + switch (particle_repr->pdgcode) { case 2112: site.particle = ParticleType::neutron; break; @@ -49,179 +309,204 @@ SourceSite mcpl_particle_to_site(const mcpl_particle_t* particle) case -11: site.particle = ParticleType::positron; break; + default: + fatal_error(fmt::format( + "MCPL: Encountered unexpected PDG code {} when converting to SourceSite.", + particle_repr->pdgcode)); + break; } // Copy position and direction - site.r.x = particle->position[0]; - site.r.y = particle->position[1]; - site.r.z = particle->position[2]; - site.u.x = particle->direction[0]; - site.u.y = particle->direction[1]; - site.u.z = particle->direction[2]; - + site.r.x = particle_repr->position[0]; + site.r.y = particle_repr->position[1]; + site.r.z = particle_repr->position[2]; + site.u.x = particle_repr->direction[0]; + site.u.y = particle_repr->direction[1]; + site.u.z = particle_repr->direction[2]; // MCPL stores kinetic energy in [MeV], time in [ms] - site.E = particle->ekin * 1e6; - site.time = particle->time * 1e-3; - site.wgt = particle->weight; - + site.E = particle_repr->ekin * 1e6; + site.time = particle_repr->time * 1e-3; + site.wgt = particle_repr->weight; return site; } -#endif - -//============================================================================== vector mcpl_source_sites(std::string path) { + ensure_mcpl_ready_or_fatal(); vector sites; -#ifdef OPENMC_MCPL - // Open MCPL file and determine number of particles - auto mcpl_file = mcpl_open_file(path.c_str()); - size_t n_sites = mcpl_hdr_nparticles(mcpl_file); + mcpl_file_t* mcpl_file = g_mcpl_api->open_file(path.c_str()); + if (!mcpl_file) { + fatal_error(fmt::format("MCPL: Could not open file '{}'. It might be " + "missing, inaccessible, or not a valid MCPL file.", + path)); + } - for (int i = 0; i < n_sites; i++) { - // Extract particle from mcpl-file, checking if it is a neutron, photon, - // electron, or positron. Otherwise skip. - const mcpl_particle_t* particle; - int pdg = 0; - while (pdg != 2112 && pdg != 22 && pdg != 11 && pdg != -11) { - particle = mcpl_read(mcpl_file); - pdg = particle->pdgcode; + size_t n_particles_in_file = g_mcpl_api->hdr_nparticles(mcpl_file); + size_t n_skipped = 0; + if (n_particles_in_file > 0) { + sites.reserve(n_particles_in_file); + } + + for (size_t i = 0; i < n_particles_in_file; ++i) { + const mcpl_particle_repr_t* p_repr = g_mcpl_api->read(mcpl_file); + if (!p_repr) { + warning(fmt::format("MCPL: Read error or unexpected end of file '{}' " + "after reading {} of {} expected particles.", + path, sites.size(), n_particles_in_file)); + break; + } + if (p_repr->pdgcode == 2112 || p_repr->pdgcode == 22 || + p_repr->pdgcode == 11 || p_repr->pdgcode == -11) { + sites.push_back(mcpl_particle_to_site(p_repr)); + } else { + n_skipped++; } - - // Convert to source site and add to vector - sites.push_back(mcpl_particle_to_site(particle)); } - // Check that some sites were read + g_mcpl_api->close_file(mcpl_file); + + if (n_skipped > 0 && n_particles_in_file > 0) { + double percent_skipped = + 100.0 * static_cast(n_skipped) / n_particles_in_file; + warning(fmt::format( + "MCPL: Skipped {} of {} total particles ({:.1f}%) in file '{}' because " + "their type is not supported by OpenMC.", + n_skipped, n_particles_in_file, percent_skipped, path)); + } + if (sites.empty()) { - fatal_error("MCPL file contained no neutron, photon, electron, or positron " - "source particles."); + if (n_particles_in_file > 0) { + fatal_error(fmt::format( + "MCPL file '{}' contained {} particles, but none were of the supported " + "types (neutron, photon, electron, positron). OpenMC cannot proceed " + "without source particles.", + path, n_particles_in_file)); + } else { + fatal_error(fmt::format( + "MCPL file '{}' is empty or contains no particle data.", path)); + } } - - mcpl_close_file(mcpl_file); -#else - fatal_error( - "Your build of OpenMC does not support reading MCPL source files."); -#endif - return sites; } -//============================================================================== - -#ifdef OPENMC_MCPL -void write_mcpl_source_bank(mcpl_outfile_t file_id, - span source_bank, const vector& bank_index) +void write_mcpl_source_bank_internal(mcpl_outfile_t* file_id, + span local_source_bank, + const vector& bank_index_all_ranks) { - int64_t dims_size = settings::n_particles; - int64_t count_size = simulation::work_per_rank; - if (mpi::master) { - // Particles are writeen to disk from the master node only + if (!file_id) { + fatal_error("MCPL: Internal error - master rank called " + "write_mcpl_source_bank_internal with null file_id."); + } + vector receive_buffer; - // Save source bank sites since the array is overwritten below + for (int rank_idx = 0; rank_idx < mpi::n_procs; ++rank_idx) { + size_t num_sites_on_rank = static_cast( + bank_index_all_ranks[rank_idx + 1] - bank_index_all_ranks[rank_idx]); + if (num_sites_on_rank == 0) + continue; + + span sites_to_write; #ifdef OPENMC_MPI - vector temp_source {source_bank.begin(), source_bank.end()}; + if (rank_idx == mpi::rank) { + sites_to_write = openmc::span( + local_source_bank.data(), num_sites_on_rank); + } else { + if (receive_buffer.size() < num_sites_on_rank) { + receive_buffer.resize(num_sites_on_rank); + } + MPI_Recv(receive_buffer.data(), num_sites_on_rank, mpi::source_site, + rank_idx, rank_idx, mpi::intracomm, MPI_STATUS_IGNORE); + sites_to_write = openmc::span( + receive_buffer.data(), num_sites_on_rank); + } +#else + sites_to_write = openmc::span( + local_source_bank.data(), num_sites_on_rank); #endif - - // loop over the other nodes and receive data - then write those. - for (int i = 0; i < mpi::n_procs; ++i) { - // number of particles for node node i - size_t count[] {static_cast(bank_index[i + 1] - bank_index[i])}; - -#ifdef OPENMC_MPI - if (i > 0) - MPI_Recv(source_bank.data(), count[0], mpi::source_site, i, i, - mpi::intracomm, MPI_STATUS_IGNORE); -#endif - // now write the source_bank data again. - for (const auto& site : source_bank) { - // particle is now at the iterator - // write it to the mcpl-file - mcpl_particle_t p; - p.position[0] = site.r.x; - p.position[1] = site.r.y; - p.position[2] = site.r.z; - - // mcpl requires that the direction vector is unit length - // which is also the case in openmc - p.direction[0] = site.u.x; - p.direction[1] = site.u.y; - p.direction[2] = site.u.z; - - // MCPL stores kinetic energy in [MeV], time in [ms] - p.ekin = site.E * 1e-6; - p.time = site.time * 1e3; - p.weight = site.wgt; - + for (const auto& site : sites_to_write) { + mcpl_particle_repr_t p_repr {}; + p_repr.position[0] = site.r.x; + p_repr.position[1] = site.r.y; + p_repr.position[2] = site.r.z; + p_repr.direction[0] = site.u.x; + p_repr.direction[1] = site.u.y; + p_repr.direction[2] = site.u.z; + p_repr.ekin = site.E * 1e-6; + p_repr.time = site.time * 1e3; + p_repr.weight = site.wgt; switch (site.particle) { case ParticleType::neutron: - p.pdgcode = 2112; + p_repr.pdgcode = 2112; break; case ParticleType::photon: - p.pdgcode = 22; + p_repr.pdgcode = 22; break; case ParticleType::electron: - p.pdgcode = 11; + p_repr.pdgcode = 11; break; case ParticleType::positron: - p.pdgcode = -11; + p_repr.pdgcode = -11; break; + default: + continue; } - - mcpl_add_particle(file_id, &p); + g_mcpl_api->add_particle(file_id, &p_repr); } } -#ifdef OPENMC_MPI - // Restore state of source bank - std::copy(temp_source.begin(), temp_source.end(), source_bank.begin()); -#endif } else { #ifdef OPENMC_MPI - MPI_Send(source_bank.data(), count_size, mpi::source_site, 0, mpi::rank, - mpi::intracomm); + if (!local_source_bank.empty()) { + MPI_Send(local_source_bank.data(), local_source_bank.size(), + mpi::source_site, 0, mpi::rank, mpi::intracomm); + } #endif } } -#endif - -//============================================================================== void write_mcpl_source_point(const char* filename, span source_bank, const vector& bank_index) { + ensure_mcpl_ready_or_fatal(); + std::string filename_(filename); const auto extension = get_file_extension(filename_); - if (extension == "") { + if (extension.empty()) { filename_.append(".mcpl"); } else if (extension != "mcpl") { - warning("write_mcpl_source_point was passed a file extension differing " - "from .mcpl, but an mcpl file will be written."); + warning(fmt::format("Specified filename '{}' has an extension '.{}', but " + "an MCPL file (.mcpl) will be written using this name.", + filename, extension)); } -#ifdef OPENMC_MCPL - mcpl_outfile_t file_id; + mcpl_outfile_t* file_id = nullptr; - std::string line; if (mpi::master) { - file_id = mcpl_create_outfile(filename_.c_str()); + file_id = g_mcpl_api->create_outfile(filename_.c_str()); + if (!file_id) { + fatal_error(fmt::format( + "MCPL: Failed to create output file '{}'. Check permissions and path.", + filename_)); + } + std::string src_line; if (VERSION_DEV) { - line = fmt::format("OpenMC {0}.{1}.{2}-dev{3}", VERSION_MAJOR, + src_line = fmt::format("OpenMC {}.{}.{}-dev{}", VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE, VERSION_COMMIT_COUNT); } else { - line = fmt::format( - "OpenMC {0}.{1}.{2}", VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE); + src_line = fmt::format( + "OpenMC {}.{}.{}", VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE); } - mcpl_hdr_set_srcname(file_id, line.c_str()); + g_mcpl_api->hdr_set_srcname(file_id, src_line.c_str()); } - write_mcpl_source_bank(file_id, source_bank, bank_index); + write_mcpl_source_bank_internal(file_id, source_bank, bank_index); if (mpi::master) { - mcpl_close_outfile(file_id); + if (file_id) { + g_mcpl_api->close_outfile(file_id); + } } -#endif } } // namespace openmc diff --git a/src/settings.cpp b/src/settings.cpp index 9afe74361..afa42a3c5 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -844,12 +844,6 @@ void read_settings_xml(pugi::xml_node root) } if (check_for_node(node_sp, "mcpl")) { source_mcpl_write = get_node_value_bool(node_sp, "mcpl"); - - // Make sure MCPL support is enabled - if (source_mcpl_write && !MCPL_ENABLED) { - fatal_error( - "Your build of OpenMC does not support writing MCPL source files."); - } } if (check_for_node(node_sp, "overwrite_latest")) { source_latest = get_node_value_bool(node_sp, "overwrite_latest"); @@ -902,12 +896,6 @@ void read_settings_xml(pugi::xml_node root) if (check_for_node(node_ssw, "mcpl")) { surf_mcpl_write = get_node_value_bool(node_ssw, "mcpl"); - - // Make sure MCPL support is enabled - if (surf_mcpl_write && !MCPL_ENABLED) { - fatal_error("Your build of OpenMC does not support writing MCPL " - "surface source files."); - } } // Get cell information if (check_for_node(node_ssw, "cell")) { diff --git a/tests/regression_tests/source_mcpl_file/settings.xml b/tests/regression_tests/source_mcpl_file/settings.xml index 47b010bff..bb08e6fd7 100644 --- a/tests/regression_tests/source_mcpl_file/settings.xml +++ b/tests/regression_tests/source_mcpl_file/settings.xml @@ -1,12 +1,11 @@ + eigenvalue - - 10 - 5 - 1000 - + 10 + 5 + 1000 -4 -4 -4 4 4 4 diff --git a/tests/regression_tests/source_mcpl_file/test.py b/tests/regression_tests/source_mcpl_file/test.py index 84ec005ae..6e668ef19 100644 --- a/tests/regression_tests/source_mcpl_file/test.py +++ b/tests/regression_tests/source_mcpl_file/test.py @@ -1,23 +1,23 @@ #!/usr/bin/env python -import openmc.lib import pytest import glob import os - +import shutil from tests.testing_harness import * + pytestmark = pytest.mark.skipif( - not openmc.lib._mcpl_enabled(), - reason="MCPL is not enabled.") + shutil.which("mcpl-config") is None, + reason="mcpl-config command not found in PATH; MCPL is likely not available." +) settings1=""" + eigenvalue - - 10 - 5 - 1000 - + 10 + 5 + 1000 -4 -4 -4 4 4 4 @@ -28,11 +28,10 @@ settings1=""" settings2 = """ - - 10 - 5 - 1000 - + eigenvalue + 10 + 5 + 1000 source.10.{} From bca818ced6c7c0a7fd1b331cb8057b682ecc854c Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Wed, 16 Jul 2025 22:29:41 -0500 Subject: [PATCH 373/671] Add accessor methods for LocalCoord (#3494) Co-authored-by: Paul Romano --- include/openmc/particle_data.h | 54 +++++++++---- include/openmc/plot.h | 2 +- src/cell.cpp | 10 +-- src/dagmc.cpp | 4 +- src/geometry.cpp | 104 +++++++++++++------------- src/mesh.cpp | 6 +- src/output.cpp | 21 +++--- src/particle.cpp | 36 ++++----- src/particle_data.cpp | 26 +++---- src/plot.cpp | 16 ++-- src/random_ray/flat_source_domain.cpp | 6 +- src/random_ray/random_ray.cpp | 10 +-- src/source.cpp | 7 +- src/tallies/filter_cell.cpp | 2 +- src/tallies/filter_cell_instance.cpp | 4 +- src/tallies/filter_distribcell.cpp | 8 +- src/tallies/filter_universe.cpp | 2 +- src/tallies/tally_scoring.cpp | 6 +- src/universe.cpp | 4 +- src/volume_calc.cpp | 4 +- 20 files changed, 179 insertions(+), 153 deletions(-) diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index be7fa7483..8dd814a29 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -88,13 +88,37 @@ public: //! clear data from a single coordinate level void reset(); - Position r; //!< particle position - Direction u; //!< particle direction - int cell {-1}; - int universe {-1}; - int lattice {-1}; - array lattice_i {{-1, -1, -1}}; - bool rotated {false}; //!< Is the level rotated? + // accessors + Position& r() { return r_; } + const Position& r() const { return r_; } + + Direction& u() { return u_; } + const Direction& u() const { return u_; } + + int& cell() { return cell_; } + const int& cell() const { return cell_; } + + int& universe() { return universe_; } + const int& universe() const { return universe_; } + + int& lattice() { return lattice_; } + int lattice() const { return lattice_; } + + array& lattice_index() { return lattice_index_; } + const array& lattice_index() const { return lattice_index_; } + + bool& rotated() { return rotated_; } + const bool& rotated() const { return rotated_; } + +private: + // Data members + Position r_; //!< particle position + Direction u_; //!< particle direction + int cell_ {-1}; + int universe_ {-1}; + int lattice_ {-1}; + array lattice_index_ {{-1, -1, -1}}; + bool rotated_ {false}; //!< Is the level rotated? }; //============================================================================== @@ -301,20 +325,20 @@ public: const Position& u_last() const { return u_last_; } // Accessors for position in global coordinates - Position& r() { return coord_[0].r; } - const Position& r() const { return coord_[0].r; } + Position& r() { return coord_[0].r(); } + const Position& r() const { return coord_[0].r(); } // Accessors for position in local coordinates - Position& r_local() { return coord_[n_coord_ - 1].r; } - const Position& r_local() const { return coord_[n_coord_ - 1].r; } + Position& r_local() { return coord_[n_coord_ - 1].r(); } + const Position& r_local() const { return coord_[n_coord_ - 1].r(); } // Accessors for direction in global coordinates - Direction& u() { return coord_[0].u; } - const Direction& u() const { return coord_[0].u; } + Direction& u() { return coord_[0].u(); } + const Direction& u() const { return coord_[0].u(); } // Accessors for direction in local coordinates - Direction& u_local() { return coord_[n_coord_ - 1].u; } - const Direction& u_local() const { return coord_[n_coord_ - 1].u; } + Direction& u_local() { return coord_[n_coord_ - 1].u(); } + const Direction& u_local() const { return coord_[n_coord_ - 1].u(); } // Surface token for the surface that the particle is currently on int& surface() { return surface_; } diff --git a/include/openmc/plot.h b/include/openmc/plot.h index 69e801c5f..7e27679ea 100644 --- a/include/openmc/plot.h +++ b/include/openmc/plot.h @@ -223,7 +223,7 @@ T SlicePlotBase::get_map() const GeometryState p; p.r() = xyz; p.u() = dir; - p.coord(0).universe = model::root_universe; + p.coord(0).universe() = model::root_universe; int level = slice_level_; int j {}; diff --git a/src/cell.cpp b/src/cell.cpp index 4b2699229..eb5c228bc 100644 --- a/src/cell.cpp +++ b/src/cell.cpp @@ -1314,10 +1314,10 @@ vector Cell::find_parent_cells( bool cell_found = false; for (auto it = coords.begin(); it != coords.end(); it++) { const auto& coord = *it; - const auto& cell = model::cells[coord.cell]; + const auto& cell = model::cells[coord.cell()]; // if the cell at this level matches the current cell, stop adding to the // stack - if (coord.cell == model::cell_map[this->id_]) { + if (coord.cell() == model::cell_map[this->id_]) { cell_found = true; break; } @@ -1327,10 +1327,10 @@ vector Cell::find_parent_cells( int lattice_idx = C_NONE; if (cell->type_ == Fill::LATTICE) { const auto& next_coord = *(it + 1); - lattice_idx = model::lattices[next_coord.lattice]->get_flat_index( - next_coord.lattice_i); + lattice_idx = model::lattices[next_coord.lattice()]->get_flat_index( + next_coord.lattice_index()); } - stack.push(coord.universe, {coord.cell, lattice_idx}); + stack.push(coord.universe(), {coord.cell(), lattice_idx}); } // if this loop finished because the cell was found and diff --git a/src/dagmc.cpp b/src/dagmc.cpp index 134360886..1d92d9d57 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -430,7 +430,7 @@ bool DAGUniverse::find_cell(GeometryState& p) const // cells, place it in the implicit complement bool found = Universe::find_cell(p); if (!found && model::universe_map[this->id_] != model::root_universe) { - p.lowest_coord().cell = implicit_complement_idx(); + p.lowest_coord().cell() = implicit_complement_idx(); found = true; } return found; @@ -676,7 +676,7 @@ std::pair DAGCell::distance( p->history().reset(); } - const auto& univ = model::universes[p->lowest_coord().universe]; + const auto& univ = model::universes[p->lowest_coord().universe()]; DAGUniverse* dag_univ = static_cast(univ.get()); if (!dag_univ) diff --git a/src/geometry.cpp b/src/geometry.cpp index 9e975c00d..cefe7d364 100644 --- a/src/geometry.cpp +++ b/src/geometry.cpp @@ -38,17 +38,17 @@ bool check_cell_overlap(GeometryState& p, bool error) // Loop through each coordinate level for (int j = 0; j < n_coord; j++) { - Universe& univ = *model::universes[p.coord(j).universe]; + Universe& univ = *model::universes[p.coord(j).universe()]; // Loop through each cell on this level for (auto index_cell : univ.cells_) { Cell& c = *model::cells[index_cell]; - if (c.contains(p.coord(j).r, p.coord(j).u, p.surface())) { - if (index_cell != p.coord(j).cell) { + if (c.contains(p.coord(j).r(), p.coord(j).u(), p.surface())) { + if (index_cell != p.coord(j).cell()) { if (error) { fatal_error( fmt::format("Overlapping cells detected: {}, {} on universe {}", - c.id_, model::cells[p.coord(j).cell]->id_, univ.id_)); + c.id_, model::cells[p.coord(j).cell()]->id_, univ.id_)); } return true; } @@ -73,7 +73,7 @@ int cell_instance_at_level(const GeometryState& p, int level) } // determine the cell instance - Cell& c {*model::cells[p.coord(level).cell]}; + Cell& c {*model::cells[p.coord(level).cell()]}; // quick exit if this cell doesn't have distribcell instances if (c.distribcell_index_ == C_NONE) @@ -82,13 +82,13 @@ int cell_instance_at_level(const GeometryState& p, int level) // compute the cell's instance int instance = 0; for (int i = 0; i < level; i++) { - const auto& c_i {*model::cells[p.coord(i).cell]}; + const auto& c_i {*model::cells[p.coord(i).cell()]}; if (c_i.type_ == Fill::UNIVERSE) { instance += c_i.offset_[c.distribcell_index_]; } else if (c_i.type_ == Fill::LATTICE) { instance += c_i.offset_[c.distribcell_index_]; - auto& lat {*model::lattices[p.coord(i + 1).lattice]}; - const auto& i_xyz {p.coord(i + 1).lattice_i}; + auto& lat {*model::lattices[p.coord(i + 1).lattice()]}; + const auto& i_xyz {p.coord(i + 1).lattice_index()}; if (lat.are_valid_indices(i_xyz)) { instance += lat.offset(c.distribcell_index_, i_xyz); } @@ -111,7 +111,7 @@ bool find_cell_inner( i_cell = *it; // Make sure the search cell is in the same universe. - int i_universe = p.lowest_coord().universe; + int i_universe = p.lowest_coord().universe(); if (model::cells[i_cell]->universe_ != i_universe) continue; @@ -120,7 +120,7 @@ bool find_cell_inner( Direction u {p.u_local()}; auto surf = p.surface(); if (model::cells[i_cell]->contains(r, u, surf)) { - p.lowest_coord().cell = i_cell; + p.lowest_coord().cell() = i_cell; found = true; break; } @@ -146,7 +146,7 @@ bool find_cell_inner( // code below this conditional, we set i_cell back to C_NONE to indicate // that. if (i_cell == C_NONE) { - int i_universe = p.lowest_coord().universe; + int i_universe = p.lowest_coord().universe(); const auto& univ {model::universes[i_universe]}; found = univ->find_cell(p); } @@ -154,7 +154,7 @@ bool find_cell_inner( if (!found) { return found; } - i_cell = p.lowest_coord().cell; + i_cell = p.lowest_coord().cell(); // Announce the cell that the particle is entering. if (found && verbose) { @@ -186,14 +186,14 @@ bool find_cell_inner( // Set the lower coordinate level universe. auto& coord {p.coord(p.n_coord())}; - coord.universe = c.fill_; + coord.universe() = c.fill_; // Set the position and direction. - coord.r = p.r_local(); - coord.u = p.u_local(); + coord.r() = p.r_local(); + coord.u() = p.u_local(); // Apply translation. - coord.r -= c.translation_; + coord.r() -= c.translation_; // Apply rotation. if (!c.rotation_.empty()) { @@ -208,11 +208,11 @@ bool find_cell_inner( // Set the position and direction. auto& coord {p.coord(p.n_coord())}; - coord.r = p.r_local(); - coord.u = p.u_local(); + coord.r() = p.r_local(); + coord.u() = p.u_local(); // Apply translation. - coord.r -= c.translation_; + coord.r() -= c.translation_; // Apply rotation. if (!c.rotation_.empty()) { @@ -220,21 +220,21 @@ bool find_cell_inner( } // Determine lattice indices. - auto& i_xyz {coord.lattice_i}; - lat.get_indices(coord.r, coord.u, i_xyz); + auto& i_xyz {coord.lattice_index()}; + lat.get_indices(coord.r(), coord.u(), i_xyz); // Get local position in appropriate lattice cell - coord.r = lat.get_local_position(coord.r, i_xyz); + coord.r() = lat.get_local_position(coord.r(), i_xyz); // Set lattice indices. - coord.lattice = c.fill_; + coord.lattice() = c.fill_; // Set the lower coordinate level universe. if (lat.are_valid_indices(i_xyz)) { - coord.universe = lat[i_xyz]; + coord.universe() = lat[i_xyz]; } else { if (lat.outer_ != NO_OUTER_UNIVERSE) { - coord.universe = lat.outer_; + coord.universe() = lat.outer_; } else { p.mark_as_lost(fmt::format( "Particle {} left lattice {}, but it has no outer definition.", @@ -261,7 +261,7 @@ bool neighbor_list_find_cell(GeometryState& p, bool verbose) // Get the cell this particle was in previously. auto coord_lvl = p.n_coord() - 1; - auto i_cell = p.coord(coord_lvl).cell; + auto i_cell = p.coord(coord_lvl).cell(); Cell& c {*model::cells[i_cell]}; // Search for the particle in that cell's neighbor list. Return if we @@ -275,15 +275,15 @@ bool neighbor_list_find_cell(GeometryState& p, bool verbose) // neighboring cell. found = find_cell_inner(p, nullptr, verbose); if (found) - c.neighbors_.push_back(p.coord(coord_lvl).cell); + c.neighbors_.push_back(p.coord(coord_lvl).cell()); return found; } bool exhaustive_find_cell(GeometryState& p, bool verbose) { - int i_universe = p.lowest_coord().universe; + int i_universe = p.lowest_coord().universe(); if (i_universe == C_NONE) { - p.coord(0).universe = model::root_universe; + p.coord(0).universe() = model::root_universe; p.n_coord() = 1; i_universe = model::root_universe; } @@ -299,32 +299,32 @@ bool exhaustive_find_cell(GeometryState& p, bool verbose) void cross_lattice(GeometryState& p, const BoundaryInfo& boundary, bool verbose) { auto& coord {p.lowest_coord()}; - auto& lat {*model::lattices[coord.lattice]}; + auto& lat {*model::lattices[coord.lattice()]}; if (verbose) { write_message( fmt::format(" Crossing lattice {}. Current position ({},{},{}). r={}", - lat.id_, coord.lattice_i[0], coord.lattice_i[1], coord.lattice_i[2], - p.r()), + lat.id_, coord.lattice_index()[0], coord.lattice_index()[1], + coord.lattice_index()[2], p.r()), 1); } // Set the lattice indices. - coord.lattice_i[0] += boundary.lattice_translation[0]; - coord.lattice_i[1] += boundary.lattice_translation[1]; - coord.lattice_i[2] += boundary.lattice_translation[2]; + coord.lattice_index()[0] += boundary.lattice_translation[0]; + coord.lattice_index()[1] += boundary.lattice_translation[1]; + coord.lattice_index()[2] += boundary.lattice_translation[2]; // Set the new coordinate position. const auto& upper_coord {p.coord(p.n_coord() - 2)}; - const auto& cell {model::cells[upper_coord.cell]}; - Position r = upper_coord.r; + const auto& cell {model::cells[upper_coord.cell()]}; + Position r = upper_coord.r(); r -= cell->translation_; if (!cell->rotation_.empty()) { r = r.rotate(cell->rotation_); } - p.r_local() = lat.get_local_position(r, coord.lattice_i); + p.r_local() = lat.get_local_position(r, coord.lattice_index()); - if (!lat.are_valid_indices(coord.lattice_i)) { + if (!lat.are_valid_indices(coord.lattice_index())) { // The particle is outside the lattice. Search for it from the base coords. p.n_coord() = 1; bool found = exhaustive_find_cell(p); @@ -337,7 +337,7 @@ void cross_lattice(GeometryState& p, const BoundaryInfo& boundary, bool verbose) } else { // Find cell in next lattice element. - p.lowest_coord().universe = lat[coord.lattice_i]; + p.lowest_coord().universe() = lat[coord.lattice_index()]; bool found = exhaustive_find_cell(p); if (!found) { @@ -367,9 +367,9 @@ BoundaryInfo distance_to_boundary(GeometryState& p) // Loop over each coordinate level. for (int i = 0; i < p.n_coord(); i++) { const auto& coord {p.coord(i)}; - const Position& r {coord.r}; - const Direction& u {coord.u}; - Cell& c {*model::cells[coord.cell]}; + const Position& r {coord.r()}; + const Direction& u {coord.u()}; + Cell& c {*model::cells[coord.cell()]}; // Find the oncoming surface in this cell and the distance to it. auto surface_distance = c.distance(r, u, p.surface(), &p); @@ -377,24 +377,24 @@ BoundaryInfo distance_to_boundary(GeometryState& p) level_surf_cross = surface_distance.second; // Find the distance to the next lattice tile crossing. - if (coord.lattice != C_NONE) { - auto& lat {*model::lattices[coord.lattice]}; + if (coord.lattice() != C_NONE) { + auto& lat {*model::lattices[coord.lattice()]}; // TODO: refactor so both lattice use the same position argument (which // also means the lat.type attribute can be removed) std::pair> lattice_distance; switch (lat.type_) { case LatticeType::rect: - lattice_distance = lat.distance(r, u, coord.lattice_i); + lattice_distance = lat.distance(r, u, coord.lattice_index()); break; case LatticeType::hex: - auto& cell_above {model::cells[p.coord(i - 1).cell]}; - Position r_hex {p.coord(i - 1).r}; + auto& cell_above {model::cells[p.coord(i - 1).cell()]}; + Position r_hex {p.coord(i - 1).r()}; r_hex -= cell_above->translation_; - if (coord.rotated) { + if (coord.rotated()) { r_hex = r_hex.rotate(cell_above->rotation_); } - r_hex.z = coord.r.z; - lattice_distance = lat.distance(r_hex, u, coord.lattice_i); + r_hex.z = coord.r().z; + lattice_distance = lat.distance(r_hex, u, coord.lattice_index()); break; } d_lat = lattice_distance.first; @@ -468,7 +468,7 @@ extern "C" int openmc_find_cell( return OPENMC_E_GEOMETRY; } - *index = geom_state.lowest_coord().cell; + *index = geom_state.lowest_coord().cell(); *instance = geom_state.cell_instance(); return 0; } diff --git a/src/mesh.cpp b/src/mesh.cpp index becbbd3fd..1b2c71eb2 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -368,11 +368,11 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, // Set birth cell attribute if (p.cell_born() == C_NONE) - p.cell_born() = p.lowest_coord().cell; + p.cell_born() = p.lowest_coord().cell(); // Initialize last cells from current cell for (int j = 0; j < p.n_coord(); ++j) { - p.cell_last(j) = p.coord(j).cell; + p.cell_last(j) = p.coord(j).cell(); } p.n_coord_last() = p.n_coord(); @@ -411,7 +411,7 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, // cross next geometric surface for (int j = 0; j < p.n_coord(); ++j) { - p.cell_last(j) = p.coord(j).cell; + p.cell_last(j) = p.coord(j).cell(); } p.n_coord_last() = p.n_coord(); diff --git a/src/output.cpp b/src/output.cpp index baaf682a1..0316e313e 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -177,25 +177,26 @@ void print_particle(Particle& p) for (auto i = 0; i < p.n_coord(); i++) { fmt::print(" Level {}\n", i); - if (p.coord(i).cell != C_NONE) { - const Cell& c {*model::cells[p.coord(i).cell]}; + if (p.coord(i).cell() != C_NONE) { + const Cell& c {*model::cells[p.coord(i).cell()]}; fmt::print(" Cell = {}\n", c.id_); } - if (p.coord(i).universe != C_NONE) { - const Universe& u {*model::universes[p.coord(i).universe]}; + if (p.coord(i).universe() != C_NONE) { + const Universe& u {*model::universes[p.coord(i).universe()]}; fmt::print(" Universe = {}\n", u.id_); } - if (p.coord(i).lattice != C_NONE) { - const Lattice& lat {*model::lattices[p.coord(i).lattice]}; + if (p.coord(i).lattice() != C_NONE) { + const Lattice& lat {*model::lattices[p.coord(i).lattice()]}; fmt::print(" Lattice = {}\n", lat.id_); - fmt::print(" Lattice position = ({},{},{})\n", p.coord(i).lattice_i[0], - p.coord(i).lattice_i[1], p.coord(i).lattice_i[2]); + fmt::print(" Lattice position = ({},{},{})\n", + p.coord(i).lattice_index()[0], p.coord(i).lattice_index()[1], + p.coord(i).lattice_index()[2]); } - fmt::print(" r = {}\n", p.coord(i).r); - fmt::print(" u = {}\n", p.coord(i).u); + fmt::print(" r = {}\n", p.coord(i).r()); + fmt::print(" u = {}\n", p.coord(i).u()); } // Display miscellaneous info. diff --git a/src/particle.cpp b/src/particle.cpp index 6cfdf2175..4758cded5 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -176,7 +176,7 @@ void Particle::event_calculate_xs() // If the cell hasn't been determined based on the particle's location, // initiate a search for the current cell. This generally happens at the // beginning of the history and again for any secondary particles - if (lowest_coord().cell == C_NONE) { + if (lowest_coord().cell() == C_NONE) { if (!exhaustive_find_cell(*this)) { mark_as_lost( "Could not find the cell containing particle " + std::to_string(id())); @@ -185,11 +185,11 @@ void Particle::event_calculate_xs() // Set birth cell attribute if (cell_born() == C_NONE) - cell_born() = lowest_coord().cell; + cell_born() = lowest_coord().cell(); // Initialize last cells from current cell for (int j = 0; j < n_coord(); ++j) { - cell_last(j) = coord(j).cell; + cell_last(j) = coord(j).cell(); } n_coord_last() = n_coord(); } @@ -248,7 +248,7 @@ void Particle::event_advance() // Short-term solution until the surface source is revised and we can use // this->move_distance(distance) for (int j = 0; j < n_coord(); ++j) { - coord(j).r += distance * coord(j).u; + coord(j).r() += distance * coord(j).u(); } double dt = distance / this->speed(); this->time() += dt; @@ -293,7 +293,7 @@ void Particle::event_cross_surface() { // Saving previous cell data for (int j = 0; j < n_coord(); ++j) { - cell_last(j) = coord(j).cell; + cell_last(j) = coord(j).cell(); } n_coord_last() = n_coord(); @@ -393,14 +393,14 @@ void Particle::event_collide() // Set all directions to base level -- right now, after a collision, only // the base level directions are changed for (int j = 0; j < n_coord() - 1; ++j) { - if (coord(j + 1).rotated) { + if (coord(j + 1).rotated()) { // If next level is rotated, apply rotation matrix - const auto& m {model::cells[coord(j).cell]->rotation_}; - const auto& u {coord(j).u}; - coord(j + 1).u = u.rotate(m); + const auto& m {model::cells[coord(j).cell()]->rotation_}; + const auto& u {coord(j).u()}; + coord(j + 1).u() = u.rotate(m); } else { // Otherwise, copy this level's direction - coord(j + 1).u = coord(j).u; + coord(j + 1).u() = coord(j).u(); } } @@ -445,7 +445,7 @@ void Particle::event_revive_from_secondary() // Since the birth cell of the particle has not been set we // have to determine it before the energy of the secondary particle can be // removed from the pulse-height of this cell. - if (lowest_coord().cell == C_NONE) { + if (lowest_coord().cell() == C_NONE) { bool verbose = settings::verbosity >= 10 || trace(); if (!exhaustive_find_cell(*this, verbose)) { mark_as_lost("Could not find the cell containing particle " + @@ -454,11 +454,11 @@ void Particle::event_revive_from_secondary() } // Set birth cell attribute if (cell_born() == C_NONE) - cell_born() = lowest_coord().cell; + cell_born() = lowest_coord().cell(); // Initialize last cells from current cell for (int j = 0; j < n_coord(); ++j) { - cell_last(j) = coord(j).cell; + cell_last(j) = coord(j).cell(); } n_coord_last() = n_coord(); } @@ -516,7 +516,7 @@ void Particle::pht_collision_energy() // determine index of cell in pulse_height_cells auto it = std::find(model::pulse_height_cells.begin(), - model::pulse_height_cells.end(), lowest_coord().cell); + model::pulse_height_cells.end(), lowest_coord().cell()); if (it != model::pulse_height_cells.end()) { int index = std::distance(model::pulse_height_cells.begin(), it); @@ -571,13 +571,13 @@ void Particle::cross_surface(const Surface& surf) // in DAGMC, we know what the next cell should be if (surf.geom_type() == GeometryType::DAG) { int32_t i_cell = next_cell(surface_index(), cell_last(n_coord() - 1), - lowest_coord().universe) - + lowest_coord().universe()) - 1; // save material and temp material_last() = material(); sqrtkT_last() = sqrtkT(); // set new cell value - lowest_coord().cell = i_cell; + lowest_coord().cell() = i_cell; auto& cell = model::cells[i_cell]; cell_instance() = 0; @@ -681,7 +681,7 @@ void Particle::cross_reflective_bc(const Surface& surf, Direction new_u) u() = new_u; // Reassign particle's cell and surface - coord(0).cell = cell_last(0); + coord(0).cell() = cell_last(0); surface() = -surface(); // If a reflective surface is coincident with a lattice or universe @@ -942,7 +942,7 @@ void add_surf_source_to_bank(Particle& p, const Surface& surf) // Check if the cell of interest has been entered bool entered = false; for (int i = 0; i < p.n_coord(); ++i) { - if (p.coord(i).cell == cell_idx) { + if (p.coord(i).cell() == cell_idx) { entered = true; } } diff --git a/src/particle_data.cpp b/src/particle_data.cpp index fef8359a3..aff2b68ba 100644 --- a/src/particle_data.cpp +++ b/src/particle_data.cpp @@ -32,20 +32,20 @@ void GeometryState::mark_as_lost(const std::stringstream& message) void LocalCoord::rotate(const vector& rotation) { - r = r.rotate(rotation); - u = u.rotate(rotation); - rotated = true; + r_ = r_.rotate(rotation); + u_ = u_.rotate(rotation); + rotated_ = true; } void LocalCoord::reset() { - cell = C_NONE; - universe = C_NONE; - lattice = C_NONE; - lattice_i[0] = 0; - lattice_i[1] = 0; - lattice_i[2] = 0; - rotated = false; + cell_ = C_NONE; + universe_ = C_NONE; + lattice_ = C_NONE; + lattice_index_[0] = 0; + lattice_index_[1] = 0; + lattice_index_[2] = 0; + rotated_ = false; } GeometryState::GeometryState() @@ -64,7 +64,7 @@ void GeometryState::advance_to_boundary_from_void() for (auto c_i : root_universe->cells_) { auto dist = - model::cells.at(c_i)->distance(root_coord.r, root_coord.u, 0, this); + model::cells.at(c_i)->distance(root_coord.r(), root_coord.u(), 0, this); if (dist.first < boundary().distance) { boundary().distance = dist.first; boundary().surface = dist.second; @@ -86,7 +86,7 @@ void GeometryState::advance_to_boundary_from_void() void GeometryState::move_distance(double length) { for (int j = 0; j < n_coord(); ++j) { - coord(j).r += length * coord(j).u; + coord(j).r() += length * coord(j).u(); } } @@ -123,7 +123,7 @@ TrackState ParticleData::get_track_state() const state.E = this->E(); state.time = this->time(); state.wgt = this->wgt(); - state.cell_id = model::cells[this->lowest_coord().cell]->id_; + state.cell_id = model::cells[this->lowest_coord().cell()]->id_; state.cell_instance = this->cell_instance(); if (this->material() != MATERIAL_VOID) { state.material_id = model::materials[material()]->id_; diff --git a/src/plot.cpp b/src/plot.cpp index dbc25b21e..db10c7600 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -54,14 +54,14 @@ void IdData::set_value(size_t y, size_t x, const GeometryState& p, int level) data_(y, x, 0) = NOT_FOUND; data_(y, x, 1) = NOT_FOUND; } else { - data_(y, x, 0) = model::cells.at(p.coord(level).cell)->id_; + data_(y, x, 0) = model::cells.at(p.coord(level).cell())->id_; data_(y, x, 1) = level == p.n_coord() - 1 ? p.cell_instance() : cell_instance_at_level(p, level); } // set material data - Cell* c = model::cells.at(p.lowest_coord().cell).get(); + Cell* c = model::cells.at(p.lowest_coord().cell()).get(); if (p.material() == MATERIAL_VOID) { data_(y, x, 2) = MATERIAL_VOID; return; @@ -83,7 +83,7 @@ PropertyData::PropertyData(size_t h_res, size_t v_res) void PropertyData::set_value( size_t y, size_t x, const GeometryState& p, int level) { - Cell* c = model::cells.at(p.lowest_coord().cell).get(); + Cell* c = model::cells.at(p.lowest_coord().cell()).get(); data_(y, x, 0) = (p.sqrtkT() * p.sqrtkT()) / K_BOLTZMANN; if (c->type_ != Fill::UNIVERSE && p.material() != MATERIAL_VOID) { Material* m = model::materials.at(p.material()).get(); @@ -1692,7 +1692,7 @@ void Ray::trace() // Advance particle, prepare for next intersection for (int lev = 0; lev < n_coord(); ++lev) { - coord(lev).r += boundary().distance * coord(lev).u; + coord(lev).r() += boundary().distance * coord(lev).u(); } surface() = boundary().surface; n_coord_last() = n_coord(); @@ -1743,7 +1743,7 @@ void ProjectionRay::on_intersection() line_segments_.emplace_back( plot_.color_by_ == PlottableInterface::PlotColorBy::mats ? material() - : lowest_coord().cell, + : lowest_coord().cell(), traversal_distance_, boundary().surface_index()); } @@ -1752,7 +1752,7 @@ void PhongRay::on_intersection() // Check if we hit an opaque material or cell int hit_id = plot_.color_by_ == PlottableInterface::PlotColorBy::mats ? material() - : lowest_coord().cell; + : lowest_coord().cell(); // If we are reflected and have advanced beyond the camera, // the ray is done. This is checked here because we should @@ -1798,8 +1798,8 @@ void PhongRay::on_intersection() // Need to apply translations to find the normal vector in // the base level universe's coordinate system. for (int lev = n_coord() - 2; lev >= 0; --lev) { - if (coord(lev + 1).rotated) { - const Cell& c {*model::cells[coord(lev).cell]}; + if (coord(lev + 1).rotated()) { + const Cell& c {*model::cells[coord(lev).cell()]}; normal = normal.inverse_rotate(c.rotation_); } } diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 4a5b43036..b04b72981 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -794,7 +794,7 @@ void FlatSourceDomain::output_to_vtk() const continue; } - int i_cell = p.lowest_coord().cell; + int i_cell = p.lowest_coord().cell(); int64_t sr = source_region_offsets_[i_cell] + p.cell_instance(); if (RandomRay::mesh_subdivision_enabled_) { int mesh_idx = base_source_regions_.mesh(sr); @@ -1070,7 +1070,7 @@ void FlatSourceDomain::convert_external_sources() "point source at {}", sp->r())); } - int i_cell = gs.lowest_coord().cell; + int i_cell = gs.lowest_coord().cell(); int64_t sr = source_region_offsets_[i_cell] + gs.cell_instance(); if (RandomRay::mesh_subdivision_enabled_) { @@ -1475,7 +1475,7 @@ SourceRegionHandle FlatSourceDomain::get_subdivided_source_region_handle( gs.r() = r + TINY_BIT * u; gs.u() = {1.0, 0.0, 0.0}; exhaustive_find_cell(gs); - int gs_i_cell = gs.lowest_coord().cell; + int gs_i_cell = gs.lowest_coord().cell(); int64_t sr_found = source_region_offsets_[gs_i_cell] + gs.cell_instance(); if (sr_found != sr) { discovered_source_regions_.unlock(sr_key); diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp index 36eab0628..2eca5b0ab 100644 --- a/src/random_ray/random_ray.cpp +++ b/src/random_ray/random_ray.cpp @@ -330,14 +330,14 @@ void RandomRay::event_advance_ray() // Advance particle for (int j = 0; j < n_coord(); ++j) { - coord(j).r += distance * coord(j).u; + coord(j).r() += distance * coord(j).u(); } } void RandomRay::attenuate_flux(double distance, bool is_active, double offset) { // Determine source region index etc. - int i_cell = lowest_coord().cell; + int i_cell = lowest_coord().cell(); // The base source region is the spatial region index int64_t sr = domain_->source_region_offsets_[i_cell] + cell_instance(); @@ -799,7 +799,7 @@ void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain) this->from_source(&site); // Locate ray - if (lowest_coord().cell == C_NONE) { + if (lowest_coord().cell() == C_NONE) { if (!exhaustive_find_cell(*this)) { this->mark_as_lost( "Could not find the cell containing particle " + std::to_string(id())); @@ -807,12 +807,12 @@ void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain) // Set birth cell attribute if (cell_born() == C_NONE) - cell_born() = lowest_coord().cell; + cell_born() = lowest_coord().cell(); } // Initialize ray's starting angular flux to starting location's isotropic // source - int i_cell = lowest_coord().cell; + int i_cell = lowest_coord().cell(); int64_t sr = domain_->source_region_offsets_[i_cell] + cell_instance(); SourceRegionHandle srh; diff --git a/src/source.cpp b/src/source.cpp index 65873f5a7..ae3daa404 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -246,9 +246,10 @@ bool Source::satisfies_spatial_constraints(Position r) const } } else { for (int i = 0; i < geom_state.n_coord(); i++) { - auto id = (domain_type_ == DomainType::CELL) - ? model::cells[geom_state.coord(i).cell]->id_ - : model::universes[geom_state.coord(i).universe]->id_; + auto id = + (domain_type_ == DomainType::CELL) + ? model::cells[geom_state.coord(i).cell()].get()->id_ + : model::universes[geom_state.coord(i).universe()].get()->id_; if ((accepted = contains(domain_ids_, id))) break; } diff --git a/src/tallies/filter_cell.cpp b/src/tallies/filter_cell.cpp index b545801ea..7a6394956 100644 --- a/src/tallies/filter_cell.cpp +++ b/src/tallies/filter_cell.cpp @@ -49,7 +49,7 @@ void CellFilter::get_all_bins( const Particle& p, TallyEstimator estimator, FilterMatch& match) const { for (int i = 0; i < p.n_coord(); i++) { - auto search = map_.find(p.coord(i).cell); + auto search = map_.find(p.coord(i).cell()); if (search != map_.end()) { match.bins_.push_back(search->second); match.weights_.push_back(1.0); diff --git a/src/tallies/filter_cell_instance.cpp b/src/tallies/filter_cell_instance.cpp index 0634175dd..316a758d1 100644 --- a/src/tallies/filter_cell_instance.cpp +++ b/src/tallies/filter_cell_instance.cpp @@ -73,7 +73,7 @@ void CellInstanceFilter::set_cell_instances(span instances) void CellInstanceFilter::get_all_bins( const Particle& p, TallyEstimator estimator, FilterMatch& match) const { - int64_t index_cell = p.lowest_coord().cell; + int64_t index_cell = p.lowest_coord().cell(); int64_t instance = p.cell_instance(); if (cells_.count(index_cell) > 0) { @@ -89,7 +89,7 @@ void CellInstanceFilter::get_all_bins( return; for (int i = 0; i < p.n_coord() - 1; i++) { - int64_t index_cell = p.coord(i).cell; + int64_t index_cell = p.coord(i).cell(); // if this cell isn't used on the filter, move on if (cells_.count(index_cell) == 0) continue; diff --git a/src/tallies/filter_distribcell.cpp b/src/tallies/filter_distribcell.cpp index 821e843da..02e88dde2 100644 --- a/src/tallies/filter_distribcell.cpp +++ b/src/tallies/filter_distribcell.cpp @@ -43,18 +43,18 @@ void DistribcellFilter::get_all_bins( int offset = 0; auto distribcell_index = model::cells[cell_]->distribcell_index_; for (int i = 0; i < p.n_coord(); i++) { - auto& c {*model::cells[p.coord(i).cell]}; + auto& c {*model::cells[p.coord(i).cell()]}; if (c.type_ == Fill::UNIVERSE) { offset += c.offset_[distribcell_index]; } else if (c.type_ == Fill::LATTICE) { - auto& lat {*model::lattices[p.coord(i + 1).lattice]}; - const auto& i_xyz {p.coord(i + 1).lattice_i}; + auto& lat {*model::lattices[p.coord(i + 1).lattice()]}; + const auto& i_xyz {p.coord(i + 1).lattice_index()}; if (lat.are_valid_indices(i_xyz)) { offset += lat.offset(distribcell_index, i_xyz) + c.offset_[distribcell_index]; } } - if (cell_ == p.coord(i).cell) { + if (cell_ == p.coord(i).cell()) { match.bins_.push_back(offset); match.weights_.push_back(1.0); return; diff --git a/src/tallies/filter_universe.cpp b/src/tallies/filter_universe.cpp index 48114cfad..f4b22decd 100644 --- a/src/tallies/filter_universe.cpp +++ b/src/tallies/filter_universe.cpp @@ -48,7 +48,7 @@ void UniverseFilter::get_all_bins( const Particle& p, TallyEstimator estimator, FilterMatch& match) const { for (int i = 0; i < p.n_coord(); i++) { - auto search = map_.find(p.coord(i).universe); + auto search = map_.find(p.coord(i).universe()); if (search != map_.end()) { match.bins_.push_back(search->second); match.weights_.push_back(1.0); diff --git a/src/tallies/tally_scoring.cpp b/src/tallies/tally_scoring.cpp index 71b103cfb..e73fb90f3 100644 --- a/src/tallies/tally_scoring.cpp +++ b/src/tallies/tally_scoring.cpp @@ -2615,7 +2615,7 @@ void score_pulse_height_tally(Particle& p, const vector& tallies) // Save original cell/energy information int orig_n_coord = p.n_coord(); - int orig_cell = p.coord(0).cell; + int orig_cell = p.coord(0).cell(); double orig_E_last = p.E_last(); for (auto i_tally : tallies) { @@ -2634,7 +2634,7 @@ void score_pulse_height_tally(Particle& p, const vector& tallies) // Temporarily change cell of particle p.n_coord() = 1; - p.coord(0).cell = cell_id; + p.coord(0).cell() = cell_id; // Determine index of cell in model::pulse_height_cells auto it = std::find(model::pulse_height_cells.begin(), @@ -2674,7 +2674,7 @@ void score_pulse_height_tally(Particle& p, const vector& tallies) } // Restore cell/energy p.n_coord() = orig_n_coord; - p.coord(0).cell = orig_cell; + p.coord(0).cell() = orig_cell; p.E_last() = orig_E_last; } } diff --git a/src/universe.cpp b/src/universe.cpp index b4ef6b4b2..78ddd54d4 100644 --- a/src/universe.cpp +++ b/src/universe.cpp @@ -45,14 +45,14 @@ bool Universe::find_cell(GeometryState& p) const Position r {p.r_local()}; Position u {p.u_local()}; auto surf = p.surface(); - int32_t i_univ = p.lowest_coord().universe; + int32_t i_univ = p.lowest_coord().universe(); for (auto i_cell : cells) { if (model::cells[i_cell]->universe_ != i_univ) continue; // Check if this cell contains the particle if (model::cells[i_cell]->contains(r, u, surf)) { - p.lowest_coord().cell = i_cell; + p.lowest_coord().cell() = i_cell; return true; } } diff --git a/src/volume_calc.cpp b/src/volume_calc.cpp index 8b5c27f14..1deffb804 100644 --- a/src/volume_calc.cpp +++ b/src/volume_calc.cpp @@ -179,7 +179,7 @@ vector VolumeCalculation::execute() const } else if (domain_type_ == TallyDomain::CELL) { for (int level = 0; level < p.n_coord(); ++level) { for (int i_domain = 0; i_domain < n; i_domain++) { - if (model::cells[p.coord(level).cell]->id_ == + if (model::cells[p.coord(level).cell()]->id_ == domain_ids_[i_domain]) { this->check_hit( p.material(), indices[i_domain], hits[i_domain]); @@ -190,7 +190,7 @@ vector VolumeCalculation::execute() const } else if (domain_type_ == TallyDomain::UNIVERSE) { for (int level = 0; level < p.n_coord(); ++level) { for (int i_domain = 0; i_domain < n; ++i_domain) { - if (model::universes[p.coord(level).universe]->id_ == + if (model::universes[p.coord(level).universe()]->id_ == domain_ids_[i_domain]) { check_hit(p.material(), indices[i_domain], hits[i_domain]); break; From 4483583dddf291e6c324399511fbbccfd582e908 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Fri, 18 Jul 2025 08:46:13 +0200 Subject: [PATCH 374/671] automatically finding appropriate dimension when making regular mesh from domain (#3468) --- openmc/mesh.py | 19 +++++++++++++++--- tests/unit_tests/test_mesh_from_domain.py | 24 +++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/openmc/mesh.py b/openmc/mesh.py index 56fe2bd1c..339702884 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -1094,7 +1094,7 @@ class RegularMesh(StructuredMesh): def from_domain( cls, domain: HasBoundingBox, - dimension: Sequence[int] = (10, 10, 10), + dimension: Sequence[int] | int = 1000, mesh_id: int | None = None, name: str = '' ): @@ -1106,8 +1106,11 @@ class RegularMesh(StructuredMesh): 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 - dimension : Iterable of int - The number of mesh cells in each direction (x, y, z). + 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 + will will be divided into that many mesh cells with roughly equal + lengths in each direction (cubes). mesh_id : int Unique identifier for the mesh name : str @@ -1125,6 +1128,16 @@ class RegularMesh(StructuredMesh): mesh = cls(mesh_id=mesh_id, name=name) mesh.lower_left = domain.bounding_box[0] mesh.upper_right = domain.bounding_box[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_size = ideal_cube_volume ** (1 / 3) + dimension = [ + max(1, int(round(side / ideal_cube_size))) + for side in domain.bounding_box.width + ] mesh.dimension = dimension return mesh diff --git a/tests/unit_tests/test_mesh_from_domain.py b/tests/unit_tests/test_mesh_from_domain.py index 0e3858913..5b1173126 100644 --- a/tests/unit_tests/test_mesh_from_domain.py +++ b/tests/unit_tests/test_mesh_from_domain.py @@ -147,3 +147,27 @@ def test_reg_mesh_from_geometry(): def test_error_from_unsupported_object(): with pytest.raises(TypeError): openmc.RegularMesh.from_domain("vacuum energy") + + +def test_regularmesh_from_domain_error_from_small_dimensions(): + surface = openmc.Sphere(r=20) + cell = openmc.Cell(region=-surface) + with pytest.raises( + ValueError, match='Unable to set "dimension" to "-2" since it is less than "1"' + ): + openmc.RegularMesh.from_domain(domain=cell, dimension=-2) + + +def test_dimensions_from_domain_dimensions_from_int(): + region = openmc.model.RectangularParallelepiped( + xmin=-100, + xmax=150, + ymin=-50, + ymax=200, + zmin=300, + zmax=400, + boundary_type="vacuum", + ) + cell = openmc.Cell(region=-region) + mesh = openmc.RegularMesh.from_domain(domain=cell, dimension=1000) + assert mesh.dimension == (14, 14, 5) From 637e04a9ba9ceae8cb09eb66203e1581d465fa56 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Fri, 18 Jul 2025 02:51:30 -0500 Subject: [PATCH 375/671] Boundary info accessors (#3496) Co-authored-by: Paul Romano --- include/openmc/particle_data.h | 44 ++++++++++++++++++++++++---------- src/event.cpp | 2 +- src/geometry.cpp | 28 +++++++++++----------- src/mesh.cpp | 12 +++++----- src/particle.cpp | 12 +++++----- src/particle_data.cpp | 14 +++++------ src/plot.cpp | 28 +++++++++++----------- src/random_ray/random_ray.cpp | 2 +- src/simulation.cpp | 2 +- 9 files changed, 81 insertions(+), 63 deletions(-) diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index 8dd814a29..1c8f1215a 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -210,23 +210,41 @@ struct CacheDataMG { // Information about nearest boundary crossing //============================================================================== -struct BoundaryInfo { - double distance {INFINITY}; //!< distance to nearest boundary - int surface { - SURFACE_NONE}; //!< surface token, non-zero if boundary is surface - int coord_level; //!< coordinate level after crossing boundary - array - lattice_translation {}; //!< which way lattice indices will change - +class BoundaryInfo { +public: void reset() { - distance = INFINITY; - surface = SURFACE_NONE; - coord_level = 0; - lattice_translation = {0, 0, 0}; + distance_ = INFINITY; + surface_ = SURFACE_NONE; + coord_level_ = 0; + lattice_translation_ = {0, 0, 0}; } + double& distance() { return distance_; } + const double& distance() const { return distance_; } + + int& surface() { return surface_; } + const int& surface() const { return surface_; } + + int coord_level() const { return coord_level_; } + int& coord_level() { return coord_level_; } + + array& lattice_translation() { return lattice_translation_; } + const array& lattice_translation() const + { + return lattice_translation_; + } + // TODO: off-by-one - int surface_index() const { return std::abs(surface) - 1; } + int surface_index() const { return std::abs(surface()) - 1; } + +private: + // Data members + double distance_ {INFINITY}; //!< distance to nearest boundary + int surface_ { + SURFACE_NONE}; //!< surface token, non-zero if boundary is surface + int coord_level_ {0}; //!< coordinate level after crossing boundary + array lattice_translation_ { + 0, 0, 0}; //!< which way lattice indices will change }; /* diff --git a/src/event.cpp b/src/event.cpp index ae914c8be..aa3987504 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -114,7 +114,7 @@ void process_advance_particle_events() p.event_advance(); if (!p.alive()) continue; - if (p.collision_distance() > p.boundary().distance) { + if (p.collision_distance() > p.boundary().distance()) { simulation::surface_crossing_queue.thread_safe_append({p, buffer_idx}); } else { simulation::collision_queue.thread_safe_append({p, buffer_idx}); diff --git a/src/geometry.cpp b/src/geometry.cpp index cefe7d364..df4850891 100644 --- a/src/geometry.cpp +++ b/src/geometry.cpp @@ -310,9 +310,9 @@ void cross_lattice(GeometryState& p, const BoundaryInfo& boundary, bool verbose) } // Set the lattice indices. - coord.lattice_index()[0] += boundary.lattice_translation[0]; - coord.lattice_index()[1] += boundary.lattice_translation[1]; - coord.lattice_index()[2] += boundary.lattice_translation[2]; + coord.lattice_index()[0] += boundary.lattice_translation()[0]; + coord.lattice_index()[1] += boundary.lattice_translation()[1]; + coord.lattice_index()[2] += boundary.lattice_translation()[2]; // Set the new coordinate position. const auto& upper_coord {p.coord(p.n_coord() - 2)}; @@ -410,7 +410,7 @@ BoundaryInfo distance_to_boundary(GeometryState& p) // If the boundary on this coordinate level is coincident with a boundary on // a higher level then we need to make sure that the higher level boundary // is selected. This logic must consider floating point precision. - double& d = info.distance; + double& d = info.distance(); if (d_surf < d_lat - FP_COINCIDENT) { if (d == INFINITY || (d - d_surf) / d >= FP_REL_PRECISION) { // Update closest distance @@ -421,29 +421,29 @@ BoundaryInfo distance_to_boundary(GeometryState& p) // have to explicitly check which half-space the particle would be // traveling into if the surface is crossed if (c.is_simple() || d == INFTY) { - info.surface = level_surf_cross; + info.surface() = level_surf_cross; } else { Position r_hit = r + d_surf * u; Surface& surf {*model::surfaces[std::abs(level_surf_cross) - 1]}; Direction norm = surf.normal(r_hit); if (u.dot(norm) > 0) { - info.surface = std::abs(level_surf_cross); + info.surface() = std::abs(level_surf_cross); } else { - info.surface = -std::abs(level_surf_cross); + info.surface() = -std::abs(level_surf_cross); } } - info.lattice_translation[0] = 0; - info.lattice_translation[1] = 0; - info.lattice_translation[2] = 0; - info.coord_level = i + 1; + info.lattice_translation()[0] = 0; + info.lattice_translation()[1] = 0; + info.lattice_translation()[2] = 0; + info.coord_level() = i + 1; } } else { if (d == INFINITY || (d - d_lat) / d >= FP_REL_PRECISION) { d = d_lat; - info.surface = SURFACE_NONE; - info.lattice_translation = level_lat_trans; - info.coord_level = i + 1; + info.surface() = SURFACE_NONE; + info.lattice_translation() = level_lat_trans; + info.coord_level() = i + 1; } } } diff --git a/src/mesh.cpp b/src/mesh.cpp index 1b2c71eb2..9147efb95 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -385,7 +385,7 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, BoundaryInfo boundary = distance_to_boundary(p); // Advance particle forward - double distance = std::min(boundary.distance, max_distance); + double distance = std::min(boundary.distance(), max_distance); p.move_distance(distance); // Determine what mesh elements were crossed by particle @@ -416,12 +416,12 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, p.n_coord_last() = p.n_coord(); // Set surface that particle is on and adjust coordinate levels - p.surface() = boundary.surface; - p.n_coord() = boundary.coord_level; + p.surface() = boundary.surface(); + p.n_coord() = boundary.coord_level(); - if (boundary.lattice_translation[0] != 0 || - boundary.lattice_translation[1] != 0 || - boundary.lattice_translation[2] != 0) { + if (boundary.lattice_translation()[0] != 0 || + boundary.lattice_translation()[1] != 0 || + boundary.lattice_translation()[2] != 0) { // Particle crosses lattice boundary cross_lattice(p, boundary); } else { diff --git a/src/particle.cpp b/src/particle.cpp index 4758cded5..f9044b2d7 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -242,7 +242,7 @@ void Particle::event_advance() } // Select smaller of the two distances - double distance = std::min(boundary().distance, collision_distance()); + double distance = std::min(boundary().distance(), collision_distance()); // Advance particle in space and time // Short-term solution until the surface source is revised and we can use @@ -298,12 +298,12 @@ void Particle::event_cross_surface() n_coord_last() = n_coord(); // Set surface that particle is on and adjust coordinate levels - surface() = boundary().surface; - n_coord() = boundary().coord_level; + surface() = boundary().surface(); + n_coord() = boundary().coord_level(); - if (boundary().lattice_translation[0] != 0 || - boundary().lattice_translation[1] != 0 || - boundary().lattice_translation[2] != 0) { + if (boundary().lattice_translation()[0] != 0 || + boundary().lattice_translation()[1] != 0 || + boundary().lattice_translation()[2] != 0) { // Particle crosses lattice boundary bool verbose = settings::verbosity >= 10 || trace(); diff --git a/src/particle_data.cpp b/src/particle_data.cpp index aff2b68ba..370ca12e4 100644 --- a/src/particle_data.cpp +++ b/src/particle_data.cpp @@ -65,22 +65,22 @@ void GeometryState::advance_to_boundary_from_void() for (auto c_i : root_universe->cells_) { auto dist = model::cells.at(c_i)->distance(root_coord.r(), root_coord.u(), 0, this); - if (dist.first < boundary().distance) { - boundary().distance = dist.first; - boundary().surface = dist.second; + if (dist.first < boundary().distance()) { + boundary().distance() = dist.first; + boundary().surface() = dist.second; } } // if no intersection or near-infinite intersection, reset // boundary information - if (boundary().distance > 1e300) { - boundary().distance = INFTY; - boundary().surface = SURFACE_NONE; + if (boundary().distance() > 1e300) { + boundary().distance() = INFTY; + boundary().surface() = SURFACE_NONE; return; } // move the particle up to (and just past) the boundary - move_distance(boundary().distance + TINY_BIT); + move_distance(boundary().distance() + TINY_BIT); } void GeometryState::move_distance(double length) diff --git a/src/plot.cpp b/src/plot.cpp index db10c7600..2cadc48ce 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -1634,7 +1634,7 @@ void Ray::trace() break; // if there is no intersection with the model, we're done - if (boundary().surface == SURFACE_NONE) + if (boundary().surface() == SURFACE_NONE) return; event_counter_++; @@ -1646,10 +1646,10 @@ void Ray::trace() // 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) { + if (boundary().surface() != SURFACE_NONE) { // set the geometry state's surface attribute to be used for // surface normal computation - surface() = boundary().surface; + surface() = boundary().surface(); on_intersection(); if (stop_) return; @@ -1674,37 +1674,37 @@ void Ray::trace() // 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) { + 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) { + 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; + 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(); + coord(lev).r() += boundary().distance() * coord(lev).u(); } - surface() = boundary().surface; + surface() = boundary().surface(); 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) { + 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; + 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 diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp index 2eca5b0ab..27f674c42 100644 --- a/src/random_ray/random_ray.cpp +++ b/src/random_ray/random_ray.cpp @@ -281,7 +281,7 @@ void RandomRay::event_advance_ray() { // Find the distance to the nearest boundary boundary() = distance_to_boundary(*this); - double distance = boundary().distance; + double distance = boundary().distance(); if (distance < 0.0) { mark_as_lost("Negative transport distance detected for particle " + diff --git a/src/simulation.cpp b/src/simulation.cpp index d9a4fd131..b9986f473 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -790,7 +790,7 @@ void transport_history_based_single_particle(Particle& p) p.event_advance(); } if (p.alive()) { - if (p.collision_distance() > p.boundary().distance) { + if (p.collision_distance() > p.boundary().distance()) { p.event_cross_surface(); } else if (p.alive()) { p.event_collide(); From 659e43af7de9030ef3fbfef32f0992c39a00820d Mon Sep 17 00:00:00 2001 From: Joffrey Dorville <54550047+JoffreyDorville@users.noreply.github.com> Date: Fri, 18 Jul 2025 03:47:08 -0500 Subject: [PATCH 376/671] Enabling MCPL source files to be read when using surf_source_read (#3472) Co-authored-by: Paul Romano --- include/openmc/source.h | 2 +- src/source.cpp | 55 ++++----- ..._true_read.dat => inputs_true_read_h5.dat} | 0 .../surface_source/inputs_true_read_mcpl.dat | 33 +++++ ...rue_write.dat => inputs_true_write_h5.dat} | 0 .../surface_source/inputs_true_write_mcpl.dat | 40 ++++++ .../surface_source/surface_source_true.h5 | Bin 106144 -> 106144 bytes .../surface_source/surface_source_true.mcpl | Bin 0 -> 36071 bytes tests/regression_tests/surface_source/test.py | 116 +++++++++++++----- 9 files changed, 187 insertions(+), 59 deletions(-) rename tests/regression_tests/surface_source/{inputs_true_read.dat => inputs_true_read_h5.dat} (100%) create mode 100644 tests/regression_tests/surface_source/inputs_true_read_mcpl.dat rename tests/regression_tests/surface_source/{inputs_true_write.dat => inputs_true_write_h5.dat} (100%) create mode 100644 tests/regression_tests/surface_source/inputs_true_write_mcpl.dat create mode 100644 tests/regression_tests/surface_source/surface_source_true.mcpl diff --git a/include/openmc/source.h b/include/openmc/source.h index d58195f10..1fbd31904 100644 --- a/include/openmc/source.h +++ b/include/openmc/source.h @@ -173,7 +173,7 @@ protected: SourceSite sample(uint64_t* seed) const override; private: - vector sites_; //!< Source sites from a file + vector sites_; //!< Source sites }; //============================================================================== diff --git a/src/source.cpp b/src/source.cpp index ae3daa404..12323f7bd 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -417,11 +417,7 @@ SourceSite IndependentSource::sample(uint64_t* seed) const FileSource::FileSource(pugi::xml_node node) : Source(node) { auto path = get_node_value(node, "file", false, true); - if (ends_with(path, ".mcpl") || ends_with(path, ".mcpl.gz")) { - sites_ = mcpl_source_sites(path); - } else { - this->load_sites_from_file(path); - } + load_sites_from_file(path); } FileSource::FileSource(const std::string& path) @@ -431,30 +427,33 @@ FileSource::FileSource(const std::string& path) void FileSource::load_sites_from_file(const std::string& path) { - // Check if source file exists - if (!file_exists(path)) { - fatal_error(fmt::format("Source file '{}' does not exist.", path)); + // If MCPL file, use the dedicated file reader + if (ends_with(path, ".mcpl") || ends_with(path, ".mcpl.gz")) { + sites_ = mcpl_source_sites(path); + } else { + // Check if source file exists + if (!file_exists(path)) { + fatal_error(fmt::format("Source file '{}' does not exist.", path)); + } + + write_message(6, "Reading source file from {}...", path); + + // Open the binary file + hid_t file_id = file_open(path, 'r', true); + + // Check to make sure this is a source file + std::string filetype; + read_attribute(file_id, "filetype", filetype); + if (filetype != "source" && filetype != "statepoint") { + fatal_error("Specified starting source file not a source file type."); + } + + // Read in the source particles + read_source_bank(file_id, sites_, false); + + // Close file + file_close(file_id); } - - // Read the source from a binary file instead of sampling from some - // assumed source distribution - write_message(6, "Reading source file from {}...", path); - - // Open the binary file - hid_t file_id = file_open(path, 'r', true); - - // Check to make sure this is a source file - std::string filetype; - read_attribute(file_id, "filetype", filetype); - if (filetype != "source" && filetype != "statepoint") { - fatal_error("Specified starting source file not a source file type."); - } - - // Read in the source particles - read_source_bank(file_id, sites_, false); - - // Close file - file_close(file_id); } SourceSite FileSource::sample(uint64_t* seed) const diff --git a/tests/regression_tests/surface_source/inputs_true_read.dat b/tests/regression_tests/surface_source/inputs_true_read_h5.dat similarity index 100% rename from tests/regression_tests/surface_source/inputs_true_read.dat rename to tests/regression_tests/surface_source/inputs_true_read_h5.dat diff --git a/tests/regression_tests/surface_source/inputs_true_read_mcpl.dat b/tests/regression_tests/surface_source/inputs_true_read_mcpl.dat new file mode 100644 index 000000000..b8d8c575a --- /dev/null +++ b/tests/regression_tests/surface_source/inputs_true_read_mcpl.dat @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + fixed source + 1000 + 10 + + surface_source_true.mcpl + + 1 + + + + 3 + + + 1 + flux + + + diff --git a/tests/regression_tests/surface_source/inputs_true_write.dat b/tests/regression_tests/surface_source/inputs_true_write_h5.dat similarity index 100% rename from tests/regression_tests/surface_source/inputs_true_write.dat rename to tests/regression_tests/surface_source/inputs_true_write_h5.dat diff --git a/tests/regression_tests/surface_source/inputs_true_write_mcpl.dat b/tests/regression_tests/surface_source/inputs_true_write_mcpl.dat new file mode 100644 index 000000000..c55223324 --- /dev/null +++ b/tests/regression_tests/surface_source/inputs_true_write_mcpl.dat @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + fixed source + 1000 + 10 + + + 0 0 0 + + + + 1 + true + 1000 + + 1 + + + + 3 + + + 1 + flux + + + diff --git a/tests/regression_tests/surface_source/surface_source_true.h5 b/tests/regression_tests/surface_source/surface_source_true.h5 index f2055d09dc3b668cc2fe85057cd999779788e9d7..d056d7328405bedf704f8690fd3b8056ec8aa101 100644 GIT binary patch delta 11203 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 NZmvQvPV5ll`9IHZo-P0Y literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source/test.py b/tests/regression_tests/surface_source/test.py index 83b78e61d..13966b26d 100644 --- a/tests/regression_tests/surface_source/test.py +++ b/tests/regression_tests/surface_source/test.py @@ -10,6 +10,25 @@ from tests.testing_harness import PyAPITestHarness from tests.regression_tests import config +def mcpl_to_array(filepath): + import mcpl + + source = [] + with mcpl.MCPLFile(filepath) as f: + for p in f.particles: + source.append( + [ + *tuple(p.position), + *tuple(p.direction), + 1.0e6 * p.ekin, + 1.0e-3 * p.time, + p.weight, + p.pdgcode, + ] + ) + return np.sort(np.array(source), axis=0) + + def assert_structured_arrays_close(arr1, arr2, rtol=1e-5, atol=1e-8): assert arr1.dtype == arr2.dtype @@ -24,8 +43,7 @@ def assert_structured_arrays_close(arr1, arr2, rtol=1e-5, atol=1e-8): @pytest.fixture def model(request): openmc.reset_auto_ids() - marker = request.node.get_closest_marker("surf_source_op") - surf_source_op = marker.args[0] + operation, file_format = request.node.get_closest_marker("params").args openmc_model = openmc.model.Model() @@ -54,15 +72,19 @@ def model(request): openmc_model.settings.batches = 10 openmc_model.settings.seed = 1 - if surf_source_op == 'write': + if operation == 'write': point = openmc.stats.Point((0, 0, 0)) pt_src = openmc.IndependentSource(space=point) openmc_model.settings.source = pt_src - openmc_model.settings.surf_source_write = {'surface_ids': [1], - 'max_particles': 1000} - elif surf_source_op == 'read': - openmc_model.settings.surf_source_read = {'path': 'surface_source_true.h5'} + surf_source_write_settings = {'surface_ids': [1], + 'max_particles': 1000} + if file_format == "mcpl": + surf_source_write_settings["mcpl"] = True + + openmc_model.settings.surf_source_write = surf_source_write_settings + elif operation == 'read': + openmc_model.settings.surf_source_read = {'path': f"surface_source_true.{file_format}"} # Tallies tal = openmc.Tally() @@ -75,22 +97,30 @@ def model(request): class SurfaceSourceTestHarness(PyAPITestHarness): + def __init__(self, statepoint_name, model=None, inputs_true=None, file_format="h5"): + super().__init__(statepoint_name, model, inputs_true) + self.file_format = file_format + def _test_output_created(self): - """Make sure surface_source.h5 has also been created.""" + """Make sure the surface_source file has also been created.""" super()._test_output_created() - # Check if 'surface_source.h5' has been created. if self._model.settings.surf_source_write: - assert os.path.exists('surface_source.h5'), \ + assert os.path.exists(f"surface_source.{self.file_format}"), \ 'Surface source file does not exist.' def _compare_output(self): """Make sure the current surface_source.h5 agree with the reference.""" if self._model.settings.surf_source_write: - with h5py.File("surface_source_true.h5", 'r') as f: - source_true = np.sort(f['source_bank'][()]) - with h5py.File("surface_source.h5", 'r') as f: - source_test = np.sort(f['source_bank'][()]) - assert_structured_arrays_close(source_true, source_test, atol=1e-07) + if self.file_format == "h5": + with h5py.File("surface_source_true.h5", 'r') as f: + source_true = np.sort(f['source_bank'][()]) + with h5py.File("surface_source.h5", 'r') as f: + source_test = np.sort(f['source_bank'][()]) + assert_structured_arrays_close(source_true, source_test, atol=1e-07) + elif self.file_format == "mcpl": + source_true = mcpl_to_array("surface_source_true.mcpl") + source_test = mcpl_to_array("surface_source.mcpl") + np.testing.assert_allclose(source_true, source_test, rtol=1e-5, atol=1e-7) def execute_test(self): """Build input XMLs, run OpenMC, check output and results.""" @@ -111,30 +141,56 @@ class SurfaceSourceTestHarness(PyAPITestHarness): def _overwrite_results(self): """Overwrite the results_true with the results_test.""" shutil.copyfile('results_test.dat', 'results_true.dat') - if os.path.exists('surface_source.h5'): - shutil.copyfile('surface_source.h5', 'surface_source_true.h5') + if os.path.exists(f"surface_source.{self.file_format}"): + shutil.copyfile(f"surface_source.{self.file_format}", f"surface_source_true.{self.file_format}") def _cleanup(self): """Delete statepoints, tally, and test files.""" super()._cleanup() - fs = 'surface_source.h5' + fs = f"surface_source.{self.file_format}" if os.path.exists(fs): os.remove(fs) -@pytest.mark.surf_source_op('write') -def test_surface_source_write(model, monkeypatch): - # Test result is based on 1 MPI process - monkeypatch.setitem(config, "mpi_np", "1") - harness = SurfaceSourceTestHarness('statepoint.10.h5', - model, - 'inputs_true_write.dat') +@pytest.mark.params('write', 'h5') +def test_surface_source_write(model, monkeypatch, request): + monkeypatch.setitem(config, "mpi_np", "1") # Results generated with 1 MPI process + operation, file_format = request.node.get_closest_marker("params").args + harness = SurfaceSourceTestHarness( + "statepoint.10.h5", model, f"inputs_true_{operation}_{file_format}.dat", + file_format=file_format + ) harness.main() -@pytest.mark.surf_source_op('read') -def test_surface_source_read(model): - harness = SurfaceSourceTestHarness('statepoint.10.h5', - model, - 'inputs_true_read.dat') +@pytest.mark.params('read', 'h5') +def test_surface_source_read(model, request): + operation, file_format = request.node.get_closest_marker("params").args + harness = SurfaceSourceTestHarness( + "statepoint.10.h5", model, f"inputs_true_{operation}_{file_format}.dat", + file_format=file_format + ) + harness.main() + + +@pytest.mark.skipif(shutil.which("mcpl-config") is None, reason="MCPL is not available.") +@pytest.mark.params('write', 'mcpl') +def test_surface_source_write_mcpl(model, monkeypatch, request): + monkeypatch.setitem(config, "mpi_np", "1") # Results generated with 1 MPI process + operation, file_format = request.node.get_closest_marker("params").args + harness = SurfaceSourceTestHarness( + "statepoint.10.h5", model, f"inputs_true_{operation}_{file_format}.dat", + file_format=file_format + ) + harness.main() + + +@pytest.mark.skipif(shutil.which("mcpl-config") is None, reason="MCPL is not available.") +@pytest.mark.params('read', 'mcpl') +def test_surface_source_read_mcpl(model, request): + operation, file_format = request.node.get_closest_marker("params").args + harness = SurfaceSourceTestHarness( + "statepoint.10.h5", model, f"inputs_true_{operation}_{file_format}.dat", + file_format=file_format + ) harness.main() From 8be65513b77c08e79e1dc5f4ef921d411ff990a9 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Fri, 18 Jul 2025 16:37:57 +0200 Subject: [PATCH 377/671] Adding material depletion function (#3420) Co-authored-by: Jon Shimwell Co-authored-by: Micah Gale Co-authored-by: Paul Romano --- openmc/deplete/independent_operator.py | 2 +- openmc/material.py | 174 +++++++++++++++++++++++++ tests/unit_tests/test_material.py | 43 ++++++ tests/unit_tests/test_materials.py | 63 +++++++++ 4 files changed, 281 insertions(+), 1 deletion(-) create mode 100644 tests/unit_tests/test_materials.py diff --git a/openmc/deplete/independent_operator.py b/openmc/deplete/independent_operator.py index 3eb50b05e..c192907cf 100644 --- a/openmc/deplete/independent_operator.py +++ b/openmc/deplete/independent_operator.py @@ -273,7 +273,7 @@ class IndependentOperator(OpenMCOperator): Returns ------- nuclides : set of str - Set of nuclide names that have cross secton data + Set of nuclide names that have cross section data """ return set(cross_sections[0].nuclides) diff --git a/openmc/material.py b/openmc/material.py index c6765178e..cec74af62 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -6,6 +6,8 @@ from numbers import Real from pathlib import Path import re import sys +import tempfile +from typing import Sequence, Dict import warnings import lxml.etree as ET @@ -1717,6 +1719,67 @@ class Material(IDManagerMixin): return mat + def deplete( + self, + multigroup_flux: Sequence[float], + energy_group_structure: Sequence[float] | str, + timesteps: Sequence[float] | Sequence[tuple[float, str]], + source_rates: float | Sequence[float], + timestep_units: str = 's', + chain_file: cv.PathLike | "openmc.deplete.Chain" | None = None, + reactions: Sequence[str] | None = None, + ) -> list[openmc.Material]: + """Depletes that material, evolving the nuclide densities + + .. versionadded:: 0.15.3 + + Parameters + ---------- + multigroup_flux: Sequence[float] + Energy-dependent multigroup flux values, where each sublist corresponds + to a specific material. Will be normalized so that it sums to 1. + energy_group_structure : Sequence[float] | str + Energy group boundaries in [eV] or the name of the group structure. + timesteps : iterable of float or iterable of tuple + Array of timesteps. Note that values are not cumulative. The units are + specified by the `timestep_units` argument when `timesteps` is an + iterable of float. Alternatively, units can be specified for each step + by passing an iterable of (value, unit) tuples. + source_rates : float or iterable of float, optional + Source rate in [neutron/sec] or neutron flux in [neutron/s-cm^2] for + each interval in :attr:`timesteps` + timestep_units : {'s', 'min', 'h', 'd', 'a', 'MWd/kg'} + Units for values specified in the `timesteps` argument. 's' means + seconds, 'min' means minutes, 'h' means hours, 'a' means Julian years + and 'MWd/kg' indicates that the values are given in burnup (MW-d of + energy deposited per kilogram of initial heavy metal). + chain_file : PathLike or Chain + Path to the depletion chain XML file or instance of openmc.deplete.Chain. + Defaults to ``openmc.config['chain_file']``. + reactions : list of str, optional + Reactions to get cross sections for. If not specified, all neutron + reactions listed in the depletion chain file are used. + + Returns + ------- + list of openmc.Material, one for each timestep + + """ + + materials = openmc.Materials([self]) + + depleted_materials_dict = materials.deplete( + multigroup_fluxes=[multigroup_flux], + energy_group_structures=[energy_group_structure], + timesteps=timesteps, + source_rates=source_rates, + timestep_units=timestep_units, + chain_file=chain_file, + reactions=reactions, + ) + + return depleted_materials_dict[self.id] + def mean_free_path(self, energy: float) -> float: """Calculate the mean free path of neutrons in the material at a given @@ -1947,3 +2010,114 @@ class Materials(cv.CheckedList): root = tree.getroot() return cls.from_xml_element(root) + + + def deplete( + self, + multigroup_fluxes: Sequence[Sequence[float]], + energy_group_structures: Sequence[Sequence[float] | str], + timesteps: Sequence[float] | Sequence[tuple[float, str]], + source_rates: float | Sequence[float], + timestep_units: str = 's', + chain_file: cv.PathLike | "openmc.deplete.Chain" | None = None, + reactions: Sequence[str] | None = None, + ) -> Dict[int, list[openmc.Material]]: + """Depletes that material, evolving the nuclide densities + + .. versionadded:: 0.15.3 + + Parameters + ---------- + multigroup_fluxes: Sequence[Sequence[float]] + Energy-dependent multigroup flux values, where each sublist corresponds + to a specific material. Will be normalized so that it sums to 1. + energy_group_structures': Sequence[Sequence[float] | str] + Energy group boundaries in [eV] or the name of the group structure. + timesteps : iterable of float or iterable of tuple + Array of timesteps. Note that values are not cumulative. The units are + specified by the `timestep_units` argument when `timesteps` is an + iterable of float. Alternatively, units can be specified for each step + by passing an iterable of (value, unit) tuples. + source_rates : float or iterable of float, optional + Source rate in [neutron/sec] or neutron flux in [neutron/s-cm^2] for + each interval in :attr:`timesteps` + timestep_units : {'s', 'min', 'h', 'd', 'a', 'MWd/kg'} + Units for values specified in the `timesteps` argument. 's' means + seconds, 'min' means minutes, 'h' means hours, 'a' means Julian years + and 'MWd/kg' indicates that the values are given in burnup (MW-d of + energy deposited per kilogram of initial heavy metal). + chain_file : PathLike or Chain + Path to the depletion chain XML file or instance of openmc.deplete.Chain. + Defaults to ``openmc.config['chain_file']``. + reactions : list of str, optional + Reactions to get cross sections for. If not specified, all neutron + reactions listed in the depletion chain file are used. + + Returns + ------- + list of openmc.Material, one for each timestep + + """ + + import openmc.deplete + from .deplete.chain import _get_chain + + # setting all materials to be depletable + for mat in self: + mat.depletable = True + + chain = _get_chain(chain_file) + + # Create MicroXS objects for all materials + micros = [] + fluxes = [] + + with openmc.lib.TemporarySession(): + for material, flux, energy in zip( + self, multigroup_fluxes, energy_group_structures + ): + temperature = material.temperature or 293.6 + micro_xs = openmc.deplete.MicroXS.from_multigroup_flux( + energies=energy, + multigroup_flux=flux, + chain_file=chain, + temperature=temperature, + reactions=reactions, + ) + micros.append(micro_xs) + fluxes.append(material.volume) + + # Create a single operator for all materials + operator = openmc.deplete.IndependentOperator( + materials=self, + fluxes=fluxes, + micros=micros, + normalization_mode="source-rate", + chain_file=chain, + ) + + integrator = openmc.deplete.PredictorIntegrator( + operator=operator, + timesteps=timesteps, + source_rates=source_rates, + timestep_units=timestep_units, + ) + + with tempfile.TemporaryDirectory() as tmpdir: + # Run integrator + results_path = Path(tmpdir) / "depletion_results.h5" + integrator.integrate(path=results_path) + + # Load depletion results + results = openmc.deplete.Results(results_path) + + # For each material, get activated composition at each timestep + all_depleted_materials = { + material.id: [ + result.get_material(str(material.id)) + for result in results + ] + for material in self + } + + return all_depleted_materials diff --git a/tests/unit_tests/test_material.py b/tests/unit_tests/test_material.py index db5f4ce32..eae814a75 100644 --- a/tests/unit_tests/test_material.py +++ b/tests/unit_tests/test_material.py @@ -3,8 +3,11 @@ from pathlib import Path import pytest +import numpy as np + import openmc from openmc.data import decay_photon_energy +from openmc.deplete import Chain import openmc.examples import openmc.model import openmc.stats @@ -712,6 +715,46 @@ def test_avoid_subnormal(run_in_tmpdir): assert mats[0].get_nuclide_atom_densities()['H2'] == 0.0 +def test_material_deplete(): + pristine_material = openmc.Material() + pristine_material.add_nuclide("Ni58", 1.0) + pristine_material.set_density("g/cm3", 7.87) + pristine_material.depletable = True + pristine_material.temperature = 293.6 + pristine_material.volume = 1. + + mg_flux = [0.5e11] * 42 + + chain = Chain.from_xml( + Path(__file__).parents[1] / "chain_ni.xml" + ) + + depleted_material = pristine_material.deplete( + multigroup_flux=mg_flux, + energy_group_structure="VITAMIN-J-42", + timesteps=[10, 70.86], + source_rates=[1e19, 0.0], + timestep_units="d", + chain_file=chain, + ) + + for material in depleted_material: + assert isinstance(material, openmc.Material) + assert len(material.get_nuclides()) > len(pristine_material.get_nuclides()) + + Co58_mat_1_step_0 = depleted_material[0].get_nuclide_atom_densities("Co58")["Co58"] + Co58_mat_1_step_1 = depleted_material[1].get_nuclide_atom_densities("Co58")["Co58"] + Co58_mat_1_step_2 = depleted_material[2].get_nuclide_atom_densities("Co58")["Co58"] + + assert Co58_mat_1_step_0 == 0.0 + + # Check that Co58 is produced in the first step + assert Co58_mat_1_step_1 > 0.0 + + # Check that Co58 is halved in the second step which is one halflife later + assert np.allclose(Co58_mat_1_step_1 * 0.5, Co58_mat_1_step_2) + + def test_mean_free_path(): mat1 = openmc.Material() diff --git a/tests/unit_tests/test_materials.py b/tests/unit_tests/test_materials.py new file mode 100644 index 000000000..6c8c59fc9 --- /dev/null +++ b/tests/unit_tests/test_materials.py @@ -0,0 +1,63 @@ +from pathlib import Path + +import openmc +from openmc.deplete import Chain + + +def test_materials_deplete(): + pristine_material_1 = openmc.Material() + pristine_material_1.add_nuclide("Ni58", 1.) + pristine_material_1.set_density("g/cm3", 7.87) + pristine_material_1.depletable = True + pristine_material_1.temperature = 293.6 + pristine_material_1.volume = 1. + + pristine_material_2 = openmc.Material() + pristine_material_2.add_nuclide("Ni60", 1.) + pristine_material_2.set_density("g/cm3", 7.87) + pristine_material_2.depletable = True + pristine_material_2.temperature = 293.6 + pristine_material_2.volume = 1. + + pristine_materials = openmc.Materials([pristine_material_1, pristine_material_2]) + + mg_flux = [0.5e11] * 42 + + chain = Chain.from_xml( + Path(__file__).parents[1] / "chain_ni.xml" + ) + + depleted_material = pristine_materials.deplete( + multigroup_fluxes=[mg_flux, mg_flux], + energy_group_structures=["VITAMIN-J-42", "VITAMIN-J-42"], + timesteps=[100, 100], + source_rates=[1e19, 0.0], + timestep_units="d", + chain_file=chain, + ) + + assert list(depleted_material.keys()) == [pristine_material_1.id, pristine_material_2.id] + for mat_id, materials in depleted_material.items(): + for material in materials: + assert isinstance(material, openmc.Material) + assert len(material.get_nuclides()) > 1 + assert mat_id == material.id + + mats = depleted_material[pristine_material_1.id] + Co58_mat_1_step_0 = mats[0].get_nuclide_atom_densities("Co58")["Co58"] + Co58_mat_1_step_1 = mats[1].get_nuclide_atom_densities("Co58")["Co58"] + Co58_mat_1_step_2 = mats[2].get_nuclide_atom_densities("Co58")["Co58"] + + assert Co58_mat_1_step_0 == 0.0 + # Co58 is the main activation product of Ni58 in the first irradiation step. + # It then decays in the second cooling step (flux = 0) + assert Co58_mat_1_step_1 > 0.0 and Co58_mat_1_step_1 > Co58_mat_1_step_2 + + Ni59_mat_1_step_0 = mats[0].get_nuclide_atom_densities("Ni59")["Ni59"] + Ni59_mat_1_step_1 = mats[1].get_nuclide_atom_densities("Ni59")["Ni59"] + Ni59_mat_1_step_2 = mats[2].get_nuclide_atom_densities("Ni59")["Ni59"] + + assert Ni59_mat_1_step_0 == 0.0 + # Ni59 is one of the main activation product of Ni60 in the first irradiation + # step. It then decays in the second cooling step (flux = 0) + assert Ni59_mat_1_step_1 > 0.0 and Ni59_mat_1_step_1 > Ni59_mat_1_step_2 From a649d7bc69c47d5f04dbca50b106b605598ffcf7 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Fri, 18 Jul 2025 18:23:02 +0200 Subject: [PATCH 378/671] Avoid adding ParentNuclideFilter twice when calling prepare_tallies (#3506) Co-authored-by: Paul Romano --- openmc/deplete/d1s.py | 4 ++-- tests/unit_tests/test_d1s.py | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/openmc/deplete/d1s.py b/openmc/deplete/d1s.py index e9f8bfe79..311bc2d69 100644 --- a/openmc/deplete/d1s.py +++ b/openmc/deplete/d1s.py @@ -243,7 +243,7 @@ def prepare_tallies( for f in tally.filters: if isinstance(f, openmc.ParticleFilter): if list(f.bins) == ['photon']: - tally.filters.append(filter) + if not tally.contains_filter(openmc.ParentNuclideFilter): + tally.filters.append(filter) break - return nuclides diff --git a/tests/unit_tests/test_d1s.py b/tests/unit_tests/test_d1s.py index 115759489..9410f2da2 100644 --- a/tests/unit_tests/test_d1s.py +++ b/tests/unit_tests/test_d1s.py @@ -84,6 +84,11 @@ def test_prepare_tallies(model): assert tally.contains_filter(openmc.ParentNuclideFilter) assert sorted(tally.filters[-1].bins) == sorted(radionuclides) + assert len(tally.filters) == 2 + # calling prepare_tallies twice should not add another ParentNuclideFilter + d1s.prepare_tallies(model, chain_file=CHAIN_PATH) + assert len(tally.filters) == 2 + def test_apply_time_correction(run_in_tmpdir): # Make simple sphere model with elemental Ni From 9d9dcc26c5a6f7b8e06335cfe7a88d296db48710 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Mon, 21 Jul 2025 23:10:08 -0500 Subject: [PATCH 379/671] Update DAGMC and libMesh precompiler definitions (#3510) --- CMakeLists.txt | 6 ++-- include/openmc/dagmc.h | 4 +-- include/openmc/mesh.h | 10 +++--- include/openmc/particle_data.h | 6 ++-- include/openmc/universe.h | 2 +- src/cell.cpp | 2 +- src/dagmc.cpp | 32 +++++++++---------- src/finalize.cpp | 2 +- src/initialize.cpp | 4 +-- src/mesh.cpp | 22 ++++++------- src/output.cpp | 6 ++-- src/particle.cpp | 10 +++--- src/state_point.cpp | 2 +- src/tallies/tally.cpp | 2 +- tests/regression_tests/dagmc/external/test.py | 2 +- tests/regression_tests/external_moab/test.py | 2 +- 16 files changed, 57 insertions(+), 57 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2b863eb08..474451c8a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -503,11 +503,11 @@ else() endif() if(OPENMC_USE_DAGMC) - target_compile_definitions(libopenmc PRIVATE DAGMC) + target_compile_definitions(libopenmc PRIVATE OPENMC_DAGMC_ENABLED) target_link_libraries(libopenmc dagmc-shared) if(OPENMC_USE_UWUW) - target_compile_definitions(libopenmc PRIVATE OPENMC_UWUW) + target_compile_definitions(libopenmc PRIVATE OPENMC_UWUW_ENABLED) target_link_libraries(libopenmc uwuw-shared) endif() elseif(OPENMC_USE_UWUW) @@ -516,7 +516,7 @@ elseif(OPENMC_USE_UWUW) endif() if(OPENMC_USE_LIBMESH) - target_compile_definitions(libopenmc PRIVATE LIBMESH) + target_compile_definitions(libopenmc PRIVATE OPENMC_LIBMESH_ENABLED) target_link_libraries(libopenmc PkgConfig::LIBMESH) endif() diff --git a/include/openmc/dagmc.h b/include/openmc/dagmc.h index 82ef1b644..0e27402a1 100644 --- a/include/openmc/dagmc.h +++ b/include/openmc/dagmc.h @@ -20,7 +20,7 @@ void check_dagmc_root_univ(); } // namespace openmc -#ifdef DAGMC +#ifdef OPENMC_DAGMC_ENABLED #include "DagMC.hpp" #include "dagmcmetadata.hpp" @@ -216,6 +216,6 @@ int32_t next_cell(int32_t surf, int32_t curr_cell, int32_t univ); } // namespace openmc -#endif // DAGMC +#endif // OPENMC_DAGMC_ENABLED #endif // OPENMC_DAGMC_H diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index 5a727c2b6..c15e25697 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -19,14 +19,14 @@ #include "openmc/vector.h" #include "openmc/xml_interface.h" -#ifdef DAGMC +#ifdef OPENMC_DAGMC_ENABLED #include "moab/AdaptiveKDTree.hpp" #include "moab/Core.hpp" #include "moab/GeomUtil.hpp" #include "moab/Matrix3.hpp" #endif -#ifdef LIBMESH +#ifdef OPENMC_LIBMESH_ENABLED #include "libmesh/bounding_box.h" #include "libmesh/dof_map.h" #include "libmesh/elem.h" @@ -61,7 +61,7 @@ extern vector> meshes; } // namespace model -#ifdef LIBMESH +#ifdef OPENMC_LIBMESH_ENABLED namespace settings { // used when creating new libMesh::MeshBase instances extern unique_ptr libmesh_init; @@ -768,7 +768,7 @@ private: virtual void initialize() = 0; }; -#ifdef DAGMC +#ifdef OPENMC_DAGMC_ENABLED class MOABMesh : public UnstructuredMesh { public: @@ -938,7 +938,7 @@ private: #endif -#ifdef LIBMESH +#ifdef OPENMC_LIBMESH_ENABLED class LibMesh : public UnstructuredMesh { public: diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index 1c8f1215a..ec393845f 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -8,7 +8,7 @@ #include "openmc/tallies/filter_match.h" #include "openmc/vector.h" -#ifdef DAGMC +#ifdef OPENMC_DAGMC_ENABLED #include "DagMC.hpp" #endif @@ -372,7 +372,7 @@ public: // Boundary information BoundaryInfo& boundary() { return boundary_; } -#ifdef DAGMC +#ifdef OPENMC_DAGMC_ENABLED // DagMC state variables moab::DagMC::RayHistory& history() { return history_; } Direction& last_dir() { return last_dir_; } @@ -417,7 +417,7 @@ private: double sqrtkT_ {-1.0}; //!< sqrt(k_Boltzmann * temperature) in eV double sqrtkT_last_ {0.0}; //!< last temperature -#ifdef DAGMC +#ifdef OPENMC_DAGMC_ENABLED moab::DagMC::RayHistory history_; Direction last_dir_; #endif diff --git a/include/openmc/universe.h b/include/openmc/universe.h index e8fbacfdc..52df432b8 100644 --- a/include/openmc/universe.h +++ b/include/openmc/universe.h @@ -6,7 +6,7 @@ namespace openmc { -#ifdef DAGMC +#ifdef OPENMC_DAGMC_ENABLED class DAGUniverse; #endif diff --git a/src/cell.cpp b/src/cell.cpp index eb5c228bc..b6862ba17 100644 --- a/src/cell.cpp +++ b/src/cell.cpp @@ -1025,7 +1025,7 @@ void populate_universes() model::universes.back()->cells_.push_back(index_cell); model::universe_map[uid] = model::universes.size() - 1; } else { -#ifdef DAGMC +#ifdef OPENMC_DAGMC_ENABLED // Skip implicit complement cells for now Universe* univ = model::universes[it->second].get(); DAGUniverse* dag_univ = dynamic_cast(univ); diff --git a/src/dagmc.cpp b/src/dagmc.cpp index 1d92d9d57..8c81952a7 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -13,7 +13,7 @@ #include "openmc/settings.h" #include "openmc/string_utils.h" -#ifdef OPENMC_UWUW +#ifdef OPENMC_UWUW_ENABLED #include "uwuw.hpp" #endif #include @@ -26,13 +26,13 @@ namespace openmc { -#ifdef DAGMC +#ifdef OPENMC_DAGMC_ENABLED const bool DAGMC_ENABLED = true; #else const bool DAGMC_ENABLED = false; #endif -#ifdef OPENMC_UWUW +#ifdef OPENMC_UWUW_ENABLED const bool UWUW_ENABLED = true; #else const bool UWUW_ENABLED = false; @@ -40,7 +40,7 @@ const bool UWUW_ENABLED = false; } // namespace openmc -#ifdef DAGMC +#ifdef OPENMC_DAGMC_ENABLED namespace openmc { @@ -131,7 +131,7 @@ void DAGUniverse::set_id() void DAGUniverse::initialize() { -#ifdef OPENMC_UWUW +#ifdef OPENMC_UWUW_ENABLED // read uwuw materials from the .h5m file if present read_uwuw_materials(); #endif @@ -456,16 +456,16 @@ void DAGUniverse::to_hdf5(hid_t universes_group) const bool DAGUniverse::uses_uwuw() const { -#ifdef OPENMC_UWUW +#ifdef OPENMC_UWUW_ENABLED return uwuw_ && !uwuw_->material_library.empty(); #else return false; -#endif // OPENMC_UWUW +#endif // OPENMC_UWUW_ENABLED } std::string DAGUniverse::get_uwuw_materials_xml() const { -#ifdef OPENMC_UWUW +#ifdef OPENMC_UWUW_ENABLED if (!uses_uwuw()) { throw std::runtime_error("This DAGMC Universe does not use UWUW materials"); } @@ -485,12 +485,12 @@ std::string DAGUniverse::get_uwuw_materials_xml() const return ss.str(); #else fatal_error("DAGMC was not configured with UWUW."); -#endif // OPENMC_UWUW +#endif // OPENMC_UWUW_ENABLED } void DAGUniverse::write_uwuw_materials_xml(const std::string& outfile) const { -#ifdef OPENMC_UWUW +#ifdef OPENMC_UWUW_ENABLED if (!uses_uwuw()) { throw std::runtime_error( "This DAGMC universe does not use UWUW materials."); @@ -503,7 +503,7 @@ void DAGUniverse::write_uwuw_materials_xml(const std::string& outfile) const mats_xml.close(); #else fatal_error("DAGMC was not configured with UWUW."); -#endif // OPENMC_UWUW +#endif // OPENMC_UWUW_ENABLED } void DAGUniverse::legacy_assign_material( @@ -565,7 +565,7 @@ void DAGUniverse::legacy_assign_material( void DAGUniverse::read_uwuw_materials() { -#ifdef OPENMC_UWUW +#ifdef OPENMC_UWUW_ENABLED // If no filename was provided, don't read UWUW materials if (filename_ == "") return; @@ -605,13 +605,13 @@ void DAGUniverse::read_uwuw_materials() } #else fatal_error("DAGMC was not configured with UWUW."); -#endif // OPENMC_UWUW +#endif // OPENMC_UWUW_ENABLED } void DAGUniverse::uwuw_assign_material( moab::EntityHandle vol_handle, std::unique_ptr& c) const { -#ifdef OPENMC_UWUW +#ifdef OPENMC_UWUW_ENABLED // lookup material in uwuw if present std::string uwuw_mat = dmd_ptr->volume_material_property_data_eh[vol_handle]; if (uwuw_->material_library.count(uwuw_mat) != 0) { @@ -627,7 +627,7 @@ void DAGUniverse::uwuw_assign_material( } #else fatal_error("DAGMC was not configured with UWUW."); -#endif // OPENMC_UWUW +#endif // OPENMC_UWUW_ENABLED } void DAGUniverse::override_assign_material(std::unique_ptr& c) const @@ -926,4 +926,4 @@ int32_t next_cell(int32_t surf, int32_t curr_cell, int32_t univ); } // namespace openmc -#endif // DAGMC +#endif // OPENMC_DAGMC_ENABLED diff --git a/src/finalize.cpp b/src/finalize.cpp index 9cbebc878..2d14cdec8 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -166,7 +166,7 @@ int openmc_finalize() // Deallocate arrays free_memory(); -#ifdef LIBMESH +#ifdef OPENMC_LIBMESH_ENABLED settings::libmesh_init.reset(); #endif diff --git a/src/initialize.cpp b/src/initialize.cpp index c5803976b..2da78137d 100644 --- a/src/initialize.cpp +++ b/src/initialize.cpp @@ -38,7 +38,7 @@ #include "openmc/vector.h" #include "openmc/weight_windows.h" -#ifdef LIBMESH +#ifdef OPENMC_LIBMESH_ENABLED #include "libmesh/libmesh.h" #endif @@ -64,7 +64,7 @@ int openmc_init(int argc, char* argv[], const void* intracomm) if (err) return err; -#ifdef LIBMESH +#ifdef OPENMC_LIBMESH_ENABLED const int n_threads = num_threads(); // initialize libMesh if it hasn't been initialized already // (if initialized externally, the libmesh_init object needs to be provided diff --git a/src/mesh.cpp b/src/mesh.cpp index 9147efb95..3e4ff1a3e 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -47,13 +47,13 @@ #include "openmc/volume_calc.h" #include "openmc/xml_interface.h" -#ifdef LIBMESH +#ifdef OPENMC_LIBMESH_ENABLED #include "libmesh/mesh_modification.h" #include "libmesh/mesh_tools.h" #include "libmesh/numeric_vector.h" #endif -#ifdef DAGMC +#ifdef OPENMC_DAGMC_ENABLED #include "moab/FileOptions.hpp" #endif @@ -63,7 +63,7 @@ namespace openmc { // Global variables //============================================================================== -#ifdef LIBMESH +#ifdef OPENMC_LIBMESH_ENABLED const bool LIBMESH_ENABLED = true; #else const bool LIBMESH_ENABLED = false; @@ -80,7 +80,7 @@ vector> meshes; } // namespace model -#ifdef LIBMESH +#ifdef OPENMC_LIBMESH_ENABLED namespace settings { unique_ptr libmesh_init; const libMesh::Parallel::Communicator* libmesh_comm {nullptr}; @@ -2134,14 +2134,14 @@ extern "C" int openmc_add_unstructured_mesh( std::string mesh_file(filename); bool valid_lib = false; -#ifdef DAGMC +#ifdef OPENMC_DAGMC_ENABLED if (lib_name == MOABMesh::mesh_lib_type) { model::meshes.push_back(std::move(make_unique(mesh_file))); valid_lib = true; } #endif -#ifdef LIBMESH +#ifdef OPENMC_LIBMESH_ENABLED if (lib_name == LibMesh::mesh_lib_type) { model::meshes.push_back(std::move(make_unique(mesh_file))); valid_lib = true; @@ -2509,7 +2509,7 @@ extern "C" int openmc_spherical_mesh_set_grid(int32_t index, index, grid_x, nx, grid_y, ny, grid_z, nz); } -#ifdef DAGMC +#ifdef OPENMC_DAGMC_ENABLED const std::string MOABMesh::mesh_lib_type = "moab"; @@ -3211,7 +3211,7 @@ void MOABMesh::write(const std::string& base_filename) const #endif -#ifdef LIBMESH +#ifdef OPENMC_LIBMESH_ENABLED const std::string LibMesh::mesh_lib_type = "libmesh"; @@ -3556,7 +3556,7 @@ double LibMesh::volume(int bin) const return this->get_element_from_bin(bin).volume(); } -#endif // LIBMESH +#endif // OPENMC_LIBMESH_ENABLED //============================================================================== // Non-member functions @@ -3605,12 +3605,12 @@ void read_meshes(pugi::xml_node root) model::meshes.push_back(make_unique(node)); } else if (mesh_type == SphericalMesh::mesh_type) { model::meshes.push_back(make_unique(node)); -#ifdef DAGMC +#ifdef OPENMC_DAGMC_ENABLED } else if (mesh_type == UnstructuredMesh::mesh_type && mesh_lib == MOABMesh::mesh_lib_type) { model::meshes.push_back(make_unique(node)); #endif -#ifdef LIBMESH +#ifdef OPENMC_LIBMESH_ENABLED } else if (mesh_type == UnstructuredMesh::mesh_type && mesh_lib == LibMesh::mesh_lib_type) { model::meshes.push_back(make_unique(node)); diff --git a/src/output.cpp b/src/output.cpp index 0316e313e..0a14e8843 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -323,10 +323,10 @@ void print_build_info() #ifdef OPENMC_MPI mpi = y; #endif -#ifdef DAGMC +#ifdef OPENMC_DAGMC_ENABLED dagmc = y; #endif -#ifdef LIBMESH +#ifdef OPENMC_LIBMESH_ENABLED libmesh = y; #endif #ifdef OPENMC_MCPL @@ -341,7 +341,7 @@ void print_build_info() #ifdef COVERAGEBUILD coverage = y; #endif -#ifdef OPENMC_UWUW +#ifdef OPENMC_UWUW_ENABLED uwuw = y; #endif diff --git a/src/particle.cpp b/src/particle.cpp index f9044b2d7..118b084b0 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -32,7 +32,7 @@ #include "openmc/track_output.h" #include "openmc/weight_windows.h" -#ifdef DAGMC +#ifdef OPENMC_DAGMC_ENABLED #include "DagMC.hpp" #endif @@ -408,7 +408,7 @@ void Particle::event_collide() if (!model::active_tallies.empty()) score_collision_derivative(*this); -#ifdef DAGMC +#ifdef OPENMC_DAGMC_ENABLED history().reset(); #endif } @@ -473,7 +473,7 @@ void Particle::event_revive_from_secondary() void Particle::event_death() { -#ifdef DAGMC +#ifdef OPENMC_DAGMC_ENABLED history().reset(); #endif @@ -553,7 +553,7 @@ void Particle::cross_surface(const Surface& surf) } // if we're crossing a CSG surface, make sure the DAG history is reset -#ifdef DAGMC +#ifdef OPENMC_DAGMC_ENABLED if (surf.geom_type() == GeometryType::CSG) history().reset(); #endif @@ -567,7 +567,7 @@ void Particle::cross_surface(const Surface& surf) // ========================================================================== // SEARCH NEIGHBOR LISTS FOR NEXT CELL -#ifdef DAGMC +#ifdef OPENMC_DAGMC_ENABLED // in DAGMC, we know what the next cell should be if (surf.geom_type() == GeometryType::DAG) { int32_t i_cell = next_cell(surface_index(), cell_last(n_coord() - 1), diff --git a/src/state_point.cpp b/src/state_point.cpp index 12dfeb82a..8195c4865 100644 --- a/src/state_point.cpp +++ b/src/state_point.cpp @@ -340,7 +340,7 @@ extern "C" int openmc_statepoint_write(const char* filename, bool* write_source) file_close(file_id); } -#if defined(LIBMESH) || defined(DAGMC) +#if defined(OPENMC_LIBMESH_ENABLED) || defined(OPENMC_DAGMC_ENABLED) // write unstructured mesh tally files write_unstructured_mesh_results(); #endif diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index 54840b518..e737f79d3 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -376,7 +376,7 @@ Tally::Tally(pugi::xml_node node) } } -#ifdef LIBMESH +#ifdef OPENMC_LIBMESH_ENABLED // ensure a tracklength tally isn't used with a libMesh filter for (auto i : this->filters_) { auto df = dynamic_cast(model::tally_filters[i].get()); diff --git a/tests/regression_tests/dagmc/external/test.py b/tests/regression_tests/dagmc/external/test.py index 57bc9ea7f..3580bfa11 100644 --- a/tests/regression_tests/dagmc/external/test.py +++ b/tests/regression_tests/dagmc/external/test.py @@ -32,7 +32,7 @@ def cpp_driver(request): target_link_libraries(main OpenMC::libopenmc) target_compile_features(main PUBLIC cxx_std_14) set(CMAKE_CXX_FLAGS "-pedantic-errors") - add_compile_definitions(DAGMC=1) + add_compile_definitions(OPENMC_DAGMC_ENABLED=1) """.format(openmc_dir))) # Create temporary build directory and change to there diff --git a/tests/regression_tests/external_moab/test.py b/tests/regression_tests/external_moab/test.py index ce4e78a2c..2d64eb14b 100644 --- a/tests/regression_tests/external_moab/test.py +++ b/tests/regression_tests/external_moab/test.py @@ -39,7 +39,7 @@ def cpp_driver(request): target_link_libraries(main OpenMC::libopenmc) target_compile_features(main PUBLIC cxx_std_14) set(CMAKE_CXX_FLAGS "-pedantic-errors") - add_compile_definitions(DAGMC=1) + add_compile_definitions(OPENMC_DAGMC_ENABLED=1) """.format(openmc_dir))) # Create temporary build directory and change to there From 6b672f772f34c804cb729eb84aedb0a1b93576ea Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Sun, 27 Jul 2025 04:42:33 +0700 Subject: [PATCH 380/671] Allow already-initialized openmc.lib in TemporarySession (#3505) --- openmc/deplete/microxs.py | 10 ++-------- openmc/lib/core.py | 12 ++++++++---- openmc/model/model.py | 32 ++++++++++++-------------------- 3 files changed, 22 insertions(+), 32 deletions(-) diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index df78f8a26..4ce199f0c 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -352,7 +352,8 @@ class MicroXS: # Create 3D array for microscopic cross sections microxs_arr = np.zeros((len(nuclides), len(mts), 1)) - def compute_microxs(): + # Compute microscopic cross sections within a temporary session + with openmc.lib.TemporarySession(**init_kwargs): # For each nuclide and reaction, compute the flux-averaged xs for nuc_index, nuc in enumerate(nuclides): if nuc not in nuclides_with_data: @@ -363,13 +364,6 @@ class MicroXS: mt, temperature, energies, multigroup_flux ) - # Compute microscopic cross sections within a temporary session - if not openmc.lib.is_initialized: - with openmc.lib.TemporarySession(**init_kwargs): - compute_microxs() - else: - compute_microxs() - return cls(microxs_arr, nuclides, reactions) @classmethod diff --git a/openmc/lib/core.py b/openmc/lib/core.py index eac0e1784..9f8db69d5 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -654,9 +654,10 @@ class TemporarySession: def __enter__(self): """Initialize the OpenMC library in a temporary directory.""" - # Make sure OpenMC is not already initialized - if openmc.lib.is_initialized: - raise RuntimeError("openmc.lib is already initialized.") + # If already initialized, the context manager is a no-op + self.already_initialized = openmc.lib.is_initialized + if self.already_initialized: + return self # Store original working directory self.orig_dir = Path.cwd() @@ -675,8 +676,11 @@ class TemporarySession: def __exit__(self, exc_type, exc_value, traceback): """Finalize the OpenMC library and clean up temporary directory.""" + if self.already_initialized: + return + try: - openmc.lib.finalize() + finalize() finally: os.chdir(self.orig_dir) self.tmp_dir.cleanup() diff --git a/openmc/model/model.py b/openmc/model/model.py index 93e029a33..b1eff5b7c 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -996,16 +996,13 @@ class Model: plot_obj.v_res = pixels[1] plot_obj.basis = basis - if self.is_initialized: - return openmc.lib.id_map(plot_obj) - else: - # Silence output by default. Also set arguments to start in volume - # calculation mode to avoid loading cross sections - init_kwargs.setdefault('output', False) - init_kwargs.setdefault('args', ['-c']) + # Silence output by default. Also set arguments to start in volume + # calculation mode to avoid loading cross sections + init_kwargs.setdefault('output', False) + init_kwargs.setdefault('args', ['-c']) - with openmc.lib.TemporarySession(self, **init_kwargs): - return openmc.lib.id_map(plot_obj) + with openmc.lib.TemporarySession(self, **init_kwargs): + return openmc.lib.id_map(plot_obj) @add_plot_params def plot( @@ -1229,20 +1226,15 @@ class Model: """ import openmc.lib - if self.is_initialized: + # Silence output by default. Also set arguments to start in volume + # calculation mode to avoid loading cross sections + init_kwargs.setdefault('output', False) + init_kwargs.setdefault('args', ['-c']) + + with openmc.lib.TemporarySession(self, **init_kwargs): return openmc.lib.sample_external_source( n_samples=n_samples, prn_seed=prn_seed ) - else: - # Silence output by default. Also set arguments to start in volume - # calculation mode to avoid loading cross sections - init_kwargs.setdefault('output', False) - init_kwargs.setdefault('args', ['-c']) - - with openmc.lib.TemporarySession(self, **init_kwargs): - return openmc.lib.sample_external_source( - n_samples=n_samples, prn_seed=prn_seed - ) def apply_tally_results(self, statepoint: PathLike | openmc.StatePoint): """Apply results from a statepoint to tally objects on the Model From 836bc487cf13e69a9d2d9ef3f79d3828609df4e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Delaporte-Mathurin?= <40028739+RemDelaporteMathurin@users.noreply.github.com> Date: Tue, 29 Jul 2025 05:34:03 -0400 Subject: [PATCH 381/671] Fix: `materials`, `plots`, and `tallies` cannot be passed as lists (#3513) Co-authored-by: Paul Romano --- openmc/model/model.py | 18 ++++++++++++------ tests/unit_tests/test_model.py | 13 +++++++++++++ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index b1eff5b7c..03fdcda1f 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -70,7 +70,7 @@ class Model: def __init__( self, geometry: openmc.Geometry | None = None, - materials: openmc.Materials = None, + materials: openmc.Materials | None = None, settings: openmc.Settings | None = None, tallies: openmc.Tallies | None = None, plots: openmc.Plots | None = None, @@ -82,7 +82,7 @@ class Model: self.plots = openmc.Plots() if plots is None else plots @property - def geometry(self) -> openmc.Geometry | None: + def geometry(self) -> openmc.Geometry: return self._geometry @geometry.setter @@ -91,7 +91,7 @@ class Model: self._geometry = geometry @property - def materials(self) -> openmc.Materials | None: + def materials(self) -> openmc.Materials: return self._materials @materials.setter @@ -100,12 +100,14 @@ class Model: if isinstance(materials, openmc.Materials): self._materials = materials else: + if not hasattr(self, '_materials'): + self._materials = openmc.Materials() del self._materials[:] for mat in materials: self._materials.append(mat) @property - def settings(self) -> openmc.Settings | None: + def settings(self) -> openmc.Settings: return self._settings @settings.setter @@ -114,7 +116,7 @@ class Model: self._settings = settings @property - def tallies(self) -> openmc.Tallies | None: + def tallies(self) -> openmc.Tallies: return self._tallies @tallies.setter @@ -123,12 +125,14 @@ class Model: if isinstance(tallies, openmc.Tallies): self._tallies = tallies else: + if not hasattr(self, '_tallies'): + self._tallies = openmc.Tallies() del self._tallies[:] for tally in tallies: self._tallies.append(tally) @property - def plots(self) -> openmc.Plots | None: + def plots(self) -> openmc.Plots: return self._plots @plots.setter @@ -137,6 +141,8 @@ class Model: if isinstance(plots, openmc.Plots): self._plots = plots else: + if not hasattr(self, '_plots'): + self._plots = openmc.Plots() del self._plots[:] for plot in plots: self._plots.append(plot) diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 3797188ba..6e4dec00f 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -892,3 +892,16 @@ def test_id_map_aligned_model(): assert tr_cell == 20, f"Expected cell ID 20 at top-right corner, got {tr_cell}" assert tr_instance == 3, f"Expected cell instance 3 at top-right corner, got {tr_instance}" assert tr_material == 5, f"Expected material ID 5 at top-right corner, got {tr_material}" + +def test_setter_from_list(): + mat = openmc.Material() + model = openmc.Model(materials=[mat]) + assert isinstance(model.materials, openmc.Materials) + + tally = openmc.Tally() + model = openmc.Model(tallies=[tally]) + assert isinstance(model.tallies, openmc.Tallies) + + plot = openmc.Plot() + model = openmc.Model(plots=[plot]) + assert isinstance(model.plots, openmc.Plots) From 4cce6ee6c05c3070bb5f256324f064570b2869bc Mon Sep 17 00:00:00 2001 From: John Tramm Date: Tue, 29 Jul 2025 05:15:40 -0500 Subject: [PATCH 382/671] Fix for Weight Window Scaling Bug (#3511) Co-authored-by: Paul Romano --- include/openmc/weight_windows.h | 1 + tests/regression_tests/weightwindows/generators/test.py | 4 ++-- tests/regression_tests/weightwindows/results_true.dat | 2 +- tests/regression_tests/weightwindows/test.py | 6 +++--- tests/testing_harness.py | 2 +- tests/unit_tests/weightwindows/test_ww_gen.py | 2 +- tests/unit_tests/weightwindows/test_wwinp_reader.py | 2 +- 7 files changed, 10 insertions(+), 9 deletions(-) diff --git a/include/openmc/weight_windows.h b/include/openmc/weight_windows.h index 6f2ef0707..763815522 100644 --- a/include/openmc/weight_windows.h +++ b/include/openmc/weight_windows.h @@ -71,6 +71,7 @@ struct WeightWindow { { lower_weight *= factor; upper_weight *= factor; + survival_weight *= factor; } }; diff --git a/tests/regression_tests/weightwindows/generators/test.py b/tests/regression_tests/weightwindows/generators/test.py index d6a40b44f..4dc0ab80d 100644 --- a/tests/regression_tests/weightwindows/generators/test.py +++ b/tests/regression_tests/weightwindows/generators/test.py @@ -46,14 +46,14 @@ def test_ww_generator(run_in_tmpdir): # just test that the generation happens successfully here assert os.path.exists('weight_windows.h5') - wws_mean = openmc.hdf5_to_wws() + wws_mean = openmc.WeightWindowsList.from_hdf5() assert len(wws_mean) == 1 # check that generation using the relative error works too wwg.update_parameters['value'] = 'rel_err' model.run() - wws_rel_err = openmc.hdf5_to_wws() + wws_rel_err = openmc.WeightWindowsList.from_hdf5() assert len(wws_rel_err) == 1 # we should not get the same set of weight windows when switching to use of diff --git a/tests/regression_tests/weightwindows/results_true.dat b/tests/regression_tests/weightwindows/results_true.dat index 671be1d7b..122c5d890 100644 --- a/tests/regression_tests/weightwindows/results_true.dat +++ b/tests/regression_tests/weightwindows/results_true.dat @@ -1 +1 @@ -a5880ad9262e8aba90801783891ee74618144101401f06fb46e954e851a3c517ab28d5f0d6e1b2b364844f08d363cba35ab23b63fc81012ea8a6a328755d56c4 \ No newline at end of file +5df0c08573ccee3fd3495c877c7dc0c4b69c245faf8473b848c89fb837dea7fd437936ffebeb6455c793e66046c46eb0ad6941cc5ef1699fc5fc8a7fcb9b5a0c \ No newline at end of file diff --git a/tests/regression_tests/weightwindows/test.py b/tests/regression_tests/weightwindows/test.py index 864f4f062..cfb651338 100644 --- a/tests/regression_tests/weightwindows/test.py +++ b/tests/regression_tests/weightwindows/test.py @@ -1,4 +1,3 @@ -from copy import deepcopy import pytest import numpy as np @@ -8,6 +7,7 @@ from openmc.stats import Discrete, Point from tests.testing_harness import HashedPyAPITestHarness + @pytest.fixture def model(): model = openmc.Model() @@ -114,7 +114,7 @@ def test_weightwindows(model): def test_wwinp_cylindrical(): - ww = openmc.wwinp_to_wws('ww_n_cyl.txt')[0] + ww = openmc.WeightWindowsList.from_wwinp('ww_n_cyl.txt')[0] mesh = ww.mesh @@ -144,7 +144,7 @@ def test_wwinp_cylindrical(): def test_wwinp_spherical(): - ww = openmc.wwinp_to_wws('ww_n_sph.txt')[0] + ww = openmc.WeightWindowsList.from_wwinp('ww_n_sph.txt')[0] mesh = ww.mesh diff --git a/tests/testing_harness.py b/tests/testing_harness.py index 6de475e6e..11ced5b38 100644 --- a/tests/testing_harness.py +++ b/tests/testing_harness.py @@ -443,7 +443,7 @@ class TolerantPyAPITestHarness(PyAPITestHarness): class WeightWindowPyAPITestHarness(PyAPITestHarness): def _get_results(self): """Digest info in the weight window file and return as a string.""" - ww = openmc.hdf5_to_wws()[0] + ww = openmc.WeightWindowsList.from_hdf5()[0] # Access the weight window bounds lower_bound = ww.lower_ww_bounds diff --git a/tests/unit_tests/weightwindows/test_ww_gen.py b/tests/unit_tests/weightwindows/test_ww_gen.py index 555421461..7ee03f55c 100644 --- a/tests/unit_tests/weightwindows/test_ww_gen.py +++ b/tests/unit_tests/weightwindows/test_ww_gen.py @@ -303,7 +303,7 @@ def test_python_hdf5_roundtrip(run_in_tmpdir, model): openmc.lib.finalize() - wws_hdf5 = openmc.hdf5_to_wws()[0] + wws_hdf5 = openmc.WeightWindowsList.from_hdf5()[0] # ensure assert all(wws.energy_bounds == wws_hdf5.energy_bounds) diff --git a/tests/unit_tests/weightwindows/test_wwinp_reader.py b/tests/unit_tests/weightwindows/test_wwinp_reader.py index 434a36753..28548a448 100644 --- a/tests/unit_tests/weightwindows/test_wwinp_reader.py +++ b/tests/unit_tests/weightwindows/test_wwinp_reader.py @@ -96,7 +96,7 @@ def id_fn(params): def test_wwinp_reader(wwinp_data, request): wwinp_file, mesh, particle_types, energy_bounds = wwinp_data - wws = openmc.wwinp_to_wws(request.node.path.parent / wwinp_file) + wws = openmc.WeightWindowsList.from_wwinp(request.node.path.parent / wwinp_file) for i, ww in enumerate(wws): e_bounds = energy_bounds[i] From 6f5e1347d4bed2af88060c6644b20a6949c7a192 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Tue, 29 Jul 2025 12:17:51 +0100 Subject: [PATCH 383/671] Add test for FW-CADIS based WW generation on a DAGMC model (#3504) Co-authored-by: Jon Shimwell Co-authored-by: Patrick Shriwise Co-authored-by: Paul Romano --- src/dagmc.cpp | 2 +- .../dagmc/dagmc_tetrahedral_no_graveyard.h5m | Bin 0 -> 31448 bytes tests/unit_tests/weightwindows/test_ww_gen.py | 70 ++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 tests/unit_tests/dagmc/dagmc_tetrahedral_no_graveyard.h5m diff --git a/src/dagmc.cpp b/src/dagmc.cpp index 8c81952a7..b2ebe89c0 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -672,7 +672,7 @@ std::pair DAGCell::distance( p->last_dir() = u; p->history().reset(); } - if (on_surface == 0) { + if (on_surface == SURFACE_NONE) { p->history().reset(); } diff --git a/tests/unit_tests/dagmc/dagmc_tetrahedral_no_graveyard.h5m b/tests/unit_tests/dagmc/dagmc_tetrahedral_no_graveyard.h5m new file mode 100644 index 0000000000000000000000000000000000000000..2aa72956b812354bbf64350252e8d04ca160ad4d GIT binary patch literal 31448 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 literal 0 HcmV?d00001 diff --git a/tests/unit_tests/weightwindows/test_ww_gen.py b/tests/unit_tests/weightwindows/test_ww_gen.py index 7ee03f55c..a4456e680 100644 --- a/tests/unit_tests/weightwindows/test_ww_gen.py +++ b/tests/unit_tests/weightwindows/test_ww_gen.py @@ -1,3 +1,4 @@ +import copy from itertools import permutations from pathlib import Path @@ -332,3 +333,72 @@ def test_ww_bounds_set_in_memory(run_in_tmpdir, model): wws.bounds = (bounds, bounds) openmc.lib.finalize() + + +@pytest.mark.skipif(not openmc.lib._dagmc_enabled(), reason="DAGMC CAD geometry is not enabled.") +def test_ww_generation_with_dagmc(run_in_tmpdir): + mat1 = openmc.Material(name="1") + mat1.add_nuclide("H1", 1, percent_type="ao") + mat1.set_density("g/cm3", 0.001) + + materials = openmc.Materials([mat1]) + dag_univ = openmc.DAGMCUniverse( + Path(__file__).parent.parent / "dagmc" / "dagmc_tetrahedral_no_graveyard.h5m") + bound_dag_univ = dag_univ.bounded_universe(padding_distance=1) + geometry = openmc.Geometry(bound_dag_univ) + + settings = openmc.Settings() + settings.batches = 6 + settings.particles = 30 + settings.run_mode = "fixed source" + + # Create a point source which are supported by random ray mode + my_source = openmc.IndependentSource() + my_source.space = openmc.stats.Point((0.25, 0.25, 0.25)) + my_source.energy = openmc.stats.delta_function(14e6) + settings.source = my_source + + model = openmc.Model(geometry, materials, settings) + + rr_model = copy.deepcopy(model) + rr_model.settings.inactive = 3 + + rr_model.convert_to_multigroup( + method="stochastic_slab", + overwrite_mgxs_library=True, + nparticles=10, + groups="CASMO-2" + ) + + rr_model.convert_to_random_ray() + + mesh = openmc.RegularMesh.from_domain(rr_model, dimension=(4, 4, 4)) + + # avoid writing files we don't make use of + rr_model.settings.output = {"summary": False, "tallies": False} + + # Subdivide random ray source regions + rr_model.settings.random_ray["source_region_meshes"] = [ + (mesh, [rr_model.geometry.root_universe]) + ] + + # less likely to get negative values in the weight window + rr_model.settings.random_ray["volume_estimator"] = "naive" + + # Add a weight window generator to the model + rr_model.settings.weight_window_generators = openmc.WeightWindowGenerator( + method="fw_cadis", + mesh=mesh, + max_realizations=42, + particle_type='neutron', + energy_bounds=[0.0, 100e6] + ) + + rr_model.run() + + model.settings.weight_windows_on = True + model.settings.weight_window_checkpoints = {"collision": True, "surface": True} + model.settings.survival_biasing = False + model.settings.weight_windows = openmc.WeightWindowsList.from_hdf5() + + model.run() From a64cc96ed5acfd063598cc8fc57f4ff0c9285eed Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Wed, 30 Jul 2025 20:10:34 +0300 Subject: [PATCH 384/671] Fixed a bug in distribcell offsets logic (#3424) --- include/openmc/cell.h | 13 ++- include/openmc/geometry_aux.h | 12 +- include/openmc/lattice.h | 2 +- include/openmc/universe.h | 1 + src/cell.cpp | 13 ++- src/finalize.cpp | 1 - src/geometry_aux.cpp | 85 +++++--------- src/lattice.cpp | 5 +- src/random_ray/flat_source_domain.cpp | 10 +- src/tallies/filter_distribcell.cpp | 2 +- .../lattice_distribmat/False/inputs_true.dat | 74 ++++++++++++ .../lattice_distribmat/False/results_true.dat | 2 + .../lattice_distribmat/True/inputs_true.dat | 107 ++++++++++++++++++ .../lattice_distribmat/True/results_true.dat | 2 + .../lattice_distribmat/__init__.py | 0 .../lattice_distribmat/test.py | 83 ++++++++++++++ 16 files changed, 325 insertions(+), 87 deletions(-) create mode 100644 tests/regression_tests/lattice_distribmat/False/inputs_true.dat create mode 100644 tests/regression_tests/lattice_distribmat/False/results_true.dat create mode 100644 tests/regression_tests/lattice_distribmat/True/inputs_true.dat create mode 100644 tests/regression_tests/lattice_distribmat/True/results_true.dat create mode 100644 tests/regression_tests/lattice_distribmat/__init__.py create mode 100644 tests/regression_tests/lattice_distribmat/test.py diff --git a/include/openmc/cell.h b/include/openmc/cell.h index 98709fb33..e9fcfe391 100644 --- a/include/openmc/cell.h +++ b/include/openmc/cell.h @@ -226,6 +226,8 @@ public: void set_temperature( double T, int32_t instance = -1, bool set_contained = false); + int32_t n_instances() const; + //! Set the rotation matrix of a cell instance //! \param[in] rot The rotation matrix of length 3 or 9 void set_rotation(const vector& rot); @@ -312,12 +314,11 @@ public: //---------------------------------------------------------------------------- // Data members - int32_t id_; //!< Unique ID - std::string name_; //!< User-defined name - Fill type_; //!< Material, universe, or lattice - int32_t universe_; //!< Universe # this cell is in - int32_t fill_; //!< Universe # filling this cell - int32_t n_instances_ {0}; //!< Number of instances of this cell + int32_t id_; //!< Unique ID + std::string name_; //!< User-defined name + Fill type_; //!< Material, universe, or lattice + int32_t universe_; //!< Universe # this cell is in + int32_t fill_; //!< Universe # filling this cell //! \brief Index corresponding to this cell in distribcell arrays int distribcell_index_ {C_NONE}; diff --git a/include/openmc/geometry_aux.h b/include/openmc/geometry_aux.h index 0c870c9fb..f60f2d649 100644 --- a/include/openmc/geometry_aux.h +++ b/include/openmc/geometry_aux.h @@ -15,8 +15,6 @@ namespace openmc { namespace model { -extern std::unordered_map> - universe_cell_counts; extern std::unordered_map universe_level_counts; } // namespace model @@ -80,15 +78,13 @@ void prepare_distribcell( const std::vector* user_distribcells = nullptr); //============================================================================== -//! Recursively search through the geometry and count cell instances. +//! Recursively search through the geometry and count universe instances. //! -//! This function will update the Cell::n_instances value for each cell in the -//! geometry. -//! \param univ_indx The index of the universe to begin searching from (probably -//! the root universe). +//! This function will update Universe.n_instances_ for each +//! universe in the geometry. //============================================================================== -void count_cell_instances(int32_t univ_indx); +void count_universe_instances(); //============================================================================== //! Recursively search through universes and count universe instances. diff --git a/include/openmc/lattice.h b/include/openmc/lattice.h index fb374ab75..f87d28b21 100644 --- a/include/openmc/lattice.h +++ b/include/openmc/lattice.h @@ -76,7 +76,7 @@ public: } //! Populate the distribcell offset tables. - int32_t fill_offset_table(int32_t offset, int32_t target_univ_id, int map, + int32_t fill_offset_table(int32_t target_univ_id, int map, std::unordered_map& univ_count_memo); //! \brief Check lattice indices. diff --git a/include/openmc/universe.h b/include/openmc/universe.h index 52df432b8..b7450224f 100644 --- a/include/openmc/universe.h +++ b/include/openmc/universe.h @@ -29,6 +29,7 @@ class Universe { public: int32_t id_; //!< Unique ID vector cells_; //!< Cells within this universe + int32_t n_instances_; //!< Number of instances of this universe //! \brief Write universe information to an HDF5 group. //! \param group_id An HDF5 group id. diff --git a/src/cell.cpp b/src/cell.cpp index b6862ba17..d838bfbb4 100644 --- a/src/cell.cpp +++ b/src/cell.cpp @@ -40,6 +40,11 @@ vector> cells; // Cell implementation //============================================================================== +int32_t Cell::n_instances() const +{ + return model::universes[universe_]->n_instances_; +} + void Cell::set_rotation(const vector& rot) { if (fill_ == C_NONE) { @@ -115,8 +120,8 @@ void Cell::set_temperature(double T, int32_t instance, bool set_contained) if (type_ == Fill::MATERIAL) { if (instance >= 0) { // If temperature vector is not big enough, resize it first - if (sqrtkT_.size() != n_instances_) - sqrtkT_.resize(n_instances_, sqrtkT_[0]); + if (sqrtkT_.size() != n_instances()) + sqrtkT_.resize(n_instances(), sqrtkT_[0]); // Set temperature for the corresponding instance sqrtkT_.at(instance) = std::sqrt(K_BOLTZMANN * T); @@ -170,7 +175,7 @@ void Cell::import_properties_hdf5(hid_t group) // Ensure number of temperatures makes sense auto n_temps = temps.size(); - if (n_temps > 1 && n_temps != n_instances_) { + if (n_temps > 1 && n_temps != n_instances()) { throw std::runtime_error(fmt::format( "Number of temperatures for cell {} doesn't match number of instances", id_)); @@ -1618,7 +1623,7 @@ extern "C" int openmc_cell_get_num_instances( set_errmsg("Index in cells array is out of bounds."); return OPENMC_E_OUT_OF_BOUNDS; } - *num_instances = model::cells[index]->n_instances_; + *num_instances = model::cells[index]->n_instances(); return 0; } diff --git a/src/finalize.cpp b/src/finalize.cpp index 2d14cdec8..e38b0b251 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -185,7 +185,6 @@ int openmc_finalize() int openmc_reset() { - model::universe_cell_counts.clear(); model::universe_level_counts.clear(); for (auto& t : model::tallies) { diff --git a/src/geometry_aux.cpp b/src/geometry_aux.cpp index 2f8a55743..51732cf8b 100644 --- a/src/geometry_aux.cpp +++ b/src/geometry_aux.cpp @@ -25,21 +25,9 @@ namespace openmc { namespace model { -std::unordered_map> - universe_cell_counts; std::unordered_map universe_level_counts; } // namespace model -// adds the cell counts of universe b to universe a -void update_universe_cell_count(int32_t a, int32_t b) -{ - auto& universe_a_counts = model::universe_cell_counts[a]; - const auto& universe_b_counts = model::universe_cell_counts[b]; - for (const auto& it : universe_b_counts) { - universe_a_counts[it.first] += it.second; - } -} - void read_geometry_xml() { // Display output message @@ -263,7 +251,7 @@ void finalize_geometry() { // Perform some final operations to set up the geometry adjust_indices(); - count_cell_instances(model::root_universe); + count_universe_instances(); partition_universes(); // Assign temperatures to cells that don't have temperatures already assigned @@ -356,35 +344,38 @@ void prepare_distribcell(const std::vector* user_distribcells) Cell& c {*model::cells[i]}; if (c.material_.size() > 1) { - if (c.material_.size() != c.n_instances_) { + if (c.material_.size() != c.n_instances()) { fatal_error(fmt::format( "Cell {} was specified with {} materials but has {} distributed " "instances. The number of materials must equal one or the number " "of instances.", - c.id_, c.material_.size(), c.n_instances_)); + c.id_, c.material_.size(), c.n_instances())); } } if (c.sqrtkT_.size() > 1) { - if (c.sqrtkT_.size() != c.n_instances_) { + if (c.sqrtkT_.size() != c.n_instances()) { fatal_error(fmt::format( "Cell {} was specified with {} temperatures but has {} distributed " "instances. The number of temperatures must equal one or the number " "of instances.", - c.id_, c.sqrtkT_.size(), c.n_instances_)); + c.id_, c.sqrtkT_.size(), c.n_instances())); } } } // Search through universes for material cells and assign each one a - // unique distribcell array index. - int distribcell_index = 0; + // distribcell array index according to the containing universe. vector target_univ_ids; for (const auto& u : model::universes) { for (auto idx : u->cells_) { if (distribcells.find(idx) != distribcells.end()) { - model::cells[idx]->distribcell_index_ = distribcell_index++; - target_univ_ids.push_back(u->id_); + if (!contains(target_univ_ids, u->id_)) { + target_univ_ids.push_back(u->id_); + } + model::cells[idx]->distribcell_index_ = + std::find(target_univ_ids.begin(), target_univ_ids.end(), u->id_) - + target_univ_ids.begin(); } } } @@ -419,8 +410,7 @@ void prepare_distribcell(const std::vector* user_distribcells) } else if (c.type_ == Fill::LATTICE) { c.offset_[map] = offset; Lattice& lat = *model::lattices[c.fill_]; - offset += - lat.fill_offset_table(offset, target_univ_id, map, univ_count_memo); + offset += lat.fill_offset_table(target_univ_id, map, univ_count_memo); } } } @@ -429,32 +419,12 @@ void prepare_distribcell(const std::vector* user_distribcells) //============================================================================== -void count_cell_instances(int32_t univ_indx) +void count_universe_instances() { - const auto univ_counts = model::universe_cell_counts.find(univ_indx); - if (univ_counts != model::universe_cell_counts.end()) { - for (const auto& it : univ_counts->second) { - model::cells[it.first]->n_instances_ += it.second; - } - } else { - for (int32_t cell_indx : model::universes[univ_indx]->cells_) { - Cell& c = *model::cells[cell_indx]; - ++c.n_instances_; - model::universe_cell_counts[univ_indx][cell_indx] += 1; - - if (c.type_ == Fill::UNIVERSE) { - // This cell contains another universe. Recurse into that universe. - count_cell_instances(c.fill_); - update_universe_cell_count(univ_indx, c.fill_); - } else if (c.type_ == Fill::LATTICE) { - // This cell contains a lattice. Recurse into the lattice universes. - Lattice& lat = *model::lattices[c.fill_]; - for (auto it = lat.begin(); it != lat.end(); ++it) { - count_cell_instances(*it); - update_universe_cell_count(univ_indx, *it); - } - } - } + for (auto& univ : model::universes) { + std::unordered_map univ_count_memo; + univ->n_instances_ = count_universe_instances( + model::root_universe, univ->id_, univ_count_memo); } } @@ -528,19 +498,16 @@ std::string distribcell_path_inner(int32_t target_cell, int32_t map, // Material cells don't contain other cells so ignore them. if (c.type_ != Fill::MATERIAL) { - int32_t temp_offset; - if (c.type_ == Fill::UNIVERSE) { - temp_offset = - offset + c.offset_[map]; // TODO: should also apply to lattice fills? - } else { + int32_t temp_offset = offset + c.offset_[map]; + if (c.type_ == Fill::LATTICE) { Lattice& lat = *model::lattices[c.fill_]; int32_t indx = lat.universes_.size() * map + lat.begin().indx_; - temp_offset = offset + lat.offsets_[indx]; + temp_offset += lat.offsets_[indx]; } // The desired cell is the first cell that gives an offset smaller or // equal to the target offset. - if (temp_offset <= target_offset - c.offset_[map]) + if (temp_offset <= target_offset) break; } } @@ -570,12 +537,12 @@ std::string distribcell_path_inner(int32_t target_cell, int32_t map, path << "l" << lat.id_; for (ReverseLatticeIter it = lat.rbegin(); it != lat.rend(); ++it) { int32_t indx = lat.universes_.size() * map + it.indx_; - int32_t temp_offset = offset + lat.offsets_[indx]; - if (temp_offset <= target_offset - c.offset_[map]) { + int32_t temp_offset = offset + lat.offsets_[indx] + c.offset_[map]; + if (temp_offset <= target_offset) { offset = temp_offset; path << "(" << lat.index_to_string(it.indx_) << ")->"; - path << distribcell_path_inner(target_cell, map, target_offset, - *model::universes[*it], offset + c.offset_[map]); + path << distribcell_path_inner( + target_cell, map, target_offset, *model::universes[*it], offset); return path.str(); } } diff --git a/src/lattice.cpp b/src/lattice.cpp index 73005ad09..efbfcb216 100644 --- a/src/lattice.cpp +++ b/src/lattice.cpp @@ -103,8 +103,8 @@ void Lattice::adjust_indices() //============================================================================== -int32_t Lattice::fill_offset_table(int32_t offset, int32_t target_univ_id, - int map, std::unordered_map& univ_count_memo) +int32_t Lattice::fill_offset_table(int32_t target_univ_id, int map, + std::unordered_map& univ_count_memo) { // If the offsets have already been determined for this "map", don't bother // recalculating all of them and just return the total offset. Note that the @@ -119,6 +119,7 @@ int32_t Lattice::fill_offset_table(int32_t offset, int32_t target_univ_id, count_universe_instances(last_univ, target_univ_id, univ_count_memo); } + int32_t offset = 0; for (LatticeIter it = begin(); it != end(); ++it) { offsets_[map * universes_.size() + it.indx_] = offset; offset += count_universe_instances(*it, target_univ_id, univ_count_memo); diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index b04b72981..409238830 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -46,7 +46,7 @@ FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) source_region_offsets_.push_back(-1); } else { source_region_offsets_.push_back(base_source_regions); - base_source_regions += c->n_instances_; + base_source_regions += c->n_instances(); } } @@ -61,7 +61,7 @@ FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) for (int i = 0; i < model::cells.size(); i++) { Cell& cell = *model::cells[i]; if (cell.type_ == Fill::MATERIAL) { - for (int j = 0; j < cell.n_instances_; j++) { + for (int j = 0; j < cell.n_instances(); j++) { source_regions_.material(source_region_id++) = cell.material(j); } } @@ -1014,7 +1014,7 @@ void FlatSourceDomain::apply_external_source_to_cell_and_children( Cell& cell = *model::cells[i_cell]; if (cell.type_ == Fill::MATERIAL) { - vector instances(cell.n_instances_); + vector instances(cell.n_instances()); std::iota(instances.begin(), instances.end(), 0); apply_external_source_to_cell_instances( i_cell, discrete, strength_factor, target_material_id, instances); @@ -1343,12 +1343,12 @@ void FlatSourceDomain::apply_mesh_to_cell_and_children(int32_t i_cell, Cell& cell = *model::cells[i_cell]; if (cell.type_ == Fill::MATERIAL) { - vector instances(cell.n_instances_); + vector instances(cell.n_instances()); std::iota(instances.begin(), instances.end(), 0); apply_mesh_to_cell_instances( i_cell, mesh_idx, target_material_id, instances, is_target_void); } else if (target_material_id == C_NONE && !is_target_void) { - for (int j = 0; j < cell.n_instances_; j++) { + for (int j = 0; j < cell.n_instances(); j++) { std::unordered_map> cell_instance_list = cell.get_contained_cells(j, nullptr); for (const auto& pair : cell_instance_list) { diff --git a/src/tallies/filter_distribcell.cpp b/src/tallies/filter_distribcell.cpp index 02e88dde2..f511a6816 100644 --- a/src/tallies/filter_distribcell.cpp +++ b/src/tallies/filter_distribcell.cpp @@ -34,7 +34,7 @@ void DistribcellFilter::set_cell(int32_t cell) assert(cell >= 0); assert(cell < model::cells.size()); cell_ = cell; - n_bins_ = model::cells[cell]->n_instances_; + n_bins_ = model::cells[cell]->n_instances(); } void DistribcellFilter::get_all_bins( diff --git a/tests/regression_tests/lattice_distribmat/False/inputs_true.dat b/tests/regression_tests/lattice_distribmat/False/inputs_true.dat new file mode 100644 index 000000000..d39a10369 --- /dev/null +++ b/tests/regression_tests/lattice_distribmat/False/inputs_true.dat @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 1.0 1.0 + 1 + 1 1 + -1.0 -1.0 + +1 + + + 1.0 1.0 + 1 + 1 1 + -1.0 0 + +1 + + + 1.0 1.0 + 1 + 1 1 + 0 -1.0 + +1 + + + 1.0 1.0 + 1 + 1 1 + 0 0 + +1 + + + + + + + + + + + + + + + eigenvalue + 1000 + 10 + 5 + + diff --git a/tests/regression_tests/lattice_distribmat/False/results_true.dat b/tests/regression_tests/lattice_distribmat/False/results_true.dat new file mode 100644 index 000000000..45a9ddb0a --- /dev/null +++ b/tests/regression_tests/lattice_distribmat/False/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +1.863277E+00 1.289821E-02 diff --git a/tests/regression_tests/lattice_distribmat/True/inputs_true.dat b/tests/regression_tests/lattice_distribmat/True/inputs_true.dat new file mode 100644 index 000000000..450306a0c --- /dev/null +++ b/tests/regression_tests/lattice_distribmat/True/inputs_true.dat @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1.0 1.0 + 8 + 1 1 + -1.0 -1.0 + +8 + + + 1.0 1.0 + 8 + 1 1 + -1.0 0 + +8 + + + 1.0 1.0 + 8 + 1 1 + 0 -1.0 + +8 + + + 1.0 1.0 + 8 + 1 1 + 0 0 + +8 + + + + + + + + + + + + + + + eigenvalue + 1000 + 10 + 5 + + diff --git a/tests/regression_tests/lattice_distribmat/True/results_true.dat b/tests/regression_tests/lattice_distribmat/True/results_true.dat new file mode 100644 index 000000000..45a9ddb0a --- /dev/null +++ b/tests/regression_tests/lattice_distribmat/True/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +1.863277E+00 1.289821E-02 diff --git a/tests/regression_tests/lattice_distribmat/__init__.py b/tests/regression_tests/lattice_distribmat/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/lattice_distribmat/test.py b/tests/regression_tests/lattice_distribmat/test.py new file mode 100644 index 000000000..4d0b6e156 --- /dev/null +++ b/tests/regression_tests/lattice_distribmat/test.py @@ -0,0 +1,83 @@ +import numpy as np +import openmc +from openmc.utility_funcs import change_directory +import pytest + +from tests.testing_harness import PyAPITestHarness + + +@pytest.fixture +def model(): + model = openmc.model.Model() + + uo2 = openmc.Material(name='UO2') + uo2.set_density('g/cm3', 10.0) + uo2.add_nuclide('U235', 1.0) + uo2.add_nuclide('O16', 2.0) + water = openmc.Material(name='light water') + water.add_nuclide('H1', 2.0) + water.add_nuclide('O16', 1.0) + water.set_density('g/cm3', 1.0) + water.add_s_alpha_beta('c_H_in_H2O') + model.materials.extend([uo2, water]) + + cyl = openmc.ZCylinder(r=0.4) + pin = openmc.model.pin([cyl], [uo2, water]) + d = 1.0 + + lattice00 = openmc.RectLattice() + lattice00.lower_left = (-d, -d) + lattice00.pitch = (d, d) + lattice00.outer = pin + lattice00.universes = [[pin]] + box00 = openmc.model.RectangularPrism(d, d, origin=(-d/2,-d/2)) + + lattice01 = openmc.RectLattice() + lattice01.lower_left = (-d, 0) + lattice01.pitch = (d, d) + lattice01.outer = pin + lattice01.universes = [[pin]] + box01 = openmc.model.RectangularPrism(d, d, origin=(-d/2,d/2)) + + lattice10 = openmc.RectLattice() + lattice10.lower_left = (0, -d) + lattice10.pitch = (d, d) + lattice10.outer = pin + lattice10.universes = [[pin]] + box10 = openmc.model.RectangularPrism(d, d, origin=(d/2,-d/2)) + + lattice11 = openmc.RectLattice() + lattice11.lower_left = (0, 0) + lattice11.pitch = (d, d) + lattice11.outer = pin + lattice11.universes = [[pin]] + box11 = openmc.model.RectangularPrism(d, d, origin=(d/2,d/2)) + + + cell00 = openmc.Cell(fill=lattice00, region = -box00) + cell01 = openmc.Cell(fill=lattice01, region = -box01) + cell10 = openmc.Cell(fill=lattice10, region = -box10) + cell11 = openmc.Cell(fill=lattice11, region = -box11) + + univ = openmc.Universe(cells=[cell00, cell01, cell10, cell11]) + + box = openmc.model.RectangularPrism(2*d, 2*d, boundary_type='reflective') + + main_cell = openmc.Cell(fill=univ, region=-box) + model.geometry = openmc.Geometry([main_cell]) + model.geometry.merge_surfaces = True + + model.settings.batches = 10 + model.settings.inactive = 5 + model.settings.particles = 1000 + + return model + +@pytest.mark.parametrize("distribmat", [False, True]) +def test_lattice(model, distribmat): + with change_directory(str(distribmat)): + openmc.reset_auto_ids() + if distribmat: + model.differentiate_mats(depletable_only=False) + harness = PyAPITestHarness('statepoint.10.h5', model) + harness.main() From 4fabed542da3d2e37eeba31872987e090b6ffc4b Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Fri, 8 Aug 2025 03:53:33 +0300 Subject: [PATCH 385/671] fixed a bug in MeshMaterialFilter.from_volumes (#3520) Co-authored-by: Paul Romano --- openmc/filter.py | 7 ++++++- tests/unit_tests/test_filter_meshmaterial.py | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/openmc/filter.py b/openmc/filter.py index f29c54085..53fec898f 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -1082,7 +1082,12 @@ class MeshMaterialFilter(MeshFilter): A new MeshMaterialFilter instance """ - bins = list(zip(*np.where(volumes._materials > -1))) + # Get flat arrays of material IDs and element indices + mat_ids = volumes._materials[volumes._materials > -1] + elems, _ = np.where(volumes._materials > -1) + + # Stack them into a 2D array of (element, material) pairs + bins = np.column_stack((elems, mat_ids)) return cls(mesh, bins) def __hash__(self): diff --git a/tests/unit_tests/test_filter_meshmaterial.py b/tests/unit_tests/test_filter_meshmaterial.py index 120533209..a05ed5a12 100644 --- a/tests/unit_tests/test_filter_meshmaterial.py +++ b/tests/unit_tests/test_filter_meshmaterial.py @@ -9,6 +9,7 @@ def test_filter_mesh_material(run_in_tmpdir): materials = [] for i in range(4): mat = openmc.Material() + mat.id = 10*(i+1) mat.add_nuclide('Fe56', 1.0) materials.append(mat) @@ -35,7 +36,7 @@ def test_filter_mesh_material(run_in_tmpdir): # MeshMaterialFilter with corresponding bins vols = mesh.material_volumes(model) mmf = openmc.MeshMaterialFilter.from_volumes(mesh, vols) - expected_bins = [(0, 1), (1, 1), (1, 2), (2, 2), (2, 3), (3, 3), (3, 4), (4, 4)] + expected_bins = [(0, 10), (1, 10), (1, 20), (2, 20), (2, 30), (3, 40), (3, 30), (4, 40)] np.testing.assert_equal(mmf.bins, expected_bins) # Create two tallies, one with a mesh filter and one with mesh-material From 4500f07b44daec71f38d0d9b73736e41f9d23815 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Fri, 8 Aug 2025 12:59:22 +0300 Subject: [PATCH 386/671] Remove reorder_attributes from openmc._xml (#3519) --- openmc/_xml.py | 17 - openmc/data/library.py | 3 +- openmc/geometry.py | 1 - openmc/material.py | 4 +- openmc/plots.py | 4 +- openmc/settings.py | 3 +- openmc/tallies.py | 3 +- .../adj_cell_rotation/inputs_true.dat | 32 +- .../asymmetric_lattice/inputs_true.dat | 92 +- .../cpp_driver/inputs_true.dat | 32 +- .../create_fission_neutrons/inputs_true.dat | 26 +- .../dagmc/external/inputs_true.dat | 16 +- .../dagmc/legacy/inputs_true.dat | 16 +- .../dagmc/refl/inputs_true.dat | 4 +- .../dagmc/universes/inputs_true.dat | 50 +- .../dagmc/uwuw/inputs_true.dat | 4 +- .../last_step_reference_materials.xml | 2082 ++++++++--------- .../diff_tally/inputs_true.dat | 116 +- .../distribmat/inputs_true.dat | 36 +- .../eigenvalue_genperbatch/inputs_true.dat | 10 +- .../energy_cutoff/inputs_true.dat | 22 +- .../energy_laws/inputs_true.dat | 16 +- .../external_moab/inputs_true.dat | 72 +- .../filter_cellfrom/inputs_true.dat | 64 +- .../filter_cellinstance/inputs_true.dat | 28 +- .../filter_energyfun/inputs_true.dat | 8 +- .../filter_mesh/inputs_true.dat | 30 +- .../filter_meshborn/inputs_true.dat | 8 +- .../filter_musurface/inputs_true.dat | 14 +- .../filter_translations/inputs_true.dat | 34 +- .../fixed_source/inputs_true.dat | 12 +- tests/regression_tests/ifp/inputs_true.dat | 10 +- .../iso_in_lab/inputs_true.dat | 106 +- .../lattice_distribmat/False/inputs_true.dat | 46 +- .../lattice_distribmat/True/inputs_true.dat | 88 +- .../lattice_hex_coincident/inputs_true.dat | 70 +- .../lattice_hex_x/inputs_true.dat | 70 +- .../lattice_multiple/inputs_true.dat | 30 +- .../lattice_rotated/inputs_true.dat | 34 +- .../regression_tests/mg_basic/inputs_true.dat | 30 +- .../mg_basic_delayed/inputs_true.dat | 28 +- .../mg_convert/inputs_true.dat | 14 +- .../mg_legendre/inputs_true.dat | 8 +- .../mg_max_order/inputs_true.dat | 8 +- .../mg_survival_biasing/inputs_true.dat | 8 +- .../mg_tallies/inputs_true.dat | 8 +- .../mg_temperature_multi/inputs_true.dat | 24 +- .../mgxs_library_ce_to_mg/inputs_true.dat | 54 +- .../inputs_true.dat | 54 +- .../mgxs_library_condense/inputs_true.dat | 54 +- .../mgxs_library_correction/inputs_true.dat | 54 +- .../mgxs_library_distribcell/inputs_true.dat | 62 +- .../mgxs_library_hdf5/inputs_true.dat | 54 +- .../mgxs_library_histogram/inputs_true.dat | 54 +- .../mgxs_library_mesh/inputs_true.dat | 30 +- .../mgxs_library_no_nuclides/inputs_true.dat | 54 +- .../mgxs_library_nuclides/inputs_true.dat | 54 +- .../inputs_true.dat | 54 +- .../adj_cell_rotation_inputs_true.dat | 32 +- .../model_xml/energy_laws_inputs_true.dat | 16 +- .../lattice_multiple_inputs_true.dat | 30 +- .../photon_production_inputs_true.dat | 16 +- .../multipole/inputs_true.dat | 26 +- .../regression_tests/ncrystal/inputs_true.dat | 14 +- .../regression_tests/periodic/inputs_true.dat | 28 +- .../periodic_6fold/inputs_true.dat | 22 +- .../periodic_hex/inputs_true.dat | 18 +- .../photon_production/inputs_true.dat | 16 +- .../photon_production_fission/inputs_true.dat | 10 +- .../photon_source/inputs_true.dat | 14 +- .../pulse_height/inputs_true.dat | 16 +- .../inputs_true.dat | 32 +- .../random_ray_adjoint_k_eff/inputs_true.dat | 74 +- .../infinite_medium/inputs_true.dat | 30 +- .../material_wise/inputs_true.dat | 30 +- .../stochastic_slab/inputs_true.dat | 30 +- .../inputs_true.dat | 30 +- .../cell/inputs_true.dat | 32 +- .../material/inputs_true.dat | 32 +- .../universe/inputs_true.dat | 32 +- .../linear/inputs_true.dat | 32 +- .../linear_xy/inputs_true.dat | 32 +- .../flat/inputs_true.dat | 32 +- .../linear/inputs_true.dat | 32 +- .../False/inputs_true.dat | 32 +- .../True/inputs_true.dat | 32 +- .../flat/inputs_true.dat | 80 +- .../linear_xy/inputs_true.dat | 80 +- .../random_ray_halton_samples/inputs_true.dat | 74 +- .../random_ray_k_eff/inputs_true.dat | 74 +- .../random_ray_k_eff_mesh/inputs_true.dat | 74 +- .../random_ray_linear/linear/inputs_true.dat | 74 +- .../linear_xy/inputs_true.dat | 74 +- .../inputs_true.dat | 32 +- .../random_ray_void/flat/inputs_true.dat | 32 +- .../random_ray_void/linear/inputs_true.dat | 32 +- .../hybrid/inputs_true.dat | 32 +- .../naive/inputs_true.dat | 32 +- .../simulation_averaged/inputs_true.dat | 32 +- .../hybrid/inputs_true.dat | 32 +- .../naive/inputs_true.dat | 32 +- .../simulation_averaged/inputs_true.dat | 32 +- .../resonance_scattering/inputs_true.dat | 16 +- .../salphabeta/inputs_true.dat | 60 +- .../score_current/inputs_true.dat | 30 +- tests/regression_tests/source/inputs_true.dat | 102 +- .../source_dlopen/inputs_true.dat | 14 +- .../inputs_true.dat | 14 +- tests/regression_tests/stride/inputs_true.dat | 10 +- .../surface_source/inputs_true_read_h5.dat | 8 +- .../surface_source/inputs_true_read_mcpl.dat | 8 +- .../surface_source/inputs_true_write_h5.dat | 10 +- .../surface_source/inputs_true_write_mcpl.dat | 10 +- .../case-01/inputs_true.dat | 54 +- .../case-02/inputs_true.dat | 54 +- .../case-03/inputs_true.dat | 54 +- .../case-04/inputs_true.dat | 54 +- .../case-05/inputs_true.dat | 54 +- .../case-06/inputs_true.dat | 54 +- .../case-07/inputs_true.dat | 54 +- .../case-08/inputs_true.dat | 54 +- .../case-09/inputs_true.dat | 54 +- .../case-10/inputs_true.dat | 54 +- .../case-11/inputs_true.dat | 54 +- .../case-12/inputs_true.dat | 42 +- .../case-13/inputs_true.dat | 42 +- .../case-14/inputs_true.dat | 42 +- .../case-15/inputs_true.dat | 42 +- .../case-16/inputs_true.dat | 42 +- .../case-17/inputs_true.dat | 42 +- .../case-18/inputs_true.dat | 42 +- .../case-19/inputs_true.dat | 42 +- .../case-20/inputs_true.dat | 42 +- .../case-21/inputs_true.dat | 42 +- .../case-a01/inputs_true.dat | 54 +- .../case-d01/inputs_true.dat | 16 +- .../case-d02/inputs_true.dat | 16 +- .../case-d03/inputs_true.dat | 16 +- .../case-d04/inputs_true.dat | 16 +- .../case-d05/inputs_true.dat | 16 +- .../case-d06/inputs_true.dat | 16 +- .../case-d07/inputs_true.dat | 42 +- .../case-d08/inputs_true.dat | 42 +- .../case-e01/inputs_true.dat | 54 +- .../case-e02/inputs_true.dat | 54 +- .../case-e03/inputs_true.dat | 42 +- .../surface_tally/inputs_true.dat | 36 +- .../regression_tests/tallies/inputs_true.dat | 108 +- .../tally_aggregation/inputs_true.dat | 30 +- .../tally_arithmetic/inputs_true.dat | 20 +- .../tally_slice_merge/inputs_true.dat | 106 +- .../time_cutoff/inputs_true.dat | 4 +- tests/regression_tests/torus/inputs_true.dat | 28 +- .../torus/large_major/inputs_true.dat | 16 +- .../inputs_true.dat | 8 +- tests/regression_tests/triso/inputs_true.dat | 718 +++--- .../unstructured_mesh/inputs_true.dat | 74 +- .../unstructured_mesh/inputs_true0.dat | 74 +- .../unstructured_mesh/inputs_true1.dat | 74 +- .../unstructured_mesh/inputs_true10.dat | 74 +- .../unstructured_mesh/inputs_true11.dat | 74 +- .../unstructured_mesh/inputs_true12.dat | 74 +- .../unstructured_mesh/inputs_true13.dat | 74 +- .../unstructured_mesh/inputs_true14.dat | 74 +- .../unstructured_mesh/inputs_true15.dat | 74 +- .../unstructured_mesh/inputs_true2.dat | 74 +- .../unstructured_mesh/inputs_true3.dat | 74 +- .../unstructured_mesh/inputs_true4.dat | 101 +- .../unstructured_mesh/inputs_true5.dat | 101 +- .../unstructured_mesh/inputs_true6.dat | 101 +- .../unstructured_mesh/inputs_true7.dat | 101 +- .../unstructured_mesh/inputs_true8.dat | 74 +- .../unstructured_mesh/inputs_true9.dat | 74 +- tests/regression_tests/void/inputs_true.dat | 106 +- .../volume_calc/inputs_true.dat | 32 +- .../volume_calc/inputs_true_mg.dat | 32 +- .../weightwindows/inputs_true.dat | 46 +- .../weightwindows_fw_cadis/inputs_true.dat | 32 +- .../flat/inputs_true.dat | 32 +- .../linear/inputs_true.dat | 32 +- 180 files changed, 5006 insertions(+), 5035 deletions(-) diff --git a/openmc/_xml.py b/openmc/_xml.py index 17389a82f..05412128d 100644 --- a/openmc/_xml.py +++ b/openmc/_xml.py @@ -63,23 +63,6 @@ def get_text(elem, name, default=None): return child.text if child is not None else default -def reorder_attributes(root): - """Sort attributes in XML to preserve pre-Python 3.8 behavior - - Parameters - ---------- - root : lxml.etree._Element - Root element - - """ - for el in root.iter(): - attrib = el.attrib - if len(attrib) > 1: - # adjust attribute order, e.g. by sorting - attribs = sorted(attrib.items()) - attrib.clear() - attrib.update(attribs) - def get_elem_tuple(elem, name, dtype=int): """Helper function to get a tuple of values from an elem diff --git a/openmc/data/library.py b/openmc/data/library.py index a6ce1bbd3..bec538c06 100644 --- a/openmc/data/library.py +++ b/openmc/data/library.py @@ -5,7 +5,7 @@ import h5py import lxml.etree as ET import openmc -from openmc._xml import clean_indentation, reorder_attributes +from openmc._xml import clean_indentation class DataLibrary(list): @@ -132,7 +132,6 @@ class DataLibrary(list): clean_indentation(root) # Write XML file - reorder_attributes(root) # TODO: Remove when support is Python 3.8+ tree = ET.ElementTree(root) tree.write(str(path), xml_declaration=True, encoding='utf-8', method='xml') diff --git a/openmc/geometry.py b/openmc/geometry.py index f56b9b6fe..8496fb23a 100644 --- a/openmc/geometry.py +++ b/openmc/geometry.py @@ -140,7 +140,6 @@ class Geometry: # Clean the indentation in the file to be user-readable xml.clean_indentation(element) - xml.reorder_attributes(element) # TODO: Remove when support is Python 3.8+ return element diff --git a/openmc/material.py b/openmc/material.py index cec74af62..db67b709c 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -17,7 +17,7 @@ import h5py import openmc import openmc.data import openmc.checkvalue as cv -from ._xml import clean_indentation, reorder_attributes +from ._xml import clean_indentation from .mixin import IDManagerMixin from .utility_funcs import input_path from . import waste @@ -1919,7 +1919,6 @@ class Materials(cv.CheckedList): clean_indentation(element, level=level+1) element.tail = element.tail.strip(' ') file.write((level+1)*spaces_per_level*' ') - reorder_attributes(element) # TODO: Remove when support is Python 3.8+ file.write(ET.tostring(element, encoding="unicode")) # Write the elements. @@ -1928,7 +1927,6 @@ class Materials(cv.CheckedList): clean_indentation(element, level=level+1) element.tail = element.tail.strip(' ') file.write((level+1)*spaces_per_level*' ') - reorder_attributes(element) # TODO: Remove when support is Python 3.8+ file.write(ET.tostring(element, encoding="unicode")) # Write the closing tag for the root element. diff --git a/openmc/plots.py b/openmc/plots.py index 9e097e2b9..34dde84e4 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -10,7 +10,7 @@ import openmc import openmc.checkvalue as cv from openmc.checkvalue import PathLike -from ._xml import clean_indentation, get_elem_tuple, reorder_attributes, get_text +from ._xml import clean_indentation, get_elem_tuple, get_text from .mixin import IDManagerMixin _BASES = {'xy', 'xz', 'yz'} @@ -1857,8 +1857,6 @@ class Plots(cv.CheckedList): # Clean the indentation in the file to be user-readable clean_indentation(self._plots_file) - # TODO: Remove when support is Python 3.8+ - reorder_attributes(self._plots_file) return self._plots_file diff --git a/openmc/settings.py b/openmc/settings.py index 36eaec6cb..327faf544 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -11,7 +11,7 @@ import openmc import openmc.checkvalue as cv from openmc.checkvalue import PathLike from openmc.stats.multivariate import MeshSpatial -from ._xml import clean_indentation, get_text, reorder_attributes +from ._xml import clean_indentation, get_text from .mesh import _read_meshes, RegularMesh, MeshBase from .source import SourceBase, MeshSource, IndependentSource from .utility_funcs import input_path @@ -2201,7 +2201,6 @@ class Settings: # Clean the indentation in the file to be user-readable clean_indentation(element) - reorder_attributes(element) # TODO: Remove when support is Python 3.8+ return element diff --git a/openmc/tallies.py b/openmc/tallies.py index 06de0205c..3a5b42519 100644 --- a/openmc/tallies.py +++ b/openmc/tallies.py @@ -15,7 +15,7 @@ import scipy.sparse as sps import openmc import openmc.checkvalue as cv -from ._xml import clean_indentation, reorder_attributes, get_text +from ._xml import clean_indentation, get_text from .mixin import IDManagerMixin from .mesh import MeshBase @@ -3329,7 +3329,6 @@ class Tallies(cv.CheckedList): # Clean the indentation in the file to be user-readable clean_indentation(element) - reorder_attributes(element) # TODO: Remove when support is Python 3.8+ return element diff --git a/tests/regression_tests/adj_cell_rotation/inputs_true.dat b/tests/regression_tests/adj_cell_rotation/inputs_true.dat index 7e2a0cf62..18c0552ce 100644 --- a/tests/regression_tests/adj_cell_rotation/inputs_true.dat +++ b/tests/regression_tests/adj_cell_rotation/inputs_true.dat @@ -1,35 +1,35 @@ - - - + + + - - + + - - - - - - - - - - + + + + + + + + + + eigenvalue 10000 10 5 - + -4.0 -4.0 -4.0 4.0 4.0 4.0 diff --git a/tests/regression_tests/asymmetric_lattice/inputs_true.dat b/tests/regression_tests/asymmetric_lattice/inputs_true.dat index 302ef24c2..ee3d68907 100644 --- a/tests/regression_tests/asymmetric_lattice/inputs_true.dat +++ b/tests/regression_tests/asymmetric_lattice/inputs_true.dat @@ -1,40 +1,40 @@ - - - - - - - + + + + + + + - - - - - - + + + + + + - - - - - + + + + + - - - - - + + + + + - + @@ -47,7 +47,7 @@ - + @@ -62,7 +62,7 @@ - + @@ -77,7 +77,7 @@ - + @@ -92,7 +92,7 @@ - + @@ -107,7 +107,7 @@ - + @@ -122,7 +122,7 @@ - + @@ -135,7 +135,7 @@ - + @@ -149,7 +149,7 @@ - + @@ -157,7 +157,7 @@ - + 1.26 1.26 17 17 @@ -190,25 +190,25 @@ 8 8 8 7 7 7 - - - - - - - - - - - - + + + + + + + + + + + + eigenvalue 100 10 5 - + -32 -32 0 32 32 32 diff --git a/tests/regression_tests/cpp_driver/inputs_true.dat b/tests/regression_tests/cpp_driver/inputs_true.dat index 5d067cb68..fd450428a 100644 --- a/tests/regression_tests/cpp_driver/inputs_true.dat +++ b/tests/regression_tests/cpp_driver/inputs_true.dat @@ -1,26 +1,26 @@ - - - + + + - - + + - - - + + + - - + + 4.0 4.0 2 2 @@ -29,12 +29,12 @@ 2 2 2 2 - - - - - - + + + + + + eigenvalue diff --git a/tests/regression_tests/create_fission_neutrons/inputs_true.dat b/tests/regression_tests/create_fission_neutrons/inputs_true.dat index 9e44a0224..47e8c3830 100644 --- a/tests/regression_tests/create_fission_neutrons/inputs_true.dat +++ b/tests/regression_tests/create_fission_neutrons/inputs_true.dat @@ -1,30 +1,30 @@ - - - - + + + + - - - - - - - + + + + + + + fixed source 100 10 - + -1 -1 -1 1 1 1 - + false diff --git a/tests/regression_tests/dagmc/external/inputs_true.dat b/tests/regression_tests/dagmc/external/inputs_true.dat index 31f4c1f88..8a6c6fe74 100644 --- a/tests/regression_tests/dagmc/external/inputs_true.dat +++ b/tests/regression_tests/dagmc/external/inputs_true.dat @@ -1,26 +1,26 @@ - - - + + + - - - + + + - + eigenvalue 100 5 0 - + -4 -4 -4 4 4 4 diff --git a/tests/regression_tests/dagmc/legacy/inputs_true.dat b/tests/regression_tests/dagmc/legacy/inputs_true.dat index b06516cb1..ad2f8e54d 100644 --- a/tests/regression_tests/dagmc/legacy/inputs_true.dat +++ b/tests/regression_tests/dagmc/legacy/inputs_true.dat @@ -1,26 +1,26 @@ - - - + + + - - - + + + - + eigenvalue 100 5 0 - + -4 -4 -4 4 4 4 diff --git a/tests/regression_tests/dagmc/refl/inputs_true.dat b/tests/regression_tests/dagmc/refl/inputs_true.dat index 979eeb492..58cb9e66f 100644 --- a/tests/regression_tests/dagmc/refl/inputs_true.dat +++ b/tests/regression_tests/dagmc/refl/inputs_true.dat @@ -3,14 +3,14 @@ - + eigenvalue 100 5 0 - + -4 -4 -4 4 4 4 diff --git a/tests/regression_tests/dagmc/universes/inputs_true.dat b/tests/regression_tests/dagmc/universes/inputs_true.dat index 4b5be3612..2fa79d831 100644 --- a/tests/regression_tests/dagmc/universes/inputs_true.dat +++ b/tests/regression_tests/dagmc/universes/inputs_true.dat @@ -1,33 +1,33 @@ - - - - - - + + + + + + - - - - - - + + + + + + - - - - - + + + + + - - + + 24.0 24.0 2 2 @@ -36,12 +36,12 @@ 1 1 1 1 - - - - - - + + + + + + eigenvalue diff --git a/tests/regression_tests/dagmc/uwuw/inputs_true.dat b/tests/regression_tests/dagmc/uwuw/inputs_true.dat index d7e4a12aa..01082cb23 100644 --- a/tests/regression_tests/dagmc/uwuw/inputs_true.dat +++ b/tests/regression_tests/dagmc/uwuw/inputs_true.dat @@ -3,14 +3,14 @@ - + eigenvalue 100 5 0 - + -4 -4 -4 4 4 4 diff --git a/tests/regression_tests/deplete_with_transport/last_step_reference_materials.xml b/tests/regression_tests/deplete_with_transport/last_step_reference_materials.xml index f61595dd9..242e2e7db 100644 --- a/tests/regression_tests/deplete_with_transport/last_step_reference_materials.xml +++ b/tests/regression_tests/deplete_with_transport/last_step_reference_materials.xml @@ -1,1049 +1,1049 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - + + + diff --git a/tests/regression_tests/diff_tally/inputs_true.dat b/tests/regression_tests/diff_tally/inputs_true.dat index 3ec4154e8..19356b64a 100644 --- a/tests/regression_tests/diff_tally/inputs_true.dat +++ b/tests/regression_tests/diff_tally/inputs_true.dat @@ -1,40 +1,40 @@ - - - - - - - + + + + + + + - - - - - - + + + + + + - - - - - + + + + + - - - - - + + + + + - + @@ -47,7 +47,7 @@ - + @@ -62,7 +62,7 @@ - + @@ -77,7 +77,7 @@ - + @@ -92,7 +92,7 @@ - + @@ -107,7 +107,7 @@ - + @@ -122,7 +122,7 @@ - + @@ -135,7 +135,7 @@ - + @@ -149,8 +149,8 @@ - - + + @@ -174,9 +174,9 @@ - + - + 1.26 1.26 17 17 @@ -277,30 +277,30 @@ 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + eigenvalue 100 3 0 - + -160 -160 -183 160 160 183 @@ -429,10 +429,10 @@ nu-fission scatter 5 - - - - - + + + + + diff --git a/tests/regression_tests/distribmat/inputs_true.dat b/tests/regression_tests/distribmat/inputs_true.dat index ade86999d..35b3b5b9f 100644 --- a/tests/regression_tests/distribmat/inputs_true.dat +++ b/tests/regression_tests/distribmat/inputs_true.dat @@ -2,24 +2,24 @@ - - - + + + - - - + + + - - - + + + - + 2.0 2.0 1 @@ -29,30 +29,30 @@ 11 11 11 11 - - - - - + + + + + eigenvalue 1000 5 0 - + -1 -1 -1 1 1 1 - + 400 400 0 0 0 7 7 - + 400 400 0 0 0 7 7 diff --git a/tests/regression_tests/eigenvalue_genperbatch/inputs_true.dat b/tests/regression_tests/eigenvalue_genperbatch/inputs_true.dat index a3e67d62f..15b810a62 100644 --- a/tests/regression_tests/eigenvalue_genperbatch/inputs_true.dat +++ b/tests/regression_tests/eigenvalue_genperbatch/inputs_true.dat @@ -1,14 +1,14 @@ - - - + + + - + eigenvalue @@ -16,7 +16,7 @@ 7 3 3 - + -4.0 -4.0 -4.0 4.0 4.0 4.0 diff --git a/tests/regression_tests/energy_cutoff/inputs_true.dat b/tests/regression_tests/energy_cutoff/inputs_true.dat index af8f86b67..557cb7a3b 100644 --- a/tests/regression_tests/energy_cutoff/inputs_true.dat +++ b/tests/regression_tests/energy_cutoff/inputs_true.dat @@ -2,28 +2,28 @@ - - + + - - - - - - - + + + + + + + fixed source 100 10 - + -1 -1 -1 1 1 1 - + 4.0 diff --git a/tests/regression_tests/energy_laws/inputs_true.dat b/tests/regression_tests/energy_laws/inputs_true.dat index 4d8e825d1..8c5191217 100644 --- a/tests/regression_tests/energy_laws/inputs_true.dat +++ b/tests/regression_tests/energy_laws/inputs_true.dat @@ -1,18 +1,18 @@ - - - - - - - + + + + + + + - + eigenvalue diff --git a/tests/regression_tests/external_moab/inputs_true.dat b/tests/regression_tests/external_moab/inputs_true.dat index 23b656bce..ed035f897 100644 --- a/tests/regression_tests/external_moab/inputs_true.dat +++ b/tests/regression_tests/external_moab/inputs_true.dat @@ -1,63 +1,63 @@ - - - + + + - - - - - - + + + + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + fixed source 100 10 - + 0.0 0.0 0.0 - + 15000000.0 1.0 - + test_mesh_tets.h5m diff --git a/tests/regression_tests/filter_cellfrom/inputs_true.dat b/tests/regression_tests/filter_cellfrom/inputs_true.dat index b85f63c6e..20d0d69d4 100644 --- a/tests/regression_tests/filter_cellfrom/inputs_true.dat +++ b/tests/regression_tests/filter_cellfrom/inputs_true.dat @@ -1,54 +1,54 @@ - - - - - - - + + + + + + + - - - + + + - - - - - + + + + + - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + eigenvalue 2000 15 5 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 diff --git a/tests/regression_tests/filter_cellinstance/inputs_true.dat b/tests/regression_tests/filter_cellinstance/inputs_true.dat index b17cc0eb5..2677d7ad2 100644 --- a/tests/regression_tests/filter_cellinstance/inputs_true.dat +++ b/tests/regression_tests/filter_cellinstance/inputs_true.dat @@ -1,22 +1,22 @@ - - - + + + - - + + - + - + 2 2 4 4 @@ -27,19 +27,19 @@ 3 3 2 3 3 3 3 2 - - - - - - + + + + + + eigenvalue 1000 5 0 - + 0.0 0.0 0.0 diff --git a/tests/regression_tests/filter_energyfun/inputs_true.dat b/tests/regression_tests/filter_energyfun/inputs_true.dat index 4b7b74d28..b7a70290f 100644 --- a/tests/regression_tests/filter_energyfun/inputs_true.dat +++ b/tests/regression_tests/filter_energyfun/inputs_true.dat @@ -1,14 +1,14 @@ - - - + + + - + eigenvalue diff --git a/tests/regression_tests/filter_mesh/inputs_true.dat b/tests/regression_tests/filter_mesh/inputs_true.dat index 4995749bb..10f70a720 100644 --- a/tests/regression_tests/filter_mesh/inputs_true.dat +++ b/tests/regression_tests/filter_mesh/inputs_true.dat @@ -1,28 +1,28 @@ - - - + + + - - + + - - - - - - - - - - + + + + + + + + + + eigenvalue diff --git a/tests/regression_tests/filter_meshborn/inputs_true.dat b/tests/regression_tests/filter_meshborn/inputs_true.dat index 3ba38d56e..a94646ec8 100644 --- a/tests/regression_tests/filter_meshborn/inputs_true.dat +++ b/tests/regression_tests/filter_meshborn/inputs_true.dat @@ -2,19 +2,19 @@ - - + + - + fixed source 2000 8 - + 0.0 -10.0 -10.0 10.0 10.0 10.0 diff --git a/tests/regression_tests/filter_musurface/inputs_true.dat b/tests/regression_tests/filter_musurface/inputs_true.dat index 031f62159..6db8543c2 100644 --- a/tests/regression_tests/filter_musurface/inputs_true.dat +++ b/tests/regression_tests/filter_musurface/inputs_true.dat @@ -1,20 +1,20 @@ - - - + + + - - + + - - + + eigenvalue diff --git a/tests/regression_tests/filter_translations/inputs_true.dat b/tests/regression_tests/filter_translations/inputs_true.dat index 5004c3217..41ae9b6dc 100644 --- a/tests/regression_tests/filter_translations/inputs_true.dat +++ b/tests/regression_tests/filter_translations/inputs_true.dat @@ -1,28 +1,28 @@ - - - + + + - - + + - - - - - - - - - - + + + + + + + + + + eigenvalue @@ -57,10 +57,10 @@ 2 - + 3 - + 4 diff --git a/tests/regression_tests/fixed_source/inputs_true.dat b/tests/regression_tests/fixed_source/inputs_true.dat index 0fdb1466e..a36741775 100644 --- a/tests/regression_tests/fixed_source/inputs_true.dat +++ b/tests/regression_tests/fixed_source/inputs_true.dat @@ -1,21 +1,21 @@ - - - - + + + + - + fixed source 100 10 - + 0.0 0.0 0.0 diff --git a/tests/regression_tests/ifp/inputs_true.dat b/tests/regression_tests/ifp/inputs_true.dat index a3a3f1d77..2d69b29ab 100644 --- a/tests/regression_tests/ifp/inputs_true.dat +++ b/tests/regression_tests/ifp/inputs_true.dat @@ -1,21 +1,21 @@ - - - + + + - + eigenvalue 1000 20 5 - + -10.0 -10.0 -10.0 10.0 10.0 10.0 diff --git a/tests/regression_tests/iso_in_lab/inputs_true.dat b/tests/regression_tests/iso_in_lab/inputs_true.dat index bb6494ada..adfcf7e51 100644 --- a/tests/regression_tests/iso_in_lab/inputs_true.dat +++ b/tests/regression_tests/iso_in_lab/inputs_true.dat @@ -1,44 +1,44 @@ - - - - - - - + + + + + + + U234 U235 U238 Xe135 O16 - - - - - - + + + + + + Zr90 Zr91 Zr92 Zr94 Zr96 - - - - - + + + + + H1 O16 B10 B11 - - - - - + + + + + H1 O16 B10 B11 - + @@ -52,7 +52,7 @@ Fe54 Fe56 Fe57 Fe58 Ni58 Ni60 Mn55 Cr52 C0 Cu63 - + @@ -68,7 +68,7 @@ H1 O16 B10 B11 Fe54 Fe56 Fe57 Fe58 Ni58 Mn55 Cr52 - + @@ -84,7 +84,7 @@ H1 O16 B10 B11 Fe54 Fe56 Fe57 Fe58 Ni58 Mn55 Cr52 - + @@ -100,7 +100,7 @@ H1 O16 B10 B11 Fe54 Fe56 Fe57 Fe58 Ni58 Mn55 Cr52 - + @@ -116,7 +116,7 @@ H1 O16 B10 B11 Fe54 Fe56 Fe57 Fe58 Ni58 Mn55 Cr52 - + @@ -132,7 +132,7 @@ H1 O16 B10 B11 Fe54 Fe56 Fe57 Fe58 Ni58 Mn55 Cr52 - + @@ -146,7 +146,7 @@ H1 O16 B10 B11 Zr90 Zr91 Zr92 Zr94 Zr96 - + @@ -161,8 +161,8 @@ - - + + @@ -186,9 +186,9 @@ - + - + 1.26 1.26 17 17 @@ -289,30 +289,30 @@ 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + eigenvalue 100 10 5 - + -160 -160 -183 160 160 183 diff --git a/tests/regression_tests/lattice_distribmat/False/inputs_true.dat b/tests/regression_tests/lattice_distribmat/False/inputs_true.dat index d39a10369..783989057 100644 --- a/tests/regression_tests/lattice_distribmat/False/inputs_true.dat +++ b/tests/regression_tests/lattice_distribmat/False/inputs_true.dat @@ -1,26 +1,26 @@ - - - - + + + + - - - + + + - - - - - + + + + + 1.0 1.0 1 @@ -53,17 +53,17 @@ 1 - - - - - - - - - - - + + + + + + + + + + + eigenvalue diff --git a/tests/regression_tests/lattice_distribmat/True/inputs_true.dat b/tests/regression_tests/lattice_distribmat/True/inputs_true.dat index 450306a0c..aec3a5400 100644 --- a/tests/regression_tests/lattice_distribmat/True/inputs_true.dat +++ b/tests/regression_tests/lattice_distribmat/True/inputs_true.dat @@ -1,59 +1,59 @@ - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - - - + + + + + 1.0 1.0 8 @@ -86,17 +86,17 @@ 8 - - - - - - - - - - - + + + + + + + + + + + eigenvalue diff --git a/tests/regression_tests/lattice_hex_coincident/inputs_true.dat b/tests/regression_tests/lattice_hex_coincident/inputs_true.dat index ac82de0ad..fafc03da1 100644 --- a/tests/regression_tests/lattice_hex_coincident/inputs_true.dat +++ b/tests/regression_tests/lattice_hex_coincident/inputs_true.dat @@ -1,45 +1,45 @@ - + - + - - + + - - - - - + + + + + - - + + - - - - - + + + + + - - - - + + + + - - + + 1.4 3
    0.0 0.0
    @@ -50,25 +50,25 @@ 2 2 2
    - - - - - - - - - - - - + + + + + + + + + + + +
    eigenvalue 1000 5 2 - + -0.9899494936611666 -0.9899494936611666 0.0 0.9899494936611666 0.9899494936611666 10.0 diff --git a/tests/regression_tests/lattice_hex_x/inputs_true.dat b/tests/regression_tests/lattice_hex_x/inputs_true.dat index 252fbfc0b..b6536c6a8 100644 --- a/tests/regression_tests/lattice_hex_x/inputs_true.dat +++ b/tests/regression_tests/lattice_hex_x/inputs_true.dat @@ -1,32 +1,32 @@ - + - - - + + + - - - - + + + + - - - + + + - - - - - + + + + + @@ -42,8 +42,8 @@ - - + + 1.235 5.0 4
    0.0 0.0 5.0
    @@ -91,29 +91,29 @@ 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
    - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + +
    eigenvalue 1000 10 5 - + -13.62546635287517 -13.62546635287517 0.0 13.62546635287517 13.62546635287517 10.0 diff --git a/tests/regression_tests/lattice_multiple/inputs_true.dat b/tests/regression_tests/lattice_multiple/inputs_true.dat index b249d97c4..06abef582 100644 --- a/tests/regression_tests/lattice_multiple/inputs_true.dat +++ b/tests/regression_tests/lattice_multiple/inputs_true.dat @@ -1,15 +1,15 @@ - - - - + + + + - - - + + + @@ -18,8 +18,8 @@ - - + + 1.2 1.2 1 @@ -37,12 +37,12 @@ 4 4 4 4 - - - - - - + + + + + + eigenvalue diff --git a/tests/regression_tests/lattice_rotated/inputs_true.dat b/tests/regression_tests/lattice_rotated/inputs_true.dat index 1b7c74870..e53b93f93 100644 --- a/tests/regression_tests/lattice_rotated/inputs_true.dat +++ b/tests/regression_tests/lattice_rotated/inputs_true.dat @@ -1,18 +1,18 @@ - - - + + + - - - + + + - - - + + + @@ -22,8 +22,8 @@ - - + + 1.25 @@ -51,18 +51,18 @@ 1 1 1 1 1 1 1 1 - - - - - + + + + + eigenvalue 1000 5 0 - + 0.0 0.0 0.0 diff --git a/tests/regression_tests/mg_basic/inputs_true.dat b/tests/regression_tests/mg_basic/inputs_true.dat index 8c6faaf07..aa2c2fa0a 100644 --- a/tests/regression_tests/mg_basic/inputs_true.dat +++ b/tests/regression_tests/mg_basic/inputs_true.dat @@ -3,29 +3,29 @@ 2g.h5 - + - + - + - + - + - - + + @@ -35,20 +35,20 @@ - - - - - - - + + + + + + + eigenvalue 1000 10 5 - + 0.0 -1000.0 -1000.0 154.90833333333333 1000.0 1000.0 diff --git a/tests/regression_tests/mg_basic_delayed/inputs_true.dat b/tests/regression_tests/mg_basic_delayed/inputs_true.dat index e0bcad15c..cc3f2cfb2 100644 --- a/tests/regression_tests/mg_basic_delayed/inputs_true.dat +++ b/tests/regression_tests/mg_basic_delayed/inputs_true.dat @@ -3,27 +3,27 @@ 2g.h5 - + - + - + - + - + - + @@ -34,20 +34,20 @@ - - - - - - - + + + + + + + eigenvalue 1000 10 5 - + 0.0 -1000.0 -1000.0 154.90833333333333 1000.0 1000.0 diff --git a/tests/regression_tests/mg_convert/inputs_true.dat b/tests/regression_tests/mg_convert/inputs_true.dat index 4e51ec80c..3b1f511e6 100644 --- a/tests/regression_tests/mg_convert/inputs_true.dat +++ b/tests/regression_tests/mg_convert/inputs_true.dat @@ -3,23 +3,23 @@ mgxs.h5 - + - - - - - + + + + + eigenvalue 100 10 5 - + -5 -5 -5 5 5 5 diff --git a/tests/regression_tests/mg_legendre/inputs_true.dat b/tests/regression_tests/mg_legendre/inputs_true.dat index 34ff7b630..81362f20a 100644 --- a/tests/regression_tests/mg_legendre/inputs_true.dat +++ b/tests/regression_tests/mg_legendre/inputs_true.dat @@ -3,21 +3,21 @@ 2g.h5 - + - - + + eigenvalue 1000 10 5 - + 0.0 -1000.0 -1000.0 929.45 1000.0 1000.0 diff --git a/tests/regression_tests/mg_max_order/inputs_true.dat b/tests/regression_tests/mg_max_order/inputs_true.dat index b6d70dc4c..c8f42d1a3 100644 --- a/tests/regression_tests/mg_max_order/inputs_true.dat +++ b/tests/regression_tests/mg_max_order/inputs_true.dat @@ -3,21 +3,21 @@ 2g.h5 - + - - + + eigenvalue 1000 10 5 - + 0.0 -1000.0 -1000.0 929.45 1000.0 1000.0 diff --git a/tests/regression_tests/mg_survival_biasing/inputs_true.dat b/tests/regression_tests/mg_survival_biasing/inputs_true.dat index 6529c60d8..bee69729d 100644 --- a/tests/regression_tests/mg_survival_biasing/inputs_true.dat +++ b/tests/regression_tests/mg_survival_biasing/inputs_true.dat @@ -3,21 +3,21 @@ 2g.h5 - + - - + + eigenvalue 1000 10 5 - + 0.0 -1000.0 -1000.0 929.45 1000.0 1000.0 diff --git a/tests/regression_tests/mg_tallies/inputs_true.dat b/tests/regression_tests/mg_tallies/inputs_true.dat index c526b65a2..f4f154244 100644 --- a/tests/regression_tests/mg_tallies/inputs_true.dat +++ b/tests/regression_tests/mg_tallies/inputs_true.dat @@ -3,21 +3,21 @@ 2g.h5 - + - - + + eigenvalue 1000 10 5 - + 0.0 -1000.0 -1000.0 929.45 1000.0 1000.0 diff --git a/tests/regression_tests/mg_temperature_multi/inputs_true.dat b/tests/regression_tests/mg_temperature_multi/inputs_true.dat index 0c452cd52..b84a6782b 100644 --- a/tests/regression_tests/mg_temperature_multi/inputs_true.dat +++ b/tests/regression_tests/mg_temperature_multi/inputs_true.dat @@ -3,31 +3,31 @@ mgxs.h5 - + - + - - - - - - - - - + + + + + + + + + eigenvalue 1000 10 5 - + -0.63 -0.63 -1 0.63 0.63 1 diff --git a/tests/regression_tests/mgxs_library_ce_to_mg/inputs_true.dat b/tests/regression_tests/mgxs_library_ce_to_mg/inputs_true.dat index 6c071263f..2f6dde1ab 100644 --- a/tests/regression_tests/mgxs_library_ce_to_mg/inputs_true.dat +++ b/tests/regression_tests/mgxs_library_ce_to_mg/inputs_true.dat @@ -1,47 +1,47 @@ - - - - - - + + + + + + - - - - - - + + + + + + - - - - - + + + + + - - - - - - - - - + + + + + + + + + eigenvalue 100 10 5 - + -0.63 -0.63 -1 0.63 0.63 1 diff --git a/tests/regression_tests/mgxs_library_ce_to_mg_nuclides/inputs_true.dat b/tests/regression_tests/mgxs_library_ce_to_mg_nuclides/inputs_true.dat index ad3d50974..e1f330cf9 100644 --- a/tests/regression_tests/mgxs_library_ce_to_mg_nuclides/inputs_true.dat +++ b/tests/regression_tests/mgxs_library_ce_to_mg_nuclides/inputs_true.dat @@ -1,47 +1,47 @@ - - - - - - + + + + + + - - - - - - + + + + + + - - - - - + + + + + - - - - - - - - - + + + + + + + + + eigenvalue 100 10 5 - + -0.63 -0.63 -1 0.63 0.63 1 diff --git a/tests/regression_tests/mgxs_library_condense/inputs_true.dat b/tests/regression_tests/mgxs_library_condense/inputs_true.dat index 8606e9fdd..4451d214c 100644 --- a/tests/regression_tests/mgxs_library_condense/inputs_true.dat +++ b/tests/regression_tests/mgxs_library_condense/inputs_true.dat @@ -1,47 +1,47 @@ - - - - - - + + + + + + - - - - - - + + + + + + - - - - - + + + + + - - - - - - - - - + + + + + + + + + eigenvalue 100 10 5 - + -0.63 -0.63 -1 0.63 0.63 1 diff --git a/tests/regression_tests/mgxs_library_correction/inputs_true.dat b/tests/regression_tests/mgxs_library_correction/inputs_true.dat index ea2d1b714..bb463778f 100644 --- a/tests/regression_tests/mgxs_library_correction/inputs_true.dat +++ b/tests/regression_tests/mgxs_library_correction/inputs_true.dat @@ -1,47 +1,47 @@ - - - - - - + + + + + + - - - - - - + + + + + + - - - - - + + + + + - - - - - - - - - + + + + + + + + + eigenvalue 100 10 5 - + -0.63 -0.63 -1 0.63 0.63 1 diff --git a/tests/regression_tests/mgxs_library_distribcell/inputs_true.dat b/tests/regression_tests/mgxs_library_distribcell/inputs_true.dat index f1a605334..e3826cc18 100644 --- a/tests/regression_tests/mgxs_library_distribcell/inputs_true.dat +++ b/tests/regression_tests/mgxs_library_distribcell/inputs_true.dat @@ -1,38 +1,38 @@ - - - - - - + + + + + + - - - - - - + + + + + + - - - - - + + + + + - - - - - - - + + + + + + + 1.26 1.26 17 17 @@ -56,19 +56,19 @@ 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 - - - - - - + + + + + + eigenvalue 100 10 5 - + -10.71 -10.71 -1 10.71 10.71 1 diff --git a/tests/regression_tests/mgxs_library_hdf5/inputs_true.dat b/tests/regression_tests/mgxs_library_hdf5/inputs_true.dat index 8606e9fdd..4451d214c 100644 --- a/tests/regression_tests/mgxs_library_hdf5/inputs_true.dat +++ b/tests/regression_tests/mgxs_library_hdf5/inputs_true.dat @@ -1,47 +1,47 @@ - - - - - - + + + + + + - - - - - - + + + + + + - - - - - + + + + + - - - - - - - - - + + + + + + + + + eigenvalue 100 10 5 - + -0.63 -0.63 -1 0.63 0.63 1 diff --git a/tests/regression_tests/mgxs_library_histogram/inputs_true.dat b/tests/regression_tests/mgxs_library_histogram/inputs_true.dat index 013fcd0fa..94c656418 100644 --- a/tests/regression_tests/mgxs_library_histogram/inputs_true.dat +++ b/tests/regression_tests/mgxs_library_histogram/inputs_true.dat @@ -1,47 +1,47 @@ - - - - - - + + + + + + - - - - - - + + + + + + - - - - - + + + + + - - - - - - - - - + + + + + + + + + eigenvalue 100 10 5 - + -0.63 -0.63 -1 0.63 0.63 1 diff --git a/tests/regression_tests/mgxs_library_mesh/inputs_true.dat b/tests/regression_tests/mgxs_library_mesh/inputs_true.dat index 1ecb7a2d3..5a6e8a20a 100644 --- a/tests/regression_tests/mgxs_library_mesh/inputs_true.dat +++ b/tests/regression_tests/mgxs_library_mesh/inputs_true.dat @@ -1,28 +1,28 @@ - - - + + + - - + + - - - - - - - - - - + + + + + + + + + + eigenvalue diff --git a/tests/regression_tests/mgxs_library_no_nuclides/inputs_true.dat b/tests/regression_tests/mgxs_library_no_nuclides/inputs_true.dat index 5cad36b7e..af09268cd 100644 --- a/tests/regression_tests/mgxs_library_no_nuclides/inputs_true.dat +++ b/tests/regression_tests/mgxs_library_no_nuclides/inputs_true.dat @@ -1,47 +1,47 @@ - - - - - - + + + + + + - - - - - - + + + + + + - - - - - + + + + + - - - - - - - - - + + + + + + + + + eigenvalue 100 10 5 - + -0.63 -0.63 -1 0.63 0.63 1 diff --git a/tests/regression_tests/mgxs_library_nuclides/inputs_true.dat b/tests/regression_tests/mgxs_library_nuclides/inputs_true.dat index 9b3a22510..c35e57f0e 100644 --- a/tests/regression_tests/mgxs_library_nuclides/inputs_true.dat +++ b/tests/regression_tests/mgxs_library_nuclides/inputs_true.dat @@ -1,47 +1,47 @@ - - - - - - + + + + + + - - - - - - + + + + + + - - - - - + + + + + - - - - - - - - - + + + + + + + + + eigenvalue 100 10 5 - + -0.63 -0.63 -1 0.63 0.63 1 diff --git a/tests/regression_tests/mgxs_library_specific_nuclides/inputs_true.dat b/tests/regression_tests/mgxs_library_specific_nuclides/inputs_true.dat index 1372741c9..dd9d1ceb0 100644 --- a/tests/regression_tests/mgxs_library_specific_nuclides/inputs_true.dat +++ b/tests/regression_tests/mgxs_library_specific_nuclides/inputs_true.dat @@ -1,47 +1,47 @@ - - - - - - + + + + + + - - - - - - + + + + + + - - - - - + + + + + - - - - - - - - - + + + + + + + + + eigenvalue 100 10 5 - + -0.63 -0.63 -1 0.63 0.63 1 diff --git a/tests/regression_tests/model_xml/adj_cell_rotation_inputs_true.dat b/tests/regression_tests/model_xml/adj_cell_rotation_inputs_true.dat index 7e2a0cf62..18c0552ce 100644 --- a/tests/regression_tests/model_xml/adj_cell_rotation_inputs_true.dat +++ b/tests/regression_tests/model_xml/adj_cell_rotation_inputs_true.dat @@ -1,35 +1,35 @@ - - - + + + - - + + - - - - - - - - - - + + + + + + + + + + eigenvalue 10000 10 5 - + -4.0 -4.0 -4.0 4.0 4.0 4.0 diff --git a/tests/regression_tests/model_xml/energy_laws_inputs_true.dat b/tests/regression_tests/model_xml/energy_laws_inputs_true.dat index 4d8e825d1..8c5191217 100644 --- a/tests/regression_tests/model_xml/energy_laws_inputs_true.dat +++ b/tests/regression_tests/model_xml/energy_laws_inputs_true.dat @@ -1,18 +1,18 @@ - - - - - - - + + + + + + + - + eigenvalue diff --git a/tests/regression_tests/model_xml/lattice_multiple_inputs_true.dat b/tests/regression_tests/model_xml/lattice_multiple_inputs_true.dat index b249d97c4..06abef582 100644 --- a/tests/regression_tests/model_xml/lattice_multiple_inputs_true.dat +++ b/tests/regression_tests/model_xml/lattice_multiple_inputs_true.dat @@ -1,15 +1,15 @@ - - - - + + + + - - - + + + @@ -18,8 +18,8 @@ - - + + 1.2 1.2 1 @@ -37,12 +37,12 @@ 4 4 4 4 - - - - - - + + + + + + eigenvalue diff --git a/tests/regression_tests/model_xml/photon_production_inputs_true.dat b/tests/regression_tests/model_xml/photon_production_inputs_true.dat index 10f0bad98..07eebaa3e 100644 --- a/tests/regression_tests/model_xml/photon_production_inputs_true.dat +++ b/tests/regression_tests/model_xml/photon_production_inputs_true.dat @@ -2,28 +2,28 @@ - - + + - - - - + + + + fixed source 10000 1 - + 0 0 0 - + 14000000.0 1.0 diff --git a/tests/regression_tests/multipole/inputs_true.dat b/tests/regression_tests/multipole/inputs_true.dat index 7bea66930..22a351240 100644 --- a/tests/regression_tests/multipole/inputs_true.dat +++ b/tests/regression_tests/multipole/inputs_true.dat @@ -2,21 +2,21 @@ - - - + + + - - - + + + - + 2.0 2.0 1 @@ -26,18 +26,18 @@ 11 11 11 11 - - - - - + + + + + eigenvalue 1000 5 0 - + -1 -1 -1 1 1 1 diff --git a/tests/regression_tests/ncrystal/inputs_true.dat b/tests/regression_tests/ncrystal/inputs_true.dat index ecbc3c54f..81ee2e312 100644 --- a/tests/regression_tests/ncrystal/inputs_true.dat +++ b/tests/regression_tests/ncrystal/inputs_true.dat @@ -1,26 +1,26 @@ - - - + + + - - + + fixed source 100000 10 - + 0 0 -20 - + 0.012 1.0 diff --git a/tests/regression_tests/periodic/inputs_true.dat b/tests/regression_tests/periodic/inputs_true.dat index 65e1b5ce3..9183db3c4 100644 --- a/tests/regression_tests/periodic/inputs_true.dat +++ b/tests/regression_tests/periodic/inputs_true.dat @@ -2,33 +2,33 @@ - - - + + + - - - + + + - - - - - - - + + + + + + + eigenvalue 1000 4 0 - + 0 0 0 5 5 0 diff --git a/tests/regression_tests/periodic_6fold/inputs_true.dat b/tests/regression_tests/periodic_6fold/inputs_true.dat index f7f413d09..075cffc12 100644 --- a/tests/regression_tests/periodic_6fold/inputs_true.dat +++ b/tests/regression_tests/periodic_6fold/inputs_true.dat @@ -2,30 +2,30 @@ - - - + + + - - - + + + - - - - + + + + eigenvalue 1000 4 0 - + 0 0 0 5 5 0 diff --git a/tests/regression_tests/periodic_hex/inputs_true.dat b/tests/regression_tests/periodic_hex/inputs_true.dat index 188c8dfa9..e65af3d94 100644 --- a/tests/regression_tests/periodic_hex/inputs_true.dat +++ b/tests/regression_tests/periodic_hex/inputs_true.dat @@ -1,19 +1,19 @@ - - - + + + - - - - - - + + + + + + eigenvalue diff --git a/tests/regression_tests/photon_production/inputs_true.dat b/tests/regression_tests/photon_production/inputs_true.dat index 10f0bad98..07eebaa3e 100644 --- a/tests/regression_tests/photon_production/inputs_true.dat +++ b/tests/regression_tests/photon_production/inputs_true.dat @@ -2,28 +2,28 @@ - - + + - - - - + + + + fixed source 10000 1 - + 0 0 0 - + 14000000.0 1.0 diff --git a/tests/regression_tests/photon_production_fission/inputs_true.dat b/tests/regression_tests/photon_production_fission/inputs_true.dat index fa379b39e..11a194e3c 100644 --- a/tests/regression_tests/photon_production_fission/inputs_true.dat +++ b/tests/regression_tests/photon_production_fission/inputs_true.dat @@ -1,21 +1,21 @@ - - - + + + - + eigenvalue 1000 5 2 - + 0 0 0 diff --git a/tests/regression_tests/photon_source/inputs_true.dat b/tests/regression_tests/photon_source/inputs_true.dat index 3d2e05243..adaa5fb42 100644 --- a/tests/regression_tests/photon_source/inputs_true.dat +++ b/tests/regression_tests/photon_source/inputs_true.dat @@ -2,22 +2,22 @@ - - - - - + + + + + - + fixed source 10000 1 - + 0 0 0 diff --git a/tests/regression_tests/pulse_height/inputs_true.dat b/tests/regression_tests/pulse_height/inputs_true.dat index 544ddb097..590928e43 100644 --- a/tests/regression_tests/pulse_height/inputs_true.dat +++ b/tests/regression_tests/pulse_height/inputs_true.dat @@ -2,22 +2,22 @@ - - - + + + - - - - + + + + fixed source 100 5 - + 0.0 0.0 0.0 diff --git a/tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat b/tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat index 65ffd5af7..30e62a885 100644 --- a/tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat +++ b/tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat @@ -3,24 +3,24 @@ mgxs.h5 - + - + - + - - - - - + + + + + 2.5 2.5 2.5 12 12 12 @@ -182,19 +182,19 @@ 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - - - - - - + + + + + + fixed source 500 10 5 - + 100.0 1.0 @@ -207,7 +207,7 @@ 500.0 100.0 - + 0.0 0.0 0.0 30.0 30.0 30.0 diff --git a/tests/regression_tests/random_ray_adjoint_k_eff/inputs_true.dat b/tests/regression_tests/random_ray_adjoint_k_eff/inputs_true.dat index 725702a49..cd4e92aa1 100644 --- a/tests/regression_tests/random_ray_adjoint_k_eff/inputs_true.dat +++ b/tests/regression_tests/random_ray_adjoint_k_eff/inputs_true.dat @@ -3,32 +3,32 @@ mgxs.h5 - + - + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + 0.126 0.126 10 10 @@ -53,23 +53,23 @@ 2 2 2 5 - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + eigenvalue @@ -80,7 +80,7 @@ 100.0 20.0 - + -1.26 -1.26 -1 1.26 1.26 1 diff --git a/tests/regression_tests/random_ray_auto_convert/infinite_medium/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert/infinite_medium/inputs_true.dat index 02be4ea7a..464c89a5d 100644 --- a/tests/regression_tests/random_ray_auto_convert/infinite_medium/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert/infinite_medium/inputs_true.dat @@ -2,36 +2,36 @@ mgxs.h5 - - + + - + - + - - - - - - - - - + + + + + + + + + eigenvalue 100 10 5 - + -0.63 -0.63 -1 0.63 0.63 1 @@ -41,7 +41,7 @@ multi-group - + -0.63 -0.63 -1.0 0.63 0.63 1.0 diff --git a/tests/regression_tests/random_ray_auto_convert/material_wise/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert/material_wise/inputs_true.dat index 02be4ea7a..464c89a5d 100644 --- a/tests/regression_tests/random_ray_auto_convert/material_wise/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert/material_wise/inputs_true.dat @@ -2,36 +2,36 @@ mgxs.h5 - - + + - + - + - - - - - - - - - + + + + + + + + + eigenvalue 100 10 5 - + -0.63 -0.63 -1 0.63 0.63 1 @@ -41,7 +41,7 @@ multi-group - + -0.63 -0.63 -1.0 0.63 0.63 1.0 diff --git a/tests/regression_tests/random_ray_auto_convert/stochastic_slab/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert/stochastic_slab/inputs_true.dat index 02be4ea7a..464c89a5d 100644 --- a/tests/regression_tests/random_ray_auto_convert/stochastic_slab/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert/stochastic_slab/inputs_true.dat @@ -2,36 +2,36 @@ mgxs.h5 - - + + - + - + - - - - - - - - - + + + + + + + + + eigenvalue 100 10 5 - + -0.63 -0.63 -1 0.63 0.63 1 @@ -41,7 +41,7 @@ multi-group - + -0.63 -0.63 -1.0 0.63 0.63 1.0 diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat b/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat index 4d4ed76dc..47325ebd7 100644 --- a/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat +++ b/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat @@ -2,36 +2,36 @@ mgxs.h5 - - + + - + - + - - - - - - - - - + + + + + + + + + eigenvalue 100 20 15 - + -0.63 -0.63 -1 0.63 0.63 1 @@ -41,7 +41,7 @@ multi-group - + -0.63 -0.63 -1.0 0.63 0.63 1.0 diff --git a/tests/regression_tests/random_ray_fixed_source_domain/cell/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/cell/inputs_true.dat index d6ca8918c..4b8af76aa 100644 --- a/tests/regression_tests/random_ray_fixed_source_domain/cell/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_domain/cell/inputs_true.dat @@ -3,24 +3,24 @@ mgxs.h5 - + - + - + - - - - - + + + + + 2.5 2.5 2.5 12 12 12 @@ -182,19 +182,19 @@ 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - - - - - - + + + + + + fixed source 90 10 5 - + 100.0 1.0 @@ -207,7 +207,7 @@ 500.0 100.0 - + 0.0 0.0 0.0 30.0 30.0 30.0 diff --git a/tests/regression_tests/random_ray_fixed_source_domain/material/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/material/inputs_true.dat index 7fae491ae..82fe48b61 100644 --- a/tests/regression_tests/random_ray_fixed_source_domain/material/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_domain/material/inputs_true.dat @@ -3,24 +3,24 @@ mgxs.h5 - + - + - + - - - - - + + + + + 2.5 2.5 2.5 12 12 12 @@ -182,19 +182,19 @@ 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - - - - - - + + + + + + fixed source 90 10 5 - + 100.0 1.0 @@ -207,7 +207,7 @@ 500.0 100.0 - + 0.0 0.0 0.0 30.0 30.0 30.0 diff --git a/tests/regression_tests/random_ray_fixed_source_domain/universe/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/universe/inputs_true.dat index 75d84efe9..c4fd06f42 100644 --- a/tests/regression_tests/random_ray_fixed_source_domain/universe/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_domain/universe/inputs_true.dat @@ -3,24 +3,24 @@ mgxs.h5 - + - + - + - - - - - + + + + + 2.5 2.5 2.5 12 12 12 @@ -182,19 +182,19 @@ 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - - - - - - + + + + + + fixed source 90 10 5 - + 100.0 1.0 @@ -207,7 +207,7 @@ 500.0 100.0 - + 0.0 0.0 0.0 30.0 30.0 30.0 diff --git a/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat index 6ef3f0871..4085b0c7a 100644 --- a/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat @@ -3,24 +3,24 @@ mgxs.h5 - + - + - + - - - - - + + + + + 2.5 2.5 2.5 12 12 12 @@ -182,19 +182,19 @@ 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - - - - - - + + + + + + fixed source 90 40 20 - + 100.0 1.0 @@ -207,7 +207,7 @@ 500.0 100.0 - + 0.0 0.0 0.0 30.0 30.0 30.0 diff --git a/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat index 805c53fe6..ff085d2fb 100644 --- a/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat @@ -3,24 +3,24 @@ mgxs.h5 - + - + - + - - - - - + + + + + 2.5 2.5 2.5 12 12 12 @@ -182,19 +182,19 @@ 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - - - - - - + + + + + + fixed source 90 40 20 - + 100.0 1.0 @@ -207,7 +207,7 @@ 500.0 100.0 - + 0.0 0.0 0.0 30.0 30.0 30.0 diff --git a/tests/regression_tests/random_ray_fixed_source_mesh/flat/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_mesh/flat/inputs_true.dat index 67c2ffc15..12c4d74ed 100644 --- a/tests/regression_tests/random_ray_fixed_source_mesh/flat/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_mesh/flat/inputs_true.dat @@ -3,24 +3,24 @@ mgxs.h5 - + - + - + - - - - - + + + + + 2.5 2.5 2.5 12 12 12 @@ -182,19 +182,19 @@ 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - - - - - - + + + + + + fixed source 90 30 15 - + 100.0 1.0 @@ -207,7 +207,7 @@ 500.0 100.0 - + 0.0 0.0 0.0 30.0 30.0 30.0 diff --git a/tests/regression_tests/random_ray_fixed_source_mesh/linear/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_mesh/linear/inputs_true.dat index 10b7c74f1..07adb1ff5 100644 --- a/tests/regression_tests/random_ray_fixed_source_mesh/linear/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_mesh/linear/inputs_true.dat @@ -3,24 +3,24 @@ mgxs.h5 - + - + - + - - - - - + + + + + 2.5 2.5 2.5 12 12 12 @@ -182,19 +182,19 @@ 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - - - - - - + + + + + + fixed source 90 30 15 - + 100.0 1.0 @@ -207,7 +207,7 @@ 500.0 100.0 - + 0.0 0.0 0.0 30.0 30.0 30.0 diff --git a/tests/regression_tests/random_ray_fixed_source_normalization/False/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_normalization/False/inputs_true.dat index 559cff0f0..ab077ae8a 100644 --- a/tests/regression_tests/random_ray_fixed_source_normalization/False/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_normalization/False/inputs_true.dat @@ -3,24 +3,24 @@ mgxs.h5 - + - + - + - - - - - + + + + + 2.5 2.5 2.5 12 12 12 @@ -182,19 +182,19 @@ 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - - - - - - + + + + + + fixed source 90 10 5 - + 100.0 1.0 @@ -207,7 +207,7 @@ 500.0 100.0 - + 0.0 0.0 0.0 30.0 30.0 30.0 diff --git a/tests/regression_tests/random_ray_fixed_source_normalization/True/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_normalization/True/inputs_true.dat index 75d84efe9..c4fd06f42 100644 --- a/tests/regression_tests/random_ray_fixed_source_normalization/True/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_normalization/True/inputs_true.dat @@ -3,24 +3,24 @@ mgxs.h5 - + - + - + - - - - - + + + + + 2.5 2.5 2.5 12 12 12 @@ -182,19 +182,19 @@ 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - - - - - - + + + + + + fixed source 90 10 5 - + 100.0 1.0 @@ -207,7 +207,7 @@ 500.0 100.0 - + 0.0 0.0 0.0 30.0 30.0 30.0 diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/flat/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_subcritical/flat/inputs_true.dat index 0bf4cfdc9..a124b5e3c 100644 --- a/tests/regression_tests/random_ray_fixed_source_subcritical/flat/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_subcritical/flat/inputs_true.dat @@ -3,34 +3,34 @@ mgxs.h5 - + - + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + 0.126 0.126 10 10 @@ -71,30 +71,30 @@ 2 12 12 13 - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + fixed source 30 125 100 - + 1.134 -1.26 -1.0 1.26 -1.134 1.0 @@ -110,7 +110,7 @@ 40.0 40.0 - + -1.26 -1.26 -1 1.26 1.26 1 diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/inputs_true.dat index f7572a072..cc557afb6 100644 --- a/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/inputs_true.dat @@ -3,34 +3,34 @@ mgxs.h5 - + - + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + 0.126 0.126 10 10 @@ -71,30 +71,30 @@ 2 12 12 13 - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + fixed source 30 125 100 - + 1.134 -1.26 -1.0 1.26 -1.134 1.0 @@ -110,7 +110,7 @@ 40.0 40.0 - + -1.26 -1.26 -1 1.26 1.26 1 diff --git a/tests/regression_tests/random_ray_halton_samples/inputs_true.dat b/tests/regression_tests/random_ray_halton_samples/inputs_true.dat index 624ab495f..3f058e45c 100644 --- a/tests/regression_tests/random_ray_halton_samples/inputs_true.dat +++ b/tests/regression_tests/random_ray_halton_samples/inputs_true.dat @@ -3,32 +3,32 @@ mgxs.h5 - + - + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + 0.126 0.126 10 10 @@ -53,23 +53,23 @@ 2 2 2 5 - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + eigenvalue @@ -80,7 +80,7 @@ 100.0 20.0 - + -1.26 -1.26 -1 1.26 1.26 1 diff --git a/tests/regression_tests/random_ray_k_eff/inputs_true.dat b/tests/regression_tests/random_ray_k_eff/inputs_true.dat index a2b058f2b..33c9cac33 100644 --- a/tests/regression_tests/random_ray_k_eff/inputs_true.dat +++ b/tests/regression_tests/random_ray_k_eff/inputs_true.dat @@ -3,32 +3,32 @@ mgxs.h5 - + - + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + 0.126 0.126 10 10 @@ -53,23 +53,23 @@ 2 2 2 5 - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + eigenvalue @@ -80,7 +80,7 @@ 100.0 20.0 - + -1.26 -1.26 -1 1.26 1.26 1 diff --git a/tests/regression_tests/random_ray_k_eff_mesh/inputs_true.dat b/tests/regression_tests/random_ray_k_eff_mesh/inputs_true.dat index cdc717198..f0822d9f9 100644 --- a/tests/regression_tests/random_ray_k_eff_mesh/inputs_true.dat +++ b/tests/regression_tests/random_ray_k_eff_mesh/inputs_true.dat @@ -3,32 +3,32 @@ mgxs.h5 - + - + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + 0.126 0.126 10 10 @@ -53,23 +53,23 @@ 2 2 2 5 - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + eigenvalue @@ -80,7 +80,7 @@ 100.0 20.0 - + -1.26 -1.26 -1 1.26 1.26 1 diff --git a/tests/regression_tests/random_ray_linear/linear/inputs_true.dat b/tests/regression_tests/random_ray_linear/linear/inputs_true.dat index 4df51bad5..006996557 100644 --- a/tests/regression_tests/random_ray_linear/linear/inputs_true.dat +++ b/tests/regression_tests/random_ray_linear/linear/inputs_true.dat @@ -3,32 +3,32 @@ mgxs.h5 - + - + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + 0.126 0.126 10 10 @@ -53,23 +53,23 @@ 2 2 2 5 - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + eigenvalue @@ -80,7 +80,7 @@ 100.0 20.0 - + -1.26 -1.26 -1 1.26 1.26 1 diff --git a/tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat b/tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat index 113771438..81527c479 100644 --- a/tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat +++ b/tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat @@ -3,32 +3,32 @@ mgxs.h5 - + - + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + 0.126 0.126 10 10 @@ -53,23 +53,23 @@ 2 2 2 5 - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + eigenvalue @@ -80,7 +80,7 @@ 100.0 20.0 - + -1.26 -1.26 -1 1.26 1.26 1 diff --git a/tests/regression_tests/random_ray_point_source_locator/inputs_true.dat b/tests/regression_tests/random_ray_point_source_locator/inputs_true.dat index 9c7cd4ab5..82ad979da 100644 --- a/tests/regression_tests/random_ray_point_source_locator/inputs_true.dat +++ b/tests/regression_tests/random_ray_point_source_locator/inputs_true.dat @@ -3,24 +3,24 @@ mgxs.h5 - + - + - + - - - - - + + + + + 2.5 2.5 2.5 12 12 12 @@ -182,19 +182,19 @@ 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - - - - - - + + + + + + fixed source 90 30 15 - + 2.5 2.5 2.5 @@ -206,7 +206,7 @@ 500.0 100.0 - + 0.0 0.0 0.0 30.0 30.0 30.0 diff --git a/tests/regression_tests/random_ray_void/flat/inputs_true.dat b/tests/regression_tests/random_ray_void/flat/inputs_true.dat index 617a66053..ea8c22f0b 100644 --- a/tests/regression_tests/random_ray_void/flat/inputs_true.dat +++ b/tests/regression_tests/random_ray_void/flat/inputs_true.dat @@ -3,24 +3,24 @@ mgxs.h5 - + - + - + - - - - - + + + + + 2.5 2.5 2.5 12 12 12 @@ -182,19 +182,19 @@ 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - - - - - - + + + + + + fixed source 90 40 20 - + 100.0 1.0 @@ -207,7 +207,7 @@ 500.0 100.0 - + 0.0 0.0 0.0 30.0 30.0 30.0 diff --git a/tests/regression_tests/random_ray_void/linear/inputs_true.dat b/tests/regression_tests/random_ray_void/linear/inputs_true.dat index 4b6a682d9..a089604ef 100644 --- a/tests/regression_tests/random_ray_void/linear/inputs_true.dat +++ b/tests/regression_tests/random_ray_void/linear/inputs_true.dat @@ -3,24 +3,24 @@ mgxs.h5 - + - + - + - - - - - + + + + + 2.5 2.5 2.5 12 12 12 @@ -182,19 +182,19 @@ 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - - - - - - + + + + + + fixed source 90 40 20 - + 100.0 1.0 @@ -207,7 +207,7 @@ 500.0 100.0 - + 0.0 0.0 0.0 30.0 30.0 30.0 diff --git a/tests/regression_tests/random_ray_volume_estimator/hybrid/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator/hybrid/inputs_true.dat index 9c15ec97d..089534d74 100644 --- a/tests/regression_tests/random_ray_volume_estimator/hybrid/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator/hybrid/inputs_true.dat @@ -3,24 +3,24 @@ mgxs.h5 - + - + - + - - - - - + + + + + 2.5 2.5 2.5 12 12 12 @@ -182,19 +182,19 @@ 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - - - - - - + + + + + + fixed source 90 10 5 - + 100.0 1.0 @@ -207,7 +207,7 @@ 500.0 100.0 - + 0.0 0.0 0.0 30.0 30.0 30.0 diff --git a/tests/regression_tests/random_ray_volume_estimator/naive/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator/naive/inputs_true.dat index 7d05f0978..56507df04 100644 --- a/tests/regression_tests/random_ray_volume_estimator/naive/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator/naive/inputs_true.dat @@ -3,24 +3,24 @@ mgxs.h5 - + - + - + - - - - - + + + + + 2.5 2.5 2.5 12 12 12 @@ -182,19 +182,19 @@ 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - - - - - - + + + + + + fixed source 90 10 5 - + 100.0 1.0 @@ -207,7 +207,7 @@ 500.0 100.0 - + 0.0 0.0 0.0 30.0 30.0 30.0 diff --git a/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/inputs_true.dat index 301671e6d..0331b562c 100644 --- a/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/inputs_true.dat @@ -3,24 +3,24 @@ mgxs.h5 - + - + - + - - - - - + + + + + 2.5 2.5 2.5 12 12 12 @@ -182,19 +182,19 @@ 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - - - - - - + + + + + + fixed source 90 10 5 - + 100.0 1.0 @@ -207,7 +207,7 @@ 500.0 100.0 - + 0.0 0.0 0.0 30.0 30.0 30.0 diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/inputs_true.dat index 6440cca04..343833210 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/inputs_true.dat @@ -3,24 +3,24 @@ mgxs.h5 - + - + - + - - - - - + + + + + 2.5 2.5 2.5 12 12 12 @@ -182,19 +182,19 @@ 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - - - - - - + + + + + + fixed source 90 40 20 - + 100.0 1.0 @@ -207,7 +207,7 @@ 500.0 100.0 - + 0.0 0.0 0.0 30.0 30.0 30.0 diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/naive/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/naive/inputs_true.dat index fcf93b046..50be43eb3 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/naive/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator_linear/naive/inputs_true.dat @@ -3,24 +3,24 @@ mgxs.h5 - + - + - + - - - - - + + + + + 2.5 2.5 2.5 12 12 12 @@ -182,19 +182,19 @@ 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - - - - - - + + + + + + fixed source 90 40 20 - + 100.0 1.0 @@ -207,7 +207,7 @@ 500.0 100.0 - + 0.0 0.0 0.0 30.0 30.0 30.0 diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/inputs_true.dat index 1df3204a3..3d64ab978 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/inputs_true.dat @@ -3,24 +3,24 @@ mgxs.h5 - + - + - + - - - - - + + + + + 2.5 2.5 2.5 12 12 12 @@ -182,19 +182,19 @@ 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - - - - - - + + + + + + fixed source 90 40 20 - + 100.0 1.0 @@ -207,7 +207,7 @@ 500.0 100.0 - + 0.0 0.0 0.0 30.0 30.0 30.0 diff --git a/tests/regression_tests/resonance_scattering/inputs_true.dat b/tests/regression_tests/resonance_scattering/inputs_true.dat index dba4535c2..ebe6a5dbe 100644 --- a/tests/regression_tests/resonance_scattering/inputs_true.dat +++ b/tests/regression_tests/resonance_scattering/inputs_true.dat @@ -1,24 +1,24 @@ - - - - - - + + + + + + - + eigenvalue 1000 10 5 - + -4 -4 -4 4 4 4 diff --git a/tests/regression_tests/salphabeta/inputs_true.dat b/tests/regression_tests/salphabeta/inputs_true.dat index ef0e843c2..56f4be375 100644 --- a/tests/regression_tests/salphabeta/inputs_true.dat +++ b/tests/regression_tests/salphabeta/inputs_true.dat @@ -1,36 +1,36 @@ - - - - - + + + + + - - - - + + + + - - - - - + + + + + - - - - - - - - - - + + + + + + + + + + @@ -40,18 +40,18 @@ - - - - - + + + + + eigenvalue 400 5 0 - + -4 -4 -4 4 4 4 diff --git a/tests/regression_tests/score_current/inputs_true.dat b/tests/regression_tests/score_current/inputs_true.dat index ddbd6c24b..42c2d3df2 100644 --- a/tests/regression_tests/score_current/inputs_true.dat +++ b/tests/regression_tests/score_current/inputs_true.dat @@ -1,28 +1,28 @@ - - - + + + - - + + - - - - - - - - - - + + + + + + + + + + eigenvalue diff --git a/tests/regression_tests/source/inputs_true.dat b/tests/regression_tests/source/inputs_true.dat index e787f24d4..9f10b79d6 100644 --- a/tests/regression_tests/source/inputs_true.dat +++ b/tests/regression_tests/source/inputs_true.dat @@ -1,150 +1,150 @@ - - - + + + - + eigenvalue 1000 10 5 - + - + -4.0 -1.0 3.0 0.2 0.3 0.5 - + -2.0 0.0 2.0 0.2 0.3 0.2 - + -1.0 0.0 1.0 0.5 0.25 0.25 - + - + - + -4.0 -4.0 -4.0 4.0 4.0 4.0 - - + + - + 1.2 -2.3 0.781 - + 1.0 1.3894954943731377 1.93069772888325 2.6826957952797255 3.72759372031494 5.17947467923121 7.196856730011519 10.0 13.894954943731374 19.306977288832496 26.826957952797247 37.2759372031494 51.7947467923121 71.96856730011518 100.0 138.94954943731375 193.06977288832496 268.26957952797244 372.7593720314938 517.9474679231207 719.6856730011514 1000.0 1389.4954943731375 1930.6977288832495 2682.6957952797247 3727.593720314938 5179.474679231207 7196.856730011514 10000.0 13894.95494373136 19306.977288832495 26826.95795279722 37275.93720314938 51794.74679231213 71968.56730011514 100000.0 138949.5494373136 193069.77288832495 268269.5795279722 372759.3720314938 517947.4679231202 719685.6730011514 1000000.0 1389495.494373136 1930697.7288832497 2682695.7952797217 3727593.720314938 5179474.679231202 7196856.730011513 10000000.0 0.0 2.9086439299358713e-08 5.80533561806147e-08 8.67817193689187e-08 1.1515347785771536e-07 1.4305204600565115e-07 1.7036278261198208e-07 1.9697346200185813e-07 2.227747351856934e-07 2.4766057919761985e-07 2.715287327665956e-07 2.9428111652990295e-07 3.1582423606228735e-07 3.360695660646056e-07 3.549339141332686e-07 3.723397626156721e-07 3.882155871468592e-07 4.024961505584776e-07 4.151227709522976e-07 4.260435628367196e-07 4.3521365033538783e-07 4.4259535159179273e-07 4.4815833361210174e-07 4.5187973690993757e-07 4.5374426944091084e-07 4.5374426944091084e-07 4.5187973690993757e-07 4.4815833361210174e-07 4.4259535159179273e-07 4.352136503353879e-07 4.2604356283671966e-07 4.1512277095229767e-07 4.0249615055847764e-07 3.8821558714685926e-07 3.723397626156722e-07 3.5493391413326864e-07 3.360695660646057e-07 3.158242360622874e-07 2.942811165299031e-07 2.715287327665957e-07 2.4766057919762e-07 2.2277473518569352e-07 1.9697346200185819e-07 1.7036278261198226e-07 1.4305204600565126e-07 1.1515347785771556e-07 8.678171936891881e-08 5.805335618061493e-08 2.9086439299358858e-08 5.559621115282002e-23 - - - + + + 0.7071067811865476 0.0 -0.7071067811865475 0.3 0.4 0.3 - + - + 1.0 1.3894954943731377 1.93069772888325 2.6826957952797255 3.72759372031494 5.17947467923121 7.196856730011519 10.0 13.894954943731374 19.306977288832496 26.826957952797247 37.2759372031494 51.7947467923121 71.96856730011518 100.0 138.94954943731375 193.06977288832496 268.26957952797244 372.7593720314938 517.9474679231207 719.6856730011514 1000.0 1389.4954943731375 1930.6977288832495 2682.6957952797247 3727.593720314938 5179.474679231207 7196.856730011514 10000.0 13894.95494373136 19306.977288832495 26826.95795279722 37275.93720314938 51794.74679231213 71968.56730011514 100000.0 138949.5494373136 193069.77288832495 268269.5795279722 372759.3720314938 517947.4679231202 719685.6730011514 1000000.0 1389495.494373136 1930697.7288832497 2682695.7952797217 3727593.720314938 5179474.679231202 7196856.730011513 10000000.0 0.0 2.9086439299358713e-08 5.80533561806147e-08 8.67817193689187e-08 1.1515347785771536e-07 1.4305204600565115e-07 1.7036278261198208e-07 1.9697346200185813e-07 2.227747351856934e-07 2.4766057919761985e-07 2.715287327665956e-07 2.9428111652990295e-07 3.1582423606228735e-07 3.360695660646056e-07 3.549339141332686e-07 3.723397626156721e-07 3.882155871468592e-07 4.024961505584776e-07 4.151227709522976e-07 4.260435628367196e-07 4.3521365033538783e-07 4.4259535159179273e-07 4.4815833361210174e-07 4.5187973690993757e-07 4.5374426944091084e-07 4.5374426944091084e-07 4.5187973690993757e-07 4.4815833361210174e-07 4.4259535159179273e-07 4.352136503353879e-07 4.2604356283671966e-07 4.1512277095229767e-07 4.0249615055847764e-07 3.8821558714685926e-07 3.723397626156722e-07 3.5493391413326864e-07 3.360695660646057e-07 3.158242360622874e-07 2.942811165299031e-07 2.715287327665957e-07 2.4766057919762e-07 2.2277473518569352e-07 1.9697346200185819e-07 1.7036278261198226e-07 1.4305204600565126e-07 1.1515347785771556e-07 8.678171936891881e-08 5.805335618061493e-08 2.9086439299358858e-08 5.559621115282002e-23 - - - - - + + + + + -2.0 0.0 2.0 0.2 0.3 0.2 - + 1.0 1.3894954943731377 1.93069772888325 2.6826957952797255 3.72759372031494 5.17947467923121 7.196856730011519 10.0 13.894954943731374 19.306977288832496 26.826957952797247 37.2759372031494 51.7947467923121 71.96856730011518 100.0 138.94954943731375 193.06977288832496 268.26957952797244 372.7593720314938 517.9474679231207 719.6856730011514 1000.0 1389.4954943731375 1930.6977288832495 2682.6957952797247 3727.593720314938 5179.474679231207 7196.856730011514 10000.0 13894.95494373136 19306.977288832495 26826.95795279722 37275.93720314938 51794.74679231213 71968.56730011514 100000.0 138949.5494373136 193069.77288832495 268269.5795279722 372759.3720314938 517947.4679231202 719685.6730011514 1000000.0 1389495.494373136 1930697.7288832497 2682695.7952797217 3727593.720314938 5179474.679231202 7196856.730011513 10000000.0 0.0 2.9086439299358713e-08 5.80533561806147e-08 8.67817193689187e-08 1.1515347785771536e-07 1.4305204600565115e-07 1.7036278261198208e-07 1.9697346200185813e-07 2.227747351856934e-07 2.4766057919761985e-07 2.715287327665956e-07 2.9428111652990295e-07 3.1582423606228735e-07 3.360695660646056e-07 3.549339141332686e-07 3.723397626156721e-07 3.882155871468592e-07 4.024961505584776e-07 4.151227709522976e-07 4.260435628367196e-07 4.3521365033538783e-07 4.4259535159179273e-07 4.4815833361210174e-07 4.5187973690993757e-07 4.5374426944091084e-07 4.5374426944091084e-07 4.5187973690993757e-07 4.4815833361210174e-07 4.4259535159179273e-07 4.352136503353879e-07 4.2604356283671966e-07 4.1512277095229767e-07 4.0249615055847764e-07 3.8821558714685926e-07 3.723397626156722e-07 3.5493391413326864e-07 3.360695660646057e-07 3.158242360622874e-07 2.942811165299031e-07 2.715287327665957e-07 2.4766057919762e-07 2.2277473518569352e-07 1.9697346200185819e-07 1.7036278261198226e-07 1.4305204600565126e-07 1.1515347785771556e-07 8.678171936891881e-08 5.805335618061493e-08 2.9086439299358858e-08 5.559621115282002e-23 - - - - - + + + + + -2.0 0.0 2.0 0.2 0.3 0.2 - + - + - + 1.0 1.3894954943731377 1.93069772888325 2.6826957952797255 3.72759372031494 5.17947467923121 7.196856730011519 10.0 13.894954943731374 19.306977288832496 26.826957952797247 37.2759372031494 51.7947467923121 71.96856730011518 100.0 138.94954943731375 193.06977288832496 268.26957952797244 372.7593720314938 517.9474679231207 719.6856730011514 1000.0 1389.4954943731375 1930.6977288832495 2682.6957952797247 3727.593720314938 5179.474679231207 7196.856730011514 10000.0 13894.95494373136 19306.977288832495 26826.95795279722 37275.93720314938 51794.74679231213 71968.56730011514 100000.0 138949.5494373136 193069.77288832495 268269.5795279722 372759.3720314938 517947.4679231202 719685.6730011514 1000000.0 1389495.494373136 1930697.7288832497 2682695.7952797217 3727593.720314938 5179474.679231202 7196856.730011513 10000000.0 0.0 2.9086439299358713e-08 5.80533561806147e-08 8.67817193689187e-08 1.1515347785771536e-07 1.4305204600565115e-07 1.7036278261198208e-07 1.9697346200185813e-07 2.227747351856934e-07 2.4766057919761985e-07 2.715287327665956e-07 2.9428111652990295e-07 3.1582423606228735e-07 3.360695660646056e-07 3.549339141332686e-07 3.723397626156721e-07 3.882155871468592e-07 4.024961505584776e-07 4.151227709522976e-07 4.260435628367196e-07 4.3521365033538783e-07 4.4259535159179273e-07 4.4815833361210174e-07 4.5187973690993757e-07 4.5374426944091084e-07 4.5374426944091084e-07 4.5187973690993757e-07 4.4815833361210174e-07 4.4259535159179273e-07 4.352136503353879e-07 4.2604356283671966e-07 4.1512277095229767e-07 4.0249615055847764e-07 3.8821558714685926e-07 3.723397626156722e-07 3.5493391413326864e-07 3.360695660646057e-07 3.158242360622874e-07 2.942811165299031e-07 2.715287327665957e-07 2.4766057919762e-07 2.2277473518569352e-07 1.9697346200185819e-07 1.7036278261198226e-07 1.4305204600565126e-07 1.1515347785771556e-07 8.678171936891881e-08 5.805335618061493e-08 2.9086439299358858e-08 5.559621115282002e-23 - - - + + + 0.7071067811865476 0.0 -0.7071067811865475 0.3 0.4 0.3 - + - + - + - + 1.0 1.3894954943731377 1.93069772888325 2.6826957952797255 3.72759372031494 5.17947467923121 7.196856730011519 10.0 13.894954943731374 19.306977288832496 26.826957952797247 37.2759372031494 51.7947467923121 71.96856730011518 100.0 138.94954943731375 193.06977288832496 268.26957952797244 372.7593720314938 517.9474679231207 719.6856730011514 1000.0 1389.4954943731375 1930.6977288832495 2682.6957952797247 3727.593720314938 5179.474679231207 7196.856730011514 10000.0 13894.95494373136 19306.977288832495 26826.95795279722 37275.93720314938 51794.74679231213 71968.56730011514 100000.0 138949.5494373136 193069.77288832495 268269.5795279722 372759.3720314938 517947.4679231202 719685.6730011514 1000000.0 1389495.494373136 1930697.7288832497 2682695.7952797217 3727593.720314938 5179474.679231202 7196856.730011513 10000000.0 0.0 2.9086439299358713e-08 5.80533561806147e-08 8.67817193689187e-08 1.1515347785771536e-07 1.4305204600565115e-07 1.7036278261198208e-07 1.9697346200185813e-07 2.227747351856934e-07 2.4766057919761985e-07 2.715287327665956e-07 2.9428111652990295e-07 3.1582423606228735e-07 3.360695660646056e-07 3.549339141332686e-07 3.723397626156721e-07 3.882155871468592e-07 4.024961505584776e-07 4.151227709522976e-07 4.260435628367196e-07 4.3521365033538783e-07 4.4259535159179273e-07 4.4815833361210174e-07 4.5187973690993757e-07 4.5374426944091084e-07 4.5374426944091084e-07 4.5187973690993757e-07 4.4815833361210174e-07 4.4259535159179273e-07 4.352136503353879e-07 4.2604356283671966e-07 4.1512277095229767e-07 4.0249615055847764e-07 3.8821558714685926e-07 3.723397626156722e-07 3.5493391413326864e-07 3.360695660646057e-07 3.158242360622874e-07 2.942811165299031e-07 2.715287327665957e-07 2.4766057919762e-07 2.2277473518569352e-07 1.9697346200185819e-07 1.7036278261198226e-07 1.4305204600565126e-07 1.1515347785771556e-07 8.678171936891881e-08 5.805335618061493e-08 2.9086439299358858e-08 5.559621115282002e-23 - diff --git a/tests/regression_tests/source_dlopen/inputs_true.dat b/tests/regression_tests/source_dlopen/inputs_true.dat index 28d1e06e2..9b4601d9f 100644 --- a/tests/regression_tests/source_dlopen/inputs_true.dat +++ b/tests/regression_tests/source_dlopen/inputs_true.dat @@ -2,23 +2,23 @@ - - - - - + + + + + - + fixed source 1000 10 0 - + diff --git a/tests/regression_tests/source_parameterized_dlopen/inputs_true.dat b/tests/regression_tests/source_parameterized_dlopen/inputs_true.dat index 6cf3473f9..088d65ada 100644 --- a/tests/regression_tests/source_parameterized_dlopen/inputs_true.dat +++ b/tests/regression_tests/source_parameterized_dlopen/inputs_true.dat @@ -2,23 +2,23 @@ - - - - - + + + + + - + fixed source 1000 10 0 - + diff --git a/tests/regression_tests/stride/inputs_true.dat b/tests/regression_tests/stride/inputs_true.dat index f93ec33d1..ebae53c05 100644 --- a/tests/regression_tests/stride/inputs_true.dat +++ b/tests/regression_tests/stride/inputs_true.dat @@ -1,21 +1,21 @@ - - - + + + - + eigenvalue 1000 10 5 - + -4 -4 -4 4 4 4 diff --git a/tests/regression_tests/surface_source/inputs_true_read_h5.dat b/tests/regression_tests/surface_source/inputs_true_read_h5.dat index 14a55f0bf..321c11d42 100644 --- a/tests/regression_tests/surface_source/inputs_true_read_h5.dat +++ b/tests/regression_tests/surface_source/inputs_true_read_h5.dat @@ -7,10 +7,10 @@ - - - - + + + + fixed source diff --git a/tests/regression_tests/surface_source/inputs_true_read_mcpl.dat b/tests/regression_tests/surface_source/inputs_true_read_mcpl.dat index b8d8c575a..2ee76f618 100644 --- a/tests/regression_tests/surface_source/inputs_true_read_mcpl.dat +++ b/tests/regression_tests/surface_source/inputs_true_read_mcpl.dat @@ -7,10 +7,10 @@ - - - - + + + + fixed source diff --git a/tests/regression_tests/surface_source/inputs_true_write_h5.dat b/tests/regression_tests/surface_source/inputs_true_write_h5.dat index 5c85de631..10e3af0a7 100644 --- a/tests/regression_tests/surface_source/inputs_true_write_h5.dat +++ b/tests/regression_tests/surface_source/inputs_true_write_h5.dat @@ -7,16 +7,16 @@ - - - - + + + + fixed source 1000 10 - + 0 0 0 diff --git a/tests/regression_tests/surface_source/inputs_true_write_mcpl.dat b/tests/regression_tests/surface_source/inputs_true_write_mcpl.dat index c55223324..e9758144d 100644 --- a/tests/regression_tests/surface_source/inputs_true_write_mcpl.dat +++ b/tests/regression_tests/surface_source/inputs_true_write_mcpl.dat @@ -7,16 +7,16 @@ - - - - + + + + fixed source 1000 10 - + 0 0 0 diff --git a/tests/regression_tests/surface_source_write/case-01/inputs_true.dat b/tests/regression_tests/surface_source_write/case-01/inputs_true.dat index 7368debd5..2a67b03dd 100644 --- a/tests/regression_tests/surface_source_write/case-01/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-01/inputs_true.dat @@ -1,47 +1,47 @@ - - - - - - - + + + + + + + - - - + + + - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + eigenvalue 100 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 diff --git a/tests/regression_tests/surface_source_write/case-02/inputs_true.dat b/tests/regression_tests/surface_source_write/case-02/inputs_true.dat index 12f15499f..527f076b9 100644 --- a/tests/regression_tests/surface_source_write/case-02/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-02/inputs_true.dat @@ -1,47 +1,47 @@ - - - - - - - + + + + + + + - - - + + + - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + eigenvalue 100 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 diff --git a/tests/regression_tests/surface_source_write/case-03/inputs_true.dat b/tests/regression_tests/surface_source_write/case-03/inputs_true.dat index cd0f7deda..58c4e0a24 100644 --- a/tests/regression_tests/surface_source_write/case-03/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-03/inputs_true.dat @@ -1,47 +1,47 @@ - - - - - - - + + + + + + + - - - + + + - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + eigenvalue 100 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 diff --git a/tests/regression_tests/surface_source_write/case-04/inputs_true.dat b/tests/regression_tests/surface_source_write/case-04/inputs_true.dat index ac3c03e37..46701aea7 100644 --- a/tests/regression_tests/surface_source_write/case-04/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-04/inputs_true.dat @@ -1,47 +1,47 @@ - - - - - - - + + + + + + + - - - + + + - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + eigenvalue 100 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 diff --git a/tests/regression_tests/surface_source_write/case-05/inputs_true.dat b/tests/regression_tests/surface_source_write/case-05/inputs_true.dat index 143e85200..c420d797c 100644 --- a/tests/regression_tests/surface_source_write/case-05/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-05/inputs_true.dat @@ -1,47 +1,47 @@ - - - - - - - + + + + + + + - - - + + + - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + eigenvalue 100 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 diff --git a/tests/regression_tests/surface_source_write/case-06/inputs_true.dat b/tests/regression_tests/surface_source_write/case-06/inputs_true.dat index f2dc7ea35..e02d5e90c 100644 --- a/tests/regression_tests/surface_source_write/case-06/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-06/inputs_true.dat @@ -1,47 +1,47 @@ - - - - - - - + + + + + + + - - - + + + - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + eigenvalue 100 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 diff --git a/tests/regression_tests/surface_source_write/case-07/inputs_true.dat b/tests/regression_tests/surface_source_write/case-07/inputs_true.dat index a6107d16f..a4588c8d0 100644 --- a/tests/regression_tests/surface_source_write/case-07/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-07/inputs_true.dat @@ -1,47 +1,47 @@ - - - - - - - + + + + + + + - - - + + + - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + eigenvalue 100 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 diff --git a/tests/regression_tests/surface_source_write/case-08/inputs_true.dat b/tests/regression_tests/surface_source_write/case-08/inputs_true.dat index b87e2b207..ecf3a6a2e 100644 --- a/tests/regression_tests/surface_source_write/case-08/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-08/inputs_true.dat @@ -1,47 +1,47 @@ - - - - - - - + + + + + + + - - - + + + - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + eigenvalue 100 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 diff --git a/tests/regression_tests/surface_source_write/case-09/inputs_true.dat b/tests/regression_tests/surface_source_write/case-09/inputs_true.dat index f4f6ebd87..5d60f9dbe 100644 --- a/tests/regression_tests/surface_source_write/case-09/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-09/inputs_true.dat @@ -1,47 +1,47 @@ - - - - - - - + + + + + + + - - - + + + - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + eigenvalue 100 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 diff --git a/tests/regression_tests/surface_source_write/case-10/inputs_true.dat b/tests/regression_tests/surface_source_write/case-10/inputs_true.dat index 372e5c011..1940826c2 100644 --- a/tests/regression_tests/surface_source_write/case-10/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-10/inputs_true.dat @@ -1,47 +1,47 @@ - - - - - - - + + + + + + + - - - + + + - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + eigenvalue 100 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 diff --git a/tests/regression_tests/surface_source_write/case-11/inputs_true.dat b/tests/regression_tests/surface_source_write/case-11/inputs_true.dat index d5401dc87..a4feff1b1 100644 --- a/tests/regression_tests/surface_source_write/case-11/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-11/inputs_true.dat @@ -1,47 +1,47 @@ - - - - - - - + + + + + + + - - - + + + - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + eigenvalue 100 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 diff --git a/tests/regression_tests/surface_source_write/case-12/inputs_true.dat b/tests/regression_tests/surface_source_write/case-12/inputs_true.dat index cb8f87741..c069f425c 100644 --- a/tests/regression_tests/surface_source_write/case-12/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-12/inputs_true.dat @@ -1,40 +1,40 @@ - - - - - - - + + + + + + + - - - + + + - - - - - - - - - - + + + + + + + + + + eigenvalue 100 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 diff --git a/tests/regression_tests/surface_source_write/case-13/inputs_true.dat b/tests/regression_tests/surface_source_write/case-13/inputs_true.dat index 23bbf0455..2a93fdb4d 100644 --- a/tests/regression_tests/surface_source_write/case-13/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-13/inputs_true.dat @@ -1,40 +1,40 @@ - - - - - - - + + + + + + + - - - + + + - - - - - - - - - - + + + + + + + + + + eigenvalue 100 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 diff --git a/tests/regression_tests/surface_source_write/case-14/inputs_true.dat b/tests/regression_tests/surface_source_write/case-14/inputs_true.dat index 29acd94f2..893f8ddc1 100644 --- a/tests/regression_tests/surface_source_write/case-14/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-14/inputs_true.dat @@ -1,40 +1,40 @@ - - - - - - - + + + + + + + - - - + + + - - - - - - - - - - + + + + + + + + + + eigenvalue 100 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 diff --git a/tests/regression_tests/surface_source_write/case-15/inputs_true.dat b/tests/regression_tests/surface_source_write/case-15/inputs_true.dat index c775f2f52..875a2fb0b 100644 --- a/tests/regression_tests/surface_source_write/case-15/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-15/inputs_true.dat @@ -1,40 +1,40 @@ - - - - - - - + + + + + + + - - - + + + - - - - - - - - - - + + + + + + + + + + eigenvalue 100 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 diff --git a/tests/regression_tests/surface_source_write/case-16/inputs_true.dat b/tests/regression_tests/surface_source_write/case-16/inputs_true.dat index 969a8f31e..347855e90 100644 --- a/tests/regression_tests/surface_source_write/case-16/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-16/inputs_true.dat @@ -1,40 +1,40 @@ - - - - - - - + + + + + + + - - - + + + - - - - - - - - - - + + + + + + + + + + eigenvalue 100 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 diff --git a/tests/regression_tests/surface_source_write/case-17/inputs_true.dat b/tests/regression_tests/surface_source_write/case-17/inputs_true.dat index 0a65db510..95d0a6712 100644 --- a/tests/regression_tests/surface_source_write/case-17/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-17/inputs_true.dat @@ -1,40 +1,40 @@ - - - - - - - + + + + + + + - - - + + + - - - - - - - - - - + + + + + + + + + + eigenvalue 100 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 diff --git a/tests/regression_tests/surface_source_write/case-18/inputs_true.dat b/tests/regression_tests/surface_source_write/case-18/inputs_true.dat index 89df27b47..807c72ae6 100644 --- a/tests/regression_tests/surface_source_write/case-18/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-18/inputs_true.dat @@ -1,40 +1,40 @@ - - - - - - - + + + + + + + - - - + + + - - - - - - - - - - + + + + + + + + + + eigenvalue 100 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 diff --git a/tests/regression_tests/surface_source_write/case-19/inputs_true.dat b/tests/regression_tests/surface_source_write/case-19/inputs_true.dat index 3ea7dbffe..42aa78a09 100644 --- a/tests/regression_tests/surface_source_write/case-19/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-19/inputs_true.dat @@ -1,40 +1,40 @@ - - - - - - - + + + + + + + - - - + + + - - - - - - - - - - + + + + + + + + + + eigenvalue 100 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 diff --git a/tests/regression_tests/surface_source_write/case-20/inputs_true.dat b/tests/regression_tests/surface_source_write/case-20/inputs_true.dat index c3c768a3d..4c7a3f11d 100644 --- a/tests/regression_tests/surface_source_write/case-20/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-20/inputs_true.dat @@ -1,40 +1,40 @@ - - - - - - - + + + + + + + - - - + + + - - - - - - - - - - + + + + + + + + + + eigenvalue 100 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 diff --git a/tests/regression_tests/surface_source_write/case-21/inputs_true.dat b/tests/regression_tests/surface_source_write/case-21/inputs_true.dat index 5dae8374d..71f9aae6a 100644 --- a/tests/regression_tests/surface_source_write/case-21/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-21/inputs_true.dat @@ -1,40 +1,40 @@ - - - - - - - + + + + + + + - - - + + + - - - - - - - - - - + + + + + + + + + + eigenvalue 100 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 diff --git a/tests/regression_tests/surface_source_write/case-a01/inputs_true.dat b/tests/regression_tests/surface_source_write/case-a01/inputs_true.dat index 5cec1b76f..e9840be87 100644 --- a/tests/regression_tests/surface_source_write/case-a01/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-a01/inputs_true.dat @@ -1,47 +1,47 @@ - - - - - - - + + + + + + + - - - + + + - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + eigenvalue 100 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 diff --git a/tests/regression_tests/surface_source_write/case-d01/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d01/inputs_true.dat index b5452080c..04703acfe 100644 --- a/tests/regression_tests/surface_source_write/case-d01/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-d01/inputs_true.dat @@ -1,26 +1,26 @@ - - - + + + - - - + + + - + eigenvalue 100 5 1 - + -4 -4 -20 4 4 20 diff --git a/tests/regression_tests/surface_source_write/case-d02/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d02/inputs_true.dat index d6ca4143c..d8b4e6803 100644 --- a/tests/regression_tests/surface_source_write/case-d02/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-d02/inputs_true.dat @@ -1,26 +1,26 @@ - - - + + + - - - + + + - + eigenvalue 100 5 1 - + -4 -4 -20 4 4 20 diff --git a/tests/regression_tests/surface_source_write/case-d03/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d03/inputs_true.dat index ba326c978..d769184aa 100644 --- a/tests/regression_tests/surface_source_write/case-d03/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-d03/inputs_true.dat @@ -1,26 +1,26 @@ - - - + + + - - - + + + - + eigenvalue 100 5 1 - + -4 -4 -20 4 4 20 diff --git a/tests/regression_tests/surface_source_write/case-d04/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d04/inputs_true.dat index c309dd155..abefc2692 100644 --- a/tests/regression_tests/surface_source_write/case-d04/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-d04/inputs_true.dat @@ -1,26 +1,26 @@ - - - + + + - - - + + + - + eigenvalue 100 5 1 - + -4 -4 -20 4 4 20 diff --git a/tests/regression_tests/surface_source_write/case-d05/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d05/inputs_true.dat index c49703f83..8b46b2b4e 100644 --- a/tests/regression_tests/surface_source_write/case-d05/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-d05/inputs_true.dat @@ -1,26 +1,26 @@ - - - + + + - - - + + + - + eigenvalue 100 5 1 - + -4 -4 -20 4 4 20 diff --git a/tests/regression_tests/surface_source_write/case-d06/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d06/inputs_true.dat index 48db2c30d..ca52cf455 100644 --- a/tests/regression_tests/surface_source_write/case-d06/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-d06/inputs_true.dat @@ -1,26 +1,26 @@ - - - + + + - - - + + + - + eigenvalue 100 5 1 - + -4 -4 -20 4 4 20 diff --git a/tests/regression_tests/surface_source_write/case-d07/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d07/inputs_true.dat index e2897125b..40c439fe6 100644 --- a/tests/regression_tests/surface_source_write/case-d07/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-d07/inputs_true.dat @@ -1,40 +1,40 @@ - - - + + + - - - + + + - + - - - - - - - - - - - - - + + + + + + + + + + + + + eigenvalue 100 5 1 - + -4 -4 -20 4 4 20 diff --git a/tests/regression_tests/surface_source_write/case-d08/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d08/inputs_true.dat index 185b8629b..c81d21b97 100644 --- a/tests/regression_tests/surface_source_write/case-d08/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-d08/inputs_true.dat @@ -1,40 +1,40 @@ - - - + + + - - - + + + - + - - - - - - - - - - - - - + + + + + + + + + + + + + eigenvalue 100 5 1 - + -4 -4 -20 4 4 20 diff --git a/tests/regression_tests/surface_source_write/case-e01/inputs_true.dat b/tests/regression_tests/surface_source_write/case-e01/inputs_true.dat index ac3c03e37..46701aea7 100644 --- a/tests/regression_tests/surface_source_write/case-e01/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-e01/inputs_true.dat @@ -1,47 +1,47 @@ - - - - - - - + + + + + + + - - - + + + - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + eigenvalue 100 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 diff --git a/tests/regression_tests/surface_source_write/case-e02/inputs_true.dat b/tests/regression_tests/surface_source_write/case-e02/inputs_true.dat index a6107d16f..a4588c8d0 100644 --- a/tests/regression_tests/surface_source_write/case-e02/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-e02/inputs_true.dat @@ -1,47 +1,47 @@ - - - - - - - + + + + + + + - - - + + + - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + eigenvalue 100 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 diff --git a/tests/regression_tests/surface_source_write/case-e03/inputs_true.dat b/tests/regression_tests/surface_source_write/case-e03/inputs_true.dat index 23bbf0455..2a93fdb4d 100644 --- a/tests/regression_tests/surface_source_write/case-e03/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-e03/inputs_true.dat @@ -1,40 +1,40 @@ - - - - - - - + + + + + + + - - - + + + - - - - - - - - - - + + + + + + + + + + eigenvalue 100 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 diff --git a/tests/regression_tests/surface_tally/inputs_true.dat b/tests/regression_tests/surface_tally/inputs_true.dat index 192c2e89f..2b070c5a3 100644 --- a/tests/regression_tests/surface_tally/inputs_true.dat +++ b/tests/regression_tests/surface_tally/inputs_true.dat @@ -1,35 +1,35 @@ - - - - - + + + + + - - - - + + + + - - - - - - - - + + + + + + + + eigenvalue 1000 10 0 - + -0.62992 -0.62992 -1 0.62992 0.62992 1 diff --git a/tests/regression_tests/tallies/inputs_true.dat b/tests/regression_tests/tallies/inputs_true.dat index 2ce36c5df..40829f865 100644 --- a/tests/regression_tests/tallies/inputs_true.dat +++ b/tests/regression_tests/tallies/inputs_true.dat @@ -1,40 +1,40 @@ - - - - - - - + + + + + + + - - - - - - + + + + + + - - - - - + + + + + - - - - - + + + + + - + @@ -47,7 +47,7 @@ - + @@ -62,7 +62,7 @@ - + @@ -77,7 +77,7 @@ - + @@ -92,7 +92,7 @@ - + @@ -107,7 +107,7 @@ - + @@ -122,7 +122,7 @@ - + @@ -135,7 +135,7 @@ - + @@ -149,8 +149,8 @@ - - + + @@ -174,9 +174,9 @@ - + - + 1.26 1.26 17 17 @@ -277,30 +277,30 @@ 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + eigenvalue 400 5 0 - + -160 -160 -183 160 160 183 @@ -342,7 +342,7 @@ 4 - + 4 diff --git a/tests/regression_tests/tally_aggregation/inputs_true.dat b/tests/regression_tests/tally_aggregation/inputs_true.dat index 9f4f95655..7351b230c 100644 --- a/tests/regression_tests/tally_aggregation/inputs_true.dat +++ b/tests/regression_tests/tally_aggregation/inputs_true.dat @@ -1,24 +1,24 @@ - - - - - - + + + + + + - - - + + + - + 1.2 1.2 1 @@ -28,11 +28,11 @@ 1 1 1 1 - - - - - + + + + + eigenvalue diff --git a/tests/regression_tests/tally_arithmetic/inputs_true.dat b/tests/regression_tests/tally_arithmetic/inputs_true.dat index 7669b4d03..2b5234c34 100644 --- a/tests/regression_tests/tally_arithmetic/inputs_true.dat +++ b/tests/regression_tests/tally_arithmetic/inputs_true.dat @@ -1,24 +1,24 @@ - - - - - + + + + + - - - + + + - - + + eigenvalue diff --git a/tests/regression_tests/tally_slice_merge/inputs_true.dat b/tests/regression_tests/tally_slice_merge/inputs_true.dat index 7159d833a..7a992cc39 100644 --- a/tests/regression_tests/tally_slice_merge/inputs_true.dat +++ b/tests/regression_tests/tally_slice_merge/inputs_true.dat @@ -1,40 +1,40 @@ - - - - - - - + + + + + + + - - - - - - + + + + + + - - - - - + + + + + - - - - - + + + + + - + @@ -47,7 +47,7 @@ - + @@ -62,7 +62,7 @@ - + @@ -77,7 +77,7 @@ - + @@ -92,7 +92,7 @@ - + @@ -107,7 +107,7 @@ - + @@ -122,7 +122,7 @@ - + @@ -135,7 +135,7 @@ - + @@ -149,8 +149,8 @@ - - + + @@ -174,9 +174,9 @@ - + - + 1.26 1.26 17 17 @@ -277,30 +277,30 @@ 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 7 - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + eigenvalue 100 10 5 - + -160 -160 -183 160 160 183 diff --git a/tests/regression_tests/time_cutoff/inputs_true.dat b/tests/regression_tests/time_cutoff/inputs_true.dat index 7d3e94f50..e1102d475 100644 --- a/tests/regression_tests/time_cutoff/inputs_true.dat +++ b/tests/regression_tests/time_cutoff/inputs_true.dat @@ -4,13 +4,13 @@ - + fixed source 100 10 - + 0.0 0.0 0.0 diff --git a/tests/regression_tests/torus/inputs_true.dat b/tests/regression_tests/torus/inputs_true.dat index a5c7a8d7a..df4af1443 100644 --- a/tests/regression_tests/torus/inputs_true.dat +++ b/tests/regression_tests/torus/inputs_true.dat @@ -1,13 +1,13 @@ - - - + + + - - + + @@ -15,15 +15,15 @@ - - - - - - - - - + + + + + + + + + eigenvalue diff --git a/tests/regression_tests/torus/large_major/inputs_true.dat b/tests/regression_tests/torus/large_major/inputs_true.dat index fa7deb29a..513a8c67e 100644 --- a/tests/regression_tests/torus/large_major/inputs_true.dat +++ b/tests/regression_tests/torus/large_major/inputs_true.dat @@ -2,27 +2,27 @@ - - + + - - + + - - - + + + fixed source 1000 10 - + -1000.0 0 0 diff --git a/tests/regression_tests/trigger_statepoint_restart/inputs_true.dat b/tests/regression_tests/trigger_statepoint_restart/inputs_true.dat index 6f0b629eb..01755ad8b 100644 --- a/tests/regression_tests/trigger_statepoint_restart/inputs_true.dat +++ b/tests/regression_tests/trigger_statepoint_restart/inputs_true.dat @@ -1,14 +1,14 @@ - - - + + + - + eigenvalue diff --git a/tests/regression_tests/triso/inputs_true.dat b/tests/regression_tests/triso/inputs_true.dat index 37f37af78..c96684568 100644 --- a/tests/regression_tests/triso/inputs_true.dat +++ b/tests/regression_tests/triso/inputs_true.dat @@ -1,38 +1,38 @@ - - - - - - + + + + + + - - + + - - + + - - - - - + + + + + - - + + - - + + @@ -42,171 +42,171 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -253,187 +253,187 @@ 24 25 26 21 22 23 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue 100 4 0 - + 0.0 0.0 0.0 diff --git a/tests/regression_tests/unstructured_mesh/inputs_true.dat b/tests/regression_tests/unstructured_mesh/inputs_true.dat index 07486d3e0..e7a485b4d 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true.dat @@ -1,54 +1,54 @@ - - - + + + - - - - - - + + + + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + fixed source 1000 10 - - - + + + 1.0 1.0 @@ -67,7 +67,7 @@ -10.0 -10.0 -10.0 10.0 10.0 10.0
    - + test_mesh_hexes.e diff --git a/tests/regression_tests/unstructured_mesh/inputs_true0.dat b/tests/regression_tests/unstructured_mesh/inputs_true0.dat index c6b28d890..2508c25b3 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true0.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true0.dat @@ -1,54 +1,54 @@ - - - + + + - - - - - - + + + + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + fixed source 1000 10 - - - + + + 1.0 1.0 @@ -67,7 +67,7 @@ -10.0 -10.0 -10.0 10.0 10.0 10.0 - + test_mesh_tets_w_holes.e diff --git a/tests/regression_tests/unstructured_mesh/inputs_true1.dat b/tests/regression_tests/unstructured_mesh/inputs_true1.dat index 40183c89b..04bbdb4b2 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true1.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true1.dat @@ -1,54 +1,54 @@ - - - + + + - - - - - - + + + + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + fixed source 1000 10 - - - + + + 1.0 1.0 @@ -67,7 +67,7 @@ -10.0 -10.0 -10.0 10.0 10.0 10.0 - + test_mesh_tets.e diff --git a/tests/regression_tests/unstructured_mesh/inputs_true10.dat b/tests/regression_tests/unstructured_mesh/inputs_true10.dat index 81dfbdc52..169f0a6b4 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true10.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true10.dat @@ -1,54 +1,54 @@ - - - + + + - - - - - - + + + + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + fixed source 1000 10 - - - + + + 1.0 1.0 @@ -67,7 +67,7 @@ -10.0 -10.0 -10.0 10.0 10.0 10.0 - + test_mesh_tets_w_holes.e diff --git a/tests/regression_tests/unstructured_mesh/inputs_true11.dat b/tests/regression_tests/unstructured_mesh/inputs_true11.dat index b224a3ebc..8163750ca 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true11.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true11.dat @@ -1,54 +1,54 @@ - - - + + + - - - - - - + + + + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + fixed source 1000 10 - - - + + + 1.0 1.0 @@ -67,7 +67,7 @@ -10.0 -10.0 -10.0 10.0 10.0 10.0 - + test_mesh_tets.e diff --git a/tests/regression_tests/unstructured_mesh/inputs_true12.dat b/tests/regression_tests/unstructured_mesh/inputs_true12.dat index a18aee627..be0655e98 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true12.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true12.dat @@ -1,54 +1,54 @@ - - - + + + - - - - - - + + + + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + fixed source 1000 10 - - - + + + 1.0 1.0 @@ -67,7 +67,7 @@ -10.0 -10.0 -10.0 10.0 10.0 10.0 - + test_mesh_tets_w_holes.e diff --git a/tests/regression_tests/unstructured_mesh/inputs_true13.dat b/tests/regression_tests/unstructured_mesh/inputs_true13.dat index 0e9123c90..42c0cc122 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true13.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true13.dat @@ -1,54 +1,54 @@ - - - + + + - - - - - - + + + + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + fixed source 1000 10 - - - + + + 1.0 1.0 @@ -67,7 +67,7 @@ -10.0 -10.0 -10.0 10.0 10.0 10.0 - + test_mesh_tets.e diff --git a/tests/regression_tests/unstructured_mesh/inputs_true14.dat b/tests/regression_tests/unstructured_mesh/inputs_true14.dat index 9d4db4904..a27c7445d 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true14.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true14.dat @@ -1,54 +1,54 @@ - - - + + + - - - - - - + + + + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + fixed source 1000 10 - - - + + + 1.0 1.0 @@ -67,7 +67,7 @@ -10.0 -10.0 -10.0 10.0 10.0 10.0 - + test_mesh_tets_w_holes.e diff --git a/tests/regression_tests/unstructured_mesh/inputs_true15.dat b/tests/regression_tests/unstructured_mesh/inputs_true15.dat index eab97b952..c69bd15d0 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true15.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true15.dat @@ -1,54 +1,54 @@ - - - + + + - - - - - - + + + + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + fixed source 1000 10 - - - + + + 1.0 1.0 @@ -67,7 +67,7 @@ -10.0 -10.0 -10.0 10.0 10.0 10.0 - + test_mesh_tets.e diff --git a/tests/regression_tests/unstructured_mesh/inputs_true2.dat b/tests/regression_tests/unstructured_mesh/inputs_true2.dat index 3521ac7b8..ba9d477b5 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true2.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true2.dat @@ -1,54 +1,54 @@ - - - + + + - - - - - - + + + + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + fixed source 1000 10 - - - + + + 1.0 1.0 @@ -67,7 +67,7 @@ -10.0 -10.0 -10.0 10.0 10.0 10.0 - + test_mesh_tets_w_holes.e diff --git a/tests/regression_tests/unstructured_mesh/inputs_true3.dat b/tests/regression_tests/unstructured_mesh/inputs_true3.dat index a376b401d..d840bfa09 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true3.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true3.dat @@ -1,54 +1,54 @@ - - - + + + - - - - - - + + + + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + fixed source 1000 10 - - - + + + 1.0 1.0 @@ -67,7 +67,7 @@ -10.0 -10.0 -10.0 10.0 10.0 10.0 - + test_mesh_tets.e diff --git a/tests/regression_tests/unstructured_mesh/inputs_true4.dat b/tests/regression_tests/unstructured_mesh/inputs_true4.dat index 843632fdd..db8e37177 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true4.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true4.dat @@ -1,54 +1,54 @@ - - - + + + - - - - - - - + + + + + + + - - - - + + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + fixed source - 100 + 1000 10 - - - + + + 1.0 1.0 @@ -56,34 +56,33 @@ 0.0 1.0 - 15000000.0 1.0 - + 10 10 10 -10.0 -10.0 -10.0 10.0 10.0 10.0 - - test_mesh_tets_w_holes.exo + + test_mesh_tets_w_holes.e - - 9 + + 1 - - 10 + + 2 - - 9 + + 1 flux tracklength - - 10 + + 2 flux tracklength diff --git a/tests/regression_tests/unstructured_mesh/inputs_true5.dat b/tests/regression_tests/unstructured_mesh/inputs_true5.dat index dc9129d98..b2bbc7e16 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true5.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true5.dat @@ -1,54 +1,54 @@ - - - + + + - - - - - - - + + + + + + + - - - - + + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + fixed source - 100 + 1000 10 - - - + + + 1.0 1.0 @@ -56,34 +56,33 @@ 0.0 1.0 - 15000000.0 1.0 - + 10 10 10 -10.0 -10.0 -10.0 10.0 10.0 10.0 - - test_mesh_tets.exo + + test_mesh_tets.e - - 11 + + 1 - - 12 + + 2 - - 11 + + 1 flux tracklength - - 12 + + 2 flux tracklength diff --git a/tests/regression_tests/unstructured_mesh/inputs_true6.dat b/tests/regression_tests/unstructured_mesh/inputs_true6.dat index 82bc30703..d6ad0d98d 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true6.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true6.dat @@ -1,54 +1,54 @@ - - - + + + - - - - - - - + + + + + + + - - - - + + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + fixed source - 100 + 1000 10 - - - + + + 1.0 1.0 @@ -56,34 +56,33 @@ 0.0 1.0 - 15000000.0 1.0 - + 10 10 10 -10.0 -10.0 -10.0 10.0 10.0 10.0 - - test_mesh_tets_w_holes.exo + + test_mesh_tets_w_holes.e - - 13 + + 1 - - 14 + + 2 - - 13 + + 1 flux tracklength - - 14 + + 2 flux tracklength diff --git a/tests/regression_tests/unstructured_mesh/inputs_true7.dat b/tests/regression_tests/unstructured_mesh/inputs_true7.dat index b982c40d0..924790a8c 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true7.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true7.dat @@ -1,54 +1,54 @@ - - - + + + - - - - - - - + + + + + + + - - - - + + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + fixed source - 100 + 1000 10 - - - + + + 1.0 1.0 @@ -56,34 +56,33 @@ 0.0 1.0 - 15000000.0 1.0 - + 10 10 10 -10.0 -10.0 -10.0 10.0 10.0 10.0 - - test_mesh_tets.exo + + test_mesh_tets.e - - 15 + + 1 - - 16 + + 2 - - 15 + + 1 flux tracklength - - 16 + + 2 flux tracklength diff --git a/tests/regression_tests/unstructured_mesh/inputs_true8.dat b/tests/regression_tests/unstructured_mesh/inputs_true8.dat index 7994e1add..c04beb211 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true8.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true8.dat @@ -1,54 +1,54 @@ - - - + + + - - - - - - + + + + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + fixed source 1000 10 - - - + + + 1.0 1.0 @@ -67,7 +67,7 @@ -10.0 -10.0 -10.0 10.0 10.0 10.0 - + test_mesh_tets_w_holes.e diff --git a/tests/regression_tests/unstructured_mesh/inputs_true9.dat b/tests/regression_tests/unstructured_mesh/inputs_true9.dat index 4fff123d7..111898ff2 100644 --- a/tests/regression_tests/unstructured_mesh/inputs_true9.dat +++ b/tests/regression_tests/unstructured_mesh/inputs_true9.dat @@ -1,54 +1,54 @@ - - - + + + - - - - - - + + + + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + fixed source 1000 10 - - - + + + 1.0 1.0 @@ -67,7 +67,7 @@ -10.0 -10.0 -10.0 10.0 10.0 10.0 - + test_mesh_tets.e diff --git a/tests/regression_tests/void/inputs_true.dat b/tests/regression_tests/void/inputs_true.dat index 7fee0779e..cd2d14206 100644 --- a/tests/regression_tests/void/inputs_true.dat +++ b/tests/regression_tests/void/inputs_true.dat @@ -2,8 +2,8 @@ - - + + @@ -58,62 +58,62 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fixed source 1000 3 - + 0.0 0.0 0.0 diff --git a/tests/regression_tests/volume_calc/inputs_true.dat b/tests/regression_tests/volume_calc/inputs_true.dat index e3a8162e3..ed7024c57 100644 --- a/tests/regression_tests/volume_calc/inputs_true.dat +++ b/tests/regression_tests/volume_calc/inputs_true.dat @@ -2,27 +2,27 @@ - - - - + + + + - - - - + + + + - - - - - + + + + + volume @@ -53,7 +53,7 @@ 100 -1.0 -1.0 -6.0 1.0 1.0 6.0 - + material @@ -61,7 +61,7 @@ 100 -1.0 -1.0 -6.0 1.0 1.0 6.0 - + cell @@ -69,7 +69,7 @@ 100 -1.0 -1.0 -6.0 1.0 1.0 6.0 - + diff --git a/tests/regression_tests/volume_calc/inputs_true_mg.dat b/tests/regression_tests/volume_calc/inputs_true_mg.dat index 2ace5cd3c..127566084 100644 --- a/tests/regression_tests/volume_calc/inputs_true_mg.dat +++ b/tests/regression_tests/volume_calc/inputs_true_mg.dat @@ -3,26 +3,26 @@ mg_lib.h5 - - - - + + + + - - - - + + + + - - - - - + + + + + volume @@ -54,7 +54,7 @@ 100 -1.0 -1.0 -6.0 1.0 1.0 6.0 - + material @@ -62,7 +62,7 @@ 100 -1.0 -1.0 -6.0 1.0 1.0 6.0 - + cell @@ -70,7 +70,7 @@ 100 -1.0 -1.0 -6.0 1.0 1.0 6.0 - + diff --git a/tests/regression_tests/weightwindows/inputs_true.dat b/tests/regression_tests/weightwindows/inputs_true.dat index 91fec4af9..eb9393179 100644 --- a/tests/regression_tests/weightwindows/inputs_true.dat +++ b/tests/regression_tests/weightwindows/inputs_true.dat @@ -2,39 +2,39 @@ - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - + + fixed source 200 2 - + 0.001 0.001 0.001 diff --git a/tests/regression_tests/weightwindows_fw_cadis/inputs_true.dat b/tests/regression_tests/weightwindows_fw_cadis/inputs_true.dat index 586bbb120..448b4b145 100644 --- a/tests/regression_tests/weightwindows_fw_cadis/inputs_true.dat +++ b/tests/regression_tests/weightwindows_fw_cadis/inputs_true.dat @@ -3,24 +3,24 @@ mgxs.h5 - + - + - + - - - - - + + + + + 2.5 2.5 2.5 12 12 12 @@ -182,19 +182,19 @@ 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - - - - - - + + + + + + fixed source 90 10 5 - + 100.0 1.0 @@ -222,7 +222,7 @@ 500.0 100.0 - + 0.0 0.0 0.0 30.0 30.0 30.0 diff --git a/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/inputs_true.dat b/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/inputs_true.dat index ffb2ac112..d82aa6fae 100644 --- a/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/inputs_true.dat +++ b/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/inputs_true.dat @@ -3,24 +3,24 @@ mgxs.h5 - + - + - + - - - - - + + + + + 2.5 2.5 2.5 12 12 12 @@ -182,19 +182,19 @@ 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - - - - - - + + + + + + fixed source 750 30 20 - + 100.0 1.0 @@ -222,7 +222,7 @@ 500.0 100.0 - + 0.0 0.0 0.0 30.0 30.0 30.0 diff --git a/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/inputs_true.dat b/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/inputs_true.dat index 6aa91ca79..9e4b21d27 100644 --- a/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/inputs_true.dat +++ b/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/inputs_true.dat @@ -3,24 +3,24 @@ mgxs.h5 - + - + - + - - - - - + + + + + 2.5 2.5 2.5 12 12 12 @@ -182,19 +182,19 @@ 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - - - - - - + + + + + + fixed source 750 30 20 - + 100.0 1.0 @@ -222,7 +222,7 @@ 500.0 100.0 - + 0.0 0.0 0.0 30.0 30.0 30.0 From e36c0aef2f9603cec8ad3b171b7667ed9ab773ff Mon Sep 17 00:00:00 2001 From: Boris Polania Date: Fri, 8 Aug 2025 07:09:24 -0700 Subject: [PATCH 387/671] Add stat:sum field to MCPL files for proper weight normalization (#3522) Co-authored-by: Paul Romano --- include/openmc/mcpl_interface.h | 13 ++- src/mcpl_interface.cpp | 36 +++++++ tests/cpp_unit_tests/CMakeLists.txt | 1 + tests/cpp_unit_tests/test_mcpl_stat_sum.cpp | 108 ++++++++++++++++++++ tests/unit_tests/test_mcpl_stat_sum.py | 69 +++++++++++++ 5 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 tests/cpp_unit_tests/test_mcpl_stat_sum.cpp create mode 100644 tests/unit_tests/test_mcpl_stat_sum.py diff --git a/include/openmc/mcpl_interface.h b/include/openmc/mcpl_interface.h index f7323e10a..a76d72e64 100644 --- a/include/openmc/mcpl_interface.h +++ b/include/openmc/mcpl_interface.h @@ -19,8 +19,17 @@ namespace openmc { //! \return Vector of source sites vector mcpl_source_sites(std::string path); -//! Write an MCPL source file -// +//! Write an MCPL source file with stat:sum metadata +//! +//! This function writes particle data to an MCPL file. For MCPL >= 2.1.0, +//! it includes a stat:sum field (key: "openmc_np1") containing the total +//! number of source particles, which is essential for proper file merging +//! and weight normalization when using MCPL files with McStas/McXtrace. +//! +//! The stat:sum field follows the crash-safety pattern: +//! - Initially set to -1 when opening (indicates incomplete file) +//! - Updated with actual particle count before closing +//! //! \param[in] filename Path to MCPL file //! \param[in] source_bank Vector of SourceSites to write to file for this //! MPI rank. diff --git a/src/mcpl_interface.cpp b/src/mcpl_interface.cpp index 13915c3b8..b8e780710 100644 --- a/src/mcpl_interface.cpp +++ b/src/mcpl_interface.cpp @@ -67,6 +67,8 @@ using mcpl_hdr_set_srcname_fpt = void (*)( using mcpl_add_particle_fpt = void (*)( mcpl_outfile_t* outfile_handle, const mcpl_particle_repr_t* particle); using mcpl_close_outfile_fpt = void (*)(mcpl_outfile_t* outfile_handle); +using mcpl_hdr_add_stat_sum_fpt = void (*)( + mcpl_outfile_t* outfile_handle, const char* key, double value); namespace openmc { @@ -110,6 +112,7 @@ struct McplApi { mcpl_hdr_set_srcname_fpt hdr_set_srcname; mcpl_add_particle_fpt add_particle; mcpl_close_outfile_fpt close_outfile; + mcpl_hdr_add_stat_sum_fpt hdr_add_stat_sum; explicit McplApi(LibraryHandleType lib_handle) { @@ -147,6 +150,15 @@ struct McplApi { load_symbol_platform("mcpl_add_particle")); close_outfile = reinterpret_cast( load_symbol_platform("mcpl_close_outfile")); + + // Try to load mcpl_hdr_add_stat_sum (available in MCPL >= 2.1.0) + // Set to nullptr if not available for graceful fallback + try { + hdr_add_stat_sum = reinterpret_cast( + load_symbol_platform("mcpl_hdr_add_stat_sum")); + } catch (const std::runtime_error&) { + hdr_add_stat_sum = nullptr; + } } }; @@ -498,12 +510,36 @@ void write_mcpl_source_point(const char* filename, span source_bank, "OpenMC {}.{}.{}", VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE); } g_mcpl_api->hdr_set_srcname(file_id, src_line.c_str()); + + // Initialize stat:sum with -1 to indicate incomplete file (issue #3514) + // This follows MCPL >= 2.1.0 convention for tracking simulation statistics + // The -1 value indicates "not available" if file creation is interrupted + if (g_mcpl_api->hdr_add_stat_sum) { + // Using key "openmc_np1" following tkittel's recommendation + // Initial value of -1 prevents misleading values in case of crashes + g_mcpl_api->hdr_add_stat_sum(file_id, "openmc_np1", -1.0); + } } write_mcpl_source_bank_internal(file_id, source_bank, bank_index); if (mpi::master) { if (file_id) { + // Update stat:sum with actual particle count before closing (issue #3514) + // This represents the original number of source particles in the + // simulation (not the number of particles in the file) + if (g_mcpl_api->hdr_add_stat_sum) { + // Calculate total source particles from active batches + // Per issue #3514: this should be the original number of source + // particles, not the number written to the file + int64_t total_source_particles = + static_cast(settings::n_batches - settings::n_inactive) * + settings::gen_per_batch * settings::n_particles; + // Update with actual count - this overwrites the initial -1 value + g_mcpl_api->hdr_add_stat_sum( + file_id, "openmc_np1", static_cast(total_source_particles)); + } + g_mcpl_api->close_outfile(file_id); } } diff --git a/tests/cpp_unit_tests/CMakeLists.txt b/tests/cpp_unit_tests/CMakeLists.txt index f0f5f2853..8fedc2daa 100644 --- a/tests/cpp_unit_tests/CMakeLists.txt +++ b/tests/cpp_unit_tests/CMakeLists.txt @@ -4,6 +4,7 @@ set(TEST_NAMES test_tally test_interpolate test_math + test_mcpl_stat_sum # Add additional unit test files here ) diff --git a/tests/cpp_unit_tests/test_mcpl_stat_sum.cpp b/tests/cpp_unit_tests/test_mcpl_stat_sum.cpp new file mode 100644 index 000000000..909830e03 --- /dev/null +++ b/tests/cpp_unit_tests/test_mcpl_stat_sum.cpp @@ -0,0 +1,108 @@ +#include +#include +#include +#include + +#include "openmc/bank.h" +#include "openmc/mcpl_interface.h" + +// Test the MCPL stat:sum functionality (issue #3514) +TEST_CASE("MCPL stat:sum field") +{ + // Check if MCPL interface is available + if (!openmc::is_mcpl_interface_available()) { + SKIP("MCPL library not available"); + } + + SECTION("stat:sum field is written to MCPL files") + { + // Create a temporary filename + std::string filename = "test_stat_sum.mcpl"; + + // Create some test particles + std::vector source_bank(100); + std::vector bank_index = {0, 100}; // 100 particles total + + // Initialize test particles + for (int i = 0; i < 100; ++i) { + source_bank[i].particle = openmc::ParticleType::neutron; + source_bank[i].r = {i * 0.1, i * 0.2, i * 0.3}; + source_bank[i].u = {0.0, 0.0, 1.0}; + source_bank[i].E = 2.0e6; // 2 MeV + source_bank[i].time = 0.0; + source_bank[i].wgt = 1.0; + } + + // Write the MCPL file + openmc::write_mcpl_source_point(filename.c_str(), source_bank, bank_index); + + // Verify the file was created + FILE* f = std::fopen(filename.c_str(), "r"); + REQUIRE(f != nullptr); + std::fclose(f); + + // Read the file back to check stat:sum + // Note: This would require mcpl_open_file and checking the header + // Since we can't easily read MCPL headers in C++ without the full MCPL API, + // we rely on the Python test to verify the actual content + + // Clean up + std::remove(filename.c_str()); + } + + SECTION("stat:sum uses correct particle count") + { + std::string filename = "test_count.mcpl"; + + // Test with different particle counts + std::vector test_counts = {1, 10, 100, 1000}; + + for (int count : test_counts) { + std::vector source_bank(count); + std::vector bank_index = {0, count}; + + // Initialize particles + for (int i = 0; i < count; ++i) { + source_bank[i].particle = openmc::ParticleType::neutron; + source_bank[i].r = {0.0, 0.0, 0.0}; + source_bank[i].u = {0.0, 0.0, 1.0}; + source_bank[i].E = 1.0e6; + source_bank[i].time = 0.0; + source_bank[i].wgt = 1.0; + } + + // Write MCPL file + openmc::write_mcpl_source_point( + filename.c_str(), source_bank, bank_index); + + // The stat:sum should equal count (verified by Python test) + // Here we just verify the file was created successfully + FILE* f = std::fopen(filename.c_str(), "r"); + REQUIRE(f != nullptr); + std::fclose(f); + + // Clean up + std::remove(filename.c_str()); + } + } + + SECTION("stat:sum handles empty particle bank") + { + std::string filename = "test_empty.mcpl"; + + // Create empty particle bank + std::vector source_bank; + std::vector bank_index = {0}; + + // This should still create a valid MCPL file with stat:sum = 0 + openmc::write_mcpl_source_point(filename.c_str(), source_bank, bank_index); + + // Verify file was created + FILE* f = std::fopen(filename.c_str(), "r"); + REQUIRE(f != nullptr); + std::fclose(f); + + // Clean up + std::remove(filename.c_str()); + } +} diff --git a/tests/unit_tests/test_mcpl_stat_sum.py b/tests/unit_tests/test_mcpl_stat_sum.py new file mode 100644 index 000000000..b69192956 --- /dev/null +++ b/tests/unit_tests/test_mcpl_stat_sum.py @@ -0,0 +1,69 @@ +"""Test for MCPL stat:sum functionality""" + +from pathlib import Path +import shutil + +import pytest +import openmc + + +@pytest.mark.skipif(shutil.which("mcpl-config") is None, reason="MCPL is not available.") +def test_mcpl_stat_sum_field(run_in_tmpdir): + """Test that MCPL files contain proper stat:sum field with particle count. + + This test verifies that when OpenMC creates MCPL source files, they contain + the stat:sum field. Since MCPL functions are not exposed in the Python API, + this test creates an actual OpenMC simulation to generate MCPL files and + then checks their content. + """ + + mcpl = pytest.importorskip("mcpl") + + # Create a minimal working model that will generate MCPL files + model = openmc.examples.pwr_pin_cell() + model.settings.batches = 5 + model.settings.inactive = 2 + model.settings.particles = 1000 + model.settings.sourcepoint = {'mcpl': True, 'separate': True} + + # Run a short simulation to generate MCPL files + model.run(output=False) + + # Find the generated MCPL file (from the last batch) + mcpl_file = Path('source.5.mcpl') + assert mcpl_file.exists(), "No MCPL files were generated" + + # Open and verify the stat:sum field exists + with mcpl.MCPLFile(mcpl_file) as f: + # Check if stat:sum field exists using convenience property + if hasattr(f, 'stat_sum'): + # Use the convenience .stat_sum property directly + stat_sum_dict = f.stat_sum + assert 'openmc_np1' in stat_sum_dict, "openmc_np1 key not found in stat_sum" + stat_sum_value = int(stat_sum_dict['openmc_np1']) + else: + # Fallback to checking comments for older MCPL versions + comments = f.comments + + # Check for stat:sum in comments (MCPL stores these as comments) + stat_sum_value = None + + for comment in comments: + if 'stat:sum:openmc_np1' in comment: + # Extract the value + parts = comment.split(':') + if len(parts) >= 4: + stat_sum_value = int(parts[3].strip()) + break + else: + pytest.skip("stat:sum field not found - may be running with MCPL < 2.1.0") + + # Verify the stat:sum value is reasonable + assert stat_sum_value != -1, "stat:sum was not updated from initial -1 value" + + # In eigenvalue mode, active batches generate source particles + active_batches = model.settings.batches - model.settings.inactive # 3 active batches + expected_particles = active_batches * model.settings.particles # 3000 total + + assert stat_sum_value == expected_particles, \ + f"stat:sum value {stat_sum_value} doesn't match expected {expected_particles}" From a11021cd07bf43b2ea3070222cd916a737b6ed80 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Fri, 8 Aug 2025 20:47:55 +0300 Subject: [PATCH 388/671] Consistent XML parsing using functions from _xml module (#3517) Co-authored-by: Paul Romano --- openmc/_xml.py | 14 ++-- openmc/cell.py | 22 +++--- openmc/dagmc.py | 8 +- openmc/data/library.py | 10 +-- openmc/deplete/chain.py | 3 +- openmc/deplete/nuclide.py | 34 +++++---- openmc/filter.py | 49 ++++++------- openmc/filter_expansion.py | 31 ++++---- openmc/lattice.py | 21 +++--- openmc/material.py | 43 ++++++----- openmc/mesh.py | 42 +++++------ openmc/plots.py | 137 +++++++++++++++++------------------ openmc/settings.py | 55 +++++++------- openmc/source.py | 12 +-- openmc/stats/multivariate.py | 34 ++++----- openmc/stats/univariate.py | 25 +++---- openmc/surface.py | 13 ++-- openmc/tallies.py | 35 +++++---- openmc/tally_derivative.py | 9 ++- openmc/trigger.py | 11 +-- openmc/volume.py | 11 +-- openmc/weight_windows.py | 15 ++-- 22 files changed, 313 insertions(+), 321 deletions(-) diff --git a/openmc/_xml.py b/openmc/_xml.py index 05412128d..758d80525 100644 --- a/openmc/_xml.py +++ b/openmc/_xml.py @@ -64,8 +64,8 @@ def get_text(elem, name, default=None): -def get_elem_tuple(elem, name, dtype=int): - """Helper function to get a tuple of values from an elem +def get_elem_list(elem, name, dtype=int): + """Helper function to get a list of values from an elem Parameters ---------- @@ -78,9 +78,9 @@ def get_elem_tuple(elem, name, dtype=int): Returns ------- - tuple of dtype - Data read from the tuple + list of dtype + Data read from the list """ - subelem = elem.find(name) - if subelem is not None: - return tuple([dtype(x) for x in subelem.text.split()]) + text = get_text(elem, name) + if text is not None: + return [dtype(x) for x in text.split()] diff --git a/openmc/cell.py b/openmc/cell.py index cd0573e8b..672afe095 100644 --- a/openmc/cell.py +++ b/openmc/cell.py @@ -8,7 +8,7 @@ from uncertainties import UFloat import openmc import openmc.checkvalue as cv -from ._xml import get_text +from ._xml import get_elem_list, get_text from .mixin import IDManagerMixin from .plots import add_plot_params from .region import Region, Complement @@ -689,9 +689,8 @@ class Cell(IDManagerMixin): c = cls(cell_id, name) # Assign material/distributed materials or fill - mat_text = get_text(elem, 'material') - if mat_text is not None: - mat_ids = mat_text.split() + mat_ids = get_elem_list(elem, 'material', str) + if mat_ids is not None: if len(mat_ids) > 1: c.fill = [materials[i] for i in mat_ids] else: @@ -706,19 +705,18 @@ class Cell(IDManagerMixin): c.region = Region.from_expression(region, surfaces) # Check for other attributes - t = get_text(elem, 'temperature') - if t is not None: - if ' ' in t: - c.temperature = [float(t_i) for t_i in t.split()] + temperature = get_elem_list(elem, 'temperature', float) + if temperature is not None: + if len(temperature) > 1: + c.temperature = temperature else: - c.temperature = float(t) + c.temperature = temperature[0] v = get_text(elem, 'volume') if v is not None: c.volume = float(v) for key in ('temperature', 'rotation', 'translation'): - value = get_text(elem, key) - if value is not None: - values = [float(x) for x in value.split()] + values = get_elem_list(elem, key, float) + if values is not None: if key == 'rotation' and len(values) == 9: values = np.array(values).reshape(3, 3) setattr(c, key, values) diff --git a/openmc/dagmc.py b/openmc/dagmc.py index 2486f7516..d1265be26 100644 --- a/openmc/dagmc.py +++ b/openmc/dagmc.py @@ -8,7 +8,7 @@ import warnings import openmc import openmc.checkvalue as cv -from ._xml import get_text +from ._xml import get_elem_list, get_text from .checkvalue import check_type, check_value from .surface import _BOUNDARY_TYPES from .bounding_box import BoundingBox @@ -468,8 +468,8 @@ class DAGMCUniverse(openmc.UniverseBase): if name is not None: out.name = name - out.auto_geom_ids = bool(elem.get('auto_geom_ids')) - out.auto_mat_ids = bool(elem.get('auto_mat_ids')) + out.auto_geom_ids = bool(get_text(elem, "auto_geom_ids")) + out.auto_mat_ids = bool(get_text(elem, "auto_mat_ids")) el_mat_override = elem.find('material_overrides') if el_mat_override is not None: @@ -480,7 +480,7 @@ class DAGMCUniverse(openmc.UniverseBase): out._material_overrides = {} for elem in el_mat_override.findall('cell_override'): cell_id = int(get_text(elem, 'id')) - mat_ids = get_text(elem, 'material_ids').split(' ') + mat_ids = get_elem_list(elem, "material_ids", str) or [] mat_objs = [mats[mat_id] for mat_id in mat_ids] out._material_overrides[cell_id] = mat_objs diff --git a/openmc/data/library.py b/openmc/data/library.py index bec538c06..b49757b0d 100644 --- a/openmc/data/library.py +++ b/openmc/data/library.py @@ -5,7 +5,7 @@ import h5py import lxml.etree as ET import openmc -from openmc._xml import clean_indentation +from openmc._xml import get_elem_list, get_text, clean_indentation class DataLibrary(list): @@ -172,9 +172,9 @@ class DataLibrary(list): directory = os.path.dirname(path) for lib_element in root.findall('library'): - filename = os.path.join(directory, lib_element.attrib['path']) - filetype = lib_element.attrib['type'] - materials = lib_element.attrib['materials'].split() + filename = os.path.join(directory, get_text(lib_element, "path")) + filetype = get_text(lib_element, "type") + materials = get_elem_list(lib_element, "materials", str) or [] library = {'path': filename, 'type': filetype, 'materials': materials} data.libraries.append(library) @@ -182,7 +182,7 @@ class DataLibrary(list): # get depletion chain data dep_node = root.find("depletion_chain") if dep_node is not None: - filename = os.path.join(directory, dep_node.attrib['path']) + filename = os.path.join(directory, get_text(dep_node, "path")) library = {'path': filename, 'type': 'depletion_chain', 'materials': []} data.libraries.append(library) diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index 8e24f716b..f1a23317f 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -22,6 +22,7 @@ from openmc.checkvalue import check_type, check_greater_than, PathLike from openmc.data import gnds_name, zam from openmc.exceptions import DataError from .nuclide import FissionYieldDistribution, Nuclide +from .._xml import get_text import openmc.data @@ -553,7 +554,7 @@ class Chain: root = ET.parse(str(filename)) for i, nuclide_elem in enumerate(root.findall('nuclide')): - this_q = fission_q.get(nuclide_elem.get("name")) + this_q = fission_q.get(get_text(nuclide_elem, "name")) nuc = Nuclide.from_xml(nuclide_elem, root, this_q) chain.add_nuclide(nuc) diff --git a/openmc/deplete/nuclide.py b/openmc/deplete/nuclide.py index 60e3e5317..958814834 100644 --- a/openmc/deplete/nuclide.py +++ b/openmc/deplete/nuclide.py @@ -14,6 +14,7 @@ import numpy as np from openmc.checkvalue import check_type from openmc.stats import Univariate +from .._xml import get_elem_list, get_text __all__ = [ "DecayTuple", "ReactionTuple", "Nuclide", "FissionYield", @@ -225,38 +226,39 @@ class Nuclide: """ nuc = cls() - nuc.name = element.get('name') + nuc.name = get_text(element, "name") # Check for half-life - if 'half_life' in element.attrib: - nuc.half_life = float(element.get('half_life')) - nuc.decay_energy = float(element.get('decay_energy', '0')) + half_life = get_text(element, "half_life") + if half_life is not None: + nuc.half_life = float(half_life) + nuc.decay_energy = float(get_text(element, "decay_energy", 0.0)) # Check for decay paths for decay_elem in element.iter('decay'): - d_type = decay_elem.get('type') - target = decay_elem.get('target') + d_type = get_text(decay_elem, "type") + target = get_text(decay_elem, "target") if target is not None and target.lower() == "nothing": target = None - branching_ratio = float(decay_elem.get('branching_ratio')) + branching_ratio = float(get_text(decay_elem, "branching_ratio")) nuc.decay_modes.append(DecayTuple(d_type, target, branching_ratio)) # Check for sources for src_elem in element.iter('source'): - particle = src_elem.get('particle') + particle = get_text(src_elem, "particle") distribution = Univariate.from_xml_element(src_elem) nuc.sources[particle] = distribution # Check for reaction paths for reaction_elem in element.iter('reaction'): - r_type = reaction_elem.get('type') - Q = float(reaction_elem.get('Q', '0')) - branching_ratio = float(reaction_elem.get('branching_ratio', '1')) + r_type = get_text(reaction_elem, "type") + Q = float(get_text(reaction_elem, "Q", 0.0)) + branching_ratio = float(get_text(reaction_elem, "branching_ratio", 1.0)) # If the type is not fission, get target and Q value, otherwise # just set null values if r_type != 'fission': - target = reaction_elem.get('target') + target = get_text(reaction_elem, "target") if target is not None and target.lower() == "nothing": target = None else: @@ -271,7 +273,7 @@ class Nuclide: fpy_elem = element.find('neutron_fission_yields') if fpy_elem is not None: # Check for use of FPY from other nuclide - parent = fpy_elem.get('parent') + parent = get_text(fpy_elem, "parent") if parent is not None: assert root is not None fpy_elem = root.find( @@ -529,9 +531,9 @@ class FissionYieldDistribution(Mapping): """ all_yields = {} for yield_elem in element.iter("fission_yields"): - energy = float(yield_elem.get("energy")) - products = yield_elem.find("products").text.split() - yields = map(float, yield_elem.find("data").text.split()) + energy = float(get_text(yield_elem, "energy")) + products = get_elem_list(yield_elem, "products", str) or [] + yields = get_elem_list(yield_elem, "data", float) or [] # Get a map of products to their corresponding yield all_yields[energy] = dict(zip(products, yields)) diff --git a/openmc/filter.py b/openmc/filter.py index 53fec898f..6a666d2a0 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -17,7 +17,7 @@ from .material import Material from .mixin import IDManagerMixin from .surface import Surface from .universe import UniverseBase -from ._xml import get_text +from ._xml import get_elem_list, get_text _FILTER_TYPES = ( @@ -259,17 +259,15 @@ class Filter(IDManagerMixin, metaclass=FilterMeta): Filter object """ - filter_type = elem.get('type') - if filter_type is None: - filter_type = elem.find('type').text + filter_type = get_text(elem, "type") # If the filter type matches this class's short_name, then # there is no overridden from_xml_element method if filter_type == cls.short_name.lower(): # Get bins from element -- the default here works for any filters # that just store a list of bins that can be represented as integers - filter_id = int(elem.get('id')) - bins = [int(x) for x in get_text(elem, 'bins').split()] + filter_id = int(get_text(elem, "id")) + bins = get_elem_list(elem, "bins", int) or [] return cls(bins, filter_id=filter_id) # Search through all subclasses and find the one matching the HDF5 @@ -701,8 +699,8 @@ class CellInstanceFilter(Filter): @classmethod def from_xml_element(cls, elem, **kwargs): - filter_id = int(elem.get('id')) - bins = [int(x) for x in get_text(elem, 'bins').split()] + filter_id = int(get_text(elem, "id")) + bins = get_elem_list(elem, "bins", int) or [] cell_instances = list(zip(bins[::2], bins[1::2])) return cls(cell_instances, filter_id=filter_id) @@ -784,8 +782,8 @@ class ParticleFilter(Filter): @classmethod def from_xml_element(cls, elem, **kwargs): - filter_id = int(elem.get('id')) - bins = get_text(elem, 'bins').split() + filter_id = int(get_text(elem, "id")) + bins = get_elem_list(elem, "bins", str) or [] return cls(bins, filter_id=filter_id) @@ -1004,12 +1002,12 @@ class MeshFilter(Filter): def from_xml_element(cls, elem: ET.Element, **kwargs) -> MeshFilter: mesh_id = int(get_text(elem, 'bins')) mesh_obj = kwargs['meshes'][mesh_id] - filter_id = int(elem.get('id')) + filter_id = int(get_text(elem, "id")) out = cls(mesh_obj, filter_id=filter_id) - translation = elem.get('translation') + translation = get_elem_list(elem, "translation", float) or [] if translation: - out.translation = [float(x) for x in translation.split()] + out.translation = translation return out @@ -1149,16 +1147,16 @@ class MeshMaterialFilter(MeshFilter): @classmethod def from_xml_element(cls, elem: ET.Element, **kwargs) -> MeshMaterialFilter: - filter_id = int(elem.get('id')) - mesh_id = int(elem.get('mesh')) + filter_id = int(get_text(elem, "id")) + mesh_id = int(get_text(elem, "mesh")) mesh_obj = kwargs['meshes'][mesh_id] - bins = [int(x) for x in get_text(elem, 'bins').split()] + bins = get_elem_list(elem, "bins", int) or [] bins = list(zip(bins[::2], bins[1::2])) out = cls(mesh_obj, bins, filter_id=filter_id) - translation = elem.get('translation') + translation = get_elem_list(elem, "translation", float) or [] if translation: - out.translation = [float(x) for x in translation.split()] + out.translation = translation return out @classmethod @@ -1557,8 +1555,8 @@ class RealFilter(Filter): @classmethod def from_xml_element(cls, elem, **kwargs): - filter_id = int(elem.get('id')) - bins = [float(x) for x in get_text(elem, 'bins').split()] + filter_id = int(get_text(elem, "id")) + bins = get_elem_list(elem, "bins", float) or [] return cls(bins, filter_id=filter_id) @@ -2447,12 +2445,13 @@ class EnergyFunctionFilter(Filter): @classmethod def from_xml_element(cls, elem, **kwargs): - filter_id = int(elem.get('id')) - energy = [float(x) for x in get_text(elem, 'energy').split()] - y = [float(x) for x in get_text(elem, 'y').split()] + filter_id = int(get_text(elem, "id")) + energy = get_elem_list(elem, "energy", float) or [] + y = get_elem_list(elem, "y", float) or [] out = cls(energy, y, filter_id=filter_id) - if elem.find('interpolation') is not None: - out.interpolation = elem.find('interpolation').text + interpolation = get_text(elem, "interpolation") + if interpolation is not None: + out.interpolation = interpolation return out def can_merge(self, other): diff --git a/openmc/filter_expansion.py b/openmc/filter_expansion.py index cdb2f20e0..b79c8fc79 100644 --- a/openmc/filter_expansion.py +++ b/openmc/filter_expansion.py @@ -4,6 +4,7 @@ import lxml.etree as ET import openmc.checkvalue as cv from .filter import Filter +from ._xml import get_text class ExpansionFilter(Filter): @@ -49,8 +50,8 @@ class ExpansionFilter(Filter): @classmethod def from_xml_element(cls, elem, **kwargs): - filter_id = int(elem.get('id')) - order = int(elem.find('order').text) + filter_id = int(get_text(elem, "id")) + order = int(get_text(elem, "order")) return cls(order, filter_id=filter_id) def merge(self, other): @@ -263,11 +264,11 @@ class SpatialLegendreFilter(ExpansionFilter): @classmethod def from_xml_element(cls, elem, **kwargs): - filter_id = int(elem.get('id')) - order = int(elem.find('order').text) - axis = elem.find('axis').text - minimum = float(elem.find('min').text) - maximum = float(elem.find('max').text) + filter_id = int(get_text(elem, "id")) + order = int(get_text(elem, "order")) + axis = get_text(elem, "axis") + minimum = float(get_text(elem, "min")) + maximum = float(get_text(elem, "max")) return cls(order, axis, minimum, maximum, filter_id=filter_id) @@ -362,10 +363,10 @@ class SphericalHarmonicsFilter(ExpansionFilter): @classmethod def from_xml_element(cls, elem, **kwargs): - filter_id = int(elem.get('id')) - order = int(elem.find('order').text) + filter_id = int(get_text(elem, "id")) + order = int(get_text(elem, "order")) filter = cls(order, filter_id=filter_id) - filter.cosine = elem.get('cosine') + filter.cosine = get_text(elem, "cosine") return filter @@ -518,11 +519,11 @@ class ZernikeFilter(ExpansionFilter): @classmethod def from_xml_element(cls, elem, **kwargs): - filter_id = int(elem.get('id')) - order = int(elem.find('order').text) - x = float(elem.find('x').text) - y = float(elem.find('y').text) - r = float(elem.find('r').text) + filter_id = int(get_text(elem, "id")) + order = int(get_text(elem, "order")) + x = float(get_text(elem, "x")) + y = float(get_text(elem, "y")) + r = float(get_text(elem, "r")) return cls(order, x, y, r, filter_id=filter_id) diff --git a/openmc/lattice.py b/openmc/lattice.py index 518068560..f0e8b40b0 100644 --- a/openmc/lattice.py +++ b/openmc/lattice.py @@ -10,7 +10,7 @@ import numpy as np import openmc import openmc.checkvalue as cv -from ._xml import get_text +from ._xml import get_elem_list, get_text from .mixin import IDManagerMixin @@ -959,18 +959,17 @@ class RectLattice(Lattice): lat_id = int(get_text(elem, 'id')) name = get_text(elem, 'name') lat = cls(lat_id, name) - lat.lower_left = [float(i) - for i in get_text(elem, 'lower_left').split()] - lat.pitch = [float(i) for i in get_text(elem, 'pitch').split()] + lat.lower_left = get_elem_list(elem, "lower_left", float) + lat.pitch = get_elem_list(elem, "pitch", float) outer = get_text(elem, 'outer') if outer is not None: lat.outer = get_universe(int(outer)) # Get array of universes - dimension = get_text(elem, 'dimension').split() + dimension = get_elem_list(elem, 'dimension', int) shape = np.array(dimension, dtype=int)[::-1] - uarray = np.array([get_universe(int(i)) for i in - get_text(elem, 'universes').split()]) + universes = get_elem_list(elem, 'universes', int) + uarray = np.array([get_universe(u) for u in universes]) uarray.shape = shape lat.universes = uarray return lat @@ -1530,8 +1529,8 @@ class HexLattice(Lattice): lat_id = int(get_text(elem, 'id')) name = get_text(elem, 'name') lat = cls(lat_id, name) - lat.center = [float(i) for i in get_text(elem, 'center').split()] - lat.pitch = [float(i) for i in get_text(elem, 'pitch').split()] + lat.center = get_elem_list(elem, "center", float) + lat.pitch = get_elem_list(elem, "pitch", float) lat.orientation = get_text(elem, 'orientation', 'y') outer = get_text(elem, 'outer') if outer is not None: @@ -1548,8 +1547,8 @@ class HexLattice(Lattice): univs = [deepcopy(univs) for i in range(n_axial)] # Get flat array of universes - uarray = np.array([get_universe(int(i)) for i in - get_text(elem, 'universes').split()]) + universes = get_elem_list(elem, "universes", int) + uarray = np.array([get_universe(u) for u in universes]) # Fill nested lists j = 0 diff --git a/openmc/material.py b/openmc/material.py index db67b709c..0afe5b670 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -17,7 +17,7 @@ import h5py import openmc import openmc.data import openmc.checkvalue as cv -from ._xml import clean_indentation +from ._xml import clean_indentation, get_elem_list, get_text from .mixin import IDManagerMixin from .utility_funcs import input_path from . import waste @@ -1672,50 +1672,55 @@ class Material(IDManagerMixin): Material generated from XML element """ - mat_id = int(elem.get('id')) + mat_id = int(get_text(elem, 'id')) + # Add NCrystal material from cfg string - if "cfg" in elem.attrib: - cfg = elem.get("cfg") + cfg = get_text(elem, "cfg") + if cfg is not None: return Material.from_ncrystal(cfg, material_id=mat_id) mat = cls(mat_id) - mat.name = elem.get('name') + mat.name = get_text(elem, 'name') - if "temperature" in elem.attrib: - mat.temperature = float(elem.get("temperature")) + temperature = get_text(elem, "temperature") + if temperature is not None: + mat.temperature = float(temperature) - if 'volume' in elem.attrib: - mat.volume = float(elem.get('volume')) + volume = get_text(elem, "volume") + if volume is not None: + mat.volume = float(volume) # Get each nuclide for nuclide in elem.findall('nuclide'): - name = nuclide.attrib['name'] + name = get_text(nuclide, "name") if 'ao' in nuclide.attrib: mat.add_nuclide(name, float(nuclide.attrib['ao'])) elif 'wo' in nuclide.attrib: mat.add_nuclide(name, float(nuclide.attrib['wo']), 'wo') # Get depletable attribute - mat.depletable = elem.get('depletable') in ('true', '1') + depletable = get_text(elem, "depletable") + mat.depletable = depletable in ('true', '1') # Get each S(a,b) table for sab in elem.findall('sab'): - fraction = float(sab.get('fraction', 1.0)) - mat.add_s_alpha_beta(sab.get('name'), fraction) + fraction = float(get_text(sab, "fraction", 1.0)) + name = get_text(sab, "name") + mat.add_s_alpha_beta(name, fraction) # Get total material density density = elem.find('density') - units = density.get('units') + units = get_text(density, "units") if units == 'sum': mat.set_density(units) else: - value = float(density.get('value')) + value = float(get_text(density, 'value')) mat.set_density(units, value) # Check for isotropic scattering nuclides - isotropic = elem.find('isotropic') + isotropic = get_elem_list(elem, "isotropic", str) if isotropic is not None: - mat.isotropic = isotropic.text.split() + mat.isotropic = isotropic return mat @@ -1982,9 +1987,9 @@ class Materials(cv.CheckedList): materials.append(Material.from_xml_element(material)) # Check for cross sections settings - xs = elem.find('cross_sections') + xs = get_text(elem, "cross_sections") if xs is not None: - materials.cross_sections = xs.text + materials.cross_sections = xs return materials diff --git a/openmc/mesh.py b/openmc/mesh.py index 339702884..2e9abd1b6 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -16,7 +16,7 @@ import openmc import openmc.checkvalue as cv from openmc.checkvalue import PathLike from openmc.utility_funcs import change_directory -from ._xml import get_text +from ._xml import get_elem_list, get_text from .mixin import IDManagerMixin from .surface import _BOUNDARY_TYPES from .utility_funcs import input_path @@ -1187,21 +1187,21 @@ class RegularMesh(StructuredMesh): mesh_id = int(get_text(elem, 'id')) mesh = cls(mesh_id=mesh_id) - dimension = get_text(elem, 'dimension') + dimension = get_elem_list(elem, "dimension", int) if dimension is not None: - mesh.dimension = [int(x) for x in dimension.split()] + mesh.dimension = dimension - lower_left = get_text(elem, 'lower_left') + lower_left = get_elem_list(elem, "lower_left", float) if lower_left is not None: - mesh.lower_left = [float(x) for x in lower_left.split()] + mesh.lower_left = lower_left - upper_right = get_text(elem, 'upper_right') + upper_right = get_elem_list(elem, "upper_right", float) if upper_right is not None: - mesh.upper_right = [float(x) for x in upper_right.split()] + mesh.upper_right = upper_right - width = get_text(elem, 'width') + width = get_elem_list(elem, "width", float) if width is not None: - mesh.width = [float(x) for x in width.split()] + mesh.width = width return mesh @@ -1507,9 +1507,9 @@ class RectilinearMesh(StructuredMesh): """ mesh_id = int(get_text(elem, 'id')) mesh = cls(mesh_id=mesh_id) - mesh.x_grid = [float(x) for x in get_text(elem, 'x_grid').split()] - mesh.y_grid = [float(y) for y in get_text(elem, 'y_grid').split()] - mesh.z_grid = [float(z) for z in get_text(elem, 'z_grid').split()] + mesh.x_grid = get_elem_list(elem, "x_grid", float) + mesh.y_grid = get_elem_list(elem, "y_grid", float) + mesh.z_grid = get_elem_list(elem, "z_grid", float) return mesh @@ -1923,10 +1923,10 @@ class CylindricalMesh(StructuredMesh): mesh_id = int(get_text(elem, 'id')) mesh = cls( - r_grid = [float(x) for x in get_text(elem, "r_grid").split()], - phi_grid = [float(x) for x in get_text(elem, "phi_grid").split()], - z_grid = [float(x) for x in get_text(elem, "z_grid").split()], - origin = [float(x) for x in get_text(elem, "origin", default=[0., 0., 0.]).split()], + r_grid = get_elem_list(elem, "r_grid", float), + phi_grid = get_elem_list(elem, "phi_grid", float), + z_grid = get_elem_list(elem, "z_grid", float), + origin = get_elem_list(elem, "origin", float) or [0., 0., 0.], mesh_id=mesh_id, ) @@ -2296,10 +2296,10 @@ class SphericalMesh(StructuredMesh): mesh_id = int(get_text(elem, 'id')) mesh = cls( mesh_id=mesh_id, - r_grid = [float(x) for x in get_text(elem, "r_grid").split()], - theta_grid = [float(x) for x in get_text(elem, "theta_grid").split()], - phi_grid = [float(x) for x in get_text(elem, "phi_grid").split()], - origin = [float(x) for x in get_text(elem, "origin", default=[0., 0., 0.]).split()], + r_grid = get_elem_list(elem, "r_grid", float), + theta_grid = get_elem_list(elem, "theta_grid", float), + phi_grid = get_elem_list(elem, "phi_grid", float), + origin = get_elem_list(elem, "origin", float) or [0., 0., 0.], ) return mesh @@ -2842,7 +2842,7 @@ class UnstructuredMesh(MeshBase): filename = get_text(elem, 'filename') library = get_text(elem, 'library') length_multiplier = float(get_text(elem, 'length_multiplier', 1.0)) - options = elem.get('options') + options = get_text(elem, "options") return cls(filename, library, mesh_id, '', length_multiplier, options) diff --git a/openmc/plots.py b/openmc/plots.py index 34dde84e4..072a9a319 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -10,7 +10,7 @@ import openmc import openmc.checkvalue as cv from openmc.checkvalue import PathLike -from ._xml import clean_indentation, get_elem_tuple, get_text +from ._xml import clean_indentation, get_elem_list, get_text from .mixin import IDManagerMixin _BASES = {'xy', 'xz', 'yz'} @@ -944,64 +944,61 @@ class Plot(PlotBase): Plot object """ - plot_id = int(elem.get("id")) + plot_id = int(get_text(elem, "id")) name = get_text(elem, 'name', '') plot = cls(plot_id, name) if "filename" in elem.keys(): - plot.filename = elem.get("filename") - plot.color_by = elem.get("color_by") - plot.type = elem.get("type") + plot.filename = get_text(elem, "filename") + plot.color_by = get_text(elem, "color_by") + plot.type = get_text(elem, "type") if plot.type == 'slice': - plot.basis = elem.get("basis") + plot.basis = get_text(elem, "basis") - plot.origin = get_elem_tuple(elem, "origin", float) - plot.width = get_elem_tuple(elem, "width", float) - plot.pixels = get_elem_tuple(elem, "pixels") - plot._background = get_elem_tuple(elem, "background") + plot.origin = tuple(get_elem_list(elem, "origin", float)) + plot.width = tuple(get_elem_list(elem, "width", float)) + plot.pixels = tuple(get_elem_list(elem, "pixels")) + background = get_elem_list(elem, "background") + if background is not None: + plot._background = tuple(background) # Set plot colors colors = {} for color_elem in elem.findall("color"): - uid = int(color_elem.get("id")) - colors[uid] = tuple([int(x) - for x in color_elem.get("rgb").split()]) + uid = int(get_text(color_elem, "id")) + colors[uid] = tuple(get_elem_list(color_elem, "rgb", int)) plot.colors = colors # Set masking information mask_elem = elem.find("mask") if mask_elem is not None: - plot.mask_components = [ - int(x) for x in mask_elem.get("components").split()] - background = mask_elem.get("background") + plot.mask_components = get_elem_list(mask_elem, "components", int) + background = get_elem_list(mask_elem, "background", int) if background is not None: - plot.mask_background = tuple( - [int(x) for x in background.split()]) + plot.mask_background = tuple(background) # show overlaps - overlap_elem = elem.find("show_overlaps") - if overlap_elem is not None: - plot.show_overlaps = (overlap_elem.text in ('true', '1')) - overlap_color = get_elem_tuple(elem, "overlap_color") + overlap = get_text(elem, "show_overlaps") + if overlap is not None: + plot.show_overlaps = (overlap in ('true', '1')) + overlap_color = get_elem_list(elem, "overlap_color", int) if overlap_color is not None: - plot.overlap_color = overlap_color + plot.overlap_color = tuple(overlap_color) # Set universe level - level = elem.find("level") + level = get_text(elem, "level") if level is not None: - plot.level = int(level.text) + plot.level = int(level) # Set meshlines mesh_elem = elem.find("meshlines") if mesh_elem is not None: - meshlines = {'type': mesh_elem.get('meshtype')} + meshlines = {'type': get_text(mesh_elem, "meshtype")} if 'id' in mesh_elem.keys(): - meshlines['id'] = int(mesh_elem.get('id')) + meshlines['id'] = int(get_text(mesh_elem, "id")) if 'linewidth' in mesh_elem.keys(): - meshlines['linewidth'] = int(mesh_elem.get('linewidth')) + meshlines['linewidth'] = int(get_text(mesh_elem, "linewidth")) if 'color' in mesh_elem.keys(): - meshlines['color'] = tuple( - [int(x) for x in mesh_elem.get('color').split()] - ) + meshlines['color'] = tuple(get_elem_list(mesh_elem, "color", int)) plot.meshlines = meshlines return plot @@ -1259,38 +1256,39 @@ class RayTracePlot(PlotBase): None """ - if "filename" in elem.keys(): - self.filename = elem.get("filename") - self.color_by = elem.get("color_by") + filename = get_text(elem, "filename") + if filename is not None: + self.filename = filename + self.color_by = get_text(elem, "color_by") - horizontal_fov = elem.find("horizontal_field_of_view") + horizontal_fov = get_text(elem, "horizontal_field_of_view") if horizontal_fov is not None: - self.horizontal_field_of_view = float(horizontal_fov.text) + self.horizontal_field_of_view = float(horizontal_fov) - if (tmp := elem.find("orthographic_width")) is not None: - self.orthographic_width = float(tmp) + orthographic_width = get_text(elem, "orthographic_width") + if orthographic_width is not None: + self.orthographic_width = float(orthographic_width) - self.pixels = get_elem_tuple(elem, "pixels") - self.camera_position = get_elem_tuple(elem, "camera_position", float) - self.look_at = get_elem_tuple(elem, "look_at", float) + self.pixels = tuple(get_elem_list(elem, "pixels", int)) + self.camera_position = tuple(get_elem_list(elem, "camera_position", float)) + self.look_at = tuple(get_elem_list(elem, "look_at", float)) - if elem.find("background") is not None: - self.background = get_elem_tuple(elem, "background") + background = get_elem_list(elem, "background", int) + if background is not None: + self.background = tuple(background) # Set masking information if (mask_elem := elem.find("mask")) is not None: - mask_components = [int(x) - for x in mask_elem.get("components").split()] + mask_components = get_elem_list(mask_elem, "components", int) # TODO: set mask components(needs geometry information) - background = mask_elem.get("background") + background = get_elem_list(mask_elem, "background", int) if background is not None: - self.mask_background = tuple( - [int(x) for x in background.split()]) + self.mask_background = tuple(background) # Set universe level - level = elem.find("level") + level = get_text(elem, "level") if level is not None: - self.level = int(level.text) + self.level = int(level) class WireframeRayTracePlot(RayTracePlot): @@ -1515,7 +1513,7 @@ class WireframeRayTracePlot(RayTracePlot): """ - plot_id = int(elem.get("id")) + plot_id = int(get_text(elem, "id")) plot_name = get_text(elem, 'name', '') plot = cls(plot_id, plot_name) plot.type = "wireframe_raytrace" @@ -1523,19 +1521,18 @@ class WireframeRayTracePlot(RayTracePlot): plot._read_xml_attributes(elem) # Attempt to get wireframe thickness.May not be present - wireframe_thickness = elem.find("wireframe_thickness") + wireframe_thickness = get_text(elem, "wireframe_thickness") if wireframe_thickness is not None: - plot.wireframe_thickness = int(wireframe_thickness.text) - wireframe_color = elem.get("wireframe_color") + plot.wireframe_thickness = int(wireframe_thickness) + wireframe_color = get_elem_list(elem, "wireframe_color", int) if wireframe_color: - plot.wireframe_color = [int(item) for item in wireframe_color] + plot.wireframe_color = wireframe_color # Set plot colors for color_elem in elem.findall("color"): - uid = int(color_elem.get("id")) - plot.colors[uid] = tuple(int(i) - for i in get_text(color_elem, 'rgb').split()) - plot.xs[uid] = float(color_elem.get("xs")) + uid = int(get_text(color_elem, "id")) + plot.colors[uid] = tuple(get_elem_list(color_elem, "rgb", int)) + plot.xs[uid] = float(get_text(color_elem, "xs")) return plot @@ -1693,15 +1690,17 @@ class SolidRayTracePlot(RayTracePlot): def _read_phong_attributes(self, elem): """Read attributes specific to the Phong plot from an XML element""" - if elem.find('light_position') is not None: - self.light_position = get_elem_tuple(elem, 'light_position', float) + light_position = get_elem_list(elem, 'light_position', float) + if light_position is not None: + self.light_position = tuple(light_position) - diffuse_fraction = elem.find('diffuse_fraction') + diffuse_fraction = get_text(elem, "diffuse_fraction") if diffuse_fraction is not None: - self.diffuse_fraction = float(diffuse_fraction.text) + self.diffuse_fraction = float(diffuse_fraction) - if elem.find('opaque_ids') is not None: - self.opaque_domains = list(get_elem_tuple(elem, 'opaque_ids', int)) + opaque_domains = get_elem_list(elem, 'opaque_ids', int) + if opaque_domains is not None: + self.opaque_domains = opaque_domains @classmethod def from_xml_element(cls, elem): @@ -1719,7 +1718,7 @@ class SolidRayTracePlot(RayTracePlot): """ - plot_id = int(elem.get("id")) + plot_id = int(get_text(elem, "id")) plot_name = get_text(elem, 'name', '') plot = cls(plot_id, plot_name) plot.type = "solid_raytrace" @@ -1729,8 +1728,8 @@ class SolidRayTracePlot(RayTracePlot): # Set plot colors for color_elem in elem.findall("color"): - uid = color_elem.get("id") - plot.colors[uid] = get_elem_tuple(color_elem, "rgb") + uid = get_text(color_elem, "id") + plot.colors[uid] = tuple(get_elem_list(color_elem, "rgb", int)) return plot @@ -1897,7 +1896,7 @@ class Plots(cv.CheckedList): # Generate each plot plots = cls() for e in elem.findall('plot'): - plot_type = e.get('type') + plot_type = get_text(e, "type") if plot_type == 'wireframe_raytrace': plots.append(WireframeRayTracePlot.from_xml_element(e)) elif plot_type == 'solid_raytrace': diff --git a/openmc/settings.py b/openmc/settings.py index 327faf544..ce3743ff5 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -11,7 +11,7 @@ import openmc import openmc.checkvalue as cv from openmc.checkvalue import PathLike from openmc.stats.multivariate import MeshSpatial -from ._xml import clean_indentation, get_text +from ._xml import clean_indentation, get_elem_list, get_text from .mesh import _read_meshes, RegularMesh, MeshBase from .source import SourceBase, MeshSource, IndependentSource from .utility_funcs import input_path @@ -1791,23 +1791,21 @@ class Settings: def _statepoint_from_xml_element(self, root): elem = root.find('state_point') if elem is not None: - text = get_text(elem, 'batches') - if text is not None: - self.statepoint['batches'] = [int(x) for x in text.split()] + batches = get_elem_list(elem, "batches", int) + if batches is not None: + self.statepoint['batches'] = batches def _sourcepoint_from_xml_element(self, root): elem = root.find('source_point') if elem is not None: for key in ('separate', 'write', 'overwrite_latest', 'batches', 'mcpl'): - value = get_text(elem, key) - if value is not None: - if key in ('separate', 'write', 'mcpl'): - value = value in ('true', '1') - elif key == 'overwrite_latest': - value = value in ('true', '1') + if key in ('separate', 'write', 'mcpl', 'overwrite_latest'): + value = get_text(elem, key) in ('true', '1') + if key == 'overwrite_latest': key = 'overwrite' - else: - value = [int(x) for x in value.split()] + else: + value = get_elem_list(elem, key, int) + if value is not None: self.sourcepoint[key] = value def _surf_source_read_from_xml_element(self, root): @@ -1824,11 +1822,12 @@ class Settings: if elem is None: return for key in ('surface_ids', 'max_particles', 'max_source_files', 'mcpl', 'cell', 'cellto', 'cellfrom'): - value = get_text(elem, key) + if key == 'surface_ids': + value = get_elem_list(elem, key, int) + else: + value = get_text(elem, key) if value is not None: - if key == 'surface_ids': - value = [int(x) for x in value.split()] - elif key == 'mcpl': + if key == 'mcpl': value = value in ('true', '1') elif key in ('max_particles', 'max_source_files', 'cell', 'cellfrom', 'cellto'): value = int(value) @@ -1958,22 +1957,21 @@ class Settings: text = get_text(root, 'temperature_method') if text is not None: self.temperature['method'] = text - text = get_text(root, 'temperature_range') + text = get_elem_list(root, "temperature_range", float) if text is not None: - self.temperature['range'] = [float(x) for x in text.split()] + self.temperature['range'] = text text = get_text(root, 'temperature_multipole') if text is not None: self.temperature['multipole'] = text in ('true', '1') def _trace_from_xml_element(self, root): - text = get_text(root, 'trace') + text = get_elem_list(root, "trace", int) if text is not None: - self.trace = [int(x) for x in text.split()] + self.trace = text def _track_from_xml_element(self, root): - text = get_text(root, 'track') - if text is not None: - values = [int(x) for x in text.split()] + values = get_elem_list(root, "track", int) + if values is not None: self.track = list(zip(values[::3], values[1::3], values[2::3])) def _ufs_mesh_from_xml_element(self, root, meshes): @@ -1990,14 +1988,15 @@ class Settings: if elem is not None: keys = ('enable', 'method', 'energy_min', 'energy_max', 'nuclides') for key in keys: - value = get_text(elem, key) + if key == 'nuclides': + value = get_elem_list(elem, key, str) + else: + value = get_text(elem, key) if value is not None: if key == 'enable': value = value in ('true', '1') elif key in ('energy_min', 'energy_max'): value = float(value) - elif key == 'nuclides': - value = value.split() self.resonance_scattering[key] = value def _create_fission_neutrons_from_xml_element(self, root): @@ -2109,8 +2108,8 @@ class Settings: mesh = MeshBase.from_xml_element(mesh_elem) domains = [] for domain_elem in mesh_elem.findall('domain'): - domain_id = int(domain_elem.get('id')) - domain_type = domain_elem.get('type') + domain_id = int(get_text(domain_elem, "id")) + domain_type = get_text(domain_elem, "type") if domain_type == 'material': domain = openmc.Material(domain_id) elif domain_type == 'cell': diff --git a/openmc/source.py b/openmc/source.py index 87e734e9a..c463ccb27 100644 --- a/openmc/source.py +++ b/openmc/source.py @@ -18,7 +18,7 @@ import openmc.checkvalue as cv from openmc.checkvalue import PathLike from openmc.stats.multivariate import UnitSphere, Spatial from openmc.stats.univariate import Univariate -from ._xml import get_text +from ._xml import get_elem_list, get_text from .mesh import MeshBase, StructuredMesh, UnstructuredMesh from .utility_funcs import input_path @@ -210,7 +210,7 @@ class SourceBase(ABC): constraints = {} domain_type = get_text(elem, "domain_type") if domain_type is not None: - domain_ids = [int(x) for x in get_text(elem, "domain_ids").split()] + domain_ids = get_elem_list(elem, "domain_ids", int) # Instantiate some throw-away domains that are used by the # constructor to assign IDs @@ -224,13 +224,13 @@ class SourceBase(ABC): domains = [openmc.Universe(uid) for uid in domain_ids] constraints['domains'] = domains - time_bounds = get_text(elem, "time_bounds") + time_bounds = get_elem_list(elem, "time_bounds", float) if time_bounds is not None: - constraints['time_bounds'] = [float(x) for x in time_bounds.split()] + constraints['time_bounds'] = time_bounds - energy_bounds = get_text(elem, "energy_bounds") + energy_bounds = get_elem_list(elem, "energy_bounds", float) if energy_bounds is not None: - constraints['energy_bounds'] = [float(x) for x in energy_bounds.split()] + constraints['energy_bounds'] = energy_bounds fissionable = get_text(elem, "fissionable") if fissionable is not None: diff --git a/openmc/stats/multivariate.py b/openmc/stats/multivariate.py index cd474fa9b..222d2d18a 100644 --- a/openmc/stats/multivariate.py +++ b/openmc/stats/multivariate.py @@ -10,7 +10,7 @@ import numpy as np import openmc import openmc.checkvalue as cv -from .._xml import get_text +from .._xml import get_elem_list, get_text from ..mesh import MeshBase from .univariate import PowerLaw, Uniform, Univariate @@ -152,9 +152,9 @@ class PolarAzimuthal(UnitSphere): """ mu_phi = cls() - uvw = get_text(elem, 'reference_uvw') + uvw = get_elem_list(elem, "reference_uvw", float) if uvw is not None: - mu_phi.reference_uvw = [float(x) for x in uvw.split()] + mu_phi.reference_uvw = uvw mu_phi.mu = Univariate.from_xml_element(elem.find('mu')) mu_phi.phi = Univariate.from_xml_element(elem.find('phi')) return mu_phi @@ -246,9 +246,9 @@ class Monodirectional(UnitSphere): """ monodirectional = cls() - uvw = get_text(elem, 'reference_uvw') + uvw = get_elem_list(elem, "reference_uvw", float) if uvw is not None: - monodirectional.reference_uvw = [float(x) for x in uvw.split()] + monodirectional.reference_uvw = uvw return monodirectional @@ -504,7 +504,7 @@ class SphericalIndependent(Spatial): r = Univariate.from_xml_element(elem.find('r')) cos_theta = Univariate.from_xml_element(elem.find('cos_theta')) phi = Univariate.from_xml_element(elem.find('phi')) - origin = [float(x) for x in elem.get('origin').split()] + origin = get_elem_list(elem, "origin", float) return cls(r, cos_theta, phi, origin=origin) @@ -626,7 +626,7 @@ 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 = [float(x) for x in elem.get('origin').split()] + origin = get_elem_list(elem, "origin", float) return cls(r, phi, z, origin=origin) @@ -743,18 +743,14 @@ class MeshSpatial(Spatial): """ - mesh_id = int(elem.get('mesh_id')) + mesh_id = int(get_text(elem, "mesh_id")) # check if this mesh has been read in from another location already if mesh_id not in meshes: raise ValueError(f'Could not locate mesh with ID "{mesh_id}"') - volume_normalized = elem.get("volume_normalized") volume_normalized = get_text(elem, 'volume_normalized').lower() == 'true' - strengths = get_text(elem, 'strengths') - if strengths is not None: - strengths = [float(b) for b in get_text(elem, 'strengths').split()] - + strengths = get_elem_list(elem, 'strengths', float) return cls(meshes[mesh_id], strengths, volume_normalized) @@ -860,12 +856,10 @@ class PointCloud(Spatial): """ - coord_data = get_text(elem, 'coords') - positions = np.array([float(b) for b in coord_data.split()]).reshape((-1, 3)) + coord_data = get_elem_list(elem, 'coords', float) + positions = np.array(coord_data).reshape((-1, 3)) - strengths = get_text(elem, 'strengths') - if strengths is not None: - strengths = [float(b) for b in strengths.split()] + strengths = get_elem_list(elem, 'strengths', float) return cls(positions, strengths) @@ -979,7 +973,7 @@ class Box(Spatial): """ only_fissionable = get_text(elem, 'type') == 'fission' - params = [float(x) for x in get_text(elem, 'parameters').split()] + params = get_elem_list(elem, "parameters", float) lower_left = params[:len(params)//2] upper_right = params[len(params)//2:] return cls(lower_left, upper_right, only_fissionable) @@ -1046,7 +1040,7 @@ class Point(Spatial): Point distribution generated from XML element """ - xyz = [float(x) for x in get_text(elem, 'parameters').split()] + xyz = get_elem_list(elem, "parameters", float) return cls(xyz) diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index e0475bf78..28d5e87ef 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -12,7 +12,7 @@ import numpy as np from scipy.integrate import trapezoid import openmc.checkvalue as cv -from .._xml import get_text +from .._xml import get_elem_list, get_text from ..mixin import EqualityMixin _INTERPOLATION_SCHEMES = { @@ -57,8 +57,7 @@ class Univariate(EqualityMixin, ABC): return Normal.from_xml_element(elem) elif distribution == 'muir': # Support older files where Muir had its own class - params = [float(x) for x in get_text(elem, 'parameters').split()] - return muir(*params) + return muir(*get_elem_list(elem, "parameters", float)) elif distribution == 'tabular': return Tabular.from_xml_element(elem) elif distribution == 'legendre': @@ -240,7 +239,7 @@ class Discrete(Univariate): Discrete distribution generated from XML element """ - params = [float(x) for x in get_text(elem, 'parameters').split()] + params = get_elem_list(elem, "parameters", float) x = params[:len(params)//2] p = params[len(params)//2:] return cls(x, p) @@ -448,8 +447,8 @@ class Uniform(Univariate): Uniform distribution generated from XML element """ - params = get_text(elem, 'parameters').split() - return cls(*map(float, params)) + params = get_elem_list(elem, "parameters", float) + return cls(*params) class PowerLaw(Univariate): @@ -557,8 +556,8 @@ class PowerLaw(Univariate): Distribution generated from XML element """ - params = get_text(elem, 'parameters').split() - return cls(*map(float, params)) + params = get_elem_list(elem, "parameters", float) + return cls(*params) class Maxwell(Univariate): @@ -737,8 +736,8 @@ class Watt(Univariate): Watt distribution generated from XML element """ - params = get_text(elem, 'parameters').split() - return cls(*map(float, params)) + params = get_elem_list(elem, "parameters", float) + return cls(*params) class Normal(Univariate): @@ -827,8 +826,8 @@ class Normal(Univariate): Normal distribution generated from XML element """ - params = get_text(elem, 'parameters').split() - return cls(*map(float, params)) + params = get_elem_list(elem, "parameters", float) + return cls(*params) def muir(e0: float, m_rat: float, kt: float): @@ -1115,7 +1114,7 @@ class Tabular(Univariate): """ interpolation = get_text(elem, 'interpolation') - params = [float(x) for x in get_text(elem, 'parameters').split()] + params = get_elem_list(elem, "parameters", float) m = (len(params) + 1)//2 # +1 for when len(params) is odd x = params[:m] p = params[m:] diff --git a/openmc/surface.py b/openmc/surface.py index 840c3125d..4839783ff 100644 --- a/openmc/surface.py +++ b/openmc/surface.py @@ -13,6 +13,7 @@ from .checkvalue import check_type, check_value, check_length, check_greater_tha from .mixin import IDManagerMixin, IDWarning from .region import Region, Intersection, Union from .bounding_box import BoundingBox +from ._xml import get_elem_list, get_text _BOUNDARY_TYPES = {'transmission', 'vacuum', 'reflective', 'periodic', 'white'} @@ -451,17 +452,17 @@ class Surface(IDManagerMixin, ABC): """ # Determine appropriate class - surf_type = elem.get('type') + surf_type = get_text(elem, "type") cls = _SURFACE_CLASSES[surf_type] # Determine ID, boundary type, boundary albedo, coefficients kwargs = {} - kwargs['surface_id'] = int(elem.get('id')) - kwargs['boundary_type'] = elem.get('boundary', 'transmission') + kwargs['surface_id'] = int(get_text(elem, "id")) + kwargs['boundary_type'] = get_text(elem, "boundary", "transmission") if kwargs['boundary_type'] in _ALBEDO_BOUNDARIES: - kwargs['albedo'] = float(elem.get('albedo', 1.0)) - kwargs['name'] = elem.get('name') - coeffs = [float(x) for x in elem.get('coeffs').split()] + kwargs['albedo'] = float(get_text(elem, "albedo", 1.0)) + kwargs['name'] = get_text(elem, "name") + coeffs = get_elem_list(elem, "coeffs", float) kwargs.update(dict(zip(cls._coeff_keys, coeffs))) return cls(**kwargs) diff --git a/openmc/tallies.py b/openmc/tallies.py index 3a5b42519..075b1e991 100644 --- a/openmc/tallies.py +++ b/openmc/tallies.py @@ -15,7 +15,7 @@ import scipy.sparse as sps import openmc import openmc.checkvalue as cv -from ._xml import clean_indentation, get_text +from ._xml import clean_indentation, get_elem_list, get_text from .mixin import IDManagerMixin from .mesh import MeshBase @@ -1006,8 +1006,8 @@ class Tally(IDManagerMixin): Tally object """ - tally_id = int(elem.get('id')) - name = elem.get('name', '') + tally_id = int(get_text(elem, "id")) + name = get_text(elem, "name", "") tally = cls(tally_id=tally_id, name=name) text = get_text(elem, 'multiply_density') @@ -1015,25 +1015,24 @@ class Tally(IDManagerMixin): tally.multiply_density = text in ('true', '1') # Read filters - filters_elem = elem.find('filters') - if filters_elem is not None: - filter_ids = [int(x) for x in filters_elem.text.split()] + filter_ids = get_elem_list(elem, "filters", int) + if filter_ids is not None: tally.filters = [kwargs['filters'][uid] for uid in filter_ids] # Read nuclides - nuclides_elem = elem.find('nuclides') - if nuclides_elem is not None: - tally.nuclides = nuclides_elem.text.split() + nuclides = get_elem_list(elem, "nuclides", str) + if nuclides is not None: + tally.nuclides = nuclides # Read scores - scores_elem = elem.find('scores') - if scores_elem is not None: - tally.scores = scores_elem.text.split() + scores = get_elem_list(elem, "scores", str) + if scores is not None: + tally.scores = scores # Set estimator - estimator_elem = elem.find('estimator') - if estimator_elem is not None: - tally.estimator = estimator_elem.text + estimator = get_text(elem, "estimator") + if estimator is not None: + tally.estimator = estimator # Read triggers tally.triggers = [ @@ -1042,9 +1041,9 @@ class Tally(IDManagerMixin): ] # Read tally derivative - deriv_elem = elem.find('derivative') - if deriv_elem is not None: - deriv_id = int(deriv_elem.text) + deriv = get_text(elem, "derivative") + if deriv is not None: + deriv_id = int(deriv) tally.derivative = kwargs['derivatives'][deriv_id] return tally diff --git a/openmc/tally_derivative.py b/openmc/tally_derivative.py index ff918c1b3..f7ba5dce5 100644 --- a/openmc/tally_derivative.py +++ b/openmc/tally_derivative.py @@ -4,6 +4,7 @@ import lxml.etree as ET import openmc.checkvalue as cv from .mixin import EqualityMixin, IDManagerMixin +from ._xml import get_text class TallyDerivative(EqualityMixin, IDManagerMixin): @@ -122,8 +123,8 @@ class TallyDerivative(EqualityMixin, IDManagerMixin): Tally derivative object """ - derivative_id = int(elem.get("id")) - variable = elem.get("variable") - material = int(elem.get("material")) - nuclide = elem.get("nuclide") if variable == "nuclide_density" else None + derivative_id = int(get_text(elem, "id")) + variable = get_text(elem, "variable") + material = int(get_text(elem, "material")) + nuclide = get_text(elem, "nuclide") if variable == "nuclide_density" else None return cls(derivative_id, variable, material, nuclide) diff --git a/openmc/trigger.py b/openmc/trigger.py index be2537e3a..70b6b7a03 100644 --- a/openmc/trigger.py +++ b/openmc/trigger.py @@ -5,6 +5,7 @@ import lxml.etree as ET import openmc.checkvalue as cv from .mixin import EqualityMixin +from ._xml import get_elem_list, get_text class Trigger(EqualityMixin): @@ -129,16 +130,16 @@ class Trigger(EqualityMixin): """ # Generate trigger object - trigger_type = elem.get("type") - threshold = float(elem.get("threshold")) - ignore_zeros = str(elem.get("ignore_zeros", "false")).lower() + trigger_type = get_text(elem, "type") + threshold = float(get_text(elem, "threshold")) + ignore_zeros = str(get_text(elem, "ignore_zeros", "false")).lower() # Try to convert to bool. Let Trigger error out on instantiation. ignore_zeros = ignore_zeros in ('true', '1') trigger = cls(trigger_type, threshold, ignore_zeros) # Add scores if present - scores = elem.get("scores") + scores = get_elem_list(elem, "scores", str) if scores is not None: - trigger.scores = scores.split() + trigger.scores = scores return trigger diff --git a/openmc/volume.py b/openmc/volume.py index df19def1e..c44adf98a 100644 --- a/openmc/volume.py +++ b/openmc/volume.py @@ -10,7 +10,7 @@ from uncertainties import ufloat import openmc import openmc.checkvalue as cv -from openmc._xml import get_text +from openmc._xml import get_elem_list, get_text _VERSION_VOLUME = 1 @@ -375,13 +375,10 @@ class VolumeCalculation: """ domain_type = get_text(elem, "domain_type") - domain_ids = get_text(elem, "domain_ids").split() - ids = [int(x) for x in domain_ids] + ids = get_elem_list(elem, "domain_ids", int) samples = int(get_text(elem, "samples")) - lower_left = get_text(elem, "lower_left").split() - lower_left = tuple([float(x) for x in lower_left]) - upper_right = get_text(elem, "upper_right").split() - upper_right = tuple([float(x) for x in upper_right]) + lower_left = tuple(get_elem_list(elem, "lower_left", float)) + upper_right = tuple(get_elem_list(elem, "upper_right", float)) # Instantiate some throw-away domains that are used by the constructor # to assign IDs diff --git a/openmc/weight_windows.py b/openmc/weight_windows.py index 90856ebab..5d52a579a 100644 --- a/openmc/weight_windows.py +++ b/openmc/weight_windows.py @@ -14,7 +14,7 @@ from openmc.filter import _PARTICLES from openmc.mesh import MeshBase, RectilinearMesh, CylindricalMesh, SphericalMesh, UnstructuredMesh import openmc.checkvalue as cv from openmc.checkvalue import PathLike -from ._xml import get_text, clean_indentation +from ._xml import get_elem_list, get_text, clean_indentation from .mixin import IDManagerMixin from .utility_funcs import change_directory @@ -379,9 +379,9 @@ class WeightWindows(IDManagerMixin): mesh = meshes[mesh_id] # Read all other parameters - lower_ww_bounds = [float(l) for l in get_text(elem, 'lower_ww_bounds').split()] - upper_ww_bounds = [float(u) for u in get_text(elem, 'upper_ww_bounds').split()] - e_bounds = [float(b) for b in get_text(elem, 'energy_bounds').split()] + lower_ww_bounds = get_elem_list(elem, "lower_ww_bounds", float) + upper_ww_bounds = get_elem_list(elem, "upper_ww_bounds", float) + e_bounds = get_elem_list(elem, "energy_bounds", float) particle_type = get_text(elem, 'particle_type') survival_ratio = float(get_text(elem, 'survival_ratio')) @@ -730,11 +730,8 @@ class WeightWindowGenerator: mesh_id = int(get_text(elem, 'mesh')) mesh = meshes[mesh_id] - - if (energy_bounds := get_text(elem, 'energy_bounds')) is not None: - energy_bounds = [float(x) for x in energy_bounds.split()] - else: - energy_bounds = None + + energy_bounds = get_elem_list(elem, "energy_bounds, float") particle_type = get_text(elem, 'particle_type') wwg = cls(mesh, energy_bounds, particle_type) From a34365396e8bd66698f5481474ebf17a5ed30d14 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Mon, 11 Aug 2025 18:51:45 +0300 Subject: [PATCH 389/671] Remove unused special accessors for tallies (#3527) --- include/openmc/tallies/tally.h | 2 -- src/tallies/tally.cpp | 4 ---- 2 files changed, 6 deletions(-) diff --git a/include/openmc/tallies/tally.h b/include/openmc/tallies/tally.h index 48f678ced..8c088c460 100644 --- a/include/openmc/tallies/tally.h +++ b/include/openmc/tallies/tally.h @@ -169,10 +169,8 @@ public: // We need to have quick access to some filters. The following gives indices // for various filters that could be in the tally or C_NONE if they are not // present. - int energy_filter_ {C_NONE}; int energyout_filter_ {C_NONE}; int delayedgroup_filter_ {C_NONE}; - int cell_filter_ {C_NONE}; vector triggers_; diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index e737f79d3..ba1838602 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -487,10 +487,6 @@ void Tally::add_filter(Filter* filter) energyout_filter_ = filters_.size(); } else if (filter->type() == FilterType::DELAYED_GROUP) { delayedgroup_filter_ = filters_.size(); - } else if (filter->type() == FilterType::CELL) { - cell_filter_ = filters_.size(); - } else if (filter->type() == FilterType::ENERGY) { - energy_filter_ = filters_.size(); } filters_.push_back(filter_idx); } From f8fa751da0437ddf42a1f4873e5162a6fcb1c51e Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Wed, 13 Aug 2025 15:41:39 +0200 Subject: [PATCH 390/671] Adding 616 group structure (#3531) --- openmc/mgxs/__init__.py | 94 ++++++++++++++++++++++++++++++++++++++++- src/particle.cpp | 2 +- 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/openmc/mgxs/__init__.py b/openmc/mgxs/__init__.py index 901c1eabb..f8de84b37 100644 --- a/openmc/mgxs/__init__.py +++ b/openmc/mgxs/__init__.py @@ -21,7 +21,7 @@ GROUP_STRUCTURES = {} - "ECCO-1968_" designed for fine group reactor cell calculations for fast, intermediate and thermal reactor applications ([SAR1990]_) - activation_ energy group structures "VITAMIN-J-42", "VITAMIN-J-175", - "TRIPOLI-315", "CCFE-709_" and "UKAEA-1102_" + "TRIPOLI-315", "LLNL-616", "CCFE-709_" and "UKAEA-1102_" .. _CASMO: http://large.stanford.edu/courses/2013/ph241/dalvi1/docs/c5.physor2006.pdf .. _SCALE44: https://www-nds.iaea.org/publications/indc/indc-czr-0001.pdf @@ -31,6 +31,7 @@ GROUP_STRUCTURES = {} .. _SHEM-361: http://merlin.polymtl.ca/downloads/FP214.pdf .. _activation: https://fispact.ukaea.uk/wiki/Keyword:GETXS .. _VITAMIN-J-42: https://www.oecd-nea.org/dbdata/nds_jefreports/jefreport-10.pdf +.. _LLNL-616: https://fispact.ukaea.uk/manual/user_manual.pdf .. _CCFE-709: https://fispact.ukaea.uk/wiki/CCFE-709_group_structure .. _UKAEA-1102: https://fispact.ukaea.uk/wiki/UKAEA-1102_group_structure .. _ECCO-1968: https://serpent.vtt.fi/mediawiki/index.php/ECCO_1968-group_structure @@ -346,6 +347,97 @@ GROUP_STRUCTURES['SHEM-361'] = np.array([ 4.06569e+06, 4.96585e+06, 6.06530e+06, 6.70319e+06, 7.40817e+06, 8.18730e+06, 9.04836e+06, 9.99999e+06, 1.16183e+07, 1.38403e+07, 1.49182e+07, 1.96403e+07]) +GROUP_STRUCTURES['LLNL-616'] = np.array([ + 1.0000e-5, 1.0471e-5, 1.0965e-5, 1.1482e-5, 1.2023e-5, 1.2589e-5, 1.3183e-5, + 1.3804e-5, 1.4454e-5, 1.5136e-5, 1.5849e-5, 1.6596e-5, 1.7378e-5, 1.8197e-5, + 1.9055e-5, 1.9953e-5, 2.0893e-5, 2.1878e-5, 2.2909e-5, 2.3988e-5, 2.5119e-5, + 2.6303e-5, 2.7542e-5, 2.8840e-5, 3.0200e-5, 3.1623e-5, 3.3113e-5, 3.4674e-5, + 3.6308e-5, 3.8019e-5, 3.9811e-5, 4.1687e-5, 4.3652e-5, 4.5709e-5, 4.7863e-5, + 5.0119e-5, 5.2481e-5, 5.4954e-5, 5.7544e-5, 6.0256e-5, 6.3096e-5, 6.6069e-5, + 6.9183e-5, 7.2444e-5, 7.5858e-5, 7.9433e-5, 8.3176e-5, 8.7096e-5, 9.1201e-5, + 9.5499e-5, 1.0000e-4, 1.0471e-4, 1.0965e-4, 1.1482e-4, 1.2023e-4, 1.2589e-4, + 1.3183e-4, 1.3804e-4, 1.4454e-4, 1.5136e-4, 1.5849e-4, 1.6596e-4, 1.7378e-4, + 1.8197e-4, 1.9055e-4, 1.9953e-4, 2.0893e-4, 2.1878e-4, 2.2909e-4, 2.3988e-4, + 2.5119e-4, 2.6303e-4, 2.7542e-4, 2.8840e-4, 3.0200e-4, 3.1623e-4, 3.3113e-4, + 3.4674e-4, 3.6308e-4, 3.8019e-4, 3.9811e-4, 4.1687e-4, 4.3652e-4, 4.5709e-4, + 4.7863e-4, 5.0119e-4, 5.2481e-4, 5.4954e-4, 5.7544e-4, 6.0256e-4, 6.3096e-4, + 6.6069e-4, 6.9183e-4, 7.2444e-4, 7.5858e-4, 7.9433e-4, 8.3176e-4, 8.7096e-4, + 9.1201e-4, 9.5499e-4, 1.0000e-3, 1.0471e-3, 1.0965e-3, 1.1482e-3, 1.2023e-3, + 1.2589e-3, 1.3183e-3, 1.3804e-3, 1.4454e-3, 1.5136e-3, 1.5849e-3, 1.6596e-3, + 1.7378e-3, 1.8197e-3, 1.9055e-3, 1.9953e-3, 2.0893e-3, 2.1878e-3, 2.2909e-3, + 2.3988e-3, 2.5119e-3, 2.6303e-3, 2.7542e-3, 2.8840e-3, 3.0200e-3, 3.1623e-3, + 3.3113e-3, 3.4674e-3, 3.6308e-3, 3.8019e-3, 3.9811e-3, 4.1687e-3, 4.3652e-3, + 4.5709e-3, 4.7863e-3, 5.0119e-3, 5.2481e-3, 5.4954e-3, 5.7544e-3, 6.0256e-3, + 6.3096e-3, 6.6069e-3, 6.9183e-3, 7.2444e-3, 7.5858e-3, 7.9433e-3, 8.3176e-3, + 8.7096e-3, 9.1201e-3, 9.5499e-3, 1.0000e-2, 1.0471e-2, 1.0965e-2, 1.1482e-2, + 1.2023e-2, 1.2589e-2, 1.3183e-2, 1.3804e-2, 1.4454e-2, 1.5136e-2, 1.5849e-2, + 1.6596e-2, 1.7378e-2, 1.8197e-2, 1.9055e-2, 1.9953e-2, 2.0893e-2, 2.1878e-2, + 2.2909e-2, 2.3988e-2, 2.5119e-2, 2.6303e-2, 2.7542e-2, 2.8840e-2, 3.0200e-2, + 3.1623e-2, 3.3113e-2, 3.4674e-2, 3.6308e-2, 3.8019e-2, 3.9811e-2, 4.1687e-2, + 4.3652e-2, 4.5709e-2, 4.7863e-2, 5.0119e-2, 5.2481e-2, 5.4954e-2, 5.7544e-2, + 6.0256e-2, 6.3096e-2, 6.6069e-2, 6.9183e-2, 7.2444e-2, 7.5858e-2, 7.9433e-2, + 8.3176e-2, 8.7096e-2, 9.1201e-2, 9.5499e-2, 1.0000e-1, 1.0471e-1, 1.0965e-1, + 1.1482e-1, 1.2023e-1, 1.2589e-1, 1.3183e-1, 1.3804e-1, 1.4454e-1, 1.5136e-1, + 1.5849e-1, 1.6596e-1, 1.7378e-1, 1.8197e-1, 1.9055e-1, 1.9953e-1, 2.0893e-1, + 2.1878e-1, 2.2909e-1, 2.3988e-1, 2.5119e-1, 2.6303e-1, 2.7542e-1, 2.8840e-1, + 3.0200e-1, 3.1623e-1, 3.3113e-1, 3.4674e-1, 3.6308e-1, 3.8019e-1, 3.9811e-1, + 4.1687e-1, 4.3652e-1, 4.5709e-1, 4.7863e-1, 5.0119e-1, 5.2481e-1, 5.4954e-1, + 5.7544e-1, 6.0256e-1, 6.3096e-1, 6.6069e-1, 6.9183e-1, 7.2444e-1, 7.5858e-1, + 7.9433e-1, 8.3176e-1, 8.7096e-1, 9.1201e-1, 9.5499e-1, 1.0000e0, 1.0471e0, + 1.0965e0, 1.1482e0, 1.2023e0, 1.2589e0, 1.3183e0, 1.3804e0, 1.4454e0, + 1.5136e0, 1.5849e0, 1.6596e0, 1.7378e0, 1.8197e0, 1.9055e0, 1.9953e0, + 2.0893e0, 2.1878e0, 2.2909e0, 2.3988e0, 2.5119e0, 2.6303e0, 2.7542e0, + 2.8840e0, 3.0200e0, 3.1623e0, 3.3113e0, 3.4674e0, 3.6308e0, 3.8019e0, + 3.9811e0, 4.1687e0, 4.3652e0, 4.5709e0, 4.7863e0, 5.0119e0, 5.2481e0, + 5.4954e0, 5.7544e0, 6.0256e0, 6.3096e0, 6.6069e0, 6.9183e0, 7.2444e0, + 7.5858e0, 7.9433e0, 8.3176e0, 8.7096e0, 9.1201e0, 9.5499e0, 1.0000e1, + 1.0471e1, 1.0965e1, 1.1482e1, 1.2023e1, 1.2589e1, 1.3183e1, 1.3804e1, + 1.4454e1, 1.5136e1, 1.5849e1, 1.6596e1, 1.7378e1, 1.8197e1, 1.9055e1, + 1.9953e1, 2.0893e1, 2.1878e1, 2.2909e1, 2.3988e1, 2.5119e1, 2.6303e1, + 2.7542e1, 2.8840e1, 3.0200e1, 3.1623e1, 3.3113e1, 3.4674e1, 3.6308e1, + 3.8019e1, 3.9811e1, 4.1687e1, 4.3652e1, 4.5709e1, 4.7863e1, 5.0119e1, + 5.2481e1, 5.4954e1, 5.7544e1, 6.0256e1, 6.3096e1, 6.6069e1, 6.9183e1, + 7.2444e1, 7.5858e1, 7.9433e1, 8.3176e1, 8.7096e1, 9.1201e1, 9.5499e1, + 1.0000e2, 1.0471e2, 1.0965e2, 1.1482e2, 1.2023e2, 1.2589e2, 1.3183e2, + 1.3804e2, 1.4454e2, 1.5136e2, 1.5849e2, 1.6596e2, 1.7378e2, 1.8197e2, + 1.9055e2, 1.9953e2, 2.0893e2, 2.1878e2, 2.2909e2, 2.3988e2, 2.5119e2, + 2.6303e2, 2.7542e2, 2.8840e2, 3.0200e2, 3.1623e2, 3.3113e2, 3.4674e2, + 3.6308e2, 3.8019e2, 3.9811e2, 4.1687e2, 4.3652e2, 4.5709e2, 4.7863e2, + 5.0119e2, 5.2481e2, 5.4954e2, 5.7544e2, 6.0256e2, 6.3096e2, 6.6069e2, + 6.9183e2, 7.2444e2, 7.5858e2, 7.9433e2, 8.3176e2, 8.7096e2, 9.1201e2, + 9.5499e2, 1.0000e3, 1.0471e3, 1.0965e3, 1.1482e3, 1.2023e3, 1.2589e3, + 1.3183e3, 1.3804e3, 1.4454e3, 1.5136e3, 1.5849e3, 1.6596e3, 1.7378e3, + 1.8197e3, 1.9055e3, 1.9953e3, 2.0893e3, 2.1878e3, 2.2909e3, 2.3988e3, + 2.5119e3, 2.6303e3, 2.7542e3, 2.8840e3, 3.0200e3, 3.1623e3, 3.3113e3, + 3.4674e3, 3.6308e3, 3.8019e3, 3.9811e3, 4.1687e3, 4.3652e3, 4.5709e3, + 4.7863e3, 5.0119e3, 5.2481e3, 5.4954e3, 5.7544e3, 6.0256e3, 6.3096e3, + 6.6069e3, 6.9183e3, 7.2444e3, 7.5858e3, 7.9433e3, 8.3176e3, 8.7096e3, + 9.1201e3, 9.5499e3, 1.0000e4, 1.0471e4, 1.0965e4, 1.1482e4, 1.2023e4, + 1.2589e4, 1.3183e4, 1.3804e4, 1.4454e4, 1.5136e4, 1.5849e4, 1.6596e4, + 1.7378e4, 1.8197e4, 1.9055e4, 1.9953e4, 2.0893e4, 2.1878e4, 2.2909e4, + 2.3988e4, 2.5119e4, 2.6303e4, 2.7542e4, 2.8840e4, 3.0200e4, 3.1623e4, + 3.3113e4, 3.4674e4, 3.6308e4, 3.8019e4, 3.9811e4, 4.1687e4, 4.3652e4, + 4.5709e4, 4.7863e4, 5.0119e4, 5.2481e4, 5.4954e4, 5.7544e4, 6.0256e4, + 6.3096e4, 6.6069e4, 6.9183e4, 7.2444e4, 7.5858e4, 7.9433e4, 8.3176e4, + 8.7096e4, 9.1201e4, 9.5499e4, 1.0000e5, 1.0471e5, 1.0965e5, 1.1482e5, + 1.2023e5, 1.2589e5, 1.3183e5, 1.3804e5, 1.4454e5, 1.5136e5, 1.5849e5, + 1.6596e5, 1.7378e5, 1.8197e5, 1.9055e5, 1.9953e5, 2.0893e5, 2.1878e5, + 2.2909e5, 2.3988e5, 2.5119e5, 2.6303e5, 2.7542e5, 2.8840e5, 3.0200e5, + 3.1623e5, 3.3113e5, 3.4674e5, 3.6308e5, 3.8019e5, 3.9811e5, 4.1687e5, + 4.3652e5, 4.5709e5, 4.7863e5, 5.0119e5, 5.2481e5, 5.4954e5, 5.7544e5, + 6.0256e5, 6.3096e5, 6.6069e5, 6.9183e5, 7.2444e5, 7.5858e5, 7.9433e5, + 8.3176e5, 8.7096e5, 9.1201e5, 9.5499e5, 1.0000e6, 1.0471e6, 1.0965e6, + 1.1482e6, 1.2023e6, 1.2589e6, 1.3183e6, 1.3804e6, 1.4454e6, 1.5136e6, + 1.5849e6, 1.6596e6, 1.7378e6, 1.8197e6, 1.9055e6, 1.9953e6, 2.0893e6, + 2.1878e6, 2.2909e6, 2.3988e6, 2.5119e6, 2.6303e6, 2.7542e6, 2.8840e6, + 3.0200e6, 3.1623e6, 3.3113e6, 3.4674e6, 3.6308e6, 3.8019e6, 3.9811e6, + 4.1687e6, 4.3652e6, 4.5709e6, 4.7863e6, 5.0119e6, 5.2481e6, 5.4954e6, + 5.7544e6, 6.0256e6, 6.3096e6, 6.6069e6, 6.9183e6, 7.2444e6, 7.5858e6, + 7.9433e6, 8.3176e6, 8.7096e6, 9.1201e6, 9.5499e6, 1.0000e7, 1.0471e7, + 1.0965e7, 1.1482e7, 1.2023e7, 1.2589e7, 1.3183e7, 1.3804e7, 1.4454e7, + 1.5136e7, 1.5849e7, 1.6596e7, 1.7378e7, 1.8197e7, 1.9055e7, 1.9953e7, + 2.0000e7 +]) GROUP_STRUCTURES['CCFE-709'] = np.array([ 1.e-5, 1.0471e-5, 1.0965e-5, 1.1482e-5, 1.2023e-5, 1.2589e-5, 1.3183e-5, 1.3804e-5, 1.4454e-5, 1.5136e-5, diff --git a/src/particle.cpp b/src/particle.cpp index 118b084b0..7a003feb9 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -105,7 +105,7 @@ void Particle::split(double wgt) bank.E = settings::run_CE ? E() : g(); bank.time = time(); - // Convert signed index to a singed surface ID + // Convert signed index to a signed surface ID if (surface() == SURFACE_NONE) { bank.surf_id = SURFACE_NONE; } else { From 14d51e0ebaa4de3f122ec85478002d559245fabc Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Thu, 14 Aug 2025 20:26:27 +0200 Subject: [PATCH 391/671] more helpful error message for dose_coefficients (#3534) --- openmc/data/effective_dose/dose.py | 7 ++++++- tests/unit_tests/test_data_dose.py | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/openmc/data/effective_dose/dose.py b/openmc/data/effective_dose/dose.py index c7f458d1c..d49043b0a 100644 --- a/openmc/data/effective_dose/dose.py +++ b/openmc/data/effective_dose/dose.py @@ -82,7 +82,12 @@ def dose_coefficients(particle, geometry='AP', data_source='icrp116'): cv.check_value('data_source', data_source, {'icrp74', 'icrp116'}) if (data_source, particle) not in _FILES: - raise ValueError(f"{particle} has no dose data in data source {data_source}.") + available_particles = sorted({p for (ds, p) in _FILES if ds == data_source}) + msg = ( + f"'{particle}' has no dose data in data source {data_source}. " + f"Available particles for {data_source} are: {available_particles}" + ) + raise ValueError(msg) elif (data_source, particle) not in _DOSE_TABLES: _load_dose_icrp(data_source, particle) diff --git a/tests/unit_tests/test_data_dose.py b/tests/unit_tests/test_data_dose.py index 2d80cf838..4f1880014 100644 --- a/tests/unit_tests/test_data_dose.py +++ b/tests/unit_tests/test_data_dose.py @@ -41,3 +41,23 @@ def test_dose_coefficients(): dose_coefficients('neutron', 'ZZ') with raises(ValueError): dose_coefficients('neutron', data_source='icrp7000') + with raises(ValueError) as excinfo: + dose_coefficients("photons", data_source="icrp116") + expected_particles = [ + "electron", + "helium", + "mu+", + "mu-", + "neutron", + "photon", + "photon kerma", + "pi+", + "pi-", + "positron", + "proton", + ] + expected_msg = ( + "'photons' has no dose data in data source icrp116. " + f"Available particles for icrp116 are: {expected_particles}" + ) + assert str(excinfo.value) == expected_msg From 75b581301256c3c77676f798883d993917c1d221 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Mon, 18 Aug 2025 15:42:38 +0200 Subject: [PATCH 392/671] Use cached property for openmc.data.Decay.sources (#3535) Co-authored-by: Paul Romano --- openmc/data/decay.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/openmc/data/decay.py b/openmc/data/decay.py index 66acb2212..1a11d3614 100644 --- a/openmc/data/decay.py +++ b/openmc/data/decay.py @@ -1,4 +1,5 @@ from collections.abc import Iterable +from functools import cached_property from io import StringIO from math import log import re @@ -339,7 +340,6 @@ class Decay(EqualityMixin): self.modes = [] self.spectra = {} self.average_energies = {} - self._sources = None # Get head record items = get_head_record(file_obj) @@ -506,14 +506,9 @@ class Decay(EqualityMixin): """ return cls(ev_or_filename) - @property + @cached_property def sources(self): """Radioactive decay source distributions""" - # If property has been computed already, return it - # TODO: Replace with functools.cached_property when support is Python 3.9+ - if self._sources is not None: - return self._sources - sources = {} name = self.nuclide['name'] decay_constant = self.decay_constant.n @@ -571,8 +566,7 @@ class Decay(EqualityMixin): merged_sources[particle_type] = combine_distributions( dist_list, [1.0]*len(dist_list)) - self._sources = merged_sources - return self._sources + return merged_sources _DECAY_PHOTON_ENERGY = {} From c5a71738642244a5d7a27898dd0491f1e3966a6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Delaporte-Mathurin?= <40028739+RemDelaporteMathurin@users.noreply.github.com> Date: Mon, 18 Aug 2025 10:34:12 -0400 Subject: [PATCH 393/671] Avoid duplicate materials written to XML (#3536) Co-authored-by: Paul Romano --- openmc/material.py | 2 +- tests/unit_tests/test_materials.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/openmc/material.py b/openmc/material.py index 0afe5b670..c7b954b66 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -1927,7 +1927,7 @@ class Materials(cv.CheckedList): file.write(ET.tostring(element, encoding="unicode")) # Write the elements. - for material in sorted(self, key=lambda x: x.id): + for material in sorted(set(self), key=lambda x: x.id): element = material.to_xml_element(nuclides_to_ignore=nuclides_to_ignore) clean_indentation(element, level=level+1) element.tail = element.tail.strip(' ') diff --git a/tests/unit_tests/test_materials.py b/tests/unit_tests/test_materials.py index 6c8c59fc9..03484e2b6 100644 --- a/tests/unit_tests/test_materials.py +++ b/tests/unit_tests/test_materials.py @@ -61,3 +61,19 @@ def test_materials_deplete(): # Ni59 is one of the main activation product of Ni60 in the first irradiation # step. It then decays in the second cooling step (flux = 0) assert Ni59_mat_1_step_1 > 0.0 and Ni59_mat_1_step_1 > Ni59_mat_1_step_2 + + +def test_export_duplicate_materials_to_xml(run_in_tmpdir): + """ + Test exporting Materials to xml with a duplicate and checking that only + unique entities are exported. + """ + my_mat = openmc.Material(name="my_mat") + my_mat2 = openmc.Material(name="my_mat2") + + materials = openmc.Materials([my_mat, my_mat2, my_mat]) + + materials.export_to_xml("materials.xml") + + materials_in = openmc.Materials.from_xml("materials.xml") + assert len(materials_in) == 2 From 68e894c4b01cfdc027b8961a0f02be987bb44c53 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Mon, 18 Aug 2025 16:40:54 +0200 Subject: [PATCH 394/671] Fix a bug in time cutoff behavior (#3526) --- src/particle.cpp | 77 ++++++++----------- src/physics_mg.cpp | 1 + .../time_cutoff/results_true.dat | 4 +- tests/unit_tests/test_mesh.py | 2 +- 4 files changed, 37 insertions(+), 47 deletions(-) diff --git a/src/particle.cpp b/src/particle.cpp index 7a003feb9..a0f1d66bd 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -44,34 +44,30 @@ namespace openmc { double Particle::speed() const { - // Determine mass in eV/c^2 - double mass; - switch (this->type()) { - case ParticleType::neutron: - mass = MASS_NEUTRON_EV; - break; - case ParticleType::photon: - mass = 0.0; - break; - case ParticleType::electron: - case ParticleType::positron: - mass = MASS_ELECTRON_EV; - break; - } - - if (this->E() < 1.0e-9 * mass) { - // If the energy is much smaller than the mass, revert to non-relativistic - // formula. The 1e-9 criterion is specifically chosen as the point below - // which the error from using the non-relativistic formula is less than the - // round-off eror when using the relativistic formula (see analysis at - // https://gist.github.com/paulromano/da3b473fe3df33de94b265bdff0c7817) - return C_LIGHT * std::sqrt(2 * this->E() / mass); + if (settings::run_CE) { + // Determine mass in eV/c^2 + double mass; + switch (this->type()) { + case ParticleType::neutron: + mass = MASS_NEUTRON_EV; + break; + case ParticleType::photon: + mass = 0.0; + break; + case ParticleType::electron: + case ParticleType::positron: + mass = MASS_ELECTRON_EV; + break; + } + // Equivalent to C * sqrt(1-(m/(m+E))^2) without problem at E<E() * (this->E() + 2 * mass)) / + (this->E() + mass); } else { - // Calculate inverse of Lorentz factor - const double inv_gamma = mass / (this->E() + mass); - - // Calculate speed via v = c * sqrt(1 - γ^-2) - return C_LIGHT * std::sqrt(1 - inv_gamma * inv_gamma); + auto& macro_xs = data::mg.macro_xs_[this->material()]; + int macro_t = this->mg_xs_cache().t; + int macro_a = macro_xs.get_angle_index(this->u()); + return 1.0 / macro_xs.get_xs(MgxsType::INVERSE_VELOCITY, this->g(), nullptr, + nullptr, nullptr, macro_t, macro_a); } } @@ -241,8 +237,14 @@ void Particle::event_advance() collision_distance() = -std::log(prn(current_seed())) / macro_xs().total; } - // Select smaller of the two distances - double distance = std::min(boundary().distance(), collision_distance()); + double speed = this->speed(); + double time_cutoff = settings::time_cutoff[static_cast(type())]; + double distance_cutoff = + (time_cutoff < INFTY) ? (time_cutoff - time()) * speed : INFTY; + + // Select smaller of the three distances + double distance = + std::min({boundary().distance(), collision_distance(), distance_cutoff}); // Advance particle in space and time // Short-term solution until the surface source is revised and we can use @@ -250,23 +252,10 @@ void Particle::event_advance() for (int j = 0; j < n_coord(); ++j) { coord(j).r() += distance * coord(j).u(); } - double dt = distance / this->speed(); + double dt = distance / speed; this->time() += dt; this->lifetime() += dt; - // Kill particle if its time exceeds the cutoff - bool hit_time_boundary = false; - double time_cutoff = settings::time_cutoff[static_cast(type())]; - if (time() > time_cutoff) { - double dt = time() - time_cutoff; - time() = time_cutoff; - lifetime() = time_cutoff; - - double push_back_distance = speed() * dt; - this->move_distance(-push_back_distance); - hit_time_boundary = true; - } - // Score track-length tallies if (!model::active_tracklength_tallies.empty()) { score_tracklength_tally(*this, distance); @@ -284,7 +273,7 @@ void Particle::event_advance() } // Set particle weight to zero if it hit the time boundary - if (hit_time_boundary) { + if (distance == distance_cutoff) { wgt() = 0.0; } } diff --git a/src/physics_mg.cpp b/src/physics_mg.cpp index c97a3d6f3..5ebef9c14 100644 --- a/src/physics_mg.cpp +++ b/src/physics_mg.cpp @@ -137,6 +137,7 @@ void create_fission_sites(Particle& p) SourceSite site; site.r = p.r(); site.particle = ParticleType::neutron; + site.time = p.time(); site.wgt = 1. / weight; site.parent_id = p.id(); site.progeny_id = p.n_progeny()++; diff --git a/tests/regression_tests/time_cutoff/results_true.dat b/tests/regression_tests/time_cutoff/results_true.dat index 1cafceb77..d3d5e1b3c 100644 --- a/tests/regression_tests/time_cutoff/results_true.dat +++ b/tests/regression_tests/time_cutoff/results_true.dat @@ -1,5 +1,5 @@ tally 1: -2.000000E+03 -4.000000E+05 +1.383148E+02 +1.913099E+03 0.000000E+00 0.000000E+00 diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index 3c8e988c0..67ca4028e 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -620,7 +620,7 @@ def test_mesh_material_volumes_serialize(): assert new_volumes.by_element(3) == [(2, 1.0)] -def test_raytrace_mesh_infinite_loop(): +def test_raytrace_mesh_infinite_loop(run_in_tmpdir): # Create a model with one large spherical cell sphere = openmc.Sphere(r=100, boundary_type='vacuum') cell = openmc.Cell(region=-sphere) From e878aeb82ff240478e3079787a1f94ea5ac58e7f Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Thu, 21 Aug 2025 11:14:21 +0200 Subject: [PATCH 395/671] not printing nuclides with 0 percent to terminal (option 2 ) (#3448) --- openmc/deplete/stepresult.py | 2 +- tests/unit_tests/test_deplete_resultslist.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/openmc/deplete/stepresult.py b/openmc/deplete/stepresult.py index 7c86b8645..1a26cbe34 100644 --- a/openmc/deplete/stepresult.py +++ b/openmc/deplete/stepresult.py @@ -233,7 +233,7 @@ class StepResult: ) from e for nuc, _ in sorted(self.index_nuc.items(), key=lambda x: x[1]): atoms = self[0, mat_id, nuc] - if atoms < 0.0: + if atoms <= 0.0: continue atom_per_bcm = atoms / vol * 1e-24 material.add_nuclide(nuc, atom_per_bcm) diff --git a/tests/unit_tests/test_deplete_resultslist.py b/tests/unit_tests/test_deplete_resultslist.py index b22a78650..308eccf7b 100644 --- a/tests/unit_tests/test_deplete_resultslist.py +++ b/tests/unit_tests/test_deplete_resultslist.py @@ -220,5 +220,4 @@ def test_stepresult_get_material(res): # Spot check number densities densities = mat1.get_nuclide_atom_densities() assert densities['Xe135'] == pytest.approx(1e-14) - assert densities['I135'] == pytest.approx(1e-21) assert densities['U234'] == pytest.approx(1.00506e-05) From 5e3249f0063e1bb9c2fd69d3b7542b4106d20baa Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Thu, 21 Aug 2025 13:13:50 -0800 Subject: [PATCH 396/671] fix tests that accidentaly got broken (#3543) --- tests/unit_tests/test_material.py | 7 ++++--- tests/unit_tests/test_materials.py | 9 +++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/unit_tests/test_material.py b/tests/unit_tests/test_material.py index eae814a75..2e3724272 100644 --- a/tests/unit_tests/test_material.py +++ b/tests/unit_tests/test_material.py @@ -738,11 +738,12 @@ def test_material_deplete(): chain_file=chain, ) - for material in depleted_material: + for i_step, material in enumerate(depleted_material): assert isinstance(material, openmc.Material) - assert len(material.get_nuclides()) > len(pristine_material.get_nuclides()) + if i_step > 0: + assert len(material.get_nuclides()) > len(pristine_material.get_nuclides()) - Co58_mat_1_step_0 = depleted_material[0].get_nuclide_atom_densities("Co58")["Co58"] + Co58_mat_1_step_0 = depleted_material[0].get_nuclide_atom_densities("Co58").get("Co58", 0.0) Co58_mat_1_step_1 = depleted_material[1].get_nuclide_atom_densities("Co58")["Co58"] Co58_mat_1_step_2 = depleted_material[2].get_nuclide_atom_densities("Co58")["Co58"] diff --git a/tests/unit_tests/test_materials.py b/tests/unit_tests/test_materials.py index 03484e2b6..5a382b777 100644 --- a/tests/unit_tests/test_materials.py +++ b/tests/unit_tests/test_materials.py @@ -38,13 +38,14 @@ def test_materials_deplete(): assert list(depleted_material.keys()) == [pristine_material_1.id, pristine_material_2.id] for mat_id, materials in depleted_material.items(): - for material in materials: + for i_step, material in enumerate(materials): assert isinstance(material, openmc.Material) - assert len(material.get_nuclides()) > 1 + if i_step > 0: + assert len(material.get_nuclides()) > 1 assert mat_id == material.id mats = depleted_material[pristine_material_1.id] - Co58_mat_1_step_0 = mats[0].get_nuclide_atom_densities("Co58")["Co58"] + Co58_mat_1_step_0 = mats[0].get_nuclide_atom_densities("Co58").get("Co58", 0.0) Co58_mat_1_step_1 = mats[1].get_nuclide_atom_densities("Co58")["Co58"] Co58_mat_1_step_2 = mats[2].get_nuclide_atom_densities("Co58")["Co58"] @@ -53,7 +54,7 @@ def test_materials_deplete(): # It then decays in the second cooling step (flux = 0) assert Co58_mat_1_step_1 > 0.0 and Co58_mat_1_step_1 > Co58_mat_1_step_2 - Ni59_mat_1_step_0 = mats[0].get_nuclide_atom_densities("Ni59")["Ni59"] + Ni59_mat_1_step_0 = mats[0].get_nuclide_atom_densities("Ni59").get("Ni59", 0.0) Ni59_mat_1_step_1 = mats[1].get_nuclide_atom_densities("Ni59")["Ni59"] Ni59_mat_1_step_2 = mats[2].get_nuclide_atom_densities("Ni59")["Ni59"] From d1df80a210f94a9b68194310e1e540a863b2ce8c Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Fri, 22 Aug 2025 06:58:39 -0800 Subject: [PATCH 397/671] Leverage particle.move_distance in event advance (#3544) --- src/particle.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/particle.cpp b/src/particle.cpp index a0f1d66bd..fb41d82e9 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -247,11 +247,7 @@ void Particle::event_advance() std::min({boundary().distance(), collision_distance(), distance_cutoff}); // Advance particle in space and time - // Short-term solution until the surface source is revised and we can use - // this->move_distance(distance) - for (int j = 0; j < n_coord(); ++j) { - coord(j).r() += distance * coord(j).u(); - } + this->move_distance(distance); double dt = distance / speed; this->time() += dt; this->lifetime() += dt; From b14eff47603dbc1e52fbef11cced930bbc6869b2 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Wed, 27 Aug 2025 22:28:11 +0300 Subject: [PATCH 398/671] fix broken CI (#3551) --- .github/workflows/format-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml index cef14ca2c..c2ceb88e2 100644 --- a/.github/workflows/format-check.yml +++ b/.github/workflows/format-check.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: cpp-linter/cpp-linter-action@v2 + - uses: cpp-linter/cpp-linter-action@v2.16.0 id: linter env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 2a278c93a67e6666c4408b752fd9c4f0e91e7cfe Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Thu, 28 Aug 2025 02:18:06 +0300 Subject: [PATCH 399/671] Revert "fix broken CI" (#3554) --- .github/workflows/format-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml index c2ceb88e2..cef14ca2c 100644 --- a/.github/workflows/format-check.yml +++ b/.github/workflows/format-check.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: cpp-linter/cpp-linter-action@v2.16.0 + - uses: cpp-linter/cpp-linter-action@v2 id: linter env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 5529731e2f7933b3386a854db24edf3e2e535a44 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Fri, 29 Aug 2025 15:06:33 +0300 Subject: [PATCH 400/671] Refactor endf_data to be a fixture (#3539) --- openmc/config.py | 16 +++---- tests/conftest.py | 6 ++- tests/unit_tests/test_data_decay.py | 9 ++-- tests/unit_tests/test_data_kalbach_mann.py | 7 +-- tests/unit_tests/test_data_misc.py | 2 +- tests/unit_tests/test_data_multipole.py | 10 ++-- tests/unit_tests/test_data_neutron.py | 56 ++++++++-------------- tests/unit_tests/test_data_photon.py | 7 ++- tests/unit_tests/test_data_thermal.py | 22 ++++----- tests/unit_tests/test_deplete_chain.py | 10 ++-- 10 files changed, 59 insertions(+), 86 deletions(-) diff --git a/openmc/config.py b/openmc/config.py index d61fb2d31..e75973495 100644 --- a/openmc/config.py +++ b/openmc/config.py @@ -193,12 +193,12 @@ def _default_config() -> _Config: """ config = _Config() - if "OPENMC_CROSS_SECTIONS" in os.environ: - config['cross_sections'] = os.environ["OPENMC_CROSS_SECTIONS"] - if "OPENMC_MG_CROSS_SECTIONS" in os.environ: - config['mg_cross_sections'] = os.environ["OPENMC_MG_CROSS_SECTIONS"] - chain_file = os.environ.get("OPENMC_CHAIN_FILE") - xs_path = config.get('cross_sections') + for key,var in _Config._PATH_KEYS.items(): + if var in os.environ: + config[key] = os.environ[var] + + chain_file = config.get("chain_file") + xs_path = config.get("cross_sections") if chain_file is None and xs_path is not None and xs_path.exists(): try: data = DataLibrary.from_xml(xs_path) @@ -209,10 +209,8 @@ def _default_config() -> _Config: else: for lib in reversed(data.libraries): if lib['type'] == 'depletion_chain': - chain_file = xs_path.parent / lib['path'] + config['chain_file'] = xs_path.parent / lib['path'] break - if chain_file is not None: - config['chain_file'] = chain_file return config diff --git a/tests/conftest.py b/tests/conftest.py index cd86da539..fa6718502 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,4 @@ +import os import pytest import openmc @@ -28,7 +29,10 @@ def run_in_tmpdir(tmpdir): yield finally: orig.chdir() - + +@pytest.fixture(scope="module") +def endf_data(): + return os.environ['OPENMC_ENDF_DATA'] @pytest.fixture(scope='session', autouse=True) def resolve_paths(): diff --git a/tests/unit_tests/test_data_decay.py b/tests/unit_tests/test_data_decay.py index 8900e7eac..de8d90a43 100644 --- a/tests/unit_tests/test_data_decay.py +++ b/tests/unit_tests/test_data_decay.py @@ -17,25 +17,22 @@ def ufloat_close(a, b): @pytest.fixture(scope='module') -def nb90(): +def nb90(endf_data): """Nb90 decay data.""" - endf_data = os.environ['OPENMC_ENDF_DATA'] filename = os.path.join(endf_data, 'decay', 'dec-041_Nb_090.endf') return openmc.data.Decay.from_endf(filename) @pytest.fixture(scope='module') -def ba137m(): +def ba137m(endf_data): """Ba137_m1 decay data.""" - endf_data = os.environ['OPENMC_ENDF_DATA'] filename = os.path.join(endf_data, 'decay', 'dec-056_Ba_137m1.endf') return openmc.data.Decay.from_endf(filename) @pytest.fixture(scope='module') -def u235_yields(): +def u235_yields(endf_data): """U235 fission product yield data.""" - endf_data = os.environ['OPENMC_ENDF_DATA'] filename = os.path.join(endf_data, 'nfy', 'nfy-092_U_235.endf') return openmc.data.FissionProductYields.from_endf(filename) diff --git a/tests/unit_tests/test_data_kalbach_mann.py b/tests/unit_tests/test_data_kalbach_mann.py index 3b837af7d..5d06669f7 100644 --- a/tests/unit_tests/test_data_kalbach_mann.py +++ b/tests/unit_tests/test_data_kalbach_mann.py @@ -7,6 +7,7 @@ import pytest import numpy as np +import openmc from openmc.data import IncidentNeutron from openmc.data.kalbach_mann import _separation_energy, _AtomicRepresentation from openmc.data import kalbach_slope @@ -129,7 +130,7 @@ def test_kalbach_slope(): ('Hg204.h5', 'n-080_Hg_204.endf') ] ) -def test_comparison_slope_hdf5(hdf5_filename, endf_filename): +def test_comparison_slope_hdf5(hdf5_filename, endf_filename, endf_data): """Test the calculation of the Kalbach-Mann slope done by OpenMC by comparing it to HDF5 data. The test is based on the first product of MT=5 (neutron). The isotopes tested have been selected because the @@ -147,13 +148,13 @@ def test_comparison_slope_hdf5(hdf5_filename, endf_filename): """ # HDF5 data - hdf5_directory = Path(os.environ['OPENMC_CROSS_SECTIONS']).parent + hdf5_directory = Path(openmc.config.get('cross_sections')).parent hdf5_data = IncidentNeutron.from_hdf5(hdf5_directory / hdf5_filename) hdf5_product = hdf5_data[5].products[0] hdf5_distribution = hdf5_product.distribution[0] # ENDF data - endf_directory = Path(os.environ['OPENMC_ENDF_DATA']) + endf_directory = Path(endf_data) endf_path = endf_directory / 'neutrons' / endf_filename endf_data = IncidentNeutron.from_endf(endf_path) endf_product = endf_data[5].products[0] diff --git a/tests/unit_tests/test_data_misc.py b/tests/unit_tests/test_data_misc.py index b65c704af..14db68913 100644 --- a/tests/unit_tests/test_data_misc.py +++ b/tests/unit_tests/test_data_misc.py @@ -30,7 +30,7 @@ def test_data_library(tmpdir): assert os.path.exists(filename) new_lib = openmc.data.DataLibrary() - directory = os.path.dirname(os.environ['OPENMC_CROSS_SECTIONS']) + directory = os.path.dirname(openmc.config.get('cross_sections')) new_lib.register_file(os.path.join(directory, 'H1.h5')) assert new_lib[-1]['type'] == 'neutron' new_lib.register_file(os.path.join(directory, 'c_Zr_in_ZrH.h5')) diff --git a/tests/unit_tests/test_data_multipole.py b/tests/unit_tests/test_data_multipole.py index bc7136a97..105099bb9 100644 --- a/tests/unit_tests/test_data_multipole.py +++ b/tests/unit_tests/test_data_multipole.py @@ -8,14 +8,14 @@ import openmc.data @pytest.fixture(scope='module') def u235(): - directory = pathlib.Path(os.environ['OPENMC_CROSS_SECTIONS']).parent + directory = pathlib.Path(openmc.config.get('cross_sections')).parent u235 = directory / 'wmp' / '092235.h5' return openmc.data.WindowedMultipole.from_hdf5(u235) @pytest.fixture(scope='module') def b10(): - directory = pathlib.Path(os.environ['OPENMC_CROSS_SECTIONS']).parent + directory = pathlib.Path(openmc.config.get('cross_sections')).parent b10 = directory / 'wmp' / '005010.h5' return openmc.data.WindowedMultipole.from_hdf5(b10) @@ -48,17 +48,15 @@ def test_export_to_hdf5(tmpdir, u235): assert os.path.exists(filename) -def test_from_endf(): +def test_from_endf(endf_data): pytest.importorskip('vectfit') - endf_data = os.environ['OPENMC_ENDF_DATA'] endf_file = os.path.join(endf_data, 'neutrons', 'n-001_H_001.endf') assert openmc.data.WindowedMultipole.from_endf( endf_file, log=True, wmp_options={"n_win": 400, "n_cf": 3}) -def test_from_endf_search(): +def test_from_endf_search(endf_data): pytest.importorskip('vectfit') - endf_data = os.environ['OPENMC_ENDF_DATA'] endf_file = os.path.join(endf_data, 'neutrons', 'n-095_Am_244.endf') assert openmc.data.WindowedMultipole.from_endf( endf_file, log=True, wmp_options={"search": True, 'rtol':1e-2}) diff --git a/tests/unit_tests/test_data_neutron.py b/tests/unit_tests/test_data_neutron.py index db6ae1eb8..d43d93ae5 100644 --- a/tests/unit_tests/test_data_neutron.py +++ b/tests/unit_tests/test_data_neutron.py @@ -14,128 +14,113 @@ _TEMPERATURES = [300., 600., 900.] @pytest.fixture(scope='module') def pu239(): """Pu239 HDF5 data.""" - directory = os.path.dirname(os.environ['OPENMC_CROSS_SECTIONS']) + directory = os.path.dirname(openmc.config.get('cross_sections')) filename = os.path.join(directory, 'Pu239.h5') return openmc.data.IncidentNeutron.from_hdf5(filename) @pytest.fixture(scope='module') -def xe135(): +def xe135(endf_data): """Xe135 ENDF data (contains SLBW resonance range)""" - endf_data = os.environ['OPENMC_ENDF_DATA'] filename = os.path.join(endf_data, 'neutrons', 'n-054_Xe_135.endf') return openmc.data.IncidentNeutron.from_endf(filename) @pytest.fixture(scope='module') -def sm150(): +def sm150(endf_data): """Sm150 ENDF data (contains MLBW resonance range)""" - endf_data = os.environ['OPENMC_ENDF_DATA'] filename = os.path.join(endf_data, 'neutrons', 'n-062_Sm_150.endf') return openmc.data.IncidentNeutron.from_endf(filename) @pytest.fixture(scope='module') -def gd154(): +def gd154(endf_data): """Gd154 ENDF data (contains Reich Moore resonance range and reosnance covariance with LCOMP=1).""" - endf_data = os.environ['OPENMC_ENDF_DATA'] filename = os.path.join(endf_data, 'neutrons', 'n-064_Gd_154.endf') return openmc.data.IncidentNeutron.from_endf(filename, covariance=True) @pytest.fixture(scope='module') -def cl35(): +def cl35(endf_data): """Cl35 ENDF data (contains RML resonance range)""" - endf_data = os.environ['OPENMC_ENDF_DATA'] filename = os.path.join(endf_data, 'neutrons', 'n-017_Cl_035.endf') return openmc.data.IncidentNeutron.from_endf(filename) @pytest.fixture(scope='module') -def am241(): +def am241(endf_data): """Am241 ENDF data (contains Madland-Nix fission energy distribution).""" - endf_data = os.environ['OPENMC_ENDF_DATA'] filename = os.path.join(endf_data, 'neutrons', 'n-095_Am_241.endf') return openmc.data.IncidentNeutron.from_endf(filename) @pytest.fixture(scope='module') -def u233(): +def u233(endf_data): """U233 ENDF data (contains Watt fission energy distribution).""" - endf_data = os.environ['OPENMC_ENDF_DATA'] filename = os.path.join(endf_data, 'neutrons', 'n-092_U_233.endf') return openmc.data.IncidentNeutron.from_endf(filename) @pytest.fixture(scope='module') -def u236(): +def u236(endf_data): """U236 ENDF data (contains Watt fission energy distribution).""" - endf_data = os.environ['OPENMC_ENDF_DATA'] filename = os.path.join(endf_data, 'neutrons', 'n-092_U_236.endf') return openmc.data.IncidentNeutron.from_endf(filename) @pytest.fixture(scope='module') -def na22(): +def na22(endf_data): """Na22 ENDF data (contains evaporation spectrum).""" - endf_data = os.environ['OPENMC_ENDF_DATA'] filename = os.path.join(endf_data, 'neutrons', 'n-011_Na_022.endf') return openmc.data.IncidentNeutron.from_endf(filename) @pytest.fixture(scope='module') -def na23(): +def na23(endf_data): """Na23 ENDF data (contains MLBW resonance covariance with LCOMP=0).""" - endf_data = os.environ['OPENMC_ENDF_DATA'] filename = os.path.join(endf_data, 'neutrons', 'n-011_Na_023.endf') return openmc.data.IncidentNeutron.from_endf(filename, covariance=True) @pytest.fixture(scope='module') -def be9(): +def be9(endf_data): """Be9 ENDF data (contains laboratory angle-energy distribution).""" - endf_data = os.environ['OPENMC_ENDF_DATA'] filename = os.path.join(endf_data, 'neutrons', 'n-004_Be_009.endf') return openmc.data.IncidentNeutron.from_endf(filename) @pytest.fixture(scope='module') -def h2(): - endf_data = os.environ['OPENMC_ENDF_DATA'] +def h2(endf_data): endf_file = os.path.join(endf_data, 'neutrons', 'n-001_H_002.endf') return openmc.data.IncidentNeutron.from_njoy( endf_file, temperatures=_TEMPERATURES) @pytest.fixture(scope='module') -def am244(): - endf_data = os.environ['OPENMC_ENDF_DATA'] +def am244(endf_data): endf_file = os.path.join(endf_data, 'neutrons', 'n-095_Am_244.endf') return openmc.data.IncidentNeutron.from_njoy(endf_file) @pytest.fixture(scope='module') -def ti50(): +def ti50(endf_data): """Ti50 ENDF data (contains Multi-level Breit-Wigner resonance range and resonance covariance with LCOMP=1).""" - endf_data = os.environ['OPENMC_ENDF_DATA'] filename = os.path.join(endf_data, 'neutrons', 'n-022_Ti_050.endf') return openmc.data.IncidentNeutron.from_endf(filename, covariance=True) @pytest.fixture(scope='module') -def cf252(): +def cf252(endf_data): """Cf252 ENDF data (contains RM resonance covariance with LCOMP=0).""" - endf_data = os.environ['OPENMC_ENDF_DATA'] filename = os.path.join(endf_data, 'neutrons', 'n-098_Cf_252.endf') return openmc.data.IncidentNeutron.from_endf(filename, covariance=True) @pytest.fixture(scope='module') -def th232(): +def th232(endf_data): """Th232 ENDF data (contains RM resonance covariance with LCOMP=2).""" - endf_data = os.environ['OPENMC_ENDF_DATA'] filename = os.path.join(endf_data, 'neutrons', 'n-090_Th_232.endf') return openmc.data.IncidentNeutron.from_endf(filename, covariance=True) @@ -453,8 +438,7 @@ def test_laboratory(be9): @needs_njoy -def test_correlated(tmpdir): - endf_data = os.environ['OPENMC_ENDF_DATA'] +def test_correlated(tmpdir, endf_data): endf_file = os.path.join(endf_data, 'neutrons', 'n-014_Si_030.endf') si30 = openmc.data.IncidentNeutron.from_njoy(endf_file, heatr=False) @@ -480,8 +464,7 @@ def test_nbody(tmpdir, h2): @needs_njoy -def test_ace_convert(run_in_tmpdir): - endf_data = os.environ['OPENMC_ENDF_DATA'] +def test_ace_convert(run_in_tmpdir, endf_data): filename = os.path.join(endf_data, 'neutrons', 'n-001_H_001.endf') ace_ascii = 'ace_ascii' ace_binary = 'ace_binary' @@ -515,8 +498,7 @@ def test_ace_table_types(): @needs_njoy -def test_high_temperature(): - endf_data = os.environ['OPENMC_ENDF_DATA'] +def test_high_temperature(endf_data): endf_file = os.path.join(endf_data, 'neutrons', 'n-001_H_001.endf') # Ensure that from_njoy works when given a high temperature diff --git a/tests/unit_tests/test_data_photon.py b/tests/unit_tests/test_data_photon.py index 010ac1f35..98b180f52 100644 --- a/tests/unit_tests/test_data_photon.py +++ b/tests/unit_tests/test_data_photon.py @@ -9,9 +9,8 @@ import openmc.data @pytest.fixture(scope='module') -def elements_endf(): +def elements_endf(endf_data): """Dictionary of element ENDF data indexed by atomic symbol.""" - endf_data = os.environ['OPENMC_ENDF_DATA'] elements = {'H': 1, 'O': 8, 'Al': 13, 'Cu': 29, 'Ag': 47, 'U': 92, 'Pu': 94} data = {} for symbol, Z in elements.items(): @@ -145,8 +144,8 @@ def test_export_to_hdf5(tmpdir, element): element2.export_to_hdf5(filename, 'w') -def test_photodat_only(run_in_tmpdir): - endf_dir = Path(os.environ['OPENMC_ENDF_DATA']) +def test_photodat_only(run_in_tmpdir, endf_data): + endf_dir = Path(endf_data) photoatomic_file = endf_dir / 'photoat' / 'photoat-001_H_000.endf' data = openmc.data.IncidentPhoton.from_endf(photoatomic_file) data.export_to_hdf5('tmp.h5', 'w') diff --git a/tests/unit_tests/test_data_thermal.py b/tests/unit_tests/test_data_thermal.py index c9fd9045b..c444d0c58 100644 --- a/tests/unit_tests/test_data_thermal.py +++ b/tests/unit_tests/test_data_thermal.py @@ -13,7 +13,7 @@ from . import needs_njoy @pytest.fixture(scope='module') def h2o(): """H in H2O thermal scattering data.""" - directory = os.path.dirname(os.environ['OPENMC_CROSS_SECTIONS']) + directory = os.path.dirname(openmc.config.get('cross_sections')) filename = os.path.join(directory, 'c_H_in_H2O.h5') return openmc.data.ThermalScattering.from_hdf5(filename) @@ -21,15 +21,14 @@ def h2o(): @pytest.fixture(scope='module') def graphite(): """Graphite thermal scattering data.""" - directory = os.path.dirname(os.environ['OPENMC_CROSS_SECTIONS']) + directory = os.path.dirname(openmc.config.get('cross_sections')) filename = os.path.join(directory, 'c_Graphite.h5') return openmc.data.ThermalScattering.from_hdf5(filename) @pytest.fixture(scope='module') -def h2o_njoy(): +def h2o_njoy(endf_data): """H in H2O generated using NJOY.""" - endf_data = os.environ['OPENMC_ENDF_DATA'] path_h1 = os.path.join(endf_data, 'neutrons', 'n-001_H_001.endf') path_h2o = os.path.join(endf_data, 'thermal_scatt', 'tsl-HinH2O.endf') return openmc.data.ThermalScattering.from_njoy( @@ -37,17 +36,15 @@ def h2o_njoy(): @pytest.fixture(scope='module') -def hzrh(): +def hzrh(endf_data): """H in ZrH thermal scattering data.""" - endf_data = os.environ['OPENMC_ENDF_DATA'] filename = os.path.join(endf_data, 'thermal_scatt', 'tsl-HinZrH.endf') return openmc.data.ThermalScattering.from_endf(filename, divide_incoherent_elastic=True) @pytest.fixture(scope='module') -def hzrh_njoy(): +def hzrh_njoy(endf_data): """H in ZrH generated using NJOY.""" - endf_data = os.environ['OPENMC_ENDF_DATA'] path_h1 = os.path.join(endf_data, 'neutrons', 'n-001_H_001.endf') path_hzrh = os.path.join(endf_data, 'thermal_scatt', 'tsl-HinZrH.endf') with_endf_data = openmc.data.ThermalScattering.from_njoy( @@ -60,9 +57,8 @@ def hzrh_njoy(): @pytest.fixture(scope='module') -def sio2(): +def sio2(endf_data): """SiO2 thermal scattering data.""" - endf_data = os.environ['OPENMC_ENDF_DATA'] filename = os.path.join(endf_data, 'thermal_scatt', 'tsl-SiO2.endf') return openmc.data.ThermalScattering.from_endf(filename, divide_incoherent_elastic=True) @@ -102,8 +98,7 @@ def test_graphite_xs(graphite): assert elastic([1e-3, 1.0]) == pytest.approx([0.0, 0.62586153]) @needs_njoy -def test_graphite_njoy(): - endf_data = os.environ['OPENMC_ENDF_DATA'] +def test_graphite_njoy(endf_data): path_c0 = os.path.join(endf_data, 'neutrons', 'n-006_C_000.endf') path_gr = os.path.join(endf_data, 'thermal_scatt', 'tsl-graphite.endf') graphite = openmc.data.ThermalScattering.from_njoy( @@ -141,8 +136,7 @@ def test_continuous_dist(h2o_njoy): assert isinstance(dist, openmc.data.IncoherentInelasticAE) -def test_h2o_endf(): - endf_data = os.environ['OPENMC_ENDF_DATA'] +def test_h2o_endf(endf_data): filename = os.path.join(endf_data, 'thermal_scatt', 'tsl-HinH2O.endf') h2o = openmc.data.ThermalScattering.from_endf(filename, divide_incoherent_elastic=True) assert not h2o.elastic diff --git a/tests/unit_tests/test_deplete_chain.py b/tests/unit_tests/test_deplete_chain.py index bd5cc2263..e90b61022 100644 --- a/tests/unit_tests/test_deplete_chain.py +++ b/tests/unit_tests/test_deplete_chain.py @@ -54,11 +54,11 @@ def simple_chain(): @pytest.fixture(scope='module') -def endf_chain(): - endf_data = Path(os.environ['OPENMC_ENDF_DATA']) - decay_data = (endf_data / 'decay').glob('*.endf') - fpy_data = (endf_data / 'nfy').glob('*.endf') - neutron_data = (endf_data / 'neutrons').glob('*.endf') +def endf_chain(endf_data): + endf_dir = Path(endf_data) + decay_data = (endf_dir / 'decay').glob('*.endf') + fpy_data = (endf_dir / 'nfy').glob('*.endf') + neutron_data = (endf_dir / 'neutrons').glob('*.endf') return Chain.from_endf(decay_data, fpy_data, neutron_data) From ec15803d4134e7e72384801ce08b03b45608d6ba Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Sat, 30 Aug 2025 03:39:45 +0200 Subject: [PATCH 401/671] adding ecco 33 (#3556) --- openmc/mgxs/__init__.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/openmc/mgxs/__init__.py b/openmc/mgxs/__init__.py index f8de84b37..5de85afb0 100644 --- a/openmc/mgxs/__init__.py +++ b/openmc/mgxs/__init__.py @@ -18,6 +18,8 @@ GROUP_STRUCTURES = {} ([ZAL1999]_ and [REARDEN2013]_) - "MPACT-X" (where X is 51 (PWR), 60 (BWR), 69 (Magnox)) from the MPACT_ reactor physics code ([KIM2019]_ and [KIM2020]_) +- "ECCO-33" intended for fast reactor criticality benchmarks. It’s derived as a + subset of VITAMIN‑J - "ECCO-1968_" designed for fine group reactor cell calculations for fast, intermediate and thermal reactor applications ([SAR1990]_) - activation_ energy group structures "VITAMIN-J-42", "VITAMIN-J-175", @@ -25,6 +27,7 @@ GROUP_STRUCTURES = {} .. _CASMO: http://large.stanford.edu/courses/2013/ph241/dalvi1/docs/c5.physor2006.pdf .. _SCALE44: https://www-nds.iaea.org/publications/indc/indc-czr-0001.pdf +.. _ECCO-33: https://serpent.vtt.fi/mediawiki/index.php/ECCO_33-group_structure .. _SCALE252: https://oecd-nea.org/science/wpncs/amct/workingarea/meeting2013/EGAMCT2013_08.pdf .. _MPACT: https://vera.ornl.gov/mpact/ .. _XMAS-172: https://www-nds.iaea.org/wimsd/energy.htm @@ -81,6 +84,16 @@ GROUP_STRUCTURES['CASMO-25'] = np.array([ 0., 3.e-2, 5.8e-2, 1.4e-1, 2.8e-1, 3.5e-1, 6.25e-1, 9.72e-1, 1.02, 1.097, 1.15, 1.855, 4., 9.877, 1.5968e1, 1.4873e2, 5.53e3, 9.118e3, 1.11e5, 5.e5, 8.21e5, 1.353e6, 2.231e6, 3.679e6, 6.0655e6, 2.e7]) +GROUP_STRUCTURES['ECCO-33'] = np.array([ + 1.0000100000E-05, 1.0000000000E-01, 5.4000000000E-01, 4.0000000000E+00, + 8.3152870000E+00, 1.3709590000E+01, 2.2603290000E+01, 4.0169000000E+01, + 6.7904050000E+01, 9.1660880000E+01, 1.4862540000E+02, 3.0432480000E+02, + 4.5399930000E+02, 7.4851830000E+02, 1.2340980000E+03, 2.0346840000E+03, + 3.3546260000E+03, 5.5308440000E+03, 9.1188200000E+03, 1.5034390000E+04, + 2.4787520000E+04, 4.0867710000E+04, 6.7379470000E+04, 1.1109000000E+05, + 1.8315640000E+05, 3.0197380000E+05, 4.9787070000E+05, 8.2085000000E+05, + 1.3533530000E+06, 2.2313020000E+06, 3.6787940000E+06, 6.0653070000E+06, + 1.0000000000E+07, 1.9640330000E+07]) GROUP_STRUCTURES['CASMO-40'] = np.array([ 0., 1.5e-2, 3.e-2, 4.2e-2, 5.8e-2, 8.e-2, 1.e-1, 1.4e-1, 1.8e-1, 2.2e-1, 2.8e-1, 3.5e-1, 6.25e-1, 8.5e-1, 9.5e-1, From 00edc77691c60411cf00431fdc6788f8fceb886a Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Sat, 30 Aug 2025 05:39:46 +0200 Subject: [PATCH 402/671] Change test order to run unit tests first. (#3533) Co-authored-by: Paul Romano --- openmc/cell.py | 6 +++--- openmc/config.py | 6 +++--- tests/unit_tests/test_config.py | 5 +++-- tools/ci/gha-script.sh | 4 ++-- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/openmc/cell.py b/openmc/cell.py index 672afe095..88aaebc8f 100644 --- a/openmc/cell.py +++ b/openmc/cell.py @@ -567,10 +567,10 @@ class Cell(IDManagerMixin): .. versionadded:: 0.14.0 """ # Create dummy universe but preserve used_ids - next_id = openmc.Universe.next_id + next_id = openmc.UniverseBase.next_id u = openmc.Universe(cells=[self]) - openmc.Universe.used_ids.remove(u.id) - openmc.Universe.next_id = next_id + openmc.UniverseBase.used_ids.remove(u.id) + openmc.UniverseBase.next_id = next_id return u.plot(*args, **kwargs) def create_xml_subelement(self, xml_element, memo=None): diff --git a/openmc/config.py b/openmc/config.py index e75973495..23d8e23a7 100644 --- a/openmc/config.py +++ b/openmc/config.py @@ -178,7 +178,7 @@ class _Config(MutableMapping): self[key] = previous_value -def _default_config() -> _Config: +def _default_config(**kwargs) -> _Config: """Create a configuration initialized from environment variables. This function checks for OPENMC_CROSS_SECTIONS, OPENMC_MG_CROSS_SECTIONS, @@ -192,11 +192,11 @@ def _default_config() -> _Config: A new configuration object. """ - config = _Config() + config = _Config(kwargs) for key,var in _Config._PATH_KEYS.items(): if var in os.environ: config[key] = os.environ[var] - + chain_file = config.get("chain_file") xs_path = config.get("cross_sections") if chain_file is None and xs_path is not None and xs_path.exists(): diff --git a/tests/unit_tests/test_config.py b/tests/unit_tests/test_config.py index 242c59b5e..45e6a7e5a 100644 --- a/tests/unit_tests/test_config.py +++ b/tests/unit_tests/test_config.py @@ -13,6 +13,7 @@ def reset_config_and_env(): """A fixture to ensure each test has a clean config, env, and CWD.""" original_env = dict(os.environ) original_cwd = os.getcwd() + original_resolve_paths = openmc.config["resolve_paths"] # Reset environment variables that affect config for key in ['OPENMC_CROSS_SECTIONS', 'OPENMC_MG_CROSS_SECTIONS', 'OPENMC_CHAIN_FILE']: @@ -25,13 +26,13 @@ def reset_config_and_env(): try: yield finally: - # Restore environment and CWD + # Restore environment, CWD and resolve_paths os.environ.clear() os.environ.update(original_env) os.chdir(original_cwd) # Restore config one last time for safety between modules - openmc.config = _default_config() + openmc.config = _default_config(resolve_paths=original_resolve_paths) def test_config_basics(): diff --git a/tools/ci/gha-script.sh b/tools/ci/gha-script.sh index c7c634ffa..c0f754c32 100755 --- a/tools/ci/gha-script.sh +++ b/tools/ci/gha-script.sh @@ -14,5 +14,5 @@ if [[ $EVENT == 'y' ]]; then args="${args} --event " fi -# Run regression and unit tests -pytest --cov=openmc -v $args tests +# Run unit tests and then regression tests +pytest --cov=openmc -v $args tests/unit_tests tests/regression_tests From eaed4009873e6dc28e0aa0794209be2857552163 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Wed, 3 Sep 2025 05:52:27 +0200 Subject: [PATCH 403/671] Fixed a bug in plotting cross sections with S(a,b) data (#3558) --- openmc/plotter.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openmc/plotter.py b/openmc/plotter.py index 85c4963a7..693cdaca4 100644 --- a/openmc/plotter.py +++ b/openmc/plotter.py @@ -413,7 +413,8 @@ def _calculate_cexs_nuclide(this, types, temperature=294., sab_name=None, # Prep S(a,b) data if needed if sab_name: - sab = openmc.data.ThermalScattering.from_hdf5(sab_name) + sab = openmc.data.ThermalScattering.from_hdf5( + library.get_by_material(sab_name, data_type='thermal')['path']) # Obtain the nearest temperature if strT in sab.temperatures: sabT = strT @@ -640,14 +641,13 @@ def _calculate_cexs_elem_mat(this, types, temperature=294., sab = openmc.data.ThermalScattering.from_hdf5( library.get_by_material(sab_name, data_type='thermal')['path']) for nuc in sab.nuclides: - sabs[nuc] = library.get_by_material(sab_name, - data_type='thermal')['path'] + sabs[nuc] = sab_name else: if sab_name: - sab = openmc.data.ThermalScattering.from_hdf5(sab_name) + sab = openmc.data.ThermalScattering.from_hdf5( + library.get_by_material(sab_name, data_type='thermal')['path']) for nuc in sab.nuclides: - sabs[nuc] = library.get_by_material(sab_name, - data_type='thermal')['path'] + sabs[nuc] = sab_name # Now we can create the data sets to be plotted xs = {} @@ -655,8 +655,8 @@ def _calculate_cexs_elem_mat(this, types, temperature=294., for nuclide in nuclides.items(): name = nuclide[0] nuc = nuclide[1] - sab_tab = sabs[name] - temp_E, temp_xs = calculate_cexs(nuc, types, T, sab_tab, cross_sections, + sab_name = sabs[name] + temp_E, temp_xs = calculate_cexs(nuc, types, T, sab_name, cross_sections, ncrystal_cfg=ncrystal_cfg ) E.append(temp_E) From 5918564727a34ac7bb071ceafe1a7bf4d50542d7 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 3 Sep 2025 11:35:11 -0500 Subject: [PATCH 404/671] Bump up tolerance for flaky activation test (#3560) --- tests/unit_tests/test_deplete_activation.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/unit_tests/test_deplete_activation.py b/tests/unit_tests/test_deplete_activation.py index 6afea8c9f..cbe00d680 100644 --- a/tests/unit_tests/test_deplete_activation.py +++ b/tests/unit_tests/test_deplete_activation.py @@ -45,7 +45,7 @@ ENERGIES = np.logspace(log10(1e-5), log10(2e7), 100) @pytest.mark.parametrize("reaction_rate_mode,reaction_rate_opts,tolerance", [ ("direct", {}, 1e-5), - ("flux", {'energies': ENERGIES}, 0.01), + ("flux", {'energies': ENERGIES}, 0.1), ("flux", {'energies': ENERGIES, 'reactions': ['(n,gamma)']}, 1e-5), ("flux", {'energies': ENERGIES, 'reactions': ['(n,gamma)'], 'nuclides': ['W186', 'H3']}, 1e-2), ]) @@ -61,11 +61,10 @@ def test_activation(run_in_tmpdir, model, reaction_rate_mode, reaction_rate_opts w186 = openmc.deplete.Nuclide('W186') w186.add_reaction('(n,gamma)', None, 0.0, 1.0) chain.add_nuclide(w186) - chain.export_to_xml('test_chain.xml') # Create transport operator op = openmc.deplete.CoupledOperator( - model, 'test_chain.xml', + model, chain, normalization_mode="source-rate", reaction_rate_mode=reaction_rate_mode, reaction_rate_opts=reaction_rate_opts, From 366509051381db89a3a6a7b3346e3b339e8af0af Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Fri, 5 Sep 2025 06:34:49 -0500 Subject: [PATCH 405/671] Do not apply boundary conditions when initialized in volume calculation mode (#3562) --- src/particle.cpp | 3 ++- tests/unit_tests/test_mesh.py | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/particle.cpp b/src/particle.cpp index fb41d82e9..fd4df14c3 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -544,7 +544,8 @@ void Particle::cross_surface(const Surface& surf) #endif // Handle any applicable boundary conditions. - if (surf.bc_ && settings::run_mode != RunMode::PLOTTING) { + if (surf.bc_ && settings::run_mode != RunMode::PLOTTING && + settings::run_mode != RunMode::VOLUME) { surf.bc_->handle_particle(*this, surf); return; } diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index 67ca4028e..1930e3264 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -620,6 +620,33 @@ def test_mesh_material_volumes_serialize(): assert new_volumes.by_element(3) == [(2, 1.0)] +def test_mesh_material_volumes_boundary_conditions(sphere_model): + """Test the material volumes method using a regular mesh + that overlaps with a vacuum boundary condition.""" + + mesh = openmc.SphericalMesh.from_domain(sphere_model.geometry, dimension=(1, 1, 1)) + # extend mesh beyond the outer sphere surface to test rays crossing the boundary condition + mesh.r_grid[-1] += 5.0 + + # add a new cell to the modelthat occupies the outside of the sphere + sphere_surfaces = list(filter(lambda s: isinstance(s, openmc.Sphere), + sphere_model.geometry.get_all_surfaces().values())) + outer_cell = openmc.Cell(region=+sphere_surfaces[0]) + sphere_model.geometry.root_universe.add_cell(outer_cell) + + volumes = mesh.material_volumes(sphere_model, (0, 100, 100)) + sphere_volume = 4/3*np.pi*25**3 + mats = sphere_model.materials + expected_volumes = [(mats[0].id, 0.25*sphere_volume), + (mats[1].id, 0.25*sphere_volume), + (mats[2].id, 0.5*sphere_volume), + (None, 4/3*np.pi*mesh.r_grid[-1]**3 - sphere_volume)] + + for evaluated, expected in zip(volumes.by_element(0), expected_volumes): + assert evaluated[0] == expected[0] + assert evaluated[1] == pytest.approx(expected[1], rel=1e-2) + + def test_raytrace_mesh_infinite_loop(run_in_tmpdir): # Create a model with one large spherical cell sphere = openmc.Sphere(r=100, boundary_type='vacuum') From ca4295748d1b547ea3e5d9c8fb1ec55b64809289 Mon Sep 17 00:00:00 2001 From: Robert Carlsen Date: Wed, 10 Sep 2025 20:13:28 -0600 Subject: [PATCH 406/671] depletion: fix performance of chain matrix construction (#3567) Co-authored-by: GuySten <62616591+GuySten@users.noreply.github.com> --- openmc/deplete/chain.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index f1a23317f..ac5c02aa5 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -627,9 +627,15 @@ class Chain: """ reactions = set() - # Use DOK matrix as intermediate representation for matrix n = len(self) - matrix = sp.dok_matrix((n, n)) + + # we accumulate indices and value entries for everything and create the matrix + # in one step at the end to avoid expensive index checks scipy otherwise does. + rows, cols, vals = [], [], [] + def setval(i, j, val): + rows.append(i) + cols.append(j) + vals.append(val) if fission_yields is None: fission_yields = self.get_default_fission_yields() @@ -639,7 +645,7 @@ class Chain: if nuc.half_life is not None: decay_constant = math.log(2) / nuc.half_life if decay_constant != 0.0: - matrix[i, i] -= decay_constant + setval(i, i, -decay_constant) # Gain from radioactive decay if nuc.n_decay_modes != 0: @@ -650,19 +656,19 @@ class Chain: if branch_val != 0.0: if target is not None: k = self.nuclide_dict[target] - matrix[k, i] += branch_val + setval(k, i, branch_val) # Produce alphas and protons from decay if 'alpha' in decay_type: k = self.nuclide_dict.get('He4') if k is not None: count = decay_type.count('alpha') - matrix[k, i] += count * branch_val + setval(k, i, count * branch_val) elif 'p' in decay_type: k = self.nuclide_dict.get('H1') if k is not None: count = decay_type.count('p') - matrix[k, i] += count * branch_val + setval(k, i, count * branch_val) if nuc.name in rates.index_nuc: # Extract all reactions for this nuclide in this cell @@ -679,13 +685,13 @@ class Chain: if r_type not in reactions: reactions.add(r_type) if path_rate != 0.0: - matrix[i, i] -= path_rate + setval(i, i, -path_rate) # Gain term; allow for total annihilation for debug purposes if r_type != 'fission': if target is not None and path_rate != 0.0: k = self.nuclide_dict[target] - matrix[k, i] += path_rate * br + setval(k, i, path_rate * br) # Determine light nuclide production, e.g., (n,d) should # produce H2 @@ -693,20 +699,20 @@ class Chain: for light_nuc in light_nucs: k = self.nuclide_dict.get(light_nuc) if k is not None: - matrix[k, i] += path_rate * br + setval(k, i, path_rate * br) else: for product, y in fission_yields[nuc.name].items(): yield_val = y * path_rate if yield_val != 0.0: k = self.nuclide_dict[product] - matrix[k, i] += yield_val + setval(k, i, yield_val) # Clear set of reactions reactions.clear() # Return CSC representation instead of DOK - return matrix.tocsc() + return sp.csc_matrix((vals, (rows, cols)), shape=(n, n)) def form_rr_term(self, tr_rates, current_timestep, mats): """Function to form the transfer rate term matrices. From c7175289eb0f801b38b7b684a5ddd4803261f66a Mon Sep 17 00:00:00 2001 From: Jack Fletcher <115663563+j-fletcher@users.noreply.github.com> Date: Thu, 11 Sep 2025 08:57:02 -0400 Subject: [PATCH 407/671] PowerLaw raises an error if sampling interval contains negative values (#3542) --- openmc/stats/univariate.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index 28d5e87ef..d6cf19f2b 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -480,6 +480,9 @@ class PowerLaw(Univariate): """ def __init__(self, a: float = 0.0, b: float = 1.0, n: float = 0.): + if a >= b: + raise ValueError( + "Lower bound of sampling interval must be less than upper bound.") self.a = a self.b = b self.n = n @@ -494,6 +497,9 @@ class PowerLaw(Univariate): @a.setter def a(self, a): cv.check_type('interval lower bound', a, Real) + if a < 0: + raise ValueError( + "PowerLaw sampling is restricted to positive-valued intervals.") self._a = a @property @@ -503,6 +509,9 @@ class PowerLaw(Univariate): @b.setter def b(self, b): cv.check_type('interval upper bound', b, Real) + if b < 0: + raise ValueError( + "PowerLaw sampling is restricted to positive-valued intervals.") self._b = b @property From afd9d0607445f00c6de97b1ff866521c319722b0 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Fri, 12 Sep 2025 22:23:17 +0200 Subject: [PATCH 408/671] Fixed a bug when combining TimeFilter, MeshFilter, and tracklength estimator (#3525) Co-authored-by: Paul Romano --- include/openmc/tallies/tally.h | 10 ++++ include/openmc/tallies/tally_scoring.h | 10 ++++ src/particle.cpp | 5 ++ src/tallies/tally.cpp | 80 ++++++++++++++++++++----- src/tallies/tally_scoring.cpp | 59 ++++++++++++++++-- tests/unit_tests/test_mesh.py | 83 +++++++++++++++++++++++++- 6 files changed, 227 insertions(+), 20 deletions(-) diff --git a/include/openmc/tallies/tally.h b/include/openmc/tallies/tally.h index 8c088c460..3beeb9d5a 100644 --- a/include/openmc/tallies/tally.h +++ b/include/openmc/tallies/tally.h @@ -203,11 +203,14 @@ extern vector> tallies; extern vector active_tallies; extern vector active_analog_tallies; extern vector active_tracklength_tallies; +extern vector active_timed_tracklength_tallies; 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 time_grid; + } // namespace model namespace simulation { @@ -239,6 +242,13 @@ void read_tallies_xml(pugi::xml_node root); //! batch to a new random variable void accumulate_tallies(); +//! Determine distance to next time boundary +// +//! \param time Current time of particle +//! \param speed Speed of particle +//! \return Distance to next time boundary (or INFTY if none) +double distance_to_time_boundary(double time, double speed); + //! Determine which tallies should be active void setup_active_tallies(); diff --git a/include/openmc/tallies/tally_scoring.h b/include/openmc/tallies/tally_scoring.h index 28f1f1622..c3ab779e6 100644 --- a/include/openmc/tallies/tally_scoring.h +++ b/include/openmc/tallies/tally_scoring.h @@ -91,6 +91,16 @@ void score_analog_tally_mg(Particle& p); //! \param distance The distance in [cm] traveled by the particle void score_tracklength_tally(Particle& p, double distance); +//! Score time filtered tallies using a tracklength estimate of the flux. +// +//! This is triggered at every event (surface crossing, lattice crossing, or +//! collision) and thus cannot be done for tallies that require post-collision +//! information. +// +//! \param p The particle being tracked +//! \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. // //! \param p The particle being tracked diff --git a/src/particle.cpp b/src/particle.cpp index fd4df14c3..f25ada02c 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -252,6 +252,11 @@ void Particle::event_advance() this->time() += dt; this->lifetime() += dt; + // Score timed track-length tallies + if (!model::active_timed_tracklength_tallies.empty()) { + score_timed_tracklength_tally(*this, distance); + } + // Score track-length tallies if (!model::active_tracklength_tallies.empty()) { score_tracklength_tally(*this, distance); diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index ba1838602..ae0bffe6e 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -27,10 +27,12 @@ #include "openmc/tallies/filter_legendre.h" #include "openmc/tallies/filter_mesh.h" #include "openmc/tallies/filter_meshborn.h" +#include "openmc/tallies/filter_meshmaterial.h" #include "openmc/tallies/filter_meshsurface.h" #include "openmc/tallies/filter_particle.h" #include "openmc/tallies/filter_sph_harm.h" #include "openmc/tallies/filter_surface.h" +#include "openmc/tallies/filter_time.h" #include "openmc/xml_interface.h" #include "xtensor/xadapt.hpp" @@ -38,9 +40,10 @@ #include "xtensor/xview.hpp" #include -#include // for max +#include // for max, set_union #include -#include // for size_t +#include // for size_t +#include // for back_inserter #include namespace openmc { @@ -56,11 +59,13 @@ vector> tallies; vector active_tallies; vector active_analog_tallies; vector active_tracklength_tallies; +vector active_timed_tracklength_tallies; vector active_collision_tallies; vector active_meshsurf_tallies; vector active_surface_tallies; vector active_pulse_height_tallies; vector pulse_height_cells; +vector time_grid; } // namespace model namespace simulation { @@ -243,8 +248,8 @@ Tally::Tally(pugi::xml_node node) for (int score : scores_) { switch (score) { case SCORE_PULSE_HEIGHT: - fatal_error( - "For pulse-height tallies, photon transport needs to be activated."); + fatal_error("For pulse-height tallies, photon transport needs to be " + "activated."); break; } } @@ -318,7 +323,8 @@ Tally::Tally(pugi::xml_node node) if (has_energyout && i_nuc == -1) { fatal_error(fmt::format( "Error on tally {}: Cannot use a " - "'nuclide_density' or 'temperature' derivative on a tally with an " + "'nuclide_density' or 'temperature' derivative on a tally with " + "an " "outgoing energy filter and 'total' nuclide rate. Instead, tally " "each nuclide in the material individually.", id_)); @@ -493,9 +499,9 @@ void Tally::add_filter(Filter* filter) void Tally::set_strides() { - // Set the strides. Filters are traversed in reverse so that the last filter - // has the shortest stride in memory and the first filter has the longest - // stride. + // Set the strides. Filters are traversed in reverse so that the last + // filter has the shortest stride in memory and the first filter has the + // longest stride. auto n = filters_.size(); strides_.resize(n, 0); int stride = 1; @@ -551,7 +557,8 @@ void Tally::set_scores(const vector& scores) // Iterate over the given scores. for (auto score_str : scores) { - // Make sure a delayed group filter wasn't used with an incompatible score. + // Make sure a delayed group filter wasn't used with an incompatible + // score. if (delayedgroup_filter_ != C_NONE) { if (score_str != "delayed-nu-fission" && score_str != "decay-rate") fatal_error("Cannot tally " + score_str + "with a delayedgroup filter"); @@ -984,8 +991,8 @@ void reduce_tally_results() } } - // Note that global tallies are *always* reduced even when no_reduce option is - // on. + // Note that global tallies are *always* reduced even when no_reduce option + // is on. // Get view of global tally values auto& gt = simulation::global_tallies; @@ -1064,21 +1071,59 @@ void accumulate_tallies() } } +double distance_to_time_boundary(double time, double speed) +{ + if (model::time_grid.empty()) { + return INFTY; + } else if (time >= model::time_grid.back()) { + return INFTY; + } else { + double next_time = + *std::upper_bound(model::time_grid.begin(), model::time_grid.end(), time); + return (next_time - time) * speed; + } +} + +//! Add new points to the global time grid +// +//! \param grid Vector of new time points to add +void add_to_time_grid(vector grid) +{ + if (grid.empty()) + return; + + // Create new vector with enough space to hold old and new grid points + vector merged; + merged.reserve(model::time_grid.size() + grid.size()); + + // Merge and remove duplicates + std::set_union(model::time_grid.begin(), model::time_grid.end(), grid.begin(), + grid.end(), std::back_inserter(merged)); + + // Swap in the new grid + model::time_grid.swap(merged); +} + void setup_active_tallies() { model::active_tallies.clear(); model::active_analog_tallies.clear(); model::active_tracklength_tallies.clear(); + model::active_timed_tracklength_tallies.clear(); model::active_collision_tallies.clear(); model::active_meshsurf_tallies.clear(); model::active_surface_tallies.clear(); model::active_pulse_height_tallies.clear(); + model::time_grid.clear(); for (auto i = 0; i < model::tallies.size(); ++i) { const auto& tally {*model::tallies[i]}; if (tally.active_) { model::active_tallies.push_back(i); + bool mesh_present = (tally.get_filter() || + tally.get_filter()); + auto time_filter = tally.get_filter(); switch (tally.type_) { case TallyType::VOLUME: @@ -1087,7 +1132,12 @@ void setup_active_tallies() model::active_analog_tallies.push_back(i); break; case TallyEstimator::TRACKLENGTH: - model::active_tracklength_tallies.push_back(i); + if (time_filter && mesh_present) { + model::active_timed_tracklength_tallies.push_back(i); + add_to_time_grid(time_filter->bins()); + } else { + model::active_tracklength_tallies.push_back(i); + } break; case TallyEstimator::COLLISION: model::active_collision_tallies.push_back(i); @@ -1123,10 +1173,12 @@ void free_memory_tally() model::active_tallies.clear(); model::active_analog_tallies.clear(); model::active_tracklength_tallies.clear(); + model::active_timed_tracklength_tallies.clear(); model::active_collision_tallies.clear(); model::active_meshsurf_tallies.clear(); model::active_surface_tallies.clear(); model::active_pulse_height_tallies.clear(); + model::time_grid.clear(); model::tally_map.clear(); } @@ -1465,8 +1517,8 @@ extern "C" int openmc_tally_get_n_realizations(int32_t index, int32_t* n) return 0; } -//! \brief Returns a pointer to a tally results array along with its shape. This -//! allows a user to obtain in-memory tally results from Python directly. +//! \brief Returns a pointer to a tally results array along with its shape. +//! This allows a user to obtain in-memory tally results from Python directly. extern "C" int openmc_tally_results( int32_t index, double** results, size_t* shape) { diff --git a/src/tallies/tally_scoring.cpp b/src/tallies/tally_scoring.cpp index e73fb90f3..04b047b64 100644 --- a/src/tallies/tally_scoring.cpp +++ b/src/tallies/tally_scoring.cpp @@ -2404,15 +2404,13 @@ void score_analog_tally_mg(Particle& p) match.bins_present_ = false; } -void score_tracklength_tally(Particle& p, double distance) +void score_tracklength_tally_general( + Particle& p, double flux, const vector& tallies) { - // Determine the tracklength estimate of the flux - double flux = p.wgt() * distance; - // Set 'none' value for log union grid index int i_log_union = C_NONE; - for (auto i_tally : model::active_tracklength_tallies) { + for (auto i_tally : tallies) { const Tally& tally {*model::tallies[i_tally]}; // Initialize an iterator over valid filter bin combinations. If there are @@ -2481,6 +2479,57 @@ void score_tracklength_tally(Particle& p, double distance) match.bins_present_ = false; } +void score_timed_tracklength_tally(Particle& p, double total_distance) +{ + double speed = p.speed(); + double total_dt = total_distance / speed; + + // save particle last state + auto time_last = p.time_last(); + auto r_last = p.r_last(); + + // move particle back + p.move_distance(-total_distance); + p.time() -= total_dt; + p.lifetime() -= total_dt; + + double distance_traveled = 0.0; + while (distance_traveled < total_distance) { + + double distance = std::min(distance_to_time_boundary(p.time(), speed), + total_distance - distance_traveled); + double dt = distance / speed; + + // Save particle last state for tracklength tallies + p.time_last() = p.time(); + p.r_last() = p.r(); + + // Advance particle in space and time + p.move_distance(distance); + p.time() += dt; + p.lifetime() += dt; + + // Determine the tracklength estimate of the flux + double flux = p.wgt() * distance; + + score_tracklength_tally_general( + p, flux, model::active_timed_tracklength_tallies); + distance_traveled += distance; + } + + p.time_last() = time_last; + p.r_last() = r_last; +} + +void score_tracklength_tally(Particle& p, double distance) +{ + + // Determine the tracklength estimate of the flux + double flux = p.wgt() * distance; + + score_tracklength_tally_general(p, flux, model::active_tracklength_tallies); +} + void score_collision_tally(Particle& p) { // Determine the collision estimate of the flux diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index 1930e3264..f0f289408 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -3,10 +3,12 @@ from tempfile import TemporaryDirectory from pathlib import Path import numpy as np +from scipy.stats import chi2 import pytest import openmc import openmc.lib from openmc.utility_funcs import change_directory +from uncertainties.unumpy import uarray, nominal_values, std_devs @pytest.mark.parametrize("val_left,val_right", [(0, 0), (-1., -1.), (2.0, 2)]) @@ -481,7 +483,7 @@ def test_umesh(run_in_tmpdir, simple_umesh, export_type): np.testing.assert_almost_equal(mean, ref_data) # attempt to apply a dataset with an improper size to a VTK write - with pytest.raises(ValueError, match='Cannot apply dataset "mean"') as e: + with pytest.raises(ValueError, match='Cannot apply dataset "mean"'): simple_umesh.write_data_to_vtk(datasets={'mean': ref_data[:-2]}, filename=filename) def test_mesh_get_homogenized_materials(): @@ -678,3 +680,82 @@ def test_raytrace_mesh_infinite_loop(run_in_tmpdir): # Run the model; this should not cause an infinite loop model.run() + + +def test_filter_time_mesh(run_in_tmpdir): + """Test combination of TimeFilter and MeshFilter""" + + # Define material + mat = openmc.Material() + mat.add_nuclide('Fe56', 1.0) + mat.set_density('g/cm3', 7.8) + + # Define geometry + surf_Z1 = openmc.XPlane(x0=-1e10, boundary_type="reflective") + surf_Z2 = openmc.XPlane(x0=1e10, boundary_type="reflective") + cell_F = openmc.Cell(fill=mat, region=+surf_Z1 & -surf_Z2) + model = openmc.Model() + model.geometry = openmc.Geometry([cell_F]) + + # Define settings + model.settings.run_mode = "fixed source" + model.settings.particles = 1000 + model.settings.batches = 20 + model.settings.output = {"tallies": False} + model.settings.cutoff = {"time_neutron": 1e-7} + + # Define tallies + + # Create a mesh filter that can be used in a tally + mesh = openmc.RegularMesh() + mesh.dimension = (21, 1, 1) + mesh.lower_left = (-20.5, -1e10, -1e10) + mesh.upper_right = (20.5, 1e10, 1e10) + time_grid = np.linspace(0.0, 1e-7, 21) + + mesh_filter = openmc.MeshFilter(mesh) + time_filter = openmc.TimeFilter(time_grid) + + # Now use the mesh filter in a tally and indicate what scores are desired + tally1 = openmc.Tally(name="collision") + tally1.estimator = "collision" + tally1.filters = [time_filter, mesh_filter] + tally1.scores = ["flux"] + tally2 = openmc.Tally(name="tracklength") + tally2.estimator = "tracklength" + tally2.filters = [time_filter, mesh_filter] + tally2.scores = ["flux"] + model.tallies = openmc.Tallies([tally1, tally2]) + + # Run and post-process + model.run(apply_tally_results=True) + + # Get radial flux distribution + flux_collision = tally1.mean.ravel() + flux_collision_unc = tally1.std_dev.ravel() + flux_tracklength = tally2.mean.ravel() + flux_tracklength_unc = tally2.std_dev.ravel() + + # Construct arrays with uncertainties + collision = uarray(flux_collision, flux_collision_unc) + tracklength = uarray(flux_tracklength, flux_tracklength_unc) + delta = collision - tracklength + + # Compute differences and standard deviations + diff = nominal_values(delta) + std_dev = std_devs(delta) + + # Exclude zero-uncertainty bins + mask = std_dev > 0.0 + dof = int(np.sum(mask)) + + # Global chi-square consistency test between collision and tracklength + # estimators. Target false positive rate ~1e-4 (1 in 10,000) + z = diff[mask] / std_dev[mask] + chi2_stat = np.sum(z * z) + alpha = 1.0e-4 + crit = chi2.ppf(1 - alpha, dof) + assert chi2_stat < crit, ( + f"Collision vs tracklength tallies disagree: chi2={chi2_stat:.2f} " + f">= {crit=:.2f} ({dof=}, {alpha=})" + ) From 607f6babe5c7fc1a441082fb355e7f30f182e72d Mon Sep 17 00:00:00 2001 From: Kevin Sawatzky <66632997+nuclearkevin@users.noreply.github.com> Date: Thu, 18 Sep 2025 23:11:06 -0500 Subject: [PATCH 409/671] Add distributed cell densities (#3546) Co-authored-by: Patrick Shriwise Co-authored-by: Paul Romano --- docs/source/capi/index.rst | 29 ++- docs/source/io_formats/properties.rst | 3 +- docs/source/io_formats/summary.rst | 3 +- include/openmc/capi.h | 4 + include/openmc/cell.h | 25 +++ include/openmc/constants.h | 4 +- include/openmc/geometry_aux.h | 6 + include/openmc/material.h | 7 + include/openmc/particle_data.h | 8 + openmc/cell.py | 43 ++++- openmc/lib/cell.py | 46 +++++ openmc/model/model.py | 16 +- src/cell.cpp | 178 +++++++++++++++++- src/geometry.cpp | 4 +- src/geometry_aux.cpp | 29 +++ src/initialize.cpp | 9 + src/material.cpp | 4 +- src/mgxs.cpp | 8 +- src/particle.cpp | 7 +- src/physics.cpp | 4 +- src/tallies/tally_scoring.cpp | 33 ++-- tests/regression_tests/cpp_driver/driver.cpp | 18 +- .../cpp_driver/results_true.dat | 22 +-- .../regression_tests/dagmc/external/main.cpp | 3 + .../lattice_distribrho/__init__.py | 0 .../lattice_distribrho/inputs_true.dat | 40 ++++ .../lattice_distribrho/results_true.dat | 2 + .../lattice_distribrho/test.py | 51 +++++ .../multipole/results_true.dat | 1 + tests/unit_tests/test_cell.py | 23 +++ tests/unit_tests/test_lib.py | 23 +++ tests/unit_tests/test_model.py | 6 +- 32 files changed, 607 insertions(+), 52 deletions(-) create mode 100644 tests/regression_tests/lattice_distribrho/__init__.py create mode 100644 tests/regression_tests/lattice_distribrho/inputs_true.dat create mode 100644 tests/regression_tests/lattice_distribrho/results_true.dat create mode 100644 tests/regression_tests/lattice_distribrho/test.py diff --git a/docs/source/capi/index.rst b/docs/source/capi/index.rst index d9ac0d1e0..2583d51df 100644 --- a/docs/source/capi/index.rst +++ b/docs/source/capi/index.rst @@ -84,6 +84,17 @@ Functions :return: Return status (negative if an error occurred) :rtype: int +.. c:function:: int openmc_cell_get_density(int32_t index, const int32_t* instance, double* density) + + Get the density of a cell + + :param int32_t index: Index in the cells array + :param int32_t* instance: Which instance of the cell. If a null pointer is passed, the density + multiplier of the first instance is returned. + :param double* density: Density of the cell in [g/cm3] + :return: Return status (negative if an error occurred) + :rtype: int + .. c:function:: int openmc_cell_set_fill(int32_t index, int type, int32_t n, const int32_t* indices) Set the fill for a cell @@ -113,8 +124,22 @@ Functions :param double T: Temperature in Kelvin :param instance: Which instance of the cell. To set the temperature for all instances, pass a null pointer. - :param set_contained: If the cell is not filled by a material, whether to set the temperatures - of all filled cells + :param bool set_contained: If the cell is not filled by a material, whether + to set the temperatures of all filled cells + :type instance: const int32_t* + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_cell_set_density(index index, double density, const int32_t* instance, bool set_contained) + + Set the density of a cell. + + :param int32_t index: Index in the cells array + :param double density: Density of the cell in [g/cm3] + :param instance: Which instance of the cell. To set the density multiplier for all + instances, pass a null pointer. + :param bool set_contained: If the cell is not filled by a material, whether + to set the density multiplier of all filled cells :type instance: const int32_t* :return: Return status (negative if an error occurred) :rtype: int diff --git a/docs/source/io_formats/properties.rst b/docs/source/io_formats/properties.rst index 5030e78f3..4cc5da379 100644 --- a/docs/source/io_formats/properties.rst +++ b/docs/source/io_formats/properties.rst @@ -4,7 +4,7 @@ Properties File Format ====================== -The current version of the properties file format is 1.0. +The current version of the properties file format is 1.1. **/** @@ -25,6 +25,7 @@ The current version of the properties file format is 1.0. **/geometry/cells/cell /** :Datasets: - **temperature** (*double[]*) -- Temperature of the cell in [K]. + - **density** (*double[]*) -- Density of the cell in [g/cm3]. **/materials/** diff --git a/docs/source/io_formats/summary.rst b/docs/source/io_formats/summary.rst index 7d3ab94d9..64ca68b9c 100644 --- a/docs/source/io_formats/summary.rst +++ b/docs/source/io_formats/summary.rst @@ -4,7 +4,7 @@ Summary File Format =================== -The current version of the summary file format is 6.0. +The current version of the summary file format is 6.1. **/** @@ -38,6 +38,7 @@ The current version of the summary file format is 6.0. is an array if the cell uses distributed materials, otherwise it is a scalar. - **temperature** (*double[]*) -- Temperature of the cell in Kelvin. + - **density** (*double[]*) -- Density of the cell in [g/cm3]. - **translation** (*double[3]*) -- Translation applied to the fill universe. This dataset is present only if fill_type is set to 'universe'. diff --git a/include/openmc/capi.h b/include/openmc/capi.h index 54257d093..d8041ef41 100644 --- a/include/openmc/capi.h +++ b/include/openmc/capi.h @@ -17,6 +17,8 @@ int openmc_cell_get_fill( int openmc_cell_get_id(int32_t index, int32_t* id); int openmc_cell_get_temperature( int32_t index, const int32_t* instance, double* T); +int openmc_cell_get_density( + int32_t index, const int32_t* instance, double* rho); int openmc_cell_get_translation(int32_t index, double xyz[]); int openmc_cell_get_rotation(int32_t index, double rot[], size_t* n); int openmc_cell_get_name(int32_t index, const char** name); @@ -27,6 +29,8 @@ int openmc_cell_set_fill( int openmc_cell_set_id(int32_t index, int32_t id); int openmc_cell_set_temperature( int32_t index, double T, const int32_t* instance, bool set_contained = false); +int openmc_cell_set_density(int32_t index, double rho, const int32_t* instance, + bool set_contained = false); int openmc_cell_set_translation(int32_t index, const double xyz[]); int openmc_cell_set_rotation(int32_t index, const double rot[], size_t rot_len); int openmc_dagmc_universe_get_cell_ids( diff --git a/include/openmc/cell.h b/include/openmc/cell.h index e9fcfe391..9dfe2339a 100644 --- a/include/openmc/cell.h +++ b/include/openmc/cell.h @@ -216,6 +216,18 @@ public: //! \return Temperature in [K] double temperature(int32_t instance = -1) const; + //! Get the density multiplier of a cell instance + //! \param[in] instance Instance index. If -1 is given, the density multiplier + //! for the first instance is returned. + //! \return Density multiplier + double density_mult(int32_t instance = -1) const; + + //! Get the density of a cell instance in g/cm3 + //! \param[in] instance Instance index. If -1 is given, the density + //! for the first instance is returned. + //! \return Density in [g/cm3] + double density(int32_t instance = -1) const; + //! Set the temperature of a cell instance //! \param[in] T Temperature in [K] //! \param[in] instance Instance index. If -1 is given, the temperature for @@ -226,6 +238,16 @@ public: void set_temperature( double T, int32_t instance = -1, bool set_contained = false); + //! Set the density of a cell instance + //! \param[in] density Density [g/cm3] + //! \param[in] instance Instance index. If -1 is given, the density + //! for all instances is set. + //! \param[in] set_contained If this cell is not filled with a material, + //! collect all contained cells with material fills and set their + //! densities. + void set_density( + double density, int32_t instance = -1, bool set_contained = false); + int32_t n_instances() const; //! Set the rotation matrix of a cell instance @@ -334,6 +356,9 @@ public: //! T. The units are sqrt(eV). vector sqrtkT_; + //! \brief Unitless density multiplier(s) within this cell. + vector density_mult_; + //! \brief Neighboring cells in the same universe. NeighborList neighbors_; diff --git a/include/openmc/constants.h b/include/openmc/constants.h index ae7056079..df13da370 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -28,11 +28,11 @@ constexpr int HDF5_VERSION[] {3, 0}; constexpr array VERSION_STATEPOINT {18, 1}; constexpr array VERSION_PARTICLE_RESTART {2, 0}; constexpr array VERSION_TRACK {3, 0}; -constexpr array VERSION_SUMMARY {6, 0}; +constexpr array VERSION_SUMMARY {6, 1}; constexpr array VERSION_VOLUME {1, 0}; constexpr array VERSION_VOXEL {2, 0}; constexpr array VERSION_MGXS_LIBRARY {1, 0}; -constexpr array VERSION_PROPERTIES {1, 0}; +constexpr array VERSION_PROPERTIES {1, 1}; constexpr array VERSION_WEIGHT_WINDOWS {1, 0}; // ============================================================================ diff --git a/include/openmc/geometry_aux.h b/include/openmc/geometry_aux.h index f60f2d649..4dafdea5c 100644 --- a/include/openmc/geometry_aux.h +++ b/include/openmc/geometry_aux.h @@ -37,6 +37,12 @@ void adjust_indices(); void assign_temperatures(); +//============================================================================== +//! Finalize densities (compute density multipliers). +//============================================================================== + +void finalize_cell_densities(); + //============================================================================== //! \brief Obtain a list of temperatures that each nuclide/thermal scattering //! table appears at in the model. Later, this list is used to determine the diff --git a/include/openmc/material.h b/include/openmc/material.h index fe587a86f..e36946c71 100644 --- a/include/openmc/material.h +++ b/include/openmc/material.h @@ -99,6 +99,13 @@ public: //---------------------------------------------------------------------------- // Accessors + //! Get the atom density in [atom/b-cm] + //! \return Density in [atom/b-cm] + double atom_density(int32_t i, double rho_multiplier = 1.0) const + { + return atom_density_(i) * rho_multiplier; + } + //! Get density in [atom/b-cm] //! \return Density in [atom/b-cm] double density() const { return density_; } diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index ec393845f..1a22f5837 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -389,6 +389,11 @@ public: const double& sqrtkT() const { return sqrtkT_; } double& sqrtkT_last() { return sqrtkT_last_; } + // density multiplier of the current and last cell + double& density_mult() { return density_mult_; } + const double& density_mult() const { return density_mult_; } + double& density_mult_last() { return density_mult_last_; } + private: int64_t id_ {-1}; //!< Unique ID @@ -417,6 +422,9 @@ private: double sqrtkT_ {-1.0}; //!< sqrt(k_Boltzmann * temperature) in eV double sqrtkT_last_ {0.0}; //!< last temperature + double density_mult_ {1.0}; //!< density multiplier + double density_mult_last_ {1.0}; //!< last density multiplier + #ifdef OPENMC_DAGMC_ENABLED moab::DagMC::RayHistory history_; Direction last_dir_; diff --git a/openmc/cell.py b/openmc/cell.py index 88aaebc8f..82a034b1c 100644 --- a/openmc/cell.py +++ b/openmc/cell.py @@ -72,6 +72,10 @@ class Cell(IDManagerMixin): temperature : float or iterable of float Temperature of the cell in Kelvin. Multiple temperatures can be given to give each distributed cell instance a unique temperature. + density : float or iterable of float + Density of the cell in [g/cm3]. Multiple densities can be given to give + each distributed cell instance a unique density. Densities set here will + override the density set on materials used to fill the cell. translation : Iterable of float If the cell is filled with a universe, this array specifies a vector that is used to translate (shift) the universe. @@ -109,6 +113,7 @@ class Cell(IDManagerMixin): self._rotation = None self._rotation_matrix = None self._temperature = None + self._density = None self._translation = None self._paths = None self._num_instances = None @@ -141,6 +146,7 @@ class Cell(IDManagerMixin): if self.fill_type == 'material': string += '\t{0: <15}=\t{1}\n'.format('Temperature', self.temperature) + string += '\t{0: <15}=\t{1}\n'.format('Density', self.density) string += '{: <16}=\t{}\n'.format('\tTranslation', self.translation) string += '{: <16}=\t{}\n'.format('\tVolume', self.volume) @@ -257,6 +263,30 @@ class Cell(IDManagerMixin): else: self._temperature = temperature + @property + def density(self): + return self._density + + @density.setter + def density(self, density): + # Make sure densities are greater than zero + cv.check_type('cell density', density, (Iterable, Real), none_ok=True) + if isinstance(density, Iterable): + cv.check_type('cell density', density, Iterable, Real) + for rho in density: + cv.check_greater_than('cell density', rho, 0.0, True) + elif isinstance(density, Real): + cv.check_greater_than('cell density', density, 0.0, True) + + # If this cell is filled with a universe or lattice, propagate + # densities to all cells contained. Otherwise, simply assign it. + if self.fill_type in ('universe', 'lattice'): + for c in self.get_all_cells().values(): + if c.fill_type == 'material': + c._density = density + else: + self._density = density + @property def translation(self): return self._translation @@ -525,6 +555,8 @@ class Cell(IDManagerMixin): clone.volume = self.volume if self.temperature is not None: clone.temperature = self.temperature + if self.density is not None: + clone.density = self.density if self.translation is not None: clone.translation = self.translation if self.rotation is not None: @@ -650,6 +682,12 @@ class Cell(IDManagerMixin): else: element.set("temperature", str(self.temperature)) + if self.density is not None: + if isinstance(self.density, Iterable): + element.set("density", ' '.join(str(t) for t in self.density)) + else: + element.set("density", str(self.density)) + if self.translation is not None: element.set("translation", ' '.join(map(str, self.translation))) @@ -711,10 +749,13 @@ class Cell(IDManagerMixin): 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) - for key in ('temperature', 'rotation', 'translation'): + for key in ('temperature', 'density', 'rotation', 'translation'): values = get_elem_list(elem, key, float) if values is not None: if key == 'rotation' and len(values) == 9: diff --git a/openmc/lib/cell.py b/openmc/lib/cell.py index 971a24cba..dfd09d2f9 100644 --- a/openmc/lib/cell.py +++ b/openmc/lib/cell.py @@ -34,6 +34,10 @@ _dll.openmc_cell_get_temperature.argtypes = [ c_int32, POINTER(c_int32), POINTER(c_double)] _dll.openmc_cell_get_temperature.restype = c_int _dll.openmc_cell_get_temperature.errcheck = _error_handler +_dll.openmc_cell_get_density.argtypes = [ + c_int32, POINTER(c_int32), POINTER(c_double)] +_dll.openmc_cell_get_density.restype = c_int +_dll.openmc_cell_get_density.errcheck = _error_handler _dll.openmc_cell_get_name.argtypes = [c_int32, POINTER(c_char_p)] _dll.openmc_cell_get_name.restype = c_int _dll.openmc_cell_get_name.errcheck = _error_handler @@ -58,6 +62,10 @@ _dll.openmc_cell_set_temperature.argtypes = [ c_int32, c_double, POINTER(c_int32), c_bool] _dll.openmc_cell_set_temperature.restype = c_int _dll.openmc_cell_set_temperature.errcheck = _error_handler +_dll.openmc_cell_set_density.argtypes = [ + c_int32, c_double, POINTER(c_int32), c_bool] +_dll.openmc_cell_set_density.restype = c_int +_dll.openmc_cell_set_density.errcheck = _error_handler _dll.openmc_cell_set_translation.argtypes = [c_int32, POINTER(c_double)] _dll.openmc_cell_set_translation.restype = c_int _dll.openmc_cell_set_translation.errcheck = _error_handler @@ -236,6 +244,44 @@ class Cell(_FortranObjectWithID): _dll.openmc_cell_set_temperature(self._index, T, instance, set_contained) + def get_density(self, instance: int | None = None): + """Get the density of a cell in [g/cm3] + + Parameters + ---------- + instance : int or None + Which instance of the cell + + """ + + if instance is not None: + instance = c_int32(instance) + + rho = c_double() + _dll.openmc_cell_get_density(self._index, instance, rho) + return rho.value + + def set_density(self, rho: float, instance: int | None = None, + set_contained: bool = False): + """Set the density of a cell + + Parameters + ---------- + rho : float + Density of the cell in [g/cm3] + instance : int or None + Which instance of the cell + set_contained : bool + If cell is not filled by a material, whether to set the density + of all filled cells + + """ + + if instance is not None: + instance = c_int32(instance) + + _dll.openmc_cell_set_density(self._index, rho, instance, set_contained) + @property def translation(self): translation = np.zeros(3) diff --git a/openmc/model/model.py b/openmc/model/model.py index 03fdcda1f..59e6fa511 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -633,7 +633,7 @@ class Model: raise ValueError("Number of cells in properties file doesn't " "match current model.") - # Update temperatures for cells filled with materials + # Update temperatures and densities for cells filled with materials for name, group in cells_group.items(): cell_id = int(name.split()[1]) cell = cells[cell_id] @@ -648,6 +648,20 @@ class Model: else: lib_cell.set_temperature(temperature[0]) + if group['density']: + density = group['density'][()] + if density.size > 1: + cell.density = [rho for rho in density] + else: + cell.density = density + if self.is_initialized: + lib_cell = openmc.lib.cells[cell_id] + if density.size > 1: + for i, rho in enumerate(density): + lib_cell.set_density(rho, i) + else: + lib_cell.set_density(density[0]) + # Make sure number of materials matches mats_group = fh['materials'] n_cells = mats_group.attrs['n_materials'] diff --git a/src/cell.cpp b/src/cell.cpp index d838bfbb4..e2479994a 100644 --- a/src/cell.cpp +++ b/src/cell.cpp @@ -101,6 +101,25 @@ double Cell::temperature(int32_t instance) const } } +double Cell::density_mult(int32_t instance) const +{ + if (instance >= 0) { + return density_mult_.size() == 1 ? density_mult_.at(0) + : density_mult_.at(instance); + } else { + return density_mult_[0]; + } +} + +double Cell::density(int32_t instance) const +{ + const int32_t mat_index = material(instance); + if (mat_index == MATERIAL_VOID) + return 0.0; + + return density_mult(instance) * model::materials[mat_index]->density_gpcc(); +} + void Cell::set_temperature(double T, int32_t instance, bool set_contained) { if (settings::temperature_method == TemperatureMethod::INTERPOLATION) { @@ -151,6 +170,47 @@ void Cell::set_temperature(double T, int32_t instance, bool set_contained) } } +void Cell::set_density(double density, int32_t instance, bool set_contained) +{ + if (type_ != Fill::MATERIAL && !set_contained) { + fatal_error( + fmt::format("Attempted to set the density multiplier of cell {} " + "which is not filled by a material.", + id_)); + } + + if (type_ == Fill::MATERIAL) { + const int32_t mat_index = material(instance); + if (mat_index == MATERIAL_VOID) + return; + + if (instance >= 0) { + // If density multiplier vector is not big enough, resize it first + if (density_mult_.size() != n_instances()) + density_mult_.resize(n_instances(), density_mult_[0]); + + // Set density multiplier for the corresponding instance + density_mult_.at(instance) = + density / model::materials[mat_index]->density_gpcc(); + } else { + // Set density multiplier for all instances + for (auto& x : density_mult_) { + x = density / model::materials[mat_index]->density_gpcc(); + } + } + } else { + auto contained_cells = this->get_contained_cells(instance); + for (const auto& entry : contained_cells) { + auto& cell = model::cells[entry.first]; + assert(cell->type_ == Fill::MATERIAL); + auto& instances = entry.second; + for (auto instance : instances) { + cell->set_density(density, instance); + } + } + } +} + void Cell::export_properties_hdf5(hid_t group) const { // Create a group for this cell. @@ -162,6 +222,15 @@ void Cell::export_properties_hdf5(hid_t group) const temps.push_back(sqrtkT_val * sqrtkT_val / K_BOLTZMANN); write_dataset(cell_group, "temperature", temps); + // Write density for one or more cell instances + if (type_ == Fill::MATERIAL && material_.size() > 0) { + vector density; + for (int32_t i = 0; i < density_mult_.size(); ++i) + density.push_back(this->density(i)); + + write_dataset(cell_group, "density", density); + } + close_group(cell_group); } @@ -176,7 +245,7 @@ void Cell::import_properties_hdf5(hid_t group) // Ensure number of temperatures makes sense auto n_temps = temps.size(); if (n_temps > 1 && n_temps != n_instances()) { - throw std::runtime_error(fmt::format( + fatal_error(fmt::format( "Number of temperatures for cell {} doesn't match number of instances", id_)); } @@ -188,6 +257,25 @@ void Cell::import_properties_hdf5(hid_t group) this->set_temperature(temps[i], i); } + // Read densities + if (object_exists(cell_group, "density")) { + vector density; + read_dataset(cell_group, "density", density); + + // Ensure number of densities makes sense + auto n_density = density.size(); + if (n_density > 1 && n_density != n_instances()) { + fatal_error(fmt::format("Number of densities for cell {} " + "doesn't match number of instances", + id_)); + } + + // Set densities. + for (int32_t i = 0; i < n_density; ++i) { + this->set_density(density[i], i); + } + } + close_group(cell_group); } @@ -227,6 +315,8 @@ void Cell::to_hdf5(hid_t cell_group) const temps.push_back(sqrtkT_val * sqrtkT_val / K_BOLTZMANN); write_dataset(group, "temperature", temps); + write_dataset(group, "density_mult", density_mult_); + } else if (type_ == Fill::UNIVERSE) { write_dataset(group, "fill_type", "universe"); write_dataset(group, "fill", model::universes[fill_]->id_); @@ -344,6 +434,44 @@ CSGCell::CSGCell(pugi::xml_node cell_node) } } + // Read the density element which can be distributed similar to temperature. + // These get assigned to the density multiplier, requiring a division by + // the material density. + // Note: calculating the actual density multiplier is deferred until materials + // are finalized. density_mult_ contains the true density in the meantime. + if (check_for_node(cell_node, "density")) { + density_mult_ = get_node_array(cell_node, "density"); + density_mult_.shrink_to_fit(); + + // Make sure this is a material-filled cell. + if (material_.size() == 0) { + fatal_error(fmt::format( + "Cell {} was specified with a density but no material. Density" + "specification is only valid for cells filled with a material.", + id_)); + } + + // Make sure this is a non-void material. + for (auto mat_id : material_) { + if (mat_id == MATERIAL_VOID) { + fatal_error(fmt::format( + "Cell {} was specified with a density, but contains a void " + "material. Density specification is only valid for cells " + "filled with a non-void material.", + id_)); + } + } + + // Make sure all densities are non-negative and greater than zero. + for (auto rho : density_mult_) { + if (rho <= 0) { + fatal_error(fmt::format( + "Cell {} was specified with a density less than or equal to zero", + id_)); + } + } + } + // Read the region specification. std::string region_spec; if (check_for_node(cell_node, "region")) { @@ -1129,6 +1257,24 @@ extern "C" int openmc_cell_set_temperature( return 0; } +extern "C" int openmc_cell_set_density( + int32_t index, double density, const int32_t* instance, bool set_contained) +{ + if (index < 0 || index >= model::cells.size()) { + strcpy(openmc_err_msg, "Index in cells array is out of bounds."); + return OPENMC_E_OUT_OF_BOUNDS; + } + + int32_t instance_index = instance ? *instance : -1; + try { + model::cells[index]->set_density(density, instance_index, set_contained); + } catch (const std::exception& e) { + set_errmsg(e.what()); + return OPENMC_E_UNASSIGNED; + } + return 0; +} + extern "C" int openmc_cell_get_temperature( int32_t index, const int32_t* instance, double* T) { @@ -1147,6 +1293,36 @@ extern "C" int openmc_cell_get_temperature( return 0; } +extern "C" int openmc_cell_get_density( + int32_t index, const int32_t* instance, double* density) +{ + if (index < 0 || index >= model::cells.size()) { + strcpy(openmc_err_msg, "Index in cells array is out of bounds."); + return OPENMC_E_OUT_OF_BOUNDS; + } + + int32_t instance_index = instance ? *instance : -1; + try { + if (model::cells[index]->type_ != Fill::MATERIAL) { + fatal_error( + fmt::format("Cell {}, instance {} is not filled with a material.", + model::cells[index]->id_, instance_index)); + } + + int32_t mat_index = model::cells[index]->material(instance_index); + if (mat_index == MATERIAL_VOID) { + *density = 0.0; + } else { + *density = model::cells[index]->density_mult(instance_index) * + model::materials[mat_index]->density_gpcc(); + } + } catch (const std::exception& e) { + set_errmsg(e.what()); + return OPENMC_E_UNASSIGNED; + } + return 0; +} + //! Get the bounding box of a cell extern "C" int openmc_cell_bounding_box( const int32_t index, double* llc, double* urc) diff --git a/src/geometry.cpp b/src/geometry.cpp index df4850891..5ff15097b 100644 --- a/src/geometry.cpp +++ b/src/geometry.cpp @@ -172,11 +172,13 @@ bool find_cell_inner( p.cell_instance() = cell_instance_at_level(p, p.n_coord() - 1); } - // Set the material and temperature. + // Set the material, temperature and density multiplier. p.material_last() = p.material(); p.material() = c.material(p.cell_instance()); p.sqrtkT_last() = p.sqrtkT(); p.sqrtkT() = c.sqrtkT(p.cell_instance()); + p.density_mult_last() = p.density_mult(); + p.density_mult() = c.density_mult(p.cell_instance()); return true; diff --git a/src/geometry_aux.cpp b/src/geometry_aux.cpp index 51732cf8b..8a145fb1f 100644 --- a/src/geometry_aux.cpp +++ b/src/geometry_aux.cpp @@ -195,6 +195,24 @@ void assign_temperatures() //============================================================================== +void finalize_cell_densities() +{ + for (auto& c : model::cells) { + // Convert to density multipliers. + if (!c->density_mult_.empty()) { + for (int32_t instance = 0; instance < c->density_mult_.size(); + ++instance) { + c->density_mult_[instance] /= + model::materials[c->material(instance)]->density_gpcc(); + } + } else { + c->density_mult_ = {1.0}; + } + } +} + +//============================================================================== + void get_temperatures( vector>& nuc_temps, vector>& thermal_temps) { @@ -362,6 +380,17 @@ void prepare_distribcell(const std::vector* user_distribcells) c.id_, c.sqrtkT_.size(), c.n_instances())); } } + + if (c.density_mult_.size() > 1) { + if (c.density_mult_.size() != c.n_instances()) { + fatal_error(fmt::format("Cell {} was specified with {} density " + "multipliers but has {} distributed " + "instances. The number of density multipliers " + "must equal one or the number " + "of instances.", + c.id_, c.density_mult_.size(), c.n_instances())); + } + } } // Search through universes for material cells and assign each one a diff --git a/src/initialize.cpp b/src/initialize.cpp index 2da78137d..36f326116 100644 --- a/src/initialize.cpp +++ b/src/initialize.cpp @@ -401,6 +401,10 @@ bool read_model_xml() // Finalize cross sections having assigned temperatures finalize_cross_sections(); + // Compute cell density multipliers now that material densities + // have been finalized (from geometry_aux.h) + finalize_cell_densities(); + if (check_for_node(root, "tallies")) read_tallies_xml(root.child("tallies")); @@ -441,6 +445,11 @@ void read_separate_xml_files() // Finalize cross sections having assigned temperatures finalize_cross_sections(); + + // Compute cell density multipliers now that material densities + // have been finalized (from geometry_aux.h) + finalize_cell_densities(); + read_tallies_xml(); // Initialize distribcell_filters diff --git a/src/material.cpp b/src/material.cpp index 32384ddf1..54caa3840 100644 --- a/src/material.cpp +++ b/src/material.cpp @@ -890,7 +890,7 @@ void Material::calculate_neutron_xs(Particle& p) const // ADD TO MACROSCOPIC CROSS SECTION // Copy atom density of nuclide in material - double atom_density = atom_density_(i); + double atom_density = this->atom_density(i, p.density_mult()); // Add contributions to cross sections p.macro_xs().total += atom_density * micro.total; @@ -925,7 +925,7 @@ void Material::calculate_photon_xs(Particle& p) const // ADD TO MACROSCOPIC CROSS SECTION // Copy atom density of nuclide in material - double atom_density = atom_density_(i); + double atom_density = this->atom_density(i, p.density_mult()); // Add contributions to material macroscopic cross sections p.macro_xs().total += atom_density * micro.total; diff --git a/src/mgxs.cpp b/src/mgxs.cpp index a88d2c196..a2c479f21 100644 --- a/src/mgxs.cpp +++ b/src/mgxs.cpp @@ -617,10 +617,12 @@ void Mgxs::calculate_xs(Particle& p) } int temperature = p.mg_xs_cache().t; int angle = p.mg_xs_cache().a; - p.macro_xs().total = xs[temperature].total(angle, p.g()); - p.macro_xs().absorption = xs[temperature].absorption(angle, p.g()); + p.macro_xs().total = xs[temperature].total(angle, p.g()) * p.density_mult(); + p.macro_xs().absorption = + xs[temperature].absorption(angle, p.g()) * p.density_mult(); p.macro_xs().nu_fission = - fissionable ? xs[temperature].nu_fission(angle, p.g()) : 0.; + fissionable ? xs[temperature].nu_fission(angle, p.g()) * p.density_mult() + : 0.; } //============================================================================== diff --git a/src/particle.cpp b/src/particle.cpp index f25ada02c..f5ad45d80 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -200,7 +200,8 @@ void Particle::event_calculate_xs() // Calculate microscopic and macroscopic cross sections if (material() != MATERIAL_VOID) { if (settings::run_CE) { - if (material() != material_last() || sqrtkT() != sqrtkT_last()) { + if (material() != material_last() || sqrtkT() != sqrtkT_last() || + density_mult() != density_mult_last()) { // If the material is the same as the last material and the // temperature hasn't changed, we don't need to lookup cross // sections again. @@ -564,9 +565,10 @@ void Particle::cross_surface(const Surface& surf) int32_t i_cell = next_cell(surface_index(), cell_last(n_coord() - 1), lowest_coord().universe()) - 1; - // save material and temp + // save material, temperature, and density multiplier material_last() = material(); sqrtkT_last() = sqrtkT(); + density_mult_last() = density_mult(); // set new cell value lowest_coord().cell() = i_cell; auto& cell = model::cells[i_cell]; @@ -577,6 +579,7 @@ void Particle::cross_surface(const Surface& surf) material() = cell->material(cell_instance()); sqrtkT() = cell->sqrtkT(cell_instance()); + density_mult() = cell->density_mult(cell_instance()); return; } #endif diff --git a/src/physics.cpp b/src/physics.cpp index 3c06e543d..8ad1b99d7 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -496,7 +496,7 @@ int sample_nuclide(Particle& p) for (int i = 0; i < n; ++i) { // Get atom density int i_nuclide = mat->nuclide_[i]; - double atom_density = mat->atom_density_[i]; + double atom_density = mat->atom_density(i, p.density_mult()); // Increment probability to compare to cutoff prob += atom_density * p.neutron_xs(i_nuclide).total; @@ -521,7 +521,7 @@ int sample_element(Particle& p) for (int i = 0; i < mat->element_.size(); ++i) { // Find atom density int i_element = mat->element_[i]; - double atom_density = mat->atom_density_[i]; + double atom_density = mat->atom_density(i, p.density_mult()); // Determine microscopic cross section double sigma = atom_density * p.photon_xs(i_element).total; diff --git a/src/tallies/tally_scoring.cpp b/src/tallies/tally_scoring.cpp index 04b047b64..f078a5c6e 100644 --- a/src/tallies/tally_scoring.cpp +++ b/src/tallies/tally_scoring.cpp @@ -233,7 +233,7 @@ double score_fission_q(const Particle& p, int score_bin, const Tally& tally, double score {0.0}; for (auto i = 0; i < material.nuclide_.size(); ++i) { auto j_nuclide = material.nuclide_[i]; - auto atom_density = material.atom_density_(i); + auto atom_density = material.atom_density(i, p.density_mult()); const Nuclide& nuc {*data::nuclides[j_nuclide]}; score += get_nuc_fission_q(nuc, p, score_bin) * atom_density * p.neutron_xs(j_nuclide).fission; @@ -696,7 +696,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, const Material& material {*model::materials[p.material()]}; for (auto i = 0; i < material.nuclide_.size(); ++i) { auto j_nuclide = material.nuclide_[i]; - auto atom_density = material.atom_density_(i); + auto atom_density = material.atom_density(i, p.density_mult()); score += p.neutron_xs(j_nuclide).fission * data::nuclides[j_nuclide]->nu( E, ReactionProduct::EmissionMode::prompt) * @@ -743,7 +743,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, const Material& material {*model::materials[p.material()]}; for (auto i = 0; i < material.nuclide_.size(); ++i) { auto j_nuclide = material.nuclide_[i]; - auto atom_density = material.atom_density_(i); + auto atom_density = material.atom_density(i, p.density_mult()); // Tally each delayed group bin individually for (auto d_bin = 0; d_bin < filt.n_bins(); ++d_bin) { auto d = filt.groups()[d_bin]; @@ -763,7 +763,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, const Material& material {*model::materials[p.material()]}; for (auto i = 0; i < material.nuclide_.size(); ++i) { auto j_nuclide = material.nuclide_[i]; - auto atom_density = material.atom_density_(i); + auto atom_density = material.atom_density(i, p.density_mult()); score += p.neutron_xs(j_nuclide).fission * data::nuclides[j_nuclide]->nu( E, ReactionProduct::EmissionMode::delayed) * @@ -824,7 +824,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, const Material& material {*model::materials[p.material()]}; for (auto i = 0; i < material.nuclide_.size(); ++i) { auto j_nuclide = material.nuclide_[i]; - auto atom_density = material.atom_density_(i); + auto atom_density = material.atom_density(i, p.density_mult()); const auto& nuc {*data::nuclides[j_nuclide]}; if (nuc.fissionable_) { const auto& rxn {*nuc.fission_rx_[0]}; @@ -849,7 +849,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, const Material& material {*model::materials[p.material()]}; for (auto i = 0; i < material.nuclide_.size(); ++i) { auto j_nuclide = material.nuclide_[i]; - auto atom_density = material.atom_density_(i); + auto atom_density = material.atom_density(i, p.density_mult()); const auto& nuc {*data::nuclides[j_nuclide]}; if (nuc.fissionable_) { const auto& rxn {*nuc.fission_rx_[0]}; @@ -893,7 +893,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, const Material& material {*model::materials[p.material()]}; for (auto i = 0; i < material.nuclide_.size(); ++i) { auto j_nuclide = material.nuclide_[i]; - auto atom_density = material.atom_density_(i); + auto atom_density = material.atom_density(i, p.density_mult()); const auto& nuc {*data::nuclides[j_nuclide]}; if (nuc.fissionable_) { const auto& rxn {*nuc.fission_rx_[0]}; @@ -924,7 +924,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, const Material& material {*model::materials[p.material()]}; for (auto i = 0; i < material.nuclide_.size(); ++i) { auto j_nuclide = material.nuclide_[i]; - auto atom_density = material.atom_density_(i); + auto atom_density = material.atom_density(i, p.density_mult()); if (p.neutron_xs(j_nuclide).elastic == CACHE_INVALID) data::nuclides[j_nuclide]->calculate_elastic_xs(p); score += p.neutron_xs(j_nuclide).elastic * atom_density * flux; @@ -1025,7 +1025,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, const Material& material {*model::materials[p.material()]}; for (auto i = 0; i < material.nuclide_.size(); ++i) { auto j_nuclide = material.nuclide_[i]; - auto atom_density = material.atom_density_(i); + auto atom_density = material.atom_density(i, p.density_mult()); score += p.neutron_xs(j_nuclide).reaction[m] * atom_density * flux; } } @@ -1079,7 +1079,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, const Material& material {*model::materials[p.material()]}; for (auto i = 0; i < material.nuclide_.size(); ++i) { auto j_nuclide = material.nuclide_[i]; - auto atom_density = material.atom_density_(i); + auto atom_density = material.atom_density(i, p.density_mult()); score += get_nuclide_xs(p, j_nuclide, score_bin) * atom_density * flux; } @@ -2383,7 +2383,8 @@ void score_analog_tally_mg(Particle& p) model::materials[p.material()]->mat_nuclide_index_[i_nuclide]; if (j == C_NONE) continue; - atom_density = model::materials[p.material()]->atom_density_(j); + atom_density = + model::materials[p.material()]->atom_density(j, p.density_mult()); } score_general_mg(p, i_tally, i * tally.scores_.size(), filter_index, @@ -2449,8 +2450,9 @@ void score_tracklength_tally_general( atom_density = 1.0; } } else { - atom_density = - tally.multiply_density() ? mat->atom_density_(j) : 1.0; + atom_density = tally.multiply_density() + ? mat->atom_density(j, p.density_mult()) + : 1.0; } } } @@ -2579,8 +2581,9 @@ void score_collision_tally(Particle& p) atom_density = 1.0; } } else { - atom_density = - tally.multiply_density() ? mat->atom_density_(j) : 1.0; + atom_density = tally.multiply_density() + ? mat->atom_density(j, p.density_mult()) + : 1.0; } } diff --git a/tests/regression_tests/cpp_driver/driver.cpp b/tests/regression_tests/cpp_driver/driver.cpp index 48ed7f317..a99c97b64 100644 --- a/tests/regression_tests/cpp_driver/driver.cpp +++ b/tests/regression_tests/cpp_driver/driver.cpp @@ -15,14 +15,16 @@ using namespace openmc; -int main(int argc, char** argv) { +int main(int argc, char** argv) +{ #ifdef OPENMC_MPI MPI_Comm world {MPI_COMM_WORLD}; int err = openmc_init(argc, argv, &world); #else int err = openmc_init(argc, argv, nullptr); #endif - if (err) fatal_error(openmc_err_msg); + if (err) + fatal_error(openmc_err_msg); // create a new cell filter auto cell_filter = Filter::create(); @@ -30,7 +32,7 @@ int main(int argc, char** argv) { // add all cells to the cell filter std::vector cell_indices; for (auto& entry : openmc::model::cell_map) { - cell_indices.push_back(entry.second); + cell_indices.push_back(entry.second); } // enable distribcells offsets for all cells prepare_distribcell(&cell_indices); @@ -39,7 +41,6 @@ int main(int argc, char** argv) { std::sort(cell_indices.begin(), cell_indices.end()); cell_filter->set_cells(cell_indices); - // create a new tally auto tally = Tally::create(); std::vector filters = {cell_filter}; @@ -60,14 +61,19 @@ int main(int argc, char** argv) { } } - // set a higher temperature for only one of the lattice cells (ID is 4 in the model) + // set a higher temperature for only one of the lattice cells (ID is 4 in the + // model) model::cells[model::cell_map[4]]->set_temperature(400.0, 3, true); + // set the density of another lattice cell to 2 + model::cells[model::cell_map[4]]->set_density(2.0, 2, true); + // the summary file will be used to check that // temperatures were set correctly so clear // error output can be provided #ifdef OPENMC_MPI - if (openmc::mpi::master) openmc::write_summary(); + if (openmc::mpi::master) + openmc::write_summary(); #else openmc::write_summary(); #endif diff --git a/tests/regression_tests/cpp_driver/results_true.dat b/tests/regression_tests/cpp_driver/results_true.dat index c2b0b8d6f..26f07842c 100644 --- a/tests/regression_tests/cpp_driver/results_true.dat +++ b/tests/regression_tests/cpp_driver/results_true.dat @@ -1,13 +1,13 @@ k-combined: -1.933305E+00 1.300360E-02 +1.953962E+00 1.828426E-02 tally 1: -9.552846E+01 -1.019358E+03 -2.887973E+01 -9.308509E+01 -9.732441E+01 -1.059022E+03 -2.217326E+02 -5.486892E+03 -2.217326E+02 -5.486892E+03 +9.607953E+01 +1.031898E+03 +2.853683E+01 +9.085469E+01 +9.745011E+01 +1.058928E+03 +2.220665E+02 +5.496813E+03 +2.220665E+02 +5.496813E+03 diff --git a/tests/regression_tests/dagmc/external/main.cpp b/tests/regression_tests/dagmc/external/main.cpp index 3765cf79a..e78ab03fa 100644 --- a/tests/regression_tests/dagmc/external/main.cpp +++ b/tests/regression_tests/dagmc/external/main.cpp @@ -100,6 +100,9 @@ int main(int argc, char* argv[]) } } + // Finalize cell densities + openmc::finalize_cell_densities(); + // Run OpenMC openmc_err = openmc_run(); if (openmc_err) diff --git a/tests/regression_tests/lattice_distribrho/__init__.py b/tests/regression_tests/lattice_distribrho/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/lattice_distribrho/inputs_true.dat b/tests/regression_tests/lattice_distribrho/inputs_true.dat new file mode 100644 index 000000000..5031bea6e --- /dev/null +++ b/tests/regression_tests/lattice_distribrho/inputs_true.dat @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + 1.0 1.0 + 2 2 + -1.0 -1.0 + +1 1 +1 1 + + + + + + + + + eigenvalue + 1000 + 10 + 5 + + diff --git a/tests/regression_tests/lattice_distribrho/results_true.dat b/tests/regression_tests/lattice_distribrho/results_true.dat new file mode 100644 index 000000000..d7094f4e3 --- /dev/null +++ b/tests/regression_tests/lattice_distribrho/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +1.904471E+00 5.255549E-03 diff --git a/tests/regression_tests/lattice_distribrho/test.py b/tests/regression_tests/lattice_distribrho/test.py new file mode 100644 index 000000000..ec94fe96b --- /dev/null +++ b/tests/regression_tests/lattice_distribrho/test.py @@ -0,0 +1,51 @@ +import openmc +import pytest + +from tests.testing_harness import PyAPITestHarness + + +@pytest.fixture +def model(): + model = openmc.Model() + + uo2 = openmc.Material(name='UO2') + uo2.set_density('g/cm3', 10.0) + uo2.add_nuclide('U235', 1.0) + uo2.add_nuclide('O16', 2.0) + water = openmc.Material(name='light water') + water.add_nuclide('H1', 2.0) + water.add_nuclide('O16', 1.0) + water.set_density('g/cm3', 1.0) + water.add_s_alpha_beta('c_H_in_H2O') + model.materials.extend([uo2, water]) + + cyl = openmc.ZCylinder(r=0.4) + pin = openmc.model.pin([cyl], [uo2, water]) + d = 1.0 + + lattice = openmc.RectLattice() + lattice.lower_left = (-d, -d) + lattice.pitch = (d, d) + lattice.universes = [[pin, pin], + [pin, pin]] + box = openmc.model.RectangularPrism( + 2.0 * d, 2.0 * d, + origin=(0.0, 0.0), + boundary_type='reflective' + ) + + pin.cells[1].density = [10.0, 20.0, 10.0, 20.0] + + model.geometry = openmc.Geometry([openmc.Cell(fill=lattice, region=-box)]) + model.geometry.merge_surfaces = True + + model.settings.batches = 10 + model.settings.inactive = 5 + model.settings.particles = 1000 + + return model + + +def test_lattice_checkerboard(model): + harness = PyAPITestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/multipole/results_true.dat b/tests/regression_tests/multipole/results_true.dat index 8e82655ce..8eb75b894 100644 --- a/tests/regression_tests/multipole/results_true.dat +++ b/tests/regression_tests/multipole/results_true.dat @@ -38,5 +38,6 @@ Cell Region = -1 Rotation = None Temperature = [500. 700. 0. 800.] + Density = None Translation = None Volume = None diff --git a/tests/unit_tests/test_cell.py b/tests/unit_tests/test_cell.py index 95c8249bb..60b205818 100644 --- a/tests/unit_tests/test_cell.py +++ b/tests/unit_tests/test_cell.py @@ -126,6 +126,29 @@ def test_temperature(cell_with_lattice): c.temperature = (300., 600., 900.) +def test_densities(cell_with_lattice): + # Make sure density propagates through universes + m = openmc.Material() + s = openmc.XPlane() + c1 = openmc.Cell(fill=m, region=+s) + c2 = openmc.Cell(fill=m, region=-s) + u1 = openmc.Universe(cells=[c1, c2]) + c = openmc.Cell(fill=u1) + + c.density = 1. + assert c1.density == 1. + assert c2.density == 1. + with pytest.raises(ValueError): + c.density = -1. + c.density = None + assert c1.density == None + assert c2.density == None + + # distributed density + cells, _, _, _ = cell_with_lattice + c = cells[0] + c.density = (1., 2., 3.) + def test_rotation(): u = openmc.Universe() c = openmc.Cell(fill=u) diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index 8ab35335f..8080ce894 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -159,6 +159,29 @@ def test_properties_temperature(lib_init): assert cell.get_temperature() == pytest.approx(200.0) +def test_cell_density(lib_init): + cell = openmc.lib.cells[1] + cell.set_density(1.5, 0) + assert cell.get_density(0) == pytest.approx(1.5) + cell.set_density(2.0) + assert cell.get_density() == pytest.approx(2.0) + + +def test_properties_cell_density(lib_init): + # Cell density should be 2.0 from above test + cell = openmc.lib.cells[1] + assert cell.get_density() == pytest.approx(2.0) + + # Export properties and change density + openmc.lib.export_properties('properties.h5') + cell.set_density(3.0) + assert cell.get_density() == pytest.approx(3.0) + + # Import properties and check that density is restored + openmc.lib.import_properties('properties.h5') + assert cell.get_density() == pytest.approx(2.0) + + def test_new_cell(lib_init): with pytest.raises(exc.AllocationError): openmc.lib.Cell(1) diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 6e4dec00f..12f0df6c9 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -251,10 +251,11 @@ def test_import_properties(run_in_tmpdir, mpi_intracomm): model = openmc.examples.pwr_pin_cell() model.init_lib(output=False, intracomm=mpi_intracomm) - # Change fuel temperature and density and export properties + # Change cell fuel temperature, density, material density and export properties cell = openmc.lib.cells[1] cell.set_temperature(600.0) cell.fill.set_density(5.0, 'g/cm3') + cell.set_density(10.0) openmc.lib.export_properties(output=False) # Import properties to existing model @@ -264,9 +265,11 @@ def test_import_properties(run_in_tmpdir, mpi_intracomm): # 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.fill.get_mass_density() == pytest.approx(5.0) # Now C assert openmc.lib.cells[1].get_temperature() == 600. + assert openmc.lib.cells[1].get_density() == pytest.approx(10.0, 1e-5) assert openmc.lib.materials[1].get_density('g/cm3') == pytest.approx(5.0) # Clear the C API @@ -283,6 +286,7 @@ def test_import_properties(run_in_tmpdir, mpi_intracomm): ) 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.fill.get_mass_density() == pytest.approx(5.0) From 007ac8148bcda4911edb0191df762fa7ef3c5b8e Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 18 Sep 2025 23:27:41 -0500 Subject: [PATCH 410/671] Allow newer Sphinx version and fix docbuild warnings (#3571) --- docs/source/conf.py | 2 -- openmc/deplete/abc.py | 9 +++++---- openmc/deplete/integrators.py | 1 - openmc/mgxs/mgxs.py | 5 +++-- openmc/plots.py | 6 ++++-- pyproject.toml | 4 ++-- 6 files changed, 14 insertions(+), 13 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 8aeff5cdc..826c20022 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -121,9 +121,7 @@ pygments_style = 'tango' # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages -import sphinx_rtd_theme html_theme = 'sphinx_rtd_theme' -html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] html_baseurl = "https://docs.openmc.org/en/stable/" html_logo = '_images/openmc_logo.png' diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index e6d4c1512..c5a6219c7 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -12,6 +12,7 @@ from copy import deepcopy from inspect import signature from numbers import Real, Integral from pathlib import Path +from textwrap import dedent import time from typing import Optional, Union, Sequence from warnings import warn @@ -526,7 +527,7 @@ class Integrator(ABC): r"""Abstract class for solving the time-integration for depletion """ - _params = r""" + _params = dedent(r""" Parameters ---------- operator : openmc.deplete.abc.TransportOperator @@ -617,7 +618,7 @@ class Integrator(ABC): .. versionadded:: 0.15.3 - """ + """) def __init__( self, @@ -1020,7 +1021,7 @@ class SIIntegrator(Integrator): the number of particles used in initial transport calculation """ - _params = r""" + _params = dedent(r""" Parameters ---------- operator : openmc.deplete.abc.TransportOperator @@ -1108,7 +1109,7 @@ class SIIntegrator(Integrator): .. versionadded:: 0.12 - """ + """) def __init__( self, diff --git a/openmc/deplete/integrators.py b/openmc/deplete/integrators.py index 000cb2c41..7c543a6cb 100644 --- a/openmc/deplete/integrators.py +++ b/openmc/deplete/integrators.py @@ -22,7 +22,6 @@ class PredictorIntegrator(Integrator): .. math:: \mathbf{n}_{i+1} = \exp\left(h\mathbf{A}(\mathbf{n}_i) \right) \mathbf{n}_i - """ _num_stages = 1 diff --git a/openmc/mgxs/mgxs.py b/openmc/mgxs/mgxs.py index 1bc7f9387..b8f2b8d3f 100644 --- a/openmc/mgxs/mgxs.py +++ b/openmc/mgxs/mgxs.py @@ -2,6 +2,7 @@ import copy from numbers import Integral import os import warnings +from textwrap import dedent import h5py import numpy as np @@ -164,7 +165,7 @@ class MGXS: """ - _params = """ + _params = dedent(""" Parameters ---------- domain : openmc.Material or openmc.Cell or openmc.Universe or openmc.RegularMesh @@ -251,7 +252,7 @@ class MGXS: .. versionadded:: 0.13.1 - """ + """) # Store whether or not the number density should be removed for microscopic # values of this data diff --git a/openmc/plots.py b/openmc/plots.py index 072a9a319..e9130f0be 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -1,6 +1,7 @@ from collections.abc import Iterable, Mapping from numbers import Integral, Real from pathlib import Path +from textwrap import dedent import h5py import lxml.etree as ET @@ -167,7 +168,8 @@ _SVG_COLORS = { 'yellowgreen': (154, 205, 50) } -_PLOT_PARAMS = """ +_PLOT_PARAMS = dedent("""\ + Parameters ---------- origin : iterable of float @@ -249,7 +251,7 @@ _PLOT_PARAMS = """ ------- matplotlib.axes.Axes Axes containing resulting image -""" +""") # Decorator for consistently adding plot parameters to docstrings (Model.plot, diff --git a/pyproject.toml b/pyproject.toml index 6e8ed798e..7705de8da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,12 +41,12 @@ dependencies = [ [project.optional-dependencies] depletion-mpi = ["mpi4py"] docs = [ - "sphinx==5.0.2", + "sphinx", "sphinxcontrib-katex", "sphinx-numfig", "jupyter", "sphinxcontrib-svg2pdfconverter", - "sphinx-rtd-theme==1.0.0" + "sphinx-rtd-theme" ] test = ["packaging", "pytest", "pytest-cov", "colorama", "openpyxl"] ci = ["cpp-coveralls", "coveralls"] From ecb0a3361f600594984b72ce37e33cd7650497b6 Mon Sep 17 00:00:00 2001 From: Ilham Variansyah Date: Fri, 19 Sep 2025 15:10:08 +0700 Subject: [PATCH 411/671] Combing for fission site sampling, and delayed neutron emission time (#2992) Co-authored-by: Gavin Ridley Co-authored-by: Paul Romano --- docs/source/methods/neutron_physics.rst | 19 +- examples/pincell_pulsed/run_pulse.py | 101 + include/openmc/settings.h | 1 + openmc/settings.py | 27 + src/eigenvalue.cpp | 106 +- src/finalize.cpp | 1 + src/physics.cpp | 20 +- src/physics_mg.cpp | 20 +- src/settings.cpp | 6 + src/tallies/tally_scoring.cpp | 3 +- .../adj_cell_rotation/results_true.dat | 2 +- .../albedo_box/results_true.dat | 2 +- .../asymmetric_lattice/results_true.dat | 2 +- .../cmfd_feed/results_true.dat | 612 +-- .../cmfd_feed_2g/results_true.dat | 538 +-- .../results_true.dat | 562 +-- .../cmfd_feed_ng/results_true.dat | 680 ++-- .../cmfd_feed_rectlin/results_true.dat | 778 ++-- .../cmfd_feed_ref_d/results_true.dat | 562 +-- .../cmfd_feed_rolling_window/results_true.dat | 562 +-- .../cmfd_nofeed/results_true.dat | 610 +-- .../cmfd_restart/results_true.dat | 612 +-- .../complex_cell/results_true.dat | 16 +- .../confidence_intervals/results_true.dat | 6 +- .../cpp_driver/results_true.dat | 22 +- .../dagmc/external/results_true.dat | 6 +- .../dagmc/legacy/results_true.dat | 6 +- .../dagmc/refl/results_true.dat | 6 +- .../dagmc/universes/inputs_true.dat | 8 + .../dagmc/universes/results_true.dat | 18 +- .../regression_tests/dagmc/universes/test.py | 134 +- .../regression_tests/density/results_true.dat | 2 +- .../test_reference_coupled_days.h5 | Bin 36048 -> 36048 bytes .../test_reference_coupled_hours.h5 | Bin 36048 -> 36048 bytes .../test_reference_coupled_minutes.h5 | Bin 36048 -> 36048 bytes .../test_reference_coupled_months.h5 | Bin 36048 -> 36048 bytes .../test_reference_fission_q.h5 | Bin 36048 -> 36048 bytes .../test_reference_source_rate.h5 | Bin 36048 -> 36048 bytes .../ref_depletion_with_ext_source.h5 | Bin 37328 -> 37328 bytes .../ref_depletion_with_feed.h5 | Bin 37328 -> 37328 bytes .../ref_depletion_with_removal.h5 | Bin 37328 -> 37328 bytes .../ref_depletion_with_transfer.h5 | Bin 37328 -> 37328 bytes .../ref_no_depletion_only_feed.h5 | Bin 37328 -> 37328 bytes .../ref_no_depletion_only_removal.h5 | Bin 37328 -> 37328 bytes .../ref_no_depletion_with_ext_source.h5 | Bin 37328 -> 37328 bytes .../ref_no_depletion_with_transfer.h5 | Bin 37328 -> 37328 bytes .../deplete_with_transfer_rates/test.py | 3 +- .../last_step_reference_materials.xml | 1280 +++---- .../deplete_with_transport/test_reference.h5 | Bin 163736 -> 163736 bytes .../diff_tally/results_true.dat | 220 +- .../distribmat/results_true.dat | 2 +- .../eigenvalue_genperbatch/results_true.dat | 6 +- .../eigenvalue_no_inactive/results_true.dat | 2 +- .../energy_grid/results_true.dat | 2 +- .../energy_laws/results_true.dat | 2 +- .../regression_tests/entropy/results_true.dat | 20 +- .../filter_cellfrom/results_true.dat | 62 +- .../filter_cellinstance/results_true.dat | 162 +- .../case-3/results_true.dat | 2 +- .../case-4/results_true.dat | 6 +- .../filter_energyfun/results_true.dat | 10 +- .../filter_mesh/results_true.dat | 2 +- .../filter_musurface/results_true.dat | 10 +- .../filter_translations/results_true.dat | 674 ++-- tests/regression_tests/ifp/results_true.dat | 14 +- .../infinite_cell/results_true.dat | 2 +- .../iso_in_lab/results_true.dat | 2 +- .../regression_tests/lattice/results_true.dat | 2 +- .../lattice_distribmat/False/results_true.dat | 2 +- .../lattice_distribmat/True/results_true.dat | 2 +- .../lattice_distribrho/results_true.dat | 2 +- .../lattice_hex/results_true.dat | 2 +- .../lattice_hex_coincident/results_true.dat | 2 +- .../lattice_hex_x/results_true.dat | 2 +- .../lattice_multiple/results_true.dat | 2 +- .../lattice_rotated/results_true.dat | 2 +- .../mg_basic/results_true.dat | 2 +- .../mg_basic_delayed/results_true.dat | 2 +- .../mg_convert/results_true.dat | 24 +- tests/regression_tests/mg_convert/test.py | 6 +- .../mg_legendre/results_true.dat | 2 +- .../mg_max_order/results_true.dat | 2 +- .../mg_survival_biasing/results_true.dat | 2 +- .../mg_tallies/results_true.dat | 2482 ++++++------ .../mg_temperature/results_true.dat | 20 +- .../mg_temperature_multi/results_true.dat | 10 +- .../mgxs_library_ce_to_mg/results_true.dat | 2 +- .../results_true.dat | 2 +- .../mgxs_library_condense/results_true.dat | 572 +-- .../mgxs_library_correction/results_true.dat | 72 +- .../mgxs_library_distribcell/results_true.dat | 142 +- .../mgxs_library_hdf5/results_true.dat | 266 +- .../mgxs_library_histogram/results_true.dat | 660 ++-- .../mgxs_library_mesh/results_true.dat | 548 +-- .../mgxs_library_no_nuclides/results_true.dat | 782 ++-- .../mgxs_library_nuclides/results_true.dat | 2 +- .../results_true.dat | 2 +- .../test_reference_materials_direct.csv | 8 +- .../microxs/test_reference_materials_flux.csv | 8 +- .../microxs/test_reference_mesh_direct.csv | 8 +- .../microxs/test_reference_mesh_flux.csv | 8 +- .../multipole/results_true.dat | 54 +- .../regression_tests/output/results_true.dat | 2 +- .../particle_restart_eigval/results_true.dat | 10 +- .../particle_restart_eigval/settings.xml | 2 +- .../particle_restart_eigval/test.py | 2 +- .../periodic/results_true.dat | 2 +- .../periodic_6fold/results_true.dat | 2 +- .../periodic_hex/results_true.dat | 2 +- .../results_true.dat | 58 +- .../ptables_off/results_true.dat | 2 +- .../quadric_surfaces/results_true.dat | 2 +- .../results_true.dat | 2 +- .../infinite_medium/results_true.dat | 2 +- .../material_wise/results_true.dat | 2 +- .../stochastic_slab/results_true.dat | 2 +- .../results_true.dat | 2 +- .../linear/results_true.dat | 4 +- .../flat/results_true.dat | 6 +- .../linear_xy/results_true.dat | 14 +- .../results_true.dat | 10 +- .../random_ray_k_eff/results_true.dat | 14 +- .../random_ray_k_eff_mesh/results_true.dat | 18 +- .../random_ray_linear/linear/results_true.dat | 6 +- .../hybrid/results_true.dat | 4 +- .../reflective_plane/results_true.dat | 2 +- .../resonance_scattering/results_true.dat | 2 +- .../rotation/results_true.dat | 2 +- .../salphabeta/results_true.dat | 2 +- .../score_current/results_true.dat | 3366 ++++++++--------- tests/regression_tests/seed/results_true.dat | 2 +- .../regression_tests/source/results_true.dat | 2 +- .../source_file/results_true.dat | 2 +- .../source_mcpl_file/results_true.dat | 2 +- .../sourcepoint_batch/results_true.dat | 4 +- .../sourcepoint_latest/results_true.dat | 2 +- .../sourcepoint_restart/results_true.dat | 1578 ++++---- .../statepoint_batch/__init__.py | 0 .../statepoint_batch/geometry.xml | 8 - .../statepoint_batch/materials.xml | 9 - .../statepoint_batch/results_true.dat | 2 - .../statepoint_batch/settings.xml | 17 - .../regression_tests/statepoint_batch/test.py | 18 - .../statepoint_restart/results_true.dat | 2306 +++++------ .../statepoint_sourcesep/results_true.dat | 2 +- .../regression_tests/stride/results_true.dat | 2 +- .../surface_source/surface_source_true.h5 | Bin 106144 -> 106144 bytes .../surface_source/surface_source_true.mcpl | Bin 36071 -> 36120 bytes .../case-01/results_true.dat | 2 +- .../case-01/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-02/results_true.dat | 2 +- .../case-02/surface_source_true.h5 | Bin 12752 -> 13896 bytes .../case-03/results_true.dat | 2 +- .../case-03/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-04/results_true.dat | 2 +- .../case-04/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-05/results_true.dat | 2 +- .../case-05/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-06/results_true.dat | 2 +- .../case-06/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-07/results_true.dat | 2 +- .../case-07/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-08/results_true.dat | 2 +- .../case-08/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-09/results_true.dat | 2 +- .../case-09/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-10/results_true.dat | 2 +- .../case-10/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-11/results_true.dat | 2 +- .../case-11/surface_source_true.h5 | Bin 6408 -> 5160 bytes .../case-12/results_true.dat | 2 +- .../case-12/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-13/results_true.dat | 2 +- .../case-13/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-14/results_true.dat | 2 +- .../case-14/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-15/results_true.dat | 2 +- .../case-15/surface_source_true.h5 | Bin 2144 -> 2144 bytes .../case-16/results_true.dat | 2 +- .../case-16/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-17/results_true.dat | 2 +- .../case-17/surface_source_true.h5 | Bin 2144 -> 2144 bytes .../case-18/results_true.dat | 2 +- .../case-18/surface_source_true.h5 | Bin 2144 -> 2144 bytes .../case-19/results_true.dat | 2 +- .../case-19/surface_source_true.h5 | Bin 2144 -> 2144 bytes .../case-20/results_true.dat | 2 +- .../case-20/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-21/results_true.dat | 2 +- .../case-21/surface_source_true.h5 | Bin 2144 -> 2144 bytes .../case-a01/results_true.dat | 2 +- .../case-a01/surface_source_true.h5 | Bin 6616 -> 6720 bytes .../case-d01/results_true.dat | 2 +- .../case-d01/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-d02/results_true.dat | 2 +- .../case-d02/surface_source_true.h5 | Bin 31472 -> 33344 bytes .../case-d03/results_true.dat | 2 +- .../case-d03/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-d04/results_true.dat | 2 +- .../case-d04/surface_source_true.h5 | Bin 31472 -> 33344 bytes .../case-d05/results_true.dat | 2 +- .../case-d05/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-d06/results_true.dat | 2 +- .../case-d06/surface_source_true.h5 | Bin 29496 -> 30744 bytes .../case-d07/results_true.dat | 2 +- .../case-d07/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-d08/results_true.dat | 2 +- .../case-d08/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-e01/results_true.dat | 2 +- .../case-e01/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-e02/results_true.dat | 2 +- .../case-e02/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-e03/results_true.dat | 2 +- .../case-e03/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../surface_tally/results_true.dat | 82 +- .../survival_biasing/results_true.dat | 34 +- .../regression_tests/tallies/results_true.dat | 2 +- .../tally_aggregation/results_true.dat | 194 +- .../tally_arithmetic/results_true.dat | 98 +- .../tally_assumesep/results_true.dat | 14 +- .../tally_nuclides/results_true.dat | 50 +- .../tally_slice_merge/results_true.dat | 104 +- tests/regression_tests/torus/results_true.dat | 2 +- tests/regression_tests/trace/results_true.dat | 2 +- .../track_output/results_true.dat | 144 +- .../translation/results_true.dat | 2 +- .../trigger_batch_interval/results_true.dat | 50 +- .../results_true.dat | 50 +- .../trigger_no_status/results_true.dat | 50 +- .../inputs_true.dat | 2 +- .../results_true.dat | 6 +- .../trigger_statepoint_restart/test.py | 10 +- .../trigger_tallies/results_true.dat | 50 +- tests/regression_tests/triso/results_true.dat | 2 +- .../uniform_fs/results_true.dat | 2 +- .../universe/results_true.dat | 2 +- .../white_plane/results_true.dat | 2 +- tests/unit_tests/test_deplete_resultslist.py | 20 +- tests/unit_tests/test_lib.py | 17 +- tests/unit_tests/test_settings.py | 2 + tests/unit_tests/test_statepoint_batches.py | 26 + 241 files changed, 11472 insertions(+), 11352 deletions(-) create mode 100644 examples/pincell_pulsed/run_pulse.py delete mode 100644 tests/regression_tests/statepoint_batch/__init__.py delete mode 100644 tests/regression_tests/statepoint_batch/geometry.xml delete mode 100644 tests/regression_tests/statepoint_batch/materials.xml delete mode 100644 tests/regression_tests/statepoint_batch/results_true.dat delete mode 100644 tests/regression_tests/statepoint_batch/settings.xml delete mode 100644 tests/regression_tests/statepoint_batch/test.py create mode 100644 tests/unit_tests/test_statepoint_batches.py diff --git a/docs/source/methods/neutron_physics.rst b/docs/source/methods/neutron_physics.rst index fe8b8ad85..2b797e3db 100644 --- a/docs/source/methods/neutron_physics.rst +++ b/docs/source/methods/neutron_physics.rst @@ -290,7 +290,10 @@ create and store fission sites for the following generation. First, the average number of prompt and delayed neutrons must be determined to decide whether the secondary neutrons will be prompt or delayed. This is important because delayed neutrons have a markedly different spectrum from prompt neutrons, one that has a -lower average energy of emission. The total number of neutrons emitted +lower average energy of emission. Furthermore, in simulations where tracking +time of neutrons is important, we need to consider the emission time delay of +the secondary neutrons, which is dependent on the decay constant of the +delayed neutron precursor. The total number of neutrons emitted :math:`\nu_t` is given as a function of incident energy in the ENDF format. Two representations exist for :math:`\nu_t`. The first is a polynomial of order :math:`N` with coefficients :math:`c_0,c_1,\dots,c_N`. If :math:`\nu_t` has this @@ -306,8 +309,8 @@ interpolation law. The number of prompt neutrons released per fission event :math:`\nu_p` is also given as a function of incident energy and can be specified in a polynomial or tabular format. The number of delayed neutrons released per fission event :math:`\nu_d` can only be specified in a tabular -format. In practice, we only need to determine :math:`nu_t` and -:math:`nu_d`. Once these have been determined, we can calculated the delayed +format. In practice, we only need to determine :math:`\nu_t` and +:math:`\nu_d`. Once these have been determined, we can calculate the delayed neutron fraction .. math:: @@ -335,8 +338,14 @@ neutrons. Otherwise, we produce :math:`\lfloor \nu \rfloor + 1` neutrons. Then, for each fission site produced, we sample the outgoing angle and energy according to the algorithms given in :ref:`sample-angle` and :ref:`sample-energy` respectively. If the neutron is to be born delayed, then -there is an extra step of sampling a delayed neutron precursor group since they -each have an associated secondary energy distribution. +there is an extra step of sampling a delayed neutron precursor group to get the +associated secondary energy distribution and the decay constant +:math:`\lambda`, which is needed to sample the emission delay time :math:`t_d`: + +.. math:: + :label: sample-delay-time + + t_d = -\frac{\ln \xi}{\lambda}. The sampled outgoing angle and energy of fission neutrons along with the position of the collision site are stored in an array called the fission diff --git a/examples/pincell_pulsed/run_pulse.py b/examples/pincell_pulsed/run_pulse.py new file mode 100644 index 000000000..b6a61ad3d --- /dev/null +++ b/examples/pincell_pulsed/run_pulse.py @@ -0,0 +1,101 @@ +import matplotlib.pyplot as plt +import numpy as np +import openmc + +############################################################################### +# Create materials for the problem + +uo2 = openmc.Material(name="UO2 fuel at 2.4% wt enrichment") +uo2.set_density("g/cm3", 10.29769) +uo2.add_element("U", 1.0, enrichment=2.4) +uo2.add_element("O", 2.0) + +helium = openmc.Material(name="Helium for gap") +helium.set_density("g/cm3", 0.001598) +helium.add_element("He", 2.4044e-4) + +zircaloy = openmc.Material(name="Zircaloy 4") +zircaloy.set_density("g/cm3", 6.55) +zircaloy.add_element("Sn", 0.014, "wo") +zircaloy.add_element("Fe", 0.00165, "wo") +zircaloy.add_element("Cr", 0.001, "wo") +zircaloy.add_element("Zr", 0.98335, "wo") + +borated_water = openmc.Material(name="Borated water") +borated_water.set_density("g/cm3", 0.740582) +borated_water.add_element("B", 2.0e-4) # 3x the original pincell +borated_water.add_element("H", 5.0e-2) +borated_water.add_element("O", 2.4e-2) +borated_water.add_s_alpha_beta("c_H_in_H2O") + +############################################################################### +# Define problem geometry + +# Create cylindrical surfaces +fuel_or = openmc.ZCylinder(r=0.39218, name="Fuel OR") +clad_ir = openmc.ZCylinder(r=0.40005, name="Clad IR") +clad_or = openmc.ZCylinder(r=0.45720, name="Clad OR") + +# Create a region represented as the inside of a rectangular prism +pitch = 1.25984 +box = openmc.model.RectangularPrism(pitch, pitch, boundary_type="reflective") + +# Create cells, mapping materials to regions +fuel = openmc.Cell(fill=uo2, region=-fuel_or) +gap = openmc.Cell(fill=helium, region=+fuel_or & -clad_ir) +clad = openmc.Cell(fill=zircaloy, region=+clad_ir & -clad_or) +water = openmc.Cell(fill=borated_water, region=+clad_or & -box) + +# Create a model and assign geometry +model = openmc.Model() +model.geometry = openmc.Geometry([fuel, gap, clad, water]) + +############################################################################### +# Define problem settings + +# Set the mode +model.settings.run_mode = "fixed source" + +# Indicate how many batches and particles to run +model.settings.batches = 10 +model.settings.particles = 10000 + +# Set time cutoff (we only care about t < 100 seconds, see tally below) +model.settings.cutoff = {"time_neutron": 100} + +# Create the neutron pulse source (by default, isotropic direction, t=0) +space = openmc.stats.Point() # At the origin (0, 0, 0) +energy = openmc.stats.delta_function(14.1e6) # At 14.1 MeV +model.settings.source = openmc.IndependentSource(space=space, energy=energy) + +############################################################################### +# Define tallies + +# Create time filter +t_grid = np.insert(np.logspace(-6, 2, 100), 0, 0.0) +time_filter = openmc.TimeFilter(t_grid) + +# Tally for total neutron density in time +density_tally = openmc.Tally(name="Density") +density_tally.filters = [time_filter] +density_tally.scores = ["inverse-velocity"] + +# Add tallies to model +model.tallies = openmc.Tallies([density_tally]) + + +# Run the model +model.run(apply_tally_results=True) + +# Bin-averaged result +density_mean = density_tally.mean.ravel() / np.diff(t_grid) + +# Plot particle density versus time +fig, ax = plt.subplots() +ax.stairs(density_mean, t_grid) +ax.set_xscale("log") +ax.set_yscale("log") +ax.set_xlabel("Time [s]") +ax.set_ylabel("Total density") +ax.grid() +plt.show() diff --git a/include/openmc/settings.h b/include/openmc/settings.h index 069d94c3b..999b1b83f 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -150,6 +150,7 @@ extern double source_rejection_fraction; //!< Minimum fraction of source sites extern int max_history_splits; //!< maximum number of particle splits for weight windows +extern int max_secondaries; //!< maximum number of secondaries in the bank extern int64_t ssw_max_particles; //!< maximum number of particles to be //!< banked on surfaces per process extern int64_t ssw_max_files; //!< maximum number of surface source files diff --git a/openmc/settings.py b/openmc/settings.py index ce3743ff5..7bbc31c19 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -128,6 +128,10 @@ class Settings: Maximum number of times a particle can split during a history .. versionadded:: 0.13 + max_secondaries : int + Maximum secondary bank size + + .. versionadded:: 0.15.3 max_tracks : int Maximum number of tracks written to a track file (per MPI process). @@ -431,6 +435,7 @@ class Settings: self._weight_window_checkpoints = {} self._max_history_splits = None self._max_tracks = None + self._max_secondaries = None self._use_decay_photons = None self._random_ray = {} @@ -1137,6 +1142,16 @@ class Settings: cv.check_greater_than('max particle splits', value, 0) self._max_history_splits = value + @property + def max_secondaries(self) -> int: + return self._max_secondaries + + @max_secondaries.setter + def max_secondaries(self, value: int): + cv.check_type('maximum secondary bank size', value, Integral) + cv.check_greater_than('max secondary bank size', value, 0) + self._max_secondaries = value + @property def max_tracks(self) -> int: return self._max_tracks @@ -1673,6 +1688,11 @@ class Settings: elem = ET.SubElement(root, "max_history_splits") elem.text = str(self._max_history_splits) + def _create_max_secondaries_subelement(self, root): + if self._max_secondaries is not None: + elem = ET.SubElement(root, "max_secondaries") + elem.text = str(self._max_secondaries) + def _create_max_tracks_subelement(self, root): if self._max_tracks is not None: elem = ET.SubElement(root, "max_tracks") @@ -2073,6 +2093,11 @@ class Settings: if text is not None: self.max_history_splits = int(text) + def _max_secondaries_from_xml_element(self, root): + text = get_text(root, 'max_secondaries') + if text is not None: + self.max_secondaries = int(text) + def _max_tracks_from_xml_element(self, root): text = get_text(root, 'max_tracks') if text is not None: @@ -2194,6 +2219,7 @@ class Settings: self._create_weight_window_checkpoints_subelement(element) self._create_max_history_splits_subelement(element) self._create_max_tracks_subelement(element) + self._create_max_secondaries_subelement(element) self._create_random_ray_subelement(element, mesh_memo) self._create_use_decay_photons_subelement(element) self._create_source_rejection_fraction_subelement(element) @@ -2302,6 +2328,7 @@ class Settings: settings._weight_window_checkpoints_from_xml_element(elem) settings._max_history_splits_from_xml_element(elem) settings._max_tracks_from_xml_element(elem) + settings._max_secondaries_from_xml_element(elem) settings._random_ray_from_xml_element(elem) settings._use_decay_photons_from_xml_element(elem) settings._source_rejection_fraction_from_xml_element(elem) diff --git a/src/eigenvalue.cpp b/src/eigenvalue.cpp index 2685bbe98..a2120a006 100644 --- a/src/eigenvalue.cpp +++ b/src/eigenvalue.cpp @@ -127,30 +127,8 @@ void synchronize_bank() "No fission sites banked on MPI rank " + std::to_string(mpi::rank)); } - // Make sure all processors start at the same point for random sampling. Then - // skip ahead in the sequence using the starting index in the 'global' - // fission bank for each processor. - - int64_t id = simulation::total_gen + overall_generation(); - uint64_t seed = init_seed(id, STREAM_TRACKING); - advance_prn_seed(start, &seed); - - // Determine how many fission sites we need to sample from the source bank - // and the probability for selecting a site. - - int64_t sites_needed; - if (total < settings::n_particles) { - sites_needed = settings::n_particles % total; - } else { - sites_needed = settings::n_particles; - } - double p_sample = static_cast(sites_needed) / total; - simulation::time_bank_sample.start(); - // ========================================================================== - // SAMPLE N_PARTICLES FROM FISSION BANK AND PLACE IN TEMP_SITES - // Allocate temporary source bank -- we don't really know how many fission // sites were created, so overallocate by a factor of 3 int64_t index_temp = 0; @@ -165,33 +143,38 @@ void synchronize_bank() temp_delayed_groups, temp_lifetimes, 3 * simulation::work_per_rank); } - for (int64_t i = 0; i < simulation::fission_bank.size(); i++) { - const auto& site = simulation::fission_bank[i]; + // ========================================================================== + // SAMPLE N_PARTICLES FROM FISSION BANK AND PLACE IN TEMP_SITES - // If there are less than n_particles particles banked, automatically add - // int(n_particles/total) sites to temp_sites. For example, if you need - // 1000 and 300 were banked, this would add 3 source sites per banked site - // and the remaining 100 would be randomly sampled. - if (total < settings::n_particles) { - for (int64_t j = 1; j <= settings::n_particles / total; ++j) { - temp_sites[index_temp] = site; - if (settings::ifp_on) { - copy_ifp_data_from_fission_banks( - i, temp_delayed_groups[index_temp], temp_lifetimes[index_temp]); - } - ++index_temp; - } - } + // We use Uniform Combing method to exactly get the targeted particle size + // [https://doi.org/10.1080/00295639.2022.2091906] - // Randomly sample sites needed - if (prn(&seed) < p_sample) { - temp_sites[index_temp] = site; - if (settings::ifp_on) { - copy_ifp_data_from_fission_banks( - i, temp_delayed_groups[index_temp], temp_lifetimes[index_temp]); - } - ++index_temp; + // Make sure all processors use the same random number seed. + int64_t id = simulation::total_gen + overall_generation(); + uint64_t seed = init_seed(id, STREAM_TRACKING); + + // Comb specification + double teeth_distance = static_cast(total) / settings::n_particles; + double teeth_offset = prn(&seed) * teeth_distance; + + // First and last hitting tooth + int64_t end = start + simulation::fission_bank.size(); + int64_t tooth_start = std::ceil((start - teeth_offset) / teeth_distance); + int64_t tooth_end = std::floor((end - teeth_offset) / teeth_distance) + 1; + + // Locally comb particles in fission_bank + double tooth = tooth_start * teeth_distance + teeth_offset; + for (int64_t i = tooth_start; i < tooth_end; i++) { + int64_t idx = std::floor(tooth) - start; + temp_sites[index_temp] = simulation::fission_bank[idx]; + if (settings::ifp_on) { + copy_ifp_data_from_fission_banks( + idx, temp_delayed_groups[index_temp], temp_lifetimes[index_temp]); } + ++index_temp; + + // Next tooth + tooth += teeth_distance; } // At this point, the sampling of source sites is done and now we need to @@ -217,37 +200,6 @@ void synchronize_bank() finish = index_temp; #endif - // Now that the sampling is complete, we need to ensure that we have exactly - // n_particles source sites. The way this is done in a reproducible manner is - // to adjust only the source sites on the last processor. - - if (mpi::rank == mpi::n_procs - 1) { - if (finish > settings::n_particles) { - // If we have extra sites sampled, we will simply discard the extra - // ones on the last processor - index_temp = settings::n_particles - start; - - } else if (finish < settings::n_particles) { - // If we have too few sites, repeat sites from the very end of the - // fission bank - sites_needed = settings::n_particles - finish; - // TODO: sites_needed > simulation::fission_bank.size() or other test to - // make sure we don't need info from other proc - for (int i = 0; i < sites_needed; ++i) { - int i_bank = simulation::fission_bank.size() - sites_needed + i; - temp_sites[index_temp] = simulation::fission_bank[i_bank]; - if (settings::ifp_on) { - copy_ifp_data_from_fission_banks(i_bank, - temp_delayed_groups[index_temp], temp_lifetimes[index_temp]); - } - ++index_temp; - } - } - - // the last processor should not be sending sites to right - finish = simulation::work_index[mpi::rank + 1]; - } - simulation::time_bank_sample.stop(); simulation::time_bank_sendrecv.start(); diff --git a/src/finalize.cpp b/src/finalize.cpp index e38b0b251..25b471a8b 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -92,6 +92,7 @@ int openmc_finalize() settings::max_lost_particles = 10; settings::max_order = 0; settings::max_particles_in_flight = 100000; + settings::max_secondaries = 10000; settings::max_particle_events = 1'000'000; settings::max_history_splits = 10'000'000; settings::max_tracks = 1000; diff --git a/src/physics.cpp b/src/physics.cpp index 8ad1b99d7..f667fd586 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -115,7 +115,7 @@ void sample_neutron_reaction(Particle& p) // Make sure particle population doesn't grow out of control for // subcritical multiplication problems. - if (p.secondary_bank().size() >= 10000) { + if (p.secondary_bank().size() >= settings::max_secondaries) { fatal_error( "The secondary particle bank appears to be growing without " "bound. You are likely running a subcritical multiplication problem " @@ -210,13 +210,23 @@ void create_fission_sites(Particle& p, int i_nuclide, const Reaction& rx) site.particle = ParticleType::neutron; site.time = p.time(); site.wgt = 1. / weight; - site.parent_id = p.id(); - site.progeny_id = p.n_progeny()++; site.surf_id = 0; // Sample delayed group and angle/energy for fission reaction sample_fission_neutron(i_nuclide, rx, &site, p); + // Reject site if it exceeds time cutoff + if (site.delayed_group > 0) { + double t_cutoff = settings::time_cutoff[static_cast(site.particle)]; + if (site.time > t_cutoff) { + continue; + } + } + + // Set parent and progeny IDs + site.parent_id = p.id(); + site.progeny_id = p.n_progeny()++; + // Store fission site in bank if (use_fission_bank) { int64_t idx = simulation::fission_bank.thread_safe_append(site); @@ -1069,6 +1079,10 @@ void sample_fission_neutron( // set the delayed group for the particle born from fission site->delayed_group = group; + // Sample time of emission based on decay constant of precursor + double decay_rate = rx.products_[site->delayed_group].decay_rate_; + site->time -= std::log(prn(p.current_seed())) / decay_rate; + } else { // ==================================================================== // PROMPT NEUTRON SAMPLED diff --git a/src/physics_mg.cpp b/src/physics_mg.cpp index 5ebef9c14..4c28cb179 100644 --- a/src/physics_mg.cpp +++ b/src/physics_mg.cpp @@ -139,8 +139,6 @@ void create_fission_sites(Particle& p) site.particle = ParticleType::neutron; site.time = p.time(); site.wgt = 1. / weight; - site.parent_id = p.id(); - site.progeny_id = p.n_progeny()++; // Sample the cosine of the angle, assuming fission neutrons are emitted // isotropically @@ -165,6 +163,24 @@ void create_fission_sites(Particle& p) // of the code, 0 is prompt. site.delayed_group = dg + 1; + // If delayed product production, sample time of emission + if (dg != -1) { + auto& macro_xs = data::mg.macro_xs_[p.material()]; + double decay_rate = + macro_xs.get_xs(MgxsType::DECAY_RATE, 0, nullptr, nullptr, &dg, 0, 0); + site.time -= std::log(prn(p.current_seed())) / decay_rate; + + // Reject site if it exceeds time cutoff + double t_cutoff = settings::time_cutoff[static_cast(site.particle)]; + if (site.time > t_cutoff) { + continue; + } + } + + // Set parent and progeny ID + site.parent_id = p.id(); + site.progeny_id = p.n_progeny()++; + // Store fission site in bank if (use_fission_bank) { int64_t idx = simulation::fission_bank.thread_safe_append(site); diff --git a/src/settings.cpp b/src/settings.cpp index afa42a3c5..03d42bb9d 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -114,6 +114,7 @@ int max_order {0}; int n_log_bins {8000}; int n_batches; int n_max_batches; +int max_secondaries {10000}; int max_history_splits {10'000'000}; int max_tracks {1000}; ResScatMethod res_scat_method {ResScatMethod::rvs}; @@ -1144,6 +1145,11 @@ void read_settings_xml(pugi::xml_node root) weight_windows_on = get_node_value_bool(root, "weight_windows_on"); } + if (check_for_node(root, "max_secondaries")) { + settings::max_secondaries = + std::stoi(get_node_value(root, "max_secondaries")); + } + if (check_for_node(root, "max_history_splits")) { settings::max_history_splits = std::stoi(get_node_value(root, "max_history_splits")); diff --git a/src/tallies/tally_scoring.cpp b/src/tallies/tally_scoring.cpp index f078a5c6e..0df80a239 100644 --- a/src/tallies/tally_scoring.cpp +++ b/src/tallies/tally_scoring.cpp @@ -1624,8 +1624,7 @@ void score_general_mg(Particle& p, int i_tally, int start_index, tally.estimator_ == TallyEstimator::COLLISION) { if (settings::survival_biasing) { // Determine weight that was absorbed - wgt_absorb = p.wgt_last() * p.neutron_xs(p.event_nuclide()).absorption / - p.neutron_xs(p.event_nuclide()).total; + wgt_absorb = p.wgt_last() * p.macro_xs().absorption / p.macro_xs().total; // Then we either are alive and had a scatter (and so g changed), // or are dead and g did not change diff --git a/tests/regression_tests/adj_cell_rotation/results_true.dat b/tests/regression_tests/adj_cell_rotation/results_true.dat index b3df12e71..ddb1546b5 100644 --- a/tests/regression_tests/adj_cell_rotation/results_true.dat +++ b/tests/regression_tests/adj_cell_rotation/results_true.dat @@ -1,2 +1,2 @@ k-combined: -4.403987E-01 1.514158E-03 +4.368327E-01 1.953533E-03 diff --git a/tests/regression_tests/albedo_box/results_true.dat b/tests/regression_tests/albedo_box/results_true.dat index 0f571f245..dca80abcd 100644 --- a/tests/regression_tests/albedo_box/results_true.dat +++ b/tests/regression_tests/albedo_box/results_true.dat @@ -1,2 +1,2 @@ k-combined: -1.590800E+00 4.251788E-03 +1.593206E+00 2.925742E-03 diff --git a/tests/regression_tests/asymmetric_lattice/results_true.dat b/tests/regression_tests/asymmetric_lattice/results_true.dat index 741327d80..3d6b610a6 100644 --- a/tests/regression_tests/asymmetric_lattice/results_true.dat +++ b/tests/regression_tests/asymmetric_lattice/results_true.dat @@ -1 +1 @@ -b0ca1fb0436732188b1a199b3250ca9a33782f8fc379b0f7ff9c582e0c794b0a0470df063cafd0b05e802b26f61eaaf9ff5c0a8a672a933246acf49eed3ebf9f \ No newline at end of file +cc76769636be4f681137598cf366e978d7347425a1dfa1b293d17a28381b2b62595fb7f0d2f126dd06972ff9e79089a18dd53aba45fa2b1f316515b91fe6495a \ No newline at end of file diff --git a/tests/regression_tests/cmfd_feed/results_true.dat b/tests/regression_tests/cmfd_feed/results_true.dat index f5f22d9ac..1ef9624d4 100644 --- a/tests/regression_tests/cmfd_feed/results_true.dat +++ b/tests/regression_tests/cmfd_feed/results_true.dat @@ -1,117 +1,117 @@ k-combined: -1.164262E+00 9.207592E-03 +1.181723E+00 9.944883E-03 tally 1: -1.156972E+01 -1.339924E+01 -2.136306E+01 -4.567185E+01 -2.859527E+01 -8.195821E+01 -3.470754E+01 -1.207851E+02 -3.766403E+01 -1.422263E+02 -3.778821E+01 -1.432660E+02 -3.573197E+01 -1.278854E+02 -2.849979E+01 -8.135515E+01 -2.073803E+01 -4.303374E+01 -1.112117E+01 -1.242944E+01 +1.169899E+01 +1.373251E+01 +2.142380E+01 +4.605511E+01 +2.968085E+01 +8.838716E+01 +3.561418E+01 +1.271206E+02 +3.777783E+01 +1.428817E+02 +3.805832E+01 +1.450213E+02 +3.439836E+01 +1.184892E+02 +2.852438E+01 +8.161896E+01 +2.088423E+01 +4.376204E+01 +1.076670E+01 +1.168108E+01 tally 2: -2.388054E+01 -2.875255E+01 -1.667791E+01 -1.403426E+01 -4.224771E+01 -8.942109E+01 -2.993088E+01 -4.490335E+01 -5.689839E+01 -1.625557E+02 -4.043633E+01 -8.212299E+01 -6.764024E+01 -2.297126E+02 -4.807902E+01 -1.161468E+02 -7.314835E+01 -2.684645E+02 -5.203584E+01 -1.359261E+02 -7.375727E+01 -2.733105E+02 -5.252944E+01 -1.386205E+02 -6.909571E+01 -2.397721E+02 -4.922548E+01 -1.217465E+02 -5.685978E+01 -1.621746E+02 -4.051938E+01 -8.237277E+01 -4.185562E+01 -8.784067E+01 -2.983570E+01 -4.467414E+01 -2.238373E+01 -2.520356E+01 -1.566758E+01 -1.234103E+01 +2.321241E+01 +2.702156E+01 +1.620912E+01 +1.317752E+01 +4.197404E+01 +8.845008E+01 +2.982666E+01 +4.469221E+01 +5.810089E+01 +1.695857E+02 +4.134123E+01 +8.588866E+01 +6.982488E+01 +2.447068E+02 +4.966939E+01 +1.238763E+02 +7.428421E+01 +2.767613E+02 +5.287955E+01 +1.403163E+02 +7.447402E+01 +2.785012E+02 +5.324628E+01 +1.423393E+02 +6.895164E+01 +2.381937E+02 +4.916366E+01 +1.211701E+02 +5.679253E+01 +1.617881E+02 +4.043125E+01 +8.204061E+01 +4.218618E+01 +8.933666E+01 +2.978592E+01 +4.456592E+01 +2.196426E+01 +2.435867E+01 +1.525576E+01 +1.175879E+01 tally 3: -1.609520E+01 -1.307542E+01 -1.033429E+00 -5.510889E-02 -2.877073E+01 -4.149542E+01 -1.964219E+00 -1.954692E-01 -3.896816E+01 -7.629752E+01 -2.484053E+00 -3.103733E-01 -4.634285E+01 -1.079367E+02 -2.974750E+00 -4.468223E-01 -5.007964E+01 -1.259202E+02 -3.181802E+00 -5.103621E-01 -5.058915E+01 -1.286193E+02 -3.249442E+00 -5.337712E-01 -4.744464E+01 -1.131026E+02 -3.067644E+00 -4.736335E-01 -3.900632E+01 -7.634433E+01 -2.443552E+00 -3.028060E-01 -2.874166E+01 -4.146375E+01 -1.810421E+00 -1.671667E-01 -1.509222E+01 -1.145579E+01 -1.014919E+00 -5.391053E-02 +1.563788E+01 +1.226528E+01 +1.053289E+00 +5.666942E-02 +2.870755E+01 +4.139654E+01 +1.838017E+00 +1.710528E-01 +3.978616E+01 +7.955764E+01 +2.560657E+00 +3.334449E-01 +4.780385E+01 +1.147770E+02 +3.139243E+00 +4.967628E-01 +5.106650E+01 +1.308704E+02 +3.170056E+00 +5.078920E-01 +5.123992E+01 +1.318586E+02 +3.211706E+00 +5.205979E-01 +4.729862E+01 +1.121695E+02 +3.068662E+00 +4.749488E-01 +3.898816E+01 +7.630564E+01 +2.516911E+00 +3.199696E-01 +2.865357E+01 +4.125742E+01 +1.852314E+00 +1.741116E-01 +1.467340E+01 +1.088460E+01 +9.268633E-01 +4.450662E-02 tally 4: -3.148231E+00 -4.974555E-01 +3.029754E+00 +4.613561E-01 0.000000E+00 0.000000E+00 -2.805439E+00 -3.982239E-01 -5.574031E+00 -1.561105E+00 +2.832501E+00 +4.049252E-01 +5.517243E+00 +1.527794E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -128,14 +128,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -5.574031E+00 -1.561105E+00 -2.805439E+00 -3.982239E-01 -5.171038E+00 -1.344877E+00 -7.372031E+00 -2.725420E+00 +5.517243E+00 +1.527794E+00 +2.832501E+00 +4.049252E-01 +5.117178E+00 +1.316972E+00 +7.333303E+00 +2.701677E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -152,14 +152,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -7.372031E+00 -2.725420E+00 -5.171038E+00 -1.344877E+00 -6.946847E+00 -2.424850E+00 -8.496610E+00 -3.627542E+00 +7.333303E+00 +2.701677E+00 +5.117178E+00 +1.316972E+00 +7.248464E+00 +2.641591E+00 +8.817788E+00 +3.905530E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -176,14 +176,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -8.496610E+00 -3.627542E+00 -6.946847E+00 -2.424850E+00 -8.479501E+00 -3.607280E+00 -9.261869E+00 -4.305912E+00 +8.817788E+00 +3.905530E+00 +7.248464E+00 +2.641591E+00 +8.646465E+00 +3.749847E+00 +9.460948E+00 +4.495388E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -200,14 +200,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -9.261869E+00 -4.305912E+00 -8.479501E+00 -3.607280E+00 -9.232858E+00 -4.278432E+00 -9.306384E+00 -4.348594E+00 +9.460948E+00 +4.495388E+00 +8.646465E+00 +3.749847E+00 +9.379341E+00 +4.415049E+00 +9.278640E+00 +4.320720E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -224,14 +224,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -9.306384E+00 -4.348594E+00 -9.232858E+00 -4.278432E+00 -9.299764E+00 -4.347828E+00 -8.511976E+00 -3.639893E+00 +9.278640E+00 +4.320720E+00 +9.379341E+00 +4.415049E+00 +9.465746E+00 +4.498591E+00 +8.656146E+00 +3.760545E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -248,14 +248,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -8.511976E+00 -3.639893E+00 -9.299764E+00 -4.347828E+00 -8.726086E+00 -3.819567E+00 -7.147277E+00 -2.562747E+00 +8.656146E+00 +3.760545E+00 +9.465746E+00 +4.498591E+00 +8.589782E+00 +3.700308E+00 +6.996002E+00 +2.456935E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -272,14 +272,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -7.147277E+00 -2.562747E+00 -8.726086E+00 -3.819567E+00 -7.218790E+00 -2.612243E+00 -5.018287E+00 -1.263077E+00 +6.996002E+00 +2.456935E+00 +8.589782E+00 +3.700308E+00 +7.352050E+00 +2.714808E+00 +5.105164E+00 +1.312559E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -296,14 +296,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -5.018287E+00 -1.263077E+00 -7.218790E+00 -2.612243E+00 -5.443494E+00 -1.487018E+00 -2.732334E+00 -3.773047E-01 +5.105164E+00 +1.312559E+00 +7.352050E+00 +2.714808E+00 +5.442756E+00 +1.486776E+00 +2.697305E+00 +3.675580E-01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -320,12 +320,12 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -2.732334E+00 -3.773047E-01 -5.443494E+00 -1.487018E+00 -3.044773E+00 -4.655756E-01 +2.697305E+00 +3.675580E-01 +5.442756E+00 +1.486776E+00 +3.017025E+00 +4.571443E-01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -345,144 +345,144 @@ tally 4: 0.000000E+00 0.000000E+00 tally 5: -1.609029E+01 -1.306718E+01 -2.230601E+00 -2.559496E-01 -2.876780E+01 -4.148686E+01 -3.835952E+00 -7.456562E-01 -3.895738E+01 -7.625344E+01 -4.841024E+00 -1.197335E+00 -4.633595E+01 -1.079043E+02 -6.236821E+00 -1.963311E+00 -5.007472E+01 -1.258967E+02 -6.749745E+00 -2.297130E+00 -5.058336E+01 -1.285894E+02 -6.727612E+00 -2.315656E+00 -4.743869E+01 -1.130735E+02 -6.338193E+00 -2.042159E+00 -3.899838E+01 -7.631289E+01 -5.187573E+00 -1.359733E+00 -2.873434E+01 -4.144233E+01 -3.815610E+00 -7.367619E-01 -1.509020E+01 -1.145268E+01 -2.125767E+00 -2.341894E-01 +1.563588E+01 +1.226217E+01 +2.209027E+00 +2.507131E-01 +2.870034E+01 +4.137550E+01 +3.726620E+00 +7.021948E-01 +3.977762E+01 +7.952285E+01 +5.304975E+00 +1.427333E+00 +4.779747E+01 +1.147456E+02 +6.528302E+00 +2.151715E+00 +5.105366E+01 +1.308037E+02 +6.986782E+00 +2.467487E+00 +5.123380E+01 +1.318264E+02 +6.845633E+00 +2.383542E+00 +4.729296E+01 +1.121433E+02 +6.252695E+00 +1.977351E+00 +3.898236E+01 +7.628315E+01 +5.461495E+00 +1.528579E+00 +2.864576E+01 +4.123538E+01 +3.857301E+00 +7.581323E-01 +1.467047E+01 +1.088031E+01 +2.277024E+00 +2.679801E-01 cmfd indices 1.000000E+01 1.000000E+00 1.000000E+00 1.000000E+00 k cmfd -1.129918E+00 -1.143848E+00 -1.147976E+00 -1.151534E+00 -1.152378E+00 -1.148219E+00 -1.150402E+00 -1.154647E+00 -1.156159E+00 -1.160048E+00 -1.167441E+00 -1.168163E+00 -1.168629E+00 -1.164120E+00 -1.165051E+00 -1.169177E+00 +1.169107E+00 +1.175079E+00 +1.173912E+00 +1.175368E+00 +1.174026E+00 +1.181745E+00 +1.182261E+00 +1.183559E+00 +1.178691E+00 +1.179222E+00 +1.179017E+00 +1.172979E+00 +1.175043E+00 +1.173458E+00 +1.174152E+00 +1.171451E+00 cmfd entropy -3.224769E+00 -3.225945E+00 -3.227421E+00 -3.226174E+00 -3.224429E+00 -3.227049E+00 -3.230710E+00 -3.230315E+00 -3.226825E+00 -3.226655E+00 -3.226588E+00 -3.224155E+00 -3.223246E+00 -3.222640E+00 -3.223920E+00 -3.222838E+00 +3.207640E+00 +3.210547E+00 +3.212218E+00 +3.209573E+00 +3.211619E+00 +3.212126E+00 +3.213163E+00 +3.214288E+00 +3.215737E+00 +3.213677E+00 +3.214925E+00 +3.215612E+00 +3.216708E+00 +3.221454E+00 +3.219048E+00 +3.218387E+00 cmfd balance -3.90454E-03 -4.08089E-03 -3.46511E-03 -4.09535E-03 -2.62009E-03 -2.23559E-03 -2.54033E-03 -2.12799E-03 -2.25864E-03 -1.85766E-03 -1.49916E-03 -1.63471E-03 -1.48377E-03 -1.59800E-03 -1.37354E-03 -1.32853E-03 +4.88208E-03 +4.75139E-03 +3.15783E-03 +3.67091E-03 +2.99797E-03 +2.91060E-03 +2.06576E-03 +1.83482E-03 +1.56292E-03 +1.58659E-03 +2.32986E-03 +1.47376E-03 +1.46673E-03 +1.22627E-03 +1.31963E-03 +1.26456E-03 cmfd dominance ratio -5.539E-01 -5.537E-01 -5.536E-01 -5.515E-01 -5.512E-01 -5.514E-01 -5.518E-01 -5.507E-01 -5.500E-01 -5.497E-01 -5.477E-01 -5.461E-01 -5.444E-01 -5.445E-01 -5.454E-01 +5.467E-01 +5.453E-01 +5.458E-01 +5.436E-01 +5.442E-01 +5.406E-01 +5.401E-01 +5.413E-01 +4.995E-01 +5.396E-01 +5.409E-01 +5.414E-01 +5.423E-01 +5.456E-01 +5.442E-01 5.441E-01 cmfd openmc source comparison -9.875240E-03 -1.106163E-02 -9.847628E-03 -6.065921E-03 -5.772039E-03 -4.615656E-03 -4.244331E-03 -3.694299E-03 -3.545814E-03 -3.213063E-03 -3.467537E-03 -3.383489E-03 -3.697591E-03 -3.937358E-03 -3.369124E-03 -3.190359E-03 +9.587418E-03 +8.150978E-03 +6.677661E-03 +6.334727E-03 +5.153692E-03 +5.082964E-03 +4.633153E-03 +4.037383E-03 +3.528742E-03 +4.559089E-03 +3.517370E-03 +3.306117E-03 +2.913809E-03 +1.906045E-03 +1.932794E-03 +1.711341E-03 cmfd source -4.360494E-02 -8.397599E-02 -1.074181E-01 -1.294531E-01 -1.385611E-01 -1.407934E-01 -1.325191E-01 -1.044311E-01 -7.660359E-02 -4.263941E-02 +4.496492E-02 +7.869674E-02 +1.100280E-01 +1.354045E-01 +1.363339E-01 +1.380533E-01 +1.314512E-01 +1.077480E-01 +7.847306E-02 +3.884630E-02 diff --git a/tests/regression_tests/cmfd_feed_2g/results_true.dat b/tests/regression_tests/cmfd_feed_2g/results_true.dat index 2b61d9f4e..e66eae13c 100644 --- a/tests/regression_tests/cmfd_feed_2g/results_true.dat +++ b/tests/regression_tests/cmfd_feed_2g/results_true.dat @@ -1,112 +1,112 @@ k-combined: -1.035567E+00 9.463160E-03 +1.027434E+00 6.509170E-03 tally 1: -1.146535E+02 -1.315267E+03 -1.157458E+02 -1.340166E+03 -1.140491E+02 -1.301364E+03 -1.146589E+02 -1.315433E+03 +1.162758E+02 +1.352562E+03 +1.138125E+02 +1.295815E+03 +1.143712E+02 +1.308316E+03 +1.150293E+02 +1.323834E+03 tally 2: -4.319968E+01 -9.360083E+01 -6.373035E+01 -2.038056E+02 -1.889646E+02 -1.812892E+03 -1.024866E+02 -5.254528E+02 -4.323262E+01 -9.360200E+01 -6.363111E+01 -2.028178E+02 -1.849746E+02 -1.711533E+03 -1.034532E+02 -5.352768E+02 -4.296541E+01 -9.249656E+01 -6.346919E+01 -2.018659E+02 -1.888697E+02 -1.812037E+03 -1.025707E+02 -5.262261E+02 -4.691085E+01 -1.269707E+02 -6.299377E+01 -1.990497E+02 -1.853864E+02 -1.719984E+03 -1.023015E+02 -5.235858E+02 +4.284580E+01 +9.207089E+01 +6.335165E+01 +2.014931E+02 +1.894187E+02 +1.818190E+03 +1.033212E+02 +5.340768E+02 +4.282771E+01 +9.186295E+01 +6.295029E+01 +1.983895E+02 +1.834276E+02 +1.684375E+03 +1.022482E+02 +5.228403E+02 +4.330690E+01 +9.402038E+01 +6.395965E+01 +2.053163E+02 +1.851113E+02 +1.714198E+03 +1.030809E+02 +5.314535E+02 +4.337097E+01 +9.426435E+01 +6.417590E+01 +2.063443E+02 +1.846817E+02 +1.706518E+03 +1.027233E+02 +5.279582E+02 tally 3: -6.034963E+01 -1.827718E+02 +5.992726E+01 +1.803120E+02 0.000000E+00 0.000000E+00 -1.865665E-02 -4.244195E-05 -4.170941E+00 -8.769372E-01 -3.453368E+00 -5.989168E-01 +2.172646E-02 +4.414237E-05 +4.181401E+00 +8.912796E-01 +3.536506E+00 +6.287425E-01 0.000000E+00 0.000000E+00 -9.743205E+01 -4.749420E+02 -8.570316E-01 -3.807993E-02 -6.005903E+01 -1.807233E+02 +9.824432E+01 +4.828691E+02 +9.116848E-01 +4.231247E-02 +5.955090E+01 +1.775522E+02 0.000000E+00 0.000000E+00 -1.885450E-02 -3.653402E-05 -4.323863E+00 -9.447512E-01 -3.465465E+00 -6.022861E-01 +1.893222E-02 +3.288000E-05 +4.048183E+00 +8.291130E-01 +3.384041E+00 +5.742363E-01 0.000000E+00 0.000000E+00 -9.843481E+01 -4.846158E+02 -9.048150E-01 -4.205551E-02 -5.996660E+01 -1.802150E+02 +9.734253E+01 +4.738861E+02 +9.157632E-01 +4.329280E-02 +6.045835E+01 +1.835255E+02 0.000000E+00 0.000000E+00 -1.221444E-02 -2.263445E-05 -4.301287E+00 -9.309882E-01 -3.456076E+00 -5.992144E-01 +1.501842E-02 +1.896931E-05 +4.289989E+00 +9.251538E-01 +3.481357E+00 +6.071667E-01 0.000000E+00 0.000000E+00 -9.761231E+01 -4.765834E+02 -8.434644E-01 -3.728180E-02 -5.961891E+01 -1.783106E+02 +9.799829E+01 +4.803591E+02 +8.899390E-01 +4.078750E-02 +6.064531E+01 +1.842746E+02 0.000000E+00 0.000000E+00 -1.500709E-02 -3.541280E-05 -4.155669E+00 -8.713442E-01 -3.455342E+00 -6.006426E-01 +1.495496E-02 +3.082640E-05 +4.321537E+00 +9.371611E-01 +3.453767E+00 +5.983727E-01 0.000000E+00 0.000000E+00 -9.726798E+01 -4.733393E+02 -9.202611E-01 -4.390503E-02 +9.771776E+01 +4.777781E+02 +8.975444E-01 +4.157471E-02 tally 4: 0.000000E+00 0.000000E+00 @@ -116,14 +116,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -8.943264E+00 -4.008855E+00 -3.661063E+01 -6.704808E+01 -8.945553E+00 -4.011707E+00 -3.696832E+01 -6.835286E+01 +8.840487E+00 +3.915792E+00 +3.700362E+01 +6.851588E+01 +8.756789E+00 +3.844443E+00 +3.672366E+01 +6.747174E+01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -132,14 +132,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -8.844569E+00 -3.924591E+00 -3.666726E+01 -6.726522E+01 -8.769637E+00 -3.855006E+00 -3.654115E+01 -6.680777E+01 +8.860460E+00 +3.940908E+00 +3.704658E+01 +6.864069E+01 +8.832046E+00 +3.916611E+00 +3.736239E+01 +6.982147E+01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -156,14 +156,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -8.945553E+00 -4.011707E+00 -3.696832E+01 -6.835286E+01 -8.943264E+00 -4.008855E+00 -3.661063E+01 -6.704808E+01 +8.756789E+00 +3.844443E+00 +3.672366E+01 +6.747174E+01 +8.840487E+00 +3.915792E+00 +3.700362E+01 +6.851588E+01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -180,14 +180,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -8.648474E+00 -3.752219E+00 -3.689442E+01 -6.808997E+01 -8.757378E+00 -3.850408E+00 -3.716920E+01 -6.909715E+01 +8.892576E+00 +3.964978E+00 +3.703525E+01 +6.860645E+01 +8.824229E+00 +3.906925E+00 +3.685909E+01 +6.796123E+01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -212,22 +212,22 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -8.783669E+00 -3.870748E+00 -3.687358E+01 -6.802581E+01 -8.755250E+00 -3.846298E+00 -3.660278E+01 -6.704349E+01 -8.769637E+00 -3.855006E+00 -3.654115E+01 -6.680777E+01 -8.844569E+00 -3.924591E+00 -3.666726E+01 -6.726522E+01 +9.050876E+00 +4.111409E+00 +3.656082E+01 +6.687580E+01 +9.042402E+00 +4.105842E+00 +3.687247E+01 +6.801050E+01 +8.832046E+00 +3.916611E+00 +3.736239E+01 +6.982147E+01 +8.860460E+00 +3.940908E+00 +3.704658E+01 +6.864069E+01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -252,14 +252,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -8.755250E+00 -3.846298E+00 -3.660278E+01 -6.704349E+01 -8.783669E+00 -3.870748E+00 -3.687358E+01 -6.802581E+01 +9.042402E+00 +4.105842E+00 +3.687247E+01 +6.801050E+01 +9.050876E+00 +4.111409E+00 +3.656082E+01 +6.687580E+01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -268,14 +268,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -8.757378E+00 -3.850408E+00 -3.716920E+01 -6.909715E+01 -8.648474E+00 -3.752219E+00 -3.689442E+01 -6.808997E+01 +8.824229E+00 +3.906925E+00 +3.685909E+01 +6.796123E+01 +8.892576E+00 +3.964978E+00 +3.703525E+01 +6.860645E+01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -301,133 +301,133 @@ tally 4: 0.000000E+00 0.000000E+00 tally 5: -6.036829E+01 -1.828885E+02 -1.008777E+02 -5.091033E+02 -1.353211E+01 -9.188155E+00 -4.618716E+01 -1.067410E+02 -6.007789E+01 -1.808371E+02 -1.018892E+02 -5.192269E+02 -1.357961E+01 -9.247052E+00 -4.618560E+01 -1.067058E+02 -5.997882E+01 -1.802895E+02 -1.010597E+02 -5.108478E+02 -1.374955E+01 -9.502494E+00 -4.619500E+01 -1.067547E+02 -5.963392E+01 -1.783999E+02 -1.007134E+02 -5.074700E+02 -1.319398E+01 -8.734106E+00 -4.586295E+01 -1.052870E+02 +5.994898E+01 +1.804403E+02 +1.017670E+02 +5.181130E+02 +1.354160E+01 +9.220935E+00 +4.648971E+01 +1.081636E+02 +5.956983E+01 +1.776668E+02 +1.007226E+02 +5.073592E+02 +1.347883E+01 +9.120344E+00 +4.609907E+01 +1.063127E+02 +6.047337E+01 +1.836168E+02 +1.014777E+02 +5.150557E+02 +1.390508E+01 +9.722899E+00 +4.629251E+01 +1.072027E+02 +6.066027E+01 +1.843661E+02 +1.011648E+02 +5.120629E+02 +1.365982E+01 +9.372236E+00 +4.602994E+01 +1.060579E+02 cmfd indices 2.000000E+00 2.000000E+00 1.000000E+00 2.000000E+00 k cmfd -1.018115E+00 -1.022665E+00 -1.020323E+00 -1.020653E+00 -1.021036E+00 -1.020623E+00 -1.021482E+00 -1.025450E+00 -1.027292E+00 -1.028065E+00 -1.027065E+00 -1.024275E+00 -1.025309E+00 -1.026039E+00 -1.026700E+00 -1.023865E+00 +1.013488E+00 +1.024396E+00 +1.015533E+00 +1.009319E+00 +1.012726E+00 +1.014831E+00 +1.021757E+00 +1.022002E+00 +1.023619E+00 +1.020953E+00 +1.023910E+00 +1.027657E+00 +1.024501E+00 +1.023838E+00 +1.025464E+00 +1.022802E+00 cmfd entropy -1.998965E+00 -1.999214E+00 -1.999348E+00 -1.999366E+00 -1.999564E+00 -1.999453E+00 -1.999533E+00 -1.999630E+00 -1.999739E+00 -1.999588E+00 -1.999581E+00 -1.999719E+00 -1.999773E+00 -1.999764E+00 -1.999821E+00 -1.999843E+00 +1.998974E+00 +1.998742E+00 +1.999128E+00 +1.998952E+00 +1.998951E+00 +1.999439E+00 +1.999626E+00 +1.999826E+00 +1.999513E+00 +1.999451E+00 +1.999514E+00 +1.999590E+00 +1.999563E+00 +1.999604E+00 +1.999742E+00 +1.999736E+00 cmfd balance -5.73174E-04 -7.55398E-04 -1.46671E-03 -6.39625E-04 -8.19008E-04 -1.93449E-03 -1.15900E-03 -1.01690E-03 -5.62788E-04 -6.90450E-04 -6.01060E-04 -5.73418E-04 -4.37190E-04 -4.82966E-04 -4.09700E-04 -3.45096E-04 +9.79896E-04 +4.24873E-04 +8.05696E-04 +1.92071E-03 +3.70731E-04 +2.81424E-04 +8.28991E-04 +6.12217E-04 +5.29185E-04 +4.97799E-04 +3.09154E-04 +1.73703E-04 +2.56689E-04 +2.64938E-04 +1.96305E-04 +1.82702E-04 cmfd dominance ratio -6.264E-03 -6.142E-03 -5.987E-03 -6.082E-03 -5.895E-03 -5.939E-03 -5.910E-03 -5.948E-03 -6.013E-03 -6.017E-03 -6.024E-03 -6.008E-03 -5.976E-03 -5.987E-03 -5.967E-03 -5.929E-03 +6.304E-03 +6.246E-03 +6.159E-03 +6.249E-03 +6.101E-03 +6.155E-03 +6.010E-03 +6.177E-03 +6.349E-03 +6.241E-03 +6.244E-03 +6.249E-03 +6.270E-03 +6.272E-03 +6.278E-03 +6.290E-03 cmfd openmc source comparison -4.832872E-05 -6.552342E-05 -7.516800E-05 -7.916087E-05 -9.022260E-05 -8.574478E-05 -7.891622E-05 -7.281636E-05 -7.750571E-05 -6.565408E-05 -6.078665E-05 -5.834343E-05 -4.758176E-05 -5.723990E-05 -4.994116E-05 -4.116808E-05 +4.046094E-05 +5.979431E-05 +3.836521E-05 +4.577591E-05 +5.012911E-05 +2.114677E-05 +2.074571E-05 +3.042280E-05 +2.408163E-05 +2.434542E-05 +1.190699E-05 +9.499301E-06 +2.354221E-05 +2.937924E-05 +1.889875E-05 +1.913866E-05 cmfd source -2.455663E-01 -2.553511E-01 -2.512257E-01 -2.478570E-01 +2.489706E-01 +2.426801E-01 +2.532142E-01 +2.551351E-01 0.000000E+00 0.000000E+00 0.000000E+00 diff --git a/tests/regression_tests/cmfd_feed_expanding_window/results_true.dat b/tests/regression_tests/cmfd_feed_expanding_window/results_true.dat index 8fd8cdbb8..b39f82f96 100644 --- a/tests/regression_tests/cmfd_feed_expanding_window/results_true.dat +++ b/tests/regression_tests/cmfd_feed_expanding_window/results_true.dat @@ -1,117 +1,117 @@ k-combined: -1.167865E+00 7.492213E-03 +1.170835E+00 5.423480E-03 tally 1: -1.146860E+01 -1.318884E+01 -2.161527E+01 -4.685283E+01 -2.951158E+01 -8.733566E+01 -3.521610E+01 -1.242821E+02 -3.774236E+01 -1.426501E+02 -3.727918E+01 -1.391158E+02 -3.377176E+01 -1.143839E+02 -2.904497E+01 -8.452907E+01 -2.090871E+01 -4.384549E+01 -1.078642E+01 -1.168086E+01 +1.205100E+01 +1.456707E+01 +2.183882E+01 +4.781179E+01 +2.844010E+01 +8.102358E+01 +3.356334E+01 +1.130832E+02 +3.660829E+01 +1.344973E+02 +3.697740E+01 +1.371500E+02 +3.400119E+01 +1.160196E+02 +2.839868E+01 +8.083199E+01 +2.140398E+01 +4.615447E+01 +1.118179E+01 +1.262942E+01 tally 2: -1.136810E+00 -1.292338E+00 -7.987303E-01 -6.379700E-01 -2.266938E+00 -5.139009E+00 -1.613483E+00 -2.603328E+00 -3.046349E+00 -9.280239E+00 -2.182459E+00 -4.763126E+00 -3.568068E+00 -1.273111E+01 -2.532456E+00 -6.413333E+00 -3.989504E+00 -1.591614E+01 -2.848301E+00 -8.112818E+00 -3.853133E+00 -1.484663E+01 -2.718493E+00 -7.390202E+00 -3.478138E+00 -1.209745E+01 -2.467281E+00 -6.087476E+00 -2.952220E+00 -8.715605E+00 -2.103261E+00 -4.423706E+00 -1.917459E+00 -3.676649E+00 -1.378369E+00 -1.899902E+00 -1.048240E+00 -1.098807E+00 -7.511947E-01 -5.642934E-01 +1.218245E+00 +1.484121E+00 +8.387442E-01 +7.034918E-01 +2.142134E+00 +4.588738E+00 +1.526727E+00 +2.330895E+00 +2.736157E+00 +7.486556E+00 +1.973921E+00 +3.896363E+00 +3.606244E+00 +1.300500E+01 +2.537580E+00 +6.439313E+00 +3.668958E+00 +1.346126E+01 +2.599095E+00 +6.755294E+00 +3.647982E+00 +1.330777E+01 +2.539750E+00 +6.450332E+00 +3.118921E+00 +9.727669E+00 +2.186447E+00 +4.780549E+00 +2.881110E+00 +8.300795E+00 +2.042635E+00 +4.172360E+00 +2.045602E+00 +4.184486E+00 +1.458384E+00 +2.126884E+00 +1.022124E+00 +1.044738E+00 +7.112678E-01 +5.059018E-01 tally 3: -7.701233E-01 -5.930898E-01 -4.481585E-02 -2.008461E-03 -1.547307E+00 -2.394158E+00 -1.226539E-01 -1.504398E-02 -2.106373E+00 -4.436806E+00 -1.450618E-01 -2.104294E-02 -2.437654E+00 -5.942157E+00 -1.521380E-01 -2.314598E-02 -2.754639E+00 -7.588038E+00 -1.745460E-01 -3.046629E-02 -2.623852E+00 -6.884601E+00 -1.851602E-01 -3.428432E-02 -2.376886E+00 -5.649588E+00 -1.615729E-01 -2.610582E-02 -2.021856E+00 -4.087900E+00 -1.533174E-01 -2.350622E-02 -1.333190E+00 -1.777397E+00 -7.076188E-02 -5.007243E-03 -7.258527E-01 -5.268622E-01 -3.656030E-02 -1.336656E-03 +8.048428E-01 +6.477720E-01 +6.603741E-02 +4.360940E-03 +1.466886E+00 +2.151755E+00 +1.002354E-01 +1.004713E-02 +1.909238E+00 +3.645189E+00 +1.202824E-01 +1.446786E-02 +2.443130E+00 +5.968886E+00 +1.627351E-01 +2.648270E-02 +2.492602E+00 +6.213064E+00 +1.910368E-01 +3.649506E-02 +2.437262E+00 +5.940245E+00 +1.568389E-01 +2.459843E-02 +2.104091E+00 +4.427200E+00 +1.450465E-01 +2.103848E-02 +1.965900E+00 +3.864762E+00 +1.367918E-01 +1.871199E-02 +1.402871E+00 +1.968047E+00 +1.061316E-01 +1.126391E-02 +6.832689E-01 +4.668565E-01 +4.599034E-02 +2.115112E-03 tally 4: -1.667432E-01 -2.780328E-02 +1.497312E-01 +2.241943E-02 0.000000E+00 0.000000E+00 -1.292567E-01 -1.670730E-02 -2.813370E-01 -7.915052E-02 +1.535839E-01 +2.358801E-02 +2.882052E-01 +8.306225E-02 0.000000E+00 0.000000E+00 0.000000E+00 @@ -128,14 +128,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -2.813370E-01 -7.915052E-02 -1.292567E-01 -1.670730E-02 -2.670549E-01 -7.131835E-02 -4.055324E-01 -1.644566E-01 +2.882052E-01 +8.306225E-02 +1.535839E-01 +2.358801E-02 +2.526805E-01 +6.384743E-02 +3.616220E-01 +1.307705E-01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -152,14 +152,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -4.055324E-01 -1.644566E-01 -2.670549E-01 -7.131835E-02 -3.848125E-01 -1.480807E-01 -4.809430E-01 -2.313062E-01 +3.616220E-01 +1.307705E-01 +2.526805E-01 +6.384743E-02 +3.594306E-01 +1.291904E-01 +4.229730E-01 +1.789062E-01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -176,14 +176,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -4.809430E-01 -2.313062E-01 -3.848125E-01 -1.480807E-01 -4.543918E-01 -2.064719E-01 -5.106133E-01 -2.607260E-01 +4.229730E-01 +1.789062E-01 +3.594306E-01 +1.291904E-01 +3.973299E-01 +1.578711E-01 +4.255879E-01 +1.811250E-01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -200,14 +200,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -5.106133E-01 -2.607260E-01 -4.543918E-01 -2.064719E-01 -4.543120E-01 -2.063994E-01 -4.626328E-01 -2.140291E-01 +4.255879E-01 +1.811250E-01 +3.973299E-01 +1.578711E-01 +4.633933E-01 +2.147333E-01 +4.672837E-01 +2.183540E-01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -224,14 +224,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -4.626328E-01 -2.140291E-01 -4.543120E-01 -2.063994E-01 -4.827759E-01 -2.330726E-01 -4.442622E-01 -1.973689E-01 +4.672837E-01 +2.183540E-01 +4.633933E-01 +2.147333E-01 +4.251073E-01 +1.807162E-01 +3.842922E-01 +1.476805E-01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -248,14 +248,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -4.442622E-01 -1.973689E-01 -4.827759E-01 -2.330726E-01 -4.630420E-01 -2.144079E-01 -3.886524E-01 -1.510507E-01 +3.842922E-01 +1.476805E-01 +4.251073E-01 +1.807162E-01 +4.045096E-01 +1.636280E-01 +3.192860E-01 +1.019436E-01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -272,14 +272,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -3.886524E-01 -1.510507E-01 -4.630420E-01 -2.144079E-01 -3.535870E-01 -1.250237E-01 -2.530312E-01 -6.402478E-02 +3.192860E-01 +1.019436E-01 +4.045096E-01 +1.636280E-01 +3.738326E-01 +1.397508E-01 +2.598153E-01 +6.750398E-02 0.000000E+00 0.000000E+00 0.000000E+00 @@ -296,14 +296,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -2.530312E-01 -6.402478E-02 -3.535870E-01 -1.250237E-01 -2.465524E-01 -6.078808E-02 -1.197152E-01 -1.433173E-02 +2.598153E-01 +6.750398E-02 +3.738326E-01 +1.397508E-01 +2.453191E-01 +6.018146E-02 +1.098964E-01 +1.207721E-02 0.000000E+00 0.000000E+00 0.000000E+00 @@ -320,12 +320,12 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -1.197152E-01 -1.433173E-02 -2.465524E-01 -6.078808E-02 -1.369631E-01 -1.875888E-02 +1.098964E-01 +1.207721E-02 +2.453191E-01 +6.018146E-02 +1.458094E-01 +2.126039E-02 0.000000E+00 0.000000E+00 0.000000E+00 @@ -345,119 +345,119 @@ tally 4: 0.000000E+00 0.000000E+00 tally 5: -7.701233E-01 -5.930898E-01 -1.386250E-01 -1.921688E-02 -1.547307E+00 -2.394158E+00 -2.630277E-01 -6.918357E-02 -2.106373E+00 -4.436806E+00 -2.807880E-01 -7.884187E-02 -2.435849E+00 -5.933361E+00 -3.322060E-01 -1.103608E-01 -2.753634E+00 -7.582501E+00 -3.825922E-01 -1.463768E-01 -2.623852E+00 -6.884601E+00 -3.888710E-01 -1.512206E-01 -2.376886E+00 -5.649588E+00 -3.196217E-01 -1.021581E-01 -2.021856E+00 -4.087900E+00 -2.897881E-01 -8.397715E-02 -1.333190E+00 -1.777397E+00 -1.627110E-01 -2.647486E-02 -7.258527E-01 -5.268622E-01 -9.348666E-02 -8.739755E-03 +8.048428E-01 +6.477720E-01 +1.018934E-01 +1.038226E-02 +1.466886E+00 +2.151755E+00 +1.414681E-01 +2.001322E-02 +1.909238E+00 +3.645189E+00 +2.450211E-01 +6.003535E-02 +2.443130E+00 +5.968886E+00 +3.360056E-01 +1.128997E-01 +2.492602E+00 +6.213064E+00 +3.266277E-01 +1.066856E-01 +2.437262E+00 +5.940245E+00 +2.878100E-01 +8.283461E-02 +2.104091E+00 +4.427200E+00 +3.440457E-01 +1.183675E-01 +1.965900E+00 +3.864762E+00 +2.880615E-01 +8.297945E-02 +1.401955E+00 +1.965478E+00 +1.646479E-01 +2.710892E-02 +6.832689E-01 +4.668565E-01 +1.147413E-01 +1.316557E-02 cmfd indices 1.000000E+01 1.000000E+00 1.000000E+00 1.000000E+00 k cmfd -1.149077E+00 -1.156751E+00 -1.158648E+00 -1.159506E+00 -1.156567E+00 -1.160259E+00 -1.150345E+00 -1.149846E+00 -1.151606E+00 -1.164544E+00 -1.174648E+00 +1.181376E+00 +1.176656E+00 +1.161939E+00 +1.163552E+00 +1.163035E+00 +1.170382E+00 +1.160597E+00 +1.154301E+00 +1.159007E+00 +1.148290E+00 +1.157088E+00 cmfd entropy -3.216173E+00 -3.228717E+00 -3.220402E+00 -3.214352E+00 -3.215636E+00 -3.213599E+00 -3.212854E+00 -3.213131E+00 -3.213196E+00 -3.205474E+00 -3.202869E+00 +3.246419E+00 +3.246511E+00 +3.252247E+00 +3.240919E+00 +3.237600E+00 +3.233990E+00 +3.234226E+00 +3.229356E+00 +3.224272E+00 +3.225381E+00 +3.226778E+00 cmfd balance -3.08825E-03 -1.42345E-03 -1.21253E-03 -1.17694E-03 -1.05901E-03 -9.29611E-04 -1.35587E-03 -1.13579E-03 -1.14964E-03 -1.29313E-03 -1.46566E-03 +4.18486E-03 +1.72126E-03 +1.10899E-03 +1.88170E-03 +1.31646E-03 +1.34128E-03 +1.57944E-03 +2.11251E-03 +1.79912E-03 +1.86000E-03 +1.47765E-03 cmfd dominance ratio 5.524E-01 -5.614E-01 -5.522E-01 -5.487E-01 -5.482E-01 -5.446E-01 -5.437E-01 -5.429E-01 -5.407E-01 -5.380E-01 -5.377E-01 +5.597E-01 +5.622E-01 +5.544E-01 +5.541E-01 +5.519E-01 +5.532E-01 +5.550E-01 +5.484E-01 +5.497E-01 +5.500E-01 cmfd openmc source comparison -1.586045E-02 -6.953134E-03 -6.860419E-03 -6.198467E-03 -5.142854E-03 -4.373354E-03 -5.564831E-03 -4.184765E-03 -1.867780E-03 -2.734784E-03 -2.523985E-03 +1.905464E-03 +4.145126E-03 +2.465876E-03 +2.346755E-03 +1.848120E-03 +3.263822E-03 +3.641639E-03 +4.031509E-03 +4.999010E-03 +6.640746E-03 +5.691414E-03 cmfd source -4.241440E-02 -8.226026E-02 -1.180811E-01 -1.328433E-01 -1.412410E-01 -1.424902E-01 -1.269340E-01 -1.096490E-01 -6.953251E-02 -3.455422E-02 +4.951338E-02 +8.478025E-02 +1.083132E-01 +1.301432E-01 +1.341190E-01 +1.445825E-01 +1.255119E-01 +1.063303E-01 +7.830158E-02 +3.840469E-02 diff --git a/tests/regression_tests/cmfd_feed_ng/results_true.dat b/tests/regression_tests/cmfd_feed_ng/results_true.dat index 153238bfe..4ea1515f1 100644 --- a/tests/regression_tests/cmfd_feed_ng/results_true.dat +++ b/tests/regression_tests/cmfd_feed_ng/results_true.dat @@ -1,208 +1,208 @@ k-combined: -1.005987E+00 1.354263E-02 +1.008852E+00 9.028695E-03 tally 1: -1.140273E+02 -1.301245E+03 -1.147962E+02 -1.319049E+03 -1.151426E+02 -1.326442E+03 -1.149265E+02 -1.321518E+03 +1.151271E+02 +1.325871E+03 +1.143934E+02 +1.309051E+03 +1.142507E+02 +1.306616E+03 +1.140242E+02 +1.300786E+03 tally 2: -3.462748E+01 -7.542476E+01 -5.129219E+01 -1.658142E+02 -1.034704E+01 -6.730603E+00 -8.672967E+00 -4.715295E+00 -1.344669E+02 -1.132019E+03 -7.262519E+01 -3.300787E+02 -3.447358E+01 -7.459545E+01 -5.075836E+01 -1.619570E+02 -1.080516E+01 -7.366369E+00 -8.908065E+00 -4.991975E+00 -1.354224E+02 -1.146824E+03 -7.300078E+01 -3.332531E+02 -3.432298E+01 -7.388666E+01 -5.096378E+01 -1.627979E+02 -1.053664E+01 -7.029789E+00 -8.826624E+00 -4.904429E+00 -1.388389E+02 -1.207280E+03 -7.376623E+01 -3.403211E+02 -4.383841E+01 -1.943331E+02 -5.165881E+01 -1.675923E+02 -1.059646E+01 -7.048052E+00 -8.763673E+00 -4.823505E+00 -1.378179E+02 -1.188104E+03 -7.431708E+01 -3.453746E+02 +3.403617E+01 +7.260478E+01 +5.031678E+01 +1.588977E+02 +1.003700E+01 +6.373741E+00 +8.514575E+00 +4.571811E+00 +1.413036E+02 +1.264708E+03 +7.321799E+01 +3.353408E+02 +3.354839E+01 +7.052647E+01 +4.895930E+01 +1.501243E+02 +9.972495E+00 +6.271276E+00 +8.436263E+00 +4.481319E+00 +1.353506E+02 +1.146040E+03 +7.309382E+01 +3.341751E+02 +3.389861E+01 +7.205501E+01 +5.005946E+01 +1.571210E+02 +1.041650E+01 +6.810868E+00 +8.839753E+00 +4.897617E+00 +1.344145E+02 +1.130242E+03 +7.270373E+01 +3.307223E+02 +3.347928E+01 +7.040185E+01 +4.940585E+01 +1.535767E+02 +9.898649E+00 +6.175319E+00 +8.406032E+00 +4.442856E+00 +1.374544E+02 +1.182191E+03 +7.334301E+01 +3.365399E+02 tally 3: -4.858880E+01 -1.488705E+02 +4.755532E+01 +1.419592E+02 0.000000E+00 0.000000E+00 -8.148667E-03 -1.337050E-05 +1.628248E-02 +3.680742E-05 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -3.347376E+00 -7.045574E-01 -2.433484E+00 -3.727266E-01 +3.347160E+00 +7.104290E-01 +2.453669E+00 +3.797470E-01 0.000000E+00 0.000000E+00 -6.095478E+00 -2.332622E+00 +5.925795E+00 +2.220734E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.235421E-01 -1.205155E-03 -3.647699E-01 -8.953589E-03 +9.542527E-02 +1.118856E-03 +3.018759E-01 +6.371059E-03 0.000000E+00 0.000000E+00 -2.673965E+00 -4.517972E-01 +2.501316E+00 +3.938401E-01 0.000000E+00 0.000000E+00 -6.841105E+01 -2.929195E+02 -5.902473E-01 -2.307683E-02 -4.792291E+01 -1.444397E+02 +6.926265E+01 +3.001504E+02 +6.765221E-01 +2.991787E-02 +4.605523E+01 +1.328435E+02 0.000000E+00 0.000000E+00 -2.183275E-02 -8.061011E-05 +1.687782E-02 +3.921162E-05 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -3.435826E+00 -7.480244E-01 -2.450829E+00 -3.800327E-01 +3.481608E+00 +7.705602E-01 +2.439374E+00 +3.779705E-01 0.000000E+00 0.000000E+00 -6.331358E+00 -2.530492E+00 +5.855806E+00 +2.162361E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.012941E-01 -8.217580E-04 -2.981274E-01 -5.863186E-03 +1.016677E-01 +9.263706E-04 +3.264878E-01 +7.385168E-03 0.000000E+00 0.000000E+00 -2.535628E+00 -4.048310E-01 +2.519730E+00 +3.986182E-01 0.000000E+00 0.000000E+00 -6.912003E+01 -2.987684E+02 -5.984862E-01 -2.386115E-02 -4.822881E+01 -1.458309E+02 +6.920950E+01 +2.996184E+02 +5.985719E-01 +2.368062E-02 +4.730723E+01 +1.403550E+02 0.000000E+00 0.000000E+00 -1.525590E-02 -3.794082E-05 +6.800415E-03 +1.163614E-05 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -3.314494E+00 -6.944527E-01 -2.424209E+00 -3.691360E-01 +3.347607E+00 +7.085691E-01 +2.556997E+00 +4.109118E-01 0.000000E+00 0.000000E+00 -6.254897E+00 -2.474317E+00 +6.171063E+00 +2.391770E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.112542E-01 -1.051688E-03 -3.234880E-01 -7.156753E-03 +7.942016E-02 +5.628747E-04 +3.456912E-01 +7.940086E-03 0.000000E+00 0.000000E+00 -2.474142E+00 -3.837496E-01 +2.684459E+00 +4.546338E-01 0.000000E+00 0.000000E+00 -6.987095E+01 -3.053358E+02 -5.928782E-01 -2.368116E-02 -4.885415E+01 -1.499856E+02 +6.850588E+01 +2.936078E+02 +6.458806E-01 +2.672541E-02 +4.673301E+01 +1.374202E+02 0.000000E+00 0.000000E+00 -1.262416E-02 -2.486286E-05 +1.504681E-02 +4.202606E-05 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -3.426826E+00 -7.480396E-01 -2.487209E+00 -3.903882E-01 +3.139138E+00 +6.206027E-01 +2.341735E+00 +3.454035E-01 0.000000E+00 0.000000E+00 -6.127303E+00 -2.364605E+00 +5.923755E+00 +2.216341E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -9.439007E-02 -1.082564E-03 -2.963491E-01 -5.652952E-03 +7.030156E-02 +4.283974E-04 +3.247594E-01 +7.133772E-03 0.000000E+00 0.000000E+00 -2.620696E+00 -4.311792E-01 +2.559691E+00 +4.119674E-01 0.000000E+00 0.000000E+00 -7.030725E+01 -3.091241E+02 -5.986966E-01 -2.352487E-02 +6.938051E+01 +3.012078E+02 +5.159565E-01 +1.730381E-02 tally 4: 0.000000E+00 0.000000E+00 @@ -216,18 +216,18 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -7.028624E+00 -3.105990E+00 -2.154648E+00 -2.948865E-01 -2.714077E+01 -4.607926E+01 -7.035506E+00 -3.118549E+00 -2.085916E+00 -2.730884E-01 -2.750091E+01 -4.731304E+01 +7.028166E+00 +3.103666E+00 +2.028371E+00 +2.606104E-01 +2.715466E+01 +4.614452E+01 +6.981028E+00 +3.059096E+00 +2.032450E+00 +2.610410E-01 +2.734281E+01 +4.675062E+01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -240,18 +240,18 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -7.170567E+00 -3.240715E+00 -2.131758E+00 -2.862355E-01 -2.747702E+01 -4.722977E+01 -7.068817E+00 -3.139275E+00 -2.095865E+00 -2.762179E-01 -2.737835E+01 -4.688689E+01 +6.969559E+00 +3.054867E+00 +2.042871E+00 +2.624815E-01 +2.766332E+01 +4.787778E+01 +7.022610E+00 +3.098329E+00 +2.109973E+00 +2.824233E-01 +2.733826E+01 +4.676309E+01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -276,18 +276,18 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -7.035506E+00 -3.118549E+00 -2.085916E+00 -2.730884E-01 -2.750091E+01 -4.731304E+01 -7.028624E+00 -3.105990E+00 -2.154648E+00 -2.948865E-01 -2.714077E+01 -4.607926E+01 +6.981028E+00 +3.059096E+00 +2.032450E+00 +2.610410E-01 +2.734281E+01 +4.675062E+01 +7.028166E+00 +3.103666E+00 +2.028371E+00 +2.606104E-01 +2.715466E+01 +4.614452E+01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -312,18 +312,18 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -7.103896E+00 -3.171147E+00 -2.095666E+00 -2.756673E-01 -2.711842E+01 -4.600196E+01 -7.197270E+00 -3.255501E+00 -2.046179E+00 -2.630301E-01 -2.729901E+01 -4.662030E+01 +6.782152E+00 +2.885448E+00 +1.951138E+00 +2.400912E-01 +2.739652E+01 +4.697696E+01 +6.873220E+00 +2.965563E+00 +1.999066E+00 +2.521586E-01 +2.732700E+01 +4.670990E+01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -360,30 +360,30 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -7.240906E+00 -3.296285E+00 -2.130816E+00 -2.850713E-01 -2.761433E+01 -4.771548E+01 -7.126427E+00 -3.199209E+00 -2.177254E+00 -2.998765E-01 -2.754936E+01 -4.748447E+01 -7.068817E+00 -3.139275E+00 -2.095865E+00 -2.762179E-01 -2.737835E+01 -4.688689E+01 -7.170567E+00 -3.240715E+00 -2.131758E+00 -2.862355E-01 -2.747702E+01 -4.722977E+01 +6.933695E+00 +3.028826E+00 +2.080336E+00 +2.719620E-01 +2.726925E+01 +4.650782E+01 +6.836291E+00 +2.935998E+00 +2.124868E+00 +2.841370E-01 +2.709685E+01 +4.591604E+01 +7.022610E+00 +3.098329E+00 +2.109973E+00 +2.824233E-01 +2.733826E+01 +4.676309E+01 +6.969559E+00 +3.054867E+00 +2.042871E+00 +2.624815E-01 +2.766332E+01 +4.787778E+01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -420,18 +420,18 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -7.126427E+00 -3.199209E+00 -2.177254E+00 -2.998765E-01 -2.754936E+01 -4.748447E+01 -7.240906E+00 -3.296285E+00 -2.130816E+00 -2.850713E-01 -2.761433E+01 -4.771548E+01 +6.836291E+00 +2.935998E+00 +2.124868E+00 +2.841370E-01 +2.709685E+01 +4.591604E+01 +6.933695E+00 +3.028826E+00 +2.080336E+00 +2.719620E-01 +2.726925E+01 +4.650782E+01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -444,18 +444,18 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -7.197270E+00 -3.255501E+00 -2.046179E+00 -2.630301E-01 -2.729901E+01 -4.662030E+01 -7.103896E+00 -3.171147E+00 -2.095666E+00 -2.756673E-01 -2.711842E+01 -4.600196E+01 +6.873220E+00 +2.965563E+00 +1.999066E+00 +2.521586E-01 +2.732700E+01 +4.670990E+01 +6.782152E+00 +2.885448E+00 +1.951138E+00 +2.400912E-01 +2.739652E+01 +4.697696E+01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -493,124 +493,124 @@ tally 4: 0.000000E+00 0.000000E+00 tally 5: -4.859695E+01 -1.489217E+02 -8.528963E+00 -4.561881E+00 -7.144500E+01 -3.194467E+02 -1.120265E+01 -7.944038E+00 -4.009821E+00 -1.012575E+00 -3.235456E+01 -6.558658E+01 -4.794474E+01 -1.445671E+02 -8.782187E+00 -4.852477E+00 -7.195036E+01 -3.237389E+02 -1.075572E+01 -7.333822E+00 -4.084680E+00 -1.054714E+00 -3.267154E+01 -6.675266E+01 -4.824407E+01 -1.459220E+02 -8.679106E+00 -4.742342E+00 -7.266858E+01 -3.302641E+02 -1.094872E+01 -7.536318E+00 -3.935828E+00 -9.749543E-01 -3.319517E+01 -6.894791E+01 -4.886677E+01 -1.500624E+02 -8.614512E+00 -4.662618E+00 -7.321408E+01 -3.352172E+02 -1.095123E+01 -7.574359E+00 -3.885927E+00 -9.539315E-01 -3.334065E+01 -6.953215E+01 +4.757160E+01 +1.420561E+02 +8.379464E+00 +4.426892E+00 +7.205748E+01 +3.248178E+02 +1.047742E+01 +6.922945E+00 +3.879420E+00 +9.517577E-01 +3.297227E+01 +6.802001E+01 +4.607211E+01 +1.329402E+02 +8.295180E+00 +4.334618E+00 +7.205259E+01 +3.247324E+02 +1.059307E+01 +7.039382E+00 +3.783039E+00 +9.030658E-01 +3.288052E+01 +6.762048E+01 +4.731403E+01 +1.403940E+02 +8.728060E+00 +4.774750E+00 +7.152809E+01 +3.200993E+02 +1.074636E+01 +7.270116E+00 +4.007351E+00 +1.010562E+00 +3.249303E+01 +6.611829E+01 +4.674806E+01 +1.375015E+02 +8.265491E+00 +4.296947E+00 +7.226174E+01 +3.267127E+02 +1.079228E+01 +7.340408E+00 +3.843888E+00 +9.273339E-01 +3.300391E+01 +6.823122E+01 cmfd indices 2.000000E+00 2.000000E+00 1.000000E+00 3.000000E+00 k cmfd -1.026473E+00 -1.024183E+00 -1.023151E+00 -1.025047E+00 -1.019801E+00 -1.020492E+00 -1.015249E+00 -1.016714E+00 -1.016047E+00 -1.019687E+00 -1.020955E+00 +1.011190E+00 +1.010705E+00 +1.014132E+00 +1.015900E+00 +1.019132E+00 +1.022616E+00 +1.023007E+00 +1.022300E+00 +1.014692E+00 +1.007628E+00 +1.006204E+00 cmfd entropy -1.999640E+00 -1.999556E+00 -1.999484E+00 -1.999718E+00 -1.999697E+00 -1.999657E+00 -1.999883E+00 -1.999902E+00 -1.999977E+00 -1.999977E+00 -1.999906E+00 +1.999167E+00 +1.999076E+00 +1.998507E+00 +1.997924E+00 +1.997814E+00 +1.997752E+00 +1.997882E+00 +1.998074E+00 +1.998109E+00 +1.998301E+00 +1.998581E+00 cmfd balance -8.10090E-04 -1.28103E-03 -7.97200E-04 -5.82188E-04 -7.20670E-04 -7.31475E-04 -5.71904E-04 -6.14057E-04 -6.00142E-04 -5.47870E-04 -3.53604E-04 +9.30124E-04 +2.56632E-04 +3.62598E-04 +4.17543E-04 +5.25720E-04 +4.90208E-04 +3.61304E-04 +2.24090E-04 +1.86602E-04 +1.78395E-04 +6.42497E-05 cmfd dominance ratio -3.977E-03 -4.018E-03 -3.950E-03 -3.866E-03 -3.840E-03 -3.888E-03 -3.867E-03 -3.896E-03 -3.924E-03 -3.885E-03 -3.913E-03 +4.194E-03 +4.234E-03 +4.149E-03 +4.209E-03 +4.185E-03 +4.197E-03 +4.175E-03 +4.105E-03 +4.056E-03 +4.122E-03 +4.113E-03 cmfd openmc source comparison -4.787501E-05 -4.450525E-05 -2.532345E-05 -3.844307E-05 -4.821504E-05 -4.840760E-05 -3.739246E-05 -3.957960E-05 -4.521480E-05 -4.072007E-05 -2.532636E-05 +2.078995E-05 +2.415218E-05 +3.311536E-05 +3.598613E-05 +3.156455E-05 +2.737017E-05 +2.468500E-05 +2.514215E-05 +1.601626E-05 +1.424027E-05 +4.201148E-06 cmfd source -2.486236E-01 -2.531628E-01 -2.460219E-01 -2.521918E-01 +2.558585E-01 +2.597562E-01 +2.529866E-01 +2.313986E-01 0.000000E+00 0.000000E+00 0.000000E+00 diff --git a/tests/regression_tests/cmfd_feed_rectlin/results_true.dat b/tests/regression_tests/cmfd_feed_rectlin/results_true.dat index 600b5d152..a98f3fbd3 100644 --- a/tests/regression_tests/cmfd_feed_rectlin/results_true.dat +++ b/tests/regression_tests/cmfd_feed_rectlin/results_true.dat @@ -1,149 +1,149 @@ k-combined: -1.160561E+00 1.029736E-02 +1.157362E+00 9.651846E-03 tally 1: -1.089904E+01 -1.193573E+01 -2.026534E+01 -4.113383E+01 -2.723584E+01 -7.440537E+01 -3.309956E+01 -1.101184E+02 -3.659327E+01 -1.341221E+02 -3.780158E+01 -1.430045E+02 -3.520883E+01 -1.241772E+02 -2.961801E+01 -8.784675E+01 -2.182029E+01 -4.781083E+01 -1.180347E+01 -1.399970E+01 +1.160989E+01 +1.351117E+01 +2.127132E+01 +4.540172E+01 +2.903242E+01 +8.450044E+01 +3.443549E+01 +1.188763E+02 +3.678332E+01 +1.355586E+02 +3.760088E+01 +1.418740E+02 +3.433077E+01 +1.181837E+02 +2.861986E+01 +8.231285E+01 +2.182277E+01 +4.792086E+01 +1.138713E+01 +1.304425E+01 tally 2: -8.794706E+00 -3.939413E+00 -6.131113E+00 -1.913116E+00 -3.265522E+01 -5.364042E+01 -2.304238E+01 -2.672742E+01 -2.250225E+01 -2.541491E+01 -1.601668E+01 -1.288405E+01 -5.525355E+01 -1.531915E+02 -3.929637E+01 -7.748545E+01 -3.222711E+01 -5.216630E+01 -2.285375E+01 -2.623641E+01 -7.051908E+01 -2.495413E+02 -5.010064E+01 -1.259701E+02 -3.728726E+01 -6.974440E+01 -2.652872E+01 -3.530919E+01 -3.747306E+01 -7.058235E+01 -2.681415E+01 -3.613314E+01 -7.296802E+01 -2.669214E+02 -5.202502E+01 -1.356733E+02 -3.333947E+01 -5.579355E+01 -2.361733E+01 -2.800800E+01 -5.785916E+01 -1.680561E+02 -4.093282E+01 -8.410711E+01 -2.377151E+01 -2.842464E+01 -1.681789E+01 -1.423646E+01 -3.422028E+01 -5.880493E+01 -2.415199E+01 -2.930240E+01 -8.890608E+00 -3.986356E+00 -6.140159E+00 -1.897764E+00 +8.861425E+00 +3.953963E+00 +6.090757E+00 +1.866235E+00 +3.309260E+01 +5.493482E+01 +2.330765E+01 +2.725350E+01 +2.295343E+01 +2.647375E+01 +1.629166E+01 +1.334081E+01 +5.714234E+01 +1.640293E+02 +4.054051E+01 +8.261123E+01 +3.331786E+01 +5.565082E+01 +2.366345E+01 +2.807637E+01 +7.034800E+01 +2.485209E+02 +5.001286E+01 +1.256087E+02 +3.651223E+01 +6.686058E+01 +2.606851E+01 +3.408881E+01 +3.729833E+01 +6.997492E+01 +2.657970E+01 +3.553107E+01 +7.212382E+01 +2.611253E+02 +5.124647E+01 +1.318756E+02 +3.304529E+01 +5.486643E+01 +2.347504E+01 +2.771353E+01 +5.711252E+01 +1.640619E+02 +4.060288E+01 +8.298231E+01 +2.384078E+01 +2.855909E+01 +1.684046E+01 +1.426180E+01 +3.329487E+01 +5.573544E+01 +2.349871E+01 +2.776487E+01 +8.676886E+00 +3.814132E+00 +5.980976E+00 +1.815143E+00 tally 3: -5.925339E+00 -1.788596E+00 -3.912632E-01 -8.061838E-03 -2.215544E+01 -2.471404E+01 -1.465191E+00 -1.092622E-01 -1.542988E+01 -1.195626E+01 -1.022876E+00 -5.356825E-02 -3.792029E+01 -7.218149E+01 -2.370476E+00 -2.839465E-01 -2.200154E+01 -2.432126E+01 -1.360836E+00 -9.402424E-02 -4.824980E+01 -1.168447E+02 -3.124366E+00 -4.907460E-01 -2.557420E+01 -3.282066E+01 -1.725855E+00 -1.519830E-01 -2.577963E+01 -3.341077E+01 -1.654042E+00 -1.386845E-01 -5.008220E+01 -1.257610E+02 -3.223857E+00 -5.237360E-01 -2.273380E+01 -2.595325E+01 -1.438369E+00 -1.050840E-01 -3.938822E+01 -7.789691E+01 -2.648324E+00 -3.559703E-01 -1.623604E+01 -1.327214E+01 -1.058882E+00 -5.680630E-02 -2.325730E+01 -2.717892E+01 -1.584276E+00 -1.275868E-01 -5.937929E+00 -1.774847E+00 -3.866918E-01 -7.994353E-03 +5.847135E+00 +1.719612E+00 +3.960040E-01 +8.297721E-03 +2.245534E+01 +2.530222E+01 +1.468678E+00 +1.094031E-01 +1.571194E+01 +1.240597E+01 +1.004169E+00 +5.156464E-02 +3.901605E+01 +7.652715E+01 +2.648696E+00 +3.571840E-01 +2.275978E+01 +2.597555E+01 +1.456067E+00 +1.075035E-01 +4.821184E+01 +1.167725E+02 +3.105774E+00 +4.859873E-01 +2.519281E+01 +3.183845E+01 +1.595498E+00 +1.292133E-01 +2.560583E+01 +3.297409E+01 +1.673871E+00 +1.429651E-01 +4.930025E+01 +1.220909E+02 +3.206604E+00 +5.220919E-01 +2.255825E+01 +2.560134E+01 +1.422870E+00 +1.027151E-01 +3.910818E+01 +7.698933E+01 +2.568903E+00 +3.333385E-01 +1.620292E+01 +1.321173E+01 +1.068678E+00 +5.798793E-02 +2.264343E+01 +2.578648E+01 +1.503553E+00 +1.158411E-01 +5.751110E+00 +1.676910E+00 +3.450582E-01 +6.411784E-03 tally 4: -3.063235E+00 -4.714106E-01 +3.051764E+00 +4.671879E-01 0.000000E+00 0.000000E+00 -1.425705E+00 -1.043616E-01 -4.355515E+00 -9.538346E-01 +1.407008E+00 +1.004485E-01 +4.354708E+00 +9.506434E-01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -160,14 +160,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -4.355515E+00 -9.538346E-01 -1.425705E+00 -1.043616E-01 -3.859174E+00 -7.519499E-01 -6.350310E+00 -2.024214E+00 +4.354708E+00 +9.506434E-01 +1.407008E+00 +1.004485E-01 +3.852730E+00 +7.498016E-01 +6.382605E+00 +2.043123E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -184,14 +184,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -6.350310E+00 -2.024214E+00 -3.859174E+00 -7.519499E-01 -4.993073E+00 -1.258264E+00 -7.194275E+00 -2.596886E+00 +6.382605E+00 +2.043123E+00 +3.852730E+00 +7.498016E-01 +5.061607E+00 +1.288306E+00 +7.281209E+00 +2.659444E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -208,14 +208,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -7.194275E+00 -2.596886E+00 -4.993073E+00 -1.258264E+00 -6.786617E+00 -2.312259E+00 -8.306978E+00 -3.459668E+00 +7.281209E+00 +2.659444E+00 +5.061607E+00 +1.288306E+00 +7.096602E+00 +2.527474E+00 +8.632232E+00 +3.736665E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -232,14 +232,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -8.306978E+00 -3.459668E+00 -6.786617E+00 -2.312259E+00 -7.603410E+00 -2.905228E+00 -8.791222E+00 -3.882935E+00 +8.632232E+00 +3.736665E+00 +7.096602E+00 +2.527474E+00 +7.759456E+00 +3.019026E+00 +8.968687E+00 +4.037738E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -256,14 +256,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -8.791222E+00 -3.882935E+00 -7.603410E+00 -2.905228E+00 -8.867113E+00 -3.938503E+00 -9.302808E+00 -4.340042E+00 +8.968687E+00 +4.037738E+00 +7.759456E+00 +3.019026E+00 +8.749025E+00 +3.839161E+00 +9.126289E+00 +4.176440E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -280,14 +280,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -9.302808E+00 -4.340042E+00 -8.867113E+00 -3.938503E+00 -9.270113E+00 -4.306513E+00 -9.263471E+00 -4.302184E+00 +9.126289E+00 +4.176440E+00 +8.749025E+00 +3.839161E+00 +9.259277E+00 +4.304020E+00 +9.165726E+00 +4.216173E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -304,14 +304,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -9.263471E+00 -4.302184E+00 -9.270113E+00 -4.306513E+00 -9.348712E+00 -4.388570E+00 -8.977713E+00 -4.047349E+00 +9.165726E+00 +4.216173E+00 +9.259277E+00 +4.304020E+00 +9.433925E+00 +4.473551E+00 +8.910566E+00 +3.991458E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -328,14 +328,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -8.977713E+00 -4.047349E+00 -9.348712E+00 -4.388570E+00 -9.234380E+00 -4.280666E+00 -8.036978E+00 -3.239853E+00 +8.910566E+00 +3.991458E+00 +9.433925E+00 +4.473551E+00 +9.099397E+00 +4.154062E+00 +7.770126E+00 +3.029928E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -352,14 +352,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -8.036978E+00 -3.239853E+00 -9.234380E+00 -4.280666E+00 -8.713633E+00 -3.808850E+00 -7.160878E+00 -2.572915E+00 +7.770126E+00 +3.029928E+00 +9.099397E+00 +4.154062E+00 +8.575842E+00 +3.694998E+00 +6.934243E+00 +2.418570E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -376,14 +376,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -7.160878E+00 -2.572915E+00 -8.713633E+00 -3.808850E+00 -7.378538E+00 -2.732122E+00 -5.145728E+00 -1.329190E+00 +6.934243E+00 +2.418570E+00 +8.575842E+00 +3.694998E+00 +7.437526E+00 +2.780629E+00 +5.136923E+00 +1.331553E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -400,14 +400,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -5.145728E+00 -1.329190E+00 -7.378538E+00 -2.732122E+00 -6.660125E+00 -2.228506E+00 -4.087925E+00 -8.435381E-01 +5.136923E+00 +1.331553E+00 +7.437526E+00 +2.780629E+00 +6.648582E+00 +2.216687E+00 +4.050691E+00 +8.279789E-01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -424,14 +424,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -4.087925E+00 -8.435381E-01 -6.660125E+00 -2.228506E+00 -4.466295E+00 -1.002320E+00 -1.468481E+00 -1.099604E-01 +4.050691E+00 +8.279789E-01 +6.648582E+00 +2.216687E+00 +4.390906E+00 +9.686686E-01 +1.391624E+00 +9.861955E-02 0.000000E+00 0.000000E+00 0.000000E+00 @@ -448,12 +448,12 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -1.468481E+00 -1.099604E-01 -4.466295E+00 -1.002320E+00 -3.139355E+00 -4.955456E-01 +1.391624E+00 +9.861955E-02 +4.390906E+00 +9.686686E-01 +3.113477E+00 +4.874296E-01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -473,164 +473,164 @@ tally 4: 0.000000E+00 0.000000E+00 tally 5: -5.925339E+00 -1.788596E+00 -8.954773E-01 -4.217679E-02 -2.215054E+01 -2.470273E+01 -2.944747E+00 -4.443186E-01 -1.542658E+01 -1.195085E+01 -1.950262E+00 -1.946340E-01 -3.791137E+01 -7.214779E+01 -4.955741E+00 -1.254830E+00 -2.200054E+01 -2.431894E+01 -3.031040E+00 -4.685971E-01 -4.823946E+01 -1.167941E+02 -6.226321E+00 -1.956308E+00 -2.556930E+01 -3.280828E+01 -3.582546E+00 -6.505962E-01 -2.577249E+01 -3.339204E+01 -3.316825E+00 -5.676842E-01 -5.007249E+01 -1.257124E+02 -6.462364E+00 -2.120157E+00 -2.273285E+01 -2.595106E+01 -2.960964E+00 -4.496277E-01 -3.937362E+01 -7.783849E+01 -5.339336E+00 -1.449257E+00 -1.623315E+01 -1.326746E+01 -2.289661E+00 -2.698618E-01 -2.325250E+01 -2.716827E+01 -3.253010E+00 -5.413837E-01 -5.937929E+00 -1.774847E+00 -9.208589E-01 -4.421403E-02 +5.846103E+00 +1.718959E+00 +8.952028E-01 +4.184063E-02 +2.245226E+01 +2.529524E+01 +3.092645E+00 +4.848878E-01 +1.571095E+01 +1.240426E+01 +2.178244E+00 +2.438479E-01 +3.900638E+01 +7.648853E+01 +5.265574E+00 +1.401505E+00 +2.275895E+01 +2.597348E+01 +3.067909E+00 +4.810969E-01 +4.820213E+01 +1.167232E+02 +6.403602E+00 +2.070034E+00 +2.519091E+01 +3.183360E+01 +3.463531E+00 +6.097568E-01 +2.560388E+01 +3.296889E+01 +3.456252E+00 +6.101655E-01 +4.929539E+01 +1.220666E+02 +6.629094E+00 +2.223366E+00 +2.255498E+01 +2.559363E+01 +2.833426E+00 +4.150907E-01 +3.909813E+01 +7.694976E+01 +5.582222E+00 +1.584757E+00 +1.620082E+01 +1.320814E+01 +2.282196E+00 +2.703156E-01 +2.263498E+01 +2.576758E+01 +3.162736E+00 +5.145038E-01 +5.750110E+00 +1.676357E+00 +9.181679E-01 +4.562885E-02 cmfd indices 1.400000E+01 1.000000E+00 1.000000E+00 1.000000E+00 k cmfd -1.125528E+00 -1.145509E+00 -1.158948E+00 -1.171983E+00 -1.180649E+00 -1.184072E+00 -1.188112E+00 -1.183095E+00 -1.182269E+00 -1.175467E+00 -1.175184E+00 -1.172637E+00 -1.171593E+00 -1.175439E+00 -1.174650E+00 -1.176474E+00 +1.166740E+00 +1.184008E+00 +1.166534E+00 +1.155559E+00 +1.164960E+00 +1.163229E+00 +1.165897E+00 +1.170104E+00 +1.170207E+00 +1.168091E+00 +1.170940E+00 +1.174589E+00 +1.174609E+00 +1.171505E+00 +1.174456E+00 +1.178370E+00 cmfd entropy -3.607059E+00 -3.604890E+00 -3.601329E+00 -3.597776E+00 -3.597360E+00 -3.597387E+00 -3.595379E+00 -3.596995E+00 -3.600901E+00 -3.601832E+00 -3.601426E+00 -3.604521E+00 -3.602848E+00 -3.603875E+00 -3.605213E+00 -3.606699E+00 +3.594757E+00 +3.587018E+00 +3.590385E+00 +3.595101E+00 +3.592151E+00 +3.600294E+00 +3.602102E+00 +3.604941E+00 +3.605897E+00 +3.604880E+00 +3.601658E+00 +3.602551E+00 +3.600160E+00 +3.604540E+00 +3.604094E+00 +3.602509E+00 cmfd balance -4.46212E-03 -4.66648E-03 -5.04274E-03 -5.21553E-03 -3.92498E-03 -2.97185E-03 -2.79785E-03 -2.66951E-03 -2.17472E-03 -1.98009E-03 -1.77035E-03 -1.51281E-03 -1.52807E-03 -1.33341E-03 -1.18155E-03 -1.07752E-03 +5.52960E-03 +5.42154E-03 +3.62152E-03 +2.92850E-03 +4.08642E-03 +2.07444E-03 +2.03704E-03 +2.06886E-03 +2.09646E-03 +1.94256E-03 +2.02728E-03 +1.89830E-03 +1.83910E-03 +1.48140E-03 +1.47034E-03 +1.64452E-03 cmfd dominance ratio -6.136E-01 -6.127E-01 -6.137E-01 -6.102E-01 -6.067E-01 -6.061E-01 -6.031E-01 6.046E-01 -6.071E-01 -6.089E-01 -6.073E-01 -6.080E-01 -6.080E-01 +6.015E-01 +6.059E-01 +6.060E-01 +6.061E-01 +6.109E-01 +6.110E-01 +6.108E-01 +6.120E-01 +6.124E-01 +6.109E-01 6.090E-01 -6.092E-01 -6.094E-01 +6.116E-01 +6.137E-01 +6.117E-01 +6.131E-01 cmfd openmc source comparison -1.043027E-02 -1.278226E-02 -1.184867E-02 -1.017186E-02 -1.099696E-02 -7.955341E-03 -8.360344E-03 -6.875508E-03 -4.824018E-03 -4.915363E-03 -5.371647E-03 -4.593100E-03 -4.894955E-03 -4.928253E-03 -4.292171E-03 -4.018545E-03 +1.035187E-02 +9.394886E-03 +6.879487E-03 +7.236029E-03 +6.543528E-03 +3.600620E-03 +2.859638E-03 +2.230047E-03 +2.180643E-03 +1.638534E-03 +1.764349E-03 +1.621487E-03 +1.221762E-03 +1.626297E-03 +1.951813E-03 +9.584126E-04 cmfd source -1.600876E-02 -6.000305E-02 -4.248071E-02 -9.935789E-02 -5.768092E-02 -1.338593E-01 -7.417398E-02 -7.102984E-02 -1.382563E-01 -6.181889E-02 -1.142473E-01 -4.571447E-02 -6.864655E-02 -1.672206E-02 +1.677059E-02 +6.229453E-02 +4.278394E-02 +1.134852E-01 +6.231020E-02 +1.327286E-01 +6.808362E-02 +7.130954E-02 +1.362127E-01 +6.048013E-02 +1.094460E-01 +4.546687E-02 +6.403310E-02 +1.459510E-02 diff --git a/tests/regression_tests/cmfd_feed_ref_d/results_true.dat b/tests/regression_tests/cmfd_feed_ref_d/results_true.dat index 9cc26b8cf..34cde1a46 100644 --- a/tests/regression_tests/cmfd_feed_ref_d/results_true.dat +++ b/tests/regression_tests/cmfd_feed_ref_d/results_true.dat @@ -1,117 +1,117 @@ k-combined: -1.167869E+00 7.492916E-03 +1.162249E+00 5.812620E-03 tally 1: -1.146821E+01 -1.318787E+01 -2.161476E+01 -4.685066E+01 -2.951084E+01 -8.733116E+01 -3.521523E+01 -1.242762E+02 -3.774181E+01 -1.426460E+02 -3.727924E+01 -1.391162E+02 -3.377236E+01 -1.143877E+02 -2.904590E+01 -8.453427E+01 -2.090941E+01 -4.384824E+01 -1.078680E+01 -1.168172E+01 +1.153831E+01 +1.338142E+01 +2.155552E+01 +4.659071E+01 +2.813997E+01 +7.941672E+01 +3.270996E+01 +1.073664E+02 +3.639852E+01 +1.329148E+02 +3.729637E+01 +1.393474E+02 +3.443129E+01 +1.186461E+02 +2.832690E+01 +8.040998E+01 +2.177527E+01 +4.771147E+01 +1.146822E+01 +1.328252E+01 tally 2: -1.136805E+00 -1.292326E+00 -7.987282E-01 -6.379667E-01 -2.266961E+00 -5.139112E+00 -1.613498E+00 -2.603376E+00 -3.046379E+00 -9.280427E+00 -2.182480E+00 -4.763219E+00 -3.568101E+00 -1.273134E+01 -2.532478E+00 -6.413444E+00 -3.989532E+00 -1.591637E+01 -2.848319E+00 -8.112921E+00 -3.853139E+00 -1.484668E+01 -2.718497E+00 -7.390223E+00 -3.478134E+00 -1.209742E+01 -2.467279E+00 -6.087465E+00 -2.952214E+00 -8.715569E+00 -2.103257E+00 -4.423688E+00 -1.917446E+00 -3.676599E+00 -1.378361E+00 -1.899878E+00 -1.048230E+00 -1.098785E+00 -7.511876E-01 -5.642828E-01 +1.024353E+00 +1.049299E+00 +6.991057E-01 +4.887487E-01 +2.200432E+00 +4.841901E+00 +1.561655E+00 +2.438768E+00 +2.910400E+00 +8.470426E+00 +2.095155E+00 +4.389674E+00 +3.466006E+00 +1.201320E+01 +2.480456E+00 +6.152662E+00 +3.711781E+00 +1.377732E+01 +2.646019E+00 +7.001418E+00 +3.953648E+00 +1.563133E+01 +2.832759E+00 +8.024524E+00 +3.597870E+00 +1.294467E+01 +2.555396E+00 +6.530048E+00 +2.860871E+00 +8.184585E+00 +2.032282E+00 +4.130169E+00 +2.006740E+00 +4.027007E+00 +1.408150E+00 +1.982886E+00 +1.035163E+00 +1.071562E+00 +7.084068E-01 +5.018402E-01 tally 3: -7.701212E-01 -5.930866E-01 -4.481580E-02 -2.008456E-03 -1.547321E+00 -2.394203E+00 -1.226538E-01 -1.504395E-02 -2.106393E+00 -4.436893E+00 -1.450617E-01 -2.104289E-02 -2.437675E+00 -5.942260E+00 -1.521379E-01 -2.314593E-02 -2.754657E+00 -7.588135E+00 -1.745458E-01 -3.046622E-02 -2.623856E+00 -6.884619E+00 -1.851600E-01 -3.428423E-02 -2.376884E+00 -5.649579E+00 -1.615728E-01 -2.610576E-02 -2.021851E+00 -4.087882E+00 -1.533172E-01 -2.350617E-02 -1.333182E+00 -1.777374E+00 -7.076179E-02 -5.007231E-03 -7.258458E-01 -5.268521E-01 -3.656026E-02 -1.336653E-03 +6.713566E-01 +4.507196E-01 +5.581718E-02 +3.115557E-03 +1.508976E+00 +2.277009E+00 +1.139601E-01 +1.298690E-02 +2.035529E+00 +4.143377E+00 +1.221001E-01 +1.490843E-02 +2.385217E+00 +5.689262E+00 +1.500087E-01 +2.250260E-02 +2.546286E+00 +6.483574E+00 +1.546601E-01 +2.391975E-02 +2.726427E+00 +7.433407E+00 +1.686144E-01 +2.843081E-02 +2.466613E+00 +6.084179E+00 +1.558230E-01 +2.428079E-02 +1.951251E+00 +3.807379E+00 +1.197744E-01 +1.434590E-02 +1.347903E+00 +1.816842E+00 +9.419149E-02 +8.872037E-03 +6.840490E-01 +4.679231E-01 +5.000289E-02 +2.500289E-03 tally 4: -1.667426E-01 -2.780308E-02 +1.561665E-01 +2.438798E-02 0.000000E+00 0.000000E+00 -1.292556E-01 -1.670700E-02 -2.813401E-01 -7.915227E-02 +1.307011E-01 +1.708276E-02 +2.703168E-01 +7.307115E-02 0.000000E+00 0.000000E+00 0.000000E+00 @@ -128,14 +128,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -2.813401E-01 -7.915227E-02 -1.292556E-01 -1.670700E-02 -2.670582E-01 -7.132006E-02 -4.055365E-01 -1.644599E-01 +2.703168E-01 +7.307115E-02 +1.307011E-01 +1.708276E-02 +2.637619E-01 +6.957033E-02 +3.685390E-01 +1.358210E-01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -152,14 +152,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -4.055365E-01 -1.644599E-01 -2.670582E-01 -7.132006E-02 -3.848164E-01 -1.480837E-01 -4.809472E-01 -2.313102E-01 +3.685390E-01 +1.358210E-01 +2.637619E-01 +6.957033E-02 +3.887017E-01 +1.510890E-01 +4.439407E-01 +1.970834E-01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -176,14 +176,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -4.809472E-01 -2.313102E-01 -3.848164E-01 -1.480837E-01 -4.543959E-01 -2.064756E-01 -5.106174E-01 -2.607301E-01 +4.439407E-01 +1.970834E-01 +3.887017E-01 +1.510890E-01 +4.456116E-01 +1.985697E-01 +4.737035E-01 +2.243950E-01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -200,14 +200,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -5.106174E-01 -2.607301E-01 -4.543959E-01 -2.064756E-01 -4.543155E-01 -2.064026E-01 -4.626331E-01 -2.140294E-01 +4.737035E-01 +2.243950E-01 +4.456116E-01 +1.985697E-01 +4.760577E-01 +2.266309E-01 +4.703245E-01 +2.212052E-01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -224,14 +224,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -4.626331E-01 -2.140294E-01 -4.543155E-01 -2.064026E-01 -4.827763E-01 -2.330729E-01 -4.442611E-01 -1.973679E-01 +4.703245E-01 +2.212052E-01 +4.760577E-01 +2.266309E-01 +4.878056E-01 +2.379543E-01 +4.373120E-01 +1.912418E-01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -248,14 +248,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -4.442611E-01 -1.973679E-01 -4.827763E-01 -2.330729E-01 -4.630415E-01 -2.144074E-01 -3.886521E-01 -1.510505E-01 +4.373120E-01 +1.912418E-01 +4.878056E-01 +2.379543E-01 +4.262194E-01 +1.816630E-01 +3.334152E-01 +1.111657E-01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -272,14 +272,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -3.886521E-01 -1.510505E-01 -4.630415E-01 -2.144074E-01 -3.535862E-01 -1.250232E-01 -2.530293E-01 -6.402384E-02 +3.334152E-01 +1.111657E-01 +4.262194E-01 +1.816630E-01 +3.560156E-01 +1.267471E-01 +2.409954E-01 +5.807879E-02 0.000000E+00 0.000000E+00 0.000000E+00 @@ -296,14 +296,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -2.530293E-01 -6.402384E-02 -3.535862E-01 -1.250232E-01 -2.465508E-01 -6.078730E-02 -1.197139E-01 -1.433141E-02 +2.409954E-01 +5.807879E-02 +3.560156E-01 +1.267471E-01 +2.646501E-01 +7.003965E-02 +1.327244E-01 +1.761576E-02 0.000000E+00 0.000000E+00 0.000000E+00 @@ -320,12 +320,12 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -1.197139E-01 -1.433141E-02 -2.465508E-01 -6.078730E-02 -1.369614E-01 -1.875841E-02 +1.327244E-01 +1.761576E-02 +2.646501E-01 +7.003965E-02 +1.480567E-01 +2.192079E-02 0.000000E+00 0.000000E+00 0.000000E+00 @@ -345,119 +345,119 @@ tally 4: 0.000000E+00 0.000000E+00 tally 5: -7.701212E-01 -5.930866E-01 -1.386254E-01 -1.921700E-02 -1.547321E+00 -2.394203E+00 -2.630318E-01 -6.918571E-02 -2.106393E+00 -4.436893E+00 -2.807911E-01 -7.884363E-02 -2.435870E+00 -5.933464E+00 -3.322093E-01 -1.103630E-01 -2.753652E+00 -7.582597E+00 -3.825961E-01 -1.463797E-01 -2.623856E+00 -6.884619E+00 -3.888719E-01 -1.512213E-01 -2.376884E+00 -5.649579E+00 -3.196211E-01 -1.021576E-01 -2.021851E+00 -4.087882E+00 -2.897873E-01 -8.397667E-02 -1.333182E+00 -1.777374E+00 -1.627096E-01 -2.647441E-02 -7.258458E-01 -5.268521E-01 -9.348575E-02 -8.739586E-03 +6.713566E-01 +4.507196E-01 +9.805793E-02 +9.615358E-03 +1.508976E+00 +2.277009E+00 +1.968348E-01 +3.874394E-02 +2.032573E+00 +4.131353E+00 +2.120922E-01 +4.498309E-02 +2.385217E+00 +5.689262E+00 +2.863031E-01 +8.196946E-02 +2.545200E+00 +6.478041E+00 +3.278920E-01 +1.075131E-01 +2.726427E+00 +7.433407E+00 +3.770758E-01 +1.421861E-01 +2.466613E+00 +6.084179E+00 +3.593230E-01 +1.291130E-01 +1.951251E+00 +3.807379E+00 +2.474112E-01 +6.121229E-02 +1.347903E+00 +1.816842E+00 +2.127109E-01 +4.524594E-02 +6.823620E-01 +4.656179E-01 +1.249863E-01 +1.562159E-02 cmfd indices 1.000000E+01 1.000000E+00 1.000000E+00 1.000000E+00 k cmfd -1.149087E+00 -1.156777E+00 -1.158641E+00 -1.159507E+00 -1.156564E+00 -1.160257E+00 -1.150344E+00 -1.149854E+00 -1.151616E+00 -1.164575E+00 -1.174683E+00 +1.181365E+00 +1.176693E+00 +1.161946E+00 +1.163565E+00 +1.163043E+00 +1.169908E+00 +1.149155E+00 +1.142379E+00 +1.152957E+00 +1.137602E+00 +1.141883E+00 cmfd entropy -3.216202E+00 -3.228703E+00 -3.220414E+00 -3.214361E+00 -3.215642E+00 -3.213607E+00 -3.212862E+00 -3.213128E+00 -3.213189E+00 -3.205465E+00 -3.202859E+00 +3.246422E+00 +3.246496E+00 +3.252238E+00 +3.240920E+00 +3.237606E+00 +3.234301E+00 +3.234103E+00 +3.229918E+00 +3.226983E+00 +3.221321E+00 +3.223622E+00 cmfd balance -3.08825E-03 -1.42554E-03 -1.21448E-03 -1.17859E-03 -1.06034E-03 -9.30949E-04 -1.35713E-03 -1.13694E-03 -1.14938E-03 -1.29296E-03 -1.46518E-03 +4.18486E-03 +1.72126E-03 +1.10906E-03 +1.88158E-03 +1.31626E-03 +1.30818E-03 +1.77315E-03 +2.16148E-03 +1.67789E-03 +2.31333E-03 +1.94932E-03 cmfd dominance ratio -5.503E-01 -5.596E-01 5.505E-01 -5.471E-01 -5.468E-01 -5.427E-01 -5.421E-01 -5.412E-01 -5.389E-01 -5.371E-01 -5.329E-01 +5.580E-01 +5.610E-01 +5.526E-01 +5.519E-01 +5.499E-01 +5.504E-01 +5.504E-01 +5.475E-01 +5.465E-01 +5.477E-01 cmfd openmc source comparison -1.571006E-02 -6.945629E-03 -6.838511E-03 -6.183655E-03 -5.138825E-03 -4.362701E-03 -5.558586E-03 -4.188314E-03 -1.837101E-03 -2.737321E-03 -2.529244E-03 +1.902234E-03 +4.110960E-03 +2.452031E-03 +2.337951E-03 +1.838979E-03 +3.138637E-03 +2.684401E-03 +2.912891E-03 +2.823494E-03 +6.391584E-03 +5.904139E-03 cmfd source -4.240947E-02 -8.226746E-02 -1.180848E-01 -1.328470E-01 -1.412449E-01 -1.424870E-01 -1.269297E-01 -1.096476E-01 -6.953001E-02 -3.455212E-02 +4.488002E-02 +8.895136E-02 +1.085930E-01 +1.229651E-01 +1.330479E-01 +1.497140E-01 +1.309102E-01 +1.028556E-01 +7.738878E-02 +4.069397E-02 diff --git a/tests/regression_tests/cmfd_feed_rolling_window/results_true.dat b/tests/regression_tests/cmfd_feed_rolling_window/results_true.dat index 5ff4a132f..2e31b35ba 100644 --- a/tests/regression_tests/cmfd_feed_rolling_window/results_true.dat +++ b/tests/regression_tests/cmfd_feed_rolling_window/results_true.dat @@ -1,117 +1,117 @@ k-combined: -1.173626E+00 1.098719E-02 +1.158333E+00 1.402684E-02 tally 1: -1.101892E+01 -1.218768E+01 -2.036233E+01 -4.152577E+01 -2.937587E+01 -8.637268E+01 -3.502389E+01 -1.231700E+02 -3.804803E+01 -1.453948E+02 -3.822561E+01 -1.465677E+02 -3.456290E+01 -1.198651E+02 -2.904088E+01 -8.470264E+01 -2.111529E+01 -4.463713E+01 -1.147633E+01 -1.326012E+01 +1.169478E+01 +1.373162E+01 +2.192038E+01 +4.844559E+01 +2.913292E+01 +8.542569E+01 +3.446069E+01 +1.201782E+02 +3.624088E+01 +1.320213E+02 +3.569791E+01 +1.278170E+02 +3.340601E+01 +1.119165E+02 +2.908648E+01 +8.514603E+01 +2.175458E+01 +4.767916E+01 +1.171268E+01 +1.378033E+01 tally 2: -1.010478E+00 -1.021066E+00 -6.902031E-01 -4.763804E-01 -1.899891E+00 -3.609584E+00 -1.322615E+00 -1.749312E+00 -2.756419E+00 -7.597845E+00 -1.955934E+00 -3.825676E+00 -3.818740E+00 -1.458278E+01 -2.704746E+00 -7.315652E+00 -3.920857E+00 -1.537312E+01 -2.843144E+00 -8.083470E+00 -3.835060E+00 -1.470768E+01 -2.728705E+00 -7.445831E+00 -3.510590E+00 -1.232424E+01 -2.497237E+00 -6.236191E+00 -2.717388E+00 -7.384198E+00 -1.903638E+00 -3.623837E+00 -2.207863E+00 -4.874659E+00 -1.563588E+00 -2.444808E+00 -1.289027E+00 -1.661591E+00 -9.022714E-01 -8.140937E-01 +1.132414E+00 +1.282361E+00 +7.822980E-01 +6.119901E-01 +2.124428E+00 +4.513196E+00 +1.490832E+00 +2.222581E+00 +3.158472E+00 +9.975946E+00 +2.221665E+00 +4.935795E+00 +3.994786E+00 +1.595831E+01 +2.828109E+00 +7.998199E+00 +3.491035E+00 +1.218732E+01 +2.461223E+00 +6.057617E+00 +3.461784E+00 +1.198395E+01 +2.473337E+00 +6.117398E+00 +3.027132E+00 +9.163527E+00 +2.150455E+00 +4.624458E+00 +2.471217E+00 +6.106911E+00 +1.737168E+00 +3.017752E+00 +1.880969E+00 +3.538044E+00 +1.316574E+00 +1.733367E+00 +1.233103E+00 +1.520543E+00 +8.543318E-01 +7.298828E-01 tally 3: -6.593197E-01 -4.347024E-01 -5.216387E-02 -2.721069E-03 -1.266446E+00 -1.603885E+00 -8.298797E-02 -6.887003E-03 -1.888709E+00 -3.567222E+00 -1.398940E-01 -1.957033E-02 -2.616078E+00 -6.843867E+00 -1.588627E-01 -2.523735E-02 -2.733300E+00 -7.470927E+00 -1.944290E-01 -3.780262E-02 -2.643656E+00 -6.988917E+00 -1.612338E-01 -2.599633E-02 -2.410643E+00 -5.811199E+00 -1.754603E-01 -3.078631E-02 -1.838012E+00 -3.378288E+00 -1.126265E-01 -1.268473E-02 -1.500033E+00 -2.250100E+00 -1.102554E-01 -1.215626E-02 -8.750096E-01 -7.656418E-01 -6.757592E-02 -4.566505E-03 +7.540113E-01 +5.685330E-01 +6.367610E-02 +4.054646E-03 +1.436984E+00 +2.064922E+00 +8.961822E-02 +8.031425E-03 +2.132240E+00 +4.546449E+00 +1.356065E-01 +1.838913E-02 +2.726356E+00 +7.433016E+00 +1.780572E-01 +3.170438E-02 +2.379700E+00 +5.662970E+00 +1.544735E-01 +2.386206E-02 +2.383471E+00 +5.680933E+00 +1.568319E-01 +2.459624E-02 +2.047271E+00 +4.191318E+00 +1.662654E-01 +2.764418E-02 +1.673107E+00 +2.799287E+00 +1.132020E-01 +1.281468E-02 +1.271576E+00 +1.616906E+00 +7.546797E-02 +5.695415E-03 +8.249906E-01 +6.806094E-01 +5.660098E-02 +3.203671E-03 tally 4: -1.490605E-01 -2.221904E-02 +1.551630E-01 +2.407556E-02 0.000000E+00 0.000000E+00 -1.139233E-01 -1.297851E-02 -2.549497E-01 -6.499934E-02 +1.487992E-01 +2.214121E-02 +2.878091E-01 +8.283409E-02 0.000000E+00 0.000000E+00 0.000000E+00 @@ -128,14 +128,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -2.549497E-01 -6.499934E-02 -1.139233E-01 -1.297851E-02 -2.191337E-01 -4.801958E-02 -3.295187E-01 -1.085826E-01 +2.878091E-01 +8.283409E-02 +1.487992E-01 +2.214121E-02 +2.936596E-01 +8.623595E-02 +3.954149E-01 +1.563529E-01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -152,14 +152,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -3.295187E-01 -1.085826E-01 -2.191337E-01 -4.801958E-02 -3.872400E-01 -1.499548E-01 -4.595835E-01 -2.112170E-01 +3.954149E-01 +1.563529E-01 +2.936596E-01 +8.623595E-02 +3.991153E-01 +1.592930E-01 +4.758410E-01 +2.264247E-01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -176,14 +176,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -4.595835E-01 -2.112170E-01 -3.872400E-01 -1.499548E-01 -4.668106E-01 -2.179121E-01 -5.112307E-01 -2.613569E-01 +4.758410E-01 +2.264247E-01 +3.991153E-01 +1.592930E-01 +4.850882E-01 +2.353106E-01 +5.210840E-01 +2.715285E-01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -200,14 +200,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -5.112307E-01 -2.613569E-01 -4.668106E-01 -2.179121E-01 -4.716605E-01 -2.224636E-01 -4.916148E-01 -2.416851E-01 +5.210840E-01 +2.715285E-01 +4.850882E-01 +2.353106E-01 +4.790245E-01 +2.294645E-01 +4.570092E-01 +2.088574E-01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -224,14 +224,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -4.916148E-01 -2.416851E-01 -4.716605E-01 -2.224636E-01 -4.696777E-01 -2.205972E-01 -4.365150E-01 -1.905453E-01 +4.570092E-01 +2.088574E-01 +4.790245E-01 +2.294645E-01 +4.505886E-01 +2.030301E-01 +3.884038E-01 +1.508575E-01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -248,14 +248,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -4.365150E-01 -1.905453E-01 -4.696777E-01 -2.205972E-01 -4.179902E-01 -1.747158E-01 -3.350564E-01 -1.122628E-01 +3.884038E-01 +1.508575E-01 +4.505886E-01 +2.030301E-01 +3.986999E-01 +1.589616E-01 +3.220889E-01 +1.037412E-01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -272,14 +272,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -3.350564E-01 -1.122628E-01 -4.179902E-01 -1.747158E-01 -3.743487E-01 -1.401370E-01 -2.400661E-01 -5.763172E-02 +3.220889E-01 +1.037412E-01 +3.986999E-01 +1.589616E-01 +3.319083E-01 +1.101631E-01 +2.101261E-01 +4.415297E-02 0.000000E+00 0.000000E+00 0.000000E+00 @@ -296,14 +296,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -2.400661E-01 -5.763172E-02 -3.743487E-01 -1.401370E-01 -3.063657E-01 -9.385994E-02 -1.605078E-01 -2.576275E-02 +2.101261E-01 +4.415297E-02 +3.319083E-01 +1.101631E-01 +2.795459E-01 +7.814588E-02 +1.306499E-01 +1.706938E-02 0.000000E+00 0.000000E+00 0.000000E+00 @@ -320,12 +320,12 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -1.605078E-01 -2.576275E-02 -3.063657E-01 -9.385994E-02 -1.700639E-01 -2.892174E-02 +1.306499E-01 +1.706938E-02 +2.795459E-01 +7.814588E-02 +1.585507E-01 +2.513833E-02 0.000000E+00 0.000000E+00 0.000000E+00 @@ -345,119 +345,119 @@ tally 4: 0.000000E+00 0.000000E+00 tally 5: -6.593197E-01 -4.347024E-01 -1.314196E-01 -1.727112E-02 -1.266446E+00 -1.603885E+00 -2.002491E-01 -4.009970E-02 -1.888709E+00 -3.567222E+00 -2.444522E-01 -5.975686E-02 -2.616078E+00 -6.843867E+00 -3.234935E-01 -1.046481E-01 -2.733300E+00 -7.470927E+00 -3.309500E-01 -1.095279E-01 -2.643656E+00 -6.988917E+00 -4.025014E-01 -1.620074E-01 -2.410643E+00 -5.811199E+00 -3.050040E-01 -9.302747E-02 -1.838012E+00 -3.378288E+00 -2.800764E-01 -7.844279E-02 -1.500033E+00 -2.250100E+00 -2.324765E-01 -5.404531E-02 -8.750096E-01 -7.656418E-01 -1.256919E-01 -1.579844E-02 +7.530237E-01 +5.670447E-01 +1.178984E-01 +1.390004E-02 +1.436984E+00 +2.064922E+00 +1.458395E-01 +2.126916E-02 +2.132240E+00 +4.546449E+00 +2.950109E-01 +8.703141E-02 +2.726356E+00 +7.433016E+00 +3.397288E-01 +1.154157E-01 +2.379700E+00 +5.662970E+00 +3.113206E-01 +9.692053E-02 +2.383471E+00 +5.680933E+00 +3.455611E-01 +1.194125E-01 +2.047271E+00 +4.191318E+00 +2.910986E-01 +8.473841E-02 +1.673107E+00 +2.799287E+00 +2.381996E-01 +5.673907E-02 +1.270672E+00 +1.614606E+00 +1.795241E-01 +3.222889E-02 +8.249906E-01 +6.806094E-01 +1.201397E-01 +1.443354E-02 cmfd indices 1.000000E+01 1.000000E+00 1.000000E+00 1.000000E+00 k cmfd -1.166297E+00 -1.148237E+00 -1.162472E+00 -1.187078E+00 -1.188411E+00 -1.194879E+00 -1.216739E+00 -1.216829E+00 -1.196596E+00 -1.199223E+00 -1.211817E+00 +1.184474E+00 +1.188170E+00 +1.165127E+00 +1.135010E+00 +1.145439E+00 +1.158451E+00 +1.154420E+00 +1.179615E+00 +1.197843E+00 +1.181252E+00 +1.186316E+00 cmfd entropy -3.215349E+00 -3.225577E+00 -3.223357E+00 -3.208828E+00 -3.211072E+00 -3.206578E+00 -3.195799E+00 -3.198236E+00 -3.220074E+00 -3.230249E+00 -3.238015E+00 +3.243654E+00 +3.244091E+00 +3.249203E+00 +3.249952E+00 +3.245538E+00 +3.240838E+00 +3.238919E+00 +3.223131E+00 +3.216002E+00 +3.220324E+00 +3.224804E+00 cmfd balance -2.07468E-03 -2.39687E-03 -1.51020E-03 -2.07618E-03 -1.89367E-03 -2.00902E-03 -2.54856E-03 -2.43636E-03 -2.48527E-03 -3.07775E-03 -3.37967E-03 +4.21104E-03 +1.38052E-03 +1.34642E-03 +2.39255E-03 +2.07426E-03 +1.27927E-03 +2.26249E-03 +2.72103E-03 +2.71504E-03 +2.19156E-03 +1.91989E-03 cmfd dominance ratio -5.505E-01 -5.558E-01 -5.584E-01 -5.477E-01 -5.477E-01 -5.426E-01 -5.335E-01 -5.305E-01 -5.427E-01 +5.520E-01 +5.535E-01 +5.628E-01 +5.696E-01 +5.723E-01 +5.651E-01 +5.648E-01 +5.555E-01 +5.448E-01 +5.488E-01 5.484E-01 -5.465E-01 cmfd openmc source comparison -5.598628E-03 -1.162952E-02 -1.083004E-02 -1.037470E-02 -5.649750E-03 -5.137914E-03 -3.868209E-03 -1.208756E-02 -5.398131E-03 -8.119598E-03 -4.573105E-03 +1.713810E-03 +2.429503E-03 +4.526209E-03 +7.978149E-03 +3.320012E-03 +3.880041E-03 +1.580215E-02 +1.663452E-02 +1.878103E-02 +7.436342E-03 +3.724478E-03 cmfd source -4.219187E-02 -8.692096E-02 -1.061389E-01 -1.199181E-01 -1.377328E-01 -1.326161E-01 -1.345872E-01 -1.112871E-01 -7.569533E-02 -5.291175E-02 +4.688167E-02 +9.066303E-02 +1.150679E-01 +1.430247E-01 +1.370466E-01 +1.267615E-01 +1.244218E-01 +1.048774E-01 +7.083441E-02 +4.042108E-02 diff --git a/tests/regression_tests/cmfd_nofeed/results_true.dat b/tests/regression_tests/cmfd_nofeed/results_true.dat index 756aa4570..ea1a0230b 100644 --- a/tests/regression_tests/cmfd_nofeed/results_true.dat +++ b/tests/regression_tests/cmfd_nofeed/results_true.dat @@ -1,117 +1,117 @@ k-combined: -1.172893E+00 8.095197E-03 +1.169143E+00 7.248013E-03 tally 1: -1.156995E+01 -1.347019E+01 -2.120053E+01 -4.531200E+01 -2.995993E+01 -8.997889E+01 -3.498938E+01 -1.226414E+02 -3.794510E+01 -1.442188E+02 -3.798115E+01 -1.446436E+02 -3.415954E+01 -1.171635E+02 -2.960329E+01 -8.785532E+01 -2.182231E+01 -4.782353E+01 -1.147379E+01 -1.321150E+01 +1.115130E+01 +1.249933E+01 +2.147608E+01 +4.643964E+01 +2.923697E+01 +8.598273E+01 +3.439175E+01 +1.189653E+02 +3.729169E+01 +1.395456E+02 +3.709975E+01 +1.380839E+02 +3.415420E+01 +1.168226E+02 +2.895696E+01 +8.419764E+01 +2.140382E+01 +4.646784E+01 +1.125483E+01 +1.275812E+01 tally 2: -2.345900E+01 -2.783672E+01 -1.627300E+01 -1.339749E+01 -4.127267E+01 -8.563716E+01 -2.934800E+01 -4.334439E+01 -5.742644E+01 -1.658158E+02 -4.092600E+01 -8.423842E+01 -6.740126E+01 -2.279288E+02 -4.796500E+01 -1.154602E+02 -7.340327E+01 -2.701349E+02 -5.235900E+01 -1.374186E+02 -7.387392E+01 -2.740829E+02 -5.277400E+01 -1.398425E+02 -6.733428E+01 -2.274414E+02 -4.800100E+01 -1.156206E+02 -5.794970E+01 -1.685421E+02 -4.124600E+01 -8.540686E+01 -4.257013E+01 -9.092401E+01 -3.014500E+01 -4.566369E+01 -2.300274E+01 -2.659051E+01 -1.613400E+01 -1.307448E+01 +2.275147E+01 +2.604974E+01 +1.587800E+01 +1.271197E+01 +4.191959E+01 +8.846441E+01 +2.966500E+01 +4.430669E+01 +5.699815E+01 +1.637359E+02 +4.037600E+01 +8.219353E+01 +6.656851E+01 +2.228703E+02 +4.718000E+01 +1.119941E+02 +7.334215E+01 +2.701014E+02 +5.223500E+01 +1.370726E+02 +7.394394E+01 +2.748295E+02 +5.273400E+01 +1.397334E+02 +6.931604E+01 +2.406234E+02 +4.949000E+01 +1.226886E+02 +5.799672E+01 +1.687992E+02 +4.142300E+01 +8.612116E+01 +4.320102E+01 +9.402138E+01 +3.068300E+01 +4.749148E+01 +2.295257E+01 +2.657057E+01 +1.606200E+01 +1.301453E+01 tally 3: -1.564900E+01 -1.239852E+01 -1.086873E+00 -6.047264E-02 -2.821700E+01 -4.006730E+01 -1.855830E+00 -1.755984E-01 -3.946300E+01 -7.833997E+01 -2.523142E+00 -3.210725E-01 -4.622500E+01 -1.072588E+02 -2.919735E+00 -4.340292E-01 -5.033400E+01 -1.270010E+02 -3.243022E+00 -5.318534E-01 -5.082400E+01 -1.297502E+02 -3.313898E+00 -5.546608E-01 -4.623700E+01 -1.072944E+02 -2.887766E+00 -4.203084E-01 -3.975000E+01 -7.934279E+01 -2.567158E+00 -3.333121E-01 -2.903500E+01 -4.237270E+01 -1.852070E+00 -1.733821E-01 -1.557800E+01 -1.219084E+01 -9.951884E-01 -5.121758E-02 +1.528200E+01 +1.177982E+01 +1.040687E+00 +5.586386E-02 +2.857900E+01 +4.113603E+01 +1.871515E+00 +1.774689E-01 +3.888800E+01 +7.626262E+01 +2.534433E+00 +3.274088E-01 +4.541400E+01 +1.037867E+02 +2.926509E+00 +4.319780E-01 +5.034500E+01 +1.273517E+02 +3.215813E+00 +5.215716E-01 +5.076100E+01 +1.295218E+02 +3.194424E+00 +5.165993E-01 +4.768100E+01 +1.138949E+02 +3.058255E+00 +4.732131E-01 +3.995800E+01 +8.015691E+01 +2.454286E+00 +3.044948E-01 +2.957000E+01 +4.412743E+01 +1.938963E+00 +1.908501E-01 +1.544000E+01 +1.202869E+01 +1.038073E+00 +5.487827E-02 tally 4: -3.111000E+00 -4.872850E-01 +3.086000E+00 +4.780160E-01 0.000000E+00 0.000000E+00 -2.794000E+00 -3.972060E-01 -5.520000E+00 -1.535670E+00 +2.739000E+00 +3.790990E-01 +5.478000E+00 +1.505928E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -128,14 +128,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -5.520000E+00 -1.535670E+00 -2.794000E+00 -3.972060E-01 -5.071000E+00 -1.305697E+00 -7.303000E+00 -2.685365E+00 +5.478000E+00 +1.505928E+00 +2.739000E+00 +3.790990E-01 +5.094000E+00 +1.310476E+00 +7.282000E+00 +2.669810E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -152,14 +152,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -7.303000E+00 -2.685365E+00 -5.071000E+00 -1.305697E+00 -7.015000E+00 -2.471981E+00 -8.545000E+00 -3.670539E+00 +7.282000E+00 +2.669810E+00 +5.094000E+00 +1.310476E+00 +6.987000E+00 +2.461137E+00 +8.487000E+00 +3.624153E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -176,14 +176,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -8.545000E+00 -3.670539E+00 -7.015000E+00 -2.471981E+00 -8.431000E+00 -3.570057E+00 -9.224000E+00 -4.268004E+00 +8.487000E+00 +3.624153E+00 +6.987000E+00 +2.461137E+00 +8.250000E+00 +3.421824E+00 +9.022000E+00 +4.088536E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -200,14 +200,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -9.224000E+00 -4.268004E+00 -8.431000E+00 -3.570057E+00 -9.217000E+00 -4.259749E+00 -9.305000E+00 -4.340149E+00 +9.022000E+00 +4.088536E+00 +8.250000E+00 +3.421824E+00 +9.300000E+00 +4.344142E+00 +9.262000E+00 +4.308946E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -224,14 +224,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -9.305000E+00 -4.340149E+00 -9.217000E+00 -4.259749E+00 -9.374000E+00 -4.415290E+00 -8.611000E+00 -3.718227E+00 +9.262000E+00 +4.308946E+00 +9.300000E+00 +4.344142E+00 +9.267000E+00 +4.310941E+00 +8.487000E+00 +3.613257E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -248,14 +248,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -8.611000E+00 -3.718227E+00 -9.374000E+00 -4.415290E+00 -8.515000E+00 -3.639945E+00 -7.056000E+00 -2.501658E+00 +8.487000E+00 +3.613257E+00 +9.267000E+00 +4.310941E+00 +8.682000E+00 +3.778194E+00 +7.123000E+00 +2.544345E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -272,14 +272,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -7.056000E+00 -2.501658E+00 -8.515000E+00 -3.639945E+00 -7.385000E+00 -2.737677E+00 -5.194000E+00 -1.356022E+00 +7.123000E+00 +2.544345E+00 +8.682000E+00 +3.778194E+00 +7.421000E+00 +2.773897E+00 +5.198000E+00 +1.369216E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -296,14 +296,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -5.194000E+00 -1.356022E+00 -7.385000E+00 -2.737677E+00 -5.436000E+00 -1.481654E+00 -2.756000E+00 -3.843860E-01 +5.198000E+00 +1.369216E+00 +7.421000E+00 +2.773897E+00 +5.567000E+00 +1.561013E+00 +2.763000E+00 +3.882750E-01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -320,12 +320,12 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -2.756000E+00 -3.843860E-01 -5.436000E+00 -1.481654E+00 -3.030000E+00 -4.643920E-01 +2.763000E+00 +3.882750E-01 +5.567000E+00 +1.561013E+00 +3.106000E+00 +4.853380E-01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -345,144 +345,144 @@ tally 4: 0.000000E+00 0.000000E+00 tally 5: -1.564300E+01 -1.238861E+01 -2.090227E+00 -2.262627E-01 -2.821400E+01 -4.005826E+01 -3.870834E+00 -7.614623E-01 -3.945400E+01 -7.830326E+01 -5.290557E+00 -1.422339E+00 -4.621500E+01 -1.072116E+02 -6.144745E+00 -1.903309E+00 -5.032900E+01 -1.269756E+02 -6.867524E+00 -2.373593E+00 -5.081000E+01 -1.296784E+02 -6.456283E+00 -2.130767E+00 -4.623000E+01 -1.072613E+02 -5.931623E+00 -1.776451E+00 -3.974500E+01 -7.932288E+01 -5.339603E+00 -1.456812E+00 -2.902800E+01 -4.235232E+01 -4.102240E+00 -8.519551E-01 -1.557800E+01 -1.219084E+01 -2.137954E+00 -2.347770E-01 +1.527700E+01 +1.177204E+01 +2.285003E+00 +2.661382E-01 +2.857200E+01 +4.111506E+01 +4.126099E+00 +8.737996E-01 +3.887800E+01 +7.622094E+01 +5.121343E+00 +1.333837E+00 +4.540600E+01 +1.037492E+02 +6.160114E+00 +1.913665E+00 +5.033400E+01 +1.272949E+02 +6.859861E+00 +2.384476E+00 +5.075800E+01 +1.295055E+02 +6.929393E+00 +2.443364E+00 +4.767500E+01 +1.138658E+02 +6.385463E+00 +2.080293E+00 +3.995300E+01 +8.013651E+01 +5.620641E+00 +1.603844E+00 +2.956200E+01 +4.410395E+01 +3.952701E+00 +7.877105E-01 +1.543500E+01 +1.202080E+01 +2.203583E+00 +2.512936E-01 cmfd indices 1.000000E+01 1.000000E+00 1.000000E+00 1.000000E+00 k cmfd -1.129918E+00 -1.148352E+00 -1.143137E+00 -1.145795E+00 -1.147285E+00 -1.148588E+00 -1.148151E+00 -1.162118E+00 -1.165380E+00 -1.162851E+00 -1.163379E+00 -1.166986E+00 -1.167838E+00 -1.171743E+00 -1.170398E+00 -1.169773E+00 +1.169107E+00 +1.173852E+00 +1.181921E+00 +1.187733E+00 +1.185766E+00 +1.177020E+00 +1.181200E+00 +1.180088E+00 +1.180676E+00 +1.174948E+00 +1.174167E+00 +1.174935E+00 +1.169912E+00 +1.169058E+00 +1.170366E+00 +1.169774E+00 cmfd entropy -3.224769E+00 -3.222795E+00 -3.221174E+00 -3.222164E+00 -3.221025E+00 -3.220967E+00 -3.223497E+00 -3.219213E+00 -3.221679E+00 -3.222084E+00 -3.222281E+00 -3.222540E+00 -3.224614E+00 -3.224193E+00 -3.224925E+00 -3.224367E+00 +3.207640E+00 +3.212075E+00 +3.215463E+00 +3.219545E+00 +3.225225E+00 +3.227103E+00 +3.229048E+00 +3.228263E+00 +3.229077E+00 +3.229932E+00 +3.229351E+00 +3.228195E+00 +3.228493E+00 +3.227823E+00 +3.225830E+00 +3.227270E+00 cmfd balance -3.90454E-03 -4.33180E-03 -3.77057E-03 -3.16391E-03 -3.11765E-03 -2.59886E-03 -2.81060E-03 -3.25473E-03 -2.68544E-03 -2.01716E-03 -1.89350E-03 -1.79159E-03 -1.51353E-03 -1.48514E-03 -1.50207E-03 -1.44045E-03 +4.88208E-03 +4.63702E-03 +3.41158E-03 +2.99755E-03 +2.78360E-03 +3.31542E-03 +2.64344E-03 +2.04609E-03 +1.84340E-03 +1.65450E-03 +1.70816E-03 +1.69952E-03 +1.51417E-03 +1.32738E-03 +1.41435E-03 +1.01462E-03 cmfd dominance ratio -5.539E-01 -5.522E-01 -5.491E-01 -5.511E-01 -5.506E-01 -5.523E-01 -5.523E-01 +5.467E-01 +5.468E-01 +5.448E-01 +5.457E-01 +5.457E-01 +5.485E-01 +5.500E-01 +5.497E-01 +5.495E-01 +5.499E-01 +5.502E-01 +5.485E-01 +5.492E-01 +5.483E-01 5.473E-01 -5.478E-01 -5.461E-01 -5.462E-01 -5.459E-01 -5.475E-01 -5.463E-01 5.466E-01 -5.469E-01 cmfd openmc source comparison -9.875240E-03 -1.119358E-02 -8.513903E-03 -7.728971E-03 -5.993771E-03 -5.837301E-03 -4.861789E-03 -5.624038E-03 -4.297229E-03 -4.029732E-03 -3.669197E-03 -3.598834E-03 -3.023310E-03 -3.347346E-03 -2.943658E-03 -2.764986E-03 +9.587418E-03 +7.785087E-03 +6.798967E-03 +5.947641E-03 +4.980801E-03 +4.272665E-03 +4.073759E-03 +4.305612E-03 +3.572759E-03 +3.785830E-03 +3.766828E-03 +3.495462E-03 +3.017281E-03 +2.857633E-03 +2.858606E-03 +2.449010E-03 cmfd source -4.561921E-02 -7.896381E-02 -1.084687E-01 -1.264057E-01 -1.408942E-01 -1.438180E-01 -1.247333E-01 -1.100896E-01 -7.897187E-02 -4.203556E-02 +4.390084E-02 +7.966902E-02 +1.087889E-01 +1.263915E-01 +1.394331E-01 +1.383156E-01 +1.319970E-01 +1.051339E-01 +8.248244E-02 +4.388774E-02 diff --git a/tests/regression_tests/cmfd_restart/results_true.dat b/tests/regression_tests/cmfd_restart/results_true.dat index f5f22d9ac..1ef9624d4 100644 --- a/tests/regression_tests/cmfd_restart/results_true.dat +++ b/tests/regression_tests/cmfd_restart/results_true.dat @@ -1,117 +1,117 @@ k-combined: -1.164262E+00 9.207592E-03 +1.181723E+00 9.944883E-03 tally 1: -1.156972E+01 -1.339924E+01 -2.136306E+01 -4.567185E+01 -2.859527E+01 -8.195821E+01 -3.470754E+01 -1.207851E+02 -3.766403E+01 -1.422263E+02 -3.778821E+01 -1.432660E+02 -3.573197E+01 -1.278854E+02 -2.849979E+01 -8.135515E+01 -2.073803E+01 -4.303374E+01 -1.112117E+01 -1.242944E+01 +1.169899E+01 +1.373251E+01 +2.142380E+01 +4.605511E+01 +2.968085E+01 +8.838716E+01 +3.561418E+01 +1.271206E+02 +3.777783E+01 +1.428817E+02 +3.805832E+01 +1.450213E+02 +3.439836E+01 +1.184892E+02 +2.852438E+01 +8.161896E+01 +2.088423E+01 +4.376204E+01 +1.076670E+01 +1.168108E+01 tally 2: -2.388054E+01 -2.875255E+01 -1.667791E+01 -1.403426E+01 -4.224771E+01 -8.942109E+01 -2.993088E+01 -4.490335E+01 -5.689839E+01 -1.625557E+02 -4.043633E+01 -8.212299E+01 -6.764024E+01 -2.297126E+02 -4.807902E+01 -1.161468E+02 -7.314835E+01 -2.684645E+02 -5.203584E+01 -1.359261E+02 -7.375727E+01 -2.733105E+02 -5.252944E+01 -1.386205E+02 -6.909571E+01 -2.397721E+02 -4.922548E+01 -1.217465E+02 -5.685978E+01 -1.621746E+02 -4.051938E+01 -8.237277E+01 -4.185562E+01 -8.784067E+01 -2.983570E+01 -4.467414E+01 -2.238373E+01 -2.520356E+01 -1.566758E+01 -1.234103E+01 +2.321241E+01 +2.702156E+01 +1.620912E+01 +1.317752E+01 +4.197404E+01 +8.845008E+01 +2.982666E+01 +4.469221E+01 +5.810089E+01 +1.695857E+02 +4.134123E+01 +8.588866E+01 +6.982488E+01 +2.447068E+02 +4.966939E+01 +1.238763E+02 +7.428421E+01 +2.767613E+02 +5.287955E+01 +1.403163E+02 +7.447402E+01 +2.785012E+02 +5.324628E+01 +1.423393E+02 +6.895164E+01 +2.381937E+02 +4.916366E+01 +1.211701E+02 +5.679253E+01 +1.617881E+02 +4.043125E+01 +8.204061E+01 +4.218618E+01 +8.933666E+01 +2.978592E+01 +4.456592E+01 +2.196426E+01 +2.435867E+01 +1.525576E+01 +1.175879E+01 tally 3: -1.609520E+01 -1.307542E+01 -1.033429E+00 -5.510889E-02 -2.877073E+01 -4.149542E+01 -1.964219E+00 -1.954692E-01 -3.896816E+01 -7.629752E+01 -2.484053E+00 -3.103733E-01 -4.634285E+01 -1.079367E+02 -2.974750E+00 -4.468223E-01 -5.007964E+01 -1.259202E+02 -3.181802E+00 -5.103621E-01 -5.058915E+01 -1.286193E+02 -3.249442E+00 -5.337712E-01 -4.744464E+01 -1.131026E+02 -3.067644E+00 -4.736335E-01 -3.900632E+01 -7.634433E+01 -2.443552E+00 -3.028060E-01 -2.874166E+01 -4.146375E+01 -1.810421E+00 -1.671667E-01 -1.509222E+01 -1.145579E+01 -1.014919E+00 -5.391053E-02 +1.563788E+01 +1.226528E+01 +1.053289E+00 +5.666942E-02 +2.870755E+01 +4.139654E+01 +1.838017E+00 +1.710528E-01 +3.978616E+01 +7.955764E+01 +2.560657E+00 +3.334449E-01 +4.780385E+01 +1.147770E+02 +3.139243E+00 +4.967628E-01 +5.106650E+01 +1.308704E+02 +3.170056E+00 +5.078920E-01 +5.123992E+01 +1.318586E+02 +3.211706E+00 +5.205979E-01 +4.729862E+01 +1.121695E+02 +3.068662E+00 +4.749488E-01 +3.898816E+01 +7.630564E+01 +2.516911E+00 +3.199696E-01 +2.865357E+01 +4.125742E+01 +1.852314E+00 +1.741116E-01 +1.467340E+01 +1.088460E+01 +9.268633E-01 +4.450662E-02 tally 4: -3.148231E+00 -4.974555E-01 +3.029754E+00 +4.613561E-01 0.000000E+00 0.000000E+00 -2.805439E+00 -3.982239E-01 -5.574031E+00 -1.561105E+00 +2.832501E+00 +4.049252E-01 +5.517243E+00 +1.527794E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -128,14 +128,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -5.574031E+00 -1.561105E+00 -2.805439E+00 -3.982239E-01 -5.171038E+00 -1.344877E+00 -7.372031E+00 -2.725420E+00 +5.517243E+00 +1.527794E+00 +2.832501E+00 +4.049252E-01 +5.117178E+00 +1.316972E+00 +7.333303E+00 +2.701677E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -152,14 +152,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -7.372031E+00 -2.725420E+00 -5.171038E+00 -1.344877E+00 -6.946847E+00 -2.424850E+00 -8.496610E+00 -3.627542E+00 +7.333303E+00 +2.701677E+00 +5.117178E+00 +1.316972E+00 +7.248464E+00 +2.641591E+00 +8.817788E+00 +3.905530E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -176,14 +176,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -8.496610E+00 -3.627542E+00 -6.946847E+00 -2.424850E+00 -8.479501E+00 -3.607280E+00 -9.261869E+00 -4.305912E+00 +8.817788E+00 +3.905530E+00 +7.248464E+00 +2.641591E+00 +8.646465E+00 +3.749847E+00 +9.460948E+00 +4.495388E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -200,14 +200,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -9.261869E+00 -4.305912E+00 -8.479501E+00 -3.607280E+00 -9.232858E+00 -4.278432E+00 -9.306384E+00 -4.348594E+00 +9.460948E+00 +4.495388E+00 +8.646465E+00 +3.749847E+00 +9.379341E+00 +4.415049E+00 +9.278640E+00 +4.320720E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -224,14 +224,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -9.306384E+00 -4.348594E+00 -9.232858E+00 -4.278432E+00 -9.299764E+00 -4.347828E+00 -8.511976E+00 -3.639893E+00 +9.278640E+00 +4.320720E+00 +9.379341E+00 +4.415049E+00 +9.465746E+00 +4.498591E+00 +8.656146E+00 +3.760545E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -248,14 +248,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -8.511976E+00 -3.639893E+00 -9.299764E+00 -4.347828E+00 -8.726086E+00 -3.819567E+00 -7.147277E+00 -2.562747E+00 +8.656146E+00 +3.760545E+00 +9.465746E+00 +4.498591E+00 +8.589782E+00 +3.700308E+00 +6.996002E+00 +2.456935E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -272,14 +272,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -7.147277E+00 -2.562747E+00 -8.726086E+00 -3.819567E+00 -7.218790E+00 -2.612243E+00 -5.018287E+00 -1.263077E+00 +6.996002E+00 +2.456935E+00 +8.589782E+00 +3.700308E+00 +7.352050E+00 +2.714808E+00 +5.105164E+00 +1.312559E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -296,14 +296,14 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -5.018287E+00 -1.263077E+00 -7.218790E+00 -2.612243E+00 -5.443494E+00 -1.487018E+00 -2.732334E+00 -3.773047E-01 +5.105164E+00 +1.312559E+00 +7.352050E+00 +2.714808E+00 +5.442756E+00 +1.486776E+00 +2.697305E+00 +3.675580E-01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -320,12 +320,12 @@ tally 4: 0.000000E+00 0.000000E+00 0.000000E+00 -2.732334E+00 -3.773047E-01 -5.443494E+00 -1.487018E+00 -3.044773E+00 -4.655756E-01 +2.697305E+00 +3.675580E-01 +5.442756E+00 +1.486776E+00 +3.017025E+00 +4.571443E-01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -345,144 +345,144 @@ tally 4: 0.000000E+00 0.000000E+00 tally 5: -1.609029E+01 -1.306718E+01 -2.230601E+00 -2.559496E-01 -2.876780E+01 -4.148686E+01 -3.835952E+00 -7.456562E-01 -3.895738E+01 -7.625344E+01 -4.841024E+00 -1.197335E+00 -4.633595E+01 -1.079043E+02 -6.236821E+00 -1.963311E+00 -5.007472E+01 -1.258967E+02 -6.749745E+00 -2.297130E+00 -5.058336E+01 -1.285894E+02 -6.727612E+00 -2.315656E+00 -4.743869E+01 -1.130735E+02 -6.338193E+00 -2.042159E+00 -3.899838E+01 -7.631289E+01 -5.187573E+00 -1.359733E+00 -2.873434E+01 -4.144233E+01 -3.815610E+00 -7.367619E-01 -1.509020E+01 -1.145268E+01 -2.125767E+00 -2.341894E-01 +1.563588E+01 +1.226217E+01 +2.209027E+00 +2.507131E-01 +2.870034E+01 +4.137550E+01 +3.726620E+00 +7.021948E-01 +3.977762E+01 +7.952285E+01 +5.304975E+00 +1.427333E+00 +4.779747E+01 +1.147456E+02 +6.528302E+00 +2.151715E+00 +5.105366E+01 +1.308037E+02 +6.986782E+00 +2.467487E+00 +5.123380E+01 +1.318264E+02 +6.845633E+00 +2.383542E+00 +4.729296E+01 +1.121433E+02 +6.252695E+00 +1.977351E+00 +3.898236E+01 +7.628315E+01 +5.461495E+00 +1.528579E+00 +2.864576E+01 +4.123538E+01 +3.857301E+00 +7.581323E-01 +1.467047E+01 +1.088031E+01 +2.277024E+00 +2.679801E-01 cmfd indices 1.000000E+01 1.000000E+00 1.000000E+00 1.000000E+00 k cmfd -1.129918E+00 -1.143848E+00 -1.147976E+00 -1.151534E+00 -1.152378E+00 -1.148219E+00 -1.150402E+00 -1.154647E+00 -1.156159E+00 -1.160048E+00 -1.167441E+00 -1.168163E+00 -1.168629E+00 -1.164120E+00 -1.165051E+00 -1.169177E+00 +1.169107E+00 +1.175079E+00 +1.173912E+00 +1.175368E+00 +1.174026E+00 +1.181745E+00 +1.182261E+00 +1.183559E+00 +1.178691E+00 +1.179222E+00 +1.179017E+00 +1.172979E+00 +1.175043E+00 +1.173458E+00 +1.174152E+00 +1.171451E+00 cmfd entropy -3.224769E+00 -3.225945E+00 -3.227421E+00 -3.226174E+00 -3.224429E+00 -3.227049E+00 -3.230710E+00 -3.230315E+00 -3.226825E+00 -3.226655E+00 -3.226588E+00 -3.224155E+00 -3.223246E+00 -3.222640E+00 -3.223920E+00 -3.222838E+00 +3.207640E+00 +3.210547E+00 +3.212218E+00 +3.209573E+00 +3.211619E+00 +3.212126E+00 +3.213163E+00 +3.214288E+00 +3.215737E+00 +3.213677E+00 +3.214925E+00 +3.215612E+00 +3.216708E+00 +3.221454E+00 +3.219048E+00 +3.218387E+00 cmfd balance -3.90454E-03 -4.08089E-03 -3.46511E-03 -4.09535E-03 -2.62009E-03 -2.23559E-03 -2.54033E-03 -2.12799E-03 -2.25864E-03 -1.85766E-03 -1.49916E-03 -1.63471E-03 -1.48377E-03 -1.59800E-03 -1.37354E-03 -1.32853E-03 +4.88208E-03 +4.75139E-03 +3.15783E-03 +3.67091E-03 +2.99797E-03 +2.91060E-03 +2.06576E-03 +1.83482E-03 +1.56292E-03 +1.58659E-03 +2.32986E-03 +1.47376E-03 +1.46673E-03 +1.22627E-03 +1.31963E-03 +1.26456E-03 cmfd dominance ratio -5.539E-01 -5.537E-01 -5.536E-01 -5.515E-01 -5.512E-01 -5.514E-01 -5.518E-01 -5.507E-01 -5.500E-01 -5.497E-01 -5.477E-01 -5.461E-01 -5.444E-01 -5.445E-01 -5.454E-01 +5.467E-01 +5.453E-01 +5.458E-01 +5.436E-01 +5.442E-01 +5.406E-01 +5.401E-01 +5.413E-01 +4.995E-01 +5.396E-01 +5.409E-01 +5.414E-01 +5.423E-01 +5.456E-01 +5.442E-01 5.441E-01 cmfd openmc source comparison -9.875240E-03 -1.106163E-02 -9.847628E-03 -6.065921E-03 -5.772039E-03 -4.615656E-03 -4.244331E-03 -3.694299E-03 -3.545814E-03 -3.213063E-03 -3.467537E-03 -3.383489E-03 -3.697591E-03 -3.937358E-03 -3.369124E-03 -3.190359E-03 +9.587418E-03 +8.150978E-03 +6.677661E-03 +6.334727E-03 +5.153692E-03 +5.082964E-03 +4.633153E-03 +4.037383E-03 +3.528742E-03 +4.559089E-03 +3.517370E-03 +3.306117E-03 +2.913809E-03 +1.906045E-03 +1.932794E-03 +1.711341E-03 cmfd source -4.360494E-02 -8.397599E-02 -1.074181E-01 -1.294531E-01 -1.385611E-01 -1.407934E-01 -1.325191E-01 -1.044311E-01 -7.660359E-02 -4.263941E-02 +4.496492E-02 +7.869674E-02 +1.100280E-01 +1.354045E-01 +1.363339E-01 +1.380533E-01 +1.314512E-01 +1.077480E-01 +7.847306E-02 +3.884630E-02 diff --git a/tests/regression_tests/complex_cell/results_true.dat b/tests/regression_tests/complex_cell/results_true.dat index 3ce3d7cbc..ddedb07ff 100644 --- a/tests/regression_tests/complex_cell/results_true.dat +++ b/tests/regression_tests/complex_cell/results_true.dat @@ -1,11 +1,11 @@ k-combined: -2.564169E-01 4.095378E-03 +2.603220E-01 1.429366E-03 tally 1: -2.607144E+00 -1.360414E+00 -2.681079E+00 -1.439354E+00 -9.627534E-01 -1.855496E-01 -1.123751E-01 +2.624819E+00 +1.378200E+00 +2.730035E+00 +1.492361E+00 +1.013707E+00 +2.055807E-01 +1.123257E-01 2.530233E-03 diff --git a/tests/regression_tests/confidence_intervals/results_true.dat b/tests/regression_tests/confidence_intervals/results_true.dat index 6a906d07e..8ca256624 100644 --- a/tests/regression_tests/confidence_intervals/results_true.dat +++ b/tests/regression_tests/confidence_intervals/results_true.dat @@ -1,5 +1,5 @@ k-combined: -2.759923E-01 6.988588E-03 +2.850178E-01 9.646334E-03 tally 1: -6.167984E+01 -4.772717E+02 +6.234169E+01 +4.884167E+02 diff --git a/tests/regression_tests/cpp_driver/results_true.dat b/tests/regression_tests/cpp_driver/results_true.dat index 26f07842c..09f188db6 100644 --- a/tests/regression_tests/cpp_driver/results_true.dat +++ b/tests/regression_tests/cpp_driver/results_true.dat @@ -1,13 +1,13 @@ k-combined: -1.953962E+00 1.828426E-02 +1.874924E+00 2.180236E-02 tally 1: -9.607953E+01 -1.031898E+03 -2.853683E+01 -9.085469E+01 -9.745011E+01 -1.058928E+03 -2.220665E+02 -5.496813E+03 -2.220665E+02 -5.496813E+03 +9.484447E+01 +1.002269E+03 +2.746252E+01 +8.406603E+01 +9.833099E+01 +1.076376E+03 +2.206380E+02 +5.417609E+03 +2.206380E+02 +5.417609E+03 diff --git a/tests/regression_tests/dagmc/external/results_true.dat b/tests/regression_tests/dagmc/external/results_true.dat index cda656937..9a6b481b7 100644 --- a/tests/regression_tests/dagmc/external/results_true.dat +++ b/tests/regression_tests/dagmc/external/results_true.dat @@ -1,5 +1,5 @@ k-combined: -9.118190E-01 3.615552E-02 +1.083415E+00 5.991738E-02 tally 1: -8.430103E+00 -1.442878E+01 +8.862860E+00 +1.602117E+01 diff --git a/tests/regression_tests/dagmc/legacy/results_true.dat b/tests/regression_tests/dagmc/legacy/results_true.dat index cda656937..9a6b481b7 100644 --- a/tests/regression_tests/dagmc/legacy/results_true.dat +++ b/tests/regression_tests/dagmc/legacy/results_true.dat @@ -1,5 +1,5 @@ k-combined: -9.118190E-01 3.615552E-02 +1.083415E+00 5.991738E-02 tally 1: -8.430103E+00 -1.442878E+01 +8.862860E+00 +1.602117E+01 diff --git a/tests/regression_tests/dagmc/refl/results_true.dat b/tests/regression_tests/dagmc/refl/results_true.dat index 533c62a90..b49a4a7a4 100644 --- a/tests/regression_tests/dagmc/refl/results_true.dat +++ b/tests/regression_tests/dagmc/refl/results_true.dat @@ -1,5 +1,5 @@ k-combined: -2.035173E+00 3.967029E-02 +2.047107E+00 8.605767E-02 tally 1: -1.064492E+01 -2.301019E+01 +1.145034E+01 +2.636875E+01 diff --git a/tests/regression_tests/dagmc/universes/inputs_true.dat b/tests/regression_tests/dagmc/universes/inputs_true.dat index 2fa79d831..be1a17383 100644 --- a/tests/regression_tests/dagmc/universes/inputs_true.dat +++ b/tests/regression_tests/dagmc/universes/inputs_true.dat @@ -48,6 +48,14 @@ 100 10 5 + + + -10.0 -10.0 -24.0 10.0 10.0 24.0 + + + true + + false diff --git a/tests/regression_tests/dagmc/universes/results_true.dat b/tests/regression_tests/dagmc/universes/results_true.dat index 76cbc9db0..aacb7d1ab 100644 --- a/tests/regression_tests/dagmc/universes/results_true.dat +++ b/tests/regression_tests/dagmc/universes/results_true.dat @@ -1,13 +1,13 @@ k-combined: -9.887663E-01 1.510336E-02 +9.719586E-01 3.630894E-02 tally 1: -4.340758E+00 -4.265459E+00 -4.712319E+00 -4.654778E+00 -4.151897E+00 -3.588090E+00 -2.965925E+00 -1.852746E+00 +4.463288E+00 +4.136647E+00 +4.769631E+00 +4.622840E+00 +4.315273E+00 +3.871129E+00 +4.091804E+00 +3.582192E+00 0.000000E+00 0.000000E+00 diff --git a/tests/regression_tests/dagmc/universes/test.py b/tests/regression_tests/dagmc/universes/test.py index 057a7b0d4..d68c6b11c 100644 --- a/tests/regression_tests/dagmc/universes/test.py +++ b/tests/regression_tests/dagmc/universes/test.py @@ -11,81 +11,87 @@ pytestmark = pytest.mark.skipif( reason="DAGMC CAD geometry is not enabled.") -class DAGMCUniverseTest(PyAPITestHarness): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) +@pytest.fixture +def pin_lattice_model(): + ### MATERIALS ### + fuel = openmc.Material(name='no-void fuel') + fuel.set_density('g/cc', 10.29769) + fuel.add_nuclide('U234', 0.93120485) + fuel.add_nuclide('U235', 0.00055815) + fuel.add_nuclide('U238', 0.022408) + fuel.add_nuclide('O16', 0.045829) - ### MATERIALS ### - fuel = openmc.Material(name='no-void fuel') - fuel.set_density('g/cc', 10.29769) - fuel.add_nuclide('U234', 0.93120485) - fuel.add_nuclide('U235', 0.00055815) - fuel.add_nuclide('U238', 0.022408) - fuel.add_nuclide('O16', 0.045829) + cladding = openmc.Material(name='clad') + cladding.set_density('g/cc', 6.55) + cladding.add_nuclide('Zr90', 0.021827) + cladding.add_nuclide('Zr91', 0.00476) + cladding.add_nuclide('Zr92', 0.0072758) + cladding.add_nuclide('Zr94', 0.0073734) + cladding.add_nuclide('Zr96', 0.0011879) - cladding = openmc.Material(name='clad') - cladding.set_density('g/cc', 6.55) - cladding.add_nuclide('Zr90', 0.021827) - cladding.add_nuclide('Zr91', 0.00476) - cladding.add_nuclide('Zr92', 0.0072758) - cladding.add_nuclide('Zr94', 0.0073734) - cladding.add_nuclide('Zr96', 0.0011879) + water = openmc.Material(name='water') + water.set_density('g/cc', 0.740582) + water.add_nuclide('H1', 0.049457) + water.add_nuclide('O16', 0.024672) + water.add_nuclide('B10', 8.0042e-06) + water.add_nuclide('B11', 3.2218e-05) + water.add_s_alpha_beta('c_H_in_H2O') - water = openmc.Material(name='water') - water.set_density('g/cc', 0.740582) - water.add_nuclide('H1', 0.049457) - water.add_nuclide('O16', 0.024672) - water.add_nuclide('B10', 8.0042e-06) - water.add_nuclide('B11', 3.2218e-05) - water.add_s_alpha_beta('c_H_in_H2O') + model = openmc.Model() + model.materials = openmc.Materials([fuel, cladding, water]) - self._model.materials = openmc.Materials([fuel, cladding, water]) + ### GEOMETRY ### + # create the DAGMC universe + pincell_univ = openmc.DAGMCUniverse(filename='dagmc.h5m', auto_geom_ids=True) - ### GEOMETRY ### - # create the DAGMC universe - pincell_univ = openmc.DAGMCUniverse(filename='dagmc.h5m', auto_geom_ids=True) + # creates another DAGMC universe, this time with within a bounded cell + bound_pincell_universe = openmc.DAGMCUniverse(filename='dagmc.h5m').bounded_universe() + # uses the bound_dag_cell as the root argument to test the type checks in openmc.Geometry + bound_pincell_geometry = openmc.Geometry(root=bound_pincell_universe) + # assigns the bound_dag_geometry to the model to test the type checks in model.Geometry setter + model.geometry = bound_pincell_geometry - # creates another DAGMC universe, this time with within a bounded cell - bound_pincell_universe = openmc.DAGMCUniverse(filename='dagmc.h5m').bounded_universe() - # uses the bound_dag_cell as the root argument to test the type checks in openmc.Geometry - bound_pincell_geometry = openmc.Geometry(root=bound_pincell_universe) - # assigns the bound_dag_geometry to the model to test the type checks in model.Geometry setter - self._model.geometry = bound_pincell_geometry + # create a 2 x 2 lattice using the DAGMC pincell + pitch = np.asarray((24.0, 24.0)) + lattice = openmc.RectLattice() + lattice.pitch = pitch + lattice.universes = [[pincell_univ] * 2] * 2 + lattice.lower_left = -pitch - # create a 2 x 2 lattice using the DAGMC pincell - pitch = np.asarray((24.0, 24.0)) - lattice = openmc.RectLattice() - lattice.pitch = pitch - lattice.universes = [[pincell_univ] * 2] * 2 - lattice.lower_left = -pitch + left = openmc.XPlane(x0=-pitch[0], name='left', boundary_type='reflective') + right = openmc.XPlane(x0=pitch[0], name='right', boundary_type='reflective') + front = openmc.YPlane(y0=-pitch[1], name='front', boundary_type='reflective') + back = openmc.YPlane(y0=pitch[1], name='back', boundary_type='reflective') + # clip the DAGMC geometry at +/- 10 cm w/ CSG planes + bottom = openmc.ZPlane(z0=-10.0, name='bottom', boundary_type='reflective') + top = openmc.ZPlane(z0=10.0, name='top', boundary_type='reflective') - left = openmc.XPlane(x0=-pitch[0], name='left', boundary_type='reflective') - right = openmc.XPlane(x0=pitch[0], name='right', boundary_type='reflective') - front = openmc.YPlane(y0=-pitch[1], name='front', boundary_type='reflective') - back = openmc.YPlane(y0=pitch[1], name='back', boundary_type='reflective') - # clip the DAGMC geometry at +/- 10 cm w/ CSG planes - bottom = openmc.ZPlane(z0=-10.0, name='bottom', boundary_type='reflective') - top = openmc.ZPlane(z0=10.0, name='top', boundary_type='reflective') + bounding_region = +left & -right & +front & -back & +bottom & -top + bounding_cell = openmc.Cell(fill=lattice, region=bounding_region) - bounding_region = +left & -right & +front & -back & +bottom & -top - bounding_cell = openmc.Cell(fill=lattice, region=bounding_region) + model.geometry = openmc.Geometry([bounding_cell]) - self._model.geometry = openmc.Geometry([bounding_cell]) + # add a cell instance tally + tally = openmc.Tally(name='cell instance tally') + # using scattering + cell_instance_filter = openmc.CellInstanceFilter(((4, 0), (4, 1), (4, 2), (4, 3), (4, 4))) + tally.filters = [cell_instance_filter] + tally.scores = ['scatter'] + model.tallies = [tally] - # add a cell instance tally - tally = openmc.Tally(name='cell instance tally') - # using scattering - cell_instance_filter = openmc.CellInstanceFilter(((4, 0), (4, 1), (4, 2), (4, 3), (4, 4))) - tally.filters = [cell_instance_filter] - tally.scores = ['scatter'] - self._model.tallies = [tally] + # settings + model.settings.particles = 100 + model.settings.batches = 10 + model.settings.inactive = 5 + model.settings.output = {'summary' : False} + model.settings.source = openmc.IndependentSource( + space=openmc.stats.Box((-10., -10., -24.), (10., 10., 24.)), + constraints={'fissionable': True}, + ) - # settings - self._model.settings.particles = 100 - self._model.settings.batches = 10 - self._model.settings.inactive = 5 - self._model.settings.output = {'summary' : False} + return model -def test_univ(): - harness = DAGMCUniverseTest('statepoint.10.h5', model=openmc.Model()) + +def test_univ(pin_lattice_model): + harness = PyAPITestHarness('statepoint.10.h5', model=pin_lattice_model) harness.main() diff --git a/tests/regression_tests/density/results_true.dat b/tests/regression_tests/density/results_true.dat index c8e3b1ede..42dc0c19f 100644 --- a/tests/regression_tests/density/results_true.dat +++ b/tests/regression_tests/density/results_true.dat @@ -1,2 +1,2 @@ k-combined: -1.110057E+00 1.303260E-02 +1.082191E+00 3.064029E-02 diff --git a/tests/regression_tests/deplete_no_transport/test_reference_coupled_days.h5 b/tests/regression_tests/deplete_no_transport/test_reference_coupled_days.h5 index 5fdc656d97018b596147839415fa967031dc18e5..9757c9791be0030366544aaf515276409d1fec03 100644 GIT binary patch delta 250 zcmcaGlj*`trVUlrj7*!Wt>qbk)NGsiKx(qJ-G3m*+MXZ8@pb@lyd9T%GRjW=*d?y` zFYxU3nKj~e3{cP}m3HXDWp;ZQ|M6taZh67#+!d;#c}L-@u1=2ZR_A0{v}>>PF&Det zlRLYuO&NUR?cWNmlWYt(c4j-fH;1`J$+;ti>$Gcesk6$S$Q3iM2RR3sJUGpA`=-;= zV?T^!9e+68n*6g{T~Y7AoEsW%5O)4tZG4Bhc#18|4Ra?u_Ly^Sn|{DEJm$8o#^lBx PamG`VXZ9Ggfb0YSug+EV delta 250 zcmcaGlj*`trVUlrj0~Hrt>qbk)NGsiKx(qJ-G3m*+MXZ8@pb@lyd9T%GRjQ;*d?y` zH}LHBnKj~e3{cQ2m3HXDWp;ZQ|Hov_Zh66~+!d;#c}L-@u1t>XR_A11v}>>PF&DdC zlRLYuO&!E5?B5EllWYt(c4j@hH;1`J$+<0s>$Gcesk8c?$Q3iM2RZwiJUGpA`=-qbk)NGsiKx(qJ-G3m*+MXZ8@pb@lyd9T%GRjW=*d?y` zFYxU3nKj~e3{cP}m3HXDWp;ZQ|M6taZh67#+!d;#c}L-@u1=2ZR_A0{v}>>PF&Det zlRLYu>lyCF+pq3wQB~R_E^iCuVisI&aZ$eh-#V=p;i{&mlVlfMLAZ*i zcGk70aZ_wzuG|%HJ?z%+%YWelx*P5-K9A(!tx38kO(WUi9@uv4t=wD@W4mje6ISIo o-nPx&qbk)NGsiKx(qJ-G3m*+MXZ8@pb@lyd9T%GRjQ;*d?y` zH}LHBnKj~e3{cQ2m3HXDWp;ZQ|Hov_Zh66~+!d;#c}L-@u1t>XR_A11v}>>PF&DdC zlRLYu>l*?q>{oTQs4DG|au%HSDR_n8PbZ7nH)UqLlyv61{8gv7;*`^aBQ6b7lBYS{ zml2t{=haH5N7EiuNwN#BAY7$X zJL}rhxGAZ<-c$Ny$$ykpGR`=wj|w?rjhJ$5A3-0R&K6{vE7Z%39E7( nZ`qbk)NGsiKx(qJ-G3m*+MXZ8@pb@lyd9T%GRjW=*d?y` zFYxU3nKj~e3{cP}m3HXDWp;ZQ|M6taZh67#+!d;#c}L-@u1=2ZR_A0{v}>>PF&Det zlRLYu>lyUp?SF;#t$8eR%Bdn>ppW_6Bqtr|eBSpVhn>!tPi9~9L&}LgoAa@zjgu26 z+ot(adqSK#J{iqlr+(2{L`*-;;IFNV#)?CS7F=y{xjXUl=Xpr3I@6T;@hXz5q&IA5 zv5cEy3v*>}jbPZ>f0zEk1!f#mJ}-&n;B)b8Tk<2=;U0J`D{lW=*w{{A@qbk)NGsiKx(qJ-G3m*+MXZ8@pb@lyd9T%GRjQ;*d?y` zH}LHBnKj~e3{cQ2m3HXDWp;ZQ|Hov_Zh66~+!d;#c}L-@u1t>XR_A11v}>>PF&DdC zlRLYu>l;KX?0<&#t$8eR%BeJ8ppW_6Bqtr|eBSpVhn-HDPi9~9L&}LgoAa@zjgu2Q z+ot(adqSK#KN-zmr+(2{L`*-;;IFNV>WV{$7F=y{xi|6h=Xpr3I@^@`@hXz5j5cg% zv5cEy3v*?EjbPZ>f0zEk1!f&nJ}-&n;PdfpTk<2=;U0J+D{lW=*x1fc@qbk)NGsiKx(qJ-G3m*+MXZ8@pb@lyd9T%GRjW=*d?y` zFYxU3nKj~e3{cP}m3HXDWp;ZQ|M6taZh67#+!d;#c}L-@u1=2ZR_A0{v}>>PF&Det zlRLYu>lrHJ>@OZrx4it+(77d2Va;+yMdwS-ExQ#Ab~|ry6VrX~Fww<=Rzg{#936T=d0$i|OP^!tI-SS7z0$Gn{Z& z?G;d7zJ+(1EzFh03~XU(X{rox4TkGo{)(6!hx6MSk9z-e@j8mc?|$K8+AJX delta 353 zcmcaGlj*`trVUlrj0~Hrt>qbk)NGsiKx(qJ-G3m*+MXZ8@pb@lyd9T%GRjQ;*d?y` zH}LHBnKj~e3{cQ2m3HXDWp;ZQ|Hov_Zh66~+!d;#c}L-@u1t>XR_A11v}>>PF&DdC zlRLYu>l=2L*`Gb2Zh85sp>s>5!kXoZiq4muTXriL>~>z~CZ_w|VWzXD+1qbk)NGsiKx(qJ-G3m*+MXZ8@pb@lyd9T%GRjW=*d?y` zFYxU3nKj~e3{cP}m3HXDWp;ZQ|M6taZh67#+!d;#c}L-@u1=2ZR_A0{v}>>PF&Det zlRLX@>Mz9E{|fC}^H}7RQ$@Z&AM>|KPCC;0yzfH}JDo9~%)aJ_loNY4=VMJ9Cnrv} zP4lJpggAA4GMc|m{i3son0}hUUt1TA6^9NjxZ2`!cjD#G^N<{MrYZH~RU}7AZ`jUa z88^ii=E&X}!Ly-Tzuv-2S()v7Nr;jr>-J+qR`) j(yL_tDB4Ng-0b85^bzNuqbk)NGsiKx(qJ-G3m*+MXZ8@pb@lyd9T%GRjQ;*d?y` zH}LHBnKj~e3{cQ2m3HXDWp;ZQ|Hov_Zh66~+!d;#c}L-@u1t>XR_A11v}>>PF&DdC zlRLX@>RHO|e}?w0c`S0usWe}pkNMjqCmrd0-uEGgolcogW?%C|%85Oj^RcFllM_4J zrukBPLYz838O>j(e$iP(Oh3)wudR#fibIDMTy1f=H}Uf4c}R{r+m!n8Dw3m&Hf(3H zjGJN$b7X&w;MvfBm;S;9W*t*LFNx&d^YLt3@*~*c?tddIZvR`@*v?S$Mt-ZqZQHUi j=~Xg+6z!yMZg%nj`iS#S@_F-%&u!mNp4nr<4D=ZQKjU?D diff --git a/tests/regression_tests/deplete_no_transport/test_reference_source_rate.h5 b/tests/regression_tests/deplete_no_transport/test_reference_source_rate.h5 index 3f3b4aa2ac5c2040059adee2acda433177ff4464..c9c7e9e363421792108b928ada201f80d20a67df 100644 GIT binary patch delta 188 zcmcaGlj*`trVUlrj7*!Wt>qbk)NGsiKx(qJ-G3m*+MXZ8@pb@lyd9T%GRjW=*d@-m zezIVXJY(x*!=9GOYrDB48NlF!J&0-uNw7aWZO=1T#?MZNPPLr+IDMB>ljZ$sYkc20 z9cH@0p%UoqH0#fzmm)1$PCV*@9!JdUoTM&I%U!2_(Yb&_G=l%Ht;>!}JlhsrZE-=^ J(=UK-2LSZmL|y;@ delta 188 zcmcaGlj*`trVUlrj0~Hrt>qbk)NGsiKx(qJ-G3m*+MXZ8@pb@lyd9T%GRjQ;*d@-m zZn9vHJmZqdhCMBl*LHJ9wv+}kSoI1xGXTMX`f~dt)Al@bW&G^4?^MgFkJEQKHCo=E zw#N63(_y9?94dj%PP6_jdMVP9<;1Bj=yAln&PnppwA^*-7o7_@L?ihB+PZAM#ItR| O)fN|oJ^cdcb^rj;XGiV; diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_ext_source.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_ext_source.h5 index aa09e1bcf4e352f94798f2cc266718b5f77fcec1..3ed38f3d6ce3973fd63e7967b1246c8012ebf063 100644 GIT binary patch delta 573 zcmcbxnCZe|rVR`F>USrv(vX|WXU6~qyH0cH%&q$X=g+=t^ep6Lzb#Dua_|(E`n7U! z`OpUY8PRu^!TFp!5AV8dqXFkLXH|aMzsJTdai`9if9wC*N=+~R;`TwqZr%q@#>vMW z>+M2=7h3)}_}|vA%{uA8q-(a1430%d*ek#d;Kyj&d@wzFTe}eISsP{QfDBqBQp=`JI)^`&u;R?d{==2-s zn85i4+}(om7e3hUe7)&`_3n9-&-csvB%GdVFCu4QX!&=O^UBAH7Cgt6IE(*mwJO-W z!+B#-()4p%W;omJ+hwTxAkH~rf6|xw^SRCkbPHbShXuGq-=Fp1_xl4b?*bke6n-^y zop{ire!6Yfa~A z`G>st!}S=N;QSVAKG6xqjke{loDWa*Y_R=tvTbGL<5t_Jzg2Rbe=QTV4LP>`DVuYd qE#LJSX19KO*iJk+f&V#c|I6bKZ`L*S*BN4Zf-;Mza^!fgFR4sY~=W9JMXb+E@U<;F%eyzpw?}G$f ze)Gy%UgoEkBKXS}J_viL2_L6yOG&csZHnKpC?=%mLfnR=CAvFMvD9oBznV zqnB0S>Nofw-+U=(Kb)WN=XuMA3PU*m=_M_}>1EDPz99oc+Y7dX%{&%x1(U>L`t6%^ z;rwj^>$QDbKic0|`k<=UfA-|_{jxsMcTd!`B(0EYX%U5cO4Fn(^ugf(IDf*fXIeAUe%rtTVWP(z{T8K6xcp-+g@m2Oli~cY z_8Yz(KhX^5UsVyDSAC|*cGsRc8_S~`Y%_B{pSqdfZW}E*kxR;Ng`jP~r~K%tS{1f? n8cfoa<2`IA9-P4cX diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_feed.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_feed.h5 index 284d5cbe4ed05d7404222e70200aa0e0318cf2ad..a147022322a1887615f9000e41992c5c48d74b2a 100644 GIT binary patch delta 717 zcmcbxnCZe|rVR`F>gOe|(vX|WXU6~qYff|M%&q$X=kK^{^ep6Lzb#CDaqtwD`n7U! zdH)9c8PRu^!TCZv5AV8dqXFlOWL19JzsJTdd8f{qf9wC*GE6W2;`TwqZvF>O#>vMW z>+K?f7h3)}_}|vI%{uA8q-(b4430%d*ek#d;LOjF-de|O4|BlPRUiIo8!Ui3=;l_> z&-+*C!_{a1=9RFyw-3%Ybjg*zc-<7vKh5|))cc$#lyAtu(7IcD>$?e-a0MZIboz~R zOyGQP?ruT(3m@!@Uu}9|y?fr|^Zl~kA6`$k=Qw;%{8){FvvcdQXY9&)&ZVj=*Y*GQ zcUIMi-=o>4?fmA&+b2`DoO06kuX0?If5++kIY!TB^^49N8-L!}`Q6rK!!Iw<=~r7O zFJzOSctNZFTF2%|S(S*0C|s5`;f2h5xHBBre&uRhjfjW`SG13b{1t)A-=CGGc<$wL zIR8qJf2zn9B{*N%O<42R2Wz`Ka~{gSpZw4Eu=GCZ`cvw5m-XMRyYT`T5z3O0*{A0H zw|y(K@ZtBDS8XrUEGjZQpa?f$`}FH;U8>mO5mDqhlRNnoA|fiv^jAdgMp(Fe@4O_hH1m{Z83w SSj4ikALmS7KT(zi6cGSL_T?-9 delta 717 zcmcbxnCZe|rVR`F>Z>Q6k%(K#Z^r-yrhI=psusP0^GzNYw1>w{u!YGhz1Cv+_dx&NP4#dhn{A0^(k<|Wh-BsJr|M%h*dKBOW?0h+y;ObZUAK!c_Xg{3q_2+rZhYCYD|M(>>!RckrP`)7pL+cB+gUvh^a0Q`aG5z*U zx^VtBf%V$Htsm_@mprKI^`AZYe7~&sYnLPTmp{prCHEUR$1TG&#&!W<$Q2!!`1IB-Ej;N5!0{b+rRUf2={k~U!{g>uQ*)&PJ@EH%d-&? zv251r#m0Md;PMYsT&2IAGO^p5Gi%Br#(%aq@3aQkhilmFN&Hwn>mM*8mT*7TX*=`Z zwt{2n!_WCwZSQ>Ezqein5fMMv?y6B-%LI>z_act7zf3_y1n2dO2G`ao!!6ADq}sxh zz7Nj7>Gdal$rd9xfBV_aVvaLzP(CmsY;-j9-Sf@i3i5LuC RXJ+^N&Yiq|qAUw2A^@aa;FkaZ diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_removal.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_removal.h5 index 271af24103cda8f8c4886da551422a824dcde360..3d48105d7b444cc16f5b6438f97f897d2c1657fd 100644 GIT binary patch delta 717 zcmcbxnCZe|rVR`F>dz&w(vX|WXU6~q2Tyb8%&q$X=Wn@d^ep6Lzb#Due()5Q`n7U! z`TPd^8PRu^!TIt#5AV8dqXFmhW>tRLzsJU|WT(!Vf9wC*>P;{H;`TwqZr%q@#>vMW z>+LFn7h3)}_}@05%{uA8q-(Yx430%d*ek#dV9C#s-de|O4|Bk$RUiIo8!Ui3==fI8 z&-+*C!_~L{=9RFyw-3$_cgdB$c-<7vf6Mqj)cc$#lyAtuuxPjT)^`&u;R;&z==2-s zn85kA+}(om7e3hcKic%bdiT7^=lf;7H{6|M&vy8p_^}!T=jcGby9shC&i7u-xMcat z$~pA#Y~B-YD$Z|SynQld%PA)V|0>5d`FEVopJViFR=?=XvGM1fo!@O;HvIAuoqn}t z@e^A&a^!z zm&5rcyWUx>09hxs&f_`!`Sj VXwNUzwei-J#go@hlw|=$1OPN1;b8y( delta 717 zcmcbxnCZe|rVR`F>gy++k%(K#Z^r-yDtv!CsusP0^9>&uw1>w{u!YGBzt&>;_dx+NQg9f*~E_{a92c~bksbysb}{@;sN=uvyBPlfvZ2^e|+<$p#5;Z|DWeAA1Vyt{JWR51gDodL-~db3=?0l9c<>YfGa2!i|My- z(uMQ43ar=mZT)CJaoK~aUjNyX&-crE#|Iv}INP{)7tx6Eos$?XLQE^liNG$9B=L2kMWFHSAOaoMeuQJJs6-)vVvY{?k8O zspAIh*B@Q6{eOIxY33&-xB*dIRqgfJZ19LU;gjhyOKT}SB8+d$GkZ555pgSi)EBKf zbOf%yE#J#=dW8|3-#w>-M>{qE$_GY7zQGThP)|#^g0zW8_ZB}jh4YuP%(^MC>7%{# SA|unRp9?3ipD4=$iUX#+2(vX|WXU6~qyH0cH%&q$X=Wn`e^ep6Lzb#CDd+-#N`n7U! z`IrX#8PRu^!TAb15AV8dqXFlC%dGsge~*n_-A;&oFv{}$u>Q15e|P`)7p!=l~VTi;Evge%D1qtkDk zV*=+pad!*KU-)4E>eHqN*1P9TKHo2!)37kyp6&2G@nbax&M|>}cN64PoNv6Camn(P zm2<@5*}Ny*RGdG&c>83^mQzj^{#A}^^6xmEKga0VtbWm%W8=>|JHOkyZ209RI{j*k z3(R9V(tYc89{BE5o9(gi=5`%t6VaSgOtzn$CLYwNcm9*T$@mvP+)a1mnv~+^B4SCo zZr5|hjT7L3GQY57;b(PGxcW^med7*YSOMn?x|uY2Emw#0S+bVLP7d<2GwSB&i_`gQ zJ4yO}r$L>DUE&)SKSN!BG{@MnWyivQn?6U1_U8ScdJQU#uY&hR_VsSVN zJeG>jY&!A(IU<(gR@u*yXVrqMe<&KNyDj`6oWDZ*mch47V>o|bqV#$FH-S(-FqR^g s=I%XEfruqnxjPE4$_?Qfo~}9FJWJrCy}=2UnHJG=Ca<3;%L0le0PHO5YybcN delta 746 zcmcbxnCZe|rVR`F>T4#Qk%(K#Z^r-yW_*7;susP0^K~8=w1>w{u!YIXz1Cv+_dx%a14#dhn{A2syJgNQRx~sM+|L?^s^eDg$*#B}e%Yiaxdzb^(xUF!D$zA|=kRAV# zbw@9&z}2tyKfd`=(0({S;m`Ay4;6-R{6&=+wTWB**-qGV;bF=3YqmGLJkO*^A);C4=!e+weTZ1P zCY7A2K5HS|LHcS*TLX*{f$E%~rnCOy0k{LQa}G}kWHf>Ew;UIGeQCQZln;!hYZ(Uu qcNJK|6-ZtZE_-kTVW3Cr%Slr*KH2}%S!Z)VVd3QU6J=RIu>=5#9qAhY diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_only_feed.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_only_feed.h5 index d080b447093018f62b3eced413db81fa606d3338..70a4a3d67a513287b449b1afb36556bf48bbff7b 100644 GIT binary patch delta 30 lcmcbxnCZe|rVVfUnI9~hG&#{#e&Pk8%?cCNFtPyo_5kbT4rc%W delta 30 lcmcbxnCZe|rVVfUnG5_5Pfm1|pLjuNv%-Wmj4VLDJpkRP4LJY+ diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_only_removal.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_only_removal.h5 index 36f89e0090b147be206c995014745f3e9cb6c884..b41218a9b295a14f08c076e026adb858b4cb5b0b 100644 GIT binary patch delta 30 lcmcbxnCZe|rVVfUnLWNvnw;n=Kk6 diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_with_ext_source.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_no_depletion_with_ext_source.h5 index f3b1b171ebcab9d99fb7b73f5b8273ac72187971..71bfdae11234c73d8c7e38e2f6883813cf73f85e 100644 GIT binary patch delta 30 lcmcbxnCZe|rVVfUnKN>yOipx_pLjuNv%-Wmj4VLDJpkM04G91M delta 30 lcmcbxnCZe|rVVfUnH}aGnVje - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + diff --git a/tests/regression_tests/deplete_with_transport/test_reference.h5 b/tests/regression_tests/deplete_with_transport/test_reference.h5 index e8478250be6497c4d6ada14ca8fd20ae217b4fc6..cc616e79102ff6121285cff1ecaecf84b7f932ce 100644 GIT binary patch literal 163736 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
    soe6>?;W*IS5G<{kw%O8uQvW&aKBrTQC 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%0D>17(icZ91W!%}Q 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 bH@12 5 1200 - + 1000000 -10 -10 -5 10 10 5 diff --git a/tests/regression_tests/particle_restart_eigval/test.py b/tests/regression_tests/particle_restart_eigval/test.py index f9d22edb8..bad3f158c 100644 --- a/tests/regression_tests/particle_restart_eigval/test.py +++ b/tests/regression_tests/particle_restart_eigval/test.py @@ -2,5 +2,5 @@ from tests.testing_harness import ParticleRestartTestHarness def test_particle_restart_eigval(): - harness = ParticleRestartTestHarness('particle_11_254.h5') + harness = ParticleRestartTestHarness('particle_8_60.h5') harness.main() diff --git a/tests/regression_tests/periodic/results_true.dat b/tests/regression_tests/periodic/results_true.dat index 2189597e1..626004b16 100644 --- a/tests/regression_tests/periodic/results_true.dat +++ b/tests/regression_tests/periodic/results_true.dat @@ -1,2 +1,2 @@ k-combined: -1.623300E+00 2.572983E-02 +1.624889E+00 1.108153E-02 diff --git a/tests/regression_tests/periodic_6fold/results_true.dat b/tests/regression_tests/periodic_6fold/results_true.dat index e60182809..04aa30878 100644 --- a/tests/regression_tests/periodic_6fold/results_true.dat +++ b/tests/regression_tests/periodic_6fold/results_true.dat @@ -1,2 +1,2 @@ k-combined: -1.858730E+00 6.824588E-03 +1.848492E+00 2.933785E-03 diff --git a/tests/regression_tests/periodic_hex/results_true.dat b/tests/regression_tests/periodic_hex/results_true.dat index 584089a15..eff00b6e1 100644 --- a/tests/regression_tests/periodic_hex/results_true.dat +++ b/tests/regression_tests/periodic_hex/results_true.dat @@ -1,2 +1,2 @@ k-combined: -2.284468E+00 2.781588E-02 +2.285622E+00 2.576768E-03 diff --git a/tests/regression_tests/photon_production_fission/results_true.dat b/tests/regression_tests/photon_production_fission/results_true.dat index b17b3621a..af325d4b0 100644 --- a/tests/regression_tests/photon_production_fission/results_true.dat +++ b/tests/regression_tests/photon_production_fission/results_true.dat @@ -1,12 +1,12 @@ k-combined: -2.278476E+00 6.220292E-02 +2.297165E+00 1.955494E-02 tally 1: -2.664071E+00 -2.369250E+00 +2.696393E+00 +2.423937E+00 0.000000E+00 0.000000E+00 -2.664071E+00 -2.369250E+00 +2.696393E+00 +2.423937E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -18,52 +18,52 @@ tally 1: 0.000000E+00 0.000000E+00 tally 2: -2.640755E+00 -2.326201E+00 -4.245217E+08 -6.012128E+16 +2.672029E+00 +2.380251E+00 +4.288244E+08 +6.130569E+16 0.000000E+00 0.000000E+00 -2.640755E+00 -2.326201E+00 -4.245217E+08 -6.012128E+16 +2.672029E+00 +2.380251E+00 +4.288244E+08 +6.130569E+16 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.675918E+05 -9.365733E+09 +1.711908E+05 +9.769096E+09 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.675918E+05 -9.365733E+09 +1.711908E+05 +9.769096E+09 0.000000E+00 0.000000E+00 tally 3: -2.657846E+00 -2.354717E+00 -4.245217E+08 -6.012128E+16 +2.649127E+00 +2.339294E+00 +4.288244E+08 +6.130569E+16 0.000000E+00 0.000000E+00 -2.657846E+00 -2.354717E+00 -4.245217E+08 -6.012128E+16 +2.649127E+00 +2.339294E+00 +4.288244E+08 +6.130569E+16 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.675918E+05 -9.365733E+09 +1.711908E+05 +9.769096E+09 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.675918E+05 -9.365733E+09 +1.711908E+05 +9.769096E+09 0.000000E+00 0.000000E+00 diff --git a/tests/regression_tests/ptables_off/results_true.dat b/tests/regression_tests/ptables_off/results_true.dat index 1652088ce..374274862 100644 --- a/tests/regression_tests/ptables_off/results_true.dat +++ b/tests/regression_tests/ptables_off/results_true.dat @@ -1,2 +1,2 @@ k-combined: -2.978318E-01 1.560055E-03 +2.968228E-01 1.770320E-03 diff --git a/tests/regression_tests/quadric_surfaces/results_true.dat b/tests/regression_tests/quadric_surfaces/results_true.dat index 382c4241e..6fb569f52 100644 --- a/tests/regression_tests/quadric_surfaces/results_true.dat +++ b/tests/regression_tests/quadric_surfaces/results_true.dat @@ -1,2 +1,2 @@ k-combined: -1.193830E+00 1.824184E-02 +1.216213E+00 2.789559E-02 diff --git a/tests/regression_tests/random_ray_adjoint_fixed_source/results_true.dat b/tests/regression_tests/random_ray_adjoint_fixed_source/results_true.dat index 7178e2f11..e9aa9015b 100644 --- a/tests/regression_tests/random_ray_adjoint_fixed_source/results_true.dat +++ b/tests/regression_tests/random_ray_adjoint_fixed_source/results_true.dat @@ -6,4 +6,4 @@ tally 2: 9.482551E+08 tally 3: 1.956327E+05 -7.654469E+09 +7.654468E+09 diff --git a/tests/regression_tests/random_ray_auto_convert/infinite_medium/results_true.dat b/tests/regression_tests/random_ray_auto_convert/infinite_medium/results_true.dat index a0d7fc545..c7584ab64 100644 --- a/tests/regression_tests/random_ray_auto_convert/infinite_medium/results_true.dat +++ b/tests/regression_tests/random_ray_auto_convert/infinite_medium/results_true.dat @@ -1,2 +1,2 @@ k-combined: -7.796949E-01 1.055316E-02 +7.797820E-01 1.054725E-02 diff --git a/tests/regression_tests/random_ray_auto_convert/material_wise/results_true.dat b/tests/regression_tests/random_ray_auto_convert/material_wise/results_true.dat index ebf510cfc..d544a27df 100644 --- a/tests/regression_tests/random_ray_auto_convert/material_wise/results_true.dat +++ b/tests/regression_tests/random_ray_auto_convert/material_wise/results_true.dat @@ -1,2 +1,2 @@ k-combined: -7.375068E-01 7.015839E-03 +7.356667E-01 6.637270E-03 diff --git a/tests/regression_tests/random_ray_auto_convert/stochastic_slab/results_true.dat b/tests/regression_tests/random_ray_auto_convert/stochastic_slab/results_true.dat index 2f444fad3..75a10a224 100644 --- a/tests/regression_tests/random_ray_auto_convert/stochastic_slab/results_true.dat +++ b/tests/regression_tests/random_ray_auto_convert/stochastic_slab/results_true.dat @@ -1,2 +1,2 @@ k-combined: -7.499679E-01 8.107614E-03 +7.551716E-01 8.117378E-03 diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/results_true.dat b/tests/regression_tests/random_ray_diagonal_stabilization/results_true.dat index c6ce6aab9..f27ad46b4 100644 --- a/tests/regression_tests/random_ray_diagonal_stabilization/results_true.dat +++ b/tests/regression_tests/random_ray_diagonal_stabilization/results_true.dat @@ -1,2 +1,2 @@ k-combined: -7.152917E-01 1.430362E-02 +7.201808E-01 1.506596E-02 diff --git a/tests/regression_tests/random_ray_fixed_source_linear/linear/results_true.dat b/tests/regression_tests/random_ray_fixed_source_linear/linear/results_true.dat index 2fc08d0a4..e90d6bfdc 100644 --- a/tests/regression_tests/random_ray_fixed_source_linear/linear/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_linear/linear/results_true.dat @@ -1,6 +1,6 @@ tally 1: -2.339085E+00 -2.747304E-01 +2.339086E+00 +2.747305E-01 tally 2: 1.089827E-01 6.069324E-04 diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/flat/results_true.dat b/tests/regression_tests/random_ray_fixed_source_subcritical/flat/results_true.dat index 831eac501..4d2fb6c57 100644 --- a/tests/regression_tests/random_ray_fixed_source_subcritical/flat/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_subcritical/flat/results_true.dat @@ -83,7 +83,7 @@ tally 1: 0.000000E+00 0.000000E+00 0.000000E+00 -1.073356E+02 +1.073355E+02 4.630895E+02 1.263975E+01 6.427678E+00 @@ -107,7 +107,7 @@ tally 1: 2.388612E-03 5.941898E-01 1.414866E-02 -5.319555E+01 +5.319554E+01 1.134034E+02 1.967517E-01 1.551313E-03 @@ -122,7 +122,7 @@ tally 1: 8.044764E+01 2.602455E+02 3.474828E-01 -4.908567E-03 +4.908566E-03 9.665057E-01 3.797493E-02 1.561720E+02 diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/results_true.dat b/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/results_true.dat index 49813144d..bf602d3e2 100644 --- a/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/results_true.dat @@ -23,7 +23,7 @@ tally 1: 0.000000E+00 0.000000E+00 0.000000E+00 -5.235812E+01 +5.235813E+01 1.099243E+02 0.000000E+00 0.000000E+00 @@ -72,12 +72,12 @@ tally 1: 0.000000E+00 0.000000E+00 9.495275E+01 -3.613372E+02 +3.613373E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.799250E+01 +5.799251E+01 1.356178E+02 0.000000E+00 0.000000E+00 @@ -86,7 +86,7 @@ tally 1: 1.066414E+02 4.570291E+02 1.254014E+01 -6.325640E+00 +6.325641E+00 3.052020E+01 3.746918E+01 4.977302E+01 @@ -107,7 +107,7 @@ tally 1: 2.371074E-03 5.920173E-01 1.404478E-02 -5.299862E+01 +5.299863E+01 1.125576E+02 1.959914E-01 1.539191E-03 @@ -118,11 +118,11 @@ tally 1: 5.781230E-02 1.341759E-04 1.430525E-01 -8.215326E-04 +8.215327E-04 7.995734E+01 2.570099E+02 3.452934E-01 -4.844058E-03 +4.844059E-03 9.604162E-01 3.747587E-02 1.556795E+02 diff --git a/tests/regression_tests/random_ray_halton_samples/results_true.dat b/tests/regression_tests/random_ray_halton_samples/results_true.dat index 7eb307da0..b62398935 100644 --- a/tests/regression_tests/random_ray_halton_samples/results_true.dat +++ b/tests/regression_tests/random_ray_halton_samples/results_true.dat @@ -21,7 +21,7 @@ tally 1: 3.807294E-03 2.376683E+00 1.151027E+00 -8.060902E-02 +8.060903E-02 1.323179E-03 1.961862E-01 7.837693E-03 @@ -109,7 +109,7 @@ tally 1: 1.195422E-03 1.864967E-01 7.080943E-03 -7.105765E+00 +7.105766E+00 1.025960E+01 8.287512E-02 1.396474E-03 @@ -133,9 +133,9 @@ tally 1: 7.031793E-01 4.562478E+00 4.165199E+00 -2.870213E+00 +2.870214E+00 1.650126E+00 -4.244068E-01 +4.244069E-01 3.608745E-02 1.032921E+00 2.137598E-01 @@ -151,7 +151,7 @@ tally 1: 1.251254E-03 1.906210E-01 7.411654E-03 -7.162706E+00 +7.162707E+00 1.042515E+01 8.273831E-02 1.391799E-03 diff --git a/tests/regression_tests/random_ray_k_eff/results_true.dat b/tests/regression_tests/random_ray_k_eff/results_true.dat index a21929d53..37eca77f3 100644 --- a/tests/regression_tests/random_ray_k_eff/results_true.dat +++ b/tests/regression_tests/random_ray_k_eff/results_true.dat @@ -24,8 +24,8 @@ tally 1: 7.808142E-02 1.242278E-03 1.900346E-01 -7.358490E-03 -7.134948E+00 +7.358491E-03 +7.134949E+00 1.034824E+01 8.272647E-02 1.391871E-03 @@ -41,7 +41,7 @@ tally 1: 3.449537E+01 1.764293E-01 6.225586E-03 -4.907292E-01 +4.907293E-01 4.816400E-02 7.567715E+00 1.145439E+01 @@ -114,7 +114,7 @@ tally 1: 8.322052E-02 1.408294E-03 2.025446E-01 -8.342070E-03 +8.342071E-03 2.094832E+01 8.816715E+01 3.234739E-02 @@ -150,15 +150,15 @@ tally 1: 7.951819E-02 1.286690E-03 1.935314E-01 -7.621556E-03 -7.119586E+00 +7.621557E-03 +7.119587E+00 1.030023E+01 8.428175E-02 1.442931E-03 2.051274E-01 8.547243E-03 2.046758E+01 -8.418767E+01 +8.418768E+01 3.181946E-02 2.034766E-04 7.873502E-02 diff --git a/tests/regression_tests/random_ray_k_eff_mesh/results_true.dat b/tests/regression_tests/random_ray_k_eff_mesh/results_true.dat index 535db3b55..83209044b 100644 --- a/tests/regression_tests/random_ray_k_eff_mesh/results_true.dat +++ b/tests/regression_tests/random_ray_k_eff_mesh/results_true.dat @@ -1,7 +1,7 @@ k-combined: 8.379203E-01 8.057199E-03 tally 1: -5.080171E+00 +5.080172E+00 5.167984E+00 1.880341E+00 7.079266E-01 @@ -16,7 +16,7 @@ tally 1: 1.692643E+00 5.794069E-01 5.445214E-02 -5.995212E-04 +5.995213E-04 1.325256E-01 3.551193E-03 2.372336E+00 @@ -35,10 +35,10 @@ tally 1: 8.394042E+01 3.100485E-02 1.932097E-04 -7.671933E-02 +7.671934E-02 1.182985E-03 1.313652E+01 -3.451847E+01 +3.451846E+01 1.764978E-01 6.230420E-03 4.909196E-01 @@ -56,7 +56,7 @@ tally 1: 0.000000E+00 0.000000E+00 1.820058E+00 -6.729238E-01 +6.729239E-01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -96,7 +96,7 @@ tally 1: 4.104063E-01 3.377132E-02 9.988468E-01 -2.000404E-01 +2.000405E-01 1.660506E+00 5.567967E-01 5.427391E-02 @@ -147,13 +147,13 @@ tally 1: 3.699815E-03 2.368737E+00 1.143184E+00 -7.950085E-02 +7.950086E-02 1.286135E-03 1.934892E-01 -7.618268E-03 +7.618269E-03 7.119767E+00 1.030095E+01 -8.427627E-02 +8.427628E-02 1.442764E-03 2.051141E-01 8.546249E-03 diff --git a/tests/regression_tests/random_ray_linear/linear/results_true.dat b/tests/regression_tests/random_ray_linear/linear/results_true.dat index 6617c7821..4c0e14370 100644 --- a/tests/regression_tests/random_ray_linear/linear/results_true.dat +++ b/tests/regression_tests/random_ray_linear/linear/results_true.dat @@ -24,7 +24,7 @@ tally 1: 5.733016E-01 1.643671E-02 1.395301E+00 -9.736091E-02 +9.736092E-02 4.539598E+01 1.030472E+02 5.263055E-01 @@ -44,7 +44,7 @@ tally 1: 2.008092E+00 2.022988E-01 4.188246E+01 -8.843468E+01 +8.843469E+01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -111,7 +111,7 @@ tally 1: 9.671931E-02 4.539193E+01 1.030326E+02 -5.345253E-01 +5.345254E-01 1.428719E-02 1.300944E+00 8.463057E-02 diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/results_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/results_true.dat index 2fc08d0a4..e90d6bfdc 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/results_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/results_true.dat @@ -1,6 +1,6 @@ tally 1: -2.339085E+00 -2.747304E-01 +2.339086E+00 +2.747305E-01 tally 2: 1.089827E-01 6.069324E-04 diff --git a/tests/regression_tests/reflective_plane/results_true.dat b/tests/regression_tests/reflective_plane/results_true.dat index a1acfaad7..a4d6edb67 100644 --- a/tests/regression_tests/reflective_plane/results_true.dat +++ b/tests/regression_tests/reflective_plane/results_true.dat @@ -1,2 +1,2 @@ k-combined: -2.276857E+00 8.776678E-03 +2.279066E+00 4.793565E-03 diff --git a/tests/regression_tests/resonance_scattering/results_true.dat b/tests/regression_tests/resonance_scattering/results_true.dat index 8316e720e..72f0933b8 100644 --- a/tests/regression_tests/resonance_scattering/results_true.dat +++ b/tests/regression_tests/resonance_scattering/results_true.dat @@ -1,2 +1,2 @@ k-combined: -1.462509E+00 2.205413E-02 +1.462428E+00 1.828903E-02 diff --git a/tests/regression_tests/rotation/results_true.dat b/tests/regression_tests/rotation/results_true.dat index 0fb8ba981..6db3d329a 100644 --- a/tests/regression_tests/rotation/results_true.dat +++ b/tests/regression_tests/rotation/results_true.dat @@ -1,2 +1,2 @@ k-combined: -4.175600E-01 1.117465E-02 +4.459219E-01 1.899168E-02 diff --git a/tests/regression_tests/salphabeta/results_true.dat b/tests/regression_tests/salphabeta/results_true.dat index d5c7f66e9..75a81075e 100644 --- a/tests/regression_tests/salphabeta/results_true.dat +++ b/tests/regression_tests/salphabeta/results_true.dat @@ -1,2 +1,2 @@ k-combined: -8.694866E-01 2.033328E-02 +8.628529E-01 3.120924E-02 diff --git a/tests/regression_tests/score_current/results_true.dat b/tests/regression_tests/score_current/results_true.dat index ff939301d..6bb74445c 100644 --- a/tests/regression_tests/score_current/results_true.dat +++ b/tests/regression_tests/score_current/results_true.dat @@ -1,274 +1,490 @@ k-combined: -2.298294E-01 3.256961E-01 +7.729082E-01 3.775399E-02 tally 1: 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.150000E-01 -2.753000E-03 -1.820000E-01 -6.756000E-03 +1.200000E-01 +2.924000E-03 +1.740000E-01 +6.270000E-03 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.110000E-01 -2.601000E-03 -1.530000E-01 -4.865000E-03 -1.910000E-01 -7.535000E-03 +1.200000E-01 +3.044000E-03 +1.510000E-01 +4.615000E-03 +1.710000E-01 +6.067000E-03 0.000000E+00 0.000000E+00 -7.100000E-02 -1.079000E-03 -1.500000E-01 -4.606000E-03 -1.820000E-01 -6.756000E-03 -1.150000E-01 -2.753000E-03 -1.610000E-01 -5.383000E-03 -1.230000E-01 -3.141000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.370000E-01 -3.951000E-03 -2.600000E-01 -1.366400E-02 -1.930000E-01 -7.573000E-03 -0.000000E+00 -0.000000E+00 -8.500000E-02 -1.533000E-03 -2.040000E-01 -8.426000E-03 -1.230000E-01 -3.141000E-03 -1.610000E-01 -5.383000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.250000E-01 -3.311000E-03 -1.620000E-01 -5.346000E-03 -1.860000E-01 -6.932000E-03 -0.000000E+00 -0.000000E+00 -7.100000E-02 -1.083000E-03 -1.630000E-01 -5.617000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.800000E-01 -7.002000E-03 -2.980000E-01 -1.815000E-02 -1.530000E-01 -4.865000E-03 -1.110000E-01 -2.601000E-03 -1.890000E-01 -7.605000E-03 -1.330000E-01 -3.743000E-03 -1.790000E-01 -6.495000E-03 -0.000000E+00 -0.000000E+00 -1.080000E-01 -2.718000E-03 -2.160000E-01 -9.662000E-03 -2.980000E-01 -1.815000E-02 -1.800000E-01 -7.002000E-03 -2.650000E-01 -1.435500E-02 -1.590000E-01 -5.285000E-03 -2.600000E-01 -1.366400E-02 -1.370000E-01 -3.951000E-03 -2.670000E-01 -1.469300E-02 -1.940000E-01 -7.678000E-03 -2.390000E-01 -1.155500E-02 -0.000000E+00 -0.000000E+00 -2.410000E-01 -1.255900E-02 -5.510000E-01 -7.358900E-02 -1.590000E-01 -5.285000E-03 -2.650000E-01 -1.435500E-02 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.620000E-01 -5.346000E-03 -1.250000E-01 -3.311000E-03 -1.870000E-01 -7.083000E-03 +6.500000E-02 +9.390000E-04 1.410000E-01 -3.999000E-03 -2.040000E-01 -8.438000E-03 +4.001000E-03 +1.740000E-01 +6.270000E-03 +1.200000E-01 +2.924000E-03 +1.830000E-01 +6.879000E-03 +1.310000E-01 +3.541000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.750000E-01 +6.659000E-03 +2.820000E-01 +1.605400E-02 +1.980000E-01 +7.932000E-03 +0.000000E+00 +0.000000E+00 +9.400000E-02 +1.912000E-03 +2.200000E-01 +9.958000E-03 +1.310000E-01 +3.541000E-03 +1.830000E-01 +6.879000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.110000E-01 +2.639000E-03 +1.360000E-01 +3.890000E-03 +1.770000E-01 +6.551000E-03 +0.000000E+00 +0.000000E+00 +6.700000E-02 +9.310000E-04 +1.570000E-01 +4.975000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.480000E-01 +4.910000E-03 +2.720000E-01 +1.490600E-02 +1.510000E-01 +4.615000E-03 +1.200000E-01 +3.044000E-03 +1.800000E-01 +6.720000E-03 +1.050000E-01 +2.285000E-03 +1.790000E-01 +6.499000E-03 0.000000E+00 0.000000E+00 9.300000E-02 -1.829000E-03 -2.240000E-01 -1.015800E-02 +1.927000E-03 +2.160000E-01 +9.418000E-03 +2.720000E-01 +1.490600E-02 +1.480000E-01 +4.910000E-03 +2.680000E-01 +1.445800E-02 +1.480000E-01 +4.488000E-03 +2.820000E-01 +1.605400E-02 +1.750000E-01 +6.659000E-03 +2.510000E-01 +1.283100E-02 +1.870000E-01 +7.731000E-03 +2.340000E-01 +1.105000E-02 +0.000000E+00 +0.000000E+00 +2.110000E-01 +9.215000E-03 +5.470000E-01 +7.228300E-02 +1.480000E-01 +4.488000E-03 +2.680000E-01 +1.445800E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.360000E-01 +3.890000E-03 +1.110000E-01 +2.639000E-03 +1.540000E-01 +4.804000E-03 +1.280000E-01 +3.302000E-03 +2.200000E-01 +9.834000E-03 +0.000000E+00 +0.000000E+00 +9.900000E-02 +2.127000E-03 +1.860000E-01 +7.198000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.430000E-01 +4.337000E-03 +1.580000E-01 +5.104000E-03 +1.050000E-01 +2.285000E-03 +1.800000E-01 +6.720000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.860000E-01 +7.178000E-03 +0.000000E+00 +0.000000E+00 +7.000000E-02 +1.020000E-03 +1.610000E-01 +5.237000E-03 +1.580000E-01 +5.104000E-03 +1.430000E-01 +4.337000E-03 +1.580000E-01 +5.254000E-03 +1.230000E-01 +3.091000E-03 +1.870000E-01 +7.731000E-03 +2.510000E-01 +1.283100E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.980000E-01 +7.934000E-03 +0.000000E+00 +0.000000E+00 +9.500000E-02 +1.949000E-03 +2.150000E-01 +9.289000E-03 +1.230000E-01 +3.091000E-03 +1.580000E-01 +5.254000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.280000E-01 +3.302000E-03 +1.540000E-01 +4.804000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.640000E-01 +5.424000E-03 +0.000000E+00 +0.000000E+00 +6.200000E-02 +8.020000E-04 +1.500000E-01 +4.588000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.700000E-01 +5.884000E-03 +2.280000E-01 +1.084800E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.540000E-01 +4.856000E-03 +2.250000E-01 +1.047100E-02 +1.410000E-01 +4.001000E-03 +6.500000E-02 +9.390000E-04 +1.360000E-01 +3.852000E-03 +6.400000E-02 +8.700000E-04 +2.280000E-01 +1.084800E-02 +1.700000E-01 +5.884000E-03 +2.230000E-01 +1.001100E-02 +1.410000E-01 +4.185000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.980000E-01 +8.038000E-03 +5.030000E-01 +5.613900E-02 +2.200000E-01 +9.958000E-03 +9.400000E-02 +1.912000E-03 +2.140000E-01 +9.376000E-03 +7.300000E-02 +1.091000E-03 +1.410000E-01 +4.185000E-03 +2.230000E-01 +1.001100E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.540000E-01 +4.808000E-03 +2.340000E-01 +1.119400E-02 +1.570000E-01 +4.975000E-03 +6.700000E-02 +9.310000E-04 +1.600000E-01 +5.134000E-03 +5.300000E-02 +6.110000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.310000E-01 +1.084100E-02 +5.390000E-01 +6.215100E-02 +2.250000E-01 +1.047100E-02 +1.540000E-01 +4.856000E-03 +2.280000E-01 +1.059800E-02 +1.600000E-01 +5.214000E-03 +2.160000E-01 +9.418000E-03 +9.300000E-02 +1.927000E-03 +2.510000E-01 +1.271500E-02 +1.030000E-01 +2.229000E-03 +5.390000E-01 +6.215100E-02 +2.310000E-01 +1.084100E-02 +4.700000E-01 +4.972200E-02 +2.190000E-01 +9.821000E-03 +5.030000E-01 +5.613900E-02 +1.980000E-01 +8.038000E-03 +5.100000E-01 +5.929000E-02 +2.140000E-01 +9.406000E-03 +5.470000E-01 +7.228300E-02 +2.110000E-01 +9.215000E-03 +5.160000E-01 +5.892200E-02 +2.550000E-01 +1.480500E-02 +2.190000E-01 +9.821000E-03 +4.700000E-01 +4.972200E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.340000E-01 +1.119400E-02 +1.540000E-01 +4.808000E-03 +2.170000E-01 +9.509000E-03 +1.600000E-01 +5.156000E-03 +1.860000E-01 +7.198000E-03 +9.900000E-02 +2.127000E-03 +2.290000E-01 +1.058700E-02 +9.000000E-02 +1.780000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.550000E-01 +4.913000E-03 +2.380000E-01 +1.153400E-02 +1.600000E-01 +5.214000E-03 +2.280000E-01 +1.059800E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.610000E-01 +5.237000E-03 +7.000000E-02 +1.020000E-03 +1.410000E-01 +4.071000E-03 +6.100000E-02 +8.350000E-04 +2.380000E-01 +1.153400E-02 +1.550000E-01 +4.913000E-03 +2.320000E-01 +1.100400E-02 +1.460000E-01 +4.498000E-03 +2.140000E-01 +9.406000E-03 +5.100000E-01 +5.929000E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.150000E-01 +9.289000E-03 +9.500000E-02 +1.949000E-03 +1.870000E-01 +7.267000E-03 +1.030000E-01 +2.543000E-03 +1.460000E-01 +4.498000E-03 +2.320000E-01 +1.100400E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.600000E-01 +5.156000E-03 +2.170000E-01 +9.509000E-03 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 1.500000E-01 -4.522000E-03 -1.630000E-01 -5.439000E-03 -1.330000E-01 -3.743000E-03 -1.890000E-01 -7.605000E-03 +4.588000E-03 +6.200000E-02 +8.020000E-04 +1.290000E-01 +3.487000E-03 +6.000000E-02 +8.060000E-04 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 +1.090000E-01 +2.437000E-03 +1.650000E-01 +5.655000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.230000E-01 +3.065000E-03 +1.570000E-01 +4.999000E-03 +6.400000E-02 +8.700000E-04 +1.360000E-01 +3.852000E-03 +1.750000E-01 +6.299000E-03 +0.000000E+00 +0.000000E+00 1.650000E-01 -5.615000E-03 -0.000000E+00 -0.000000E+00 -7.100000E-02 -1.191000E-03 -1.550000E-01 -5.011000E-03 -1.630000E-01 -5.439000E-03 -1.500000E-01 -4.522000E-03 -1.680000E-01 -5.966000E-03 -1.290000E-01 -3.515000E-03 -1.940000E-01 -7.678000E-03 -2.670000E-01 -1.469300E-02 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.810000E-01 -6.765000E-03 -0.000000E+00 -0.000000E+00 -1.080000E-01 -2.694000E-03 -2.070000E-01 -8.673000E-03 -1.290000E-01 -3.515000E-03 -1.680000E-01 -5.966000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.410000E-01 -3.999000E-03 -1.870000E-01 -7.083000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.870000E-01 -7.213000E-03 -0.000000E+00 -0.000000E+00 -8.200000E-02 -1.536000E-03 -1.760000E-01 -6.550000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.750000E-01 -6.163000E-03 -2.570000E-01 -1.347100E-02 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.560000E-01 -4.914000E-03 -2.350000E-01 -1.135900E-02 -1.500000E-01 -4.606000E-03 -7.100000E-02 -1.079000E-03 -1.530000E-01 -4.837000E-03 -5.600000E-02 -7.260000E-04 -2.570000E-01 -1.347100E-02 -1.750000E-01 -6.163000E-03 -2.460000E-01 -1.235000E-02 -1.420000E-01 -4.326000E-03 +5.655000E-03 +1.090000E-01 +2.437000E-03 +1.480000E-01 +4.674000E-03 +1.320000E-01 +3.548000E-03 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 +1.440000E-01 +4.282000E-03 +2.190000E-01 +9.683000E-03 +7.300000E-02 +1.091000E-03 +2.140000E-01 +9.376000E-03 1.830000E-01 -7.133000E-03 -5.110000E-01 -5.696100E-02 -2.040000E-01 -8.426000E-03 -8.500000E-02 -1.533000E-03 -2.100000E-01 -8.928000E-03 -9.100000E-02 -1.807000E-03 -1.420000E-01 -4.326000E-03 -2.460000E-01 -1.235000E-02 +6.803000E-03 +0.000000E+00 +0.000000E+00 +1.320000E-01 +3.548000E-03 +1.480000E-01 +4.674000E-03 0.000000E+00 0.000000E+00 0.000000E+00 @@ -277,376 +493,160 @@ tally 1: 0.000000E+00 0.000000E+00 0.000000E+00 +1.350000E-01 +3.895000E-03 1.430000E-01 -4.207000E-03 -2.120000E-01 -9.278000E-03 -1.630000E-01 -5.617000E-03 -7.100000E-02 -1.083000E-03 -1.770000E-01 -6.293000E-03 -7.900000E-02 -1.495000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.300000E-01 -1.081000E-02 -5.200000E-01 -5.835000E-02 -2.350000E-01 -1.135900E-02 -1.560000E-01 -4.914000E-03 -2.370000E-01 -1.142300E-02 -1.360000E-01 -3.882000E-03 -2.160000E-01 -9.662000E-03 -1.080000E-01 -2.718000E-03 -2.090000E-01 -8.841000E-03 -1.030000E-01 -2.271000E-03 -5.200000E-01 -5.835000E-02 -2.300000E-01 -1.081000E-02 -5.110000E-01 -5.615900E-02 -2.430000E-01 -1.228300E-02 -5.110000E-01 -5.696100E-02 -1.830000E-01 -7.133000E-03 -5.130000E-01 -6.002100E-02 -2.420000E-01 -1.209200E-02 -5.510000E-01 -7.358900E-02 -2.410000E-01 -1.255900E-02 -5.140000E-01 -5.890200E-02 -2.100000E-01 -1.004600E-02 -2.430000E-01 -1.228300E-02 -5.110000E-01 -5.615900E-02 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.120000E-01 -9.278000E-03 -1.430000E-01 -4.207000E-03 -2.380000E-01 -1.133600E-02 -1.640000E-01 -5.444000E-03 -2.240000E-01 -1.015800E-02 -9.300000E-02 -1.829000E-03 -1.950000E-01 -7.705000E-03 -1.030000E-01 -2.191000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.680000E-01 -5.878000E-03 -2.230000E-01 -1.049900E-02 -1.360000E-01 -3.882000E-03 -2.370000E-01 -1.142300E-02 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.550000E-01 -5.011000E-03 -7.100000E-02 -1.191000E-03 -1.510000E-01 -4.845000E-03 -6.400000E-02 -9.460000E-04 -2.230000E-01 -1.049900E-02 -1.680000E-01 -5.878000E-03 -2.490000E-01 -1.255900E-02 -1.280000E-01 -3.448000E-03 -2.420000E-01 -1.209200E-02 -5.130000E-01 -6.002100E-02 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.070000E-01 -8.673000E-03 -1.080000E-01 -2.694000E-03 -2.000000E-01 -8.202000E-03 -9.600000E-02 -2.024000E-03 -1.280000E-01 -3.448000E-03 -2.490000E-01 -1.255900E-02 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.640000E-01 -5.444000E-03 -2.380000E-01 -1.133600E-02 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.760000E-01 -6.550000E-03 -8.200000E-02 -1.536000E-03 -1.670000E-01 -5.653000E-03 -4.900000E-02 -5.630000E-04 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.320000E-01 -3.626000E-03 -1.510000E-01 -4.831000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.370000E-01 -3.855000E-03 -1.610000E-01 -5.233000E-03 -5.600000E-02 -7.260000E-04 -1.530000E-01 -4.837000E-03 -1.610000E-01 -5.261000E-03 -0.000000E+00 -0.000000E+00 -1.510000E-01 -4.831000E-03 -1.320000E-01 -3.626000E-03 -1.490000E-01 -4.731000E-03 -1.320000E-01 -3.514000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.550000E-01 -4.921000E-03 -2.410000E-01 -1.168300E-02 -9.100000E-02 -1.807000E-03 -2.100000E-01 -8.928000E-03 -2.090000E-01 -8.775000E-03 -0.000000E+00 -0.000000E+00 -1.320000E-01 -3.514000E-03 -1.490000E-01 -4.731000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.150000E-01 -2.891000E-03 -1.560000E-01 -5.154000E-03 -7.900000E-02 -1.495000E-03 -1.770000E-01 -6.293000E-03 +4.297000E-03 +5.300000E-02 +6.110000E-04 1.600000E-01 -5.352000E-03 +5.134000E-03 +1.380000E-01 +4.220000E-03 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.610000E-01 -5.273000E-03 -2.310000E-01 -1.098100E-02 -1.610000E-01 -5.233000E-03 -1.370000E-01 -3.855000E-03 -1.760000E-01 -6.482000E-03 -1.340000E-01 -3.882000E-03 +1.900000E-01 +7.412000E-03 +2.420000E-01 +1.208400E-02 +1.570000E-01 +4.999000E-03 +1.230000E-01 +3.065000E-03 +1.730000E-01 +6.279000E-03 +1.330000E-01 +3.621000E-03 1.030000E-01 -2.271000E-03 -2.090000E-01 -8.841000E-03 -1.830000E-01 -6.761000E-03 -0.000000E+00 -0.000000E+00 -2.310000E-01 -1.098100E-02 -1.610000E-01 -5.273000E-03 -2.620000E-01 -1.380600E-02 -1.700000E-01 -5.970000E-03 -2.410000E-01 -1.168300E-02 -1.550000E-01 -4.921000E-03 -2.250000E-01 -1.014100E-02 -1.370000E-01 -3.805000E-03 +2.229000E-03 +2.510000E-01 +1.271500E-02 2.100000E-01 -1.004600E-02 -5.140000E-01 -5.890200E-02 -2.320000E-01 -1.093000E-02 +8.970000E-03 0.000000E+00 0.000000E+00 -1.700000E-01 -5.970000E-03 -2.620000E-01 -1.380600E-02 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.560000E-01 -5.154000E-03 -1.150000E-01 -2.891000E-03 -1.430000E-01 -4.163000E-03 -1.120000E-01 -2.640000E-03 -1.030000E-01 -2.191000E-03 -1.950000E-01 -7.705000E-03 -1.760000E-01 -6.392000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -9.200000E-02 -1.822000E-03 -1.370000E-01 -3.843000E-03 -1.340000E-01 -3.882000E-03 -1.760000E-01 -6.482000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -6.400000E-02 -9.460000E-04 -1.510000E-01 -4.845000E-03 -1.800000E-01 -6.520000E-03 -0.000000E+00 -0.000000E+00 -1.370000E-01 -3.843000E-03 -9.200000E-02 -1.822000E-03 -1.300000E-01 -3.604000E-03 -1.170000E-01 -2.789000E-03 -1.370000E-01 -3.805000E-03 -2.250000E-01 -1.014100E-02 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -9.600000E-02 -2.024000E-03 +2.420000E-01 +1.208400E-02 +1.900000E-01 +7.412000E-03 +2.610000E-01 +1.399900E-02 2.000000E-01 -8.202000E-03 -1.820000E-01 -6.660000E-03 +8.410000E-03 +2.190000E-01 +9.683000E-03 +1.440000E-01 +4.282000E-03 +2.690000E-01 +1.478100E-02 +1.540000E-01 +4.986000E-03 +2.550000E-01 +1.480500E-02 +5.160000E-01 +5.892200E-02 +2.310000E-01 +1.098900E-02 +0.000000E+00 +0.000000E+00 +2.000000E-01 +8.410000E-03 +2.610000E-01 +1.399900E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.430000E-01 +4.297000E-03 +1.350000E-01 +3.895000E-03 +1.760000E-01 +6.534000E-03 +1.170000E-01 +2.807000E-03 +9.000000E-02 +1.780000E-03 +2.290000E-01 +1.058700E-02 +2.080000E-01 +8.854000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.230000E-01 +3.305000E-03 +1.760000E-01 +6.598000E-03 +1.330000E-01 +3.621000E-03 +1.730000E-01 +6.279000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +6.100000E-02 +8.350000E-04 +1.410000E-01 +4.071000E-03 +1.800000E-01 +6.574000E-03 +0.000000E+00 +0.000000E+00 +1.760000E-01 +6.598000E-03 +1.230000E-01 +3.305000E-03 +1.690000E-01 +6.135000E-03 +1.220000E-01 +3.344000E-03 +1.540000E-01 +4.986000E-03 +2.690000E-01 +1.478100E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.030000E-01 +2.543000E-03 +1.870000E-01 +7.267000E-03 +1.690000E-01 +5.731000E-03 +0.000000E+00 +0.000000E+00 +1.220000E-01 +3.344000E-03 +1.690000E-01 +6.135000E-03 +0.000000E+00 +0.000000E+00 0.000000E+00 0.000000E+00 1.170000E-01 -2.789000E-03 -1.300000E-01 -3.604000E-03 +2.807000E-03 +1.760000E-01 +6.534000E-03 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.120000E-01 -2.640000E-03 -1.430000E-01 -4.163000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -4.900000E-02 -5.630000E-04 -1.670000E-01 -5.653000E-03 -1.670000E-01 -5.701000E-03 +6.000000E-02 +8.060000E-04 +1.290000E-01 +3.487000E-03 +1.790000E-01 +6.747000E-03 0.000000E+00 0.000000E+00 tally 2: @@ -660,12 +660,12 @@ tally 2: 0.000000E+00 0.000000E+00 0.000000E+00 -1.150000E-01 -2.753000E-03 +1.200000E-01 +2.924000E-03 0.000000E+00 0.000000E+00 -1.820000E-01 -6.756000E-03 +1.740000E-01 +6.270000E-03 0.000000E+00 0.000000E+00 0.000000E+00 @@ -676,256 +676,160 @@ tally 2: 0.000000E+00 0.000000E+00 0.000000E+00 -1.110000E-01 -2.601000E-03 +1.200000E-01 +3.044000E-03 0.000000E+00 0.000000E+00 -1.530000E-01 -4.865000E-03 +1.510000E-01 +4.615000E-03 0.000000E+00 0.000000E+00 -1.910000E-01 -7.535000E-03 +1.710000E-01 +6.067000E-03 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -7.100000E-02 -1.079000E-03 -0.000000E+00 -0.000000E+00 -1.500000E-01 -4.606000E-03 -0.000000E+00 -0.000000E+00 -1.820000E-01 -6.756000E-03 -0.000000E+00 -0.000000E+00 -1.150000E-01 -2.753000E-03 -0.000000E+00 -0.000000E+00 -1.610000E-01 -5.383000E-03 -0.000000E+00 -0.000000E+00 -1.230000E-01 -3.141000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.370000E-01 -3.951000E-03 -0.000000E+00 -0.000000E+00 -2.600000E-01 -1.366400E-02 -0.000000E+00 -0.000000E+00 -1.930000E-01 -7.573000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -8.500000E-02 -1.533000E-03 -0.000000E+00 -0.000000E+00 -2.040000E-01 -8.426000E-03 -0.000000E+00 -0.000000E+00 -1.230000E-01 -3.141000E-03 -0.000000E+00 -0.000000E+00 -1.610000E-01 -5.383000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.250000E-01 -3.311000E-03 -0.000000E+00 -0.000000E+00 -1.620000E-01 -5.346000E-03 -0.000000E+00 -0.000000E+00 -1.860000E-01 -6.932000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -7.100000E-02 -1.083000E-03 -0.000000E+00 -0.000000E+00 -1.630000E-01 -5.617000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.800000E-01 -7.002000E-03 -0.000000E+00 -0.000000E+00 -2.980000E-01 -1.815000E-02 -0.000000E+00 -0.000000E+00 -1.530000E-01 -4.865000E-03 -0.000000E+00 -0.000000E+00 -1.110000E-01 -2.601000E-03 -0.000000E+00 -0.000000E+00 -1.890000E-01 -7.605000E-03 -0.000000E+00 -0.000000E+00 -1.330000E-01 -3.743000E-03 -0.000000E+00 -0.000000E+00 -1.790000E-01 -6.495000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.080000E-01 -2.718000E-03 -0.000000E+00 -0.000000E+00 -2.160000E-01 -9.662000E-03 -0.000000E+00 -0.000000E+00 -2.980000E-01 -1.815000E-02 -0.000000E+00 -0.000000E+00 -1.800000E-01 -7.002000E-03 -0.000000E+00 -0.000000E+00 -2.650000E-01 -1.435500E-02 -0.000000E+00 -0.000000E+00 -1.590000E-01 -5.285000E-03 -0.000000E+00 -0.000000E+00 -2.600000E-01 -1.366400E-02 -0.000000E+00 -0.000000E+00 -1.370000E-01 -3.951000E-03 -0.000000E+00 -0.000000E+00 -2.670000E-01 -1.469300E-02 -0.000000E+00 -0.000000E+00 -1.940000E-01 -7.678000E-03 -0.000000E+00 -0.000000E+00 -2.390000E-01 -1.155500E-02 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.410000E-01 -1.255900E-02 -0.000000E+00 -0.000000E+00 -5.510000E-01 -7.358900E-02 -0.000000E+00 -0.000000E+00 -1.590000E-01 -5.285000E-03 -0.000000E+00 -0.000000E+00 -2.650000E-01 -1.435500E-02 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.620000E-01 -5.346000E-03 -0.000000E+00 -0.000000E+00 -1.250000E-01 -3.311000E-03 -0.000000E+00 -0.000000E+00 -1.870000E-01 -7.083000E-03 +6.500000E-02 +9.390000E-04 0.000000E+00 0.000000E+00 1.410000E-01 -3.999000E-03 +4.001000E-03 0.000000E+00 0.000000E+00 -2.040000E-01 -8.438000E-03 +1.740000E-01 +6.270000E-03 +0.000000E+00 +0.000000E+00 +1.200000E-01 +2.924000E-03 +0.000000E+00 +0.000000E+00 +1.830000E-01 +6.879000E-03 +0.000000E+00 +0.000000E+00 +1.310000E-01 +3.541000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.750000E-01 +6.659000E-03 +0.000000E+00 +0.000000E+00 +2.820000E-01 +1.605400E-02 +0.000000E+00 +0.000000E+00 +1.980000E-01 +7.932000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +9.400000E-02 +1.912000E-03 +0.000000E+00 +0.000000E+00 +2.200000E-01 +9.958000E-03 +0.000000E+00 +0.000000E+00 +1.310000E-01 +3.541000E-03 +0.000000E+00 +0.000000E+00 +1.830000E-01 +6.879000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.110000E-01 +2.639000E-03 +0.000000E+00 +0.000000E+00 +1.360000E-01 +3.890000E-03 +0.000000E+00 +0.000000E+00 +1.770000E-01 +6.551000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +6.700000E-02 +9.310000E-04 +0.000000E+00 +0.000000E+00 +1.570000E-01 +4.975000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.480000E-01 +4.910000E-03 +0.000000E+00 +0.000000E+00 +2.720000E-01 +1.490600E-02 +0.000000E+00 +0.000000E+00 +1.510000E-01 +4.615000E-03 +0.000000E+00 +0.000000E+00 +1.200000E-01 +3.044000E-03 +0.000000E+00 +0.000000E+00 +1.800000E-01 +6.720000E-03 +0.000000E+00 +0.000000E+00 +1.050000E-01 +2.285000E-03 +0.000000E+00 +0.000000E+00 +1.790000E-01 +6.499000E-03 0.000000E+00 0.000000E+00 0.000000E+00 @@ -933,11 +837,659 @@ tally 2: 0.000000E+00 0.000000E+00 9.300000E-02 -1.829000E-03 +1.927000E-03 0.000000E+00 0.000000E+00 -2.240000E-01 -1.015800E-02 +2.160000E-01 +9.418000E-03 +0.000000E+00 +0.000000E+00 +2.720000E-01 +1.490600E-02 +0.000000E+00 +0.000000E+00 +1.480000E-01 +4.910000E-03 +0.000000E+00 +0.000000E+00 +2.680000E-01 +1.445800E-02 +0.000000E+00 +0.000000E+00 +1.480000E-01 +4.488000E-03 +0.000000E+00 +0.000000E+00 +2.820000E-01 +1.605400E-02 +0.000000E+00 +0.000000E+00 +1.750000E-01 +6.659000E-03 +0.000000E+00 +0.000000E+00 +2.510000E-01 +1.283100E-02 +0.000000E+00 +0.000000E+00 +1.870000E-01 +7.731000E-03 +0.000000E+00 +0.000000E+00 +2.340000E-01 +1.105000E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.110000E-01 +9.215000E-03 +0.000000E+00 +0.000000E+00 +5.470000E-01 +7.228300E-02 +0.000000E+00 +0.000000E+00 +1.480000E-01 +4.488000E-03 +0.000000E+00 +0.000000E+00 +2.680000E-01 +1.445800E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.360000E-01 +3.890000E-03 +0.000000E+00 +0.000000E+00 +1.110000E-01 +2.639000E-03 +0.000000E+00 +0.000000E+00 +1.540000E-01 +4.804000E-03 +0.000000E+00 +0.000000E+00 +1.280000E-01 +3.302000E-03 +0.000000E+00 +0.000000E+00 +2.200000E-01 +9.834000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +9.900000E-02 +2.127000E-03 +0.000000E+00 +0.000000E+00 +1.860000E-01 +7.198000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.430000E-01 +4.337000E-03 +0.000000E+00 +0.000000E+00 +1.580000E-01 +5.104000E-03 +0.000000E+00 +0.000000E+00 +1.050000E-01 +2.285000E-03 +0.000000E+00 +0.000000E+00 +1.800000E-01 +6.720000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.860000E-01 +7.178000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +7.000000E-02 +1.020000E-03 +0.000000E+00 +0.000000E+00 +1.610000E-01 +5.237000E-03 +0.000000E+00 +0.000000E+00 +1.580000E-01 +5.104000E-03 +0.000000E+00 +0.000000E+00 +1.430000E-01 +4.337000E-03 +0.000000E+00 +0.000000E+00 +1.580000E-01 +5.254000E-03 +0.000000E+00 +0.000000E+00 +1.230000E-01 +3.091000E-03 +0.000000E+00 +0.000000E+00 +1.870000E-01 +7.731000E-03 +0.000000E+00 +0.000000E+00 +2.510000E-01 +1.283100E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.980000E-01 +7.934000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +9.500000E-02 +1.949000E-03 +0.000000E+00 +0.000000E+00 +2.150000E-01 +9.289000E-03 +0.000000E+00 +0.000000E+00 +1.230000E-01 +3.091000E-03 +0.000000E+00 +0.000000E+00 +1.580000E-01 +5.254000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.280000E-01 +3.302000E-03 +0.000000E+00 +0.000000E+00 +1.540000E-01 +4.804000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.640000E-01 +5.424000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +6.200000E-02 +8.020000E-04 +0.000000E+00 +0.000000E+00 +1.500000E-01 +4.588000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.700000E-01 +5.884000E-03 +0.000000E+00 +0.000000E+00 +2.280000E-01 +1.084800E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.540000E-01 +4.856000E-03 +0.000000E+00 +0.000000E+00 +2.250000E-01 +1.047100E-02 +0.000000E+00 +0.000000E+00 +1.410000E-01 +4.001000E-03 +0.000000E+00 +0.000000E+00 +6.500000E-02 +9.390000E-04 +0.000000E+00 +0.000000E+00 +1.360000E-01 +3.852000E-03 +0.000000E+00 +0.000000E+00 +6.400000E-02 +8.700000E-04 +0.000000E+00 +0.000000E+00 +2.280000E-01 +1.084800E-02 +0.000000E+00 +0.000000E+00 +1.700000E-01 +5.884000E-03 +0.000000E+00 +0.000000E+00 +2.230000E-01 +1.001100E-02 +0.000000E+00 +0.000000E+00 +1.410000E-01 +4.185000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.980000E-01 +8.038000E-03 +0.000000E+00 +0.000000E+00 +5.030000E-01 +5.613900E-02 +0.000000E+00 +0.000000E+00 +2.200000E-01 +9.958000E-03 +0.000000E+00 +0.000000E+00 +9.400000E-02 +1.912000E-03 +0.000000E+00 +0.000000E+00 +2.140000E-01 +9.376000E-03 +0.000000E+00 +0.000000E+00 +7.300000E-02 +1.091000E-03 +0.000000E+00 +0.000000E+00 +1.410000E-01 +4.185000E-03 +0.000000E+00 +0.000000E+00 +2.230000E-01 +1.001100E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.540000E-01 +4.808000E-03 +0.000000E+00 +0.000000E+00 +2.340000E-01 +1.119400E-02 +0.000000E+00 +0.000000E+00 +1.570000E-01 +4.975000E-03 +0.000000E+00 +0.000000E+00 +6.700000E-02 +9.310000E-04 +0.000000E+00 +0.000000E+00 +1.600000E-01 +5.134000E-03 +0.000000E+00 +0.000000E+00 +5.300000E-02 +6.110000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.310000E-01 +1.084100E-02 +0.000000E+00 +0.000000E+00 +5.390000E-01 +6.215100E-02 +0.000000E+00 +0.000000E+00 +2.250000E-01 +1.047100E-02 +0.000000E+00 +0.000000E+00 +1.540000E-01 +4.856000E-03 +0.000000E+00 +0.000000E+00 +2.280000E-01 +1.059800E-02 +0.000000E+00 +0.000000E+00 +1.600000E-01 +5.214000E-03 +0.000000E+00 +0.000000E+00 +2.160000E-01 +9.418000E-03 +0.000000E+00 +0.000000E+00 +9.300000E-02 +1.927000E-03 +0.000000E+00 +0.000000E+00 +2.510000E-01 +1.271500E-02 +0.000000E+00 +0.000000E+00 +1.030000E-01 +2.229000E-03 +0.000000E+00 +0.000000E+00 +5.390000E-01 +6.215100E-02 +0.000000E+00 +0.000000E+00 +2.310000E-01 +1.084100E-02 +0.000000E+00 +0.000000E+00 +4.700000E-01 +4.972200E-02 +0.000000E+00 +0.000000E+00 +2.190000E-01 +9.821000E-03 +0.000000E+00 +0.000000E+00 +5.030000E-01 +5.613900E-02 +0.000000E+00 +0.000000E+00 +1.980000E-01 +8.038000E-03 +0.000000E+00 +0.000000E+00 +5.100000E-01 +5.929000E-02 +0.000000E+00 +0.000000E+00 +2.140000E-01 +9.406000E-03 +0.000000E+00 +0.000000E+00 +5.470000E-01 +7.228300E-02 +0.000000E+00 +0.000000E+00 +2.110000E-01 +9.215000E-03 +0.000000E+00 +0.000000E+00 +5.160000E-01 +5.892200E-02 +0.000000E+00 +0.000000E+00 +2.550000E-01 +1.480500E-02 +0.000000E+00 +0.000000E+00 +2.190000E-01 +9.821000E-03 +0.000000E+00 +0.000000E+00 +4.700000E-01 +4.972200E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.340000E-01 +1.119400E-02 +0.000000E+00 +0.000000E+00 +1.540000E-01 +4.808000E-03 +0.000000E+00 +0.000000E+00 +2.170000E-01 +9.509000E-03 +0.000000E+00 +0.000000E+00 +1.600000E-01 +5.156000E-03 +0.000000E+00 +0.000000E+00 +1.860000E-01 +7.198000E-03 +0.000000E+00 +0.000000E+00 +9.900000E-02 +2.127000E-03 +0.000000E+00 +0.000000E+00 +2.290000E-01 +1.058700E-02 +0.000000E+00 +0.000000E+00 +9.000000E-02 +1.780000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.550000E-01 +4.913000E-03 +0.000000E+00 +0.000000E+00 +2.380000E-01 +1.153400E-02 +0.000000E+00 +0.000000E+00 +1.600000E-01 +5.214000E-03 +0.000000E+00 +0.000000E+00 +2.280000E-01 +1.059800E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.610000E-01 +5.237000E-03 +0.000000E+00 +0.000000E+00 +7.000000E-02 +1.020000E-03 +0.000000E+00 +0.000000E+00 +1.410000E-01 +4.071000E-03 +0.000000E+00 +0.000000E+00 +6.100000E-02 +8.350000E-04 +0.000000E+00 +0.000000E+00 +2.380000E-01 +1.153400E-02 +0.000000E+00 +0.000000E+00 +1.550000E-01 +4.913000E-03 +0.000000E+00 +0.000000E+00 +2.320000E-01 +1.100400E-02 +0.000000E+00 +0.000000E+00 +1.460000E-01 +4.498000E-03 +0.000000E+00 +0.000000E+00 +2.140000E-01 +9.406000E-03 +0.000000E+00 +0.000000E+00 +5.100000E-01 +5.929000E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.150000E-01 +9.289000E-03 +0.000000E+00 +0.000000E+00 +9.500000E-02 +1.949000E-03 +0.000000E+00 +0.000000E+00 +1.870000E-01 +7.267000E-03 +0.000000E+00 +0.000000E+00 +1.030000E-01 +2.543000E-03 +0.000000E+00 +0.000000E+00 +1.460000E-01 +4.498000E-03 +0.000000E+00 +0.000000E+00 +2.320000E-01 +1.100400E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.600000E-01 +5.156000E-03 +0.000000E+00 +0.000000E+00 +2.170000E-01 +9.509000E-03 0.000000E+00 0.000000E+00 0.000000E+00 @@ -949,19 +1501,19 @@ tally 2: 0.000000E+00 0.000000E+00 1.500000E-01 -4.522000E-03 +4.588000E-03 0.000000E+00 0.000000E+00 -1.630000E-01 -5.439000E-03 +6.200000E-02 +8.020000E-04 0.000000E+00 0.000000E+00 -1.330000E-01 -3.743000E-03 +1.290000E-01 +3.487000E-03 0.000000E+00 0.000000E+00 -1.890000E-01 -7.605000E-03 +6.000000E-02 +8.060000E-04 0.000000E+00 0.000000E+00 0.000000E+00 @@ -972,220 +1524,100 @@ tally 2: 0.000000E+00 0.000000E+00 0.000000E+00 +1.090000E-01 +2.437000E-03 +0.000000E+00 +0.000000E+00 +1.650000E-01 +5.655000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.230000E-01 +3.065000E-03 +0.000000E+00 +0.000000E+00 +1.570000E-01 +4.999000E-03 +0.000000E+00 +0.000000E+00 +6.400000E-02 +8.700000E-04 +0.000000E+00 +0.000000E+00 +1.360000E-01 +3.852000E-03 +0.000000E+00 +0.000000E+00 +1.750000E-01 +6.299000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 1.650000E-01 -5.615000E-03 +5.655000E-03 0.000000E+00 0.000000E+00 +1.090000E-01 +2.437000E-03 0.000000E+00 0.000000E+00 +1.480000E-01 +4.674000E-03 0.000000E+00 0.000000E+00 -7.100000E-02 -1.191000E-03 +1.320000E-01 +3.548000E-03 0.000000E+00 0.000000E+00 -1.550000E-01 -5.011000E-03 0.000000E+00 0.000000E+00 -1.630000E-01 -5.439000E-03 0.000000E+00 0.000000E+00 -1.500000E-01 -4.522000E-03 0.000000E+00 0.000000E+00 -1.680000E-01 -5.966000E-03 -0.000000E+00 -0.000000E+00 -1.290000E-01 -3.515000E-03 -0.000000E+00 -0.000000E+00 -1.940000E-01 -7.678000E-03 -0.000000E+00 -0.000000E+00 -2.670000E-01 -1.469300E-02 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.810000E-01 -6.765000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.080000E-01 -2.694000E-03 -0.000000E+00 -0.000000E+00 -2.070000E-01 -8.673000E-03 -0.000000E+00 -0.000000E+00 -1.290000E-01 -3.515000E-03 -0.000000E+00 -0.000000E+00 -1.680000E-01 -5.966000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.410000E-01 -3.999000E-03 -0.000000E+00 -0.000000E+00 -1.870000E-01 -7.083000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.870000E-01 -7.213000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -8.200000E-02 -1.536000E-03 -0.000000E+00 -0.000000E+00 -1.760000E-01 -6.550000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.750000E-01 -6.163000E-03 -0.000000E+00 -0.000000E+00 -2.570000E-01 -1.347100E-02 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.560000E-01 -4.914000E-03 -0.000000E+00 -0.000000E+00 -2.350000E-01 -1.135900E-02 -0.000000E+00 -0.000000E+00 -1.500000E-01 -4.606000E-03 -0.000000E+00 -0.000000E+00 -7.100000E-02 -1.079000E-03 -0.000000E+00 -0.000000E+00 -1.530000E-01 -4.837000E-03 -0.000000E+00 -0.000000E+00 -5.600000E-02 -7.260000E-04 -0.000000E+00 -0.000000E+00 -2.570000E-01 -1.347100E-02 -0.000000E+00 -0.000000E+00 -1.750000E-01 -6.163000E-03 -0.000000E+00 -0.000000E+00 -2.460000E-01 -1.235000E-02 -0.000000E+00 -0.000000E+00 -1.420000E-01 -4.326000E-03 0.000000E+00 0.000000E+00 +1.440000E-01 +4.282000E-03 0.000000E+00 0.000000E+00 +2.190000E-01 +9.683000E-03 0.000000E+00 0.000000E+00 +7.300000E-02 +1.091000E-03 0.000000E+00 0.000000E+00 +2.140000E-01 +9.376000E-03 0.000000E+00 0.000000E+00 1.830000E-01 -7.133000E-03 +6.803000E-03 0.000000E+00 0.000000E+00 -5.110000E-01 -5.696100E-02 0.000000E+00 0.000000E+00 -2.040000E-01 -8.426000E-03 0.000000E+00 0.000000E+00 -8.500000E-02 -1.533000E-03 +1.320000E-01 +3.548000E-03 0.000000E+00 0.000000E+00 -2.100000E-01 -8.928000E-03 -0.000000E+00 -0.000000E+00 -9.100000E-02 -1.807000E-03 -0.000000E+00 -0.000000E+00 -1.420000E-01 -4.326000E-03 -0.000000E+00 -0.000000E+00 -2.460000E-01 -1.235000E-02 +1.480000E-01 +4.674000E-03 0.000000E+00 0.000000E+00 0.000000E+00 @@ -1204,456 +1636,24 @@ tally 2: 0.000000E+00 0.000000E+00 0.000000E+00 +1.350000E-01 +3.895000E-03 +0.000000E+00 +0.000000E+00 1.430000E-01 -4.207000E-03 +4.297000E-03 0.000000E+00 0.000000E+00 -2.120000E-01 -9.278000E-03 -0.000000E+00 -0.000000E+00 -1.630000E-01 -5.617000E-03 -0.000000E+00 -0.000000E+00 -7.100000E-02 -1.083000E-03 -0.000000E+00 -0.000000E+00 -1.770000E-01 -6.293000E-03 -0.000000E+00 -0.000000E+00 -7.900000E-02 -1.495000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.300000E-01 -1.081000E-02 -0.000000E+00 -0.000000E+00 -5.200000E-01 -5.835000E-02 -0.000000E+00 -0.000000E+00 -2.350000E-01 -1.135900E-02 -0.000000E+00 -0.000000E+00 -1.560000E-01 -4.914000E-03 -0.000000E+00 -0.000000E+00 -2.370000E-01 -1.142300E-02 -0.000000E+00 -0.000000E+00 -1.360000E-01 -3.882000E-03 -0.000000E+00 -0.000000E+00 -2.160000E-01 -9.662000E-03 -0.000000E+00 -0.000000E+00 -1.080000E-01 -2.718000E-03 -0.000000E+00 -0.000000E+00 -2.090000E-01 -8.841000E-03 -0.000000E+00 -0.000000E+00 -1.030000E-01 -2.271000E-03 -0.000000E+00 -0.000000E+00 -5.200000E-01 -5.835000E-02 -0.000000E+00 -0.000000E+00 -2.300000E-01 -1.081000E-02 -0.000000E+00 -0.000000E+00 -5.110000E-01 -5.615900E-02 -0.000000E+00 -0.000000E+00 -2.430000E-01 -1.228300E-02 -0.000000E+00 -0.000000E+00 -5.110000E-01 -5.696100E-02 -0.000000E+00 -0.000000E+00 -1.830000E-01 -7.133000E-03 -0.000000E+00 -0.000000E+00 -5.130000E-01 -6.002100E-02 -0.000000E+00 -0.000000E+00 -2.420000E-01 -1.209200E-02 -0.000000E+00 -0.000000E+00 -5.510000E-01 -7.358900E-02 -0.000000E+00 -0.000000E+00 -2.410000E-01 -1.255900E-02 -0.000000E+00 -0.000000E+00 -5.140000E-01 -5.890200E-02 -0.000000E+00 -0.000000E+00 -2.100000E-01 -1.004600E-02 -0.000000E+00 -0.000000E+00 -2.430000E-01 -1.228300E-02 -0.000000E+00 -0.000000E+00 -5.110000E-01 -5.615900E-02 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.120000E-01 -9.278000E-03 -0.000000E+00 -0.000000E+00 -1.430000E-01 -4.207000E-03 -0.000000E+00 -0.000000E+00 -2.380000E-01 -1.133600E-02 -0.000000E+00 -0.000000E+00 -1.640000E-01 -5.444000E-03 -0.000000E+00 -0.000000E+00 -2.240000E-01 -1.015800E-02 -0.000000E+00 -0.000000E+00 -9.300000E-02 -1.829000E-03 -0.000000E+00 -0.000000E+00 -1.950000E-01 -7.705000E-03 -0.000000E+00 -0.000000E+00 -1.030000E-01 -2.191000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.680000E-01 -5.878000E-03 -0.000000E+00 -0.000000E+00 -2.230000E-01 -1.049900E-02 -0.000000E+00 -0.000000E+00 -1.360000E-01 -3.882000E-03 -0.000000E+00 -0.000000E+00 -2.370000E-01 -1.142300E-02 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.550000E-01 -5.011000E-03 -0.000000E+00 -0.000000E+00 -7.100000E-02 -1.191000E-03 -0.000000E+00 -0.000000E+00 -1.510000E-01 -4.845000E-03 -0.000000E+00 -0.000000E+00 -6.400000E-02 -9.460000E-04 -0.000000E+00 -0.000000E+00 -2.230000E-01 -1.049900E-02 -0.000000E+00 -0.000000E+00 -1.680000E-01 -5.878000E-03 -0.000000E+00 -0.000000E+00 -2.490000E-01 -1.255900E-02 -0.000000E+00 -0.000000E+00 -1.280000E-01 -3.448000E-03 -0.000000E+00 -0.000000E+00 -2.420000E-01 -1.209200E-02 -0.000000E+00 -0.000000E+00 -5.130000E-01 -6.002100E-02 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.070000E-01 -8.673000E-03 -0.000000E+00 -0.000000E+00 -1.080000E-01 -2.694000E-03 -0.000000E+00 -0.000000E+00 -2.000000E-01 -8.202000E-03 -0.000000E+00 -0.000000E+00 -9.600000E-02 -2.024000E-03 -0.000000E+00 -0.000000E+00 -1.280000E-01 -3.448000E-03 -0.000000E+00 -0.000000E+00 -2.490000E-01 -1.255900E-02 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.640000E-01 -5.444000E-03 -0.000000E+00 -0.000000E+00 -2.380000E-01 -1.133600E-02 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.760000E-01 -6.550000E-03 -0.000000E+00 -0.000000E+00 -8.200000E-02 -1.536000E-03 -0.000000E+00 -0.000000E+00 -1.670000E-01 -5.653000E-03 -0.000000E+00 -0.000000E+00 -4.900000E-02 -5.630000E-04 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.320000E-01 -3.626000E-03 -0.000000E+00 -0.000000E+00 -1.510000E-01 -4.831000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.370000E-01 -3.855000E-03 -0.000000E+00 -0.000000E+00 -1.610000E-01 -5.233000E-03 -0.000000E+00 -0.000000E+00 -5.600000E-02 -7.260000E-04 -0.000000E+00 -0.000000E+00 -1.530000E-01 -4.837000E-03 -0.000000E+00 -0.000000E+00 -1.610000E-01 -5.261000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.510000E-01 -4.831000E-03 -0.000000E+00 -0.000000E+00 -1.320000E-01 -3.626000E-03 -0.000000E+00 -0.000000E+00 -1.490000E-01 -4.731000E-03 -0.000000E+00 -0.000000E+00 -1.320000E-01 -3.514000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.550000E-01 -4.921000E-03 -0.000000E+00 -0.000000E+00 -2.410000E-01 -1.168300E-02 -0.000000E+00 -0.000000E+00 -9.100000E-02 -1.807000E-03 -0.000000E+00 -0.000000E+00 -2.100000E-01 -8.928000E-03 -0.000000E+00 -0.000000E+00 -2.090000E-01 -8.775000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.320000E-01 -3.514000E-03 -0.000000E+00 -0.000000E+00 -1.490000E-01 -4.731000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.150000E-01 -2.891000E-03 -0.000000E+00 -0.000000E+00 -1.560000E-01 -5.154000E-03 -0.000000E+00 -0.000000E+00 -7.900000E-02 -1.495000E-03 -0.000000E+00 -0.000000E+00 -1.770000E-01 -6.293000E-03 +5.300000E-02 +6.110000E-04 0.000000E+00 0.000000E+00 1.600000E-01 -5.352000E-03 +5.134000E-03 +0.000000E+00 +0.000000E+00 +1.380000E-01 +4.220000E-03 0.000000E+00 0.000000E+00 0.000000E+00 @@ -1668,232 +1668,248 @@ tally 2: 0.000000E+00 0.000000E+00 0.000000E+00 -1.610000E-01 -5.273000E-03 +1.900000E-01 +7.412000E-03 0.000000E+00 0.000000E+00 -2.310000E-01 -1.098100E-02 +2.420000E-01 +1.208400E-02 0.000000E+00 0.000000E+00 -1.610000E-01 -5.233000E-03 +1.570000E-01 +4.999000E-03 0.000000E+00 0.000000E+00 -1.370000E-01 -3.855000E-03 +1.230000E-01 +3.065000E-03 0.000000E+00 0.000000E+00 -1.760000E-01 -6.482000E-03 +1.730000E-01 +6.279000E-03 0.000000E+00 0.000000E+00 -1.340000E-01 -3.882000E-03 +1.330000E-01 +3.621000E-03 0.000000E+00 0.000000E+00 1.030000E-01 -2.271000E-03 +2.229000E-03 0.000000E+00 0.000000E+00 -2.090000E-01 -8.841000E-03 -0.000000E+00 -0.000000E+00 -1.830000E-01 -6.761000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.310000E-01 -1.098100E-02 -0.000000E+00 -0.000000E+00 -1.610000E-01 -5.273000E-03 -0.000000E+00 -0.000000E+00 -2.620000E-01 -1.380600E-02 -0.000000E+00 -0.000000E+00 -1.700000E-01 -5.970000E-03 -0.000000E+00 -0.000000E+00 -2.410000E-01 -1.168300E-02 -0.000000E+00 -0.000000E+00 -1.550000E-01 -4.921000E-03 -0.000000E+00 -0.000000E+00 -2.250000E-01 -1.014100E-02 -0.000000E+00 -0.000000E+00 -1.370000E-01 -3.805000E-03 +2.510000E-01 +1.271500E-02 0.000000E+00 0.000000E+00 2.100000E-01 -1.004600E-02 +8.970000E-03 0.000000E+00 0.000000E+00 -5.140000E-01 -5.890200E-02 0.000000E+00 0.000000E+00 -2.320000E-01 -1.093000E-02 0.000000E+00 0.000000E+00 +2.420000E-01 +1.208400E-02 0.000000E+00 0.000000E+00 +1.900000E-01 +7.412000E-03 0.000000E+00 0.000000E+00 -1.700000E-01 -5.970000E-03 -0.000000E+00 -0.000000E+00 -2.620000E-01 -1.380600E-02 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.560000E-01 -5.154000E-03 -0.000000E+00 -0.000000E+00 -1.150000E-01 -2.891000E-03 -0.000000E+00 -0.000000E+00 -1.430000E-01 -4.163000E-03 -0.000000E+00 -0.000000E+00 -1.120000E-01 -2.640000E-03 -0.000000E+00 -0.000000E+00 -1.030000E-01 -2.191000E-03 -0.000000E+00 -0.000000E+00 -1.950000E-01 -7.705000E-03 -0.000000E+00 -0.000000E+00 -1.760000E-01 -6.392000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -9.200000E-02 -1.822000E-03 -0.000000E+00 -0.000000E+00 -1.370000E-01 -3.843000E-03 -0.000000E+00 -0.000000E+00 -1.340000E-01 -3.882000E-03 -0.000000E+00 -0.000000E+00 -1.760000E-01 -6.482000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -6.400000E-02 -9.460000E-04 -0.000000E+00 -0.000000E+00 -1.510000E-01 -4.845000E-03 -0.000000E+00 -0.000000E+00 -1.800000E-01 -6.520000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.370000E-01 -3.843000E-03 -0.000000E+00 -0.000000E+00 -9.200000E-02 -1.822000E-03 -0.000000E+00 -0.000000E+00 -1.300000E-01 -3.604000E-03 -0.000000E+00 -0.000000E+00 -1.170000E-01 -2.789000E-03 -0.000000E+00 -0.000000E+00 -1.370000E-01 -3.805000E-03 -0.000000E+00 -0.000000E+00 -2.250000E-01 -1.014100E-02 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -9.600000E-02 -2.024000E-03 +2.610000E-01 +1.399900E-02 0.000000E+00 0.000000E+00 2.000000E-01 -8.202000E-03 +8.410000E-03 +0.000000E+00 +0.000000E+00 +2.190000E-01 +9.683000E-03 +0.000000E+00 +0.000000E+00 +1.440000E-01 +4.282000E-03 +0.000000E+00 +0.000000E+00 +2.690000E-01 +1.478100E-02 +0.000000E+00 +0.000000E+00 +1.540000E-01 +4.986000E-03 +0.000000E+00 +0.000000E+00 +2.550000E-01 +1.480500E-02 +0.000000E+00 +0.000000E+00 +5.160000E-01 +5.892200E-02 +0.000000E+00 +0.000000E+00 +2.310000E-01 +1.098900E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-01 +8.410000E-03 +0.000000E+00 +0.000000E+00 +2.610000E-01 +1.399900E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.430000E-01 +4.297000E-03 +0.000000E+00 +0.000000E+00 +1.350000E-01 +3.895000E-03 +0.000000E+00 +0.000000E+00 +1.760000E-01 +6.534000E-03 +0.000000E+00 +0.000000E+00 +1.170000E-01 +2.807000E-03 +0.000000E+00 +0.000000E+00 +9.000000E-02 +1.780000E-03 +0.000000E+00 +0.000000E+00 +2.290000E-01 +1.058700E-02 +0.000000E+00 +0.000000E+00 +2.080000E-01 +8.854000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.230000E-01 +3.305000E-03 +0.000000E+00 +0.000000E+00 +1.760000E-01 +6.598000E-03 +0.000000E+00 +0.000000E+00 +1.330000E-01 +3.621000E-03 +0.000000E+00 +0.000000E+00 +1.730000E-01 +6.279000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +6.100000E-02 +8.350000E-04 +0.000000E+00 +0.000000E+00 +1.410000E-01 +4.071000E-03 +0.000000E+00 +0.000000E+00 +1.800000E-01 +6.574000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.760000E-01 +6.598000E-03 +0.000000E+00 +0.000000E+00 +1.230000E-01 +3.305000E-03 +0.000000E+00 +0.000000E+00 +1.690000E-01 +6.135000E-03 +0.000000E+00 +0.000000E+00 +1.220000E-01 +3.344000E-03 +0.000000E+00 +0.000000E+00 +1.540000E-01 +4.986000E-03 +0.000000E+00 +0.000000E+00 +2.690000E-01 +1.478100E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.030000E-01 +2.543000E-03 +0.000000E+00 +0.000000E+00 +1.870000E-01 +7.267000E-03 +0.000000E+00 +0.000000E+00 +1.690000E-01 +5.731000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.220000E-01 +3.344000E-03 +0.000000E+00 +0.000000E+00 +1.690000E-01 +6.135000E-03 +0.000000E+00 +0.000000E+00 0.000000E+00 0.000000E+00 -1.820000E-01 -6.660000E-03 0.000000E+00 0.000000E+00 0.000000E+00 @@ -1901,11 +1917,11 @@ tally 2: 0.000000E+00 0.000000E+00 1.170000E-01 -2.789000E-03 +2.807000E-03 0.000000E+00 0.000000E+00 -1.300000E-01 -3.604000E-03 +1.760000E-01 +6.534000E-03 0.000000E+00 0.000000E+00 0.000000E+00 @@ -1916,32 +1932,16 @@ tally 2: 0.000000E+00 0.000000E+00 0.000000E+00 -1.120000E-01 -2.640000E-03 +6.000000E-02 +8.060000E-04 0.000000E+00 0.000000E+00 -1.430000E-01 -4.163000E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -4.900000E-02 -5.630000E-04 -0.000000E+00 -0.000000E+00 -1.670000E-01 -5.653000E-03 -0.000000E+00 -0.000000E+00 -1.670000E-01 -5.701000E-03 +1.290000E-01 +3.487000E-03 +0.000000E+00 +0.000000E+00 +1.790000E-01 +6.747000E-03 0.000000E+00 0.000000E+00 0.000000E+00 diff --git a/tests/regression_tests/seed/results_true.dat b/tests/regression_tests/seed/results_true.dat index bcd711825..ff34071c1 100644 --- a/tests/regression_tests/seed/results_true.dat +++ b/tests/regression_tests/seed/results_true.dat @@ -1,2 +1,2 @@ k-combined: -3.069730E-01 2.632099E-03 +3.015003E-01 5.094212E-03 diff --git a/tests/regression_tests/source/results_true.dat b/tests/regression_tests/source/results_true.dat index 673d27c8a..951075bbb 100644 --- a/tests/regression_tests/source/results_true.dat +++ b/tests/regression_tests/source/results_true.dat @@ -1,2 +1,2 @@ k-combined: -2.959436E-01 2.782384E-03 +3.034717E-01 2.799386E-03 diff --git a/tests/regression_tests/source_file/results_true.dat b/tests/regression_tests/source_file/results_true.dat index 264e3d580..359e0526e 100644 --- a/tests/regression_tests/source_file/results_true.dat +++ b/tests/regression_tests/source_file/results_true.dat @@ -1,2 +1,2 @@ k-combined: -2.983135E-01 5.116978E-03 +2.827397E-01 1.150437E-03 diff --git a/tests/regression_tests/source_mcpl_file/results_true.dat b/tests/regression_tests/source_mcpl_file/results_true.dat index 264e3d580..3ba1a4520 100644 --- a/tests/regression_tests/source_mcpl_file/results_true.dat +++ b/tests/regression_tests/source_mcpl_file/results_true.dat @@ -1,2 +1,2 @@ k-combined: -2.983135E-01 5.116978E-03 +2.827397E-01 1.150438E-03 diff --git a/tests/regression_tests/sourcepoint_batch/results_true.dat b/tests/regression_tests/sourcepoint_batch/results_true.dat index 2770f25ed..3665bdd08 100644 --- a/tests/regression_tests/sourcepoint_batch/results_true.dat +++ b/tests/regression_tests/sourcepoint_batch/results_true.dat @@ -1,3 +1,3 @@ k-combined: -3.074376E-01 3.049465E-03 -3.667754E+00 -7.701697E+00 2.213664E+00 +2.920435E-01 9.109227E-04 +1.101997E+00 -8.197502E+00 4.294606E+00 diff --git a/tests/regression_tests/sourcepoint_latest/results_true.dat b/tests/regression_tests/sourcepoint_latest/results_true.dat index bbf03de94..97b997ae6 100644 --- a/tests/regression_tests/sourcepoint_latest/results_true.dat +++ b/tests/regression_tests/sourcepoint_latest/results_true.dat @@ -1,2 +1,2 @@ k-combined: -3.070134E-01 3.900396E-03 +2.940336E-01 7.338463E-04 diff --git a/tests/regression_tests/sourcepoint_restart/results_true.dat b/tests/regression_tests/sourcepoint_restart/results_true.dat index b6d85872d..c20b5f2a0 100644 --- a/tests/regression_tests/sourcepoint_restart/results_true.dat +++ b/tests/regression_tests/sourcepoint_restart/results_true.dat @@ -1,46 +1,14 @@ k-combined: -3.070134E-01 3.900396E-03 +2.940336E-01 7.338463E-04 tally 1: 1.300000E-02 -4.700000E-05 -5.741381E-03 -9.030024E-06 +3.900000E-05 +5.833114E-03 +7.476880E-06 0.000000E+00 0.000000E+00 -3.067112E-04 -9.407179E-08 -1.000000E-03 -1.000000E-06 -3.015814E-04 -9.095135E-08 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.600000E-02 -1.420000E-04 -1.268989E-02 -3.464490E-05 -0.000000E+00 -0.000000E+00 -2.948908E-04 -8.696057E-08 -1.000000E-03 -1.000000E-06 -1.535009E-03 -1.224718E-06 -1.000000E-03 -1.000000E-06 -0.000000E+00 -0.000000E+00 -3.200000E-02 -2.760000E-04 -1.248507E-02 -3.714039E-05 -0.000000E+00 -0.000000E+00 -9.208601E-04 -4.708050E-07 +1.164000E-03 +5.072152E-07 0.000000E+00 0.000000E+00 0.000000E+00 @@ -49,602 +17,26 @@ tally 1: 0.000000E+00 0.000000E+00 0.000000E+00 -1.900000E-02 -7.500000E-05 -8.218811E-03 -1.462963E-05 +2.100000E-02 +1.110000E-04 +1.108363E-02 +2.889480E-05 0.000000E+00 0.000000E+00 -9.070399E-04 -2.743247E-07 -0.000000E+00 -0.000000E+00 -9.047443E-04 -8.185622E-07 -1.000000E-03 -1.000000E-06 -0.000000E+00 -0.000000E+00 -1.200000E-02 -5.000000E-05 -3.938144E-03 -5.245641E-06 -0.000000E+00 -0.000000E+00 -3.067112E-04 -9.407179E-08 -1.000000E-03 -1.000000E-06 -5.897816E-04 -3.478423E-07 -0.000000E+00 -0.000000E+00 -2.948908E-04 -8.696057E-08 -2.000000E-02 -1.260000E-04 -9.079852E-03 -2.657486E-05 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -3.100000E-02 -2.010000E-04 -1.336346E-02 -3.645780E-05 -0.000000E+00 -0.000000E+00 -2.102710E-03 -1.524522E-06 -1.000000E-03 -1.000000E-06 -9.083133E-04 -4.632477E-07 -1.000000E-03 -1.000000E-06 -0.000000E+00 -0.000000E+00 -2.300000E-02 -1.090000E-04 -1.334578E-02 -3.590708E-05 -0.000000E+00 -0.000000E+00 -6.031628E-04 -3.638054E-07 -1.000000E-03 -1.000000E-06 -1.530605E-03 -1.035684E-06 +5.861433E-04 +3.435640E-07 2.000000E-03 2.000000E-06 -6.082927E-04 -1.850231E-07 -1.400000E-02 -4.200000E-05 -7.257468E-03 -1.221578E-05 -0.000000E+00 -0.000000E+00 -2.948908E-04 -8.696057E-08 -0.000000E+00 -0.000000E+00 -8.980536E-04 -4.507660E-07 +2.043490E-03 +1.793389E-06 2.000000E-03 -4.000000E-06 -0.000000E+00 -0.000000E+00 -9.000000E-03 -2.700000E-05 -5.193169E-03 -1.187321E-05 -0.000000E+00 -0.000000E+00 -3.054379E-04 -9.329231E-08 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.000000E-02 -2.400000E-05 -5.170002E-03 -7.361546E-06 -0.000000E+00 -0.000000E+00 -6.003287E-04 -1.802529E-07 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.800000E-02 -1.720000E-04 -1.699195E-02 -6.267008E-05 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.800000E-02 -1.980000E-04 -1.338750E-02 -4.565234E-05 -0.000000E+00 -0.000000E+00 -9.183134E-04 -4.676871E-07 -1.000000E-03 -1.000000E-06 -1.537188E-03 -2.362946E-06 -2.000000E-03 -4.000000E-06 -0.000000E+00 -0.000000E+00 -2.900000E-02 -1.950000E-04 -1.178615E-02 -3.130999E-05 -0.000000E+00 -0.000000E+00 -3.015814E-04 -9.095135E-08 -0.000000E+00 -0.000000E+00 -1.179563E-03 -1.391369E-06 -1.000000E-03 -1.000000E-06 -0.000000E+00 -0.000000E+00 -1.500000E-02 -6.500000E-05 -5.706064E-03 -9.729293E-06 -0.000000E+00 -0.000000E+00 -2.948908E-04 -8.696057E-08 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -3.000000E-03 -9.000000E-06 -1.516642E-03 -1.579637E-06 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -5.897816E-04 -3.478423E-07 -1.000000E-03 -1.000000E-06 -0.000000E+00 -0.000000E+00 -2.900000E-02 -1.970000E-04 -1.274851E-02 -3.511219E-05 -0.000000E+00 -0.000000E+00 -9.057666E-04 -4.601298E-07 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.800000E-02 -7.600000E-05 -8.803642E-03 -1.730938E-05 -0.000000E+00 -0.000000E+00 -3.074376E-04 -9.451786E-08 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.900000E-02 -1.830000E-04 -1.245316E-02 -3.378176E-05 -0.000000E+00 -0.000000E+00 -6.016020E-04 -1.810324E-07 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.000000E-03 -1.000000E-06 -6.090190E-04 -1.854692E-07 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -6.031628E-04 -3.638054E-07 -1.000000E-03 -1.000000E-06 -0.000000E+00 -0.000000E+00 -1.900000E-02 -8.700000E-05 -8.781239E-03 -1.676750E-05 -0.000000E+00 -0.000000E+00 -6.128755E-04 -1.878102E-07 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -3.500000E-02 -2.930000E-04 -1.372494E-02 -4.750881E-05 -0.000000E+00 -0.000000E+00 -9.090396E-04 -2.755502E-07 -0.000000E+00 -0.000000E+00 -5.897816E-04 -3.478423E-07 -1.000000E-03 -1.000000E-06 -0.000000E+00 -0.000000E+00 -3.700000E-02 -3.270000E-04 -1.492073E-02 -5.134793E-05 -0.000000E+00 -0.000000E+00 -6.134225E-04 -3.762872E-07 -1.000000E-03 -1.000000E-06 -3.074376E-04 -9.451786E-08 -1.000000E-03 -1.000000E-06 -6.148751E-04 -3.780714E-07 -3.500000E-02 -2.810000E-04 -1.728232E-02 -6.621096E-05 -0.000000E+00 -0.000000E+00 -1.214477E-03 -3.688425E-07 -0.000000E+00 -0.000000E+00 -1.481145E-03 -1.482321E-06 -1.000000E-03 -1.000000E-06 -3.015814E-04 -9.095135E-08 -8.000000E-03 -1.800000E-05 -3.359923E-03 -3.081200E-06 -0.000000E+00 -0.000000E+00 -3.067112E-04 -9.407179E-08 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -7.000000E-03 -1.500000E-05 -3.660087E-03 -3.561493E-06 -0.000000E+00 -0.000000E+00 -9.203130E-04 -4.713637E-07 -1.000000E-03 -1.000000E-06 -3.015814E-04 -9.095135E-08 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -3.500000E-02 -2.630000E-04 -1.425810E-02 -4.231681E-05 -0.000000E+00 -0.000000E+00 -1.516059E-03 -4.597939E-07 -0.000000E+00 -0.000000E+00 -1.221752E-03 -1.492677E-06 -1.000000E-03 -1.000000E-06 -0.000000E+00 -0.000000E+00 -2.300000E-02 -1.110000E-04 -1.184871E-02 -2.918189E-05 -0.000000E+00 -0.000000E+00 -6.016020E-04 -1.810324E-07 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.600000E-02 -6.600000E-05 -7.879913E-03 -1.566560E-05 -0.000000E+00 -0.000000E+00 -9.039098E-04 -2.724298E-07 -0.000000E+00 -0.000000E+00 -3.067112E-04 -9.407179E-08 -1.000000E-03 -1.000000E-06 -0.000000E+00 -0.000000E+00 -9.000000E-03 -1.900000E-05 -4.256120E-03 -4.431220E-06 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -6.000000E-03 -1.200000E-05 -2.739774E-03 -2.333986E-06 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.400000E-02 -6.800000E-05 -7.182805E-03 -1.822340E-05 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.500000E-02 -6.300000E-05 -5.734575E-03 -9.207711E-06 -0.000000E+00 -0.000000E+00 -6.070193E-04 -1.842437E-07 -0.000000E+00 -0.000000E+00 -8.846723E-04 -7.826452E-07 -1.000000E-03 -1.000000E-06 -0.000000E+00 -0.000000E+00 +2.000000E-06 +2.930717E-04 +8.589100E-08 2.000000E-02 -8.800000E-05 -9.416733E-03 -2.020354E-05 -0.000000E+00 -0.000000E+00 -6.003287E-04 -1.802529E-07 -0.000000E+00 -0.000000E+00 -6.031628E-04 -3.638054E-07 -0.000000E+00 -0.000000E+00 -3.015814E-04 -9.095135E-08 -7.000000E-03 -1.500000E-05 -2.413278E-03 -1.439032E-06 -0.000000E+00 -0.000000E+00 -2.948908E-04 -8.696057E-08 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -7.000000E-03 -1.500000E-05 -4.226919E-03 -4.173413E-06 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.300000E-02 -1.590000E-04 -1.125341E-02 -3.890966E-05 -0.000000E+00 -0.000000E+00 -5.964722E-04 -1.779119E-07 -0.000000E+00 -0.000000E+00 -8.846723E-04 -7.826452E-07 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -3.200000E-02 -2.260000E-04 -1.458976E-02 -4.416780E-05 -0.000000E+00 -0.000000E+00 -1.814806E-03 -9.096012E-07 -1.000000E-03 -1.000000E-06 -6.031628E-04 -3.638054E-07 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -3.100000E-02 -2.050000E-04 -1.241089E-02 -3.545088E-05 -0.000000E+00 -0.000000E+00 -1.515620E-03 -1.191731E-06 -0.000000E+00 -0.000000E+00 -6.121491E-04 -1.873641E-07 -1.000000E-03 -1.000000E-06 -0.000000E+00 -0.000000E+00 -1.000000E-02 -2.600000E-05 -3.936862E-03 -3.749154E-06 -0.000000E+00 -0.000000E+00 -2.948908E-04 -8.696057E-08 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.100000E-02 -4.700000E-05 -5.084641E-03 -9.248485E-06 +9.000000E-05 +9.334862E-03 +1.853104E-05 0.000000E+00 0.000000E+00 0.000000E+00 @@ -658,45 +50,93 @@ tally 1: 0.000000E+00 0.000000E+00 2.500000E-02 -1.770000E-04 -1.021860E-02 -3.099521E-05 +1.430000E-04 +1.110224E-02 +2.872840E-05 0.000000E+00 0.000000E+00 -3.074376E-04 -9.451786E-08 +5.833203E-04 +1.701353E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.000000E-03 +1.100000E-05 +1.752967E-03 +1.027501E-06 +0.000000E+00 +0.000000E+00 +2.930717E-04 +8.589100E-08 0.000000E+00 0.000000E+00 -9.086007E-04 -4.570977E-07 -2.000000E-03 -2.000000E-06 -3.054379E-04 -9.329231E-08 -1.500000E-02 -5.300000E-05 -7.584986E-03 -1.188822E-05 0.000000E+00 0.000000E+00 -6.128755E-04 -1.878102E-07 -1.000000E-03 -1.000000E-06 -6.108758E-04 -3.731692E-07 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 1.800000E-02 -8.600000E-05 -6.672457E-03 -1.369871E-05 +7.600000E-05 +7.300761E-03 +1.254112E-05 0.000000E+00 0.000000E+00 -6.141488E-04 -1.885896E-07 +5.841969E-04 +1.706438E-07 +0.000000E+00 +0.000000E+00 +5.861433E-04 +3.435640E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.000000E-02 +1.820000E-04 +1.518122E-02 +4.757233E-05 +0.000000E+00 +0.000000E+00 +2.927374E-04 +8.569516E-08 +2.000000E-03 +2.000000E-06 +2.631167E-03 +3.503358E-06 +1.000000E-03 +1.000000E-06 +2.920435E-04 +8.528942E-08 +2.500000E-02 +1.390000E-04 +9.343205E-03 +1.910189E-05 +0.000000E+00 +0.000000E+00 +2.039724E-03 +1.271217E-06 +1.000000E-03 +1.000000E-06 +5.861433E-04 +3.435640E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.400000E-02 +1.340000E-04 +1.080336E-02 +2.533338E-05 +0.000000E+00 +0.000000E+00 +2.914595E-04 +8.494867E-08 0.000000E+00 0.000000E+00 0.000000E+00 @@ -705,46 +145,14 @@ tally 1: 0.000000E+00 0.000000E+00 0.000000E+00 -1.100000E-02 -2.700000E-05 -3.927884E-03 -3.728010E-06 +1.400000E-02 +5.200000E-05 +6.711472E-03 +1.216306E-05 0.000000E+00 0.000000E+00 -3.074376E-04 -9.451786E-08 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -9.000000E-03 -1.900000E-05 -4.217645E-03 -4.306910E-06 -0.000000E+00 -0.000000E+00 -6.128755E-04 -1.878102E-07 +5.847809E-04 +1.709846E-07 0.000000E+00 0.000000E+00 0.000000E+00 @@ -754,37 +162,89 @@ tally 1: 0.000000E+00 0.000000E+00 1.700000E-02 -7.700000E-05 -7.235504E-03 -1.301737E-05 +7.900000E-05 +6.413106E-03 +1.186604E-05 0.000000E+00 0.000000E+00 -3.054379E-04 -9.329231E-08 -0.000000E+00 -0.000000E+00 -1.531736E-03 -8.439742E-07 -3.000000E-03 -3.000000E-06 -3.074376E-04 -9.451786E-08 -9.000000E-03 -1.900000E-05 -3.018298E-03 -2.174671E-06 +5.861433E-04 +3.435640E-07 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -6.141488E-04 -1.885896E-07 +0.000000E+00 +0.000000E+00 +2.900000E-02 +1.770000E-04 +1.314196E-02 +3.678739E-05 +0.000000E+00 +0.000000E+00 +1.167073E-03 +5.110971E-07 +0.000000E+00 +0.000000E+00 +5.861433E-04 +3.435640E-07 1.000000E-03 1.000000E-06 -3.074376E-04 -9.451786E-08 +0.000000E+00 +0.000000E+00 +2.900000E-02 +1.830000E-04 +1.372758E-02 +4.360498E-05 +0.000000E+00 +0.000000E+00 +5.858090E-04 +1.715862E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.200000E-02 +1.000000E-04 +9.631311E-03 +2.001285E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.829191E-04 +3.397947E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +8.000000E-03 +1.800000E-05 +4.094439E-03 +4.622331E-06 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.170949E-03 +1.371123E-06 +1.000000E-03 +1.000000E-06 +0.000000E+00 +0.000000E+00 +9.000000E-03 +5.100000E-05 +4.664405E-03 +1.087264E-05 0.000000E+00 0.000000E+00 0.000000E+00 @@ -797,14 +257,362 @@ tally 1: 0.000000E+00 0.000000E+00 0.000000E+00 +2.200000E-02 +1.300000E-04 +9.644227E-03 +2.300481E-05 +0.000000E+00 +0.000000E+00 +8.771587E-04 +4.270487E-07 +0.000000E+00 +0.000000E+00 +2.927374E-04 +8.569516E-08 +1.000000E-03 +1.000000E-06 +0.000000E+00 +0.000000E+00 +2.500000E-02 +1.630000E-04 +1.257283E-02 +3.946813E-05 +0.000000E+00 +0.000000E+00 +1.171259E-03 +8.583084E-07 +0.000000E+00 +0.000000E+00 +1.462299E-03 +1.112414E-06 +1.000000E-03 +1.000000E-06 +0.000000E+00 +0.000000E+00 +2.400000E-02 +1.400000E-04 +1.138156E-02 +3.565057E-05 +0.000000E+00 +0.000000E+00 +2.930717E-04 +8.589100E-08 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +9.000000E-03 +2.100000E-05 +4.383252E-03 +6.073597E-06 +0.000000E+00 +0.000000E+00 +8.707460E-04 +7.581986E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.100000E-02 +3.500000E-05 +6.410606E-03 +1.066242E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-03 +1.000000E-06 +8.756564E-04 +4.254898E-07 +1.000000E-03 +1.000000E-06 +0.000000E+00 +0.000000E+00 +1.600000E-02 +6.000000E-05 +7.287540E-03 +1.279808E-05 +0.000000E+00 +0.000000E+00 +5.822922E-04 +1.695337E-07 +0.000000E+00 +0.000000E+00 +1.172287E-03 +1.374256E-06 +1.000000E-03 +1.000000E-06 +0.000000E+00 +0.000000E+00 +3.400000E-02 +2.820000E-04 +1.603656E-02 +6.229803E-05 +0.000000E+00 +0.000000E+00 +1.460479E-03 +7.689660E-07 +1.000000E-03 +1.000000E-06 +1.165972E-03 +6.797578E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.900000E-02 +8.500000E-05 +7.880322E-03 +1.387762E-05 +0.000000E+00 +0.000000E+00 +8.755466E-04 +4.261064E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +8.000000E-03 +2.400000E-05 +4.666802E-03 +4.926489E-06 +0.000000E+00 +0.000000E+00 +5.829191E-04 +3.397947E-07 +0.000000E+00 +0.000000E+00 +8.759908E-04 +4.256857E-07 2.000000E-03 2.000000E-06 -1.203204E-03 -7.241295E-07 +0.000000E+00 +0.000000E+00 +1.500000E-02 +5.900000E-05 +8.180347E-03 +1.622409E-05 +0.000000E+00 +0.000000E+00 +2.902487E-04 +8.424429E-08 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.300000E-02 +1.390000E-04 +7.593262E-03 +1.708292E-05 +0.000000E+00 +0.000000E+00 +1.750169E-03 +1.193182E-06 +1.000000E-03 +1.000000E-06 +1.461465E-03 +7.684663E-07 +2.000000E-03 +4.000000E-06 +2.914595E-04 +8.494867E-08 +2.300000E-02 +1.350000E-04 +9.639670E-03 +2.161898E-05 +0.000000E+00 +0.000000E+00 +8.781869E-04 +4.288534E-07 +0.000000E+00 +0.000000E+00 +8.707460E-04 +7.581986E-07 +1.000000E-03 +1.000000E-06 +0.000000E+00 +0.000000E+00 +2.100000E-02 +1.090000E-04 +9.045520E-03 +2.171457E-05 +0.000000E+00 +0.000000E+00 +5.851152E-04 +1.711804E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.800000E-02 +7.000000E-05 +1.167023E-02 +2.840320E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-03 +1.000000E-06 +5.854747E-04 +3.427806E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.000000E-03 +5.000000E-06 +1.171618E-03 +6.863446E-07 +0.000000E+00 +0.000000E+00 +2.930717E-04 +8.589100E-08 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.300000E-02 +4.500000E-05 +6.428153E-03 +9.064469E-06 +0.000000E+00 +0.000000E+00 +2.927374E-04 +8.569516E-08 +1.000000E-03 +1.000000E-06 +2.920435E-04 +8.528942E-08 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.800000E-02 +9.000000E-05 +8.185938E-03 +1.967120E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +9.000000E-03 +2.300000E-05 +4.665341E-03 +5.426897E-06 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.000000E-03 +4.000000E-06 +3.497088E-03 +3.563633E-06 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.000000E-03 +9.000000E-06 +2.333045E-03 +1.870613E-06 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.172287E-03 +1.374256E-06 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +8.000000E-03 +2.200000E-05 +6.413648E-03 +1.136518E-05 +0.000000E+00 +0.000000E+00 +2.927374E-04 +8.569516E-08 +0.000000E+00 +0.000000E+00 +2.930717E-04 +8.589100E-08 +1.000000E-03 +1.000000E-06 +0.000000E+00 +0.000000E+00 +2.000000E-02 +1.200000E-04 +7.007604E-03 +1.379504E-05 +0.000000E+00 +0.000000E+00 +5.851152E-04 +1.711804E-07 +0.000000E+00 +0.000000E+00 +5.861433E-04 +3.435640E-07 +1.000000E-03 +1.000000E-06 +0.000000E+00 +0.000000E+00 +1.700000E-02 +7.900000E-05 +7.281401E-03 +1.600845E-05 0.000000E+00 0.000000E+00 0.000000E+00 @@ -818,9 +626,9 @@ tally 1: 0.000000E+00 0.000000E+00 7.000000E-03 -1.500000E-05 -4.818843E-03 -6.474375E-06 +1.900000E-05 +3.218959E-03 +4.543526E-06 0.000000E+00 0.000000E+00 0.000000E+00 @@ -833,46 +641,10 @@ tally 1: 0.000000E+00 0.000000E+00 0.000000E+00 -2.400000E-02 -1.180000E-04 -1.088639E-02 -2.567229E-05 -0.000000E+00 -0.000000E+00 -3.067112E-04 -9.407179E-08 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.300000E-02 -5.100000E-05 -6.002667E-03 -1.149916E-05 -0.000000E+00 -0.000000E+00 -3.015814E-04 -9.095135E-08 -0.000000E+00 -0.000000E+00 -3.067112E-04 -9.407179E-08 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.948908E-04 -8.696057E-08 -0.000000E+00 -0.000000E+00 -2.948908E-04 -8.696057E-08 +9.000000E-03 +1.900000E-05 +4.380921E-03 +4.182826E-06 0.000000E+00 0.000000E+00 0.000000E+00 @@ -885,6 +657,42 @@ tally 1: 0.000000E+00 0.000000E+00 0.000000E+00 +2.100000E-02 +1.050000E-04 +9.932207E-03 +2.153598E-05 +0.000000E+00 +0.000000E+00 +5.854747E-04 +3.427806E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.800000E-02 +7.400000E-05 +7.301011E-03 +1.358176E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.804973E-04 +3.369772E-07 +1.000000E-03 +1.000000E-06 +0.000000E+00 +0.000000E+00 +2.300000E-02 +1.610000E-04 +9.647933E-03 +2.657983E-05 0.000000E+00 0.000000E+00 0.000000E+00 @@ -897,14 +705,110 @@ tally 1: 0.000000E+00 0.000000E+00 0.000000E+00 -1.300000E-02 -4.700000E-05 -5.486562E-03 -7.965488E-06 +9.000000E-03 +1.900000E-05 +2.915422E-03 +2.203274E-06 +0.000000E+00 +0.000000E+00 +8.775182E-04 +4.280701E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-03 +1.000000E-06 +2.920435E-04 +8.528942E-08 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.000000E-03 +9.000000E-06 +2.044244E-03 +1.796182E-06 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.800000E-02 +9.000000E-05 +6.718632E-03 +1.203946E-05 +0.000000E+00 +0.000000E+00 +2.927374E-04 +8.569516E-08 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.100000E-02 +2.700000E-05 +4.675703E-03 +5.128811E-06 +0.000000E+00 +0.000000E+00 +2.914595E-04 +8.494867E-08 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-03 +1.000000E-06 +2.914595E-04 +8.494867E-08 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-03 +1.000000E-06 +2.920435E-04 +8.528942E-08 +0.000000E+00 +0.000000E+00 0.000000E+00 0.000000E+00 -3.054379E-04 -9.329231E-08 0.000000E+00 0.000000E+00 0.000000E+00 @@ -914,13 +818,9 @@ tally 1: 0.000000E+00 0.000000E+00 8.000000E-03 -1.800000E-05 -3.334317E-03 -3.609712E-06 -0.000000E+00 -0.000000E+00 -2.948908E-04 -8.696057E-08 +2.600000E-05 +4.670349E-03 +6.133014E-06 0.000000E+00 0.000000E+00 0.000000E+00 @@ -929,20 +829,120 @@ tally 1: 0.000000E+00 0.000000E+00 0.000000E+00 -2.000000E-02 -1.080000E-04 -9.741552E-03 -2.552400E-05 0.000000E+00 0.000000E+00 -3.015814E-04 -9.095135E-08 0.000000E+00 0.000000E+00 -1.800986E-03 -1.622276E-06 -1.000000E-03 -1.000000E-06 +1.500000E-02 +6.100000E-05 +8.751735E-03 +1.922421E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.400000E-02 +5.000000E-05 +6.126605E-03 +9.620966E-06 +0.000000E+00 +0.000000E+00 +5.835031E-04 +1.702381E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-03 +2.000000E-06 +5.847809E-04 +1.709846E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-03 +2.000000E-06 +8.719569E-04 +4.219258E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +9.000000E-03 +2.700000E-05 +4.085939E-03 +4.936434E-06 +0.000000E+00 +0.000000E+00 +2.930717E-04 +8.589100E-08 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +9.000000E-03 +2.500000E-05 +5.846700E-03 +9.739696E-06 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.300000E-02 +3.900000E-05 +6.128455E-03 +1.013669E-05 +0.000000E+00 +0.000000E+00 +5.851152E-04 +1.711804E-07 +0.000000E+00 +0.000000E+00 +1.172287E-03 +1.374256E-06 +0.000000E+00 +0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -962,11 +962,11 @@ tally 1: 0.000000E+00 0.000000E+00 tally 2: -5.818100E-01 -6.772558E-02 -6.344749E-01 -8.054355E-02 -3.690180E+00 -2.724752E+00 -4.102635E+01 -3.367908E+02 +5.554367E-01 +6.170503E-02 +6.055520E-01 +7.334109E-02 +3.526894E+00 +2.488065E+00 +3.925114E+01 +3.081807E+02 diff --git a/tests/regression_tests/statepoint_batch/__init__.py b/tests/regression_tests/statepoint_batch/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/regression_tests/statepoint_batch/geometry.xml b/tests/regression_tests/statepoint_batch/geometry.xml deleted file mode 100644 index bc56030e1..000000000 --- a/tests/regression_tests/statepoint_batch/geometry.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/tests/regression_tests/statepoint_batch/materials.xml b/tests/regression_tests/statepoint_batch/materials.xml deleted file mode 100644 index 2472a7471..000000000 --- a/tests/regression_tests/statepoint_batch/materials.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/tests/regression_tests/statepoint_batch/results_true.dat b/tests/regression_tests/statepoint_batch/results_true.dat deleted file mode 100644 index f5c855d77..000000000 --- a/tests/regression_tests/statepoint_batch/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -3.048864E-01 1.689118E-03 diff --git a/tests/regression_tests/statepoint_batch/settings.xml b/tests/regression_tests/statepoint_batch/settings.xml deleted file mode 100644 index e2f8dad47..000000000 --- a/tests/regression_tests/statepoint_batch/settings.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - eigenvalue - 10 - 5 - 1000 - - - - -4 -4 -4 4 4 4 - - - - diff --git a/tests/regression_tests/statepoint_batch/test.py b/tests/regression_tests/statepoint_batch/test.py deleted file mode 100644 index 323b28fc6..000000000 --- a/tests/regression_tests/statepoint_batch/test.py +++ /dev/null @@ -1,18 +0,0 @@ -from tests.testing_harness import TestHarness - - -class StatepointTestHarness(TestHarness): - def __init__(self): - super().__init__(None) - - def _test_output_created(self): - """Make sure statepoint files have been created.""" - sps = ('statepoint.03.h5', 'statepoint.06.h5', 'statepoint.09.h5') - for sp in sps: - self._sp_name = sp - TestHarness._test_output_created(self) - - -def test_statepoint_batch(): - harness = StatepointTestHarness() - harness.main() diff --git a/tests/regression_tests/statepoint_restart/results_true.dat b/tests/regression_tests/statepoint_restart/results_true.dat index aebc971f4..b919b3000 100644 --- a/tests/regression_tests/statepoint_restart/results_true.dat +++ b/tests/regression_tests/statepoint_restart/results_true.dat @@ -1,66 +1,18 @@ k-combined: -3.070134E-01 3.900396E-03 +2.940336E-01 7.338463E-04 tally 1: 1.300000E-02 -4.700000E-05 +3.900000E-05 1.300000E-02 -4.700000E-05 -5.741381E-03 -9.030024E-06 +3.900000E-05 +5.833114E-03 +7.476880E-06 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -3.067112E-04 -9.407179E-08 -1.000000E-03 -1.000000E-06 -1.000000E-03 -1.000000E-06 -3.015814E-04 -9.095135E-08 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.600000E-02 -1.420000E-04 -2.600000E-02 -1.420000E-04 -1.268989E-02 -3.464490E-05 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.948908E-04 -8.696057E-08 -1.000000E-03 -1.000000E-06 -1.000000E-03 -1.000000E-06 -1.535009E-03 -1.224718E-06 -1.000000E-03 -1.000000E-06 -1.000000E-03 -1.000000E-06 -0.000000E+00 -0.000000E+00 -3.200000E-02 -2.760000E-04 -3.200000E-02 -2.760000E-04 -1.248507E-02 -3.714039E-05 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -9.208601E-04 -4.708050E-07 +1.164000E-03 +5.072152E-07 0.000000E+00 0.000000E+00 0.000000E+00 @@ -73,900 +25,36 @@ tally 1: 0.000000E+00 0.000000E+00 0.000000E+00 -1.900000E-02 -7.500000E-05 -1.900000E-02 -7.500000E-05 -8.218811E-03 -1.462963E-05 +2.100000E-02 +1.110000E-04 +2.100000E-02 +1.110000E-04 +1.108363E-02 +2.889480E-05 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -9.070399E-04 -2.743247E-07 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -9.047443E-04 -8.185622E-07 -1.000000E-03 -1.000000E-06 -1.000000E-03 -1.000000E-06 -0.000000E+00 -0.000000E+00 -1.200000E-02 -5.000000E-05 -1.200000E-02 -5.000000E-05 -3.938144E-03 -5.245641E-06 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -3.067112E-04 -9.407179E-08 -1.000000E-03 -1.000000E-06 +5.861433E-04 +3.435640E-07 2.000000E-03 -4.000000E-06 -5.897816E-04 -3.478423E-07 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.948908E-04 -8.696057E-08 -2.000000E-02 -1.260000E-04 -2.000000E-02 -1.260000E-04 -9.079852E-03 -2.657486E-05 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -3.100000E-02 -2.010000E-04 -3.100000E-02 -2.010000E-04 -1.336346E-02 -3.645780E-05 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.102710E-03 -1.524522E-06 -1.000000E-03 -1.000000E-06 -1.000000E-03 -1.000000E-06 -9.083133E-04 -4.632477E-07 -1.000000E-03 -1.000000E-06 -1.000000E-03 -1.000000E-06 -0.000000E+00 -0.000000E+00 -2.300000E-02 -1.090000E-04 -2.300000E-02 -1.090000E-04 -1.334578E-02 -3.590708E-05 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -6.031628E-04 -3.638054E-07 -1.000000E-03 -1.000000E-06 -1.000000E-03 -1.000000E-06 -1.530605E-03 -1.035684E-06 +2.000000E-06 +3.000000E-03 +5.000000E-06 +2.043490E-03 +1.793389E-06 2.000000E-03 2.000000E-06 2.000000E-03 2.000000E-06 -6.082927E-04 -1.850231E-07 -1.400000E-02 -4.200000E-05 -1.400000E-02 -4.200000E-05 -7.257468E-03 -1.221578E-05 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.948908E-04 -8.696057E-08 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -8.980536E-04 -4.507660E-07 -2.000000E-03 -4.000000E-06 -2.000000E-03 -4.000000E-06 -0.000000E+00 -0.000000E+00 -9.000000E-03 -2.700000E-05 -9.000000E-03 -2.700000E-05 -5.193169E-03 -1.187321E-05 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -3.054379E-04 -9.329231E-08 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.000000E-02 -2.400000E-05 -1.000000E-02 -2.400000E-05 -5.170002E-03 -7.361546E-06 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -6.003287E-04 -1.802529E-07 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.800000E-02 -1.720000E-04 -2.800000E-02 -1.720000E-04 -1.699195E-02 -6.267008E-05 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.800000E-02 -1.980000E-04 -2.800000E-02 -1.980000E-04 -1.338750E-02 -4.565234E-05 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -9.183134E-04 -4.676871E-07 -1.000000E-03 -1.000000E-06 -1.000000E-03 -1.000000E-06 -1.537188E-03 -2.362946E-06 -2.000000E-03 -4.000000E-06 -2.000000E-03 -4.000000E-06 -0.000000E+00 -0.000000E+00 -2.900000E-02 -1.950000E-04 -2.900000E-02 -1.950000E-04 -1.178615E-02 -3.130999E-05 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -3.015814E-04 -9.095135E-08 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.179563E-03 -1.391369E-06 -1.000000E-03 -1.000000E-06 -1.000000E-03 -1.000000E-06 -0.000000E+00 -0.000000E+00 -1.500000E-02 -6.500000E-05 -1.500000E-02 -6.500000E-05 -5.706064E-03 -9.729293E-06 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.948908E-04 -8.696057E-08 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -3.000000E-03 -9.000000E-06 -3.000000E-03 -9.000000E-06 -1.516642E-03 -1.579637E-06 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -5.897816E-04 -3.478423E-07 -1.000000E-03 -1.000000E-06 -1.000000E-03 -1.000000E-06 -0.000000E+00 -0.000000E+00 -2.900000E-02 -1.970000E-04 -2.900000E-02 -1.970000E-04 -1.274851E-02 -3.511219E-05 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -9.057666E-04 -4.601298E-07 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.800000E-02 -7.600000E-05 -1.800000E-02 -7.600000E-05 -8.803642E-03 -1.730938E-05 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -3.074376E-04 -9.451786E-08 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.900000E-02 -1.830000E-04 -2.900000E-02 -1.830000E-04 -1.245316E-02 -3.378176E-05 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -6.016020E-04 -1.810324E-07 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.000000E-03 -1.000000E-06 -1.000000E-03 -1.000000E-06 -6.090190E-04 -1.854692E-07 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -6.031628E-04 -3.638054E-07 -1.000000E-03 -1.000000E-06 -1.000000E-03 -1.000000E-06 -0.000000E+00 -0.000000E+00 -1.900000E-02 -8.700000E-05 -1.900000E-02 -8.700000E-05 -8.781239E-03 -1.676750E-05 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -6.128755E-04 -1.878102E-07 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -3.500000E-02 -2.930000E-04 -3.500000E-02 -2.930000E-04 -1.372494E-02 -4.750881E-05 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -9.090396E-04 -2.755502E-07 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -5.897816E-04 -3.478423E-07 -1.000000E-03 -1.000000E-06 -1.000000E-03 -1.000000E-06 -0.000000E+00 -0.000000E+00 -3.700000E-02 -3.270000E-04 -3.700000E-02 -3.270000E-04 -1.492073E-02 -5.134793E-05 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -6.134225E-04 -3.762872E-07 -1.000000E-03 -1.000000E-06 -1.000000E-03 -1.000000E-06 -3.074376E-04 -9.451786E-08 -1.000000E-03 -1.000000E-06 -1.000000E-03 -1.000000E-06 -6.148751E-04 -3.780714E-07 -3.500000E-02 -2.810000E-04 -3.500000E-02 -2.810000E-04 -1.728232E-02 -6.621096E-05 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.214477E-03 -3.688425E-07 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.481145E-03 -1.482321E-06 -1.000000E-03 -1.000000E-06 -1.000000E-03 -1.000000E-06 -3.015814E-04 -9.095135E-08 -8.000000E-03 -1.800000E-05 -8.000000E-03 -1.800000E-05 -3.359923E-03 -3.081200E-06 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -3.067112E-04 -9.407179E-08 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -7.000000E-03 -1.500000E-05 -7.000000E-03 -1.500000E-05 -3.660087E-03 -3.561493E-06 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -9.203130E-04 -4.713637E-07 -1.000000E-03 -1.000000E-06 -1.000000E-03 -1.000000E-06 -3.015814E-04 -9.095135E-08 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -3.500000E-02 -2.630000E-04 -3.500000E-02 -2.630000E-04 -1.425810E-02 -4.231681E-05 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.516059E-03 -4.597939E-07 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.221752E-03 -1.492677E-06 -1.000000E-03 -1.000000E-06 -1.000000E-03 -1.000000E-06 -0.000000E+00 -0.000000E+00 -2.300000E-02 -1.110000E-04 -2.300000E-02 -1.110000E-04 -1.184871E-02 -2.918189E-05 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -6.016020E-04 -1.810324E-07 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.600000E-02 -6.600000E-05 -1.600000E-02 -6.600000E-05 -7.879913E-03 -1.566560E-05 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -9.039098E-04 -2.724298E-07 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -3.067112E-04 -9.407179E-08 -1.000000E-03 -1.000000E-06 -1.000000E-03 -1.000000E-06 -0.000000E+00 -0.000000E+00 -9.000000E-03 -1.900000E-05 -9.000000E-03 -1.900000E-05 -4.256120E-03 -4.431220E-06 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -6.000000E-03 -1.200000E-05 -6.000000E-03 -1.200000E-05 -2.739774E-03 -2.333986E-06 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.400000E-02 -6.800000E-05 -1.400000E-02 -6.800000E-05 -7.182805E-03 -1.822340E-05 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.500000E-02 -6.300000E-05 -1.500000E-02 -6.300000E-05 -5.734575E-03 -9.207711E-06 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -6.070193E-04 -1.842437E-07 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -8.846723E-04 -7.826452E-07 -1.000000E-03 -1.000000E-06 -1.000000E-03 -1.000000E-06 -0.000000E+00 -0.000000E+00 +2.930717E-04 +8.589100E-08 2.000000E-02 -8.800000E-05 +9.000000E-05 2.000000E-02 -8.800000E-05 -9.416733E-03 -2.020354E-05 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -6.003287E-04 -1.802529E-07 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -6.031628E-04 -3.638054E-07 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -3.015814E-04 -9.095135E-08 -7.000000E-03 -1.500000E-05 -7.000000E-03 -1.500000E-05 -2.413278E-03 -1.439032E-06 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.948908E-04 -8.696057E-08 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -7.000000E-03 -1.500000E-05 -7.000000E-03 -1.500000E-05 -4.226919E-03 -4.173413E-06 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.300000E-02 -1.590000E-04 -2.300000E-02 -1.590000E-04 -1.125341E-02 -3.890966E-05 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -5.964722E-04 -1.779119E-07 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -8.846723E-04 -7.826452E-07 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -3.200000E-02 -2.260000E-04 -3.200000E-02 -2.260000E-04 -1.458976E-02 -4.416780E-05 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.814806E-03 -9.096012E-07 -1.000000E-03 -1.000000E-06 -1.000000E-03 -1.000000E-06 -6.031628E-04 -3.638054E-07 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -3.100000E-02 -2.050000E-04 -3.100000E-02 -2.050000E-04 -1.241089E-02 -3.545088E-05 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.515620E-03 -1.191731E-06 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -6.121491E-04 -1.873641E-07 -1.000000E-03 -1.000000E-06 -1.000000E-03 -1.000000E-06 -0.000000E+00 -0.000000E+00 -1.000000E-02 -2.600000E-05 -1.000000E-02 -2.600000E-05 -3.936862E-03 -3.749154E-06 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.948908E-04 -8.696057E-08 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.100000E-02 -4.700000E-05 -1.100000E-02 -4.700000E-05 -5.084641E-03 -9.248485E-06 +9.000000E-05 +9.334862E-03 +1.853104E-05 0.000000E+00 0.000000E+00 0.000000E+00 @@ -986,65 +74,401 @@ tally 1: 0.000000E+00 0.000000E+00 2.500000E-02 -1.770000E-04 +1.430000E-04 2.500000E-02 +1.430000E-04 +1.110224E-02 +2.872840E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.833203E-04 +1.701353E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.000000E-03 +1.100000E-05 +5.000000E-03 +1.100000E-05 +1.752967E-03 +1.027501E-06 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.930717E-04 +8.589100E-08 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.800000E-02 +7.600000E-05 +1.800000E-02 +7.600000E-05 +7.300761E-03 +1.254112E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.841969E-04 +1.706438E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.861433E-04 +3.435640E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.000000E-02 +1.820000E-04 +3.000000E-02 +1.820000E-04 +1.518122E-02 +4.757233E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.927374E-04 +8.569516E-08 +2.000000E-03 +2.000000E-06 +2.000000E-03 +2.000000E-06 +2.631167E-03 +3.503358E-06 +1.000000E-03 +1.000000E-06 +1.000000E-03 +1.000000E-06 +2.920435E-04 +8.528942E-08 +2.500000E-02 +1.390000E-04 +2.500000E-02 +1.390000E-04 +9.343205E-03 +1.910189E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.039724E-03 +1.271217E-06 +1.000000E-03 +1.000000E-06 +1.000000E-03 +1.000000E-06 +5.861433E-04 +3.435640E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.400000E-02 +1.340000E-04 +2.400000E-02 +1.340000E-04 +1.080336E-02 +2.533338E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.914595E-04 +8.494867E-08 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.400000E-02 +5.200000E-05 +1.400000E-02 +5.200000E-05 +6.711472E-03 +1.216306E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.847809E-04 +1.709846E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.700000E-02 +7.900000E-05 +1.700000E-02 +7.900000E-05 +6.413106E-03 +1.186604E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.861433E-04 +3.435640E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.900000E-02 1.770000E-04 -1.021860E-02 -3.099521E-05 +2.900000E-02 +1.770000E-04 +1.314196E-02 +3.678739E-05 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -3.074376E-04 -9.451786E-08 +1.167073E-03 +5.110971E-07 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -9.086007E-04 -4.570977E-07 -2.000000E-03 -2.000000E-06 -2.000000E-03 -2.000000E-06 -3.054379E-04 -9.329231E-08 -1.500000E-02 -5.300000E-05 -1.500000E-02 -5.300000E-05 -7.584986E-03 -1.188822E-05 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -6.128755E-04 -1.878102E-07 +5.861433E-04 +3.435640E-07 1.000000E-03 1.000000E-06 1.000000E-03 1.000000E-06 -6.108758E-04 -3.731692E-07 +0.000000E+00 +0.000000E+00 +2.900000E-02 +1.830000E-04 +2.900000E-02 +1.830000E-04 +1.372758E-02 +4.360498E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.858090E-04 +1.715862E-07 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.800000E-02 -8.600000E-05 -1.800000E-02 -8.600000E-05 -6.672457E-03 -1.369871E-05 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -6.141488E-04 -1.885896E-07 +0.000000E+00 +0.000000E+00 +2.200000E-02 +1.000000E-04 +2.200000E-02 +1.000000E-04 +9.631311E-03 +2.001285E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.829191E-04 +3.397947E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +8.000000E-03 +1.800000E-05 +8.000000E-03 +1.800000E-05 +4.094439E-03 +4.622331E-06 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.170949E-03 +1.371123E-06 +1.000000E-03 +1.000000E-06 +1.000000E-03 +1.000000E-06 +0.000000E+00 +0.000000E+00 +9.000000E-03 +5.100000E-05 +9.000000E-03 +5.100000E-05 +4.664405E-03 +1.087264E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.200000E-02 +1.300000E-04 +2.200000E-02 +1.300000E-04 +9.644227E-03 +2.300481E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +8.771587E-04 +4.270487E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.927374E-04 +8.569516E-08 +1.000000E-03 +1.000000E-06 +1.000000E-03 +1.000000E-06 +0.000000E+00 +0.000000E+00 +2.500000E-02 +1.630000E-04 +2.500000E-02 +1.630000E-04 +1.257283E-02 +3.946813E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.171259E-03 +8.583084E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.462299E-03 +1.112414E-06 +1.000000E-03 +1.000000E-06 +1.000000E-03 +1.000000E-06 +0.000000E+00 +0.000000E+00 +2.400000E-02 +1.400000E-04 +2.400000E-02 +1.400000E-04 +1.138156E-02 +3.565057E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.930717E-04 +8.589100E-08 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +9.000000E-03 +2.100000E-05 +9.000000E-03 +2.100000E-05 +4.383252E-03 +6.073597E-06 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +8.707460E-04 +7.581986E-07 0.000000E+00 0.000000E+00 0.000000E+00 @@ -1058,129 +482,89 @@ tally 1: 0.000000E+00 0.000000E+00 1.100000E-02 -2.700000E-05 +3.500000E-05 1.100000E-02 -2.700000E-05 -3.927884E-03 -3.728010E-06 +3.500000E-05 +6.410606E-03 +1.066242E-05 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -3.074376E-04 -9.451786E-08 0.000000E+00 0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -9.000000E-03 -1.900000E-05 -9.000000E-03 -1.900000E-05 -4.217645E-03 -4.306910E-06 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -6.128755E-04 -1.878102E-07 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.700000E-02 -7.700000E-05 -1.700000E-02 -7.700000E-05 -7.235504E-03 -1.301737E-05 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -3.054379E-04 -9.329231E-08 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.531736E-03 -8.439742E-07 -3.000000E-03 -3.000000E-06 -3.000000E-03 -3.000000E-06 -3.074376E-04 -9.451786E-08 -9.000000E-03 -1.900000E-05 -9.000000E-03 -1.900000E-05 -3.018298E-03 -2.174671E-06 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -6.141488E-04 -1.885896E-07 1.000000E-03 1.000000E-06 1.000000E-03 1.000000E-06 -3.074376E-04 -9.451786E-08 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 +8.756564E-04 +4.254898E-07 +1.000000E-03 +1.000000E-06 +1.000000E-03 +1.000000E-06 +0.000000E+00 +0.000000E+00 +1.600000E-02 +6.000000E-05 +1.600000E-02 +6.000000E-05 +7.287540E-03 +1.279808E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.822922E-04 +1.695337E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.172287E-03 +1.374256E-06 +1.000000E-03 +1.000000E-06 +1.000000E-03 +1.000000E-06 +0.000000E+00 +0.000000E+00 +3.400000E-02 +2.820000E-04 +3.400000E-02 +2.820000E-04 +1.603656E-02 +6.229803E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.460479E-03 +7.689660E-07 +1.000000E-03 +1.000000E-06 +1.000000E-03 +1.000000E-06 +1.165972E-03 +6.797578E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.900000E-02 +8.500000E-05 +1.900000E-02 +8.500000E-05 +7.880322E-03 +1.387762E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +8.755466E-04 +4.261064E-07 0.000000E+00 0.000000E+00 0.000000E+00 @@ -1193,20 +577,348 @@ tally 1: 0.000000E+00 0.000000E+00 0.000000E+00 +8.000000E-03 +2.400000E-05 +8.000000E-03 +2.400000E-05 +4.666802E-03 +4.926489E-06 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 +5.829191E-04 +3.397947E-07 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 +8.759908E-04 +4.256857E-07 2.000000E-03 2.000000E-06 2.000000E-03 2.000000E-06 -1.203204E-03 -7.241295E-07 +0.000000E+00 +0.000000E+00 +1.500000E-02 +5.900000E-05 +1.500000E-02 +5.900000E-05 +8.180347E-03 +1.622409E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.902487E-04 +8.424429E-08 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.300000E-02 +1.390000E-04 +2.300000E-02 +1.390000E-04 +7.593262E-03 +1.708292E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.750169E-03 +1.193182E-06 +1.000000E-03 +1.000000E-06 +1.000000E-03 +1.000000E-06 +1.461465E-03 +7.684663E-07 +2.000000E-03 +4.000000E-06 +2.000000E-03 +4.000000E-06 +2.914595E-04 +8.494867E-08 +2.300000E-02 +1.350000E-04 +2.300000E-02 +1.350000E-04 +9.639670E-03 +2.161898E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +8.781869E-04 +4.288534E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +8.707460E-04 +7.581986E-07 +1.000000E-03 +1.000000E-06 +1.000000E-03 +1.000000E-06 +0.000000E+00 +0.000000E+00 +2.100000E-02 +1.090000E-04 +2.100000E-02 +1.090000E-04 +9.045520E-03 +2.171457E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.851152E-04 +1.711804E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.800000E-02 +7.000000E-05 +1.800000E-02 +7.000000E-05 +1.167023E-02 +2.840320E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-03 +1.000000E-06 +1.000000E-03 +1.000000E-06 +5.854747E-04 +3.427806E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.000000E-03 +5.000000E-06 +3.000000E-03 +5.000000E-06 +1.171618E-03 +6.863446E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.930717E-04 +8.589100E-08 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.300000E-02 +4.500000E-05 +1.300000E-02 +4.500000E-05 +6.428153E-03 +9.064469E-06 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.927374E-04 +8.569516E-08 +1.000000E-03 +1.000000E-06 +1.000000E-03 +1.000000E-06 +2.920435E-04 +8.528942E-08 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.800000E-02 +9.000000E-05 +1.800000E-02 +9.000000E-05 +8.185938E-03 +1.967120E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +9.000000E-03 +2.300000E-05 +9.000000E-03 +2.300000E-05 +4.665341E-03 +5.426897E-06 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.000000E-03 +4.000000E-06 +4.000000E-03 +4.000000E-06 +3.497088E-03 +3.563633E-06 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.000000E-03 +9.000000E-06 +5.000000E-03 +9.000000E-06 +2.333045E-03 +1.870613E-06 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.172287E-03 +1.374256E-06 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +8.000000E-03 +2.200000E-05 +8.000000E-03 +2.200000E-05 +6.413648E-03 +1.136518E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.927374E-04 +8.569516E-08 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.930717E-04 +8.589100E-08 +1.000000E-03 +1.000000E-06 +1.000000E-03 +1.000000E-06 +0.000000E+00 +0.000000E+00 +2.000000E-02 +1.200000E-04 +2.000000E-02 +1.200000E-04 +7.007604E-03 +1.379504E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.851152E-04 +1.711804E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.861433E-04 +3.435640E-07 +1.000000E-03 +1.000000E-06 +1.000000E-03 +1.000000E-06 +0.000000E+00 +0.000000E+00 +1.700000E-02 +7.900000E-05 +1.700000E-02 +7.900000E-05 +7.281401E-03 +1.600845E-05 0.000000E+00 0.000000E+00 0.000000E+00 @@ -1226,11 +938,11 @@ tally 1: 0.000000E+00 0.000000E+00 7.000000E-03 -1.500000E-05 +1.900000E-05 7.000000E-03 -1.500000E-05 -4.818843E-03 -6.474375E-06 +1.900000E-05 +3.218959E-03 +4.543526E-06 0.000000E+00 0.000000E+00 0.000000E+00 @@ -1249,18 +961,16 @@ tally 1: 0.000000E+00 0.000000E+00 0.000000E+00 -2.400000E-02 -1.180000E-04 -2.400000E-02 -1.180000E-04 -1.088639E-02 -2.567229E-05 +9.000000E-03 +1.900000E-05 +9.000000E-03 +1.900000E-05 +4.380921E-03 +4.182826E-06 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -3.067112E-04 -9.407179E-08 0.000000E+00 0.000000E+00 0.000000E+00 @@ -1273,24 +983,22 @@ tally 1: 0.000000E+00 0.000000E+00 0.000000E+00 -1.300000E-02 -5.100000E-05 -1.300000E-02 -5.100000E-05 -6.002667E-03 -1.149916E-05 0.000000E+00 0.000000E+00 +2.100000E-02 +1.050000E-04 +2.100000E-02 +1.050000E-04 +9.932207E-03 +2.153598E-05 0.000000E+00 0.000000E+00 -3.015814E-04 -9.095135E-08 0.000000E+00 0.000000E+00 +5.854747E-04 +3.427806E-07 0.000000E+00 0.000000E+00 -3.067112E-04 -9.407179E-08 0.000000E+00 0.000000E+00 0.000000E+00 @@ -1301,22 +1009,36 @@ tally 1: 0.000000E+00 0.000000E+00 0.000000E+00 -2.948908E-04 -8.696057E-08 +1.800000E-02 +7.400000E-05 +1.800000E-02 +7.400000E-05 +7.301011E-03 +1.358176E-05 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.948908E-04 -8.696057E-08 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 +5.804973E-04 +3.369772E-07 +1.000000E-03 +1.000000E-06 +1.000000E-03 +1.000000E-06 0.000000E+00 0.000000E+00 +2.300000E-02 +1.610000E-04 +2.300000E-02 +1.610000E-04 +9.647933E-03 +2.657983E-05 0.000000E+00 0.000000E+00 0.000000E+00 @@ -1335,28 +1057,162 @@ tally 1: 0.000000E+00 0.000000E+00 0.000000E+00 +9.000000E-03 +1.900000E-05 +9.000000E-03 +1.900000E-05 +2.915422E-03 +2.203274E-06 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 +8.775182E-04 +4.280701E-07 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.300000E-02 -4.700000E-05 -1.300000E-02 -4.700000E-05 -5.486562E-03 -7.965488E-06 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -3.054379E-04 -9.329231E-08 +0.000000E+00 +0.000000E+00 +1.000000E-03 +1.000000E-06 +1.000000E-03 +1.000000E-06 +2.920435E-04 +8.528942E-08 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.000000E-03 +9.000000E-06 +5.000000E-03 +9.000000E-06 +2.044244E-03 +1.796182E-06 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.800000E-02 +9.000000E-05 +1.800000E-02 +9.000000E-05 +6.718632E-03 +1.203946E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.927374E-04 +8.569516E-08 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.100000E-02 +2.700000E-05 +1.100000E-02 +2.700000E-05 +4.675703E-03 +5.128811E-06 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.914595E-04 +8.494867E-08 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-03 +1.000000E-06 +1.000000E-03 +1.000000E-06 +2.914595E-04 +8.494867E-08 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-03 +1.000000E-06 +1.000000E-03 +1.000000E-06 +2.920435E-04 +8.528942E-08 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 @@ -1370,17 +1226,11 @@ tally 1: 0.000000E+00 0.000000E+00 8.000000E-03 -1.800000E-05 +2.600000E-05 8.000000E-03 -1.800000E-05 -3.334317E-03 -3.609712E-06 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.948908E-04 -8.696057E-08 +2.600000E-05 +4.670349E-03 +6.133014E-06 0.000000E+00 0.000000E+00 0.000000E+00 @@ -1393,28 +1243,178 @@ tally 1: 0.000000E+00 0.000000E+00 0.000000E+00 -2.000000E-02 -1.080000E-04 -2.000000E-02 -1.080000E-04 -9.741552E-03 -2.552400E-05 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -3.015814E-04 -9.095135E-08 +0.000000E+00 +0.000000E+00 +1.500000E-02 +6.100000E-05 +1.500000E-02 +6.100000E-05 +8.751735E-03 +1.922421E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.400000E-02 +5.000000E-05 +1.400000E-02 +5.000000E-05 +6.126605E-03 +9.620966E-06 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.835031E-04 +1.702381E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-03 +2.000000E-06 +2.000000E-03 +2.000000E-06 +5.847809E-04 +1.709846E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-03 +2.000000E-06 +2.000000E-03 +2.000000E-06 +8.719569E-04 +4.219258E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +9.000000E-03 +2.700000E-05 +9.000000E-03 +2.700000E-05 +4.085939E-03 +4.936434E-06 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.930717E-04 +8.589100E-08 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +9.000000E-03 +2.500000E-05 +9.000000E-03 +2.500000E-05 +5.846700E-03 +9.739696E-06 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.300000E-02 +3.900000E-05 +1.300000E-02 +3.900000E-05 +6.128455E-03 +1.013669E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.851152E-04 +1.711804E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.172287E-03 +1.374256E-06 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.800986E-03 -1.622276E-06 -1.000000E-03 -1.000000E-06 -1.000000E-03 -1.000000E-06 0.000000E+00 0.000000E+00 0.000000E+00 @@ -1442,11 +1442,11 @@ tally 1: 0.000000E+00 0.000000E+00 tally 2: -5.818100E-01 -6.772558E-02 -6.344749E-01 -8.054355E-02 -3.690180E+00 -2.724752E+00 -4.102635E+01 -3.367908E+02 +5.554367E-01 +6.170503E-02 +6.055520E-01 +7.334109E-02 +3.526894E+00 +2.488065E+00 +3.925114E+01 +3.081807E+02 diff --git a/tests/regression_tests/statepoint_sourcesep/results_true.dat b/tests/regression_tests/statepoint_sourcesep/results_true.dat index bbf03de94..97b997ae6 100644 --- a/tests/regression_tests/statepoint_sourcesep/results_true.dat +++ b/tests/regression_tests/statepoint_sourcesep/results_true.dat @@ -1,2 +1,2 @@ k-combined: -3.070134E-01 3.900396E-03 +2.940336E-01 7.338463E-04 diff --git a/tests/regression_tests/stride/results_true.dat b/tests/regression_tests/stride/results_true.dat index a65411150..825de3766 100644 --- a/tests/regression_tests/stride/results_true.dat +++ b/tests/regression_tests/stride/results_true.dat @@ -1,2 +1,2 @@ k-combined: -2.978080E-01 6.106774E-03 +2.953207E-01 2.874356E-03 diff --git a/tests/regression_tests/surface_source/surface_source_true.h5 b/tests/regression_tests/surface_source/surface_source_true.h5 index d056d7328405bedf704f8690fd3b8056ec8aa101..2c2a19038fc1b6d9c7785e8a7f591e9e5c7fd959 100644 GIT binary patch delta 17120 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 Tbzz-5f4SO>oo}#+&4TX#jWsSQ delta 17152 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 diff --git a/tests/regression_tests/surface_source/surface_source_true.mcpl b/tests/regression_tests/surface_source/surface_source_true.mcpl index 7c819ac9bd02179ed93a1cd94b6fd8237a7b0893..ca4b7c2c4257c88baff2f9bd1c2a756fdfde92bf 100644 GIT binary patch delta 3894 zcmZYAF>tbX9l&wlQkyDTth9|vl`6!jspWl375k#1qVg(lrETs^l`2-Od0X02<*lir z#)^uX-r;-CkZJ~o3>h-yybKH(7&2sFV93CbfgwYBmw_Pz-ethgxthE0eg1II|Bm19 z8FKc|yWaWNUH3ngfBNRttLdBXhTikPzbkj{k6(G<&b0?0f8xnUebc!8!tLvgH(tG7 zfBnVRUaeMMd;Qw=NB;ls+EssdYRNl=}AUA1+gp|YW5Ft3&> z8fZt+yOY6$WoERkb)D-->Gvjsh?-i_p}Gp+lMK$d%#^mYrv5$u2pV(ApriwBDs*o$ z7<2L78$sY)*NJ*cye}EdSYtu^>gZA<<=>wS;@Z%$RyEb0a^YkUQ(ILHwX69Ln42wT zHBd#-51N@}PPomC&b6-eeP(8phl(g1F*ldl#9rifW$hn|+)GoNeOCB??*aLRJD)+d{DKD7Q{Dbzv3Wppp ztBG35JY)ylW>n`&&)dmEH-aXQd_H7E;g6V|%iL1e`IKi|`l$Jx8(iZ7`wV@|?9N4& zxT(N-%o8pu5i>Un>~WtnUb3eA!*&u|2;P1;5sZA^^Es}mR+am>eXz}#E>u~=&lWE@;Jl)ru@jcL z!);D@&WzHZH9wm?;vpjnf6n||W|vz$<0(^GS~5QyJW$^`^r#&;msE7#WWW=RofDt8 z1Lqp++-HxMoH75H9XM~W!()yZOWBKam8;z49xqZ0!G9i01lg#oi`b9%o8TQ zXnq!0XHOmHOU;~X%xBE6edlA282gg>ovU1Bn|qvU=$upbNqb?77aVY2(d%YshC5v6 zghwnZlC=}Au&Z1UNENYH|&NjwmIOC(Yzfvms#ODcX-4J)8Dig z=M`?V%X6MFqSUv{&ov&h&#>mcZGM)x#Z8`a%q1-r%+ERx*kedD6;-%les*}m5fh4k z$8NaFeQxlQ$IL7CT{~fwQ|>aS3$>Lknwu@oYeyALG*ITIooHQU9cf#Ur_9Y2rqxwb zXF62sd*;@fmetpm=1S&fiA!pfZUkcTBsaKjyiG;P!KgpyYg@O@)5orLfLL^;FbMfeJtL zW7uIrBh?fy`!QVQC8x}5_jxac7u;j^M_vwFjQMWB9HT$>l2~S&M;tOz@nSr)!V0_G z;Tb1P{lrW1>>AhE=QihF@Pb%k+OwN%a?C?6{?v{7B6K*i`NAq)? zG53yGWu#@7T;T<~%(`>N7E^z+ORh2MPM>AY{n;*A;yE{&c4y2c7yn{*)){uE$7RmA z#X{Tc>~P7Q5gUyE)%-ee4taLZxyUI4=Kf}OwwZ8e$QtMWZhp3S$pQ23L^sUO43Al6 f%$;>sxy@Z3@q*{fx*K`hE}7;IS2$tw?N|N_q0^+p delta 3830 zcmXBSF>tbX9l&whVnrJ*ucZ|&w$wbasMu1AN)?q_RIJp}9;Z=bMMX<1RaB~}VvQPY zREE4f8KRegAwz}?@yw7RLxv0)GGxdQ?=moOCj&zU1_lN{cfNPu`~080&p*fSN4EcY z=YRik=dI_SsaCStCoAK--uC~0w{Jas$9=b-xc`pq?VaHEYVfa<<{e?>&LB){OKn~2 zNWon}|E?fxsjNe7YOX7lB0-o^PfcCwK*_fU;fxIywXY3Lb)mxDL733G>N?k+ZtmU; zci$0&Yntdxxo8l^v>YvmryOYEok2KejaltzRU@4!^R6I_s-vog+E)6WAUtM-ZB=xn zt$Tw0f~g(L478~$wUm0d(b?pj2P|o(o{H}=I`^5>R1FpGH8$6I!E+{5zjrCTKeiaI z#Ek9vj5mzk3c|eeGS@iZF2|fO`(EpCUS*d%JmrWBsws1y6|lpojunYp11sF-7KglM zTEYF+!4{9=K_7=IYp&7**1;?FoKu=TP!5}Hyw5tg&r42OR6SvI?(u>*Oep_;YvCH# zdB$_bJ`jX)=NwnqKwUahEroFt6OA)v(Jmo-(F|!1%0jz#Wb`Vpf?Ye-%7Y$2qEFRTcTL@mb-JTTJU( zZ3T}Soh=?d8uT$&S*5fUu*WN2GNt54jL!xSxX&4p0E`^E3w5M4|&Nslbv%?`JC%xgweQSg+t zu*Jitf)huar@Uc7`OjGi*Lcn|#ufX# zm2jE69B{%hbIN|fnCd#8@RZS_6*yN}ag$rT z;x$vx*ot$LEgtZYGtOCJv1BdW;Ds)olT3Wc_^flEdz|uy1?9hNe6I1FXN)WM72|W6 zyBu)BF>}h6jm}k`@`MYDe%0u#a);X-amb81NqbY}Q%Op4N4vbLF2mw$`+w-RH|;tZHQEW#%+c zS7V(j`+||Rs+vx;qv(r9W|f&2gMW8kT#1C!Z}>1OeA9=~y6QUDp5n_s3|E$y!l#xO z!(Gp7n&?cqZ~0Et)v1=X@NFN8HIBK%tVUW@<~u$VJB;dBRYg|pTU*-pe9hpymaIb! z{cO&(pO;?pJ+a3lUcMCc{a5mPz9%-g$$d_ByWHS} z7tH;@YP{QJou}O6!Vj&6HRip$!!?e0#!SO%yxZY2k2zrEN7lj$v)kLw>>X&$N|$o zH9p&1aOa4%R?z>M4SFnd#7$;gxnhT@whgk$14Ue$vBKidZIK(i<^==y62GuT)_KT1 z&UwR0>*SuihUgEdL zXPt*U=bZ7b@!jod#rcxEOu9E=gSp=son20O%7Xg~ZyKL9?s11B-8kpn&HUaf+2I+F z8FM$XW}QrPz&6J`V)o6T{|B4&Ysx&~CZn!ivC7mRZIVq6dBBRRGj6f?C!=$Nfjbv$ zF|lrR);Z@MOYYpT$NZm-&ow69Ib(ydzZjp(oN~Z|J7cc1_Lg;Wm2r1YxWed$RkF$n zx0!Qi$S%`=wMw?R;LZ`VZw39o*`&uZN8Dt_l`D3b`nyfC$%rcloT=hm>{$Ufc+J3_ UmJT&n$$LFrD(Rhu_Is`W0l@o@)c^nh diff --git a/tests/regression_tests/surface_source_write/case-01/results_true.dat b/tests/regression_tests/surface_source_write/case-01/results_true.dat index 8979eb554..d4d1d1e5a 100644 --- a/tests/regression_tests/surface_source_write/case-01/results_true.dat +++ b/tests/regression_tests/surface_source_write/case-01/results_true.dat @@ -1,2 +1,2 @@ k-combined: -5.169123E-02 9.144814E-03 +5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/surface_source_write/case-01/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-01/surface_source_true.h5 index 82dca4d032eec7395fe8697247c7dc4fb01f51e1..a43646158be4af47fc9ed18b9c2e2246ca57e35e 100644 GIT binary patch delta 6332 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

    8Ps3wt{)|lwChWr z>wEgHZq}F9kLi8l%Q4!^ku@@=aYVUZ*01eCWUtk!p6!Onl^dpsEORp^;!^_tuX-yDkx)DD+NuXCYWSCo5Fn-BvVpE6uU?aC*vKe)8N z(%;^ubRCL*(r?Jvy3F&>d-~4U`rxvZn`*B)jbHb32+$f^+@>aCuKAlY5M|8JfgLqj zRx7c}KD}CSAXlG0UwR9R>|;xl1beJG;D$CPxiSr%grDatRs?lhsz`?5=-wmY&HSa= z$xsq^W&P-BvFJZZ^s7WAZW*RPux+eg!`J-fRVh%i<<`}$4@a4n)5cw9LT~{c95544 zWJ@IEmH#2zk=dsLD1wgH`s>8QKdst z=a69*30j-17k-3)VJl5J8-fbItmZpdWKX?9EN;DhYWuQSUMM@TXj2tbJbnyMoH_@B zi0qN+bEos1uf?MG!M3*7o2WaS-lpGNd}QY)JnB)(n^}y-IaUQb4EK2*@eGT5cX)W7 z__UKd+>__wPoVjogPZvcbq+Pq8S`PcCW3k9)!+^!Igqne$(nu<%q15XoZUG61`(EQ)_>A%e5H#n- z8CPSGtwiZU2*A89)yI5rIu;eM{>f=9vgO&b5K1mT9-9_J)fgQ;&HE4x3X8HbrHOT` z5{sKMJFEk6n%HYhk%q6a!fE#Nv51?4UY?D`FLoYY5zb$3#A3s`3;RCAqWS$4PVFjr z5ebeDk(@Y=e_@NFY7qo2y5W&Ue1^*h5ZnzNY<&QW>{j!!I6J^J{Xu zp4cyQ22fl&lo*vi%c6K&J(iBYfZb*u-@><}%z$8*>&yreuV!nv5Q~oP*VPJ9%a<(6x|z5TD$~;0N>NbJqg~X_){}Spg~JrkbPzd z==<)Y4GF{iO<$vQypuCc|3ciK$$V6t!$S?SU&;*0rMMN=C!Y+|QcEb-)#Q zr>X9Gp?+_p&B^lKf9ZoQ25zyvW>iaCk40S{drmF4%>VM`oCoTloLRH;&NQmy)`1Jr z{!cZe`^d_n8E^e7cf;OIXYY@FrA}SE#&g{EJNhCpY}4iD-_$YMONNH&*VKbOL)Ldj z{H{NBVpl<$x=p=+{5oX!%_o{m)$ubAER6kE?(m@nAqNIqXbN(-BuW11(lgu949oNR yso-U+evwQTwQ}Db{k`Ok(YKTDsE1fQ+*i?Q(0tx8e$q#2lT0~vG~H#wIQ$-atIAN{WDz20}boVCsx_Gf?Zb+5JWwf70p(bVRdCp-`L zaD36CTbc?P46;OGE6uY+>bgTYB?|U{Gu$Bf}Paz zJ-fBEa5F7w=clFAYMc-UC5NXK{D1m?$^trC=ucNMrK+a+QxVnAXvR3?>Emr{zyE-Z zhwINGnqMNc2LHo!A=O{8e$9w!N_w%55 z5%tS&lHb|qu(SJ5Kil-1_P-YWuCJft;h#;AUmN}=|GU0+_HH&u?d|qEdVBhK&Ggav zO}}4@cxN1k^=E-7?vRhS!+vKwoZhbuev|*TkC%=2VP{)6`kdPNoA$pJ{o0pp#+!f4 z8)jzapA!pDoo6ge)>L;Ut@*VtAJzMRG_H|-=VWAHsL3>Q2Ac1CQRnNgef~fH?=3*) z@LZ7{HU>n)4>dTv`wl5R@Py1<;}9*+D$sUKf-E?%4X`b}pEbbS4!@L6T_jbkMezrB z+H0+oR-?4<2R~BUSp0|$ehw<7^5^GsUvj#w3NxblrssN^!dE({b@~aHW84kk1(#eF zc}RjE`$9|nOj^M~sqeyPOk~ldg2~B{+;#t$vvEl6mdw*p@Oka3Sf2_`0_76$1n-Zv zOuj@j(@}B#I9_xQM&$2Gk{iqex7kH6I!5%tW)=~DD9nO3 z7vDLSAuP|(Zfw6Geq{fY-G1E5e?9&bhvwrn)}P{V(g4)-PjM(4i&@y8;&9C(e&+rZ zha1D``u<=F9%=>EC$_)ssUD}VJHqa^9UF}+ph3q-=z%UWIKR}#Lz}jpV z_U!aRa7l$DP%fn#7FbMn9y7iR3ig_PlPM?tV~*7U{fwnz^ALyjuGC3+CIUq+o?t3z zvi*tO2e2XL*tLykDcZobMzE5>|T5$P^} z)2Z?nx*n&#n`_?S+W?+PZIO3!HC*?2;=;1xHfR+ob+5)i2UQER7%rWIBZ=VzIY+#D z>&S{U`b{40JCa7{Y*5uRG)?RPDxn%XH^-(xbN{#2QzzTth)vC;QnCC@-D&3EtM!RIA<9OZNkFQ+vQE(g{|9#0PB>1gIaroh;!-gt|s{CwAAbL3CD} zjjQNpY`1k7jtDGfkP z<15l{w;gS5(21i$2DBBxYgjD7G zpHD4RV5r9o!}(KsEdE2Beg7fO{{Ili8pCmoS{G!j)&ZipmC6^RtO%tVPuQ}CKXK?u zH54-2&6)CdJt*)S4>wuY4GD_f3esZ;dfplgJ!WF;*9S11KjjB@7|xaS+x`zV7Z7-b zqIn%Q4buHuE$K||{oY)8@8>^w*Cu`a2+o@ zGpQNyaOw2*JtacbchU)I)>}}=L5w{X4g0`5x)Im5|h4P1dX@k3tXhb zm?Mng*enZkLJn{cB>SsXExzFhG(E8-qod~iB!KYPmgzq!huXr$QuAF~pxkiIG?!5U za=vHUyefjRUyETlr%O(Rm|6_Lp_6^OnM-HK=M`_V`3;s@c+u_r_*RZeD3Q!-^|_)4 z78P}UUUYR8GWpeE`tzbVhVw^;%Aq|sJYqdw$L2zYOu0qMF6OiA0iu;WfDQVR%F*bonOc zP{${LK7W;!a;#sAAx8zH$L#dTu`o6~@^z`N^8MUVx}LEp#ns+fAHlW=CT2it1!=c; z?NB(^3UhK|nld6J(7zhpZ?j0LGUQM%c;+n?<#23%y^Bwf18L!Ba$LaQM(4N^*P#!$ z-i42AmMCAeXoGo~LBrfd9dO@<zT?8yQkWQ$7OgmX07R+beX=#o9c*%N0ZzzLv??x1bV;9+I`q3Fv@TBg)^;zyFH3*6cYu z7R=b+u<@wngG|5Ii0iwpy3ZFfv>P@aC2^l~C|%eKZ#C)X8IRBIPp_w5T@iP$6@~=_kM->R0)54< zbJR)`p@E)SVYa6t3P?|@0la?CN~qm@&|m)6Kh_-ns*BC@)PZnua2vD3LzWQYWB zl~d-r1jRxw^z^+hhcCJFZzfm*^yF8kPPQd73^~}iH!?+tUzLdoiN1PK|M~VdI%hS% zvCoNpjX)3$&a*j}4{>UjagA2pPMUG_11 z0MxL5BKyzlqWi%ul{Tp2&<wHjIM8<+o)k{3W4tf1 z!f-6JNdTxQm35>Ep9lcK`Y7S&cAicg(y+;T`bkZk;IGV+}C& zlJEHez>mJJj&2bOWjuduFdT4nTbg0SXRy^w74}KZ?oa)4juE6CngG+h`bhtsoq*7w zp+vB*fmTlH%e~s<(AI7a;PlrDhW>_)e)FlyP3Y$KTlqd5%NTNOF?yyqOd7nZ=S04JA&%_jXF({{ zcmiu=8D4E~A=s~D7^jyL45Sx`W(xZDK|zls=d*q-NFlHDu}3%58FH|3eDe#|sFgk3 z2xs9pFDT7U=R`hjlzc*{0$<$9tcpFGLA+IhLH-~Kjs^L?DYI8WnZnD0Nn#rra%k%( zP1fpqjmwW(KESZ+CAr@5^9VFO+%Mz^cLs@&Un;;UQ=t}ixJMXUxwpg5CV9t~+bW>A z!Z!_htA-hJuyHT*Q?7D*;!DU@AUw!?X4VfxL+2@3ZAk|EIkmHNw|0RP{{?3Knk|q= zaP=8yXB3@K-;!-N2WK%&2sQm=Ws~l`u^DdfH6L%{nce>g<^&D%$*01`@KIQ%`~kXs zP4K#VsTbN?3S{{6??OdV!}l=F!NJC(+X9^8!&IAqN0iOx%wx0jXJuoiS~p)TxQox@ zh;k>uL>uodmtXWjlf^TWHdPOG;l~w;Y7a8B+cJ!v?7u5FkB9WYOCQ7pG(~5h^W9?R zE=x3Qfw2qabk%EHp|xMjQcF@Jobs)VvD?jqO1P{pOLw`%kRyiS+*^G;C?I_lR^YRX zqc_g_+YRvxGV*uo;L9uP$y4o}piUrFa!9cOI%rjPyX}xgLj=wpS)#$1BZc8~Y4}yG zoX z64W^u{^V^m3yKOsoh{6aIcqVTfNsL2*9X{Nk*&J!DYcpX zqQ}SS6&ng+GT?A-P;P_m7cv8{eV0K;jCwZ(&Y=hU95!;N{eqLBqwvN=>3uJ`2|A~O ztHXkGc?%r+j$63@@OwyhNjo+0lmx}Iiqc#;RMGTX57(G6F}B-!svcV0`&v-uldkIs zjB_P(xGbT(Z~gIpfnPCIkeFWu=i@rjmyg>(gw2B0Mtj>JchFtofN(9;zFDZ)at_W0 zjNOUg^J2I+cO2v4)RgY)5&*s(ypxC&5%E`O`r)!0^RQMlqIMaPQ_( z+Z;19^w~1+eAYQQTQPbb@ww)lOB;Zs6~|7kG#R0DNb5$cShZ{5@sZ_+_2FaKBgmm^ zxQGn>W}atwC266dk4}WGoP$H1Sae#>{VI)>CZ*tr;{%7K1J!iSt(lZ-vM|@>f=J0C-!%5FtM%0z)f@|_(GDhnw z>725Untto`-$Lg@0_|O6bpR)?Ds z@Kse#hh_Ctxifx!=tloT^G?XI_?gP1>O4>o@Op=IOA9O$I%OH!t%!Es8a;JOax262 zjkceo&7bk{>V_%R5%_iVAviHVi_R%6*|`=8$b>!L!w+hq1kj~VX3322gXu#n*ayZI zqFxf4iH0{d7;+3TdPc$*r&;=c0cugqYwvEzpxZ4V_h74delf6+%8B9MQv#RlJM}2p zjSSgC)IINeu0@|NcIUNxqr{M7jNu?Bj!YF86adF<^{eEAh;%(;&q|Y4U+IKfwrKgQ z$UXxrgS7s>!$O316=#%;c)kLD)|a)olsgPL*nQTi(;oBs)=$8oFBbAa8k`8_GVlau z+uC!d7A1pox4-W(!ZpDKuZGW_NbLqDXKE+UB7vaE(K$32j{jrM^38tb`eLkv%n41Y z$!KOe$2-;{E+DQ2o@c-NWo(29krR)Z4?k^!w`wLs`l2(zX-BhygBx*F{r~zq6s(@G zZ>FbSpX-6gl_JR&3z-QtJsSNvm-Qczpj;MNA^&g_4CY+yq09LJ`nttTZFd<(vZ8%5 zxm+2)H?yOhi2fOAG`YRiX99BghNZ|`EkI}-gqt&JoJ@kMYf?OpA18wKg$i-eta;EN z;*D{x1~WQZ zjE*dSy&`&!I1wB7@;ypjKN30tA8!?Y@XmVnJ-WjI>48VRRj}V? z+qt!Ex1k>4fy$->WN1H;@-S*t6W#a3`^=?HYz)VT_8gIBxVQV<-tOYY6OhKw>GRRq zI0R&H-pK4}fETj9R@ZLNhe4m8b8-}QLzXk8_T^7`(FB}9VaFVtMKl04Et86A8W5X+ zPi13zttU7Llwv&LxPzQk%Tyh#`O400SX&F(^jzcgY`S5|7wsW=Z87xnwY7x2IXFTX zPW6_*JDeN)0jVqgg2utwI7F>lu^8vr1r}aDR`@DE6->ViU=o>VhMa=ewF{8dsDVaM z@y9thOEH|)%VIr(lDH7=iyQArI<(T)gFBtPq#f?n;6+1{@yd`Ma9dS|XdGM(rlJi^ zxdV97)K5Cafhvs02g?y14u7`FG#l!zbXsmW$xfhL2A*ItEqmg9eJxnz6@9IVb!Ojf z>7O8eu@edr^YdE*wb90%1y`=lp$EHP(BlsQ#1JCrohY=^cslz$p35ck@nxY8ps|!y zomZ?7)QQzyImq7&^yVE9t=piE%EqT2e*8b+Sbtyt{G@I-W=WqQcUes70r~%#}Gfq7=i>Kf8~~mW-t_{Qt3ezia~sAK7=RSzaYF7?6D2=A?^!0BFM+8i7SI)RcQO1jG1 z9QXjmBl$u%_;3?w94r3u2V!QW0FtlapRH(vERW>6-xJ%R3u^SG)1=Q%jo z-#P4%`lj)kWG;=Eo=dU@QG>0DSe!+k@uthj;3?et|A7&6=(8nh(qLTS+OCA(Im5_KXr5Oa3 zKDfC`xeGdxPSyJ<0<>OYT0L$K4tBpFqxx9amb(U&@Clb+KQ|j+zxdGk>H7Ibpyxht zkC+@tF zL(EOUyP?N!-P#sFHs1<%ceKJ_GGyjd6GJ(jYlyqp81G9|G2?S)r{chYhyAdewfK=_ zS|8nR`_M=7j$-+s8M%TtIamn~H!Gc;-b02~X1#IG2Ja$y&Cl{|=QHL|FPKj26VJ z8SI}FGumLsa>1A7R=j8p^QgDx1IF>_9t=nOO!+lG&%l zvkag1FcSf{<5InoA~!;*#uLm-7S`OacnNR4=NTL?Zv(RJxtEf~JD|nN6ZnMI&8UFn PQlfn_{VsfZ4t)1X13I-#{y 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 fIbtUfR=hm1DWH4{y87gfrX|1|kOEHDC< diff --git a/tests/regression_tests/surface_source_write/case-03/results_true.dat b/tests/regression_tests/surface_source_write/case-03/results_true.dat index 8979eb554..d4d1d1e5a 100644 --- a/tests/regression_tests/surface_source_write/case-03/results_true.dat +++ b/tests/regression_tests/surface_source_write/case-03/results_true.dat @@ -1,2 +1,2 @@ k-combined: -5.169123E-02 9.144814E-03 +5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/surface_source_write/case-03/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-03/surface_source_true.h5 index 04a6ac9a387d5bd34e4d46fc533e90171d779238..4dd5f821af92dd98395a8df31af2a81fab8a8ba4 100644 GIT binary patch delta 14582 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

    9~Z3h2uxPwu_`YUdKuQ+}EC2UHt6_FFSG|=04 z>ecYf6w|D$s*6O@^TGW94_%F^*RWWS-l&;70FM+nXy_h^2bI;epfq49s$x)VI=k2s zcjnB;EHjyu8m>&(99@Cq`i9$9wWB=9vwupr{K$vUc-Cwl3EBYkjj0TP8mGYNg-sK- zuY}O=_0qLY={D22*gk-Qg^=VEm)>s=I$@Yl(5PPT5Aa6zkxHsu2PpJ8`nq!+g!VSt zyR3`bK;5zXb+O`>sMPmJ-O(!+cnH~Wcyr0bx}={<<1_4#_^8cWSLTmH%{dp=);ul) zMba_p{^z^ERQ+}Byc#<2v#B7oZ&5%a7oD`$dWU_>ig|M-2kruuhL1YyxoAwW0Mc^y zn2ONN4q%zPZ=LndY$&cz`u#+!1;l=}MQcdiU}d6t+$VY-Vl1`7(m4+M5(~517TZI4 zTNXddJtBx?&Dv!q;W!93WsGdyY&!tU)vY_FZxjRR%%<-Kg&8ovr(x$6BNQoFp%}C@ z>=<5+Z8_Mrbh#yOpx}Xx=$?JheqCn}%wBHeUtXI7>J$z{B>1;OF7D2)*CN`%uJ8?^ zjO82Ir^w$7UY(m>BL84VHKA|aN8&>yjf>6hpW;G}D#exlPHqNMzf#o;tm)u;mFkF& zb{pV|i&l7$Jqb>z7Twl)^W;C-txQfn?^eT$`);k0E)NazAYNhD^W!|)0j(?gO12mS zx7PVqZ&VXVkUd{0?ll4CwkI8o?a9O!a89dzv;R>kGM$9~oNE%VgzPu0f5W{cvi-tN zW5&HA>xBjxozLN;3(N;_GS_d5OOb! zOIa5Ka|JHRccIn)$=Br7W=~mPKHLvp3!SvgH3g7kLSl(KvpxZhBWb0?3*BJFKRo=4 zQ$NEyYI%OvW-owmktUhvLH&Q?-f_4v{EsOgZJ*bI>>FzZ5N2@v9arEs5;LgV@SlDN z$y6lJwb+*={O2kIBg%=$o)dlGhf6C|_Q=>n4c^$zKCOd7L+^`p3>o;3zzMkDAtMo0-o}#O?H1B0H11z#Nd>4c&zMuiv9eX zP&{X^VALWqdTs2&w_tO#o4-?y0my8`Xea4b&*Rr?<1Tu zu539f(hu$=aZ8kZ$_A>sC!g8-s-u3L%|S{n>UcCbaNzbA>i>2tD#mdq@+7N_$$Ydm zrxR!KUHS|(&KqP>QY%4Dg_K0jcn<`U-e-QC6hz4L?qj};m2wol6?`YNysvEw2 z7moi71ly#A+AglS9TV6%_P4i=+x5PTx0u@E;lZ zD~j&Ui43O=y7-aH39HYyuX$K5P8|imXcI-nIU_J_*~Jw%ZTesbb#u62ZZ@dOUKw*S z{}cQmZ;=1}h5@?wiA33|W=3SPcFWEjzC#iuyxa~4;B)f&B#)JB$XCGsQnK65w-2tk zNRXS=^$ARAlY;ENJOM%aR)z{OXAo`;hn+v~p;lNEjw(4NL3;SCT^1*0}xjX|P zc-YcCMxz~$zrENW6Z!>~2j2U>r>PnAqHm)5Pxv8OCE!K(hd?~yT`;R-UP_+09WVs0 zIEkKW4W0yCbCT`uH#I}M6DDcATs=^(>CJMNcQ4_a8sqMw+I7hLORHBF9zKg(&B^YY zO8U~y33mMuRyn^egl`BUN9_HZDz-xV=%RnBl?T9=vQ%3#<9klsxZwuxwSuV9z2Xnw zcOW>sHPmw?#sTY{&-^|0q;Zh<{Okq6x>?j2+;jCrnj< z4IkfS=mqq{9R~2&mddk8s?1nN&!9UV&~6;KH`|R)M(xQ3j?a{?8_mYI?zDs>af?;j zI$%fkMC#l{^{{bExxM-3W^nkUc-5X!31okr*Vc_EuKp*kL2}QUord`GK^wZkcU4Gw z6n0!SSh;6eGYpC}A*8uA!SMOl+GY4VfaZ>=ufdtk;QWq^B6q?&d_%{{cW)MfmL9=F zW17LZGWkMkK9Jb~-A=wwZk^Qv(oTdd;(pu?l16&hc1hQR!{L=yv(U?6a{OW@^lqER z#XkJu#O)2T5&j+j3C!R+n{f&9Msm>#uAdm03>$zOUwRb!WaL6EKO?7Z=PbCzO7TQ0p(LiQd$B3 zhKptMf;!;`xukPMkjZ=ylmhni#6;Iw6ls(fIC#s)6nMikPz)}xDhj?`R! zB=jTH6vti5;yLp;yrAy0!~9O@mxkKS4;P>@`#odm$qtZg;Fg?s zuMLtVoLn42pCi|8s_X~7op3*J;x2b7ipeE+;(kbfBqv`U{CC`}6q=oyrSpIC{q6Pp zW~ovgOxRY~XPk+@AEF)H)a}u2M+YAW69=@OC4v_z`#rw4GmbmPIp1jIkB}t9Ks;*M O#%aD%)7KiDeE$zPU;ldbH#oO@*%qLeIA2$d*lQE8==_Ar&S(xN0=tB^BfOUN$! zzVEV!zGKE*KJ)whUZ45rzRtPlzVG{apXWK-bB7enND5|5;>R;rD-?Kr5XJ}MaIC~g zRx##(~q= zj9$5Tm$Z_wLglb~^r1AO(MmxIDhqMsKM_LN3HaQrv7VgV5=5|4fYJ!R)QD1Mr7N@1 zl~>Z0*_X-`%fdMcc=4_$TWX&u5udRNP_6YMS8lN76h_JnmMTj)35;RiiK1Mh)JD2o z1bl#m!r68G>g7JqXS@Qq9% z7SHx*FX13O1bpG$TL%Nojg~6k@DOm1h$B3U4|4Moa1F$rywpD7ru0cDuPUyFn8~}? zq?wnDU5<~{yCP47+>0Cud<5JLVi@0InqFEtrmvNBYdfL|GU?fnmGCFN0Z`lggxiT_$F zNncB4s47ap|0r_3RkI9MnqqH~s48Bf$l-MBG8W-t1iaC0=dmJk9of4muao`nz$N6S z%Z#8jm7{M;sa^M9Yvt){seJv`5b$pjUKKBBLt~&wM8(bVf$Iw&wVIADbOyEQ8*u{u zqd2y2?G9ps$$WX<&~$%m8y1rd9~z9%=MeemA?@VpG)M*C0~;y;0Y2}9+&h{ zUQc~d61fs41r=|hwxk%PPSH?RN)`V&=kvPA2X5W67V~+Ue^EN zL6-)FDRSl5zL}jLrRA5ba+4!l)zKqRw;jQP1SP+5k(TYk;A_nApFfQ~4mqc8uOK14 zVV4j2UxL)TtfVyuk2!6KzX>JhstTE=vf!F0vE1n|hoH<_03apKSD*l!dn!)*NKbY+XBGWBhU5@A{M z&Gfgql?nI)wf))35*!O%%(0-I^`dla#1=)c?BOj-4^v2QrFf&;dR1IBF>F1Rj}2X$ zZFJ@BbYI+yN2OYY~WM!-!Fqt$30tVX~`6mPQaxRQH@0m zHw^+_CH1ohd$=?aE zM0%#yErc8;#F+mx!Wa<#zpn06I7Bikx$NQYXRd5b=mRcZ zGBSsQcfncG^HRsYX0Y-_LfC<>aO9M>{U3!#t_$5wA-95#oVB&t-LuUz;CQs;7>E26 zMy}+l@HSJoU{L!wAWZd58?1dR6_o^f!1vwa;w|R3DEI2NQs&1pn9=w}T&Bfq(019G zcsU$$fxnTrs$mdNt67ojc|s@6If}td64WOmy+Ay*Yz!negC4E4K(SI2bk|Eyt)mJQ zF)1;}O5;lu(F1skR~vpUpNrTiAu#1}VK6v&8U%$OTjMGF7uL$FG!r7aKU=9fa9`b5P4})GWHA2N)?oBc4fa1w7HFRTa+?L91ZYua8BmP?ebgr$fuw zanP}QugRiXSHy@!U;4)+p*e=J9J!Qp{(MFY7**q5!PwmbIM@k8L!a8<#)5Ux32p}H z{+M~MmSw8}PU5wV=6GSFiyQKm)p9M2@;Zldg~C=1;}Aa7sXwE60`OlR6!RJBf^Sv` zUshnK0wX0}Td&Bs0vX3Umw$_(sQI>Qk6g)|X|p9|QL6LH?${;y>+PWN)wbv_%}j`! zj=c0Iy;gYPWhiN7M+uzc7GN#x?goMC|DN9CLD5}5J(^BZQ7HYwvuHUjEG=$qx(8e5 z;8`9eXUUj+n#Yqq*xbv?wrGdp(Ffl&-|m7^CrqOQw-m#~kIwsR_>Lh;_XVBsHcRUu zUSb$9$KS3;>sC`&5tO&_(dmrdFl?!~gT)oet(I}WfY>Ea;$Jmu{-hSzN@iTFI?x09 z@+bc3@2ZDCMh*;UOlc$kI=?0~UtRKog_X6Cj#IOUuh69_u-k5T>XGf^;G{lqnjrQ8 zM~7WZzBNstEAL!ftN2$aHvWFns$2){;+Wa^km>~#PhVJ%3+aTs)_8xZl>y1>J;ZON zSpw23-dWh)?g5#I&z1dqGoh(=VO>R5D-^YJEsV&KLvQ%>Kb~6VO9FH@UiZHu<2*kP zj)=SESqb#hdws>6vBgh03N* zpti@egVnbhc1u6&o#AeSKN=k(lU!G$zS={Q1?ChGij#@bnK-|imqj~m1wyT6MG*5( zpLO26htIei*@AvI0UJ4K2anud;A?SO@}OA}{P%cg1Jgw_^!J&%t_9~Mn@GVGTf}8t zyyos&e?_o>k#v%A{8#7I5v*gQRT8bvUIeLT}>Gd;y%oN`P_m-Hpuk+>GEg?T>(~6u6_gXD)Wkc;jGobah=~z|JLuY z%;ct4oUl1+6T2=dos2>r4k+x_EV80q3K&-zikzJQb?bOGrw0GQI+z6VW7yo=U}^W- ztBQPIq5r3*foPK+V7g7|sJm@9G9PvIRjdub*ojk?q&AyWalFK!O=6UHNzh)hcx7~X zsP=k#4toa!V@c%?!D!gTE$Dv5G8J6ibxkCVwF>~(!{=D0I$-662e+-RtD|zdky_g$ zH8CfWq$n{po5%TEat?mBm;Y`&{11kGdz-`p8Jsc+ zt;qLD9aZ+n7TCZjMM1f|j^&DO9s}t>pRUJ+8+g*gZrcE-;ckF%cobWI`~}TKf3D^9 zX@!bo8UNUAa}g!a+d5*k+c7<*=xp4x=C&8&jwi_uS8Q~iW*|{-v;I%q>;3~4hpF^rW>uiK%aeTL1d zp;izq^VvUmRuffyfb+W7Xn5tao7}0h+a2;N1ECZ=@IpxWO<9Ji>N;@RC~3*1;p-g4Q2JK0qzy;5tcDsK=T&+`+ZKl=)qfwH9NH_ z-bztdjt01R{biA}@8|k{Ox2fjYk#j*oPc>G!k+r48Ngq4_Emt`ASj1FbmJ{5An_u< zhI`#{#K>8n;K6T?#r`@v+)IV$t~ol7z(41QEI*5lWBPpKj&XX*R0VGxygp-yFNHEO z+%EMc{V+ejx1Rr=5;8Zn?_WLtW6T@m7jd~4uczE*s#&}TAucmt+uLar!$nNf(yU(f zfM?bZ?-jmnhFZAjzNC^eaEW>ESkS*%ureXx?P<%*1zgI)5W^*YH3xXJF*j)y7sZ8M zrrcGaSwp-=0Qk!$mCnNr3Rn~>QigH{p^GZej|B;_?zPnv90ocDY~?45hk#P0{3wrD zI^cNpmEoYRGq7Y$x0Tw;jhb)FTh$w+gYEd0DE@f#V5Nz*cMZ&%eb%D(_b*U>)74;Z z(gP*Dwrm{EX@$iP-`Nt_D`OxGHC)ArY#KjC&$4f?~8{<8v1}bZpX1ADQ#3lfmrD!sk{&^6hB!{ zQ(*D>huumhq+$>@wP_vRI@5#si{p>2pIwNtFf8~Ys{WuD%GAY5v9Jz-{FM>n!K=2S zB@*pl27fAIxTg@!+W&9~9R{7;Pz_hIsid^gTv8c))bT`o6rng`$?8SwF2|7*Zsv&-*# zO%HrvyeTQXs1;s+j-F4SK~byS9Wt71Y722m(LtR~hpVAwqu29Cz@-z;cgAgT2=z8A zvdU_dHTg~puw-Kd*AuEimbCnl?Ab1GK7z@9_?9tx-)Fb-;}ptnjDo8{$JYt2+XY@v z1J>x_w?RR8tM!TC3`&SXaTPM_%+ky4(kYI=Cy>1iK)xOo-dckaOi$PSQp+jj=D^Lp${Wh{u z4*i%CeRKUfbSJnZ)8T&Md!&(s8-v+8heBfvRfTbC!;ebckwO%<-9;($a@U9j(T{a5=}9l#~%dc>ZU&miY(=A+^tMbO}zAD&Ho)>+Vk zqNpBG%2HVS;9iI1U(875HvZzav3aN!v$5REya^yG*8#z&3PwP-X<(0f{HUp21) zdN?s{(%;({6I74R@S8b_R!@E~lDxXY%gp#NHzNHn+>~#rac722F%$I`Ow>MfE_3o+ z&0!f0C)*Ismp=*vdU zedtrhvkzv^6Lz2I297tJlB`5>VaQl~!{*>YaFTz8R_V)9$X!z&xT_@t9{s(Y!|{+h z3dDZqM9qp}cn-31vtZ7t|sv5>(H~5X;dAO!gJsaXHWhQG+(c;9?+ol0ue>l&+zo%T@M%kGw}UrnrPl_c1knBWZ`_kUxRgaOqG)HV5jfv@s}7v_ zq}g4ua|j$VEI#i~)-sRV^3A!>A3&3rscGwzHn7?6{m2amVN`Y|Vt9M4GNv6MCi4mD z>B|riGpIJ5fZR7ZxTiZ{;N93 zPztVH63IA}(hl%a17gP-TA)tT*&e0HX86ur_(tOSNhJNmc$iKR0YfpN^ngd@xOa(k3)_fbi#2TbXd zV!Ng(Wz@@nY z8&r3jpj!xhtaDZBn2qMdBQ8R^I}MFR6+Sfp#xG6>j$dmAx@Q$V0@n>dtrxz%H?>M( z$WLqb#DC>L&d}~IpN1mZ$OK!C#Lfev9~(bq>LSn?>7kW?M2rn!?&bYF{ceVFFF0st z%YS&y3t%ID@yVX^{oum2_){?AdK?Ue3WzOSOIa0qxM0G;*Q1nS8bsq?IZ^Q4a;~pqB5~h@&>MqF%DYy?}_U zq!PBI1K!_~wT)L0kF@gP_H*%dU@!1#Q$rIC-@p%*BHFi{JK*=v!DFlPyWlRZvu19m z{veL+?@pZlriTr_n<>5Gm-;yE@Wo@m=4`X4;b$CaN>D#DV4x2;*xt_H|L!Mz+Z}sg zCsRF?KP@AD{kR$Wh0t%naasaa=dLGPUavn{}tMh281)^Q1$FH31fL_KG zR?4ZSs2zhqgZHcwHX3Z9!=1R;Q-A1J z8OAq4T9V~9`4qyGzwR&-Y=d&5n#i8$Zh()&*BX=8%g&vDS)dtk?t8#ifvhxq5mX>K!b1-S3qqxJo z#B?7B*fGd&e5C;F`)$BFAyyBBORY|Gjk2LPw4SqG?nSVO+D@7CQf{m|wr*$uN*USZ zjBFhN-cP)f)js#a%(MoE2>TRh$zvxe<=g`PC|h{Oe>F$rFXex58eWUddA4*m?zrT~ zTJh~KO;M8HgiVy`u?$e7F7|i7V+wqJ&7AA`TsN#KI^)nuY6I_zyan7yqeyGC*Z$({ zP1tH{2OX|B8<^J()60Zp$4T|@fwz$Gn&5gM_WHK| zyG#$H@6t}e9d&mw53r-dy~O4J;#hAz4UfeAGW%`9xk-S(QkDVNcESmX(&G|>ZIE^E z{izef!wvgWu;GWmH>A}$|8y4Lk^3$v_1Ra{`t#paW%lLwJX@g zgNWhYs`_pfUeX5dsNhbV$Snnu#}qs=0=r=Oxc1-cWmAYx<&G|uj>NAX=D5{F zoF{e9zLSoB_o|S6MLYvAs@eLx-O`?k^eFzG#N*g@csuawWWRkQJR$W6U$w3cZm}}T zOFOKAf^HoD-iOlIIODY7WyB~?RUA3b?n%u-ITJ@b&G8q&?l%fMc@|nyGg`x*g#WkF zaW6R;sr4~$R&7}hjzq2`HtjRi9;G-U^>O)q1RQ~QWZz;UD|8f*VRt&q)z?g%^p`0Esa_fiE@I3G^)@T1`{wk>mObgPmx))PC$t9x5BrWnr(toyaGEaBH_P8SxHHkS;&RJd8^-%|iIy^L7 zd8!O}wqCMgY);1_gmwZ-!)4XYEzmSxI{*9BedBj}T`Jg7n&hjSUkUqdONR0V2LUoq zj4irx3g)V`yk++nMd$aYINw=zh|EM8Z;na!R92p!fI?G=bt@{H;dZ&dv33I8@a|?N z*MICC(4eDmz4Q7gFp}b%ls7kt?9wMDt|FiR(oRBYX9YBNz7m^K-=q%%nzia2d~vpR zzd-A_-RD@|a$vjq7cM}xA6!x0D)l9#8k$*OZrH7bpgM1?e;uK+qo~QU2>74)tVlt! zR=UFG7Wn-5+%AQda;Wp@6fZ|oH`vkX7(x)Lf?-Fur@M=`02LklbY_(#`gCKce818d zHbbtU5na4?`p)*RPOrt{wpjCQm{edd{Mfef>O-*t820PZowwJz;kdBppKk$eQ15i0 zaN0#J^bf~pms!QJh4F^6v%?chw+iWG?P|PtZa))=uvVmQUv&%E_55)01Fj*!%YRzb z-r)z7buk!60y2SGLt=%lW9lI9#oZ(v=S`R}b21)Y!`rz?~&1O=sOF$1Cf04nai}pGx z|6C+Yk(2y&8#!x7a{n}?ck3%4?C`C+?$!s3AH6qyrQZYJcKwrlcclePd~a**As-bK zNCd=uW5!+>0N8_&@fYlqggL*2G5wLtBwLW}2EJ6LD(o$;2JB~HtboS;#fGa)Tiwyvii{{W3JTXvms2{_;Ey0*@I5cmqM`g+jA2;EfR98!PCXMA zLE=F6m(fQFB|K>J^G+KFs&)lcul(c+sj3*nPx3XUFOvT$g%%mf_N8QEOlFGs`keV7N8OzPPFHKY;ub zBL0DP8C*XjzHuUI5IVTL>~;x=gOdmJcgYViqp$CxmV1}s^3(A>d;Uva1gCVklG@^B{eq$Gje|(=Gzkrc*UBy@iqiB>&^b8N@}Q6bfC|OGJazJ zwYM!DvgC@6GG1musIO%uN#0zk>0%HAT-nT0Ee(4?+-a_z!zQheLs0qhfdeQyYiy9_ zN#;wNmg#euBEEHfoA|NA4$_-wHL1gY7?Ik-eRfkNZJ;FRZq12~4p{6{C2{N3Z}6a+ zWA(3pb;$07((S7^P<)=kSC~$R`kU9>SI?IM8qbdgDGJ5`f59WUZR@uumw|!g52}Zg zS|QGFL$IYGc`IUlBT7<-)s70xB(-zQUHcRIx@pNiZXLjCVsk zY`QZ6i?x5lgHzFdZ$by)-rWMpr}?&_v6g||49jro6P`QK`0}*^ofvLLHA8;=rhdpL z0>=^TlU8d!c_=H4CEK0_B-41|PGmMCHHFCmvpgOG|jtX#aYe*Cu<28Hs6m z`leQRX}W*6^<1*5XEV6(=*XvV@F;ZJi$8Y%OdoLe84XR%S4IUlpM4xp#-)u_s}})j z6RLTj*OS{Dn}P5BJ^G)|U<-GWT+?TxKHi7m4eNLO{X-=9a?eqN@UKJALCB{B#9N`> zd<^-LyE{cLZ`n`=g!g`l z)(af~$2~<)p7JgM0ixzF$_vy{C)9qu!4C~=3oAi~dpUW<;g7~m7;bL7yYcCxe}K{3 zD8hG<93b%8!|OAt5uErV-J=`Y2liat?vfOYqG7(*_MbBk#c(C*aMPZb>d##3hoQfv z1WY7bY51fl##O~Hx39-b_N))eB#;y%bpnb5iGxgy|^8+-x7O4x$NWh z+(T;UT=tx1K#J((@jj_s)f&-H#UpgJX0eX zQ^a$yLm$L|yE*qEo_t2`nqXWt+y=8-3Vh8W}xYdy!(DtsJHhUjJR-v#ee)KP;TN% zdd)Bd934|$)TWgp9)phqW3@k`|6=x*gID5TD(v(7 zK>fzC1Fk=QL*xpVj$>^(__m%UWZM}F^xx4(S|_YSv0WE6^Vu^!Z>)D=1cpx9>>fMT z41B^wafW|dp?u=&>W=bgutQijqCcw#wm(q1xFJ>+<-cWwqPr<56cnS56Ou@~z-*&Y4ZP3cSw3r91wVW(#Zd1#8sL!|;1hT4uE46BCOr?P)%x0<3& z6ScfMsWwvBDbcaBbBxxLQfUCxYE}}-JTmG;)+o$ojLVrnQUhLlG#{Ow?uTz5-4K$Q z&VoLjl}tQi+fe6qZg1$Gza*N zn}D$#m)3mj=NHeeMN=4uO$8W-u)Pp{Ue?H0z!Nt&1y8tn{aB~8u-f=@lv(YTe>0P$eoWr9x$P(FeDg|`O>Bw6*^qK9MLsu zTbnR7dG#&&0-Z*n)8jqEzs_`noXVYz1-WVPqzl5kYim1Hyl=sIQj!;~4Ck5OPX0>| zEiX&UBh%n_d28yws9Hgqc=ZgalK+SC<@YeQ3^F2rj^|tRPeLmX#sK_*2JH^mdE@iN zh`;jaxb1-M#btbHd1M;yU7NjJ=WkBHr?W+G{S;?0+!FRKXEyOx_~$PJkL&R=IOv@a zH1eq%N~h*0_~199Nl}UGcar&%b)bYF`7Z`8LOSguDd!JGi~z6IBfb~RAHc9_qC$~T zCyeG2lsb>^1ZO!uRUJ520Jz_9sU1;PL`yZj_S_`1qYdF26d%&vY%BjpFAf-fRTR3s z;~V&GO`4F#4S>mWf_EfmDnM80 z&}WS+Z>(SyXk0oOZfyb`EO+Jr_meVNnfhFik5*ny=;{J7F08%qTs!a&&OESrdL4Q! zy++-L?3=Xj%ILGkxCZxAhc=hOXy0m%UMp;KL}DU+O6|Lv13x~TylC~f1XQBpKdgNR zq1)KW{rm@3qdw(C#oT3lb?I~%;!zKY)vt%>blhY_G`3wpGMLRHvWm0d-_#7!&Dts; za(Hy*i{JquW`6R7<~a>?cj5i@6=Yo6@{K;(%Q?SO;zQ9=423=s@y>IbfbQmTO5@v_ z;g1&|82kNuVe6CoTx>hYvCp9E`sJ7&dhlGif)yE;=JWb=e3PeN{bKWHCQ++dNqTK< z3`f#OVFRD1LuNxJGz#RJopk8|!3Qn*KmTooCfn?UFAVUZAravYY-E4<|KV;8{jAf| zhfTdmfepWp$-@<-(1+uF8w`3OyfZiS(Z21z0RRf^CwRp|j(Qzck zi}z3 z>43sp-x(Q;U|&j-_U@?{$r34p;WzdUleD$%zQfq~K#!}v zZTC5?qVh%ASxGa6wY(f^6ELOb*Wj(mLU@N5-Sbg#03v~}-rk?BgsV-DWM_8iqFJr% zch*wxyCc0iqN?p@tDbowre~(O_s4c~MtGLGGq`LJFc;J{oXYXV+ zKz`0tYt8jT^FkIOhT2VN;hnNiYRkOJgbsImhQcZ(iC!8WVwt0J=o0fJu#Kvdb=mzD z*rfbs?M@_r?wuTGnSQ(p-s&i>2rraDZyy#IP&HS@W(QL`T))*k`*$T}L)?J_^|oN? z|1$oY3m#O|R)CG-u(k40FDz@g7Yz&Qfq*0D$}NT4&|Qiy7d**-Ri%yMW^`WV&#;oQ z!2w5_4-2Ze8ng5ToY(zGb|-@}SbOb+O}||aJSRf>mTcJq{NL89?(ZP~(c`A{if8fa z*g~9~g(rWe{>QTNl$lC3VAwRje%0DysK%M}RP$^fP_Fv*B3rcp@|b_;s941nLVFtYa30F3*M-SQuXqmZvsufSb^5#je$G>xn7g%4vr z>jxVe;FhML8s-slI*h$`mB|xJ)EhZ5BO#)Kp;%H-3c4aYeOXJu%EPPkl{A=0e8-*+ zCpz^&ywDfD_Z8nkZeZ5tb*_aN*rP!8!EXnJHO2!pj$qXh${AK z3tT7*8CvR`HlU2nz3g^no}&dQ(mcDpa-%Z?5?{x=Li8&6v!0yuuBQm(k1bhvs?X27 z1Ng*#4!!xdodR!83uKu+bqW6f!EIX x1p6+yC(B;{;0}oaZ6AB8s9YQ7H|Tc9g8rqH#4ejiN$EWkj88gfg

    oW$1!8?BrhjHUh$i}aHbi-0KW$n25J*;!JI=&=Thh>3_GAP!`19#BOE5ATRXj*zM$0!+WK%7C7a;)Kt5H2+POrK5`f=ggioE>Vtkh ze3G$L7C_qF`b5+-V6{7-JpGQ*XAI0p%)H`s&l{FdVLuO0&_>eF#;i9p|t?o=9z1&>ehfy<56t!i8_eoxlhe}YxJc=eJL9W zu7|&KN=v$dn(gMCp$9#b`7F~qAjE0HqX3GAIdVCls)lLmJ}2^qMj*}0;^#7zricko z*Ea^zhKJ%08!1FrQp-0*9WKZF;pqp1g;9jR;7hiO!$f=mUgeMM}EXD{sU z6}n=+xe5rne~Z=$9|30$3ZL`!ssMh%xR=$%>c}U1f0o0cvU}qe8Ms3ejONv2Jun&9HBhG7w6`goY@M_{+1lj4j%%pm#v)=LJ)+8@s44l zdN_5wiKB3_y<7IYFBySM*Vx)WTCR>a6{UrK3z|{D+BdoDP--yZ>u}z6?7Y3lvfYCi#~?_NLJ{nq@yUksixs06cb|Ll)r?*#SH63#4eGeS3Qwp&JJRbOcoZguSu zlb1ph@Y1CKtkacM9nSAqR7i8I1GVqxmRk1rfSLNIw0D_$pmIv8hODC+@~(o@P+oCW z2XrLb=en`@)$qSCGAxDQcA+2k25;ReR5cHi^MoV59jJp3DvM>yl15;YZ1@NNeVL$1 zsfyoZ%T{FJT|{aGc9pLzitm@Ou9&+Ob#OC_wSCojA_(ej$i}Mwf!mxv8rF?>fhfPJ zHpj*;Q2ZA&JAA$sd;-p_d^khogc~wlxkg`fEITdROc!rm1(Th$*4cfe#D-}ybEC6q zmZ~dZ;A0lgxr&WJ{H4RTTG=_UF;<{|-Y6eTE5^FC$*CY}-SuvpR9AJN zMwM59k0vc?_eGQeSI6NGq4yRk>k#&R&UJyo!F2fW(k%v`=@Ova#*q+5GY0JKQ(iWG zuf#UrofQbKkD%_O(6~ddhA@W@FM%503IE#zUC`bOV<0x23}1_WtJzaH1b*pH9d<5i zfY_@XTJ}xVAbk_VHLHLf$Y`~l159f!F(22v$H-N1ZDh%j~_4?O#kgPZ8=gUvfOSXB_Z;Z4=uH(v*JV4z+3yTNi_;IT?q#UiLsm7a`f&!`)E*;|VqV zJL6LgVrrbL^WVMZ;;zpXWqodc%7cx?VjYMfbBl$v#e8 z90I+^X4ko?)WDzNHj_sq>S3;LZbNS8b|mK}>w}49HOaFr`jtRt?!&k=^9XQK&vV09 zA`#989B3VqBI!Z*E^p~nBZzZrKRq`$4C9~P5|En9hDUm9*Kti*A-ep#?Mi6IsC+5W ze2UEe?R^gdwoL%#IPKxIKl8Bbr3_s(>6#!`!XWAE=rEXVP9eyd)k43`XLz%uE8&pK zv5N$(4pOi#e^8iq6&D@NpTGS8BYtcN9tT{OKdZU`e>*R4T;B{lGNAMJmUTB2_^R5c zYTF0ms}gd(?JA(~2y5;oeO1Ip+^hQ58r;nkqARJX^}epof??R4ZGC@(&^UGV6*m9n zz5Z)6WVQEWIk1oc;?Jzle0!g?I&EoUmGzTGf~q4~bV<0B)mH_D8)eVR`#$^|oFA(U zvzj5oC!Qy?KZOm!vh%0T-277odW}vqJ`Ed%na?G3d&aH;vE$*j+%4M?aUOH)nl<`T zULR24H_dT(5wxaYui87gh0}@m-Cq_ zIO_E=XyR)>luRr9cm$)59K_wtSRnDG=s+|n%{#0`R%{z z{&YY|+E87c$DJ@LrTr`ZO*e34et0|dYa0+tJagv6q%xvh8Wq#OhA-vy0fn#lxavIP zBO>_H=|wkUQv>N+TjNbf3Sfg-<{wGg5ioOsKTLeS2K0qL^Sj1g351KaE7H@Xkyt&m z-F0hl(XR}&SkG5B(+xwKDEpus%d1o!c(#1t@5-x&>@)+&kIy|I!j(hWW={`f4S6FR z7`7b=K58&Yzs4WXuMC7s*jR@?bU+XJXN!7SOCZ5dME_*y2;h8PB`x(U9~2_B*FX04 zfmkQTL3p7X_=IE~-ZdwOXxa(dKcib67wA_8Vque-k(K$-@ZYl zfc~nESkC!OXj$-wzutZf>S=X6m-sA+_!&>oY+1t>{hpwd&`Ok(%7u|E9_i0}iEwHw zA4byg7ch+*ygyb3fkyES{T7A}7{V=@5m`D2Y-(?OfA`uPF$!==99`qT=$8Ph?K^L_ zGFHKa$vr6=)r%Cq*bI8y!|bwbxR92SdAq3&2%VT@eiY_RIV-mLT9JI@7$$|!f z9E&qf10dw+PTu5&PH1ekTkz5d4|46{T{|YaG4dl|@&t?q_Do)vc;a;@I3m~5`}Elo zJRcDG=wabkAQ^2otSOhy2F#+B(El@V5ra0WU7J8_%UJ7+11$iczHo7^} zBj02;OdgFcqq8 z^Vo3^?$Q0@mf_%lJo*`#Ub|+3pggLe=zLt)nWhlb18a=`kss%XFw?`{`y^?@W&DDAer*Kjo8p3iS}AOT&I?-I{@nevV-xQhIX}Yd2+?wTZXa{&n@GU2OP43M_Dk$1_Id`Ox9&Cn;A1@n?m2^Jy?(3P;;ix z%&{7N*m>oZ5Eud0tzqHx7L`Edt%g#dHy5@_w%=fv`uOUALjzxREH68#T>(a0=^js$ zE~t(3Kga7fj>D>g;y=o(^k~_^0qE3VT{P*D4|52+L!a$8L-fydocZU#l)!@F z3Smkjo-@WI{DcU2eNGr}7q5X0w>oGnOqmVosxB*U!EGb$t-I$KBsP01P^=j^$yO| k18m8By$0#rjQhzTK+|M|@U2^acVyXtdH+46G)Ce6FN^`QivR!s delta 15900 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 Uq2BhUB_p9~^Oh3&ubP(s0|V?-tpET3 diff --git a/tests/regression_tests/surface_source_write/case-11/results_true.dat b/tests/regression_tests/surface_source_write/case-11/results_true.dat index 8979eb554..d4d1d1e5a 100644 --- a/tests/regression_tests/surface_source_write/case-11/results_true.dat +++ b/tests/regression_tests/surface_source_write/case-11/results_true.dat @@ -1,2 +1,2 @@ k-combined: -5.169123E-02 9.144814E-03 +5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/surface_source_write/case-11/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-11/surface_source_true.h5 index 0edb06c88e9a4ba368a72bb0a16b698553dee244..b5b6036201bede37dac2749563dcd68620584009 100644 GIT binary patch delta 1887 zcmZ9Mc|4SP9LLQebC4W^bu3zGRGY;*qu(=<=7nD6$|jO?T^m-Hb{8q7EsGvfvt5YF zhNeSN{DxdvOU@A;j+!;OCQS{k%zB3LdNt2Kzt`{gyngTJ^Z9nEq zgb~Pkatn{%o%4i10=|ig&Mwm;_M0?^-_cS?#^DOl^CVq@SS&^#keCYVJ(p`C9UIWg zBn`=#0hx(U&_^T5I+AS~nTcyb+sO+T1oOKWs4Zn-w?b75jir2tn?UmxPdF4N{=O4R zp)OpeQ<=D2G>)q4IsY}^MOo5?ams$MD`Ofi>`9zGefS?~I!D3N{XMf=F7^RWgX)x? z2tF8X`Y5*Zdk&wxix{_x^hPfGH@xOOsJeSY$C zd1t~B*SIhJullpg5Z$hQOBOmA4OO^#PVI*dm)x|$0l zT&$LiMJ{1+1)s!LO7$~Q1&E@zQ^>6 zHc3EYiEho_TwfGWBoTSY$i06vhhBO>ENx4~ATVy?L6v)JYK z7O))?9G>hSfOYG85evUINIPw&a9chL5nM0|b~(OFiVcuayGs9qyPrb?49H{c>UN)5 z!VlP;YMkMBK|Q5`;lNGh(B9{ATZG;OoZ8W!f)6S|3Nm)*-)2r>MZg-0D4C%|RW(fm zw^_*haLcs3{28Q_aq&$jcz_*2-I;fV2evg@-K(#<1?scX(?aYbkw+)LkBzkzN@s{H zqpjuNU&e(55&Rq!K2pU}Ko(w;5dUhw!pcN%$h=U?B3$Bu>-xbzSQYT#Y;Ux>i9!ze zkQti8(wWAzVf`eFEzqg|e3bG`YL%m$%F%DzE&z{6_)asQZ1^B3q~USmBseD!UhGY7 z04|v}uGM?#jPMb0iuqdM(tm@BdA7MPiu#X7CnDLo-1fUS-b1^0fw|P6F_6!D8*Q)K z3Ea(aR^F$3K-8{l9b57bfR{xyqLUjLpev{}tSi-IOuJ^RTdg}R0E=ch8ssyqUpwX? zHM0E98LeLM>K~%x3d2UgHRPgkYU99-5@f)&bYyH&Ec~s(MjoA1mIR_FnQ&hsDFtt| z40-LhFNTma0!Zhx{iF5x@Yu>ypM=hwr5JRh_cjg|kAsHvYF1YZ#_V)?T(_GiSjc}Wa<)g_fID}D|>9@?p0zWX(N)nd7g$t{9U2Gjg4tNGyI zrN?2LghL=1-m*IB)Q-?P1YH0C delta 2822 zcmZvec|4SB8^>piWrk$QdXg;(bq-1yr2Cn?UP3BDC0nPom*td5l4Pl*Je4JBDoxrL zSqt4$O%madElXw?g9+Is$=l3$eBR@8o`0^-eLv6r{rs-qbzS#NhF;97D|#YC)ocS_ z0!beyeDrgY2|fgo3|aJtxQghGr2W1*DiYy%JPS=Gsu9lMaA+aXR-)Kxp$bxKf!-!6 z3HCHewjzf#(SxMLf@KWJ7SBX$NWueyzg8pg};9PG#a z^DO#+9oM03Ooa)RGIP$BDfGa$b2ZIYK^#CM+dmu))Im!0>)mz6htXps0aj`rmgbsa zZqYUb-E-b3=6;tzgs+LnLQdFWMS2P>VlBGQ^XP|a1uCySJwL(POS+Gb1SW$fx#wA5 zBBoIw`L9@Y!mzcaswk4w8usk<%_&SZqeEA-hxe`%co)gwtV|jq{+jx!(5+lRk?K*D zzb8(iTnI`q7n;DYg*5s~QrVRAHS}WKem0CB&!Oz~nSg~G-e038^Z^<2Ut-qBT7i$4 zp812EX88hL%8cZu79mi|~wRkpzZto5*NxS78MgbVIk8>bsl;9pG_P*fJ417f?s~cJwN> zfY68<>jjh~F#YXD0rc)d+ohD8e>piHYi|DL9CRwcOkilWpHEyLg=1{q*d4AV-2fwX z9BU5ecLM_t*~+8-9biwmy>!j-D`0JSerT?uI6zraivPwfu_W&`8ycSD4h_&>`Bc^vc$@QkUe9J0JSwM9^YH@*Xx5)%ijq3OeN)Q}rdK-B zWUl|FyK@VAMq1G9q}dXQ&PJ>>mSwMg+o#6{KI?UEc#9H{N*g1*Ye6Sikyv!|TQCWd9n4chDiJV~UaJr+BBl1Hq zz3!3=Gf4ztxm|XU8L9-X@jVYKiu<6-^k85|VV3% zhj3f^ft;Gi64&+?c;ad7pwgwk!I_{9{Y?{RVP=*~ie~Qs{8H;K=I0iUY&w6!;34{w1EKSrS87eF<`h) z%YS9^2;W$KkE;KpPB>hLrR~%Wtx7Q1o^^l54bQ;pciLYmh8i^h6Il)sD;D1Y@iLR` zT?!w;YJH1H56whyH2qw~DiR)`RH7o^CUrqG6^gE-SN66*-71m%)7iDaWZ=N$@-PIC)Fs_+4+G?Bi|8b=xOBTi>4}5spEVIe{L;i zy~bvvw{0S|W1GaB$Hb<;9%qkpA@|}1V>cBZ7%>absV`#z_4L>(b?0h$<*XygV6g%c zLZ)rr`XiEmeetpDXR%oD%x&oNvEl&Xn#ibjB2P9Z^S}!2Hp!g^JWyU1lwD`{4$Rm; z6|o3m!w9eEBTJhYNYbuO)2l&SFsvQMn~for7Ic~h0hz@j$F3{$l9O;q+R>E1Gy0xE zYL%y@+QetrN?rXVyZa05Ka_Z#6IzXosH*yxY+_>XNzJ7yi(boS+2|hS(U3vL`^m<46rIG{_k5rE2XdWqcvlH0KA3gaG1AIuLpCl z>u~_tY&yxrbIuHed>*O)5?cbEH|CvRlH3EXx{%ISX>p)z3X{s%x*L(Sdh#GOsg{rB zx3Pwxd8Dt|w1yQ+FcK*Xrz+7-DDK5N{-~q{dPU-nrd1Vzdm8IbrcwKVld{H=pZZCN zZ29yJce7b-Sjcsne9epzN2b(j0&OO7h;W~X%oH7=n;3Ni^Yl3Q0?&pgs-SM- zqE@K-uFs63q(zY;eM<93O0kqTVZ7OBl<26-P@jUcIL5niUt?P51l;3l5MM~Z7hMU{7A2cO~=#6t8e}-PK}BIf>GPO~uCebzk%=W$$^|{{aQqMX&$> diff --git a/tests/regression_tests/surface_source_write/case-12/results_true.dat b/tests/regression_tests/surface_source_write/case-12/results_true.dat index d793a7e42..ad927bdf3 100644 --- a/tests/regression_tests/surface_source_write/case-12/results_true.dat +++ b/tests/regression_tests/surface_source_write/case-12/results_true.dat @@ -1,2 +1,2 @@ k-combined: -4.752377E-02 6.773395E-03 +4.929000E-02 8.212396E-03 diff --git a/tests/regression_tests/surface_source_write/case-12/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-12/surface_source_true.h5 index ea81a8250956ba2f3da07bde3332e13963cd26d7..ca5eda8e2f347a1fc88b05db6690ad5b6d3a2a51 100644 GIT binary patch literal 33344 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 MnYIUOOW3*p1=$pDP5=M^ literal 33344 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 S)+dnMzTq}Zl@F~h?f(H)kZg?r diff --git a/tests/regression_tests/surface_source_write/case-13/results_true.dat b/tests/regression_tests/surface_source_write/case-13/results_true.dat index d793a7e42..ad927bdf3 100644 --- a/tests/regression_tests/surface_source_write/case-13/results_true.dat +++ b/tests/regression_tests/surface_source_write/case-13/results_true.dat @@ -1,2 +1,2 @@ k-combined: -4.752377E-02 6.773395E-03 +4.929000E-02 8.212396E-03 diff --git a/tests/regression_tests/surface_source_write/case-13/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-13/surface_source_true.h5 index 08bd809852152200e5449745616feef691f46778..ca5eda8e2f347a1fc88b05db6690ad5b6d3a2a51 100644 GIT binary patch literal 33344 zcmeI52{={X|L;xd6d{F5W*MT)l4sPktBq5%rehp z$UJk*5~a(RecJo{zJB+ApQrEldG2%X>FKPs_t|H?&gZi}YuxX1+|t+9U9@n;Lb~Y( ziA2Xl$MIJdd-^r|K(5c6!H%)lEoU?Iv#Hx`8q6@0p{HY{n|^KC?0Jsa@&y@ZP8iRg z-)5?#LpOCLRz8-_zD&1l{!EU9o%sLh|5F~&*FpaZm0hz|WAT(d8)uj^&YV1ZdjCNS zYpWB-{yK%lC4yb>f0!dyL9Zwy^ieEC7VV?Sr@n=5Q z^zppE&OM%ufBM;$K~Fz(rS$9@u#}NbmT~$N_f+5?o=Q(o{r{glxTngW_L0N?;X+<=AmUqx`vM)~x`wURl9w{9`grk^SS zi~Uox?G?}a|M~yD2PpGT3VibX1_bY(sk6zMfHUtPkz-S~-_ki*3R)kDP`DSif_c25 z2?HE$@N53$osJ?Ilw@aoP)AN&bLRSfFr8v`F+Dfwn(x{YjYfWIQaY9WOV zxZ6{-)K7OU+OW)pZ(Z-)_RO@$8T+Q6EsbOS_|nFxoQ#|FSpjuGRe#7bt!*R+4HingrQtfee z9BcWJ@eSUKE>aAiXa&5Yi#SToHNyDIZ5Q-De1*+9T8ze(YG|fg$N@GEfjQ$~cKV57 z{;@c~VjKhgQ+`{30cO*GiZipY_znA~I4r*rf93uu4l50(Ya)CSo@oJPmp6XyDI13~ zFO$eCZ1=aX+g=2>w2$1j?xFy9-b4GCD_+4ie%%7``CKS}yWf^-KPf`IVRb* zv~#93xqKU@#&!C2^{2kC=*Uc2kLVL5+CUa^>b&8qMp)0$YPhxgD^z%QM8vyI4ZUZv zDQUi=Ac2FmnQ0Ci4dra1QA9E(f4;kv91=v4|mQlt_e+_I?;$QQUC z?fmi?%2)_q@bL`=R=F-wm&cV*(kX^vgUGo#-1y?D`9E(D54CK3=FfAMj#^SFBi{uQl%yxv>T#OhZ$JFhvhkTeZ;t@JcxwL7 za|Een<1>GrBZM!WnkOM|ifq)*2lYW~iHA8q57Hwm6vc|ZcJ~1GTHh58q18~Q+w0W? zb37!U3S^WKkVl0aQMN{D;`#?)Kf=_q@fi}bxL}`f;cyAm{PC>p)h>F3`^E#G_vRE3 zay?mqN4EsH8j6`1=%s)`(M)>JtLxBp;XlJ+_}u-Q$ZYY%b;FG3I@4UjtApcTY{st%Ca!$aKBWlu;>1{p$VM zs|g&etxV5Dq~_q92#BgZY2F3yS*zyV_wRs4mpXUHF*m|)_`+D~O*^dG$j6XrR}BN= z7C5yVilI+VdTw6mJU2&r4vueqs!wY{KUip{{uXI%g*Q6A)(S1|gXI3jWMOt+U|zwn z4Y(A*%m&5QB|8C1y46&e$09kWJ!}3Y&RQDIO*wv(yLU(6%xMy08MJ{=hlErk&eg|yWO z9DKZC`@GmW%#Pb#!U@uk8^KqDBFbLUC~Sz@de%w36L44VU47pu1FEKSUC!)pf~gOK z?Pa*uq5Pud_F(J#(@2ooN;xJ3V#=ffW41Vv{9LxqksVjDTyp~r@&}5d=U7w zX)k}5*b1HMYUK6oy5QZlLmH|aji97Loij&13ua}$L;HEwp&}BJ{>%2v9oI^P^05OF z`A@eWWvuAg^l)0zk)O*Mke43Su-x_c{$-ubYLbUV8@L?)(@SbR9LShAyFV&ugE!O7 zK14hgLRH(EPrfc)M~F9kKe}$Vd~B^hb4VgHe30`RbR7jpSg!{hTmBBRgtbK*hW5eT z%xtnu&13-NNRnGxBH*sp6=L4o0oqUYS@|Sgg22J|Z`eHuTF#&L+j<(mVfOrKj_Mqo z%hfJRdA1LL14)aE?%H%g6kDu7Uk~tZl3i?M-wh9C8}H{hPl2(_?9ZKsU&6O?2a>njm%?nh zeh zY&afS6PJCDfn0whzOV5C1(>~9ZscB+0`5B$N4ohG!G$r&NeLRe&=2K4$IAwO`)#`I z;QfZo60rVy=8%M3*-ddfm)#1-FEm~)Q6GnI0?E=Lp?zRfP9@K-G7+j&7HbU-#DdN7 zpX$#n+J&0SQ^Z#WI(Ae%*f-n^Y{i-j;@jJRO*yDycV3G+-?`*|BAsK-xYnM7^QYU7 z&K#UTzeTr?p6v&PF<0jc_t25`c|S-CWHbRwuY8~Q&UO&LL||ack5VYH8PRdK;zU;` z_V=q;5a&DiIMAJg^QZ06`4mpGgLCC=a9?c{)6KK?5brnr z`F>~1zvMTgIXGTRnD&6%32;xgo7ZM^7;av)B0qMp2Xt1y*scF52gIusa0zBrg2@fd z%daimg}yW$7m{*bG^gKaH5IN#iJH5`D8T>qTh(C7JY?o$kjVW7IafBw<^mJPLcZ+o zc5rpxfbTPr7C4e&x9jQ(Lsa#3WlxOwG6DxbA2p$|=Q+1ZAVr9QJX=1J8B2O3@`Hi= zvOkbFbg#UT4xS|<=uY_8gMtQx*l6H80qya|=R zD(Tl+vV2Z^w$X6@DSNilaApokh|gg3xc8Daz@Q%V9+sDYIDbm{k-#2c@VN8+HIX=Q zt7*Zes+Jl+`LU_wd%QlXxsQEYWHT`i@Z--88qPmu&(42|vx|namRs-Wikb=V`2Jbm ziIWppJditA=4gwJHv`KB866?(tAK1#&mz&Ut#DMzqhQDNY@{^ff_p@kEFli??PoU) z=k3L>j0>Jq?*}9AZ@(=&>Gcb~sPqm|60Qb2Mi2HU^7Mk^19s4BECz~_Eo_#b--M1X zxSH}|oQJ@{w;wYa&OcQ@d;TSkISt2+#G}yX982A&Fuqq(@BEN~yk5mXOEaVswtQt? z91`^vWUX0s_f|m#Xod4!76$91pO7Dt2j1|_>9@T!9KY?u4X&Gi?~76#`YAg#MquDM z^7>5IBJk+NGBl)(0z0m@Eb**+1}X!>Zk_$8i^?R#?q~6y`+o2~8qPmeKNkNI$C8G_ z6()N0Y9|G2H%RqUzMcwhCUW7RuVuuld@%2=(W0Q8Js_PY%+$B#3*=^bEgA6S$)qhFiSr6Gd5BxO_!naNg;;-pmt zW#AaI*JIul6i`^aq-~@o4nF4RjxM>m33U<(d%=8X?r~v14dbODodlhOr`Qh>g)nY^Tq7^*}rwO!e#L})+w zae-FPmwz$%`P;?!us9&ITAm$0zWgIRTw-mqd`}HnHQ{)z;$08yKDJnXxor~YNN~^_ z=vjf*lT?!FxAPM?Se;MrQ_$-9k~9NFMl>0a7cLWnW-^1+e!=a%?Ln_AwSe)7PVJ5h znLs>5Sh!?a4-j0qIBWbIFY3*H#KnV|bQt#2Os)w(!e3q=X`;QcnepYWd#64Di3 z%)8399_B^&u_1bguHgXM{Kk&qRwRZBCRzrsAPPC2;@Mnw9Dvu$@$FE|&ajdbZ|0;e+! zjfEv;AiCHvJ1xyCBb_NrM-Yy{F^0pvDR{`bxaX4>Z5^>)j zA8%M5tDi^%)?$rUL%^;2*naLVs=gDk4VN=D8}5SX@f_b)3B^IXbzH9{CK`a)w=45* zNT(vE%SHln?2QTbU}aDHjkvy=(vRNuU}`aH#_S-wuc)jw=NN|P6pRGqK0krJyk9hA zLMc<}^^_0mN`PQn4#URnC|c(cy*qYf4S|Ez#WV-IAA`lgS#2}7JflIN8WEfG#dLtm zOY`0OLA~odaF+aDd)1@~y78$+XQ_9==+G-s`P@qAi3Jl2OFfA97qB{+=3sh0tp9mB zK=G>!oYJxU^&pX}FYQv?G0_XQeHM@Lu=)y@F0kV$XzhmbYudHUhmt|4luk?fJY#fQ zGuzgt^&)fHL#yXg{kc6T#Hj_$auBBv8}_SY_kv_JO#E_a7EG^cJ>77i4>Y^oF52TB z0ju<%X>4dVNAnL~aTI^Kl)%AadDEaq3OJq-&#U0UgkZeNwsgHMOH$yu z_^_kR*$u$U&e8HpM;EZqWAd#1C68`f`FKlj#@z8ntLJ;Gz5biBSq)TT997nDU?5|h zLIY1aY5$jCX8`Z>E19j(&T?2z;dnc6`RdBle{(*1y|YJNWsMfW9(=sf>iH(Wd*52$ zG)%1@c5}0zi^T^)9QtW8F6}dPHK|V)bLoX|-Yq$JhOHcMR;D_xKC=mpjL0M(cO%9D zKHe}r9~N)&gY!q(J*o3|B)C}PNPB5N-12svGqSb;_!}qK?)7U0%=s}|_0o+nMp^&W z@OcR|=6P@UURUCNJidM~J>QIfuxAo-q9%MZv-A+y$}3y3O>Z17RoIX_bh{Je^`vPi zT#SWf{l4Sc?9K4BhpNn^hX9IZ6|0Le$`kyC@84+ke9On31gnnFA?Lo-#~dvz!u*GL zP#U^2R#gFd{WDx6jjeE%iudiG6V2fLu`LD)$z3$o*GLO6}`#PJ($xe7I-h00Ba10;``Yj-9G{d|&hb$kBZK%n8%bRapiTjpV zT};O{rsu=X@y(ZVLGw#NkIzSss-6a{9g#1+d`rqt_y{XkDhkC+kfE`Md%zE$J{Tw# zH2J|?AI&X)dgr>bD8U|le=wJxZ)k)Ie7p7?)^1sHcCdsQ;XzK^6}N2!o|S!vHJ>*@ z2|I1Uw5`pc;=$#cUT-W=wl;Bcvn_ERkFOtEJ>L$_TTk=Nn2=+oal&zGRNX%k5ZXXF z^`H)X*Im6i)IAN}y3cBmH`EOhHjvm4RH>nY#zyCURTAS3Uq8hC{@?L-d)scwdGzH# zC41civc@1>^Y)BB>BU#@^jhD7QnxP9P}AmCc&P*q#`Um0csURC7GgWmxt3Tzczc#( zMH5mI;%Y_Cl`!uFN)kL`OlKN_V5GD%XFUZx^4;N>$(8^c8pZkAISWC<+OB~GwxVd4 zjn>{bM z1s;$x=vrn+k2q+y#8&U6K#0V&v|oA-t%iE8PI8w)tsA2)99<6)i8|ki#(VYzzhP~1 zx}EZ4Cd{S{zmA^TQ1%@@+Z&+DqLz;N4OtykSC!LW42^^Gw}_jRLp_#t#^VJ&VBf$V zPC(v{?tWs>ao3%wf5XSM04=A*vvK3ePgp;I)Y*PA4!-;a9O3-#W7bv4`tYh%04bk!@N0e4N)mVPI;AAEl;NW+;qB$1zrY$$oMq!C12xS$(l-vMnR zPRKY<>6~=f1y#P?&4LXjCJL1PBDm>TZa_nm3i{M)cYx+gVqD|>CPc$2e_p|A)jSSZ z)Sruf&!T>>0C9b`Z|U%xB6!wmd)LYA_fX6EKD%Qc1zcO3UH9Hv2vrduY;fq2Bh(Mx zZ^AU3I)B&w!Xd*j+$1%WQudizKb2|HlEItaft}utp8E_c;b5|;rhGy#I4v^j@Wql5 z4O6un=V&4B!{Rw2G@MhPT~f-!x?sYnf}LC`GcxlsiF|DRkuAotX)t%Rck`jGF98Re z581`H55^p3m#cash%Sq8Iv**cNU#Urene?FRS(zmN>1G$9RMud!`^+r?WxwO>jT-4 z-twi5Yg`p9)!9G5+uQ@LI@G?jaWh60-=5pDCX1+t#oM!LmV?!qtoXv1f})>T`D9kH z>`fg`-$49jk+>kHM&M3nu)_-w6d0F-y^_t#Es$F}3<*hlnvl6D3KREoFzYR8QG8jHL z+6=!kHLrcbjiNX2{1jt&MT|E*M`D(Pt$*Sq)~+k=rq16H*4oo;$A*8vA60isj6)-U zBppm>desa#tFQR4l<$N$&AB^RJq^*Y@)`nb{jW&8}Gyt1-1LXVO-CzTM)VzpFEp%t0VYZ^g zT0&go<4uZ&Gjm8Hcf2h3|K=Y;ZzdhZyD$prTmV-!D4v2zoyjy3{wBV%F5ZQi?l1;KT{2Ctb{!cUoW2l5_tf}8D29tQgkBE}!O&4y9|A+GWB zH$}{0vnf(F2m{4CKyA2Ys;-TZ6V&u=PKBD1 zsO*T@swYc{x=wsM#dL$%{z1Zo-$AKejEMR$r+Sp`2P((-`=GcOYX?{-@ce?RO%B|r zIC{Q#Q47%0w0`}vUmwkW)RD&LOzf}m99rEVRzKrxKM!wW?*?O52Tw<7Re>z26C1sJ zyPT;Ajpuv*yyt^kd%7y=|wsUSjo?tqk$&%Q0?+@7o4CKI)`EwI)Fi z8OJHT@&2Tgp$QfA#p7+`{*J_bF?<}%8cH;0>VAyusM`<84gH|%tDov3W_of5N#!J~ zP8(1dv$9O=h#b59gWePCOd}TJafSLUCM@XD~LOV#-xBPPNYa<9;X3`gwUkSCu-8OceH$a0- zyxHWUxCk7)JzDtUr}CO(DcOrz2EZ8mtvAAUzvrXJH#*rG$J~Q^#)tLaJ?;g??;Bp5 z8CAh2?H3LNE<;cky`n%3SK{~U@%C(>;kbNTTSEz^-j5;MEI51lZhH}Yy2v_R`#?3c z^Xe9RBGe0ABd*k4DS8RD)~5P42J)aM&df_ba!eALDQ@Pz8@`=t&vLMHU81h`*A#le z5d4O> zM~B9qnL`pn-nCx$!_n`+cIUMt3v7OZwS5NBE}7kc;cKX$s{U7Sq_BQt=%zI260nka zTec5UebfE$lGft{4!(YLX*g9Uax#+F(IK-uvWD&1*Pa5y@Ww#e)lawDfWPe_roN~) zX#DzXLX!APFtmP4QKdd7YHs+*pp%I>&S5^9?%(uiIG$BpBHP$Izy^b}-G0dLxb|-} z>M$Zd0EzpDoo?zkL8T4{`%$C|o(f|WFXAMjCrsHh)llO2gZG>MEC=haXYD}JFGLJ= z?@od#aQtznD>Ip^NlC+`xf_(d;xAtECLP4TVr9Jfst!hNAK;Orh@oQ(dG6LV3(e`b zjWqTc1_~V%8IFNTPwT6gPF2q~V-h)HBEP{hvkh?XM?KAU zDD|d4M0sf{Sl?q_Y3b4mz1+o4zTB>d+V6Ycx^&axIqjj<^X;~0AvG8L0%z`rD8^6C zb7$*|{NsG=>O=iaz}!q>qhhfyus>*E)t}V~8{^{dyUB^5nGM@2Y$|x?ve` zTrhWkO{?b<^~EWB@J z9IuDQ_#~D19U*e?aX_o*6Ld;hYkO$~(Df+uTyBe@`mH&ecOaKJ3oxFHE3+{#1>c5C z`OAiSVKSXSelE|4 zaju48r;9!UuS`a-((e?Q-o7JA;E@)3wrt_muF1LW!SsAsJ1rWzaqYPG@A2S3$^^$c zG81wo)oB@h$ys38-;>|b(FBuh;x0QEbwc@$2m0n)R-?yX2Yotvia6iF_dB$DK2m)N zQ%N8*G8<<|@W-q{6Nhdn;o((;`qY5vkrcDbRo&pR`Y6X`-vS_yE=m>PwM5VJxso)h zh~pf-{m|<9u<~6U4^>>^tb^B5eY~|dwLmYf9=b!9`+(M_B+BrLSeST^KRcx@4Omu| zCYYE?p|M|9_4qgv$2oj|O{?e2-Ke@!)`Af!^vk3V2%xSX$=_~SbcH`}0*}5Sg%CEw zbfy@#?tuoV+9p)Bq-i~BC1SR9zxawd+Yhat?^IZmh`ehXFpqvgACvUEzrOcD;>G2A zG4MK%C>{IWLa@_u(fDu|1uC*mT$cYJfF>lUzDpdLTkk@v=fnIKHRfKBb$0@0etdOD z;^hR~`s<9%h)5?;U|#EdBC{D@N`mK3S^L9iBPP2CEyAdNc)!yFA>w{7-fy&ezHJ8< z2@FS9L3z6)(ziNCvG#)mct4jGu`7qU^9#J^zsrWwJFE_I+~@_Dn(UJdBlS?;Q?{SQ zAn`s1ew~Hs`LMW-6Z(9cPVzgnZXGv^FQ1Ra9a;4JvqHv&*;C(}394Ql-wlFJz1qa< zUk8PdT=&rc5%kog$V=oLaXp7`r?h%L%x|~H-^zzPo=1*d&J~-^+5jUqbdrUqbSELB zY322$r7*WPX}_*{C9rR7m9Vr}jvSTP_GGEwV?rF@{YIn-Lz9YDfgzS!YqGx!#~E+~352bdV=WSwx)LR@d?AJ%ldHYbNx&$rj(#l;i1 zM}UFuj@PVxjbN5bu1H+_X~O+2yt-7?;pUze5P7?o#n-O`o;dI&;uo7Py7E|#xyEi;jG%J)Bi`CwSPaplivOYu^vG0%dCn zPhRI1`0{#*K;o3n!9tWl={Eh`{ph|K$Iv{J$PhXAs>g^9dD5eG|JMy>vef&fEKKg* zuq9&gkD#DD;BFo5k)c}$ZKW9A7QQ)xXiV@#DgC%gs2}`#&Vq)(VqQ}1EY}P>)~NP; zx6GjW&3jqxqb{BXNVhaC>Vt1HWUFyY%DB-CH!iuTV`Z`seQP=v>nTNC&*A-MNyC{r zB$01QuH?Am#(?-|$+g<$4nVLr?P(qh1wIn@G<41_1v&GKz3Mlmfgc*L(@+`xtn?S<`Uj`9{y56)6D;hZQ9E@iHNpeI2*I4{WhYgk6lBT%93p zU{85s;a#IP*jMYWdaYPx>ie26GS1E?>PPW@+fT!pEf=}WL`eW@P7wV`hF=6bOF9_Xh`PINiHrqK6WG zud@;(!pfcEHQO+@r0}G(+x`v1{3^>2U1<`$H{53TXJp`H?wii5&d6 zjqQhFxJ`a^d}Q4ID?^q<>-@kzaC@IUpJC{ysr}ajf#ZdJkm>Oi;d=gK&@gB`xmQFH zZGR9e@1RN4AK*C*7+^MK^xD13@mvYDeU`AiUmeRi5AiUgw{R880DjuBW<74b5G~3( zW`|S&tXeT;EyK~fJe%E(u`?;AGROBny z0m4^2Q}C^<>ck3IG4f!D0gA z%%miwV)C=r)@O}SPSCRHvBem;H^S{);N1y?W%HjtZK;Gy`jk)QiG2lC&HQ4+CcLQB zuaaw=X2f+PR`&F|5%VF&*`!;=9CYCKJaM^r@M{Y@3RqFIjNBMq2+!SnwSJ{81r|w{ zafBu1KzWI>R8PLOD5<#g$jHD$CpECw8RThrmTwH`*~8SQM$?gj@>@QQ2=TaL!c zgjg#bC+ZKdvZw2Z=r1}t{##YL8`>EWvlGhBym9no%)Up*os2v~y1>{`gZY%V6;QNt zyRX6O8ZgupXTUvwDQefSe>B?|A@~i?VIewf_K1XZoVon*2tx{pRK0I=cpE+Ppf!FO zZ)`L8xN2bXi$W)Gd6!uA$ow8~ZU5vgCZd2=Z(bN%?>Kk;V0u0bcdGG1YL*@oGILBK zpMG$i+tA@BNO9dRb5W=iI4v;!c1MQ-g-!2!7;=!|j_sCSKL1#EqpdFLsK~gLv3YI|t)36#G}y;g@NhCu{r<|rmbR;v z;K}=r>RXd}AX-dB$CV=;swf68WJ{yKhOZVMHugM4GB4$DAHQ~l5N~*UFg@Q)`PefF z*?*_eEwF0>K6p9KqL|nX_>w0XHTU!Y_bZ<+ud*nHuTs3bm=;#S_HLgA^{d5Ex@*Pg zipjZkHFN3t!nZFgZMZc8z1ef*9lJ(>Gyf8&%7iK1mW^dlheip^BtI`&tx^rhyhm^6 z$e5rTUkk+XT_?6vygjsfKFn|YoaVviizu+i2Tpv-?F4H{T9hKwJ`m0j?$FFfhKDE0 z<=(wa2Yu(S)N9_7MNee9iEO=2oWJ2YbLjbyYlGjf*>m^<>ti00%QM?SMQnwANJ$rn zSVJif+Yk~5cc@y>E(3d@$xs!&d;^iyy zdUuI_!*k}+*AcJKtHs5Q08#!@&Z`r;F%;a3uU+Z8_ zGuSV{{j_#@2Z#=R%Jd>S4^FPXwJV-c3k_=eu+!}wah`~c)zkeOt5aJEb zq1D;lQ9I&co63yb*RN3fB2VR!<=+awu(GNI3l;?_nt0ZM4N?b5F4Y5eW9@SZlvWO4ZQyZH^^jb{iW!|n!f zP`~5n$TK{nazX@OshmcPFZjpGVL8N~RoWEyFdA)5bT&=VsfS)551=xup0UGFaC+-@zj%pvkxyfkr~!^fK-p?vIsgbXO>N#ZlicV6TK}^2+tY$YFV@%=wG95b6id5yFZl zq*LGDeo}VwXk`O%-MPDF;g!!&n|q^_4O<_Gj;dxUl*$98EyHn%YvWa>g)owAcmn{!-t9DkB~FKEcHM82I^)$iKu9rQz&T_0s)lF@UXqkmB2wJL57sVaLQn z)bULjSRx3~%_S+f|!o#_DgnVZ_6IWY)Z?eWSk~ zBxFaL@R1`iwb1SOZu82n$DoV#qc88H9w5FXGfTul5uI-o*+Y&c{th6%{m|;)?qrfS zj_&#iU%Kmle2`jA<4E44?y;)dC0p9cIo*{^T?Pz3!kaWKYZB^+|m&Lhc;h8L%ElTqg^8~Xbf1f zS2_hz(^X@I0a z&v0?&Kv0xh8}+8(9kS{1w}J&1%?KQP|Ay(`W&_FCS=^?FM=m z``N)zEes4MwQV>Ng*;X+sa}~tT$kY652mxj_LC$ozBemOq0V~|weGa_3N1gtrgF+x z#jIwacV`iIY-~FS$~`9Rlv4!qai@DIAa>zMs{puBe zBXB0eiyxK?qdDj56JNLx?ZMBhXmxhdPA8VC_kD%x-=E)Iy5aZz;Ln*uGM#!5Ztp28 zmyCG>#EkEyTuJDI>pS2NegREXY-@qdO)28M3U3dlv%}b1&zW#bpJSrd4<$a>G@Nm& z|6wV5z0a-*J_Wuf51uUtuba0kuldpr^A_5+ej3z7_ZYO^+@(sKkK*fxR%fR$5~QT; zOh@IY@F&HIUrhmJGJS0M+Rf0SFRtZ*b|WC$=*DT5HBIRv{gXQ8twzO`Dl#eS%+0}c zc31`3DmAL>YEs~l-fPyp^pn{5gK!M3sidqag357{O7dxGK2o zKFsYNqwoPX`-P};vo-;j136BhdIQRtP;b&HI=4NvI=dH%Y_89_7?8t$o2|>Q{r;Zb zN~s$gkGD4f{zqq6Zzxqx>HAI!+urK{3ao2>`SWR_lZRG)?5HQkHQt_CL$T_aO-ba} zhhB^_I!}TglcspVM}6?My~2W+jeT&~>X~AfbUX+}LJq6PWkK>`E)XOL&) z#x)kh)ANJb3ZQZd)pI5m=1;(?Rh}2WZ2PUhZpvSL!OE%>+CB|2dfMs)g6{h%t#qP5 znVK}O+*_I`Q-G(;^(vy@@a+eo5@s^tC52^4znIB0M-x*nY=aQ5$5(b2h-D{Y&w?xDun z!u;Jk`F@VWRX&UzYF-`ScJ@e~u-X?;ZS&eWOsWB%VUGGRQ6P(sl7vUEa1!S`_;#u_ z%fXTz)$hE1w*8*x#Hy+BJT@4Fc{{tMR&9&}!yMmwJPdn4)cAq3u(8 z@?)<(dh0&q0116*pLM3%XxTmH#P)=_IodRwoSTwd42KxV80X5e8m9D1Ol0v0Kco9f zrI4h$aLdQ0R$#9;$)>B<3eBu*rB<)WMD%;(jOBv25!xx<9-UbZ7J~`~dmpWyy8nuC z7=xdjbI2HjhU`ms25pW34;+K#8a`4$M5z9gV)hyRUdVze`;KlKC0}|QNKWSJwH4?Z{oSwbE7Y$A3trW5w?tm*Q zdKk^58=&m#-P@&JRRX>*L3Sl8B+)0?S!(vniTXD@hgSbqxbEGK)oQ=vK;4-9VdxWkVHWLXXa)Qko@#CojAl68$c>9|(NqC7KdhPqSBwF1qorl>6&L+hKU7g1t>jF}%J1YnFdq zA2f`hxQ#JNoIXAU#CIs}=lEmW(`S-40nL!aUw}e*#xdS~EAEK+!u|WB-*ytAEok z_~=dw&Ii&D_Uk4u{{6cfL%Qn@eBTxW&J?IgxGzWp9pww8;@taSv+26w3*$m)=I3Oy zTn|}f=5zk%edn!2LuQYr^zlX`N&HWeK^D6U-^=!9_`PXc^kO3l_>p_x!1I1G3^bPD zA}=b1=fx`Cep#Z1wyyd~uxC1c@qWYfZ&+*;jL5|2k?4>aAPKPxx}Nohvl*&$gdFr^ zr~$g#*JXp=bc2a^FDmlx6hp>kpUYO%tDyyfzX+V^_v72?_L%~y<2DEPV?LCx3*Po! z+^)vOOrCj}M4neFcz52y8rV5R-yGlA0vUXlq|*zvK(0q$HjeHzK}TiG{d+a$)~V9! z-%juMQ2ui98_1Dhw1Me24$X2BfrNKP}O|3)V`NOHN$&EEta!VS0zF3F)I z63Cj(Tj%~=0$TmsIq!u}>?f)H8(Bsm$Skq296BpJ<<5?6f^>KJl~X3#q0ixM4}EG% z!R9M5$5LW>P%*hiBlH+?{)V6LVEVUN_tA}|E4X`dFd?)4LXP;eJt*m_03}8#z3&`a zfI*o(<;K+C0TSa9u=+x2fZBHctACsqL{luwYTUi%-cQ8zZ&)1oCz?j3eJG}m+tnYo zoZqps1ANHj9=zvY4EL~(4}2Nz1jny5qGL`CAkj|ov?m9OQdazWHj+%#^Wo!wR{uu6 zzS}iKhYl$X*rRG^LEYaW3%?ZD@XDzS!q4nzN^cs1*FL^PR0axzzFes57~A+1i_PW zJ2zTtB34Fk!dP>OIxc+sq1C^g?$xFn%WegZXXiU@awww4TUE-fIQ83A@KWpOj_cnG zprLtB%L?Zn$dXjALZ>Z?t{$NiTYjDRJ6ZVoHLd;)v!^J_Fz904Ah6$Y^6d~~H#D9v zt*Lvs3p_b>DN2W_1Qe$`zB1TR4_fz&uDr^mi(Ywg(m7m)sC&ZOvu~zgnr9MnDOV5C z-1!wg^`9E-ZdSniac^pWl~x0bYibpdDpS9|Qzq{jytxdBAX>{-{hCC2)=LEh>&$(h z)`EsGw#Ak^^Gpxu*1tMpnNQVIkiXdSwaC|(K^#zI#@m<>>&gUp)8t ze!CgUaxRwXO%_8Bn~zA9u+QBeSkZ9eT2(HczRZlwoF*aRJ3Rt!ax#)%Nve;((r5wq zem*kfoNNWUpU~!IwH2WH#-}GcyO~k@RK2_248(OKzWrF!a47oXgMqIqfXeER5>n); ze&(+SiL5}sWii+FW_U3&XouTiEEHSTk_Olo1S0O({N7joSYv})JbihD^W|Y{!^pS*oIu4EPfPr z9(1ig+%WZbS(c@|V?P}Wy^LyoCw^(6qrbWse#sEmCHVfDI6s{F`{4f&hmpn}ZqIiK zaTMx)GC5RtsO0Z5N0&tG<<6`ayxx$hIz4l_&pDf~Y6AS*^Z_0uOFR9^ayDfw_@POiDr&tHqfTiYwqPu?$oC&VJ}zc0hxFUKfa6S<3hE z#aH<8V|%)WQaO-T+AiDwB@og5puJ0c&u&5-U}aCo0mh`|cz|j<5!(Tn&c-BCoi`6T z)~w`v>S`UByqDb}HK@i*VYFW!@GE8tYJMzU7pzj|2<_Ry MnYIUOOW3*p1=$pDP5=M^ literal 33344 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 Q0?F+gZo^dh(CX6uAE<_K5&!@I diff --git a/tests/regression_tests/surface_source_write/case-14/results_true.dat b/tests/regression_tests/surface_source_write/case-14/results_true.dat index d793a7e42..ad927bdf3 100644 --- a/tests/regression_tests/surface_source_write/case-14/results_true.dat +++ b/tests/regression_tests/surface_source_write/case-14/results_true.dat @@ -1,2 +1,2 @@ k-combined: -4.752377E-02 6.773395E-03 +4.929000E-02 8.212396E-03 diff --git a/tests/regression_tests/surface_source_write/case-14/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-14/surface_source_true.h5 index 50cddcc70cd27df73f37a288d86a906d7e7694fe..c9900bd9c0469d3b171f863f0518d6c94e966027 100644 GIT binary patch literal 33344 zcmeI52{={X|L;xd6d{EoGL*T@l42eb3{etIDk>>+Bne?1v&=J* z%rnOv#Y6dHQ~z=RWtIp3Yi(pMBQrd_L>5#{E9WO?_?MMGJWs(oH`| zBswNKj=!?l)34bFa&6`ec8tAlHJhQIP2FeH5QdoyJsl(6^lQsz&vVR{FUUA^!g%)l zHd7rPx~VI%^09RGWx8eaXL2O$#Q#tKpYnjd4*FN9?3%S2i>I8~IK!NA`sA5Y`wv=L z?>lkquTxlDBG?80hw0+kc%_YNRyx_)@)yvNrvBL+IeyT=$?hOl{F12*^VENgKl8b! zkLUe$?$K=g)6cdHdit3wrDxxOrHpj4jMJyMrvm@*RC;Ra|NrE{Jyrg+j~xFe7f)TY zSP)EEG5tX36j|sLe@(sL>96CdZecc^EBU+ko}ay#*qi)k34izAGqaarb^M?2nmO}- zvyD@o-CvKHD4IR^o8<5O<8Z|GuQ*fro9pqX{_ef!4mtdF34*`yZ}NZl-UA1Z?{hkM z!1BxsQ`?mB z-&~JBg@5n7si>W9Hw+96f3+ymY&&D5x0(&-1}wb)DuQ!1%BL@`lgK^1b&HWU{Zt89 z?Ay(@S3K|k=l}N}pv*tH@}t)`Ab96=olWipoOuU{9GANNrq0PS(E3n>!o9E+%;O77 z9N=h!UkfI0cNEK@Bzxn7I&xx~GuQWn=@hGr>AAsQl{)j{U(efHbT-R#4E=T8)Rp8A z9mmd(+nLCxK36uVvDSdYjh8QO@B0j0*T3AhO1=yF>K#!YutU+lr1B)Ll|ls0biY0Q znC8$7FLeJDhc>(zYMsJOE$gZFoLwPPA{fR%{#Cp>Z>Mq%+??hGnk&>w4$5XQn;Q*f;%bWgO?vpFT$AWZt094%DM3?_VvuVKlQ7+^l-) zviW`u%-z1U+mpQ(HoB~$1SG4VGAFHH*nXUwGw(OzuiQW7H>|EO9H&fBV|e)})gBk8 zv6dg1-{9StV#SDwR=~Gv5l89SMwoD^?Y!Rmudq2+i_y4B4b5^7J;27Xa?UuIoql4N ze=H8L7{@^Wl;0L$fZ6n);>;{8e#8DL4$E)EU%7vZ!%D;HnuwT$r&~bzrH!9@%E#f% z%OoKKcW5tjXe_uo?EFGYwqtnR1#okcX9 zcFxo$*KfnrxX!qy{>1Mk9hoWnA$_8F8^}iN&Ka(5g!LS)hFiP8LWQ?SM10!R(7TqK zlIJ@K5;$0!ndY$3aQ-R3vHwdP+WDq_Vl{)T!64Odyca)4Ox|E5mtBz26^}0is?3fu zuhcr=Ru_6@UE3z8D8*EeeqJ4IudzF=`)O{!EvB*O@Wmc)=XF1+oW|E2%Y00k$ct=; z1A_yaK&NFwW(#i>blG1ivCy;~N*h_-mYy1aOi8!(CXWfvSwEb!9IQY6r|M^Vy@G%I zQ=FxE2sKYagyqAS*PZQx0nA6XpPI*rjPhQv6Wlq~ZUSu*-uR}#YbQ79>eQ5ifIBPa z?RQy+MwQ;+(~lG-_zm;XbX;>$%f@G>I1WA>i_5yZSPGu^^$P?0@?4`YjVqxfJBDF{sJS`Z_~NPgKW`5YwQPLm&vTaJi>KyENPmu2 zvd9(&^28optMh$*U~gcJ>cz@N;3j@im2X=alz;i5QWbVUy7tagA+sMy2Yg)XAHROi z`r)OPjnDjfd-(9hQ}chGvw~VSKJ(`}{P^Ojc@m;<=ab+H2MVyAjJ#)?z=*`;PBvfR zYXoM|OR8k#yFj9n^aNX7E|^^2mT5_+jHax0^D&^kJmA*2I_QsznoxB zfMmNMMwylJsE`xN)+kL}|KRIKm|8YILqZl8?iDT?E`^#uo|eDdMUQY_zwi6bf&xOX zrL5%9Ed_3dq9z7v$+b7#HyKUI`k`%pnOGj5===F82+{7h8q9 z#xo;2>e>ERx3&Qlez_q-9x}Mq!ax^tw+&QZ9PhKNP(e==1hFypNE0~t_9OZC95QRP z&!LaQV}RMVbGz2X9{5#vZT6r?2jn?1=5uy$15m5*NK4AAhI9aeATXUMXzfr0T0 zoZAgW(Z?sfHZOFUnz&?fg%Y%0oUk(kq-HUAQ4Ee+>}oPf!lJ0o!BGzqZ^-oP)O&|zc}hD%c0- z;$SO~Jc?NJoT~y&2SkMEi#ov4icbMnNlh?(L&M9iZH2IFP41n|sG5b4wm@WnK&NZ*-ovW#bRRbcKK8T&qWz~ z*JPvvnR`-39HtbYgSx$T=qdr{S68Ol46CC0VaqgaROaR=U;si&B1_#aG#ZT<1io$B z%ikuoLg%_#c|H3scxUa9hAKxRD6LfI%$3iE*;#MVex7xxh`2<+vfXpXwGyFx?0`i6 z)9ptYD|$9PoSuB-=W+(*g=Y<{aQnS~S!W|j^0aINmm+?8ON~bW8H;9*hlOqMMuz$O z$VWn`YFqQkS7qx6@rLh5*UgrXt@URPNo0oia^8b(qu>bZwZLP`-$It~wiv^(K4`|w zCd<@J20)G^v85#v?rP-~_1O;4ezNb1#~I=T4!(cG?m^IU{~rVA=B^DmH6p}@gK-gZYUg2BO@)NNzc<&ab2wtw~xfDW4vdhA#~ zcf6_1a%lPk@+CXAxRrf9z^6%ev5`YJJd|U+pW_?_#xb)$a~^&H-^d+E+3rvVbL5sk z&!6HvylIsrh0e`U|9cLiz4@c}$V4%W3Cvr?EBXVD#RzAAD(VB}8=iTtC`kll^Toj= zy{BOFN~w1vU>B-QNskVfWhKM`_SvWVQH{U%+aKr5egGO8>5p?}Cp7=TIWB>S)Yyf-ukbxyKJeRb(`^Uu zH)NK8_180pB;>Lg#rClHYa?n zKfP!dY9W^=CARxFXSy6b2c6{*@2UQIziG{Kh{>PlY#;(=k4T6yN56(bS_*W2wNvfW zp6~Eu8NE(1e>v#bQF(vwa5JzKZ7xh`Zv!?JpqkxfE$VXnqQ{90jydC6dk)T@Za+G6 zaDx07-8y=v9~8x2nJ?T!N7m7Z1s-N@!2>0sFzZj^t3ylok_|j9CZO(SOdDb4{{iZ+P z?`-*({AM%<$9oCWZg49R?#^-d-i!{z&5L*o;s$#_XU%gn{g1gIL8XvOFuMv&ZfIV9 zb>S}bh3UAEl*^(y{YI;)a5GBMG!vzOfLCu+Ln!l*nU6st_ZQ|~-XNO?Oq`1NbGqBX zm3aexPeoebNT&U+E4+rN>Z_`rSg~aU4t_prLSxS}Zj~U45CeI(d?YK5^ibpn1NlXN z5aZtBVyOErA^N>=4~Q^1cC7WpXYlpsP;5c_7lcvwxMb~3Z2||sE-|Iy{8RR9rQyVz zBroWB9Ea_LAS~+Z`7)E3$>(j7f)Am!(9G2!=V^K;5K(Nd-gT@7zEBfQ&)B>PmA@k4 z-&(qSPJ6b|aQ-QKw$pHC4oQgbV9dDBk~YAg9{dhgl!EvGO2v_&9$@gO^W9aEcyP06 z!KUh#T0r@+sq}k-KB~EweOpvBF%IzK&kh>SKV{F(e~Gh;hO?Gi?29(Dl_owzy}}s;{kZRLZk($F&@!Ec3iaWVS3J4)E>AjE3{( z!dJ!xo>genQwfE}X;`;&Nj!SMlm=sgw-SCK7kmY>^%jxM;8 z`hJ{;z`?g4a~jS+RX@A`C5{CR$DPEZ(B~3I-KQ|VTUzh(fPuVT#Xw6lv=g>`WnLT_ z{S{=dS$*ebVI^pV^IR8(=%XKz9})*%^UvwGJv1Eu?ZXXjn}6?%QXKm!J2ggN&{^{O zEVp9t@cA+{w2cBguCy%is(T8m0>f{f`Jju+B*yJ$@tOO6@Ln3uKUF`L{}RWFhQk%U z>gbhD3f69rnkW1{mE26^qCr2a$khd4-W#Ju!8?0E22Z%DU(096$q%! zb=TZ+y>FI-?Nc0kAa`TCW;=Mms#wH>G9fcBlgMgomIMvOmcwOcrVGLYYe3ey@jE-^ zx*=JiTUP8Ff_hkip?CDe{te$wt!eDB^GFCPPMXoLP3h7Q!CewEszuD?eNJMe)rIBY z7_;{yK3)naDp}GtQX3B+32?`h-q?gXuMB_Ae0%P3VLuJ$pNfM6G@MyKA*D}i`Jc>V zK*E8DySGmXupKD&t}ko^N3DkxuBmhZle3f3g%4AKzOuQzgpMexL@u*k(WgXcKlpKh zR?k;(A>`Sc#dondAhTMX89%=K13X-6ZL)lKEm%F_bhYwr59~g+Sbn)}GU!Nj)EnsG zMe9i_DfHU~2pp`=r}rsn^?b>iK_VlX49IiWi9vIjL2AF?{?7KG_vJ5u@v+XA9p|%v zSg5dY>9QUmxNvdy_*p*GhyD6bS#je28@7g@=FsZ-Fuy%Johi3(^#C|y(#y1q$&bt@^We`!-B0O#^U$x>B?Ve2O0Wm-x9RCz@3=XW7EwY3QVEi=R0c1`d#JJZODjB&)h`KZx`b8~Yr zCdLZ#;!$sr`@LT&^Ez0<0{H=6xpGc8Gd~Ag6VDfpTXw*E9_t$~zHb4}Op;cadF<$g zRJGcytvUpI@b!b`iFT0?-qz}+nJ!;pzIUgbTqYwT{lmq!JGmE}jn+oGbhCi-X@cHcZzKHs)bGGQ-KR-qJyELh|&ty_rnp89Pbjh6PP= zRv}*?gAUKh7kkS=FI%%FS8^9@-6wy<{^k&}QNkiV?6V%h9(;d*<*|5J*m$R1NPP%A zlL;(ZxwM}eZ?P8zNFGn?V2?((*+6zjG8e!$R4~^Uo1F=;aLTvm2#gRLqERH8Zu#& zsq|Xv`*o#2uq~Hi<8~CS^NcZz8(Bl(V0AIg!S2Ukad1Z4+&$lD5U57R<$g9Dpz_lF zw!T;IItN@NzJIx5(gfZ4)nc;MyI@S%<>&%#CG^CCiG^jJ#QO_aolJ8uJs;NpJQ<+) z*9A@KSpIsD$Tb&tDejo)1=~J}#d_}h3YRXh=O}FLhVpCLwJe5GK$w(HOU687bXzmq z)~59$bJ|0z=TrT;Jvh|41d$E+j!D4yZ9$G!0k*bvZgLDcwo|(X>;K_twysEYgy)8>p;n{@n zqs=)Dz}w!*>T*XHaL8x!`tnO2-L~SmeobCNv035|-(A|H1r#sNOw zFg+g@Z}NllN7=oo^LHeqMB_+%SwGzJW}OSNwgChfC))1uZw1T+v0C-gjWAYO|K;#G zaWwW>Z^Rxq;(k27elR`XjDN6a5^|z8Vl%V!5ZKBmTe(ed94=MZkT-Oz6Xf@#YbacZ zgXR5xUa2M%aSvLrOXHqa^jAdZ6oli>N~9YtO<(S zYYV1tZ3dP1FWvBdZHcnAiIJOaiSu}T{m|<9c5vQ&Qee)694m_#j#s1V{*l1228!MN zI`Cara&wqRI=p$0)gXVU8zgQZu^*^bLj{eE&i<+*#v8tVi2MD&pgAYAk2v_9$iSMcO&--0stF3?ch=3aEM6b{Dsu-$(#5A_jZJJGq8SU-4smSaT| zQWD~}kDMoN(Fv5qc|@5`Hv+*ZX=ToO3V7(Z!zqg`5jHf6@wampfrhnR0}E_dp(*abs56&>3ZvZ22~ z+bW^5R3!D`7MVPvcf;JLW2Z9`mC!N~Z)@*v;y8zow-p3`VFx6{RCf6!7-b?m>}XuE_z4AE ztStF%SyT$*u9TBCczU7!-T^l6)>6=)c){1-c@q8E%U z&U9@tEC5%>cAf63S3(2tn5HlNPHaE;{#uZRGjm8HKM~nb`glnrh(3Q_H`<{C+C-j^ zahcLN>97l`e7lnk8%j+SDE-B7)3Ln3h9(vC$v(3{%@@SD#`{f(hEwsZl67D6IABqK zw(5H}^?L<~+ta;EhhG=NGtS$)PUgIWS}yn4o$@K*>e`&Tch*9vir8R-V~-r6e(-)1 zrs32Dxa}7X9flDmXF^)@zd856X4{dz`IM{s2 zu6}(m_At9#^=mce9J^1#siiT7DU_GD2)cw%`z|uYJ)A!q+8lAd6kOS$h zUf8(BSHm)${R4c>J>ZJtmlrnf#;D?(vs>0=6ZNoodsfeKusV|!TNqnd{1Ypm%qp6* zsl)jjNVp^tAI#JUJje|DI+?qnuI7`f>(NoL!A|RmW7irqA)4; zW~=Nd+b~2Pvs~Ti&xp*tOd=0QjVM)cG(nlJ(lJU!D7+rDNqf?w3tIDB)!76j(DQE% z7x;}5=LdMdiP3QAB>2PoFU%(|%ek#y+dE0MYtOIzH79pg!L*9|hhGV_!G=u+!)HgE z;a8^SwU4<`^v3OP9l(y z3i`O-OzB1MD=$(IFM^6E51aLUN`sT>I&psISkd-Drwh)k#C;09J(z9~Yg2)?7Hv`Cs@;I;GZT`?}jmHEKOFU(#V_yq>M`Ru1-nRlBBy=%_yBR1M zU+=!$wgKHX|Ng~fN#eMG_Zz0?!#F{nmxM)D24Qv}0>a|EdYiw1{pQD;B>6wV5Leei zZvlXkYRm#BUHWe&20Alm-ZKji0|M zVh)>4k?KJhB-R1GL};ezI{y0IFN8R~6I5&jdz03G3e_ry>LyM<1sFO(ZQtfJs40QU zj+n20yp*Wx#J5vSH;C;YBu@Arl-k9Js1I|hN9(?)a*V$ZiixszfORXMomaKVg?kl8 z&y_4{0a}{YuU_=)qd5;d()nG8{WYFLs~g1XXPoWl;Z5w_U~J#PQ_))0AY1ChMsL4v zs9&la!oIozu73V?b&5g|h}qc3>a=Gq`YxMC=YbnBuJQGQ=?01Jn;LH#dX5f>^#b4W zC9OH1=n z0fjM}+eSUjpttve08@JW&Z+xX5$1~r-B_~WDzo)Yz0o~z-jMx{9Z8C4bpJ-dsya?W{ow6Es04h5 zgcOIBlb2nh0Mo3O=3@(($xnWS#ycdogA{$M&v(Bzf}mw4eZd7)P)p2xW7jzYG}y$4 zO)i>?z`@(2g)e?8uQ`^QvzTQ7jIrN*Eo}dLK6-qkv#oLLUATLESpV&#UQqI`;gz{j zHGJHD{y@+&1a;Lb4$^QVe!m`X&juQf>$kPFlo0Cu7_!ZRGnekP7sDrutTVI^)IfXh zZqdg=z0fW4a^2}2Rqlb%FW@bLNAzn zmhb0Ykxs3j@|0NDcgN!)8&l)2bKWIDNkwbqegFmDesTDRkc2LJ+~44#(GM1a-|+V6 z(AYC`NJ7ZF*6Y4M`W@Kryn1AT%}=nl&mhJ%s~a$U4f9vk{|b&2)o%>jlnz}3S1@nO z@kMHGcpP5RdYr()*N-j@r}{*0X39D`WR^$Pus!q2YvnM!K9GLp02yrO%047(O)UWFn4pn2)CWH$57TS2dT&Hues%!Qf1{Kk_@S0~(Dw zjL7#v(*9xR8~RO9sl(A>6zPI?;f!L%oFw#wDSMV0N*sUie$$`jVEy&19Z1Ic$f0ht zWS9!aA9cDhlewCdG)$ViLHSF8k|nP*K*CE_#v3o|VD$C@9yy9AI<}DKPF=ImoPOI# zV~=5w&_R*mSeX2zzM9FdX0{oV$dMBT4OU_0P*Zft7QN~w=$Um=`qcY&_^vhV{aMKY zMCw5&pF)5#frGclfQGZm?RKrB)9>|sdf@qX$qEMKot)+J0;YEOT8_2xr(hd6l(;WS zp&=S-){0)O{ycG_Cekq~bW z_)b27fgE@CwfU00lQ4Glfm6$yHt?;!qbOT^tGceSdRe=vk>PvmZ=L`Tl6@U^mF?BR8;!)I-jqd;5$N z^w3z}z+i;mc z`A{!Rp<5aC$p}Sl%q~5lKSb0);yJW>zI*!yR;}pm1E(z#M0!1__nXK)3?XkmRTRK@ zH^cB##UFro7Nd9BcM8mC-;uoXp%!|keBqU@$+_*p^n6%5Egrgl^|;UP@!&w}1jjlu z6LLAtc^Q4_8DQ1lQ_#`T1e0yzFS!(VLivvS`W9Ox(c`azKOVIs&Uf(r4y~S#RA0(e z8pMpu#u*awA$!oou^WnedKaUpX!U$p`EE{!Dlc-@!K-P$KH8gFpf^_!-Jwf;KxiP0Es;-c=WJHSmv*-f@sq07bx0{w-5zm^y!*56tgv~I6 zDVD8!paH732~{s?T957%G2gmhjCaoVL#yYr3vUvUcWVO{G0*8^lYjTuci)RYzjQYi zUgKFs$G)cs>~vZ*KHNotimVftoeY!;_@f}*Xj+-Y`%*W!6yz1Q3BF2R|Q{S5ju8~aW2EleOH}M73 zK_Mj1V>D0%wVM=qft)3-=kV>6R?mm|?bi4k`Oru6$Z^ZL;xbqpVC05QvhbAdBy=>r zqQ0yQ=6y-tuWL~S92#52tt^)#N5!{2Uh4md5C?d_(dzjy&Ym&$p)ERNFyhLdhQQ%T zSV`V`lX-Us5D$6>DE3nB|fylh%Hm@OT5SELC;9vAYFC-RfoW^Y4Hs4t$RM#iom{IF_q!5I^_$ zL#yZG<@C~Cbeo>+usP=rf z%B1?uXW5sBT|5ntZfSb-d%tGLR_mUedA%8KTyjBYpUFb>jp>` z-%9v`1QZ?$ua_j=$H4o|nua6KKYH$rNGV7>tRS(Mj|su->$vrOV9UNF*u}`n)fw6b zc2^`7-7#u|eP29Oua>AxeP8o==9&3K{V3jV`)N3{N+i{@?%&r`s$xW+?IZ zIx8tEoGJPZ9eFiceQ z*nSv>+vHEjPsZ)PGGvRl&JXGXxAr>l8-{(H+J8L|G+xvPnI2sht`|544THy1dPNk` z_WN=2j+#XM0iMHv0cKN1Z?n}-XG^K=vy|nXWE|%_#M7MK(oHB6_-n_R_qg{$v^f8m zJyHp@!m7vQo*YEnZT;sL=wB!J4bQ>G11vU_eXGJ^oaqql&QX*3UEitg=SF&Pv7cNA zh~Ryy?x%7e%Ab@`W9e&wd78E#)K|))n{K=~X!uhAnfbUg>r;FjVC7?GRhr#f_H|?o zM3VKwnO{-wn~;Z{E34r@F9VOp`v!Zibif@0Proti$)bhg0sobQ#RSHg zNl8fM1ps z!jp5Mym)z<7ynw6R8schjMCimL~Jfc)Tfb<4?YD7JdMm`o9bO3hwl6Uu0BijH3X;j zt%ve;nOwV}m9MmfI~of&^7Pbih**a{9~hA5u8}6h0p1?0t}uHrzZnFcU(L!>2Kys# z9PL~c3orI}=6@Ay1kQVhN?R}Zffa(kgjMLeA!mS;%#+27QLV(C%u(XRxW;p^ISCdI z!#_67+iWooj6vO?e*Q2PZ%B+aWwbdw794k9)8W3g9!BOH?QG2H1_w{@iEIsDj>gG^ zS}PqV>JPB8r|XC4FFHDbo7K7-+8Gh^6Ur`p@$_WOzK6%1jl4p;z}QiP`II-6a8=cI zKLg2HFw_)pz&(E{YTvMbG{+br_zlltAv$dKh=g>UzVzV;Ln??;y=QZH8$EKrHDMWF zTr>EvdSLRiLML#2n^gVK;x2G&|L7wsqJY+HUKm&JG*(a>X_1w_#ZWv zS=B()zhP}=x_)T&e6w~SXB^EBqkkbLsgawl6DdxH$rS*mLEbx<-MEz!K-G#3|jDja6`mMk&l9KP#40sR3lZqqlNp zOwf(5R>t#RBeqk#J+yj0%x?mm79kdkD6q#DPJGPk1ZzoJlw#995Wx`P*vwCchbJoJ z-oD5HedjLMYu=PaPh`1^Y`sRDzu`G^==qSVgWs<@a0CGBW1bSrv)VyrT%|*3X%~oG zL#YVg9SqmV48RGhPZFg+huGow#Qw)LBTk8g>$pSn1* zj>0#0mo_Zo&x18yip!yo`0Y2x6uyFsqpvlzquZv|al+j>RrS#NgbaQDL_Ik4dADZXq#>$!>9V}u z9irdxoVj#%m&3Q^*SQbCE9qaZH;+-^`6T;`CI|(zo-7mE6_E)Ac{rWXiV|=@?Noe< zg$cT2|1+Z!W1=nqUq6`64jUJ+I5?Xoe}Px15B>}i;`*?%A39vIFq93W0EDu=>`Oor ztJp( z*uRqd$(Q9FASUby)AN{oIJy4jt^`IcG`Q*gPWQLOc_KDePxo)MIy=R@>yNXlslTI% zS-@YoaU{SE5iB}!iAT`mZSl0$d#=hE55S1#_6nI8sYH3#;jZL5Zp#@|i}?{0%kmWN-xo!!esCy4v$1b+SY|DokdUq&i&9u;R$Ed3b(!_BNA8&$$^05OFGN7EBXS(K)zrS&A{+M8<-~aiw&t1ccWCu*S8SFUtH17skq`EgS@Wm%jsAL& zkR9p5M~=jPf$qo6EULO5fiBh$etZvmfY_3(Y!OFAbiPqk4>^wbJAnB1L#uzgokiL> zy6Y!=;i3ECep(HcLmyO?`*v>!j7qS+P*7F}EOpEJ!b-n@TS1;>GhWV<7Jy z=~P5bXARHB$Q^|K4bPcF|AyEFN?qxR>H|vxHybaLZ-WAo`P_7LcCJdGs;?KgT-&kP@D5d1gFF?R ze7$V41s*&ax@6^pCfL(tE4RP;D=3uC=RJdbLmKE;-<2%Tk~1C2*`_`9QmDH_*G# z&klyZz@QLP+lCX-$Rp*_niYw}bqT)xU^+W&KS}(;JM*Gc>bw_G>rP*<(DDOps-S#T z%x(sHw-<58#kGUryko-7xy7I$K4t|AY(;$3-yT?)OZ?s;zQ3l`*`0NFh)GRn#Oy$- ztwprC^1r~3IXo{1-etg{c!`zmUs?c+ZPI@l+YYSGEPb@xW+^&(oG~*hg6KCqhgN5| zEsoyaEaV5I%Vs>nvV>|E8NOf=mG&rv`wM?CbH$V4y=Mo_1oJ5HS)oLl*m?z2#_9IJ zk|tt1#rsWZrUGc5r+#NYWV2&7r5+YmSsg&W*Fn~=sJm5V2ej5_u@Kr_0Xc`QU%nJ* z1TJKF;r()9H1}+M(sNg$J@|PQtBh-CAgKLy9=B!rO!C>@fD$vnJfqXPKz=LrDlRjbNPW ze^`oN?X_=$Pk`UagJ&whtLE*>Yd*Kb{Droy9|twj-3F~UcBvBQqxkxv)!8YG1S=`K z&`~)m0?F}WS5iT_Odnf;b~Ci>i*LEF-3Z7wy78LjO;ftafaH#OlBnoXMJ8pPxjC55 z4yzzrrABpKO$t2Hd)1ncei9ph5RRcWRg^WwP&r;gNj^OtNFB{l*x^k9OX^FLo}EHa zl5TlGQXnx7@cj<0&Mr?!zTb~L^*di;ZOl=dnyDPkE$<^obSh!|XM5vCURhH*)iwJV z&-G6Iu50D7{_#zS-oA{nmo09Deh1H?)!A(vU$(x2a}rDdVZ|3t+0^kTzeztK%b*Ly z9Om|jRd^4Z{X^BcS(||Cfm~-$vjOEytT*XgHMc#qI=kmdY;MoE7?8vMo2@IZ{{Eic z3aRTGkGD4ffrqDAuPaqe>HAI!+urQ}3ao2>1@LR4lZRG+=%^>gHQt_CL$T_aO-bZe zhn|lzx=ex|lcog0hkfvsgTjK?jeT%<-&4gd=>!mlgdSFp&xYi~Tp(Bwpy!vZXM0jY zjB6~0r{@Q=6+q<_spn2CESP}RtGzCK-u7F6-Bhsn{Jwo<(Dq5F(UVqZ5PZ*HX@xTd z%G9QN=iSsqnF76Ju2mELhHpOzl`xZuC@m^a{>4n5IU6^xO!lg?aK5aW-^S^)%P&T+KM_19aVq3jyR9Uuj|p70%!k@6&;;xh0?YO>mF*H zEi5qG$^Ua4uJ&c@Q1k8pw{k}Eh1EWT8k<)x;ZhCoG;{R(i9%U)lq5WQnUgr*!M9Vb zSq_%$sCn!Cv+eghCr(X`=aIo6%-`89wR&Sb80Pra<7wCfvIlB-SQ9cP|3ZiA6Vkg?@`!gtkxV z$&bDA?5%sB3&i!MebS$MA|<&d3(@b5HzFwP4cV9O4Bi|I?mLCZHGH6e$T0oKCGsUuJ>|>k#T&&?*~7=I@8%KX4bRb= z1u4?hHDgOpfAGUp1v$!9A(yfRc=WB+!O)AScUv)x3O$RzU%NIbW z@Yb5qR(;g5{)usIBC)^5d^FuoHxkOn4oGCHfaGE2v?$gCz~#u}ceqXlfPB=3QP@WbU6V-nANo;z9MJ0D zioIIPKX*}o=LcabIC}A+VkOkS|CWKbwhCOKGmmU{?FD<5_V)^iCcvES$I4&bRYGsr z{&xQIKp&G#)58fL- zwsIAeLl@nl{||fc^+T(Fn~4MxInic~*zvRhkYQbX>8^ecod5oQ%Zbu1IL>Q!bCq-h zn7qLD$Xp{A-umW35?-Q*Uj6DNczHf^Krwi4@Jr<;cj*10R@g9A!*>v6T`EemM>r;w( zo~JA_^Ev`~O-kG+jiWnF&`}wSfL@Kcb*i-b zw^RE)l|Ntj2DIbd4U%5{z~%{Lk-8mG*)SJ8l+(=YzupNBl3ed_3p7E;2m`Ldi*o3Q zII?E**13O|fL8x@)@R{khe>MxMwVF_Y@Sq90bLZHaOcD|LApBv%Bd6W(D(4R2fnps zVDshJW2tdGsHj|{5qgX`f5XprF#X%C`{>3p6g<2*n2=e2Ax8q(?w58|f>NW@-nWh| zz@XfLa((LW0Eu#~-1nK%0JZG{B!8R}L{qKGYdyT@-cQ8zZ&(}zB$-C1zb~PV+codE zoZGRY1H8}T9=sb+0(Y~H4}2c&1jjEoqGQetAjw|wlotnzQh0wo9Z4bT`S5W-tA8V3 zGjj{op+m|7cdOc4QulYr!Y@{Cc!#eWo+lIPRlA(o zX8+v#Vzl}<%wOjVR2+cIAh6~_Ep-_^CR1*FR`PR53Gz(}#X57>4*03nm{ zJ2zTsBKwS9hqLApbzJ!NL#ux~)vHZ6meUHH&dhh-F7_Gxp3Q97ZUK;GE2d(>8t+>LZi(YO>-*6AM5t$$_2s(`AeAb+;yZ;`Jrg)W0NeSByb$XrMXb#kY`U8yC*YrgTL zNlXXC3_6MT8}WX#qT$5owx1eTrbEQ@_f_r=``sU$;u}?Q+g=2>`t6oH!_xs?J%8r; z{Z=!SJl?DbDD%i?DP!0!O2K|DWN|8Qlkaj z{rS+4bFvlaengv>eW?UB*FQeq+0BePr0L!9VIZy>@$JW&hC|U88w`3`2~;FMh)a>D z`kB8TB(ehimc?Axn&E}0;2rLRaZq$wljj1(PB?k5v&{dp23mgT@WlSdMEy11Z~JLD zbl%@inwoBb(XkQ21|H>9&i;Uhl^y~eu!E05YsbxG5Pu{|e^G1;jL^9zXR}iSEq=>; zP`ZKmJAio30b0)N=MPA&bw`4WVUwcRdVi%Kpwvu5@5#yz$nwTFUZ$lO_&qs#gAZ*6 zHjaxv(wi(nGkj&WKZ@%R)+Jb9Ha)+lr{SF1IXOSDxRcsGSD;qj{dS|!*oIt_B6bvZ z9(1ce+%WZbS(c@~Wj_@My^X&3P5jbAM}KuO{E{KAOYr?Qaeg@U_rd=m4kL{{++J@J z<0;hrWOA79P}_OBLXeR_p19FHPZ{sr>H{s60! LX?w7?gq{0eR8MbD literal 33344 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 TdaX|&xqZWJm?|GyUE2QxR|Ib8 diff --git a/tests/regression_tests/surface_source_write/case-15/results_true.dat b/tests/regression_tests/surface_source_write/case-15/results_true.dat index d793a7e42..ad927bdf3 100644 --- a/tests/regression_tests/surface_source_write/case-15/results_true.dat +++ b/tests/regression_tests/surface_source_write/case-15/results_true.dat @@ -1,2 +1,2 @@ k-combined: -4.752377E-02 6.773395E-03 +4.929000E-02 8.212396E-03 diff --git a/tests/regression_tests/surface_source_write/case-15/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-15/surface_source_true.h5 index 3e15017a16626a9a66d903daf1721a97dc3c4058..5bdb39b2a703fa7cdb17a153a688cb0ea7664f1c 100644 GIT binary patch delta 17 YcmaDL@IYXL2s_I?%~KhhCD=U}0X^3S1ONa4 delta 17 YcmaDL@IYXL2s=ysjEuC+66_v~06HE8kN^Mx diff --git a/tests/regression_tests/surface_source_write/case-16/results_true.dat b/tests/regression_tests/surface_source_write/case-16/results_true.dat index a97be70ca..cd0619c1f 100644 --- a/tests/regression_tests/surface_source_write/case-16/results_true.dat +++ b/tests/regression_tests/surface_source_write/case-16/results_true.dat @@ -1,2 +1,2 @@ k-combined: -1.496403E+00 3.891283E-02 +1.403371E+00 1.456192E-02 diff --git a/tests/regression_tests/surface_source_write/case-16/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-16/surface_source_true.h5 index 49f67e8205608e848f0130ed65b472ff98eee046..79ac8e1a65e38b5b4c38de45c29da9f3579a35aa 100644 GIT binary patch literal 33344 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 J^epFt{|~;VRf_-s literal 33344 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 NWw4>&FGk7_{y#Mh($ diff --git a/tests/regression_tests/surface_source_write/case-19/results_true.dat b/tests/regression_tests/surface_source_write/case-19/results_true.dat index a97be70ca..cd0619c1f 100644 --- a/tests/regression_tests/surface_source_write/case-19/results_true.dat +++ b/tests/regression_tests/surface_source_write/case-19/results_true.dat @@ -1,2 +1,2 @@ k-combined: -1.496403E+00 3.891283E-02 +1.403371E+00 1.456192E-02 diff --git a/tests/regression_tests/surface_source_write/case-19/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-19/surface_source_true.h5 index f4261451dd4832b3ebf2e4f014543c850f8f35de..db8a49dceeb20ebdc3e0c1b8db765cafed5a4052 100644 GIT binary patch delta 17 YcmaDL@IYXL2s_I|%~KhhCD=U}0X^~t2LJ#7 delta 17 YcmaDL@IYXL2s=yKjEuC+66_v~06JX-mjD0& diff --git a/tests/regression_tests/surface_source_write/case-20/results_true.dat b/tests/regression_tests/surface_source_write/case-20/results_true.dat index 7ccce7cc3..44293cf09 100644 --- a/tests/regression_tests/surface_source_write/case-20/results_true.dat +++ b/tests/regression_tests/surface_source_write/case-20/results_true.dat @@ -1,2 +1,2 @@ k-combined: -1.149925E+00 2.542255E-01 +1.266853E+00 4.552028E-02 diff --git a/tests/regression_tests/surface_source_write/case-20/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-20/surface_source_true.h5 index fa8a4e628728deb4c4d75b03cd31f0aefe6d132c..01d2abf61cf8a41876d664d1aa505a967e7559cb 100644 GIT binary patch literal 33344 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

    `BTVz zMB)c?1qpIG95ua!pEsVu;}p{9 z#_cYhfCb4xjcWC{`%mhpT$(D17i8z$d9tvt z8ho#&DRBQf2v1mhowJjS1A2ZLM4YaK$B*K3!aP^_q71u$(dVFjXKR|VesD>?+PAf% z31+Vb-Lp{`fxm*C`D?4&;Jz=rJ1-C=BPy>w)FW>35X>JfJiGa6O1{sx0+X7B2&*Z&Y!Xj1B^Cvgz~u>-!zsX$VxDOPUCSdE-qe@ zbpH-nsY^$`kKo#)9C9Xh_XNI!jva&i?O!Xw!|870mM0UiM&^)mm&65Gd zqpH*%i4*Pz<8x9vn=ZvX?}EJz!6oTzIL<`|p-1jP-@xMdYFPE*Y(OWje%1W?Bovbu zd}tF$gl3Yu^~oxJgrFW>JUy~(k@1Z#*%gGzd!&naQ z!J)vTqe(zW;iH)MXcPEz-u|rIt8qABlQnD2n+&+Y@b7yLKHAbQwpWHAB#U@ z!o?|w((+m+*Sew2@2LhckqlTo=UH!PFaj4$?VbchBj}OH??P+-!36W?44&Qio6guC zmHY-A{oV8r99)1Kn<+7}m0yi-8a2a%6_&Yz`Q@ZX<#u%&w5_A;z< zs7n9bL+uzF2cgBs&ZNJ}1W}K8 zTs#LG;MChF{*Eqyro9$n&lAf=(67(qaRjxgkN%(~au=#UOP41)ld?O%H zepJX+(hc8xlxs?&hTy^(i97J*1>~`9WU&AH?Q=*R4v71kEkNcntGRD#xcliPgR{dv>#X^Z{Wo?q!m}HRSP$u4 zK!RE2_Btw1hKp0sJ)hUO(>(yVK(dTjm}$y{=-3GRh#8kE#L>+M4)k?G6tF89Fdl5(FvmjjSg?^nDedmR0KxKc8tgDj3 z=5WWwyDzmE0l8-fw(m1zw6o8JgrbY#v96$++ossY$LK%6)5i@Mf$Z zRtrj>9uw+;^YsQ_=8yLwdx7iAzA<@%9P=%}X8QIj#q+DGBXCvp@VTddaDE+fO&k4j zp%}!$y1=NS=Wx<%I(W!_655}?ImmFX1OY33(}CmL=cISa7>`&kg6Jw&rj96Fzw!8! z#~jgOeQ;-@jzV-MGM0p{8lbn$IYOK?5L}r8 zPoC*fSGl}C-tMmtCFan1W9?DjX1Mp+vbfuWPT=k(^sX2+3=Oq9B`=(0MssEaAL@-f zOpvn`H`vOi1fsT&7ktK{O(MId&o$gQI}gV{^sniAf%5ycqW+{BsP<}#)@bh#+-;Gi zwWpyT@kCQRXgW){9^LZ2EzXJVrtY-aDUcjdvuJ68Yll2{a6M(3+YfGxaYzYTm&2KN zONQ0v15kWCb5t|_IZ(eYB^KGpPEZd%hxR#Z#`za-q2STsfk>%2xMep#sw!)-6A@7F zt4Gf1@8#fX&+}&iB9m}r%;`z|t3&ANW7CCUf`sEpWLrWu)7`vfjQek|&zEj@E+1)_ z#^%ql#;OZvgbIMF;=X&r5~DEZtBA8T=z@=8BH83`WB@38&C=C>`y7{_wl&Cb3-JAD z#$D(^j@iIdV#IIdc*G7(K#kSI!WpWGaHWJaSpQfx^evz@+c&_B2poUgU8g~qL%ykF zD+#F2DcyO004}h3)E`bG$84-$pYQBO%{!%$1rJaj&stUa32Pfh1n;cR7pJ>iGd|!G z380S{Gl9sJ*N?x%QNjN5Y7cYb7%&KznMRW&smO9;ufkP!PrLzTTcZ} zVEsDA%rP@lu^Ce6$ZNgoD}!9O#*Vp_jKPX8t9kks=^*2VUH5)p!sC1KIl02M;hcOZ z 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 I=Y?4Z0NP<`Z~y=R diff --git a/tests/regression_tests/surface_source_write/case-21/results_true.dat b/tests/regression_tests/surface_source_write/case-21/results_true.dat index 7ccce7cc3..44293cf09 100644 --- a/tests/regression_tests/surface_source_write/case-21/results_true.dat +++ b/tests/regression_tests/surface_source_write/case-21/results_true.dat @@ -1,2 +1,2 @@ k-combined: -1.149925E+00 2.542255E-01 +1.266853E+00 4.552028E-02 diff --git a/tests/regression_tests/surface_source_write/case-21/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-21/surface_source_true.h5 index ed87ce849f788a346f99843aa263565fd326040c..4e5dcb446c26e85f1f763b75e4a33e490c1ba01d 100644 GIT binary patch delta 17 YcmaDL@IYXL2s_Io%~KhhCD=U}0X_Q$2mk;8 delta 17 YcmaDL@IYXL2s=yWjEuC+66_v~06KUDng9R* diff --git a/tests/regression_tests/surface_source_write/case-a01/results_true.dat b/tests/regression_tests/surface_source_write/case-a01/results_true.dat index 8979eb554..d4d1d1e5a 100644 --- a/tests/regression_tests/surface_source_write/case-a01/results_true.dat +++ b/tests/regression_tests/surface_source_write/case-a01/results_true.dat @@ -1,2 +1,2 @@ k-combined: -5.169123E-02 9.144814E-03 +5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/surface_source_write/case-a01/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-a01/surface_source_true.h5 index 4a86235679085a07458904653a5b63f162059dea..8dc148ee07eb33d6374387e557b35914f5fa2003 100644 GIT binary patch delta 3450 zcmZ|Rc|4Ts7Xa`WVn~$jnyw`+q|qQ*TAX(pg`_MMg(MY8){wHLg;a9y)HMlpQ&LH{ z%|-GcOG&OhWSAJ+G-OFkwqGg7#Nf@v6t1YBymF%U=TifCb0G@VSYnaXjI@yZlQ`-d2@t!g zN{ZjMWrzwATvAigTxcHEPjZpn%)@)Q5A#B%(L8^$)dWhJEFK@C_d%Tk=X{np3$4l6 zZM2@&2Xj&@X>+17BZbZu?`tnu5qf^r`&@1!jRmx3hPOoT^je7!1P6)-=*?vSWOOL6?*mNQMK7HlD z`K>SzeX?-M^A;9V@|0E^WW{&MJdhg|>SGf<47$)s&7h)E;VD#QSmx^^QVbTKH>+)T z?t{U@l;CsG4WK8xR_D*_Wypb6^Xu92MzFeKX)*Y0nD5F8xOU>yC`Q=`lzWO^P++it zdmGElnKs@)??&o=q}XhyHdK(DI^5jef|x8FI}a~8@?MYoY6cvQnPr@_ z;m!yO{wqCpG=3E3ZmjyPXw?~5CYEGKGw28MMJjPkaUC)Z*^*>+%>t4DlY14?3afbD>rv|R!5-!)=n?QlDAL!edwS&+r+lR~3(oxaI z&WI2r7PJQ99fB;i$fUL`|CD57zH7sEg}8BXO#g`Y+dgnMJ}#whI{~eSZmo+M`ixj> zj^aELx{--;YnL{@6NRhQWyNt`fwy+qbne3WWQx&!xy$LMd{s_Wq~4 zry*@c?#Op{<;^wL3%>Byt)RIj$j7gb4iZzj(i$EONS5+qaCod2Yzb2Or(E^1bQ`Sk z1^)8Pe|2`3@7Hij{&cE>Vq@TwKR@AB^$n|UrqOz)a)rP&RzPr42<9(6GFH8!p6`junxsR_edb&-a&{ew)PU+tP9ryP2;l$7AR??$@ za@$;(*1KEzOYR$3@DG8c*uD@u@3|q&BmB!u9d^vjUj-h>jem^f6jNjejnvhO(uGoNGi!(O2G6x zWlKt@Y5az;6-ZboW-0E-us?(h_Se2!^0R^KDxKVnj?|#xjJ7=oux{iMAvW*D*AAGQ za3h#z8Has%lyxNHkS)K!f*a8XH$vJw77MxB$GfZ(pM)~{Ln+!XZ{cXCo{4NS8{Bi0 z4*li(0i`|})vb#x07mA*O_oY^*vY!J(UaBj{6E zIu5kQ`{?uC22kBs`3$=NCR~4&@7|HxiL~}9Ew4Y!M0%M%y5?8jqm*kA6k&^Z*si4| zL1$n8w|lAP9=ktp0#Xz!BubaFAv}Erajwev;DAIAYbR`0L}!h8&TYfRd!Z#5cf95w0CnLR5Q>8d#8b- z{dFk%V!Xx!lV0@oYZJ-2%L{Xwh!PXqK6{>+b$Nq<$8U`g&VIhT3dy%tDl+q?QC>#4 zRisxZ7*4S^&!;qi_m`00i}je`pK>CAK(&__Q_z`r9sqAB9=E7enah?z%(QL2qHQZoOeGG3Dy^+Xo(rEFtY5h zO{hE@5$0JYDnF^mR+rQzof7+R*RRXe>CPn%%1T&ks6-h?4064mJvj#^y3UNbyktWL zrh2#9z8s`-S~(_djgao!M)H(##iEbKJ-KA}vkrJ(qTFbud)nU3tr?YsobjJ)J&h8` z(Kx2%Fyx5~*Xr6|LD~Bcl)ei8477nS$&HbvuqEEeGT&R1ygTil+tGj3S5m-L!IhnS zZZwTb;~&1Smu`W~XV0s8JlPQ1b%Gj|@ENLbl8W1WGtrX1QHN{YMR4(k%|XirdocIk zjC4p2vvrsqXqyDwUgGMt3Pl45*Sq{m?Z)pLa`Tb4UzNtuL58I-`*szGY2TM6NU}gu zG^Hu`CIc0Qy>&ZLuS)i9J6&|aNc87GQxt#`N9fr(;uFwSd(;qG`jN)i;q`kphEar8 z#iLV|Y_ws+4PD!uN-%DBsj0`_!|mJh?3doknEbZtaT8n;e*#+zxCWwol-E8P0#7DK zao-pRdDf4(z9IYrEseetjneH%UOJ=9?>rMasrGY3lNlgss1>VuTo2okTJCV)=3fab z3&1tzp=W6pU!i}6*5Sc(A9#&eK$p;%Oz@_I1vz?Xw50*sII??Q;8?&ofJk{_4D6k& zdVg|>^~|K4t?&*3*O4AppZER9li2NR;{N@}v(%(Rb1b6(<@jYtDs;R>35q>>x#KKY z`k5_!E}#vLTl^hB?hVCi@~ S3!@oVZf5V?%PoXBH~tSAEx4rs delta 3364 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 diff --git a/tests/regression_tests/surface_source_write/case-d01/results_true.dat b/tests/regression_tests/surface_source_write/case-d01/results_true.dat index 7fb415cb1..26b9e30a3 100644 --- a/tests/regression_tests/surface_source_write/case-d01/results_true.dat +++ b/tests/regression_tests/surface_source_write/case-d01/results_true.dat @@ -1,2 +1,2 @@ k-combined: -9.263843E-01 3.193920E-02 +9.288719E-01 6.877101E-03 diff --git a/tests/regression_tests/surface_source_write/case-d01/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-d01/surface_source_true.h5 index 8f8cd604dc7d685af08c32e69922749b6ad01654..9a66315ffe508938cf4b2499f980718982314636 100644 GIT binary patch delta 9388 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 delta 9466 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 Hqv8I4L9t>9 delta 18778 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 aQMJeStOfD{|Cz_a|9q$Cvjm9y`~LvP#Sf?e diff --git a/tests/regression_tests/surface_source_write/case-d03/results_true.dat b/tests/regression_tests/surface_source_write/case-d03/results_true.dat index 7fb415cb1..26b9e30a3 100644 --- a/tests/regression_tests/surface_source_write/case-d03/results_true.dat +++ b/tests/regression_tests/surface_source_write/case-d03/results_true.dat @@ -1,2 +1,2 @@ k-combined: -9.263843E-01 3.193920E-02 +9.288719E-01 6.877101E-03 diff --git a/tests/regression_tests/surface_source_write/case-d03/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-d03/surface_source_true.h5 index 8435e8f17f4284ea1916ca71e048265aea650f19..0af7160d4bb91e4efc535bbfa8bb2aa24851db7e 100644 GIT binary patch delta 15119 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 Dx83k^ delta 15115 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%0Aql-Vqu@~HIk$GDEq6hZIj&^OD zR6tdyUnf?+rxx}}G*kAvzHNKl91G%hs8uRBL==nGKJ-vINdn8@WVH6==20;JYem10(15FcnKXt5_inXHg+h||d|LxsRnBu%oGm!GV@TEomzTnqu zv1>wiUNmkS1ou06tgE=X!EYn|*Pglg@Ty|Z*3B96sN-Hfm!Gy)xZlzi$CZ|xl&n7q zP`;wvsPeu=0IBXOO1#m>uk3?sOIsX&8RO?L)xm->XJ&Pri4aA4PdwHawO7Xc$qtt) z1Z&}1L(xx1UyTCcP9#ak;u~l^8=PBthYPLl{h4qf<|>Y*EjJehp1m@>TNLxWx9$`C zL_m_APnk$|%45wnfjqU1!*JcK)p4(9jqvzodL!RgH6YQZ_u8c5X4FrF&$h8H49D)I znX&*6&To4@)PvK(bo&c_6ESm@*`mWQWw7Goo!+tu1@L06XWXZ_aTw`)-Gun@4b;0< zI|FYFAz!>UymQGqf@AS=Gfw}wjgIXJC6zm}5bn(QmJa!_Z}?sA)>%+_JWOO{_A2@^+lkF6!h9|hDUP}{IQIDF2Gs;0Lek(w!gYBci{vQK5Hzxo9 delta 18496 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 delta 17721 zcmZ|0cRZHS|3B`wZhP#XNF*MkW{W*;F!fmX*iRp|&FGm${E@{gGI zkj{yYw&`f(Jfjf(?FVj0(u9bC1VV7+Lnada!bKXI$V?_O@pNopljC`Z$ap6H|71!s zlNpKY>fBpcBIB3@{*xENOeP3M)-bRBZi1OiWa*V(mwnWd@;hr5GU0q=0t<=#A2#tn zBxnecnyi4ZJJOq#Wce?jX*C~Djt;5NUCHw2=>40nv`<&C{{FD*yhv zQY$lF-D;|TZ{{Zh=56ej<>O+u+47Z!;eQm%f=g+!aD zcS# zs3~Ghk=l`sOx#$U_d0ZDh01^WsFaONNQ&fUU+p6eb~52fq#yp@2kc~`#At}!t3{(V zoI4z3Mt@Fv&mFrdJ?+NvuNPA}R#B=s$i&KB2=T4F+1k${bvVhy1B!7oCm93(G5$~W zkC!vuOPjyPJwHGx$;3q_>d=p-Byu07lsvJVd4*yBWO0luC9{c(Oc;)o;$E#6;U*IW zKiekfTgXw+5|=Zl(Krs`ya6SXi-$~X=zhjOJY+^eJG-2z_ZSQIVh!WqEmWtJu@)o~3?mZ+SIc06WTNWi6He}5c9cdb2$2b?k={b9 zWg>*g!~j=CbSP1uf-|>V#wijNUM=G&OeVe=f~R&&{TInnVKSj0l3Qf8jD`rAconKS z@UVMPa2|+|2`Z7Dt7Yzx$i!slEz^t#OejU%NMu4@WGZPDrh2)|z8psp=i~p?;X0yZ z!luX|(bY1KMajhD$;Ly{ZeJ`4X%3 zb}W}^irM!r=lFm1JXw-Vr2lx7PE*{5^22w_nf7`~+uuk#Q!<65$OPF)C#nBTl8&p> z3hin-Ri(D+iX076WWDQ1Sd|EJp>v?FPDTOyk@!yg^UE^88FtXUTeuS#YNnY#+cX4{ z;t$lmjI~2K7qdLS9bAc%l}VC*6^|zJx)dOOMngl=gH}fC$|T8v{}r#@dSEeHMFbID z0?ggrbjaqQy^#Y|T$tC+uAMB>qu}jt@0*QJYeB8*=NCeiwNNQx1pWC$6`jpHEoQsL z>R;nla2Z$e@JZ5Q+rHlg4AV_VDwn>IuwK_au+Pw{_EENU);vloqIn`ldw|*w~NHa8)1<>xwi*RyqnD3wqmKZrGK8hC zH=4mlf1c=pQZ_WlB-X>F|HzuY_+o+UOjHu7uQcRFwmz_AIlv--Na;K>`FV|mM0r&> zo6~iIy=6feor_iA^-_=w=pBTPX_e&(97^b=vU?s+Q&(n#6~1g#{T5VzZ?g1E2h^3n z!W%t8!W_&m>PcP`K`zq_RqXls4SESZ8g<&(11tHS&bb8ifx-GSL>@05G{)SuHrs0d z8edK-+?yqjcX8~e!H&l5mWS7*Fl#rDb6V~sWMo6S(36AB!1)g6jt2P=z;k>4LRon= z9QM^{Hhs{5bi4PrMD07c77sWsK5Eocie}3K`{BUoJ4ufw687^uw}e}_3}PXW=vt`t z1RfZ1*ZTXW6<%`vrmg#J1lZ9sNeh`MaBsG6)X;bXXbXYq*gQ&EDKrVugCkBZ0l}_-p6x zH?}mYYJ#?O3q?7iL-1L3;FS+m-;wy) z_rc+nSJwI*$Hmu`+R%`bUINUBm7X18bGsxmF;9O+BTO3EC+s11;(I@s2R=8I=Xzi{ zo4&YXW*%77HnrH-G=wNSvZQR^d2WrbIF$}@&!22klxN2VGSM`L0YSt~Kh?upp8;d% zi)iA?9f3z5#pp)U)WYNY2%83;)`Nq>yB=ITxfxaI*WQveb#RTZ1QjmVjzTle&ypC! z*3ZW>#aWQNvqCe;&!=J0_DJaaw-d;V`p$f^=z&+W{G%>Tcfcz=F+VlfHlpjY%|~bIV{;vc*m*%V6`xZ0Wd6YXoRKkl?Dw#5NIbfxgA^6+Q@t4j-S>2ez>PxA-y21+ zLr?UZel}TYu>a%=Pe9pv;2NCVL9KK(eA1m)1cG^#;j=e}umV2jL5AQvF z@+p_83yxSkW=q&O0Nqkl&#IR#B43`G%Sle`Tx-WVyuQ`+V9g=54}=A{NdLR?2rCJR zzr$mxzgY!)wmINk!lv&~-{fVq!M7p6C9eCbo3joM{MmXgrE*dMzbYPg20NQEnMmMu=IOB_@CyCJybA`fOHVqdyYB7|Inqq`l^XA=_I?>rCsaM2J*l) zVJnL@9aL9w=~h!8S4pMWZ|n#|>8(?n-V0)ihVj9lXxAaPLbr*hOE&=0li2qgLWiK- z_IOJocN=`$TX1gw^cM7!*0~q$wiLeVRQeXvws1T(6+xO}uwC8ybcnbzkIsgx^hk0~ z+M^8*`oOames{-9UC>k|*leG43#>9-D78=FMb*y*cia{~zQ$K`6&K$D$X-l~NtF=B zDzq}eH|Nb8r*@t+1DYyC$L?svKH3p~cNWx5+weSVJ(xX7a)Z|7hC z-gtNoSBr}8!0*pB9lOT?@=N+rd=?4Wy-md7OpGRSQI*dr3+n|s^4vpGEPa6IY?7R; zbT#;sJ+1x8S`Cdl5^zwqY9)Rx{+TT6sraUfw443h!;5X7jXNc4%!hm!g?k;PwUCZ` z>65;bMH zv$$(5d~sa-fM9;`TW(kh3A^hxb>E9^1fIOo!4h>)13P@vBUj+kILwtz+#F(91h!1i z?3nU?590PPev4WZMF-SI4jO&b`zPs^9O;oK>WYK+J zm@0y46#8r5?V~{)lDGT{aeEJvlrJoHvi8EK4sD$(SC_Yw`a5rSGZ(|Vv-f9TEh(U7 z9|Y7mZCuuHb?{nO)2hZCRB<047XHh{SODS0PTE)9lu?sG_E<%`=RD7c-;ZCvC0R5I z)4fKo)fd-;=@K^A_`O1?ZbrfDZ3!zIp%wLYSMl&5wC}SYHHa5L+UBKk7a@gE2T;?JILpU^_a#;VjJ~{S9k;w@~RYy^wV@ z=#BuU$JohaahnU}u!EOK2jGX6if_@<*<(@&WTAz|dtO7YwBenSo61=5C> zYFN31Tfk+uLHKOywE2(w9ne@On}&m-0bB{1UlerhM67F1k*2nJujv4&^u4_6hB~v8 zBzEX^sxo66J9hZ`8KwmW73`M&oAl@TePDOeI z7;tqu^)I>^{tZSDN!VyY95hfhIH+-Atu};8^+{*Nl+@oOJf2~toCJ_X7mY@8E- zt!2*q*tO*;;p^>^O!IUo$sWtR;8+O6stGd}PHjamzwPk*s=H#(tf+%h;W{XWCyCCo zBckVv$IrYJL>31}Hcdu~Al5=wrj-^0AXcY{gJ9eRI%-*uC^yx?du$Ol{uL@{Z8-ZM zI~vO2Ri6sCYx<0l_eXw&@HlbC@PjBKoI&@wutE?is&3@yvmb$0Em?C*htvm5*#|LKN&wl#c1Zt|$giF1|Q4UTL2;<)&nV%tbm)+2BtY+e^T z8}%d+tT(<*nq3l;=9h>f^^U^;?@Rft!l8f%Rap%2Nr%=v&ux$XHb!|;ypk)jj<4}G zq|#xa)K1w@x)JOTEAe!lAtA~cjEBy=m&62*{NyU&=mSKTV3qpDR=~6_$o$RqE?C+J zKb9D1ptrS;_6={K=xe-+i}S7e{=I<=K~9p#*gZ3CA9C7e{;az6<>TM9!#ik74?(_?HnW#9%NHS+!Q;O zD)(yh?zt|YuoPi!me~hsO+)Mr&vgR&XHsS3K5gJ+Pi?f{HU+d#X3^vwzAA9qvg)Y?ZXpxTXM9|rVYgzRt+Z$NYJ2dlL)6z>1C0Cv*nBB1W2l!Xa<^b~ z$|q0=vtu}^`8={3Mkrn1rTS=G7{#fnJ&m<#AazyQWW`XFnex=48PKdlO zT}O`?a6I-MTM$MJn5sHVvlOw9yFB3&vp#UOBKad*atE;8rUg$0w}WY~VmnpWEvTex zELrvlrC;|@#mkf8m!)=3=n>SR+^FH0)EwU*{T&w@!fY0CXg z<1qm+YT%Q{Ro@66G;{tkFA zc|jZ4cEaQ>T&0A89@yb5efxEu9Lo35uw?UPr!{@|Q|aLIwWo-ILj>FD`*Ddcm>wgG zPk$Ij*J0(2i-BiOb-^FUFV+n@^uUXvXpf;&2h2-5D_g-kiEKTo6#wP)iqpFizt&W^ z*+p3Vj(ib}X4#+Py+niYFp0gis8Gii!zDyd?Cyech?1Gt(ktk)IbT0>CBuekg{ zH5uhU8~U}(pMq;ch5OXC4k}BK@{A0&*pNz+oQAcFbx85yn`WQTb*DqJ@Wq)Q2=COV#EBvF7bV9 z!GI4kobv~X{?OSEB!t%TSN@6aad508_^ZuRDU3ga<((dT9f*HX*s;*o4Tbp+eE71l zA7+|oy`(K*Mj1y#SwH%&kX;c0-@@Qqx|@Rjfo(}bh_BE=ar%oa$j@8C{3rI#gYGdV zWv!WFu+X`0Zai-ke2;!L=GOHMhLjkLX?9N|Mjsmf{)nb{akvwV^F5o^7qwAP7@2SO zwN`6oLU`|pls zjwSA3<9x}Yp3_c`gpjwTL2i7hEQpN0zwMr{{Fn(bBD7z>j>CD7!+fr+>meUD%pg%ixWrtDgc@NxIO6kf)1NK|(8 z&G*tq#|8|QU*20;6<6Y6&ADC0Z_7R#nRSm1+c2t_uYW}nTtg|u>CbrxVI32bmTn%2@W7?`X*=y7z-=Adi$SFkke%wEO$w1g z-)+B8U9x1iHt$fo+XE-Khg3Lbph|+H*yAr!$li7C9qsB<@F6cb=yy^V^b*@rEJ=)`Y zJ(YwcWNtq2&`SV0ws)9~Hl+zLXg(cB`Fnw>s`hMVau1m9>!P_bvxo@HI_}swN*TvE zUwpi^?#O3VC-wtz{Rh!Y%_PL@V#1`Rm?qX#kZ!~>mkwNry?gf6jDyFs7M_`>Qo(3J z(goM$ct}g*e@ClL(HD2)@vv|hcop`jTL_Uo?LB;xRs{Qab@wp+hyZdl_K|D*%?>D& zuV3PLs1tOACGdyH_kv5-H(XB6FrYaui9-&9!E56U_r-BsmGsS_h^Qc9;ro|+@DV3s zezsSAZ!iz?)$S^GnPCLny{O7s!Cwk^v*65)(`}IEaj)}vzYQpVPK=I5&9OCo#c;sY zG`THUCbmurIoN0*<1ju2r+;_)E%nkMjQ&oOEIk9z)JJdk%-3dc!AzYUZT$gjU0AP4 z{(1pI{7CDIqZ-!uQv2;Axj8?c^49{r3yED4e@NKbuQb;t(>0OoyJ7K@zjGlJXK(aR z8iE2o^*5`xcY&1j{JusA7nr+t;_yc*@GF0+b@$HOJP z8w+}itXQe2>qA*Y2>WwIF|hlDIC71zKHlT_5PTf0@X^=11Im6=nm-g<0goi-2cL;R z(7k8MJm=Ubn^fGt#%TvhzqhZgT0V>pS)@)aiC`J$E4mzdMUiB;hZZq^N5KY3+p(vi zuVCHjd1q_s1~|N{ee>T|LDcMa`hof9``7fP#?5`?d}w_kCzfMisN%~hhn+DH=hUC#i8S%bq(lJI9MK z7Eep0#PeeB1dSzZ52<3IjJ%y8ZEZkH;MGJx#`2-LeSb^e@_187-*h;1R2#K?O>g(c zoMPQj`}J<8v|U^~Nl2+izv7?d9;lJ`S%+SdgxxqCZ9Q`KI|vFZ2=_cP0=CXt9~$V5 z02gk;r4P*OPz#wjQF)e?^WjQ7;J!NEZy|!3$C2OtP$0lPu6+6NSKxlhHhV1?~> zwa|+$Fn)mluGT^?EHx%5o-S+!N=l&}y(xO=ZaIb=k6FroM+xt+)%1<7a?=Hd89<*> zH&y;c3cF_Xb3Djd5J_xiQ3$@%2ou2ecYN>rK!k2d+vcs^z}ftIBtxh<(s09s=Y+}a zwSJ?<-8D~q@u``FDZR3MQbOtnVp88XoKxmRicAJ~2Iuv{uML7xbH4rHaA&w?lXL>i zJhan^&PX4vcyzb@W-sMXu1v)j2~N3YxG06~jFx%z!KDfqeTh95JHUbjzp<3LOzZ$h ziwqkrzYKzrYa3$bHhu%5(w)Atp{nS_kOOTtwr`Cuj*IJ?Jmz3m6t)}>+h47}%*uyM zMG@%|~ zzoq!@BMH$F2-eBS8HX4BU2m|c(;~bz#}CC>w}Y!r0-;-W48h8tL}l=@7IsM`7Hq$x zi&iTBY2zbP_P_W&04|67?${dMqf*G$T8u|jn-=-3x?$6vL1|?E>3Vyk$S$az!9py% z(hb)2Cd5snognUBHG|e!di3G7QBpyv_nHnkF0R85nWl?8X-l9%(`0XFiwF| ziMy=t8ebe2=WCI+H~s1-5@wy%xB{*UMo`-}STvzRpzgU>ld zPow*QJEODPiHvNJ@S%XD@7f1WVvAc{^>tByr^jmHIr?ks5Plzmn=pCmtTzKv1u^IF zH`AbT`5tFCYeh?_2=d`EP_X3dgSCtG&3_N{fcTU0W;L7*%kli@`JXfr8WHmPcz87B zK7`tm*YU{mx&BueORo>vMrfx;a@V`rN~dZdrejV&4t5VhgT@z59D6#U(Y>4J^n5Fz z4yj9Lovs2pC}^rRww+?O;+8xvM?~BnhnIT=vFdZ~!#*<{*wouHj;JltNVl#LJu~cq zD7Q(_ZNFZ~65Q~mjjjfNf3L;*!RfzA6;Nr(gwa;@B)X-Px^DZ3TI+@+_92@8OUSm`0?`QkaTw=V`3ANx9*?cD{m zw@gK3uyLbA|B8Ii_`Pd!hU4PnSiSvc1fLZT_C`0=Rz{H@X)zk*NY+%tCiea6z5Apa z)(d&QIvzg&jo- zrbPv0pK@(@^y~wmTzh}Lo>DuU?$P(%H!%!O+kMg8{y_&lS7zI*>_;)>a1$5rw_rV< z{F+d1Oj7fK|6&b4)}JJK!2639qOHbo_K)p2tgh4dYP0(YOiDdoE`9n80>$rE49MxB z-lJ3Pyc3j{dD~ZYz>}t;vfS2P$)N8zpGVOW36oZckyAP&gh{1uG0F(5gq06$H@~*( zhWmB1ro=1BilYN~Ml!)?}?SxCP#laJAsg!OJ`-#b6BX4lz2T)xr) zszEZ7t7Z!*IC(?7|4C(Rp%)DIaedO?d=)w!wX}Ti%ZZx!?EO)H=YRR)xIgw(rf|>; zA%%L*XCrDvkf_;hTb@6bMe=A5U2OKRfFF1|2w8i2VR%;chyBa%2IvlHxofJy$3_9@*M&Hfz5P8xE7tfn+_EYPD zFTA6gZhTTkjrVcPTTMM#>z%!;xOntGEB_n7S;dMJtt0JD+a-ieiWzApLq*Kz@~vjB z+kN2Gx*sB^hn6q=T!lK-w*7!i9|HUue#xV5l|Dgzx1RnFF5c^bM{^B`V$4{RTY_K- zqcGObn0Jj+bv+iJCb(Ja<^T-*kjBuF9R(8Vyt6+B^+4tQd#1$p$f19$CS%Z+{~PxZ zXVVsg9${p{{5h?+IRkdVYUm+-wKB#OROqZ$@dJ1|U*s27tcJVuEUeG94MC^p++9nq zB51)2!{8THyVv;QRyQv5&$?-e9iCE1r>Wu6TLxMz@D_WzG^-}YTJZ$>rF{eu7NP8+ zDq~=&tg#6_`wf&u1t^OD;ztox`Ls*1E7vY7QA&;Lmix%KI#&vrmFGR2Xf_8A%-A+8 zpB=Eddkb!6#ej=&e1c` zNY=vkpWcF_4rFxjNy+daq`Zp4Ep$8{8hO$`AABi{JSo%jIedZvELAX~h{R#O-CR1VH>%crT% z%6CSE17JgQu4WPsE%Lr1(JtAu7v%j>Pu!_m0_{}K30-3Ahl9sGT@AM^Ah)X1C%qz8 zK4@6!Hyjrq7eN&oerlIEu}yc|-0kHAvG-n!6E@e^BUX?43{ozRz+dO&Hb^SB!Jh7& zHYwJ*P~I+e=BK?FO5RbJvu_8*Wxy?eoOYYwH4UaPRxFc~zOc|y2pLS5&lYKD7&K~gGHq%%Yeg|!L(~zqptNJEHtR)4-V;ub_QRHHGhmEy2Sf8dB0Px z67crpxP<8pE+zq93->BXBGyLOUZHq;a(|(5pV%%u3 zO-uKm(_6@6zgq9a1g%?s2Y!F3dp0r&UL~4(o>OUo88n1 zYPb6+QhqDwC?E1;BpW%S$cymYlu$nz#ELDbE#+J04udnC^%M5AKY(GjpOE33Vi2F6 zbXR_Z8Cq3wKiTsb1($tQ09@zy#M{@RwhAK-^mg?NC=;gBRZuCDvIK+EuQgqM-3LCk z35RFNbU|HAVc=+Q3)uO7>w_InHlw?rxLo=`PuYFoE;_C+&wRCfCFc}qmps+#*(ilz zi%}V!_EMOnsd}zpNF6xv@jc6}ePdANwEP2i$e-CSh_o(5@FZc{7dZ?l7C;^&9r=~KBn9#Y8cLoeEyXscju!xLaRk6aGM$juBYm( zeHdTB^{)!%IC>I5?P+xh>02_We2M4H+s7!kDk7@_;NwH#XP)VuSy8M#TJ#pPBR?Xr z_x?gw4FlGFY1`SY%db+o=aO!TyzYdbi#A&SO7DhBE%V_n8#kfn1Cl-qaZ_H8l5k;H zQzb{*88}J8zF(7juB6=x+0=A|#SXPYhZMhO3!lbdKR>@wqWyc&@!o7gRkH@{HX9n; zBEXNnB2WC>$W7Um`T$Xxx<0e0%4L@T#r+0h4QO|9_ok(QoQbR<;eW%J&Tft2-v+BGe(h&#lj z!i}8%8rt?oOaM!Lrt2Qe0Aq>1@fZ z^y=<_*Hs%i1sI2+W{j!YnE6K-b)%s;S9AeU^fLFcoQqwHU;MQ!9u`bFdD)%;?3lHl z>9(yEg4mqr64d8VLfqd7?<;*Z4*Sz0gX1_0VB+A*dy5{Ap>yyfZwt?PWOvGSx3QGC z{}cCT7$f`Wn+_0+))07?`|s-ACkI^hq>)p4YQ}AG?eOWFG+A29Ua0ctLD}^monTHS zvEh#CJd(+tX%YD_Dsx1NM> z8M}o|xx9gkvF%;h<^hn_C?7NS_6N9?6Me+NwG9Lp2Kl~Oe(J#$ex^(K0p*s0+I2c? zlzp7Xl?(BT5^C9%Du7h*U+#E%R~7l7YLN84un*WgBgFG`cYy{$1|o|`6L_F**L77D zMFsvUzL0o`tm%NiYQ_1IZf;7uv3~ixp_jR7>W~gOaEa;u&Ixsdbo-jF@0LDL85H=v zef&1KoWD727i~W{3QEH&S!7Y}I)@{HT@?2McZYCXMpv}D+lPRuI=%0Fd|C?0Jn1-} zbX^Pi9#ejsEYt#K9?;15%#T1WLv}YQ`&_s?^Ylx(qjO-}h(J|r;ka^iZC*;nJgArcI>pKmL zpnXP3`jhbtpx)!;N01Xj&qUZ+a_pkG^V&Gk)l>@e!4mm|F#C74oImccA%zu_S?&x9 zh<45}i)%+G%&!s&y8WyhsFJkVt(!Zc*tM>cdGUg1qqANUTLxvngX7}*VzxU%Tf;@L zaq_di4xn7 z2vFQ^+|9!GCn_3#Z^n*EVT3H}*sZLzSS{U+F0?@oL)({39IJ-F@~$^xfvp2ZH0RhK zdr%G@X)DdP$@C)S*94}Gx;)nU4R?odIgYF=7h4YmG4c+}qP$I<$g7C4^8LXwh}lBr ztzR2P;9U>d_*kSG2;>we1o$_DIy1tA>LU~M^0o!(7(2>sjXn;zn!YVfk(*gwhrU)B z8%i&u?0@c?;S(#rALJ_Y39r*S; zywZQ;z2l?=noW~kQs=qyx#-INfg0CdcH2I1jUOYj67Tx8h+?#hPdZHkE1<#p@b4R9 zdO=On>y5{s^#Y0h3lkbmweV%QSw`$q2l%68yI{(exu!2QE+a?K(n;+FIC@WrznVxw zkU6Ksu;D`RSAEZhtfCg^ZF;8m)yW=^{ET_qu@^ls>cSD!Pfr)+h#Hi9`GT@u7_RDo z&y$knMw2`Z0$443QE2jdF6688so*M|^%z&>Q0w~ek5KQ!ky84)eh|Df#J+#|d{i_Z zqvu(riCQRXZF|*5*_V*1^nL3xe`Xhn4oRQoVQ(=PK@7bw)O3rr0_Oam*4X7*aGA^W zv9iJ#coE~_(CSwVvW603H~rQ{A7)%Xz-&cvFQ{Fo$>_46GifADc46;6S&Ip9<=Lyz zB6E7=3+cCZ)zuN`^U9jDJwE}$KhB0DgKZ#0SoG8N1|#&_F-MMYdkQY@4z1FrG0L5I z8oB&2eyk1V@v$HeT#>BQoY7wZTsPQ9&Nhz&k5fbM(3v#w_HfPp&z>o;kYp5Vn zU~!~jq^^3c-*8+!&Zb{^np|9(2Rg!?kHeO~GAXqkURWIa3-;via@@LD4}LP64<7o` z13rj1d@fDvg>imbHTmZ?(B9<53U)*iMaqsZZh=n?k5UGIAr7g?1RVP?Ozh+6R zAaZm*EIJZhkXKHxLqee#*gbs0{-|LPs4NvpZobfkuNYiHd0%x7I7 zu>J3L+O9!3yi7V1Y4<@Z0$Q)(n&W`0=@&lY`QkGq%#+lxek6MuxNv0u8Om41 z#+#2mXq+5{?%Uh%s2&W19~xd1Szc>^gmX?0$&aPc#_m_LODzBE!{GW>9n@(+`Z%!B z)4?`GTS3h2z$LQ#W_2uAO?lv0)F>9E{Wrb`* z8IZ+f+i~W@g9xKph>ZIl%KicOOK`qDn*96w`bd~*uWI#&k6i!@Y)~1ttAVsf89h$a z55bNXH!QWnzkrGocj+RX4)EQTB~Qy>6Ur{{czQC*bWPv=RQldLX6W)xO9biLHFPMa zkPeG*8@yd6Er1OP{5n^cFans$TXe1()dLm}Zr1$*b+A=X^@xqTEV_s5P3*>b%4;QR zA7;T<%1?2A`4;y(S^n*Y3TSIE^*#NY6cV$A)kPq51iEXzxzWPY08@_9lzXSw!ibOX zo?n**T68NSaD0Ih5BL*4oOY=vx`QxJLL&Hj5Z^Bquvp!}ESO#$IrXa2^YHR5c456r s!o6EPpoi;0uYz+6JU6(>S4NA7D!zTWojcX@pALNg9^yN%0^<7qKhRc}SpWb4 diff --git a/tests/regression_tests/surface_source_write/case-d07/results_true.dat b/tests/regression_tests/surface_source_write/case-d07/results_true.dat index 63ea1a64d..5a4ea6689 100644 --- a/tests/regression_tests/surface_source_write/case-d07/results_true.dat +++ b/tests/regression_tests/surface_source_write/case-d07/results_true.dat @@ -1,2 +1,2 @@ k-combined: -9.756086E-01 4.639209E-02 +9.947197E-01 3.711779E-02 diff --git a/tests/regression_tests/surface_source_write/case-d07/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-d07/surface_source_true.h5 index 0332f2dcfb100123fe3a4ee3a47f690be2af1bab..7ab2e2c807b2f2080745d6cd48db84e75f9a21b0 100644 GIT binary patch literal 33344 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 diff --git a/tests/regression_tests/surface_source_write/case-d08/results_true.dat b/tests/regression_tests/surface_source_write/case-d08/results_true.dat index 63ea1a64d..5a4ea6689 100644 --- a/tests/regression_tests/surface_source_write/case-d08/results_true.dat +++ b/tests/regression_tests/surface_source_write/case-d08/results_true.dat @@ -1,2 +1,2 @@ k-combined: -9.756086E-01 4.639209E-02 +9.947197E-01 3.711779E-02 diff --git a/tests/regression_tests/surface_source_write/case-d08/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-d08/surface_source_true.h5 index 2525592045ca7912138b0559d72d9d4eaeda6118..5da7564d1f4cd31322ff9a471a6dea175ba8fd0a 100644 GIT binary patch literal 33344 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 Bgy8@H literal 33344 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 diff --git a/tests/regression_tests/surface_source_write/case-e01/results_true.dat b/tests/regression_tests/surface_source_write/case-e01/results_true.dat index 8979eb554..d4d1d1e5a 100644 --- a/tests/regression_tests/surface_source_write/case-e01/results_true.dat +++ b/tests/regression_tests/surface_source_write/case-e01/results_true.dat @@ -1,2 +1,2 @@ k-combined: -5.169123E-02 9.144814E-03 +5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/surface_source_write/case-e01/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-e01/surface_source_true.h5 index 3047519e8c52b140a4756266192c9d81c7d204fb..0dda15ed2192739435e8f50e126831021d620f53 100644 GIT binary patch delta 16344 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 aVrYY7u{m!7cDBerf3wGbaon^L@c#gi>OkTE delta 16353 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 aVrYY7u{m!7cDBerf3wGbaon^L@c#hNxIp6o delta 16353 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 TJDVPnIAXpS-rknc>Fxgq)!}e; literal 33344 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

    l11eivG9^A2FF@o(kK8lbM?n}(nd+t7C#e>`?H zC6C)gdGK=|xNVf6k)K$De&^fTD6DwFa;qE2&u7ybxseT<>-PJ~OtzUXh^T>XbIBxI74S-f2E^v>Q+dKd( z-#@rNdTB^j%8i$t=S}3$pUa_fFekOBW2V6=uy}!eJX0wPR=U(7 z-GrqAvaX#KlQAn0?px1zPh6=DK8ntz%zLklo>gGUN;pEc1HA7~%7f?O-XZe%u0Ozo*!suD$6BDRzI&-Os)jl?)epW= zAajWQhgQ*4brXM0B%Lphy-S|1Um@@I;ti|1A@|)`%!c%ldU<||3O|?Dx6d>(8*WqOWqHTV=3xy;CZ86jVurT+b7$HKF`5_`Nw|s zCnLDt5RY-ogE`f+u!SD1bE?i(0mBEu@)5VH!NR)jch+;&L*ETsZ=K&Ijb366otJAp zb3et;mrOSht=${GHkjXWR}q0>(fFnYN|~IQ2FaS-E9c_x}14*rw?z z_1rEB$ff2O)h%oXo<3{uJkAtEAAkAf(63CM{}BBFeofv_r(&0SeRb0`8L_nrx=$i| zi{LBIGqWl-cfcFT`Jh@L8Jz8i@!WGa8zu~T9*`UCN028cn+1oINp?V#ho7u4+8q@u zi7rN4EcyV0Yowc!Tou%aKSXt(h}%aSBCI7>rGYt>#|0UmPt5=8Uwkt9)`+BzTOMvS zA?J@0IrzU%f$Ob9HMHmZi3T90J;=TA(nS8|KQAgGAE_vFtM3g+IWAM#ZJ7#&+HWVu z?d$}WT8|d08uTK_je-4dT1`mu;JTW$H~f4FSu83N+s@z9HnINcka^L@?#mc>*_ib@ z+^G{BU1{I+=*bs&@`H#?^>`dG`D`|nsW=-ov*9{p`C#V!H$3kNkAXg9AK=U0HogCY z%<1VCe%Cn);+gElTQ}vwRK3W`+wyH7*3UF4+PVPTDhl8%PZmS>MO>C*yGV}Th~v+0 zvLF;@CU5F&CPqwkyPK#R7b7zDHWlfIF&8u@^8D|+RPs}BorJmj|~wGUAq`g5W9?{BFuj6gZNL9d#G^VNu9$kVCl znUv551WJRnm@Wl?TgJBTL#$;$&y;OVjVw?lj4xaTEwQ7Pa-G};$tU1xtWt+FkYLL%o5&bC6 z#O<1|`jN3jei4lPaNm*7@b@}}&%47F(Zc0$SZZC|ouLxom1ckZ7;iHaI(nINEWsaP z1)N^|ZYN24BXa1^N#J^06Y|wtazO(~q(q;UIm3cgItU!fJU4NUp!(y^c$2rl$vyL8 z0bdfB_h8)m!2A~omUr^Uu=P8|47r?AMd+3C+7vGp5-?N7n!YiZCu5S8N01^ z+PSU`AmV1ej`d}*)3x|03)e(`-0}?^xtuM?Xg@{MD2F`$5IMp$4qiCizU8~mb2@&* zGPjn#mv}!4mWfFK_7kxncbA*=tNv_Q&A_^X9}xQw-u}2ec)f#x0}F=@`r+ly^RdB;s^O*TOII%1wS%Z#2lMwb zyn+L_zDu7dErLud9+YH7uR)U^-@i5cEqR@fs5g8)jE;jjGOoN3S;>Ow$aZcOIMNMI z^hc*3C}@MthxXQnX6HkPeH8*#!r8F)e)oY+e}FD)LNa(Zk>}fZn@{Qu*B8!NaOwJm zScB>F=-5|?2DNql1krQ#mKR@3g=pAuYpPl&%*ruecKOr?sM+yqVPmcw>Ma%akohoq z9hTTW!~b=-c=JgcN z!aP`ZTCW_J2gCZp`^%LaL03V$$jjm{V8y9JjBdf5P;p}8r2m2(TDrw5?4<~KzCh%N z;{aNDCZaV0p;&S}6o$Tpo@z5LHUfGpNxulVi;;Mmynf?c2iyzKWiRID;V zZ&Me~MeG57{;! edE5?HrP?A#ixAW#=z`r^N%DD3qCE8bS^o!D&vB6e diff --git a/tests/regression_tests/surface_tally/results_true.dat b/tests/regression_tests/surface_tally/results_true.dat index 8b27fa192..70d5cad2c 100644 --- a/tests/regression_tests/surface_tally/results_true.dat +++ b/tests/regression_tests/surface_tally/results_true.dat @@ -1,52 +1,52 @@ mean,std. dev. -2.1200000e-02,1.8366636e-03 -7.4900000e-02,2.2083176e-03 -1.6220000e-01,2.8079253e-03 -6.4010000e-01,8.6838931e-03 -2.1000000e-03,3.7859389e-04 -5.6000000e-03,6.8637534e-04 -1.1700000e-02,1.4456832e-03 -4.1700000e-02,2.7041121e-03 -2.1200000e-02,1.8366636e-03 -7.4900000e-02,2.2083176e-03 -1.6220000e-01,2.8079253e-03 -6.4010000e-01,8.6838931e-03 -2.1000000e-03,3.7859389e-04 -5.6000000e-03,6.8637534e-04 -1.1700000e-02,1.4456832e-03 -4.1700000e-02,2.7041121e-03 -5.2000000e-03,5.9254629e-04 -3.9200000e-02,2.5508169e-03 -4.0200000e-02,1.9310331e-03 -4.1360000e-01,8.0072190e-03 -0.0000000e+00,0.0000000e+00 -1.9000000e-03,3.7859389e-04 +2.4100000e-02,2.1052844e-03 +6.7900000e-02,2.3211587e-03 +1.6860000e-01,4.4800794e-03 +6.4710000e-01,9.1826527e-03 +2.2000000e-03,3.8873013e-04 +5.5000000e-03,1.0979779e-03 +1.2500000e-02,1.5438048e-03 +4.4700000e-02,1.9035055e-03 +2.4100000e-02,2.1052844e-03 +6.7900000e-02,2.3211587e-03 +1.6860000e-01,4.4800794e-03 +6.4710000e-01,9.1826527e-03 +2.2000000e-03,3.8873013e-04 +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.6400000e-02,1.2840907e-03 --5.2000000e-03,5.9254629e-04 --3.9200000e-02,2.5508169e-03 --4.0200000e-02,1.9310331e-03 --4.1360000e-01,8.0072190e-03 -0.0000000e+00,0.0000000e+00 --1.9000000e-03,3.7859389e-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.6400000e-02,1.2840907e-03 -1.6000000e-02,2.1602469e-03 -3.5700000e-02,2.9441090e-03 -1.2200000e-01,3.5932035e-03 -2.2650000e-01,9.2463206e-03 +-1.5000000e-03,4.5338235e-04 +-3.0000000e-04,1.5275252e-04 +-1.7300000e-02,1.4609738e-03 +1.6800000e-02,1.5902481e-03 +3.1300000e-02,3.8094911e-03 +1.2700000e-01,5.0990195e-03 +2.2330000e-01,9.3631073e-03 2.1000000e-03,3.7859389e-04 -3.7000000e-03,8.1717671e-04 -1.1600000e-02,1.4772347e-03 -2.5300000e-02,1.9723083e-03 +4.0000000e-03,1.0540926e-03 +1.2200000e-02,1.6110728e-03 +2.7400000e-02,1.6613248e-03 0.0000000e+00,0.0000000e+00 --2.9700000e-02,2.4587711e-03 +-3.2000000e-02,1.5634719e-03 0.0000000e+00,0.0000000e+00 --3.5090000e-01,3.7161808e-03 +-3.5400000e-01,7.5938572e-03 0.0000000e+00,0.0000000e+00 --2.1000000e-03,4.8189441e-04 +-3.0000000e-03,6.4978629e-04 0.0000000e+00,0.0000000e+00 --2.1400000e-02,1.7397318e-03 +-2.1800000e-02,1.4892205e-03 0.0000000e+00,0.0000000e+00 0.0000000e+00,0.0000000e+00 0.0000000e+00,0.0000000e+00 diff --git a/tests/regression_tests/survival_biasing/results_true.dat b/tests/regression_tests/survival_biasing/results_true.dat index 740272201..932414e98 100644 --- a/tests/regression_tests/survival_biasing/results_true.dat +++ b/tests/regression_tests/survival_biasing/results_true.dat @@ -1,20 +1,20 @@ k-combined: -9.879232E-01 8.097582E-03 +9.517646E-01 1.303111E-02 tally 1: -4.295331E+01 -3.691096E+02 -1.802724E+01 -6.501934E+01 -2.193500E+00 -9.627609E-01 -1.893529E+00 -7.174104E-01 -4.902380E+00 -4.808570E+00 -3.418014E-02 -2.337353E-04 -3.667128E+08 -2.690758E+16 +4.164635E+01 +3.470110E+02 +1.724300E+01 +5.949580E+01 +2.124917E+00 +9.034789E-01 +1.844790E+00 +6.809026E-01 +4.784396E+00 +4.579552E+00 +3.348849E-02 +2.243613E-04 +3.573090E+08 +2.554338E+16 tally 2: -1.802724E+01 -6.501934E+01 +1.724300E+01 +5.949580E+01 diff --git a/tests/regression_tests/tallies/results_true.dat b/tests/regression_tests/tallies/results_true.dat index 3dda0f9b2..1d3aca4b0 100644 --- a/tests/regression_tests/tallies/results_true.dat +++ b/tests/regression_tests/tallies/results_true.dat @@ -1 +1 @@ -ddfbb0a6f5498eb8ff33bb10beb64e244b015861919eb4837bd82855e5fd87c3ff97dfa382d3d60afa81c48d9f857fba8e0b99263e7b35587f8adb75be1fc0ec \ No newline at end of file +d01c3accd5b4de2aa166a77df28cfe42f5738a44c2480752fcfae7564a507362fff006b6dffb7b1dfe248e14bacef0070cabacea5d75c5996653e5605f7c7384 \ No newline at end of file diff --git a/tests/regression_tests/tally_aggregation/results_true.dat b/tests/regression_tests/tally_aggregation/results_true.dat index 172adbfe1..ee6263373 100644 --- a/tests/regression_tests/tally_aggregation/results_true.dat +++ b/tests/regression_tests/tally_aggregation/results_true.dat @@ -1,97 +1,97 @@ -[[1.6001087e-05 5.4190949e-04] - [3.2669968e-01 1.7730523e-01] - [1.8149266e-02 7.1525113e-01]], [[1.6239097e-05 5.7499676e-04] - [3.1268633e-01 1.7004165e-01] - [1.8873778e-02 7.0217885e-01]], [[1.6693071e-05 5.4106379e-04] - [3.3370208e-01 1.8051505e-01] - [1.9081306e-02 7.3647547e-01]], [[1.6725399e-05 5.1680146e-04] - [3.2854628e-01 1.7757185e-01] - [1.9529646e-02 7.1005198e-01]][[2.4751834e-07 4.3304565e-05] - [8.6840718e-03 4.3442535e-03] - [3.7054051e-04 6.5678133e-03]], [[4.0830852e-07 4.9612204e-05] - [1.2104241e-02 5.9708675e-03] - [7.1398189e-04 8.2408482e-03]], [[2.6546344e-07 2.5234256e-05] - [6.5083211e-03 3.2185071e-03] - [4.2935150e-04 5.1707774e-03]], [[2.7845203e-07 3.4453402e-05] - [3.3125427e-03 1.7749508e-03] - [6.0407731e-04 7.5353872e-03]][[1.0455251e-06 8.1938339e-04] - [1.1392765e+00 5.6434761e-01] - [1.4142831e-06 5.1213654e-01]], [[2.2009815e-06 1.0418935e-03] - [1.4422554e-01 1.0070647e-01] - [3.9488382e-05 7.9236390e-01]], [[1.4606612e-05 2.2374470e-04] - [1.2962842e-02 2.9268594e-02] - [3.1339824e-04 1.1056439e+00]], [[4.7805535e-05 8.9749934e-05] - [5.1694597e-03 1.1111105e-02] - [7.5279695e-02 4.5381305e-01]][[1.4955183e-08 1.1461548e-05] - [1.6454649e-02 8.1236707e-03] - [2.0028007e-08 6.8264819e-03]], [[1.6334486e-07 7.7617340e-05] - [2.1161584e-03 1.4078445e-03] - [1.4742097e-05 6.9350862e-03]], [[1.7444757e-07 1.9026095e-06] - [1.4084092e-04 2.0345990e-04] - [5.9546175e-06 8.6038812e-03]], [[5.6449127e-07 1.0110585e-06] - [5.9158873e-05 1.2485325e-04] - [1.0936497e-03 5.0836702e-03]][[0.2866239 0.2725595]], [[0.2729156 0.258831 ]], [[0.2920724 0.2755922]], [[0.287667 0.2703207]], [[0.035617 0.2232707]], [[0.0353082 0.2224423]], [[0.0370072 0.2288263]], [[0.0363348 0.219573 ]], [[0.0033013 0.2850034]], [[0.0032726 0.2771099]], [[0.0034234 0.2951295]], [[0.0032935 0.2778935]], [[0.0193227 0.1122647]], [[0.0200799 0.1144123]], [[0.0202971 0.1179836]], [[0.0207973 0.1203534]][[0.0086351 0.0061876]], [[0.0120688 0.0074831]], [[0.0064221 0.0035246]], [[0.0030483 0.0024267]], [[0.0009185 0.0033896]], [[0.0009223 0.0039584]], [[0.0010541 0.0038599]], [[0.0012934 0.0028331]], [[6.5126963e-05 2.8486593e-03]], [[7.1110242e-05 4.3424340e-03]], [[5.8911409e-05 2.4296178e-03]], [[8.4278765e-05 6.4182190e-03]], [[0.0003712 0.0020298]], [[0.0007152 0.0036115]], [[0.0004298 0.0019677]], [[0.0006046 0.0021965]][[2.0694650e-04] - [4.2868191e-01] - [1.3029457e-01]], [[1.9694048e-04] - [4.0809445e-01] - [1.2345525e-01]], [[2.1012645e-04] - [4.3670571e-01] - [1.3074883e-01]], [[2.0641548e-04] - [4.3014207e-01] - [1.2763930e-01]], [[0.0002584] - [0.0608867] - [0.1977426]], [[0.0003026] - [0.0602368] - [0.1972111]], [[0.0002509] - [0.0624699] - [0.2031126]], [[0.0002322] - [0.0613385] - [0.1943371]], [[5.9349736e-05] - [1.0506741e-02] - [2.7773865e-01]], [[5.7855564e-05] - [1.0389781e-02] - [2.6993483e-01]], [[6.1819700e-05] - [1.0910874e-02] - [2.8758020e-01]], [[5.9326317e-05] - [1.0424040e-02] - [2.7070367e-01]], [[3.3192167e-05] - [3.9295320e-03] - [1.2762462e-01]], [[3.3882691e-05] - [4.0069157e-03] - [1.3045143e-01]], [[3.4882103e-05] - [4.1306182e-03] - [1.3411514e-01]], [[3.5598508e-05] - [4.2134987e-03] - [1.3690156e-01]][[6.4505980e-06] - [9.6369034e-03] - [4.4700460e-03]], [[8.2880772e-06] - [1.3456171e-02] - [4.5368708e-03]], [[3.9384462e-06] - [7.1570638e-03] - [1.5629142e-03]], [[2.3565894e-06] - [3.4040401e-03] - [1.8956916e-03]], [[4.2806047e-05] - [1.1841015e-03] - [3.3058802e-03]], [[4.8905696e-05] - [1.0360139e-03] - [3.9298937e-03]], [[2.4911390e-05] - [1.2173305e-03] - [3.8114473e-03]], [[3.4347869e-05] - [1.5820360e-03] - [2.6824612e-03]], [[1.0809230e-06] - [1.0316180e-04] - [2.8475354e-03]], [[6.4150814e-07] - [1.1179425e-04] - [4.3415770e-03]], [[7.3848139e-07] - [9.3111666e-05] - [2.4285475e-03]], [[1.2349384e-06] - [1.7152843e-04] - [6.4164799e-03]], [[4.5871493e-07] - [5.4732075e-05] - [2.0627309e-03]], [[8.1649994e-07] - [9.7699188e-05] - [3.6803256e-03]], [[4.5174850e-07] - [5.3913784e-05] - [2.0133571e-03]], [[5.0962865e-07] - [6.0338032e-05] - [2.2773911e-03]] \ No newline at end of file +[[1.6242805e-05 6.2367673e-04] + [3.2895972e-01 1.7786452e-01] + [1.8044266e-02 7.0451122e-01]], [[1.6113947e-05 5.3572864e-04] + [3.1517504e-01 1.7132833e-01] + [1.8305682e-02 7.0072832e-01]], [[1.6472052e-05 5.5758006e-04] + [3.2362364e-01 1.7597163e-01] + [1.9107080e-02 7.2600440e-01]], [[1.6693277e-05 4.9204218e-04] + [3.2429262e-01 1.7600573e-01] + [1.9042489e-02 7.3053854e-01]][[2.9719061e-07 8.0925438e-05] + [8.9432259e-03 4.4057583e-03] + [5.2078407e-04 6.4387688e-03]], [[1.8572081e-07 2.5667235e-05] + [1.1641603e-02 5.7444311e-03] + [2.6983611e-04 6.9783224e-03]], [[2.4994113e-07 5.4993390e-05] + [6.0464041e-03 3.0764811e-03] + [2.6559118e-04 7.7279780e-03]], [[2.5965232e-07 3.9618775e-05] + [1.0409564e-02 5.1850104e-03] + [2.7831231e-04 7.4480576e-03]][[1.0329435e-06 8.0861229e-04] + [1.1265142e+00 5.5796079e-01] + [1.3965860e-06 5.0338828e-01]], [[2.2264172e-06 1.0828687e-03] + [1.4724979e-01 1.0226107e-01] + [2.2656045e-05 7.7618945e-01]], [[1.4789782e-05 2.2786630e-04] + [1.3149968e-02 2.9847169e-02] + [3.2638111e-04 1.1287326e+00]], [[4.7472938e-05 8.9680338e-05] + [5.1371041e-03 1.1101179e-02] + [7.4149084e-02 4.5347217e-01]][[1.7024771e-08 1.2878244e-05] + [1.8795566e-02 9.2705008e-03] + [2.2676749e-08 7.0878353e-03]], [[2.2018925e-07 1.0785388e-04] + [2.6715019e-03 1.6657928e-03] + [1.2479083e-05 8.6814018e-03]], [[2.0928485e-07 1.6600875e-06] + [1.1859535e-04 1.7969083e-04] + [1.5981387e-05 8.0489308e-03]], [[4.0016381e-07 7.6799157e-07] + [4.4157583e-05 9.4462925e-05] + [7.0115110e-04 3.8678806e-03]][[0.287433 0.2698042]], [[0.2751228 0.2598525]], [[0.280516 0.2648274]], [[0.2834448 0.2676736]], [[0.0370558 0.2148509]], [[0.0355217 0.2130189]], [[0.0385317 0.2292804]], [[0.0361655 0.2223832]], [[0.0033268 0.2855078]], [[0.0033498 0.2849403]], [[0.003359 0.2901157]], [[0.0034555 0.2982438]], [[0.0192046 0.1128364]], [[0.0195026 0.1147807]], [[0.0203405 0.1183102]], [[0.0202861 0.1187358]][[0.00889 0.0051934]], [[0.0115249 0.0072767]], [[0.0058841 0.0040289]], [[0.0103341 0.0063266]], [[0.0009734 0.0048865]], [[0.0016425 0.0038133]], [[0.0013902 0.0048875]], [[0.0012487 0.0039808]], [[1.8994140e-05 2.0410145e-03]], [[6.8277082e-05 3.4865782e-03]], [[6.1643568e-05 4.9220817e-03]], [[7.4147413e-05 4.9263295e-03]], [[0.0005213 0.0024208]], [[0.0002702 0.0014313]], [[0.0002665 0.0022006]], [[0.0002788 0.0014892]][[2.0601081e-04] + [4.2976789e-01] + [1.2726338e-01]], [[1.9802070e-04] + [4.1138025e-01] + [1.2339705e-01]], [[2.0174854e-04] + [4.1947542e-01] + [1.2566615e-01]], [[2.0386518e-04] + [4.2385140e-01] + [1.2706310e-01]], [[0.0003403] + [0.0624955] + [0.189071 ]], [[0.0002601] + [0.060449 ] + [0.1878314]], [[0.0002765] + [0.0652412] + [0.2022944]], [[0.0002082] + [0.0613252] + [0.1970153]], [[6.0336874e-05] + [1.0617692e-02] + [2.7815664e-01]], [[5.9823997e-05] + [1.0664059e-02] + [2.7756620e-01]], [[6.0873689e-05] + [1.0744209e-02] + [2.8266963e-01]], [[6.1621521e-05] + [1.0971177e-02] + [2.9066650e-01]], [[3.3297391e-05] + [3.9432037e-03] + [1.2806450e-01]], [[3.3863464e-05] + [4.0100180e-03] + [1.3023932e-01]], [[3.4930617e-05] + [4.1344613e-03] + [1.3448129e-01]], [[3.5061805e-05] + [4.1505999e-03] + [1.3483614e-01]][[5.8259835e-06] + [9.9096571e-03] + [2.7933778e-03]], [[8.0858909e-06] + [1.2843377e-02] + [4.5631248e-03]], [[4.3418585e-06] + [6.5638646e-03] + [2.7874612e-03]], [[6.9046376e-06] + [1.1531588e-02] + [3.7205377e-03]], [[8.0710599e-05] + [1.0889301e-03] + [4.8613465e-03]], [[2.4337121e-05] + [1.8861117e-03] + [3.6987509e-03]], [[5.4812439e-05] + [1.7085353e-03] + [4.7852139e-03]], [[3.9007791e-05] + [1.4998363e-03] + [3.8929646e-03]], [[7.5757577e-07] + [2.5845755e-05] + [2.0409391e-03]], [[1.0302346e-06] + [1.1907557e-04] + [3.4852130e-03]], [[9.1936614e-07] + [1.2974020e-04] + [4.9207575e-03]], [[5.6493555e-07] + [1.2113685e-04] + [4.9253980e-03]], [[5.4602210e-07] + [6.5233025e-05] + [2.4754550e-03]], [[3.2091909e-07] + [3.8472906e-05] + [1.4560974e-03]], [[4.8475651e-07] + [5.9108262e-05] + [2.2158781e-03]], [[3.3737822e-07] + [4.0544269e-05] + [1.5145628e-03]] \ No newline at end of file diff --git a/tests/regression_tests/tally_arithmetic/results_true.dat b/tests/regression_tests/tally_arithmetic/results_true.dat index 143930dd4..baa380587 100644 --- a/tests/regression_tests/tally_arithmetic/results_true.dat +++ b/tests/regression_tests/tally_arithmetic/results_true.dat @@ -1,49 +1,49 @@ -[2.04229e-07 1.28343e-13 1.79333e-04 1.12698e-10 1.89952e-07 1.51210e-07 - 1.66796e-04 1.32777e-04 6.13215e-03 3.85362e-09 3.14746e-03 1.97795e-09 - 5.70345e-03 4.54021e-03 2.92742e-03 2.33037e-03 2.10575e-07 1.28419e-13 - 1.84905e-04 1.12764e-10 1.89188e-07 1.50221e-07 1.66125e-04 1.31908e-04 - 6.32269e-03 3.85587e-09 3.24526e-03 1.97911e-09 5.68054e-03 4.51051e-03 - 2.91566e-03 2.31512e-03 1.96696e-07 1.26388e-13 1.72718e-04 1.10981e-10 - 1.91283e-07 1.53449e-07 1.67964e-04 1.34743e-04 5.90596e-03 3.79491e-09 - 3.03136e-03 1.94782e-09 5.74342e-03 4.60742e-03 2.94794e-03 2.36486e-03 - 2.06016e-07 1.35264e-13 1.80901e-04 1.18775e-10 2.05873e-07 1.65814e-07 - 1.80776e-04 1.45600e-04 6.18579e-03 4.06142e-09 3.17499e-03 2.08461e-09 - 6.18150e-03 4.97869e-03 3.17279e-03 2.55542e-03 2.36089e-03 4.46651e-05 - 1.02263e-02 1.93469e-04 1.18179e-04 2.62375e-05 5.11897e-04 1.13649e-04 - 2.56040e-02 4.84394e-04 4.55595e-02 8.61927e-04 1.28165e-03 2.84546e-04 - 2.28056e-03 5.06320e-04 2.29877e-03 4.53056e-05 9.95724e-03 1.96244e-04 - 1.14803e-04 2.57898e-05 4.97276e-04 1.11710e-04 2.49302e-02 4.91340e-04 - 4.43606e-02 8.74289e-04 1.24504e-03 2.79691e-04 2.21542e-03 4.97680e-04 - 2.31940e-03 4.34238e-05 1.00466e-02 1.88093e-04 1.17620e-04 2.69280e-05 - 5.09479e-04 1.16640e-04 2.51540e-02 4.70932e-04 4.47588e-02 8.37974e-04 - 1.27560e-03 2.92034e-04 2.26979e-03 5.19644e-04 2.19212e-03 4.53914e-05 - 9.49530e-03 1.96615e-04 1.09609e-04 2.41615e-05 4.74778e-04 1.04657e-04 - 2.37736e-02 4.92271e-04 4.23026e-02 8.75944e-04 1.18871e-03 2.62032e-04 - 2.11519e-03 4.66257e-04][2.04229e-07 1.28343e-13 1.79333e-04 1.12698e-10 1.89952e-07 1.51210e-07 - 1.66796e-04 1.32777e-04 6.13215e-03 3.85362e-09 3.14746e-03 1.97795e-09 - 5.70345e-03 4.54021e-03 2.92742e-03 2.33037e-03 2.10575e-07 1.28419e-13 - 1.84905e-04 1.12764e-10 1.89188e-07 1.50221e-07 1.66125e-04 1.31908e-04 - 6.32269e-03 3.85587e-09 3.24526e-03 1.97911e-09 5.68054e-03 4.51051e-03 - 2.91566e-03 2.31512e-03 1.96696e-07 1.26388e-13 1.72718e-04 1.10981e-10 - 1.91283e-07 1.53449e-07 1.67964e-04 1.34743e-04 5.90596e-03 3.79491e-09 - 3.03136e-03 1.94782e-09 5.74342e-03 4.60742e-03 2.94794e-03 2.36486e-03 - 2.06016e-07 1.35264e-13 1.80901e-04 1.18775e-10 2.05873e-07 1.65814e-07 - 1.80776e-04 1.45600e-04 6.18579e-03 4.06142e-09 3.17499e-03 2.08461e-09 - 6.18150e-03 4.97869e-03 3.17279e-03 2.55542e-03 2.36089e-03 4.46651e-05 - 1.02263e-02 1.93469e-04 1.18179e-04 2.62375e-05 5.11897e-04 1.13649e-04 - 2.56040e-02 4.84394e-04 4.55595e-02 8.61927e-04 1.28165e-03 2.84546e-04 - 2.28056e-03 5.06320e-04 2.29877e-03 4.53056e-05 9.95724e-03 1.96244e-04 - 1.14803e-04 2.57898e-05 4.97276e-04 1.11710e-04 2.49302e-02 4.91340e-04 - 4.43606e-02 8.74289e-04 1.24504e-03 2.79691e-04 2.21542e-03 4.97680e-04 - 2.31940e-03 4.34238e-05 1.00466e-02 1.88093e-04 1.17620e-04 2.69280e-05 - 5.09479e-04 1.16640e-04 2.51540e-02 4.70932e-04 4.47588e-02 8.37974e-04 - 1.27560e-03 2.92034e-04 2.26979e-03 5.19644e-04 2.19212e-03 4.53914e-05 - 9.49530e-03 1.96615e-04 1.09609e-04 2.41615e-05 4.74778e-04 1.04657e-04 - 2.37736e-02 4.92271e-04 4.23026e-02 8.75944e-04 1.18871e-03 2.62032e-04 - 2.11519e-03 4.66257e-04][0.0057 0.00454 0.00293 0.00233 0.00568 0.00451 0.00292 0.00232 0.00574 - 0.00461 0.00295 0.00236 0.00618 0.00498 0.00317 0.00256 0.00128 0.00028 - 0.00228 0.00051 0.00125 0.00028 0.00222 0.0005 0.00128 0.00029 0.00227 - 0.00052 0.00119 0.00026 0.00212 0.00047][0.00018 0.00017 0.00315 0.00293 0.00018 0.00017 0.00325 0.00292 0.00017 - 0.00017 0.00303 0.00295 0.00018 0.00018 0.00317 0.00317 0.01023 0.00051 - 0.04556 0.00228 0.00996 0.0005 0.04436 0.00222 0.01005 0.00051 0.04476 - 0.00227 0.0095 0.00047 0.0423 0.00212][0.00293 0.00292 0.00295 0.00317 0.00228 0.00222 0.00227 0.00212] \ No newline at end of file +[2.29467e-07 1.47622e-13 2.02646e-04 1.30367e-10 2.20704e-07 1.76624e-07 + 1.94907e-04 1.55980e-04 6.32267e-03 4.06754e-09 3.26873e-03 2.10286e-09 + 6.08121e-03 4.86666e-03 3.14390e-03 2.51599e-03 1.89223e-07 1.06256e-13 + 1.67106e-04 9.38364e-11 1.53300e-07 1.20220e-07 1.35382e-04 1.06168e-04 + 5.21380e-03 2.92775e-09 2.69546e-03 1.51361e-09 4.22399e-03 3.31252e-03 + 2.18374e-03 1.71253e-03 2.21146e-07 1.41349e-13 1.95297e-04 1.24828e-10 + 2.11844e-07 1.69398e-07 1.87083e-04 1.49598e-04 6.09339e-03 3.89470e-09 + 3.15020e-03 2.01351e-09 5.83710e-03 4.66754e-03 3.01770e-03 2.41305e-03 + 1.92872e-07 1.05908e-13 1.70328e-04 9.35293e-11 1.59466e-07 1.25399e-07 + 1.40827e-04 1.10741e-04 5.31433e-03 2.91817e-09 2.74744e-03 1.50865e-09 + 4.39388e-03 3.45520e-03 2.27158e-03 1.78629e-03 2.44601e-03 4.71663e-05 + 1.09430e-02 2.11013e-04 1.23573e-04 2.76181e-05 5.52841e-04 1.23558e-04 + 2.71755e-02 5.24023e-04 4.76200e-02 9.18253e-04 1.37291e-03 3.06841e-04 + 2.40577e-03 5.37681e-04 2.29014e-03 4.44674e-05 1.02456e-02 1.98938e-04 + 1.12524e-04 2.48126e-05 5.03411e-04 1.11007e-04 2.54438e-02 4.94038e-04 + 4.45855e-02 8.65710e-04 1.25016e-03 2.75671e-04 2.19067e-03 4.83061e-04 + 2.38500e-03 4.58053e-05 1.06700e-02 2.04924e-04 1.22899e-04 2.95267e-05 + 5.49826e-04 1.32097e-04 2.64977e-02 5.08903e-04 4.64323e-02 8.91758e-04 + 1.36542e-03 3.28045e-04 2.39265e-03 5.74839e-04 2.17094e-03 4.21772e-05 + 9.71236e-03 1.88693e-04 1.09358e-04 2.46984e-05 4.89245e-04 1.10496e-04 + 2.41194e-02 4.68594e-04 4.22648e-02 8.21124e-04 1.21498e-03 2.74402e-04 + 2.12902e-03 4.80839e-04][2.29467e-07 1.47622e-13 2.02646e-04 1.30367e-10 2.20704e-07 1.76624e-07 + 1.94907e-04 1.55980e-04 6.32267e-03 4.06754e-09 3.26873e-03 2.10286e-09 + 6.08121e-03 4.86666e-03 3.14390e-03 2.51599e-03 1.89223e-07 1.06256e-13 + 1.67106e-04 9.38364e-11 1.53300e-07 1.20220e-07 1.35382e-04 1.06168e-04 + 5.21380e-03 2.92775e-09 2.69546e-03 1.51361e-09 4.22399e-03 3.31252e-03 + 2.18374e-03 1.71253e-03 2.21146e-07 1.41349e-13 1.95297e-04 1.24828e-10 + 2.11844e-07 1.69398e-07 1.87083e-04 1.49598e-04 6.09339e-03 3.89470e-09 + 3.15020e-03 2.01351e-09 5.83710e-03 4.66754e-03 3.01770e-03 2.41305e-03 + 1.92872e-07 1.05908e-13 1.70328e-04 9.35293e-11 1.59466e-07 1.25399e-07 + 1.40827e-04 1.10741e-04 5.31433e-03 2.91817e-09 2.74744e-03 1.50865e-09 + 4.39388e-03 3.45520e-03 2.27158e-03 1.78629e-03 2.44601e-03 4.71663e-05 + 1.09430e-02 2.11013e-04 1.23573e-04 2.76181e-05 5.52841e-04 1.23558e-04 + 2.71755e-02 5.24023e-04 4.76200e-02 9.18253e-04 1.37291e-03 3.06841e-04 + 2.40577e-03 5.37681e-04 2.29014e-03 4.44674e-05 1.02456e-02 1.98938e-04 + 1.12524e-04 2.48126e-05 5.03411e-04 1.11007e-04 2.54438e-02 4.94038e-04 + 4.45855e-02 8.65710e-04 1.25016e-03 2.75671e-04 2.19067e-03 4.83061e-04 + 2.38500e-03 4.58053e-05 1.06700e-02 2.04924e-04 1.22899e-04 2.95267e-05 + 5.49826e-04 1.32097e-04 2.64977e-02 5.08903e-04 4.64323e-02 8.91758e-04 + 1.36542e-03 3.28045e-04 2.39265e-03 5.74839e-04 2.17094e-03 4.21772e-05 + 9.71236e-03 1.88693e-04 1.09358e-04 2.46984e-05 4.89245e-04 1.10496e-04 + 2.41194e-02 4.68594e-04 4.22648e-02 8.21124e-04 1.21498e-03 2.74402e-04 + 2.12902e-03 4.80839e-04][0.00608 0.00487 0.00314 0.00252 0.00422 0.00331 0.00218 0.00171 0.00584 + 0.00467 0.00302 0.00241 0.00439 0.00346 0.00227 0.00179 0.00137 0.00031 + 0.00241 0.00054 0.00125 0.00028 0.00219 0.00048 0.00137 0.00033 0.00239 + 0.00057 0.00121 0.00027 0.00213 0.00048][0.0002 0.00019 0.00327 0.00314 0.00017 0.00014 0.0027 0.00218 0.0002 + 0.00019 0.00315 0.00302 0.00017 0.00014 0.00275 0.00227 0.01094 0.00055 + 0.04762 0.00241 0.01025 0.0005 0.04459 0.00219 0.01067 0.00055 0.04643 + 0.00239 0.00971 0.00049 0.04226 0.00213][0.00314 0.00218 0.00302 0.00227 0.00241 0.00219 0.00239 0.00213] \ No newline at end of file diff --git a/tests/regression_tests/tally_assumesep/results_true.dat b/tests/regression_tests/tally_assumesep/results_true.dat index 9d7cbf9c3..c9ccf0928 100644 --- a/tests/regression_tests/tally_assumesep/results_true.dat +++ b/tests/regression_tests/tally_assumesep/results_true.dat @@ -1,11 +1,11 @@ k-combined: -5.683578E-01 1.129170E-02 +6.268465E-01 1.154810E-02 tally 1: -6.753950E+00 -9.188305E+00 +7.828708E+00 +1.230478E+01 tally 2: -2.278558E-01 -1.052944E-02 +2.582239E-01 +1.360117E-02 tally 3: -1.179091E+01 -2.795539E+01 +1.339335E+01 +3.663458E+01 diff --git a/tests/regression_tests/tally_nuclides/results_true.dat b/tests/regression_tests/tally_nuclides/results_true.dat index 7f9641fc8..93a0e03fb 100644 --- a/tests/regression_tests/tally_nuclides/results_true.dat +++ b/tests/regression_tests/tally_nuclides/results_true.dat @@ -1,28 +1,28 @@ k-combined: -1.132463E+00 5.721067E-02 +9.732610E-01 1.400780E-02 tally 1: -7.310528E+00 -1.080411E+01 -1.664215E+00 -5.567756E-01 -1.609340E+00 -5.205634E-01 -5.646313E+00 -6.459724E+00 -7.310528E+00 -1.080411E+01 -1.664215E+00 -5.567756E-01 -1.609340E+00 -5.205634E-01 -5.646313E+00 -6.459724E+00 +7.123025E+00 +1.021136E+01 +1.619905E+00 +5.258592E-01 +1.561322E+00 +4.881841E-01 +5.503120E+00 +6.105683E+00 +7.123025E+00 +1.021136E+01 +1.619905E+00 +5.258592E-01 +1.561322E+00 +4.881841E-01 +5.503120E+00 +6.105683E+00 tally 2: -7.310528E+00 -1.080411E+01 -1.664215E+00 -5.567756E-01 -1.609340E+00 -5.205634E-01 -5.646313E+00 -6.459724E+00 +7.123025E+00 +1.021136E+01 +1.619905E+00 +5.258592E-01 +1.561322E+00 +4.881841E-01 +5.503120E+00 +6.105683E+00 diff --git a/tests/regression_tests/tally_slice_merge/results_true.dat b/tests/regression_tests/tally_slice_merge/results_true.dat index 58b15fc18..4b2cbdf1a 100644 --- a/tests/regression_tests/tally_slice_merge/results_true.dat +++ b/tests/regression_tests/tally_slice_merge/results_true.dat @@ -1,45 +1,45 @@ cell energy low [eV] energy high [eV] nuclide score mean std. dev. -0 21 0.00e+00 6.25e-01 U235 fission 1.75e-01 1.20e-02 cell energy low [eV] energy high [eV] nuclide score mean std. dev. -0 21 0.00e+00 6.25e-01 U235 nu-fission 4.26e-01 2.92e-02 cell energy low [eV] energy high [eV] nuclide score mean std. dev. -0 21 0.00e+00 6.25e-01 U238 fission 2.41e-07 1.61e-08 cell energy low [eV] energy high [eV] nuclide score mean std. dev. -0 21 0.00e+00 6.25e-01 U238 nu-fission 6.00e-07 4.01e-08 cell energy low [eV] energy high [eV] nuclide score mean std. dev. -0 21 6.25e-01 2.00e+07 U235 fission 2.89e-02 2.07e-03 cell energy low [eV] energy high [eV] nuclide score mean std. dev. -0 21 6.25e-01 2.00e+07 U235 nu-fission 7.07e-02 5.06e-03 cell energy low [eV] energy high [eV] nuclide score mean std. dev. -0 21 6.25e-01 2.00e+07 U238 fission 1.41e-02 5.76e-04 cell energy low [eV] energy high [eV] nuclide score mean std. dev. -0 21 6.25e-01 2.00e+07 U238 nu-fission 3.95e-02 1.90e-03 cell energy low [eV] energy high [eV] nuclide score mean std. dev. -0 27 0.00e+00 6.25e-01 U235 fission 1.51e-01 1.17e-02 cell energy low [eV] energy high [eV] nuclide score mean std. dev. -0 27 0.00e+00 6.25e-01 U235 nu-fission 3.69e-01 2.86e-02 cell energy low [eV] energy high [eV] nuclide score mean std. dev. -0 27 0.00e+00 6.25e-01 U238 fission 2.07e-07 1.47e-08 cell energy low [eV] energy high [eV] nuclide score mean std. dev. -0 27 0.00e+00 6.25e-01 U238 nu-fission 5.17e-07 3.66e-08 cell energy low [eV] energy high [eV] nuclide score mean std. dev. -0 27 6.25e-01 2.00e+07 U235 fission 2.57e-02 2.43e-03 cell energy low [eV] energy high [eV] nuclide score mean std. dev. -0 27 6.25e-01 2.00e+07 U235 nu-fission 6.30e-02 5.92e-03 cell energy low [eV] energy high [eV] nuclide score mean std. dev. -0 27 6.25e-01 2.00e+07 U238 fission 1.47e-02 1.41e-03 cell energy low [eV] energy high [eV] nuclide score mean std. dev. -0 27 6.25e-01 2.00e+07 U238 nu-fission 4.12e-02 4.02e-03 cell energy low [eV] energy high [eV] nuclide score mean std. dev. -0 21 0.00e+00 6.25e-01 U235 fission 1.75e-01 1.20e-02 -1 21 0.00e+00 6.25e-01 U235 nu-fission 4.26e-01 2.92e-02 -2 21 0.00e+00 6.25e-01 U238 fission 2.41e-07 1.61e-08 -3 21 0.00e+00 6.25e-01 U238 nu-fission 6.00e-07 4.01e-08 -4 21 6.25e-01 2.00e+07 U235 fission 2.89e-02 2.07e-03 -5 21 6.25e-01 2.00e+07 U235 nu-fission 7.07e-02 5.06e-03 -6 21 6.25e-01 2.00e+07 U238 fission 1.41e-02 5.76e-04 -7 21 6.25e-01 2.00e+07 U238 nu-fission 3.95e-02 1.90e-03 -8 27 0.00e+00 6.25e-01 U235 fission 1.51e-01 1.17e-02 -9 27 0.00e+00 6.25e-01 U235 nu-fission 3.69e-01 2.86e-02 -10 27 0.00e+00 6.25e-01 U238 fission 2.07e-07 1.47e-08 -11 27 0.00e+00 6.25e-01 U238 nu-fission 5.17e-07 3.66e-08 -12 27 6.25e-01 2.00e+07 U235 fission 2.57e-02 2.43e-03 -13 27 6.25e-01 2.00e+07 U235 nu-fission 6.30e-02 5.92e-03 -14 27 6.25e-01 2.00e+07 U238 fission 1.47e-02 1.41e-03 -15 27 6.25e-01 2.00e+07 U238 nu-fission 4.12e-02 4.02e-03 +0 21 0.00e+00 6.25e-01 U235 fission 1.93e-01 1.92e-02 cell energy low [eV] energy high [eV] nuclide score mean std. dev. +0 21 0.00e+00 6.25e-01 U235 nu-fission 4.70e-01 4.67e-02 cell energy low [eV] energy high [eV] nuclide score mean std. dev. +0 21 0.00e+00 6.25e-01 U238 fission 2.65e-07 2.60e-08 cell energy low [eV] energy high [eV] nuclide score mean std. dev. +0 21 0.00e+00 6.25e-01 U238 nu-fission 6.60e-07 6.47e-08 cell energy low [eV] energy high [eV] nuclide score mean std. dev. +0 21 6.25e-01 2.00e+07 U235 fission 3.39e-02 1.53e-03 cell energy low [eV] energy high [eV] nuclide score mean std. dev. +0 21 6.25e-01 2.00e+07 U235 nu-fission 8.30e-02 3.75e-03 cell energy low [eV] energy high [eV] nuclide score mean std. dev. +0 21 6.25e-01 2.00e+07 U238 fission 1.70e-02 2.01e-03 cell energy low [eV] energy high [eV] nuclide score mean std. dev. +0 21 6.25e-01 2.00e+07 U238 nu-fission 4.76e-02 6.12e-03 cell energy low [eV] energy high [eV] nuclide score mean std. dev. +0 27 0.00e+00 6.25e-01 U235 fission 7.61e-02 5.84e-03 cell energy low [eV] energy high [eV] nuclide score mean std. dev. +0 27 0.00e+00 6.25e-01 U235 nu-fission 1.85e-01 1.42e-02 cell energy low [eV] energy high [eV] nuclide score mean std. dev. +0 27 0.00e+00 6.25e-01 U238 fission 1.06e-07 7.62e-09 cell energy low [eV] energy high [eV] nuclide score mean std. dev. +0 27 0.00e+00 6.25e-01 U238 nu-fission 2.64e-07 1.90e-08 cell energy low [eV] energy high [eV] nuclide score mean std. dev. +0 27 6.25e-01 2.00e+07 U235 fission 1.69e-02 1.34e-03 cell energy low [eV] energy high [eV] nuclide score mean std. dev. +0 27 6.25e-01 2.00e+07 U235 nu-fission 4.15e-02 3.29e-03 cell energy low [eV] energy high [eV] nuclide score mean std. dev. +0 27 6.25e-01 2.00e+07 U238 fission 1.10e-02 1.69e-03 cell energy low [eV] energy high [eV] nuclide score mean std. dev. +0 27 6.25e-01 2.00e+07 U238 nu-fission 3.13e-02 5.35e-03 cell energy low [eV] energy high [eV] nuclide score mean std. dev. +0 21 0.00e+00 6.25e-01 U235 fission 1.93e-01 1.92e-02 +1 21 0.00e+00 6.25e-01 U235 nu-fission 4.70e-01 4.67e-02 +2 21 0.00e+00 6.25e-01 U238 fission 2.65e-07 2.60e-08 +3 21 0.00e+00 6.25e-01 U238 nu-fission 6.60e-07 6.47e-08 +4 21 6.25e-01 2.00e+07 U235 fission 3.39e-02 1.53e-03 +5 21 6.25e-01 2.00e+07 U235 nu-fission 8.30e-02 3.75e-03 +6 21 6.25e-01 2.00e+07 U238 fission 1.70e-02 2.01e-03 +7 21 6.25e-01 2.00e+07 U238 nu-fission 4.76e-02 6.12e-03 +8 27 0.00e+00 6.25e-01 U235 fission 7.61e-02 5.84e-03 +9 27 0.00e+00 6.25e-01 U235 nu-fission 1.85e-01 1.42e-02 +10 27 0.00e+00 6.25e-01 U238 fission 1.06e-07 7.62e-09 +11 27 0.00e+00 6.25e-01 U238 nu-fission 2.64e-07 1.90e-08 +12 27 6.25e-01 2.00e+07 U235 fission 1.69e-02 1.34e-03 +13 27 6.25e-01 2.00e+07 U235 nu-fission 4.15e-02 3.29e-03 +14 27 6.25e-01 2.00e+07 U238 fission 1.10e-02 1.69e-03 +15 27 6.25e-01 2.00e+07 U238 nu-fission 3.13e-02 5.35e-03 sum(distribcell) energy low [eV] energy high [eV] nuclide score mean std. dev. 0 (0, 100, 2000, 30000) 0.00e+00 6.25e-01 U235 fission 0.00e+00 0.00e+00 1 (0, 100, 2000, 30000) 0.00e+00 6.25e-01 U235 nu-fission 0.00e+00 0.00e+00 2 (0, 100, 2000, 30000) 0.00e+00 6.25e-01 U238 fission 0.00e+00 0.00e+00 3 (0, 100, 2000, 30000) 0.00e+00 6.25e-01 U238 nu-fission 0.00e+00 0.00e+00 -4 (0, 100, 2000, 30000) 6.25e-01 2.00e+07 U235 fission 9.53e-07 9.53e-07 -5 (0, 100, 2000, 30000) 6.25e-01 2.00e+07 U235 nu-fission 2.37e-06 2.37e-06 -6 (0, 100, 2000, 30000) 6.25e-01 2.00e+07 U238 fission 1.25e-08 1.25e-08 -7 (0, 100, 2000, 30000) 6.25e-01 2.00e+07 U238 nu-fission 3.15e-08 3.15e-08 +4 (0, 100, 2000, 30000) 6.25e-01 2.00e+07 U235 fission 0.00e+00 0.00e+00 +5 (0, 100, 2000, 30000) 6.25e-01 2.00e+07 U235 nu-fission 0.00e+00 0.00e+00 +6 (0, 100, 2000, 30000) 6.25e-01 2.00e+07 U238 fission 0.00e+00 0.00e+00 +7 (0, 100, 2000, 30000) 6.25e-01 2.00e+07 U238 nu-fission 0.00e+00 0.00e+00 8 (500, 5000, 50000) 0.00e+00 6.25e-01 U235 fission 0.00e+00 0.00e+00 9 (500, 5000, 50000) 0.00e+00 6.25e-01 U235 nu-fission 0.00e+00 0.00e+00 10 (500, 5000, 50000) 0.00e+00 6.25e-01 U238 fission 0.00e+00 0.00e+00 @@ -49,19 +49,19 @@ 14 (500, 5000, 50000) 6.25e-01 2.00e+07 U238 fission 0.00e+00 0.00e+00 15 (500, 5000, 50000) 6.25e-01 2.00e+07 U238 nu-fission 0.00e+00 0.00e+00 sum(mesh) energy low [eV] energy high [eV] nuclide score mean std. dev. -0 ((1, 1), (1, 2)) 0.00e+00 6.25e-01 U235 fission 6.73e-03 3.04e-03 -1 ((1, 1), (1, 2)) 0.00e+00 6.25e-01 U235 nu-fission 1.64e-02 7.40e-03 -2 ((1, 1), (1, 2)) 0.00e+00 6.25e-01 U238 fission 8.80e-09 3.84e-09 -3 ((1, 1), (1, 2)) 0.00e+00 6.25e-01 U238 nu-fission 2.19e-08 9.56e-09 -4 ((1, 1), (1, 2)) 6.25e-01 2.00e+07 U235 fission 9.89e-04 3.53e-04 -5 ((1, 1), (1, 2)) 6.25e-01 2.00e+07 U235 nu-fission 2.42e-03 8.60e-04 -6 ((1, 1), (1, 2)) 6.25e-01 2.00e+07 U238 fission 5.01e-04 2.13e-04 -7 ((1, 1), (1, 2)) 6.25e-01 2.00e+07 U238 nu-fission 1.36e-03 5.86e-04 -8 ((2, 1), (2, 2)) 0.00e+00 6.25e-01 U235 fission 1.63e-02 4.21e-03 -9 ((2, 1), (2, 2)) 0.00e+00 6.25e-01 U235 nu-fission 3.98e-02 1.02e-02 -10 ((2, 1), (2, 2)) 0.00e+00 6.25e-01 U238 fission 2.28e-08 5.90e-09 -11 ((2, 1), (2, 2)) 0.00e+00 6.25e-01 U238 nu-fission 5.67e-08 1.47e-08 -12 ((2, 1), (2, 2)) 6.25e-01 2.00e+07 U235 fission 1.79e-03 4.31e-04 -13 ((2, 1), (2, 2)) 6.25e-01 2.00e+07 U235 nu-fission 4.37e-03 1.05e-03 -14 ((2, 1), (2, 2)) 6.25e-01 2.00e+07 U238 fission 7.51e-04 2.51e-04 -15 ((2, 1), (2, 2)) 6.25e-01 2.00e+07 U238 nu-fission 2.02e-03 6.71e-04 +0 ((1, 1), (1, 2)) 0.00e+00 6.25e-01 U235 fission 1.94e-03 1.03e-03 +1 ((1, 1), (1, 2)) 0.00e+00 6.25e-01 U235 nu-fission 4.74e-03 2.50e-03 +2 ((1, 1), (1, 2)) 0.00e+00 6.25e-01 U238 fission 2.74e-09 1.42e-09 +3 ((1, 1), (1, 2)) 0.00e+00 6.25e-01 U238 nu-fission 6.83e-09 3.53e-09 +4 ((1, 1), (1, 2)) 6.25e-01 2.00e+07 U235 fission 9.02e-04 3.69e-04 +5 ((1, 1), (1, 2)) 6.25e-01 2.00e+07 U235 nu-fission 2.24e-03 9.12e-04 +6 ((1, 1), (1, 2)) 6.25e-01 2.00e+07 U238 fission 1.44e-03 8.42e-04 +7 ((1, 1), (1, 2)) 6.25e-01 2.00e+07 U238 nu-fission 4.37e-03 2.64e-03 +8 ((2, 1), (2, 2)) 0.00e+00 6.25e-01 U235 fission 1.27e-02 2.76e-03 +9 ((2, 1), (2, 2)) 0.00e+00 6.25e-01 U235 nu-fission 3.09e-02 6.72e-03 +10 ((2, 1), (2, 2)) 0.00e+00 6.25e-01 U238 fission 1.70e-08 3.57e-09 +11 ((2, 1), (2, 2)) 0.00e+00 6.25e-01 U238 nu-fission 4.25e-08 8.90e-09 +12 ((2, 1), (2, 2)) 6.25e-01 2.00e+07 U235 fission 1.43e-03 1.69e-04 +13 ((2, 1), (2, 2)) 6.25e-01 2.00e+07 U235 nu-fission 3.52e-03 4.20e-04 +14 ((2, 1), (2, 2)) 6.25e-01 2.00e+07 U238 fission 1.37e-03 2.98e-04 +15 ((2, 1), (2, 2)) 6.25e-01 2.00e+07 U238 nu-fission 4.16e-03 1.08e-03 diff --git a/tests/regression_tests/torus/results_true.dat b/tests/regression_tests/torus/results_true.dat index 42fb209cc..84cd3c7a4 100644 --- a/tests/regression_tests/torus/results_true.dat +++ b/tests/regression_tests/torus/results_true.dat @@ -1,2 +1,2 @@ k-combined: -7.667201E-01 1.136882E-02 +7.666453E-01 1.478848E-02 diff --git a/tests/regression_tests/trace/results_true.dat b/tests/regression_tests/trace/results_true.dat index bbf03de94..97b997ae6 100644 --- a/tests/regression_tests/trace/results_true.dat +++ b/tests/regression_tests/trace/results_true.dat @@ -1,2 +1,2 @@ k-combined: -3.070134E-01 3.900396E-03 +2.940336E-01 7.338463E-04 diff --git a/tests/regression_tests/track_output/results_true.dat b/tests/regression_tests/track_output/results_true.dat index 148b54a30..205ac3eec 100644 --- a/tests/regression_tests/track_output/results_true.dat +++ b/tests/regression_tests/track_output/results_true.dat @@ -143,76 +143,74 @@ neutron [((-9.469716e-01, -2.580266e-01, 1.414357e-01), (1.438873e-01, 2.365819e ((-3.920090e+00, -9.211794e-01, 2.908406e+00), (9.094673e-01, -3.659267e-01, -1.974002e-01), 9.547085e+00, 1.260840e-07, 1.000000e+00, 23, 2387, 1) ((-3.729948e+00, -9.976834e-01, 2.867135e+00), (-2.916959e-01, -6.494657e-01, -7.022164e-01), 8.590591e+00, 1.750036e-07, 1.000000e+00, 23, 2387, 1) ((-3.744933e+00, -1.031047e+00, 2.831062e+00), (-2.916959e-01, -6.494657e-01, -7.022164e-01), 8.590591e+00, 1.876753e-07, 0.000000e+00, 23, 2387, 1)] -neutron [((6.474155e+00, -5.192870e+00, 5.413003e+00), (-9.112559e-01, 3.950037e-01, 1.165536e-01), 2.052226e+06, 4.450859e-05, 1.000000e+00, 21, 2367, 2) - ((6.037250e+00, -5.003484e+00, 5.468885e+00), (-9.112559e-01, 3.950037e-01, 1.165536e-01), 2.052226e+06, 4.450883e-05, 1.000000e+00, 22, 2367, 3) - ((5.942573e+00, -4.962444e+00, 5.480995e+00), (-9.112559e-01, 3.950037e-01, 1.165536e-01), 2.052226e+06, 4.450889e-05, 1.000000e+00, 23, 2367, 1) - ((5.861800e+00, -4.927431e+00, 5.491326e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.450893e-05, 1.000000e+00, 23, 2367, 1) - ((5.725160e+00, -4.971424e+00, 5.491002e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.450903e-05, 1.000000e+00, 23, 2366, 1) - ((5.494150e+00, -5.045802e+00, 5.490454e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.450919e-05, 1.000000e+00, 22, 2366, 3) - ((5.392865e+00, -5.078412e+00, 5.490213e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.450927e-05, 1.000000e+00, 21, 2366, 2) - ((4.612759e+00, -5.329579e+00, 5.488362e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.450982e-05, 1.000000e+00, 22, 2366, 3) - ((4.511474e+00, -5.362189e+00, 5.488121e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.450989e-05, 1.000000e+00, 23, 2366, 1) - ((4.089400e+00, -5.498082e+00, 5.487119e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.451019e-05, 1.000000e+00, 23, 2365, 1) - ((3.384113e+00, -5.725160e+00, 5.485445e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.451069e-05, 1.000000e+00, 23, 2351, 1) - ((2.453640e+00, -6.024740e+00, 5.483237e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.451136e-05, 1.000000e+00, 23, 2350, 1) - ((2.086813e+00, -6.142846e+00, 5.482366e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.451162e-05, 1.000000e+00, 22, 2350, 3) - ((1.993594e+00, -6.172859e+00, 5.482145e+00), (-9.518772e-01, -3.064714e-01, -2.259335e-03), 1.141417e+06, 4.451168e-05, 1.000000e+00, 21, 2350, 2) - ((1.325704e+00, -6.387896e+00, 5.480560e+00), (8.986086e-01, -4.380574e-01, -2.466265e-02), 9.775839e+05, 4.451216e-05, 1.000000e+00, 21, 2350, 2) - ((2.061403e+00, -6.746538e+00, 5.460368e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451276e-05, 1.000000e+00, 21, 2350, 2) - ((1.122794e+00, -6.498940e+00, 4.743664e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451375e-05, 1.000000e+00, 22, 2350, 3) - ((1.036483e+00, -6.476172e+00, 4.677758e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451384e-05, 1.000000e+00, 23, 2350, 1) - ((8.178800e-01, -6.418507e+00, 4.510837e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451407e-05, 1.000000e+00, 23, 2349, 1) - ((5.725264e-01, -6.353784e+00, 4.323490e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451432e-05, 1.000000e+00, 22, 2349, 3) - ((4.668297e-01, -6.325902e+00, 4.242782e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451444e-05, 1.000000e+00, 21, 2349, 2) - ((-2.989816e-01, -6.123888e+00, 3.658023e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451524e-05, 1.000000e+00, 22, 2349, 3) - ((-4.046782e-01, -6.096006e+00, 3.577315e+00), (-7.778764e-01, 2.051975e-01, -5.939716e-01), 7.813219e+05, 4.451535e-05, 1.000000e+00, 23, 2349, 1) - ((-8.040445e-01, -5.990656e+00, 3.272367e+00), (-7.989001e-01, -1.890553e-01, -5.709787e-01), 6.637114e+05, 4.451577e-05, 1.000000e+00, 23, 2349, 1) - ((-8.178800e-01, -5.993930e+00, 3.262478e+00), (-7.989001e-01, -1.890553e-01, -5.709787e-01), 6.637114e+05, 4.451579e-05, 1.000000e+00, 23, 2348, 1) - ((-1.077910e+00, -6.055465e+00, 3.076633e+00), (-4.487118e-01, 1.670254e-01, -8.779295e-01), 4.550608e+05, 4.451608e-05, 1.000000e+00, 23, 2348, 1) - ((-1.215611e+00, -6.004208e+00, 2.807214e+00), (7.872329e-01, 4.481433e-01, -4.235941e-01), 3.544207e+03, 4.451641e-05, 1.000000e+00, 23, 2348, 1) - ((-1.100296e+00, -5.938564e+00, 2.745166e+00), (8.468456e-01, 5.310954e-01, 2.811230e-02), 2.812308e+03, 4.451819e-05, 1.000000e+00, 23, 2348, 1) - ((-8.178800e-01, -5.761448e+00, 2.754541e+00), (8.468456e-01, 5.310954e-01, 2.811230e-02), 2.812308e+03, 4.452273e-05, 1.000000e+00, 23, 2349, 1) - ((-7.600184e-01, -5.725160e+00, 2.756462e+00), (8.468456e-01, 5.310954e-01, 2.811230e-02), 2.812308e+03, 4.452366e-05, 1.000000e+00, 23, 2363, 1) - ((-2.947156e-01, -5.433347e+00, 2.771908e+00), (8.468456e-01, 5.310954e-01, 2.811230e-02), 2.812308e+03, 4.453115e-05, 1.000000e+00, 22, 2363, 3) - ((-2.073333e-01, -5.378546e+00, 2.774809e+00), (8.468456e-01, 5.310954e-01, 2.811230e-02), 2.812308e+03, 4.453256e-05, 1.000000e+00, 21, 2363, 2) - ((-1.746705e-01, -5.358062e+00, 2.775893e+00), (5.278774e-01, 5.459205e-01, 6.506276e-01), 2.806481e+03, 4.453309e-05, 1.000000e+00, 21, 2363, 2) - ((-8.681718e-02, -5.267205e+00, 2.884176e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.453536e-05, 1.000000e+00, 21, 2363, 2) - ((-3.684221e-01, -5.266924e+00, 2.745840e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.453967e-05, 1.000000e+00, 22, 2363, 3) - ((-4.840903e-01, -5.266809e+00, 2.689019e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.454144e-05, 1.000000e+00, 23, 2363, 1) - ((-8.178800e-01, -5.266475e+00, 2.525049e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.454656e-05, 1.000000e+00, 23, 2362, 1) - ((-1.151175e+00, -5.266142e+00, 2.361321e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.455166e-05, 1.000000e+00, 22, 2362, 3) - ((-1.266464e+00, -5.266027e+00, 2.304687e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.455343e-05, 1.000000e+00, 21, 2362, 2) - ((-2.005772e+00, -5.265288e+00, 1.941509e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.456475e-05, 1.000000e+00, 22, 2362, 3) - ((-2.121061e+00, -5.265173e+00, 1.884875e+00), (-8.975500e-01, 8.968076e-04, -4.409119e-01), 2.764929e+03, 4.456652e-05, 1.000000e+00, 23, 2362, 1) - ((-2.393759e+00, -5.264900e+00, 1.750915e+00), (-9.607392e-01, -1.749373e-01, 2.153533e-01), 1.619469e+03, 4.457070e-05, 1.000000e+00, 23, 2362, 1) - ((-2.453640e+00, -5.275804e+00, 1.764337e+00), (-9.607392e-01, -1.749373e-01, 2.153533e-01), 1.619469e+03, 4.457182e-05, 1.000000e+00, 23, 2361, 1) - ((-2.470658e+00, -5.278903e+00, 1.768152e+00), (-6.728606e-01, -2.916408e-01, -6.798561e-01), 4.873672e+02, 4.457214e-05, 1.000000e+00, 23, 2361, 1) - ((-3.463692e+00, -5.709318e+00, 7.647939e-01), (-2.249302e-01, -9.330150e-01, -2.808728e-01), 1.790901e+02, 4.462047e-05, 1.000000e+00, 23, 2361, 1) - ((-3.467511e+00, -5.725160e+00, 7.600247e-01), (-2.249302e-01, -9.330150e-01, -2.808728e-01), 1.790901e+02, 4.462139e-05, 1.000000e+00, 23, 2347, 1) - ((-3.533785e+00, -6.000066e+00, 6.772677e-01), (-2.249302e-01, -9.330150e-01, -2.808728e-01), 1.790901e+02, 4.463730e-05, 1.000000e+00, 22, 2347, 3) - ((-3.562245e+00, -6.118119e+00, 6.417291e-01), (-2.249302e-01, -9.330150e-01, -2.808728e-01), 1.790901e+02, 4.464414e-05, 1.000000e+00, 21, 2347, 2) - ((-3.723934e+00, -6.788806e+00, 4.398272e-01), (-2.249302e-01, -9.330150e-01, -2.808728e-01), 1.790901e+02, 4.468297e-05, 1.000000e+00, 22, 2347, 3) - ((-3.752394e+00, -6.906859e+00, 4.042885e-01), (-2.249302e-01, -9.330150e-01, -2.808728e-01), 1.790901e+02, 4.468981e-05, 1.000000e+00, 23, 2347, 1) - ((-3.848515e+00, -7.305571e+00, 2.842613e-01), (6.498448e-01, -3.243413e-01, -6.873896e-01), 2.328979e+01, 4.471290e-05, 1.000000e+00, 23, 2347, 1) - ((-3.737619e+00, -7.360920e+00, 1.669579e-01), (6.498448e-01, -3.243413e-01, -6.873896e-01), 2.328979e+01, 4.473846e-05, 1.000000e+00, 11, 1750, 1) - ((-3.574308e+00, -7.442429e+00, -5.788041e-03), (6.794505e-01, 3.157106e-01, -6.623246e-01), 1.590416e+01, 4.477611e-05, 1.000000e+00, 11, 1750, 1) - ((-3.398889e+00, -7.360920e+00, -1.767852e-01), (6.794505e-01, 3.157106e-01, -6.623246e-01), 1.590416e+01, 4.482292e-05, 1.000000e+00, 23, 2347, 1) - ((-2.904712e+00, -7.131298e+00, -6.585068e-01), (-4.836985e-02, -2.819627e-01, -9.582053e-01), 2.610202e+00, 4.495477e-05, 1.000000e+00, 23, 2347, 1) - ((-2.923579e+00, -7.241285e+00, -1.032280e+00), (-6.089807e-01, -2.347428e-01, -7.576532e-01), 1.671268e+00, 4.512933e-05, 1.000000e+00, 23, 2347, 1) - ((-3.012516e+00, -7.275567e+00, -1.142930e+00), (3.221931e-01, -5.406104e-01, -7.771306e-01), 1.275709e-01, 4.521100e-05, 1.000000e+00, 23, 2347, 1) - ((-2.984032e+00, -7.323360e+00, -1.211633e+00), (-8.629564e-01, -1.719529e-01, -4.751195e-01), 1.897424e-02, 4.538995e-05, 1.000000e+00, 23, 2347, 1) - ((-3.172528e+00, -7.360920e+00, -1.315413e+00), (-8.629564e-01, -1.719529e-01, -4.751195e-01), 1.897424e-02, 4.653641e-05, 1.000000e+00, 11, 1750, 1) - ((-3.602328e+00, -7.446562e+00, -1.552049e+00), (-1.530190e-01, -7.092235e-01, 6.881767e-01), 1.306951e-02, 4.915052e-05, 1.000000e+00, 11, 1750, 1) - ((-3.625236e+00, -7.552741e+00, -1.449021e+00), (-1.976867e-01, 2.826475e-01, 9.386322e-01), 2.145548e-02, 5.009730e-05, 1.000000e+00, 11, 1750, 1) - ((-3.636290e+00, -7.536936e+00, -1.396537e+00), (1.266676e-01, 2.265353e-01, -9.657314e-01), 1.815564e-02, 5.037329e-05, 1.000000e+00, 11, 1750, 1) - ((-3.621792e+00, -7.511008e+00, -1.507069e+00), (-4.275550e-01, 8.441443e-01, 3.234457e-01), 1.848049e-02, 5.098741e-05, 1.000000e+00, 11, 1750, 1) - ((-3.697811e+00, -7.360920e+00, -1.449560e+00), (-4.275550e-01, 8.441443e-01, 3.234457e-01), 1.848049e-02, 5.193300e-05, 1.000000e+00, 23, 2347, 1) - ((-3.703436e+00, -7.349814e+00, -1.445305e+00), (-7.832167e-01, 5.713670e-01, 2.451762e-01), 1.861705e-02, 5.200297e-05, 1.000000e+00, 23, 2347, 1) - ((-3.823243e+00, -7.262414e+00, -1.407801e+00), (-9.917106e-01, 5.069164e-02, 1.180695e-01), 4.703334e-02, 5.281350e-05, 1.000000e+00, 23, 2347, 1) - ((-4.089400e+00, -7.248809e+00, -1.376113e+00), (-9.917106e-01, 5.069164e-02, 1.180695e-01), 4.703334e-02, 5.370820e-05, 1.000000e+00, 23, 2346, 1) - ((-4.131081e+00, -7.246679e+00, -1.371151e+00), (9.108632e-01, -4.018526e-01, 9.403556e-02), 8.057393e-02, 5.384831e-05, 1.000000e+00, 23, 2346, 1) - ((-4.089400e+00, -7.265067e+00, -1.366848e+00), (9.108632e-01, -4.018526e-01, 9.403556e-02), 8.057393e-02, 5.396486e-05, 1.000000e+00, 23, 2347, 1) - ((-3.872134e+00, -7.360920e+00, -1.344418e+00), (9.108632e-01, -4.018526e-01, 9.403556e-02), 8.057393e-02, 5.457240e-05, 1.000000e+00, 11, 1750, 1) - ((-3.673062e+00, -7.448746e+00, -1.323866e+00), (2.976906e-01, 8.174443e-01, -4.931178e-01), 5.538060e-02, 5.512905e-05, 1.000000e+00, 11, 1750, 1) - ((-3.658580e+00, -7.408978e+00, -1.347855e+00), (-5.409120e-01, 8.345378e-01, -1.046943e-01), 8.122576e-02, 5.527851e-05, 1.000000e+00, 11, 1750, 1) - ((-3.682981e+00, -7.371331e+00, -1.352578e+00), (-5.409120e-01, 8.345378e-01, -1.046943e-01), 8.122576e-02, 5.539295e-05, 0.000000e+00, 11, 1750, 1)] +neutron [((6.474155e+00, -5.192870e+00, 5.413003e+00), (-9.597810e-01, -1.081529e-01, -2.590820e-01), 9.900333e+05, 4.450859e-05, 1.000000e+00, 21, 2367, 2) + ((6.142109e+00, -5.230286e+00, 5.323371e+00), (-9.597810e-01, -1.081529e-01, -2.590820e-01), 9.900333e+05, 4.450884e-05, 1.000000e+00, 22, 2367, 3) + ((6.041244e+00, -5.241652e+00, 5.296144e+00), (-9.597810e-01, -1.081529e-01, -2.590820e-01), 9.900333e+05, 4.450892e-05, 1.000000e+00, 23, 2367, 1) + ((6.004037e+00, -5.245845e+00, 5.286100e+00), (-6.195407e-01, -7.332723e-01, -2.801446e-01), 5.512347e+05, 4.450895e-05, 1.000000e+00, 23, 2367, 1) + ((5.725160e+00, -5.575916e+00, 5.159997e+00), (-6.195407e-01, -7.332723e-01, -2.801446e-01), 5.512347e+05, 4.450939e-05, 1.000000e+00, 23, 2366, 1) + ((5.599064e+00, -5.725160e+00, 5.102979e+00), (-6.195407e-01, -7.332723e-01, -2.801446e-01), 5.512347e+05, 4.450958e-05, 1.000000e+00, 23, 2352, 1) + ((5.296886e+00, -6.082810e+00, 4.966340e+00), (-6.195407e-01, -7.332723e-01, -2.801446e-01), 5.512347e+05, 4.451006e-05, 1.000000e+00, 22, 2352, 3) + ((5.240003e+00, -6.150135e+00, 4.940619e+00), (-6.195407e-01, -7.332723e-01, -2.801446e-01), 5.512347e+05, 4.451015e-05, 1.000000e+00, 21, 2352, 2) + ((4.752072e+00, -6.727638e+00, 4.719986e+00), (3.721510e-02, -9.647240e-01, 2.606197e-01), 5.045933e+05, 4.451092e-05, 1.000000e+00, 21, 2352, 2) + ((4.764028e+00, -7.037568e+00, 4.803713e+00), (3.721510e-02, -9.647240e-01, 2.606197e-01), 5.045933e+05, 4.451124e-05, 1.000000e+00, 22, 2352, 3) + ((4.767580e+00, -7.129630e+00, 4.828584e+00), (3.721510e-02, -9.647240e-01, 2.606197e-01), 5.045933e+05, 4.451134e-05, 1.000000e+00, 23, 2352, 1) + ((4.776502e+00, -7.360920e+00, 4.891066e+00), (3.721510e-02, -9.647240e-01, 2.606197e-01), 5.045933e+05, 4.451158e-05, 1.000000e+00, 23, 2338, 1) + ((4.778286e+00, -7.407162e+00, 4.903559e+00), (4.571054e-01, -3.889563e-01, 7.998548e-01), 1.818775e+05, 4.451163e-05, 1.000000e+00, 23, 2338, 1) + ((4.981926e+00, -7.580442e+00, 5.259893e+00), (4.571054e-01, -3.889563e-01, 7.998548e-01), 1.818775e+05, 4.451239e-05, 1.000000e+00, 22, 2338, 3) + ((5.154144e+00, -7.726985e+00, 5.561246e+00), (4.571054e-01, -3.889563e-01, 7.998548e-01), 1.818775e+05, 4.451303e-05, 1.000000e+00, 21, 2338, 2) + ((5.313757e+00, -7.862801e+00, 5.840540e+00), (4.571054e-01, -3.889563e-01, 7.998548e-01), 1.818775e+05, 4.451362e-05, 1.000000e+00, 22, 2338, 3) + ((5.485976e+00, -8.009344e+00, 6.141893e+00), (4.571054e-01, -3.889563e-01, 7.998548e-01), 1.818775e+05, 4.451426e-05, 1.000000e+00, 23, 2338, 1) + ((5.725160e+00, -8.212869e+00, 6.560424e+00), (4.571054e-01, -3.889563e-01, 7.998548e-01), 1.818775e+05, 4.451515e-05, 1.000000e+00, 23, 2339, 1) + ((6.004950e+00, -8.450945e+00, 7.050007e+00), (4.571054e-01, -3.889563e-01, 7.998548e-01), 1.818775e+05, 4.451618e-05, 1.000000e+00, 22, 2339, 3) + ((6.360530e+00, -8.753512e+00, 7.672210e+00), (4.571054e-01, -3.889563e-01, 7.998548e-01), 1.818775e+05, 4.451750e-05, 1.000000e+00, 23, 2339, 1) + ((6.646303e+00, -8.996680e+00, 8.172263e+00), (4.571054e-01, -3.889563e-01, 7.998548e-01), 1.818775e+05, 4.451856e-05, 1.000000e+00, 23, 2326, 1) + ((6.702490e+00, -9.044491e+00, 8.270582e+00), (2.028534e-01, -9.607965e-01, -1.889990e-01), 1.805295e+04, 4.451877e-05, 1.000000e+00, 23, 2326, 1) + ((6.745127e+00, -9.246436e+00, 8.230857e+00), (2.028534e-01, -9.607965e-01, -1.889990e-01), 1.805295e+04, 4.451990e-05, 1.000000e+00, 22, 2326, 3) + ((6.767219e+00, -9.351070e+00, 8.210274e+00), (2.028534e-01, -9.607965e-01, -1.889990e-01), 1.805295e+04, 4.452049e-05, 1.000000e+00, 21, 2326, 2) + ((6.885344e+00, -9.910562e+00, 8.100216e+00), (-3.857824e-01, -3.006310e-01, -8.722344e-01), 1.795765e+04, 4.452362e-05, 1.000000e+00, 21, 2326, 2) + ((6.803615e+00, -9.974251e+00, 7.915431e+00), (-7.096135e-01, 6.778415e-01, 1.923009e-01), 1.779137e+04, 4.452476e-05, 1.000000e+00, 21, 2326, 2) + ((6.334815e+00, -9.526441e+00, 8.042473e+00), (-1.748210e-01, 9.821705e-01, -6.912779e-02), 1.729669e+04, 4.452835e-05, 1.000000e+00, 21, 2326, 2) + ((6.304853e+00, -9.358111e+00, 8.030625e+00), (-1.748210e-01, 9.821705e-01, -6.912779e-02), 1.729669e+04, 4.452929e-05, 1.000000e+00, 22, 2326, 3) + ((6.288777e+00, -9.267793e+00, 8.024268e+00), (-1.748210e-01, 9.821705e-01, -6.912779e-02), 1.729669e+04, 4.452979e-05, 1.000000e+00, 23, 2326, 1) + ((6.275105e+00, -9.190979e+00, 8.018862e+00), (-7.174952e-01, 5.897531e-01, -3.706642e-01), 9.207900e+03, 4.453022e-05, 1.000000e+00, 23, 2326, 1) + ((6.038719e+00, -8.996680e+00, 7.896743e+00), (-7.174952e-01, 5.897531e-01, -3.706642e-01), 9.207900e+03, 4.453271e-05, 1.000000e+00, 23, 2339, 1) + ((6.028805e+00, -8.988531e+00, 7.891621e+00), (-2.989803e-01, 4.300779e-01, -8.518473e-01), 5.653774e+03, 4.453281e-05, 1.000000e+00, 23, 2339, 1) + ((5.725160e+00, -8.551743e+00, 7.026484e+00), (-2.989803e-01, 4.300779e-01, -8.518473e-01), 5.653774e+03, 4.454257e-05, 1.000000e+00, 23, 2338, 1) + ((5.507324e+00, -8.238390e+00, 6.405832e+00), (-2.989803e-01, 4.300779e-01, -8.518473e-01), 5.653774e+03, 4.454958e-05, 1.000000e+00, 22, 2338, 3) + ((5.417387e+00, -8.109017e+00, 6.149584e+00), (-2.989803e-01, 4.300779e-01, -8.518473e-01), 5.653774e+03, 4.455247e-05, 1.000000e+00, 21, 2338, 2) + ((5.162017e+00, -7.741671e+00, 5.421991e+00), (7.522612e-01, -1.036919e-01, -6.506543e-01), 5.619608e+03, 4.456069e-05, 1.000000e+00, 21, 2338, 2) + ((5.184146e+00, -7.744722e+00, 5.402851e+00), (7.522612e-01, -1.036919e-01, -6.506543e-01), 5.619608e+03, 4.456097e-05, 1.000000e+00, 22, 2338, 3) + ((5.348057e+00, -7.767315e+00, 5.261079e+00), (7.522612e-01, -1.036919e-01, -6.506543e-01), 5.619608e+03, 4.456307e-05, 1.000000e+00, 23, 2338, 1) + ((5.581568e+00, -7.799502e+00, 5.059108e+00), (2.029690e-01, -4.131185e-01, -8.877706e-01), 3.359662e+03, 4.456606e-05, 1.000000e+00, 23, 2338, 1) + ((5.707007e+00, -8.054818e+00, 4.510447e+00), (3.894183e-01, -7.459178e-01, -5.403333e-01), 3.303714e+03, 4.457377e-05, 1.000000e+00, 23, 2338, 1) + ((5.725160e+00, -8.089590e+00, 4.485259e+00), (3.894183e-01, -7.459178e-01, -5.403333e-01), 3.303714e+03, 4.457436e-05, 1.000000e+00, 23, 2339, 1) + ((5.750893e+00, -8.138881e+00, 4.449554e+00), (-2.879829e-01, -5.545287e-01, -7.807456e-01), 1.744626e+03, 4.457519e-05, 1.000000e+00, 23, 2339, 1) + ((5.725160e+00, -8.188431e+00, 4.379790e+00), (-2.879829e-01, -5.545287e-01, -7.807456e-01), 1.744626e+03, 4.457674e-05, 1.000000e+00, 23, 2338, 1) + ((5.532143e+00, -8.560097e+00, 3.856504e+00), (-6.185172e-01, -7.766669e-01, -1.192687e-01), 8.559363e+02, 4.458834e-05, 1.000000e+00, 23, 2338, 1) + ((5.522333e+00, -8.572415e+00, 3.854613e+00), (-3.452053e-01, -2.657159e-02, -9.381510e-01), 9.272133e+01, 4.458873e-05, 1.000000e+00, 23, 2338, 1) + ((5.366948e+00, -8.584375e+00, 3.432328e+00), (4.761677e-01, -8.187551e-01, -3.207871e-01), 6.522002e+00, 4.462253e-05, 1.000000e+00, 23, 2338, 1) + ((5.516634e+00, -8.841755e+00, 3.331487e+00), (4.237115e-01, -6.022578e-01, 6.765752e-01), 9.968491e-01, 4.471152e-05, 1.000000e+00, 23, 2338, 1) + ((5.625629e+00, -8.996680e+00, 3.505530e+00), (4.237115e-01, -6.022578e-01, 6.765752e-01), 9.968491e-01, 4.489779e-05, 1.000000e+00, 23, 2325, 1) + ((5.723424e+00, -9.135684e+00, 3.661687e+00), (7.706137e-01, -6.358415e-01, 4.312932e-02), 5.695813e-01, 4.506493e-05, 1.000000e+00, 23, 2325, 1) + ((5.725160e+00, -9.137116e+00, 3.661784e+00), (7.706137e-01, -6.358415e-01, 4.312932e-02), 5.695813e-01, 4.506708e-05, 1.000000e+00, 23, 2326, 1) + ((6.079210e+00, -9.429247e+00, 3.681599e+00), (7.706137e-01, -6.358415e-01, 4.312932e-02), 5.695813e-01, 4.550721e-05, 1.000000e+00, 22, 2326, 3) + ((6.147194e+00, -9.485341e+00, 3.685404e+00), (7.706137e-01, -6.358415e-01, 4.312932e-02), 5.695813e-01, 4.559172e-05, 1.000000e+00, 21, 2326, 2) + ((6.864744e+00, -1.007740e+01, 3.725564e+00), (3.888690e-01, -8.895503e-01, -2.397522e-01), 5.691123e-01, 4.648372e-05, 1.000000e+00, 21, 2326, 2) + ((6.908415e+00, -1.017730e+01, 3.698639e+00), (3.888690e-01, -8.895503e-01, -2.397522e-01), 5.691123e-01, 4.659135e-05, 1.000000e+00, 22, 2326, 3) + ((6.945959e+00, -1.026318e+01, 3.675492e+00), (3.888690e-01, -8.895503e-01, -2.397522e-01), 5.691123e-01, 4.668388e-05, 1.000000e+00, 23, 2326, 1) + ((6.966122e+00, -1.030930e+01, 3.663060e+00), (7.493672e-01, -6.303754e-01, 2.026713e-01), 2.849108e-01, 4.673357e-05, 1.000000e+00, 23, 2326, 1) + ((7.350254e+00, -1.063244e+01, 3.766951e+00), (7.493672e-01, -6.303754e-01, 2.026713e-01), 2.849108e-01, 4.742789e-05, 1.000000e+00, 23, 2311, 1) + ((7.360920e+00, -1.064141e+01, 3.769836e+00), (7.493672e-01, -6.303754e-01, 2.026713e-01), 2.849108e-01, 4.744717e-05, 1.000000e+00, 23, 2312, 1) + ((7.449168e+00, -1.071565e+01, 3.793703e+00), (1.626881e-01, -1.480918e-01, 9.755006e-01), 2.025207e-01, 4.760667e-05, 1.000000e+00, 23, 2312, 1) + ((7.495754e+00, -1.075805e+01, 4.073039e+00), (-1.185723e-01, -6.787603e-01, 7.247241e-01), 2.121260e-01, 4.806671e-05, 1.000000e+00, 23, 2312, 1) + ((7.451135e+00, -1.101347e+01, 4.345753e+00), (-4.978877e-01, -3.750598e-02, 8.664301e-01), 2.609768e-02, 4.865741e-05, 1.000000e+00, 23, 2312, 1) + ((7.426401e+00, -1.101533e+01, 4.388795e+00), (9.221835e-01, -6.407530e-02, 3.814078e-01), 2.031765e-02, 4.887973e-05, 1.000000e+00, 23, 2312, 1) + ((7.438938e+00, -1.101621e+01, 4.393980e+00), (8.832139e-01, 3.563144e-01, 3.049151e-01), 2.038019e-02, 4.894869e-05, 1.000000e+00, 23, 2312, 1) + ((7.578355e+00, -1.095996e+01, 4.442112e+00), (5.564936e-01, 8.135970e-01, 1.684482e-01), 4.869241e-02, 4.974811e-05, 1.000000e+00, 23, 2312, 1) + ((7.802376e+00, -1.063244e+01, 4.509922e+00), (5.564936e-01, 8.135970e-01, 1.684482e-01), 4.869241e-02, 5.106705e-05, 1.000000e+00, 23, 2327, 1) + ((7.826035e+00, -1.059785e+01, 4.517083e+00), (-8.227572e-01, -5.662268e-01, 4.957651e-02), 8.202195e-02, 5.120634e-05, 1.000000e+00, 23, 2327, 1) + ((7.775776e+00, -1.063244e+01, 4.520112e+00), (-8.227572e-01, -5.662268e-01, 4.957651e-02), 8.202195e-02, 5.136055e-05, 1.000000e+00, 23, 2312, 1) + ((7.360920e+00, -1.091795e+01, 4.545109e+00), (-8.227572e-01, -5.662268e-01, 4.957651e-02), 8.202195e-02, 5.263343e-05, 1.000000e+00, 23, 2311, 1) + ((7.180267e+00, -1.104227e+01, 4.555995e+00), (5.568446e-01, -6.706436e-01, -4.900625e-01), 5.560680e-02, 5.318772e-05, 1.000000e+00, 23, 2311, 1) + ((7.207392e+00, -1.107494e+01, 4.532123e+00), (9.936512e-01, 4.529367e-02, -1.029847e-01), 8.142074e-02, 5.333706e-05, 1.000000e+00, 23, 2311, 1) + ((7.252245e+00, -1.107290e+01, 4.527474e+00), (9.936512e-01, 4.529367e-02, -1.029847e-01), 8.142074e-02, 5.345144e-05, 0.000000e+00, 23, 2311, 1)] diff --git a/tests/regression_tests/translation/results_true.dat b/tests/regression_tests/translation/results_true.dat index f832aa296..6e03d2224 100644 --- a/tests/regression_tests/translation/results_true.dat +++ b/tests/regression_tests/translation/results_true.dat @@ -1,2 +1,2 @@ k-combined: -4.087580E-01 4.466279E-03 +4.076610E-01 6.454244E-03 diff --git a/tests/regression_tests/trigger_batch_interval/results_true.dat b/tests/regression_tests/trigger_batch_interval/results_true.dat index 0571d6c4f..92fa99d87 100644 --- a/tests/regression_tests/trigger_batch_interval/results_true.dat +++ b/tests/regression_tests/trigger_batch_interval/results_true.dat @@ -1,28 +1,28 @@ k-combined: -9.767929E-01 5.327552E-03 +9.863217E-01 6.499354E-03 tally 1: -1.405471E+01 -1.976444E+01 -3.186139E+00 -1.015515E+00 -3.088643E+00 -9.542994E-01 -1.086857E+01 -1.182011E+01 -1.405471E+01 -1.976444E+01 -3.186139E+00 -1.015515E+00 -3.088643E+00 -9.542994E-01 -1.086857E+01 -1.182011E+01 +1.423436E+01 +2.027258E+01 +3.223740E+00 +1.039872E+00 +3.125016E+00 +9.771709E-01 +1.101062E+01 +1.212977E+01 +1.423436E+01 +2.027258E+01 +3.223740E+00 +1.039872E+00 +3.125016E+00 +9.771709E-01 +1.101062E+01 +1.212977E+01 tally 2: -1.405471E+01 -1.976444E+01 -3.186139E+00 -1.015515E+00 -3.088643E+00 -9.542994E-01 -1.086857E+01 -1.182011E+01 +1.423436E+01 +2.027258E+01 +3.223740E+00 +1.039872E+00 +3.125016E+00 +9.771709E-01 +1.101062E+01 +1.212977E+01 diff --git a/tests/regression_tests/trigger_no_batch_interval/results_true.dat b/tests/regression_tests/trigger_no_batch_interval/results_true.dat index 0571d6c4f..92fa99d87 100644 --- a/tests/regression_tests/trigger_no_batch_interval/results_true.dat +++ b/tests/regression_tests/trigger_no_batch_interval/results_true.dat @@ -1,28 +1,28 @@ k-combined: -9.767929E-01 5.327552E-03 +9.863217E-01 6.499354E-03 tally 1: -1.405471E+01 -1.976444E+01 -3.186139E+00 -1.015515E+00 -3.088643E+00 -9.542994E-01 -1.086857E+01 -1.182011E+01 -1.405471E+01 -1.976444E+01 -3.186139E+00 -1.015515E+00 -3.088643E+00 -9.542994E-01 -1.086857E+01 -1.182011E+01 +1.423436E+01 +2.027258E+01 +3.223740E+00 +1.039872E+00 +3.125016E+00 +9.771709E-01 +1.101062E+01 +1.212977E+01 +1.423436E+01 +2.027258E+01 +3.223740E+00 +1.039872E+00 +3.125016E+00 +9.771709E-01 +1.101062E+01 +1.212977E+01 tally 2: -1.405471E+01 -1.976444E+01 -3.186139E+00 -1.015515E+00 -3.088643E+00 -9.542994E-01 -1.086857E+01 -1.182011E+01 +1.423436E+01 +2.027258E+01 +3.223740E+00 +1.039872E+00 +3.125016E+00 +9.771709E-01 +1.101062E+01 +1.212977E+01 diff --git a/tests/regression_tests/trigger_no_status/results_true.dat b/tests/regression_tests/trigger_no_status/results_true.dat index fb6c0086a..a62e1b3fe 100644 --- a/tests/regression_tests/trigger_no_status/results_true.dat +++ b/tests/regression_tests/trigger_no_status/results_true.dat @@ -1,28 +1,28 @@ k-combined: -9.682315E-01 3.302924E-03 +9.858966E-01 1.500542E-02 tally 1: -7.046510E+00 -9.932168E+00 -1.591675E+00 -5.067777E-01 -1.541572E+00 -4.753743E-01 -5.454835E+00 -5.951990E+00 -7.046510E+00 -9.932168E+00 -1.591675E+00 -5.067777E-01 -1.541572E+00 -4.753743E-01 -5.454835E+00 -5.951990E+00 +7.081828E+00 +1.003709E+01 +1.607382E+00 +5.171386E-01 +1.559242E+00 +4.866413E-01 +5.474447E+00 +5.997737E+00 +7.081828E+00 +1.003709E+01 +1.607382E+00 +5.171386E-01 +1.559242E+00 +4.866413E-01 +5.474447E+00 +5.997737E+00 tally 2: -7.046510E+00 -9.932168E+00 -1.591675E+00 -5.067777E-01 -1.541572E+00 -4.753743E-01 -5.454835E+00 -5.951990E+00 +7.081828E+00 +1.003709E+01 +1.607382E+00 +5.171386E-01 +1.559242E+00 +4.866413E-01 +5.474447E+00 +5.997737E+00 diff --git a/tests/regression_tests/trigger_statepoint_restart/inputs_true.dat b/tests/regression_tests/trigger_statepoint_restart/inputs_true.dat index 01755ad8b..59a829700 100644 --- a/tests/regression_tests/trigger_statepoint_restart/inputs_true.dat +++ b/tests/regression_tests/trigger_statepoint_restart/inputs_true.dat @@ -16,7 +16,7 @@ 15 10 - 0.003 + 0.002 std_dev diff --git a/tests/regression_tests/trigger_statepoint_restart/results_true.dat b/tests/regression_tests/trigger_statepoint_restart/results_true.dat index 3e6619d74..3cb1e230d 100644 --- a/tests/regression_tests/trigger_statepoint_restart/results_true.dat +++ b/tests/regression_tests/trigger_statepoint_restart/results_true.dat @@ -1,5 +1,5 @@ k-combined: -3.014717E-01 2.864764E-03 +2.948661E-01 1.949846E-03 tally 1: -8.946107E+01 -7.281263E+02 +5.515170E+01 +4.349007E+02 diff --git a/tests/regression_tests/trigger_statepoint_restart/test.py b/tests/regression_tests/trigger_statepoint_restart/test.py index 8144c1d0b..b242f7f1c 100644 --- a/tests/regression_tests/trigger_statepoint_restart/test.py +++ b/tests/regression_tests/trigger_statepoint_restart/test.py @@ -29,7 +29,7 @@ def model(): settings.inactive = 10 settings.particles = 400 # Choose a sufficiently low threshold to enable use of trigger - settings.keff_trigger = {'type': 'std_dev', 'threshold': 0.003} + settings.keff_trigger = {'type': 'std_dev', 'threshold': 0.002} settings.trigger_max_batches = 1000 settings.trigger_batch_interval = 1 settings.trigger_active = True @@ -41,10 +41,10 @@ def model(): tallies = openmc.Tallies([t]) # Put it all together - model = openmc.model.Model(materials=materials, - geometry=geometry, - settings=settings, - tallies=tallies) + model = openmc.Model(materials=materials, + geometry=geometry, + settings=settings, + tallies=tallies) return model diff --git a/tests/regression_tests/trigger_tallies/results_true.dat b/tests/regression_tests/trigger_tallies/results_true.dat index 0571d6c4f..92fa99d87 100644 --- a/tests/regression_tests/trigger_tallies/results_true.dat +++ b/tests/regression_tests/trigger_tallies/results_true.dat @@ -1,28 +1,28 @@ k-combined: -9.767929E-01 5.327552E-03 +9.863217E-01 6.499354E-03 tally 1: -1.405471E+01 -1.976444E+01 -3.186139E+00 -1.015515E+00 -3.088643E+00 -9.542994E-01 -1.086857E+01 -1.182011E+01 -1.405471E+01 -1.976444E+01 -3.186139E+00 -1.015515E+00 -3.088643E+00 -9.542994E-01 -1.086857E+01 -1.182011E+01 +1.423436E+01 +2.027258E+01 +3.223740E+00 +1.039872E+00 +3.125016E+00 +9.771709E-01 +1.101062E+01 +1.212977E+01 +1.423436E+01 +2.027258E+01 +3.223740E+00 +1.039872E+00 +3.125016E+00 +9.771709E-01 +1.101062E+01 +1.212977E+01 tally 2: -1.405471E+01 -1.976444E+01 -3.186139E+00 -1.015515E+00 -3.088643E+00 -9.542994E-01 -1.086857E+01 -1.182011E+01 +1.423436E+01 +2.027258E+01 +3.223740E+00 +1.039872E+00 +3.125016E+00 +9.771709E-01 +1.101062E+01 +1.212977E+01 diff --git a/tests/regression_tests/triso/results_true.dat b/tests/regression_tests/triso/results_true.dat index fa1842e54..d9850e410 100644 --- a/tests/regression_tests/triso/results_true.dat +++ b/tests/regression_tests/triso/results_true.dat @@ -1,2 +1,2 @@ k-combined: -1.716873E+00 5.266107E-02 +1.604832E+00 2.031393E-03 diff --git a/tests/regression_tests/uniform_fs/results_true.dat b/tests/regression_tests/uniform_fs/results_true.dat index 46654b49b..f7ceecfb7 100644 --- a/tests/regression_tests/uniform_fs/results_true.dat +++ b/tests/regression_tests/uniform_fs/results_true.dat @@ -1,2 +1,2 @@ k-combined: -3.685309E-01 1.861720E-03 +3.675645E-01 4.342970E-03 diff --git a/tests/regression_tests/universe/results_true.dat b/tests/regression_tests/universe/results_true.dat index bbf03de94..97b997ae6 100644 --- a/tests/regression_tests/universe/results_true.dat +++ b/tests/regression_tests/universe/results_true.dat @@ -1,2 +1,2 @@ k-combined: -3.070134E-01 3.900396E-03 +2.940336E-01 7.338463E-04 diff --git a/tests/regression_tests/white_plane/results_true.dat b/tests/regression_tests/white_plane/results_true.dat index 6f1d064eb..ffb19491d 100644 --- a/tests/regression_tests/white_plane/results_true.dat +++ b/tests/regression_tests/white_plane/results_true.dat @@ -1,2 +1,2 @@ k-combined: -2.279719E+00 5.380792E-03 +2.274312E+00 4.223342E-03 diff --git a/tests/unit_tests/test_deplete_resultslist.py b/tests/unit_tests/test_deplete_resultslist.py index 308eccf7b..9a4699a4f 100644 --- a/tests/unit_tests/test_deplete_resultslist.py +++ b/tests/unit_tests/test_deplete_resultslist.py @@ -21,14 +21,14 @@ def test_get_activity(res): t_ref = np.array([0.0, 1296000.0, 2592000.0, 3888000.0]) a_ref = np.array( - [1.25167956e+06, 3.71938527e+11, 4.43264300e+11, 3.55547176e+11]) + [1.25167956e+06, 3.69842310e+11, 3.70099291e+11, 3.53629755e+11]) np.testing.assert_allclose(t, t_ref) np.testing.assert_allclose(a, a_ref) # Check by_nuclide a_xe135_ref = np.array( - [2.106574218e+05, 1.227519888e+11, 1.177491828e+11, 1.031986176e+11]) + [2.10657422e+05, 1.12825236e+11, 1.09055177e+11, 1.07491257e+11]) t_nuc, a_nuc = res.get_activity("1", by_nuclide=True) a_xe135 = np.array([a_nuc_i["Xe135"] for a_nuc_i in a_nuc]) @@ -43,7 +43,7 @@ def test_get_atoms(res): t_ref = np.array([0.0, 1296000.0, 2592000.0, 3888000.0]) n_ref = np.array( - [6.67473282e+08, 3.88942731e+14, 3.73091215e+14, 3.26987387e+14]) + [6.67473282e+08, 3.57489567e+14, 3.45544042e+14, 3.40588723e+14]) np.testing.assert_allclose(t, t_ref) np.testing.assert_allclose(n, n_ref) @@ -71,7 +71,7 @@ def test_get_decay_heat(res): t_ref = np.array([0.0, 1296000.0, 2592000.0, 3888000.0]) dh_ref = np.array( - [1.27933813e-09, 5.85347232e-03, 7.38773010e-03, 5.79954067e-03]) + [1.27933813e-09, 5.95370258e-03, 6.01335600e-03, 5.69831173e-03]) t, dh = res.get_decay_heat("1") @@ -80,7 +80,7 @@ def test_get_decay_heat(res): # Check by nuclide dh_xe135_ref = np.array( - [1.27933813e-09, 7.45481920e-04, 7.15099509e-04, 6.26732849e-04]) + [1.27933813e-09, 6.85196014e-04, 6.62300168e-04, 6.52802366e-04]) t_nuc, dh_nuc = res.get_decay_heat("1", by_nuclide=True) dh_nuc_xe135 = np.array([dh_nuc_i["Xe135"] for dh_nuc_i in dh_nuc]) @@ -95,7 +95,7 @@ def test_get_mass(res): t_ref = np.array([0.0, 1296000.0, 2592000.0, 3888000.0]) n_ref = np.array( - [6.67473282e+08, 3.88942731e+14, 3.73091215e+14, 3.26987387e+14]) + [6.67473282e+08, 3.57489567e+14, 3.45544042e+14, 3.40588723e+14]) # Get g n_ref *= openmc.data.atomic_mass('Xe135') / openmc.data.AVOGADRO @@ -123,8 +123,8 @@ def test_get_reaction_rate(res): t, r = res.get_reaction_rate("1", "Xe135", "(n,gamma)") t_ref = [0.0, 1296000.0, 2592000.0, 3888000.0] - n_ref = [6.67473282e+08, 3.88942731e+14, 3.73091215e+14, 3.26987387e+14] - xs_ref = [2.53336104e-05, 4.21747011e-05, 3.48616127e-05, 3.61775563e-05] + n_ref = [6.67473282e+08, 3.57489567e+14, 3.45544042e+14, 3.40588723e+14] + xs_ref = [3.10220818e-05, 3.36754072e-05, 3.12740350e-05, 3.86717693e-05] np.testing.assert_allclose(t, t_ref) np.testing.assert_allclose(r, np.array(n_ref) * xs_ref) @@ -136,8 +136,8 @@ def test_get_keff(res): t_min, k = res.get_keff(time_units='min') t_ref = [0.0, 1296000.0, 2592000.0, 3888000.0] - k_ref = [1.1596402556, 1.1914183335, 1.2292570871, 1.1797030302] - u_ref = [0.0270680649, 0.0219163444, 0.024268508 , 0.0221401194] + k_ref = [1.1773089172, 1.2231748584, 1.1611455694, 1.1714783649] + u_ref = [0.0384666252, 0.0311915665, 0.0226370102, 0.0315964732] np.testing.assert_allclose(t, t_ref) np.testing.assert_allclose(t_min * 60, t_ref) diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index 8080ce894..43bc5a8f6 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -161,16 +161,21 @@ def test_properties_temperature(lib_init): def test_cell_density(lib_init): cell = openmc.lib.cells[1] - cell.set_density(1.5, 0) - assert cell.get_density(0) == pytest.approx(1.5) - cell.set_density(2.0) - assert cell.get_density() == pytest.approx(2.0) + print('density', cell.get_density()) + orig_density = cell.get_density() + try: + cell.set_density(1.5, 0) + assert cell.get_density(0) == pytest.approx(1.5) + cell.set_density(2.0) + assert cell.get_density() == pytest.approx(2.0) + finally: + cell.set_density(orig_density) def test_properties_cell_density(lib_init): # Cell density should be 2.0 from above test cell = openmc.lib.cells[1] - assert cell.get_density() == pytest.approx(2.0) + orig_density = cell.get_density() # Export properties and change density openmc.lib.export_properties('properties.h5') @@ -179,7 +184,7 @@ def test_properties_cell_density(lib_init): # Import properties and check that density is restored openmc.lib.import_properties('properties.h5') - assert cell.get_density() == pytest.approx(2.0) + assert cell.get_density() == pytest.approx(orig_density) def test_new_cell(lib_init): diff --git a/tests/unit_tests/test_settings.py b/tests/unit_tests/test_settings.py index 7f202bcdc..6611e4227 100644 --- a/tests/unit_tests/test_settings.py +++ b/tests/unit_tests/test_settings.py @@ -67,6 +67,7 @@ def test_export_to_xml(run_in_tmpdir): ) } s.max_particle_events = 100 + s.max_secondaries = 1_000_000 s.source_rejection_fraction = 0.01 # Make sure exporting XML works @@ -144,4 +145,5 @@ def test_export_to_xml(run_in_tmpdir): assert s.random_ray['distance_active'] == 100.0 assert s.random_ray['ray_source'].space.lower_left == [-1., -1., -1.] assert s.random_ray['ray_source'].space.upper_right == [1., 1., 1.] + assert s.max_secondaries == 1_000_000 assert s.source_rejection_fraction == 0.01 diff --git a/tests/unit_tests/test_statepoint_batches.py b/tests/unit_tests/test_statepoint_batches.py new file mode 100644 index 000000000..bf54e1878 --- /dev/null +++ b/tests/unit_tests/test_statepoint_batches.py @@ -0,0 +1,26 @@ +from pathlib import Path + +import openmc + + +def test_statepoint_batches(run_in_tmpdir): + # Create a minimal model + mat = openmc.Material() + mat.add_nuclide('U235', 1.0) + mat.set_density('g/cm3', 4.5) + sphere = openmc.Sphere(r=10.0, boundary_type='vacuum') + cell = openmc.Cell(fill=mat, region=-sphere) + model = openmc.Model() + model.geometry = openmc.Geometry([cell]) + model.settings.batches = 10 + model.settings.inactive = 5 + model.settings.particles = 100 + + # Specify when statepoints should be written + model.settings.statepoint = {'batches': [3, 6, 9]} + + # Run model and ensure that statepoints are created + model.run() + sp_files = ['statepoint.03.h5', 'statepoint.06.h5', 'statepoint.09.h5'] + for f in sp_files: + assert Path(f).is_file() From ca63da91b9b0fc0cd144766155b533318d4a71c3 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Fri, 19 Sep 2025 15:00:51 -0500 Subject: [PATCH 412/671] Ensure n_dimension_ attribute is set for unstructured meshes. (#3575) --- include/openmc/mesh.h | 3 +-- src/mesh.cpp | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index c15e25697..fbc6c46f5 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -666,9 +666,8 @@ class UnstructuredMesh : public Mesh { public: // Constructors - UnstructuredMesh() {}; + UnstructuredMesh() { n_dimension_ = 3; }; UnstructuredMesh(pugi::xml_node node); - UnstructuredMesh(const std::string& filename); static const std::string mesh_type; virtual std::string get_mesh_type() const override; diff --git a/src/mesh.cpp b/src/mesh.cpp index 3e4ff1a3e..b7396a25a 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -590,6 +590,8 @@ Position StructuredMesh::sample_element( UnstructuredMesh::UnstructuredMesh(pugi::xml_node node) : Mesh(node) { + n_dimension_ = 3; + // check the mesh type if (check_for_node(node, "type")) { auto temp = get_node_value(node, "type", true, true); @@ -2519,7 +2521,9 @@ MOABMesh::MOABMesh(pugi::xml_node node) : UnstructuredMesh(node) } MOABMesh::MOABMesh(const std::string& filename, double length_multiplier) + : UnstructuredMesh() { + n_dimension_ = 3; filename_ = filename; set_length_multiplier(length_multiplier); initialize(); @@ -3242,6 +3246,7 @@ LibMesh::LibMesh(libMesh::MeshBase& input_mesh, double length_multiplier) LibMesh::LibMesh(const std::string& filename, double length_multiplier) : adaptive_(false) { + n_dimension_ = 3; set_mesh_pointer_from_filename(filename); set_length_multiplier(length_multiplier); initialize(); From 8f36ff2b3a5e2984254f7b21f1b92af7ad18140b Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 23 Sep 2025 10:06:29 -0500 Subject: [PATCH 413/671] Update find_package calls in OpenMCConfig.cmake (#3572) --- cmake/OpenMCConfig.cmake.in | 11 +++++++---- openmc/deplete/transfer_rates.py | 2 +- tests/regression_tests/unstructured_mesh/test.py | 2 +- tests/unit_tests/dagmc/test_plot.py | 5 ++++- tests/unit_tests/test_model.py | 4 ++++ tests/unit_tests/test_region.py | 4 ++++ tests/unit_tests/test_universe.py | 4 ++++ tests/unit_tests/weightwindows/test_wwinp_reader.py | 2 +- tools/ci/gha-script.sh | 5 ++++- 9 files changed, 30 insertions(+), 9 deletions(-) diff --git a/cmake/OpenMCConfig.cmake.in b/cmake/OpenMCConfig.cmake.in index 3fe0c1bcd..837a39c78 100644 --- a/cmake/OpenMCConfig.cmake.in +++ b/cmake/OpenMCConfig.cmake.in @@ -1,9 +1,12 @@ get_filename_component(OpenMC_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) -find_package(fmt REQUIRED HINTS ${OpenMC_CMAKE_DIR}/../fmt) -find_package(pugixml REQUIRED HINTS ${OpenMC_CMAKE_DIR}/../pugixml) -find_package(xtl REQUIRED HINTS ${OpenMC_CMAKE_DIR}/../xtl) -find_package(xtensor REQUIRED HINTS ${OpenMC_CMAKE_DIR}/../xtensor) +# Compute the install prefix from this file's location +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/openmc/deplete/transfer_rates.py b/openmc/deplete/transfer_rates.py index 4c28c7d15..4f2b9aba5 100644 --- a/openmc/deplete/transfer_rates.py +++ b/openmc/deplete/transfer_rates.py @@ -366,7 +366,7 @@ class ExternalSourceRates(ExternalRates): rate : float External source rate in units of mass per time. A positive or negative value corresponds to a feed or removal rate, respectively. - units : {'g/s', 'g/min', 'g/h', 'g/d', 'g/a'} + rate_units : {'g/s', 'g/min', 'g/h', 'g/d', 'g/a'} Units for values specified in the `rate` argument. 's' for seconds, 'min' for minutes, 'h' for hours, 'a' for Julian years. timesteps : list of int, optional diff --git a/tests/regression_tests/unstructured_mesh/test.py b/tests/regression_tests/unstructured_mesh/test.py index 0082198dd..7607531d8 100644 --- a/tests/regression_tests/unstructured_mesh/test.py +++ b/tests/regression_tests/unstructured_mesh/test.py @@ -256,7 +256,7 @@ for i, (lib, estimator, ext_geom, holes) in enumerate(product(*param_values)): def test_unstructured_mesh_tets(model, test_opts): # skip the test if the library is not enabled if test_opts['library'] == 'moab' and not openmc.lib._dagmc_enabled(): - pytest.skip("DAGMC (and MOAB) mesh not enbaled in this build.") + pytest.skip("DAGMC (and MOAB) mesh not enabled in this build.") if test_opts['library'] == 'libmesh' and not openmc.lib._libmesh_enabled(): pytest.skip("LibMesh is not enabled in this build.") diff --git a/tests/unit_tests/dagmc/test_plot.py b/tests/unit_tests/dagmc/test_plot.py index 62022b258..6ce1d79a2 100644 --- a/tests/unit_tests/dagmc/test_plot.py +++ b/tests/unit_tests/dagmc/test_plot.py @@ -64,5 +64,8 @@ def test_plotting_geometry_filled_with_dagmc_universe(request): cell2 = openmc.Cell(fill=csg_material, region=+sphere1 & -sphere2) geometry = openmc.Geometry([cell1, cell2]) - geometry.plot() + + # Close plot to avoid warning + import matplotlib.pyplot as plt + plt.close() diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 12f0df6c9..60f8b1a25 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -654,6 +654,10 @@ def test_model_plot(): test_mask = (image_data == white) | (image_data == red) assert np.all(test_mask), "Colors other than white or red found in overlap plot image" + # Close plots to avoid warning + import matplotlib.pyplot as plt + plt.close('all') + def test_model_id_map_initialization(run_in_tmpdir): model = openmc.examples.pwr_assembly() diff --git a/tests/unit_tests/test_region.py b/tests/unit_tests/test_region.py index cbcd19831..cb9fa171b 100644 --- a/tests/unit_tests/test_region.py +++ b/tests/unit_tests/test_region.py @@ -248,6 +248,10 @@ def test_plot(): c_before = openmc.Cell() region.plot() + # Close plot to avoid warning + import matplotlib.pyplot as plt + plt.close() + # Ensure that calling plot doesn't affect cell ID space c_after = openmc.Cell() assert c_after.id - 1 == c_before.id diff --git a/tests/unit_tests/test_universe.py b/tests/unit_tests/test_universe.py index 46d4ec3f7..efe8552a6 100644 --- a/tests/unit_tests/test_universe.py +++ b/tests/unit_tests/test_universe.py @@ -99,6 +99,10 @@ def test_plot(run_in_tmpdir, sphere_model): pixels=100, ) + # Close plots to avoid warning + import matplotlib.pyplot as plt + plt.close('all') + def test_get_nuclides(uo2): c = openmc.Cell(fill=uo2) diff --git a/tests/unit_tests/weightwindows/test_wwinp_reader.py b/tests/unit_tests/weightwindows/test_wwinp_reader.py index 28548a448..637463bb2 100644 --- a/tests/unit_tests/weightwindows/test_wwinp_reader.py +++ b/tests/unit_tests/weightwindows/test_wwinp_reader.py @@ -140,4 +140,4 @@ def test_wwinp_reader_failures(wwinp_data, request): filename, expected_failure = wwinp_data with pytest.raises(expected_failure): - _ = openmc.wwinp_to_wws(request.node.path.parent / filename) + _ = openmc.WeightWindowsList.from_wwinp(request.node.path.parent / filename) diff --git a/tools/ci/gha-script.sh b/tools/ci/gha-script.sh index c0f754c32..c1d292137 100755 --- a/tools/ci/gha-script.sh +++ b/tools/ci/gha-script.sh @@ -15,4 +15,7 @@ if [[ $EVENT == 'y' ]]; then fi # Run unit tests and then regression tests -pytest --cov=openmc -v $args tests/unit_tests tests/regression_tests +pytest --cov=openmc -v $args \ + tests/test_matplotlib_import.py \ + tests/unit_tests \ + tests/regression_tests From ed433fe1cd9479e355cfe4ca6ec8fb99359f98cf Mon Sep 17 00:00:00 2001 From: Kevin Sawatzky <66632997+nuclearkevin@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:31:14 -0500 Subject: [PATCH 414/671] Fix performance regression in libMesh unstructured mesh tallies (#3577) --- include/openmc/mesh.h | 49 ++++++++++++++----- src/mesh.cpp | 109 ++++++++++++++++++++++++------------------ 2 files changed, 101 insertions(+), 57 deletions(-) diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index fbc6c46f5..a56705c9e 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -990,25 +990,26 @@ public: libMesh::MeshBase* mesh_ptr() const { return m_; }; +protected: + // Methods + + //! Translate a bin value to an element reference + virtual const libMesh::Elem& get_element_from_bin(int bin) const; + + //! Translate an element pointer to a bin index + virtual int get_bin_from_element(const libMesh::Elem* elem) const; + + libMesh::MeshBase* m_; //!< pointer to libMesh MeshBase instance, always set + //!< during intialization private: void initialize() override; void set_mesh_pointer_from_filename(const std::string& filename); void build_eqn_sys(); - // Methods - - //! Translate a bin value to an element reference - const libMesh::Elem& get_element_from_bin(int bin) const; - - //! Translate an element pointer to a bin index - int get_bin_from_element(const libMesh::Elem* elem) const; - // Data members unique_ptr unique_m_ = nullptr; //!< pointer to the libMesh MeshBase instance, only used if mesh is //!< created inside OpenMC - libMesh::MeshBase* m_; //!< pointer to libMesh MeshBase instance, always set - //!< during intialization vector> pl_; //!< per-thread point locators unique_ptr @@ -1022,8 +1023,34 @@ private: libMesh::BoundingBox bbox_; //!< bounding box of the mesh libMesh::dof_id_type first_element_id_; //!< id of the first element in the mesh +}; + +class AdaptiveLibMesh : public LibMesh { +public: + // Constructor + AdaptiveLibMesh( + libMesh::MeshBase& input_mesh, double length_multiplier = 1.0); + + // Overridden methods + int n_bins() const override; + + void add_score(const std::string& var_name) override; + + void set_score_data(const std::string& var_name, const vector& values, + const vector& std_dev) override; + + void write(const std::string& filename) const override; + +protected: + // Overridden methods + int get_bin_from_element(const libMesh::Elem* elem) const override; + + const libMesh::Elem& get_element_from_bin(int bin) const override; + +private: + // Data members + const libMesh::dof_id_type num_active_; //!< cached number of active elements - const bool adaptive_; //!< whether this mesh has adaptivity enabled or not std::vector bin_to_elem_map_; //!< mapping bin indices to dof indices for active //!< elements diff --git a/src/mesh.cpp b/src/mesh.cpp index b7396a25a..58d218b9c 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -3219,7 +3219,7 @@ void MOABMesh::write(const std::string& base_filename) const const std::string LibMesh::mesh_lib_type = "libmesh"; -LibMesh::LibMesh(pugi::xml_node node) : UnstructuredMesh(node), adaptive_(false) +LibMesh::LibMesh(pugi::xml_node node) : UnstructuredMesh(node) { // filename_ and length_multiplier_ will already be set by the // UnstructuredMesh constructor @@ -3230,7 +3230,6 @@ LibMesh::LibMesh(pugi::xml_node node) : UnstructuredMesh(node), adaptive_(false) // create the mesh from a pointer to a libMesh Mesh LibMesh::LibMesh(libMesh::MeshBase& input_mesh, double length_multiplier) - : adaptive_(input_mesh.n_active_elem() != input_mesh.n_elem()) { if (!dynamic_cast(&input_mesh)) { fatal_error("At present LibMesh tallies require a replicated mesh. Please " @@ -3244,7 +3243,6 @@ LibMesh::LibMesh(libMesh::MeshBase& input_mesh, double length_multiplier) // create the mesh from an input file LibMesh::LibMesh(const std::string& filename, double length_multiplier) - : adaptive_(false) { n_dimension_ = 3; set_mesh_pointer_from_filename(filename); @@ -3307,21 +3305,6 @@ void LibMesh::initialize() auto first_elem = *m_->elements_begin(); first_element_id_ = first_elem->id(); - // if the mesh is adaptive elements aren't guaranteed by libMesh to be - // contiguous in ID space, so we need to map from bin indices (defined over - // active elements) to global dof ids - if (adaptive_) { - bin_to_elem_map_.reserve(m_->n_active_elem()); - elem_to_bin_map_.resize(m_->n_elem(), -1); - for (auto it = m_->active_elements_begin(); it != m_->active_elements_end(); - it++) { - auto elem = *it; - - bin_to_elem_map_.push_back(elem->id()); - elem_to_bin_map_[elem->id()] = bin_to_elem_map_.size() - 1; - } - } - // bounding box for the mesh for quick rejection checks bbox_ = libMesh::MeshTools::create_bounding_box(*m_); libMesh::Point ll = bbox_.min(); @@ -3379,7 +3362,7 @@ std::string LibMesh::library() const int LibMesh::n_bins() const { - return m_->n_active_elem(); + return m_->n_elem(); } int LibMesh::n_surface_bins() const @@ -3402,14 +3385,6 @@ int LibMesh::n_surface_bins() const void LibMesh::add_score(const std::string& var_name) { - if (adaptive_) { - warning(fmt::format( - "Exodus output cannot be provided as unstructured mesh {} is adaptive.", - this->id_)); - - return; - } - if (!equation_systems_) { build_eqn_sys(); } @@ -3445,14 +3420,6 @@ void LibMesh::remove_scores() void LibMesh::set_score_data(const std::string& var_name, const vector& values, const vector& std_dev) { - if (adaptive_) { - warning(fmt::format( - "Exodus output cannot be provided as unstructured mesh {} is adaptive.", - this->id_)); - - return; - } - if (!equation_systems_) { build_eqn_sys(); } @@ -3496,14 +3463,6 @@ void LibMesh::set_score_data(const std::string& var_name, void LibMesh::write(const std::string& filename) const { - if (adaptive_) { - warning(fmt::format( - "Exodus output cannot be provided as unstructured mesh {} is adaptive.", - this->id_)); - - return; - } - write_message(fmt::format( "Writing file: {}.e for unstructured mesh {}", filename, this->id_)); libMesh::ExodusII_IO exo(*m_); @@ -3537,8 +3496,7 @@ int LibMesh::get_bin(Position r) const int LibMesh::get_bin_from_element(const libMesh::Elem* elem) const { - int bin = - adaptive_ ? elem_to_bin_map_[elem->id()] : elem->id() - first_element_id_; + int bin = elem->id() - first_element_id_; if (bin >= n_bins() || bin < 0) { fatal_error(fmt::format("Invalid bin: {}", bin)); } @@ -3553,7 +3511,7 @@ std::pair, vector> LibMesh::plot( const libMesh::Elem& LibMesh::get_element_from_bin(int bin) const { - return adaptive_ ? m_->elem_ref(bin_to_elem_map_.at(bin)) : m_->elem_ref(bin); + return m_->elem_ref(bin); } double LibMesh::volume(int bin) const @@ -3561,6 +3519,65 @@ double LibMesh::volume(int bin) const return this->get_element_from_bin(bin).volume(); } +AdaptiveLibMesh::AdaptiveLibMesh( + libMesh::MeshBase& input_mesh, double length_multiplier) + : LibMesh(input_mesh, length_multiplier), num_active_(m_->n_active_elem()) +{ + // if the mesh is adaptive elements aren't guaranteed by libMesh to be + // contiguous in ID space, so we need to map from bin indices (defined over + // active elements) to global dof ids + bin_to_elem_map_.reserve(num_active_); + elem_to_bin_map_.resize(m_->n_elem(), -1); + for (auto it = m_->active_elements_begin(); it != m_->active_elements_end(); + it++) { + auto elem = *it; + + bin_to_elem_map_.push_back(elem->id()); + elem_to_bin_map_[elem->id()] = bin_to_elem_map_.size() - 1; + } +} + +int AdaptiveLibMesh::n_bins() const +{ + return num_active_; +} + +void AdaptiveLibMesh::add_score(const std::string& var_name) +{ + warning(fmt::format( + "Exodus output cannot be provided as unstructured mesh {} is adaptive.", + this->id_)); +} + +void AdaptiveLibMesh::set_score_data(const std::string& var_name, + const vector& values, const vector& std_dev) +{ + warning(fmt::format( + "Exodus output cannot be provided as unstructured mesh {} is adaptive.", + this->id_)); +} + +void AdaptiveLibMesh::write(const std::string& filename) const +{ + warning(fmt::format( + "Exodus output cannot be provided as unstructured mesh {} is adaptive.", + this->id_)); +} + +int AdaptiveLibMesh::get_bin_from_element(const libMesh::Elem* elem) const +{ + int bin = elem_to_bin_map_[elem->id()]; + if (bin >= n_bins() || bin < 0) { + fatal_error(fmt::format("Invalid bin: {}", bin)); + } + return bin; +} + +const libMesh::Elem& AdaptiveLibMesh::get_element_from_bin(int bin) const +{ + return m_->elem_ref(bin_to_elem_map_.at(bin)); +} + #endif // OPENMC_LIBMESH_ENABLED //============================================================================== From 66e7d8634cbace480e3f6ccf58ca959ecfbf614e Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Wed, 24 Sep 2025 11:02:19 +0200 Subject: [PATCH 415/671] Remove several TODOs related to C++17 support (#3574) Co-authored-by: Paul Romano --- src/bank.cpp | 13 +++---------- src/event.cpp | 13 +++++++------ src/hdf5_interface.cpp | 3 +-- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/src/bank.cpp b/src/bank.cpp index 9955939f6..3e806b3c0 100644 --- a/src/bank.cpp +++ b/src/bank.cpp @@ -79,16 +79,9 @@ void sort_fission_bank() // Perform exclusive scan summation to determine starting indices in fission // bank for each parent particle id - int64_t tmp = simulation::progeny_per_particle[0]; - simulation::progeny_per_particle[0] = 0; - for (int64_t i = 1; i < simulation::progeny_per_particle.size(); i++) { - int64_t value = simulation::progeny_per_particle[i - 1] + tmp; - tmp = simulation::progeny_per_particle[i]; - simulation::progeny_per_particle[i] = value; - } - - // TODO: C++17 introduces the exclusive_scan() function which could be - // used to replace everything above this point in this function. + std::exclusive_scan(simulation::progeny_per_particle.begin(), + simulation::progeny_per_particle.end(), + simulation::progeny_per_particle.begin(), 0); // We need a scratch vector to make permutation of the fission bank into // sorted order easy. Under normal usage conditions, the fission bank is diff --git a/src/event.cpp b/src/event.cpp index aa3987504..f33e132d0 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -1,4 +1,5 @@ #include "openmc/event.h" + #include "openmc/material.h" #include "openmc/simulation.h" #include "openmc/timer.h" @@ -73,17 +74,17 @@ void process_calculate_xs_events(SharedArray& queue) { simulation::time_event_calculate_xs.start(); - // TODO: If using C++17, perform a parallel sort of the queue - // by particle type, material type, and then energy, in order to - // improve cache locality and reduce thread divergence on GPU. Prior - // to C++17, std::sort is a serial only operation, which in this case - // makes it too slow to be practical for most test problems. + // TODO: If using C++17, we could perform a parallel sort of the queue by + // particle type, material type, and then energy, in order to improve cache + // locality and reduce thread divergence on GPU. However, the parallel + // algorithms typically require linking against an additional library (Intel + // TBB). Prior to C++17, std::sort is a serial only operation, which in this + // case makes it too slow to be practical for most test problems. // // std::sort(std::execution::par_unseq, queue.data(), queue.data() + // queue.size()); int64_t offset = simulation::advance_particle_queue.size(); - ; #pragma omp parallel for schedule(runtime) for (int64_t i = 0; i < queue.size(); i++) { diff --git a/src/hdf5_interface.cpp b/src/hdf5_interface.cpp index e90aa7490..bf1f79549 100644 --- a/src/hdf5_interface.cpp +++ b/src/hdf5_interface.cpp @@ -225,8 +225,7 @@ void get_name(hid_t obj_id, std::string& name) { size_t size = 1 + H5Iget_name(obj_id, nullptr, 0); name.resize(size); - // TODO: switch to name.data() when using C++17 - H5Iget_name(obj_id, &name[0], size); + H5Iget_name(obj_id, name.data(), size); } int get_num_datasets(hid_t group_id) From 767db7e6a090b88a594be72b4971d71e3c2be243 Mon Sep 17 00:00:00 2001 From: Joffrey Dorville <54550047+JoffreyDorville@users.noreply.github.com> Date: Thu, 25 Sep 2025 14:58:29 -0500 Subject: [PATCH 416/671] Fix IFP implementation (#3580) --- include/openmc/ifp.h | 7 +++---- include/openmc/particle_data.h | 1 + src/ifp.cpp | 4 ++-- src/particle.cpp | 1 + src/physics.cpp | 9 +++------ tests/regression_tests/ifp/results_true.dat | 4 ++-- .../case-03/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-04/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-05/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-07/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-08/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-09/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-10/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-12/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-13/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-14/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-a01/surface_source_true.h5 | Bin 6720 -> 6720 bytes .../case-d07/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-d08/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-e01/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-e02/surface_source_true.h5 | Bin 33344 -> 33344 bytes .../case-e03/surface_source_true.h5 | Bin 33344 -> 33344 bytes 22 files changed, 12 insertions(+), 14 deletions(-) diff --git a/include/openmc/ifp.h b/include/openmc/ifp.h index 633a262d5..01904d13c 100644 --- a/include/openmc/ifp.h +++ b/include/openmc/ifp.h @@ -68,15 +68,14 @@ vector _ifp(const T& value, const vector& data) //! //! Add the IFP information in the IFP banks using the same index //! as the one used to append the fission site to the fission bank. +//! The information stored are the delayed group number and lifetime +//! of the neutron that created the fission event. //! Multithreading protection is guaranteed by the index returned by the //! thread_safe_append call in physics.cpp. //! -//! Needs to be done after the delayed group is found. -//! //! \param[in] p Particle -//! \param[in] site Fission site //! \param[in] idx Bank index from the thread_safe_append call in physics.cpp -void ifp(const Particle& p, const SourceSite& site, int64_t idx); +void ifp(const Particle& p, int64_t idx); //! Resize the IFP banks used in the simulation void resize_simulation_ifp_banks(); diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index 1a22f5837..afcd56476 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -631,6 +631,7 @@ public: int& event_mt() { return event_mt_; } // MT number of collision const int& event_mt() const { return event_mt_; } int& delayed_group() { return delayed_group_; } // delayed group + const int& delayed_group() const { return delayed_group_; } const int& parent_nuclide() const { return parent_nuclide_; } int& parent_nuclide() { return parent_nuclide_; } // Parent nuclide diff --git a/src/ifp.cpp b/src/ifp.cpp index 1f81f26f6..cc4a76538 100644 --- a/src/ifp.cpp +++ b/src/ifp.cpp @@ -28,13 +28,13 @@ bool is_generation_time_or_both() return false; } -void ifp(const Particle& p, const SourceSite& site, int64_t idx) +void ifp(const Particle& p, int64_t idx) { if (is_beta_effective_or_both()) { const auto& delayed_groups = simulation::ifp_source_delayed_group_bank[p.current_work() - 1]; simulation::ifp_fission_delayed_group_bank[idx] = - _ifp(site.delayed_group, delayed_groups); + _ifp(p.delayed_group(), delayed_groups); } if (is_generation_time_or_both()) { const auto& lifetimes = diff --git a/src/particle.cpp b/src/particle.cpp index f5ad45d80..402af2498 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -144,6 +144,7 @@ void Particle::from_source(const SourceSite* src) time() = src->time; time_last() = src->time; parent_nuclide() = src->parent_nuclide; + delayed_group() = src->delayed_group; // Convert signed surface ID to signed index if (src->surf_id != SURFACE_NONE) { diff --git a/src/physics.cpp b/src/physics.cpp index f667fd586..e947fecbb 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -246,18 +246,15 @@ void create_fission_sites(Particle& p, int i_nuclide, const Reaction& rx) } // Iterated Fission Probability (IFP) method if (settings::ifp_on) { - ifp(p, site, idx); + ifp(p, idx); } } else { p.secondary_bank().push_back(site); } - // Set the delayed group on the particle as well - p.delayed_group() = site.delayed_group; - // Increment the number of neutrons born delayed - if (p.delayed_group() > 0) { - nu_d[p.delayed_group() - 1]++; + if (site.delayed_group > 0) { + nu_d[site.delayed_group - 1]++; } // Write fission particles to nuBank diff --git a/tests/regression_tests/ifp/results_true.dat b/tests/regression_tests/ifp/results_true.dat index 1d8f69e12..466ca1f01 100644 --- a/tests/regression_tests/ifp/results_true.dat +++ b/tests/regression_tests/ifp/results_true.dat @@ -3,7 +3,7 @@ k-combined: tally 1: 9.109384E-08 5.667165E-16 -6.500000E-02 -6.710000E-04 +5.200000E-02 +5.420000E-04 1.489000E+01 1.480036E+01 diff --git a/tests/regression_tests/surface_source_write/case-03/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-03/surface_source_true.h5 index 4dd5f821af92dd98395a8df31af2a81fab8a8ba4..228f5a7d0a462f38a1cb0b4274dd40d4307b5a12 100644 GIT binary patch delta 42 ycmX@m!gQd8X@dwmbDaH^&Eo8yDU2+W6SL1VvTZiZaTj7_nw%&gzj;R83T6N=*$u}4 delta 48 zcmX@m!gQd8X@dwm^EJ&=o5k5ZQzln%3QR7@J}|k1S75V4j)f42F}XlMVDpT+70dv* Csu83B diff --git a/tests/regression_tests/surface_source_write/case-04/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-04/surface_source_true.h5 index ee00af3895c349d60d786af1aedef39e05e24d20..c276af40f4302f4e0feff883bc3c472bc4df7ca1 100644 GIT binary patch delta 38 ucmX@m!gQd8X@dwmbDaH^&Eo8xzKkrB7lxc?WZPUAI-ie`X|rI8z8nArx(svx delta 44 xcmX@m!gQd8X@dwm^L5Qro5k5XeJ59N3QXP*a$vFtm%!$R&|0h5VN3AkPef$3?Q>mU+^N7 CA`&M6 diff --git a/tests/regression_tests/surface_source_write/case-12/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-12/surface_source_true.h5 index ca5eda8e2f347a1fc88b05db6690ad5b6d3a2a51..6c6925daba145c5db52a281023dd017f066e0732 100644 GIT binary patch delta 117 zcmX@m!gQd8X@dwmbAtVq&Eo8lQj<>z2yAXpOweRx+5FH;oYOG=2mfb3*B|@WHpD(y z^yT0FTL*$W*`i%Qt;HW$zbo>Udt8lS9X5&5&?bMwZEqk?BA) UUPkiE0XaX*)4?VeR9xT$0J0-9HUIzs delta 124 zcmV-?0E7R)gaW{X0a}p%0E$1%ExYoLO_SJDO6Vllc)#~laN_h elc)#~qxe}KlPD=5lYkHilOT9N0RppOc;5**VKsyR diff --git a/tests/regression_tests/surface_source_write/case-13/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-13/surface_source_true.h5 index ca5eda8e2f347a1fc88b05db6690ad5b6d3a2a51..6c6925daba145c5db52a281023dd017f066e0732 100644 GIT binary patch delta 117 zcmX@m!gQd8X@dwmbAtVq&Eo8lQj<>z2yAXpOweRx+5FH;oYOG=2mfb3*B|@WHpD(y z^yT0FTL*$W*`i%Qt;HW$zbo>Udt8lS9X5&5&?bMwZEqk?BA) UUPkiE0XaX*)4?VeR9xT$0J0-9HUIzs delta 124 zcmV-?0E7R)gaW{X0a}p%0E$1%ExYoLO_SJDO6Vllc)#~laN_h elc)#~qxe}KlPD=5lYkHilOT9N0RppOc;5**VKsyR diff --git a/tests/regression_tests/surface_source_write/case-14/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-14/surface_source_true.h5 index c9900bd9c0469d3b171f863f0518d6c94e966027..6c6925daba145c5db52a281023dd017f066e0732 100644 GIT binary patch delta 117 zcmX@m!gQd8X@dwmbAtVq&Eo8lQj<>z2yAXpOweRx+5FH;oYOG=2mfb3*B|@WHpD(y z^yT0FTL*$W*`i%Qt;HW$zbo>Udt8lS9X5&5&?bMwZEqk?BA) UUPkiE0XaX*)4?VeR9xT$0J0-9HUIzs delta 124 zcmV-?0E7R)gaW{X0a}p%0E$1%ExYoLO_SJDO6Vllc)#~laN_h elc)#~qxe}KlPD=5lYkHilOT9N0RppOc;5**(=~+v diff --git a/tests/regression_tests/surface_source_write/case-a01/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-a01/surface_source_true.h5 index 8dc148ee07eb33d6374387e557b35914f5fa2003..da36fc505eb67f3da0ce48511464b699a4fb16e3 100644 GIT binary patch delta 70 zcmX?La=>JR2s?9<{gut)?19XaFK}f{&fv<}Y{8wtJxM%c@(i(z$rph10tuivkhTEQ W1(F$)IV3$8StcLklb?J;G6MjFG#L5- delta 68 zcmX?La=>JR2s`s*%~PAj*#ns;U*O7^oWYf`*@8QPdy;s@X>K%LUYL4oBy*u_xg7`DD%%1Q6vVXpy?7Ij1?@Ya&Zg=K`&foCGUf}-ZSJHay^ESNO9MxAl*L;*XO>X>K%KZYL4oBy*u{Hg7`DD%%1Q6ynnu+?7Ij1?@Ya&ZgX>K%LUYL4oBy*u_xg7`DD%%1Q6vVXpy?7Ij1?@Ya&Zg=K`&foCGUf}-ZSJHay^ESNO9MxAl*L;*XO>X>K%KZYL4oBy*u{Hg7`DD%%1Q6ynnu+?7Ij1?@Ya&ZgLSiQX diff --git a/tests/regression_tests/surface_source_write/case-e01/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-e01/surface_source_true.h5 index 0dda15ed2192739435e8f50e126831021d620f53..bbfbd152bc6543871750cb9bd4cc55fe0899754a 100644 GIT binary patch delta 39 vcmX@m!gQd8X@dwm^L6Jdo5k5Vtr*!hD>|xkF|us-^iEf2WZJy3sGlDI{ml#e delta 45 zcmV+|0Mh@!gaW{X0|xkF|us-^iEf2WZJy3sGlDI{ml#e delta 45 zcmV+|0Mh@!gaW{X0|fgu z`(V+RfBSD82<~KyeqpaT&*);s!pHXSSC}u~^zD?rU%;v3d2LP(9h)bH)H5-%Y!(bR wXPmsFL|}77#0D`CW3ohk#N->+5}P~ne=vh(CKPKhf^3?6!Bk-Ljgkyr09VyO4FCWD delta 148 zcmX@m!gQd8X@dwm^Bj*eo5k5BRTx<|8*2A+P4 Date: Fri, 26 Sep 2025 12:27:05 -0400 Subject: [PATCH 417/671] Multi-group capability for kinetics parameter calculations with Iterated Fission Probability (#3425) Co-authored-by: GuySten Co-authored-by: Paul Romano --- docs/source/usersguide/kinetics.rst | 25 +++++++- openmc/model/model.py | 41 +++++++++++-- openmc/statepoint.py | 58 +++++++++++++++++++ src/tallies/tally.cpp | 3 +- src/tallies/tally_scoring.cpp | 9 +++ .../ifp/groupwise/__init__.py | 0 .../ifp/groupwise/inputs_true.dat | 43 ++++++++++++++ .../ifp/groupwise/results_true.dat | 21 +++++++ tests/regression_tests/ifp/groupwise/test.py | 40 +++++++++++++ tests/regression_tests/ifp/total/__init__.py | 0 .../ifp/{ => total}/inputs_true.dat | 0 .../ifp/{ => total}/results_true.dat | 0 .../regression_tests/ifp/{ => total}/test.py | 1 - tests/unit_tests/test_ifp.py | 39 +++++++++++++ 14 files changed, 272 insertions(+), 8 deletions(-) create mode 100644 tests/regression_tests/ifp/groupwise/__init__.py create mode 100644 tests/regression_tests/ifp/groupwise/inputs_true.dat create mode 100644 tests/regression_tests/ifp/groupwise/results_true.dat create mode 100644 tests/regression_tests/ifp/groupwise/test.py create mode 100644 tests/regression_tests/ifp/total/__init__.py rename tests/regression_tests/ifp/{ => total}/inputs_true.dat (100%) rename tests/regression_tests/ifp/{ => total}/results_true.dat (100%) rename tests/regression_tests/ifp/{ => total}/test.py (99%) diff --git a/docs/source/usersguide/kinetics.rst b/docs/source/usersguide/kinetics.rst index bdf26d341..9024ff822 100644 --- a/docs/source/usersguide/kinetics.rst +++ b/docs/source/usersguide/kinetics.rst @@ -67,6 +67,23 @@ are needed to compute kinetics parameters in OpenMC: Obtaining kinetics parameters ----------------------------- +The ``Model`` class can be used to automatically generate all IFP tallies using +the Python API with :attr:`openmc.Settings.ifp_n_generation` greater than 0 and +the :meth:`openmc.Model.add_ifp_kinetics_tallies` method:: + + model = openmc.Model(geometry, settings=settings) + model.add_kinetics_parameters_tallies(num_groups=6) # Add 6 precursor groups + +Alternatively, each of the tallies can be manually defined using group-wise or +total :math:`\beta_{\text{eff}}` specified by providing a 6-group +:class:`openmc.DelayedGroupFilter`:: + + beta_tally = openmc.Tally(name="group-beta-score") + beta_tally.scores = ["ifp-beta-numerator"] + + # Add DelayedGroupFilter to enable group-wise tallies + beta_tally.filters = [openmc.DelayedGroupFilter(list(range(1, 7)))] + Here is an example showing how to declare the three available IFP scores in a single tally:: @@ -95,6 +112,12 @@ for ``ifp-denominator``: \beta_{\text{eff}} = \frac{S_{\text{ifp-beta-numerator}}}{S_{\text{ifp-denominator}}} +The kinetics parameters can be retrieved directly from a statepoint file using +the :meth:`openmc.StatePoint.ifp_results` method:: + + with openmc.StatePoint(output_path) as sp: + generation_time, beta_eff = sp.get_kinetics_parameters() + .. only:: html .. rubric:: References @@ -107,4 +130,4 @@ for ``ifp-denominator``: of the Iterated Fission Probability Method in OpenMC to Compute Adjoint-Weighted Kinetics Parameters", International Conference on Mathematics and Computational Methods Applied to Nuclear Science and Engineering (M&C 2025), Denver, April 27-30, - 2025 (to be presented). + 2025. diff --git a/openmc/model/model.py b/openmc/model/model.py index 59e6fa511..c1ffafafd 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections.abc import Iterable, Sequence import copy -from functools import lru_cache +from functools import cache from pathlib import Path import math from numbers import Integral, Real @@ -160,7 +160,7 @@ class Model: return False @property - @lru_cache(maxsize=None) + @cache def _materials_by_id(self) -> dict: """Dictionary mapping material ID --> material""" if self.materials: @@ -170,14 +170,14 @@ class Model: return {mat.id: mat for mat in mats} @property - @lru_cache(maxsize=None) + @cache def _cells_by_id(self) -> dict: """Dictionary mapping cell ID --> cell""" cells = self.geometry.get_all_cells() return {cell.id: cell for cell in cells.values()} @property - @lru_cache(maxsize=None) + @cache def _cells_by_name(self) -> dict[int, openmc.Cell]: # Get the names maps, but since names are not unique, store a set for # each name key. In this way when the user requests a change by a name, @@ -190,7 +190,7 @@ class Model: return result @property - @lru_cache(maxsize=None) + @cache def _materials_by_name(self) -> dict[int, openmc.Material]: if self.materials is None: mats = self.geometry.get_all_materials().values() @@ -203,6 +203,37 @@ class Model: result[mat.name].add(mat) return result + def add_kinetics_parameters_tallies(self, num_groups: int | None = None): + """Add tallies for calculating kinetics parameters using the IFP method. + + This method adds tallies to the model for calculating two kinetics + parameters, the generation time and the effective delayed neutron + fraction (beta effective). After a model is run, these parameters can be + determined through the :meth:`openmc.StatePoint.ifp_results` method. + + Parameters + ---------- + num_groups : int, optional + Number of precursor groups to filter the delayed neutron fraction. + If None, only the total effective delayed neutron fraction is + tallied. + + """ + if not any('ifp-time-numerator' in t.scores for t in self.tallies): + gen_time_tally = openmc.Tally(name='IFP time numerator') + gen_time_tally.scores = ['ifp-time-numerator'] + self.tallies.append(gen_time_tally) + if not any('ifp-beta-numerator' in t.scores for t in self.tallies): + beta_tally = openmc.Tally(name='IFP beta numerator') + beta_tally.scores = ['ifp-beta-numerator'] + if num_groups is not None: + beta_tally.filters = [openmc.DelayedGroupFilter(list(range(1, num_groups + 1)))] + self.tallies.append(beta_tally) + if not any('ifp-denominator' in t.scores for t in self.tallies): + denom_tally = openmc.Tally(name='IFP denominator') + denom_tally.scores = ['ifp-denominator'] + self.tallies.append(denom_tally) + @classmethod def from_xml( cls, diff --git a/openmc/statepoint.py b/openmc/statepoint.py index 715becf48..29c11921c 100644 --- a/openmc/statepoint.py +++ b/openmc/statepoint.py @@ -1,4 +1,5 @@ from datetime import datetime +from collections import namedtuple import glob import re import os @@ -8,6 +9,7 @@ import h5py import numpy as np from pathlib import Path from uncertainties import ufloat +from uncertainties.unumpy import uarray import openmc import openmc.checkvalue as cv @@ -15,6 +17,9 @@ import openmc.checkvalue as cv _VERSION_STATEPOINT = 18 +KineticsParameters = namedtuple("KineticsParameters", ["generation_time", "beta_effective"]) + + class StatePoint: """State information on a simulation at a certain point in time (at the end of a given batch). Statepoints can be used to analyze tally results as well @@ -710,3 +715,56 @@ class StatePoint: tally_filter.paths = cell.paths self._summary = summary + + def get_kinetics_parameters(self) -> KineticsParameters: + """Get kinetics parameters from IFP tallies. + + This method searches the tallies in the statepoint for the tallies + required to compute kinetics parameters using the Iterated Fission + Probability (IFP) method. + + Returns + ------- + KineticsParameters + A named tuple containing the generation time and effective delayed + neutron fraction. If the necessary tallies for one or both + parameters are not found, that parameter is returned as None. + + """ + + denom_tally = None + gen_time_tally = None + beta_tally = None + for tally in self.tallies.values(): + if 'ifp-denominator' in tally.scores: + denom_tally = self.get_tally(scores=['ifp-denominator']) + if 'ifp-time-numerator' in tally.scores: + gen_time_tally = self.get_tally(scores=['ifp-time-numerator']) + if 'ifp-beta-numerator' in tally.scores: + beta_tally = self.get_tally(scores=['ifp-beta-numerator']) + + if denom_tally is None: + return KineticsParameters(None, None) + + def get_ufloat(tally, score): + return uarray(tally.get_values(scores=[score]), + tally.get_values(scores=[score], value='std_dev')) + + denom_values = get_ufloat(denom_tally, 'ifp-denominator') + if gen_time_tally is None: + generation_time = None + else: + gen_time_values = get_ufloat(gen_time_tally, 'ifp-time-numerator') + gen_time_values /= denom_values*self.keff + generation_time = gen_time_values.flatten()[0] + + if beta_tally is None: + beta_effective = None + else: + beta_values = get_ufloat(beta_tally, 'ifp-beta-numerator') + beta_values /= denom_values + beta_effective = beta_values.flatten() + if beta_effective.size == 1: + beta_effective = beta_effective[0] + + return KineticsParameters(generation_time, beta_effective) diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index ae0bffe6e..b9c615ecb 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -560,7 +560,8 @@ void Tally::set_scores(const vector& scores) // Make sure a delayed group filter wasn't used with an incompatible // score. if (delayedgroup_filter_ != C_NONE) { - if (score_str != "delayed-nu-fission" && score_str != "decay-rate") + if (score_str != "delayed-nu-fission" && score_str != "decay-rate" && + score_str != "ifp-beta-numerator") fatal_error("Cannot tally " + score_str + "with a delayedgroup filter"); } diff --git a/src/tallies/tally_scoring.cpp b/src/tallies/tally_scoring.cpp index 0df80a239..67e851644 100644 --- a/src/tallies/tally_scoring.cpp +++ b/src/tallies/tally_scoring.cpp @@ -964,6 +964,15 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, if (delayed_groups.size() == settings::ifp_n_generation) { if (delayed_groups[0] > 0) { score = p.wgt_last(); + if (tally.delayedgroup_filter_ != C_NONE) { + auto i_dg_filt = tally.filters()[tally.delayedgroup_filter_]; + const DelayedGroupFilter& filt { + *dynamic_cast( + model::tally_filters[i_dg_filt].get())}; + score_fission_delayed_dg(i_tally, delayed_groups[0] - 1, + score, score_index, p.filter_matches()); + continue; + } } } } diff --git a/tests/regression_tests/ifp/groupwise/__init__.py b/tests/regression_tests/ifp/groupwise/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/ifp/groupwise/inputs_true.dat b/tests/regression_tests/ifp/groupwise/inputs_true.dat new file mode 100644 index 000000000..6d7e20717 --- /dev/null +++ b/tests/regression_tests/ifp/groupwise/inputs_true.dat @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + eigenvalue + 1000 + 20 + 5 + + + -10.0 -10.0 -10.0 10.0 10.0 10.0 + + + true + + + 5 + + + + 1 2 3 4 5 6 + + + ifp-time-numerator + + + 1 + ifp-beta-numerator + + + ifp-denominator + + + diff --git a/tests/regression_tests/ifp/groupwise/results_true.dat b/tests/regression_tests/ifp/groupwise/results_true.dat new file mode 100644 index 000000000..ea66a8de3 --- /dev/null +++ b/tests/regression_tests/ifp/groupwise/results_true.dat @@ -0,0 +1,21 @@ +k-combined: +1.006559E+00 5.389391E-03 +tally 1: +9.109384E-08 +5.667165E-16 +tally 2: +3.000000E-03 +9.000000E-06 +0.000000E+00 +0.000000E+00 +2.100000E-02 +1.370000E-04 +2.800000E-02 +2.220000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +tally 3: +1.489000E+01 +1.480036E+01 diff --git a/tests/regression_tests/ifp/groupwise/test.py b/tests/regression_tests/ifp/groupwise/test.py new file mode 100644 index 000000000..a1a0ebefb --- /dev/null +++ b/tests/regression_tests/ifp/groupwise/test.py @@ -0,0 +1,40 @@ +"""Test the Iterated Fission Probability (IFP) method to compute adjoint-weighted +kinetics parameters using dedicated tallies.""" + +import openmc +import pytest + +from tests.testing_harness import PyAPITestHarness + +@pytest.fixture() +def ifp_model(): + # Material + material = openmc.Material(name="core") + material.add_nuclide("U235", 1.0) + material.set_density('g/cm3', 16.0) + + # Geometry + radius = 10.0 + sphere = openmc.Sphere(r=radius, boundary_type="vacuum") + cell = openmc.Cell(region=-sphere, fill=material) + geometry = openmc.Geometry([cell]) + + # Settings + settings = openmc.Settings() + settings.particles = 1000 + settings.batches = 20 + settings.inactive = 5 + settings.ifp_n_generation = 5 + + model = openmc.Model(settings=settings, geometry=geometry) + + space = openmc.stats.Box(*cell.bounding_box) + model.settings.source = openmc.IndependentSource( + space=space, constraints={'fissionable': True}) + model.add_kinetics_parameters_tallies(num_groups=6) + return model + + +def test_iterated_fission_probability(ifp_model): + harness = PyAPITestHarness("statepoint.20.h5", model=ifp_model) + harness.main() diff --git a/tests/regression_tests/ifp/total/__init__.py b/tests/regression_tests/ifp/total/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/ifp/inputs_true.dat b/tests/regression_tests/ifp/total/inputs_true.dat similarity index 100% rename from tests/regression_tests/ifp/inputs_true.dat rename to tests/regression_tests/ifp/total/inputs_true.dat diff --git a/tests/regression_tests/ifp/results_true.dat b/tests/regression_tests/ifp/total/results_true.dat similarity index 100% rename from tests/regression_tests/ifp/results_true.dat rename to tests/regression_tests/ifp/total/results_true.dat diff --git a/tests/regression_tests/ifp/test.py b/tests/regression_tests/ifp/total/test.py similarity index 99% rename from tests/regression_tests/ifp/test.py rename to tests/regression_tests/ifp/total/test.py index 6969a54c4..18b89cfc0 100644 --- a/tests/regression_tests/ifp/test.py +++ b/tests/regression_tests/ifp/total/test.py @@ -6,7 +6,6 @@ import pytest from tests.testing_harness import PyAPITestHarness - @pytest.fixture() def ifp_model(): model = openmc.Model() diff --git a/tests/unit_tests/test_ifp.py b/tests/unit_tests/test_ifp.py index 8d0fd9801..e527f1624 100644 --- a/tests/unit_tests/test_ifp.py +++ b/tests/unit_tests/test_ifp.py @@ -47,3 +47,42 @@ def test_exceptions(options, error, run_in_tmpdir, geometry): tallies = openmc.Tallies([tally]) model = openmc.Model(geometry=geometry, settings=settings, tallies=tallies) model.run() + + +@pytest.mark.parametrize( + "num_groups, use_auto_tallies", + [ + (None, True), + (None, False), + (6, True), + (6, False), + ], +) +def test_get_kinetics_parameters(run_in_tmpdir, geometry, num_groups, use_auto_tallies): + # Create basic model + model = openmc.Model(geometry=geometry) + model.settings.particles = 1000 + model.settings.batches = 20 + model.settings.inactive = 5 + model.settings.ifp_n_generation = 5 + + # Add IFP tallies either via the convenience method or manually + if use_auto_tallies: + model.add_kinetics_parameters_tallies(num_groups=num_groups) + else: + for score in ["ifp-time-numerator", "ifp-beta-numerator", "ifp-denominator"]: + tally = openmc.Tally() + tally.scores = [score] + if score == "ifp-beta-numerator" and num_groups is not None: + tally.filters = [openmc.DelayedGroupFilter(list(range(1, num_groups + 1)))] + model.tallies.append(tally) + + # Run and get kinetics parameters + sp_file = model.run() + with openmc.StatePoint(sp_file) as sp: + params = sp.get_kinetics_parameters() + assert isinstance(params, openmc.KineticsParameters) + assert params.generation_time is not None + assert params.beta_effective is not None + if num_groups is not None: + assert len(params.beta_effective) == num_groups From 4011b7a5515e4098f179d825313f574680d3a68f Mon Sep 17 00:00:00 2001 From: Perry <100789850+yrrepy@users.noreply.github.com> Date: Fri, 26 Sep 2025 09:46:23 -0700 Subject: [PATCH 418/671] Optional separation of mesh-material-volume calc from get_homogenized_materials (#3581) Co-authored-by: Paul Romano --- openmc/mesh.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/openmc/mesh.py b/openmc/mesh.py index 2e9abd1b6..9601207e9 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -287,6 +287,7 @@ class MeshBase(IDManagerMixin, ABC): model: openmc.Model, n_samples: int | tuple[int, int, int] = 10_000, include_void: bool = True, + material_volumes: MeshMaterialVolumes | None = None, **kwargs ) -> list[openmc.Material]: """Generate homogenized materials over each element in a mesh. @@ -305,8 +306,12 @@ class MeshBase(IDManagerMixin, ABC): the x, y, and z dimensions. include_void : bool, optional Whether homogenization should include voids. + material_volumes : MeshMaterialVolumes, optional + Previously computed mesh material volumes to use for homogenization. + If not provided, they will be computed by calling + :meth:`material_volumes`. **kwargs - Keyword-arguments passed to :meth:`MeshBase.material_volumes`. + Keyword-arguments passed to :meth:`material_volumes`. Returns ------- @@ -314,7 +319,10 @@ class MeshBase(IDManagerMixin, ABC): Homogenized material in each mesh element """ - vols = self.material_volumes(model, n_samples, **kwargs) + if material_volumes is None: + vols = self.material_volumes(model, n_samples, **kwargs) + else: + vols = material_volumes mat_volume_by_element = [vols.by_element(i) for i in range(vols.num_elements)] # Create homogenized material for each element @@ -424,7 +432,6 @@ class MeshBase(IDManagerMixin, ABC): # Restore original tallies model.tallies = original_tallies - return volumes From feefcc671307fc84e7357430f5f07343503919e5 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Wed, 1 Oct 2025 00:09:15 +0200 Subject: [PATCH 419/671] Adding tally filter type option to statepoint get_tally (#3584) Co-authored-by: Jon Shimwell Co-authored-by: GuySten <62616591+GuySten@users.noreply.github.com> --- openmc/statepoint.py | 9 +++- tests/unit_tests/test_statepoint.py | 65 +++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 tests/unit_tests/test_statepoint.py diff --git a/openmc/statepoint.py b/openmc/statepoint.py index 29c11921c..a763db397 100644 --- a/openmc/statepoint.py +++ b/openmc/statepoint.py @@ -536,7 +536,7 @@ class StatePoint: def get_tally(self, scores=[], filters=[], nuclides=[], name=None, id=None, estimator=None, exact_filters=False, exact_nuclides=False, exact_scores=False, - multiply_density=None, derivative=None): + multiply_density=None, derivative=None, filter_type=None): """Finds and returns a Tally object with certain properties. This routine searches the list of Tallies and returns the first Tally @@ -580,6 +580,9 @@ class StatePoint: to the same value as this parameter. derivative : openmc.TallyDerivative, optional TallyDerivative object to match. + filter_type : type, optional + If not None, the Tally must have at least one Filter that is an + instance of this type. For example `openmc.MeshFilter`. Returns ------- @@ -653,6 +656,10 @@ class StatePoint: if not contains_filters: continue + if filter_type is not None: + if not any(isinstance(f, filter_type) for f in test_tally.filters): + continue + # Determine if Tally has the queried Nuclide(s) if nuclides: if not all(nuclide in test_tally.nuclides for nuclide in nuclides): diff --git a/tests/unit_tests/test_statepoint.py b/tests/unit_tests/test_statepoint.py new file mode 100644 index 000000000..7ffaf7ec2 --- /dev/null +++ b/tests/unit_tests/test_statepoint.py @@ -0,0 +1,65 @@ +import openmc + + +def test_get_tally_filter_type(run_in_tmpdir): + """Test various ways of retrieving tallies from a StatePoint object.""" + + mat = openmc.Material() + mat.add_nuclide("H1", 1.0) + mat.set_density("g/cm3", 10.0) + + sphere = openmc.Sphere(r=10.0, boundary_type="vacuum") + cell = openmc.Cell(fill=mat, region=-sphere) + geometry = openmc.Geometry([cell]) + + settings = openmc.Settings() + settings.particles = 10 + settings.batches = 2 + settings.run_mode = "fixed source" + + reg_mesh = openmc.RegularMesh().from_domain(cell) + tally1 = openmc.Tally(tally_id=1) + mesh_filter = openmc.MeshFilter(reg_mesh) + tally1.filters = [mesh_filter] + tally1.scores = ["flux"] + + tally2 = openmc.Tally(tally_id=2, name="heating tally") + cell_filter = openmc.CellFilter(cell) + tally2.filters = [cell_filter] + tally2.scores = ["heating"] + + tallies = openmc.Tallies([tally1, tally2]) + model = openmc.Model( + geometry=geometry, materials=[mat], settings=settings, tallies=tallies + ) + + sp_filename = model.run() + + sp = openmc.StatePoint(sp_filename) + + tally_found = sp.get_tally(filter_type=openmc.MeshFilter) + assert tally_found.id == 1 + + tally_found = sp.get_tally(filter_type=openmc.CellFilter) + assert tally_found.id == 2 + + tally_found = sp.get_tally(filters=[mesh_filter]) + assert tally_found.id == 1 + + tally_found = sp.get_tally(filters=[cell_filter]) + assert tally_found.id == 2 + + tally_found = sp.get_tally(scores=["heating"]) + assert tally_found.id == 2 + + tally_found = sp.get_tally(name="heating tally") + assert tally_found.id == 2 + + tally_found = sp.get_tally(name=None) + assert tally_found.id == 1 + + tally_found = sp.get_tally(id=1) + assert tally_found.id == 1 + + tally_found = sp.get_tally(id=2) + assert tally_found.id == 2 From 3ac64d9a01d8d10ff126d62d8a9f618fbd803f4b Mon Sep 17 00:00:00 2001 From: John Tramm Date: Thu, 2 Oct 2025 11:02:43 -0500 Subject: [PATCH 420/671] Random Ray Base Source Region Refactor (#3576) --- include/openmc/constants.h | 5 + .../openmc/random_ray/flat_source_domain.h | 47 ++- .../openmc/random_ray/linear_source_domain.h | 2 +- include/openmc/random_ray/random_ray.h | 1 - .../openmc/random_ray/random_ray_simulation.h | 9 +- include/openmc/random_ray/source_region.h | 1 - src/random_ray/flat_source_domain.cpp | 373 ++++++++++-------- src/random_ray/linear_source_domain.cpp | 47 +-- src/random_ray/random_ray.cpp | 121 +++--- src/random_ray/random_ray_simulation.cpp | 190 ++++----- src/random_ray/source_region.cpp | 28 +- src/settings.cpp | 1 - .../random_ray_low_density/__init__.py | 0 .../random_ray_low_density/inputs_true.dat | 244 ++++++++++++ .../random_ray_low_density/results_true.dat | 9 + .../random_ray_low_density/test.py | 60 +++ .../results_true.dat | 8 +- 17 files changed, 700 insertions(+), 446 deletions(-) create mode 100644 tests/regression_tests/random_ray_low_density/__init__.py create mode 100644 tests/regression_tests/random_ray_low_density/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_low_density/results_true.dat create mode 100644 tests/regression_tests/random_ray_low_density/test.py diff --git a/include/openmc/constants.h b/include/openmc/constants.h index df13da370..a0d164613 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -68,6 +68,11 @@ constexpr double MIN_HITS_PER_BATCH {1.5}; // prevent extremely large adjoint source terms from being generated. constexpr double ZERO_FLUX_CUTOFF {1e-22}; +// The minimum macroscopic cross section value considered non-void for the +// random ray solver. Materials with any group with a cross section below this +// value will be converted to pure void. +constexpr double MINIMUM_MACRO_XS {1e-6}; + // ============================================================================ // MATH AND PHYSICAL CONSTANTS diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index 78351fcc5..d4e802734 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -27,8 +27,9 @@ public: //---------------------------------------------------------------------------- // Methods - virtual void update_neutron_source(double k_eff); - double compute_k_eff(double k_eff_old) const; + virtual void update_single_neutron_source(SourceRegionHandle& srh); + virtual void update_all_neutron_sources(); + void compute_k_eff(); virtual void normalize_scalar_flux_and_volumes( double total_active_distance_per_iteration); @@ -41,7 +42,7 @@ public: void output_to_vtk() const; void convert_external_sources(); void count_external_source_regions(); - void set_adjoint_sources(const vector& forward_flux); + void set_adjoint_sources(); void flux_swap(); virtual double evaluate_flux_at_point(Position r, int64_t sr, int g) const; double compute_fixed_source_normalization_factor() const; @@ -54,9 +55,8 @@ public: bool is_target_void); void apply_mesh_to_cell_and_children(int32_t i_cell, int32_t mesh_idx, int32_t target_material_id, bool is_target_void); - void prepare_base_source_regions(); SourceRegionHandle get_subdivided_source_region_handle( - int64_t sr, int mesh_bin, Position r, double dist, Direction u); + SourceRegionKey sr_key, Position r, Direction u); void finalize_discovered_source_regions(); void apply_transport_stabilization(); int64_t n_source_regions() const @@ -67,6 +67,10 @@ public: { return source_regions_.n_source_regions() * negroups_; } + int64_t lookup_base_source_region_idx(const GeometryState& p) const; + SourceRegionKey lookup_source_region_key(const GeometryState& p) const; + int64_t lookup_mesh_bin(int64_t sr, Position r) const; + int lookup_mesh_idx(int64_t sr) const; //---------------------------------------------------------------------------- // Static Data members @@ -86,6 +90,7 @@ public: //---------------------------------------------------------------------------- // Public Data members + double k_eff_ {1.0}; // Eigenvalue bool mapped_all_tallies_ {false}; // If all source regions have been visited int64_t n_external_source_regions_ {0}; // Total number of source regions with @@ -110,14 +115,6 @@ public: // The abstract container holding all source region-specific data SourceRegionContainer source_regions_; - // Base source region container. When source region subdivision via mesh - // is in use, this container holds the original (non-subdivided) material - // filled cell instance source regions. These are useful as they can be - // initialized with external source and mesh domain information ahead of time. - // Then, dynamically discovered source regions can be initialized by cloning - // their base region. - SourceRegionContainer base_source_regions_; - // Parallel hash map holding all source regions discovered during // a single iteration. This is a threadsafe data structure that is cleaned // out after each iteration and stored in the "source_regions_" container. @@ -134,8 +131,17 @@ public: // Map that relates a SourceRegionKey to the external source index. This map // is used to check if there are any point sources within a subdivided source // region at the time it is discovered. - std::unordered_map - point_source_map_; + std::unordered_map, SourceRegionKey::HashFunctor> + external_point_source_map_; + + // Map that relates a base source region index to the external source index. + // This map is used to check if there are any volumetric sources within a + // subdivided source region at the time it is discovered. + std::unordered_map> external_volumetric_source_map_; + + // Map that relates a base source region index to a mesh index. This map + // is used to check which subdivision mesh is present in a source region. + std::unordered_map mesh_map_; // If transport corrected MGXS data is being used, there may be negative // in-group scattering cross sections that can result in instability in MOC @@ -147,12 +153,11 @@ protected: //---------------------------------------------------------------------------- // Methods void apply_external_source_to_source_region( - Discrete* discrete, double strength_factor, SourceRegionHandle& srh); - void apply_external_source_to_cell_instances(int32_t i_cell, - Discrete* discrete, double strength_factor, int target_material_id, - const vector& instances); - void apply_external_source_to_cell_and_children(int32_t i_cell, - Discrete* discrete, double strength_factor, int32_t target_material_id); + int src_idx, SourceRegionHandle& srh); + void apply_external_source_to_cell_instances(int32_t i_cell, int src_idx, + int target_material_id, const vector& instances); + void apply_external_source_to_cell_and_children( + int32_t i_cell, int src_idx, int32_t target_material_id); virtual void set_flux_to_flux_plus_source(int64_t sr, double volume, int g); void set_flux_to_source(int64_t sr, int g); virtual void set_flux_to_old_flux(int64_t sr, int g); diff --git a/include/openmc/random_ray/linear_source_domain.h b/include/openmc/random_ray/linear_source_domain.h index 67fdd99f8..0098c7820 100644 --- a/include/openmc/random_ray/linear_source_domain.h +++ b/include/openmc/random_ray/linear_source_domain.h @@ -20,7 +20,7 @@ class LinearSourceDomain : public FlatSourceDomain { public: //---------------------------------------------------------------------------- // Methods - void update_neutron_source(double k_eff) override; + void update_single_neutron_source(SourceRegionHandle& srh) override; void normalize_scalar_flux_and_volumes( double total_active_distance_per_iteration) override; diff --git a/include/openmc/random_ray/random_ray.h b/include/openmc/random_ray/random_ray.h index abf2a2688..40c67ef95 100644 --- a/include/openmc/random_ray/random_ray.h +++ b/include/openmc/random_ray/random_ray.h @@ -48,7 +48,6 @@ public: static double distance_active_; // Active ray length static unique_ptr ray_source_; // Starting source for ray sampling static RandomRaySourceShape source_shape_; // Flag for linear source - static bool mesh_subdivision_enabled_; // Flag for mesh subdivision static RandomRaySampleMethod sample_method_; // Flag for sampling method //---------------------------------------------------------------------------- diff --git a/include/openmc/random_ray/random_ray_simulation.h b/include/openmc/random_ray/random_ray_simulation.h index b94e7401b..3dec48bf2 100644 --- a/include/openmc/random_ray/random_ray_simulation.h +++ b/include/openmc/random_ray/random_ray_simulation.h @@ -21,11 +21,7 @@ public: // Methods void compute_segment_correction_factors(); void apply_fixed_sources_and_mesh_domains(); - void prepare_fixed_sources_adjoint(vector& forward_flux, - SourceRegionContainer& forward_source_regions, - SourceRegionContainer& forward_base_source_regions, - std::unordered_map& - forward_source_region_map); + void prepare_fixed_sources_adjoint(); void simulate(); void output_simulation_results() const; void instability_check( @@ -45,9 +41,6 @@ private: // Contains all flat source region data unique_ptr domain_; - // Random ray eigenvalue - double k_eff_ {1.0}; - // Tracks the average FSR miss rate for analysis and reporting double avg_miss_rate_ {0.0}; diff --git a/include/openmc/random_ray/source_region.h b/include/openmc/random_ray/source_region.h index 5c5b31f39..0f5a747ff 100644 --- a/include/openmc/random_ray/source_region.h +++ b/include/openmc/random_ray/source_region.h @@ -308,7 +308,6 @@ public: //---------------------------------------------------------------------------- // Constructors SourceRegion(int negroups, bool is_linear); - SourceRegion(const SourceRegionHandle& handle, int64_t parent_sr); SourceRegion() = default; //---------------------------------------------------------------------------- diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 409238830..1bf27e1ed 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -53,24 +53,6 @@ FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) // Initialize source regions. bool is_linear = RandomRay::source_shape_ != RandomRaySourceShape::FLAT; source_regions_ = SourceRegionContainer(negroups_, is_linear); - source_regions_.assign( - base_source_regions, SourceRegion(negroups_, is_linear)); - - // Initialize materials - int64_t source_region_id = 0; - for (int i = 0; i < model::cells.size(); i++) { - Cell& cell = *model::cells[i]; - if (cell.type_ == Fill::MATERIAL) { - for (int j = 0; j < cell.n_instances(); j++) { - source_regions_.material(source_region_id++) = cell.material(j); - } - } - } - - // Sanity check - if (source_region_id != base_source_regions) { - fatal_error("Unexpected number of source regions"); - } // Initialize tally volumes if (volume_normalized_flux_tallies_) { @@ -118,34 +100,24 @@ void FlatSourceDomain::accumulate_iteration_flux() } } -// Compute new estimate of scattering + fission sources in each source region -// based on the flux estimate from the previous iteration. -void FlatSourceDomain::update_neutron_source(double k_eff) +void FlatSourceDomain::update_single_neutron_source(SourceRegionHandle& srh) { - simulation::time_update_src.start(); - - double inverse_k_eff = 1.0 / k_eff; - -// Reset all source regions to zero (important for void regions) -#pragma omp parallel for - for (int64_t se = 0; se < n_source_elements(); se++) { - source_regions_.source(se) = 0.0; + // Reset all source regions to zero (important for void regions) + for (int g = 0; g < negroups_; g++) { + srh.source(g) = 0.0; } // Add scattering + fission source -#pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions(); sr++) { - int material = source_regions_.material(sr); - if (material == MATERIAL_VOID) { - continue; - } + int material = srh.material(); + if (material != MATERIAL_VOID) { + double inverse_k_eff = 1.0 / k_eff_; for (int g_out = 0; g_out < negroups_; g_out++) { double sigma_t = sigma_t_[material * negroups_ + g_out]; double scatter_source = 0.0; double fission_source = 0.0; for (int g_in = 0; g_in < negroups_; g_in++) { - double scalar_flux = source_regions_.scalar_flux_old(sr, g_in); + double scalar_flux = srh.scalar_flux_old(g_in); double sigma_s = sigma_s_[material * negroups_ * negroups_ + g_out * negroups_ + g_in]; double nu_sigma_f = nu_sigma_f_[material * negroups_ + g_in]; @@ -154,18 +126,30 @@ void FlatSourceDomain::update_neutron_source(double k_eff) scatter_source += sigma_s * scalar_flux; fission_source += nu_sigma_f * scalar_flux * chi; } - source_regions_.source(sr, g_out) = + srh.source(g_out) = (scatter_source + fission_source * inverse_k_eff) / sigma_t; } } // Add external source if in fixed source mode if (settings::run_mode == RunMode::FIXED_SOURCE) { -#pragma omp parallel for - for (int64_t se = 0; se < n_source_elements(); se++) { - source_regions_.source(se) += source_regions_.external_source(se); + for (int g = 0; g < negroups_; g++) { + srh.source(g) += srh.external_source(g); } } +} + +// Compute new estimate of scattering + fission sources in each source region +// based on the flux estimate from the previous iteration. +void FlatSourceDomain::update_all_neutron_sources() +{ + simulation::time_update_src.start(); + +#pragma omp parallel for + for (int64_t sr = 0; sr < n_source_regions(); sr++) { + SourceRegionHandle srh = source_regions_.get_source_region_handle(sr); + update_single_neutron_source(srh); + } simulation::time_update_src.stop(); } @@ -320,7 +304,7 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() // Generates new estimate of k_eff based on the differences between this // iteration's estimate of the scalar flux and the last iteration's estimate. -double FlatSourceDomain::compute_k_eff(double k_eff_old) const +void FlatSourceDomain::compute_k_eff() { double fission_rate_old = 0; double fission_rate_new = 0; @@ -365,7 +349,7 @@ double FlatSourceDomain::compute_k_eff(double k_eff_old) const p[sr] = sr_fission_source_new; } - double k_eff_new = k_eff_old * (fission_rate_new / fission_rate_old); + double k_eff_new = k_eff_ * (fission_rate_new / fission_rate_old); double H = 0.0; // defining an inverse sum for better performance @@ -385,7 +369,7 @@ double FlatSourceDomain::compute_k_eff(double k_eff_old) const // Adds entropy value to shared entropy vector in openmc namespace. simulation::entropy.push_back(H); - return k_eff_new; + k_eff_ = k_eff_new; } // This function is responsible for generating a mapping between random @@ -652,7 +636,6 @@ void FlatSourceDomain::random_ray_tally() "random ray mode."); break; } - // Apply score to the appropriate tally bin Tally& tally {*model::tallies[task.tally_idx]}; #pragma omp atomic @@ -726,21 +709,21 @@ void FlatSourceDomain::output_to_vtk() const print_plot(); // Outer loop over plots - for (int p = 0; p < model::plots.size(); p++) { + for (int plt = 0; plt < model::plots.size(); plt++) { // Get handle to OpenMC plot object and extract params - Plot* openmc_plot = dynamic_cast(model::plots[p].get()); + Plot* openmc_plot = dynamic_cast(model::plots[plt].get()); // Random ray plots only support voxel plots if (!openmc_plot) { warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting " "is allowed in random ray mode.", - p)); + plt)); continue; } else if (openmc_plot->type_ != Plot::PlotType::voxel) { warning(fmt::format("Plot {} is invalid plot type -- only voxel plotting " "is allowed in random ray mode.", - p)); + plt)); continue; } @@ -794,23 +777,11 @@ void FlatSourceDomain::output_to_vtk() const continue; } - int i_cell = p.lowest_coord().cell(); - int64_t sr = source_region_offsets_[i_cell] + p.cell_instance(); - if (RandomRay::mesh_subdivision_enabled_) { - int mesh_idx = base_source_regions_.mesh(sr); - int mesh_bin; - if (mesh_idx == C_NONE) { - mesh_bin = 0; - } else { - mesh_bin = model::meshes[mesh_idx]->get_bin(p.r()); - } - SourceRegionKey sr_key {sr, mesh_bin}; - auto it = source_region_map_.find(sr_key); - if (it != source_region_map_.end()) { - sr = it->second; - } else { - sr = -1; - } + SourceRegionKey sr_key = lookup_source_region_key(p); + int64_t sr = -1; + auto it = source_region_map_.find(sr_key); + if (it != source_region_map_.end()) { + sr = it->second; } voxel_indices[z * Ny * Nx + y * Nx + x] = sr; @@ -967,13 +938,17 @@ void FlatSourceDomain::output_to_vtk() const } void FlatSourceDomain::apply_external_source_to_source_region( - Discrete* discrete, double strength_factor, SourceRegionHandle& srh) + int src_idx, SourceRegionHandle& srh) { - srh.external_source_present() = 1; - + auto s = model::external_sources[src_idx].get(); + auto is = dynamic_cast(s); + auto discrete = dynamic_cast(is->energy()); + double strength_factor = is->strength(); const auto& discrete_energies = discrete->x(); const auto& discrete_probs = discrete->prob(); + srh.external_source_present() = 1; + for (int i = 0; i < discrete_energies.size(); i++) { int g = data::mg.get_group_index(discrete_energies[i]); srh.external_source(g) += discrete_probs[i] * strength_factor; @@ -981,8 +956,7 @@ void FlatSourceDomain::apply_external_source_to_source_region( } void FlatSourceDomain::apply_external_source_to_cell_instances(int32_t i_cell, - Discrete* discrete, double strength_factor, int target_material_id, - const vector& instances) + int src_idx, int target_material_id, const vector& instances) { Cell& cell = *model::cells[i_cell]; @@ -1000,16 +974,13 @@ void FlatSourceDomain::apply_external_source_to_cell_instances(int32_t i_cell, if (target_material_id == C_NONE || cell_material_id == target_material_id) { int64_t source_region = source_region_offsets_[i_cell] + j; - SourceRegionHandle srh = - source_regions_.get_source_region_handle(source_region); - apply_external_source_to_source_region(discrete, strength_factor, srh); + external_volumetric_source_map_[source_region].push_back(src_idx); } } } void FlatSourceDomain::apply_external_source_to_cell_and_children( - int32_t i_cell, Discrete* discrete, double strength_factor, - int32_t target_material_id) + int32_t i_cell, int src_idx, int32_t target_material_id) { Cell& cell = *model::cells[i_cell]; @@ -1017,14 +988,14 @@ void FlatSourceDomain::apply_external_source_to_cell_and_children( vector instances(cell.n_instances()); std::iota(instances.begin(), instances.end(), 0); apply_external_source_to_cell_instances( - i_cell, discrete, strength_factor, target_material_id, instances); + i_cell, src_idx, target_material_id, instances); } else if (target_material_id == C_NONE) { std::unordered_map> cell_instance_list = cell.get_contained_cells(0, nullptr); for (const auto& pair : cell_instance_list) { int32_t i_child_cell = pair.first; - apply_external_source_to_cell_instances(i_child_cell, discrete, - strength_factor, target_material_id, pair.second); + apply_external_source_to_cell_instances( + i_child_cell, src_idx, target_material_id, pair.second); } } } @@ -1070,36 +1041,17 @@ void FlatSourceDomain::convert_external_sources() "point source at {}", sp->r())); } - int i_cell = gs.lowest_coord().cell(); - int64_t sr = source_region_offsets_[i_cell] + gs.cell_instance(); + SourceRegionKey key = lookup_source_region_key(gs); - if (RandomRay::mesh_subdivision_enabled_) { - // If mesh subdivision is enabled, we need to determine which subdivided - // mesh bin the point source coordinate is in as well - int mesh_idx = source_regions_.mesh(sr); - int mesh_bin; - if (mesh_idx == C_NONE) { - mesh_bin = 0; - } else { - mesh_bin = model::meshes[mesh_idx]->get_bin(gs.r()); - } - // With the source region and mesh bin known, we can use the - // accompanying SourceRegionKey as a key into a map that stores the - // corresponding external source index for the point source. Notably, we - // do not actually apply the external source to any source regions here, - // as if mesh subdivision is enabled, they haven't actually been - // discovered & initilized yet. When discovered, they will read from the - // point_source_map to determine if there are any point source terms - // that should be applied. - SourceRegionKey key {sr, mesh_bin}; - point_source_map_[key] = es; - } else { - // If we are not using mesh subdivision, we can apply the external - // source directly to the source region as we do for volumetric domain - // constraint sources. - SourceRegionHandle srh = source_regions_.get_source_region_handle(sr); - apply_external_source_to_source_region(energy, strength_factor, srh); - } + // With the source region and mesh bin known, we can use the + // accompanying SourceRegionKey as a key into a map that stores the + // corresponding external source index for the point source. Notably, we + // do not actually apply the external source to any source regions here, + // as if mesh subdivision is enabled, they haven't actually been + // discovered & initilized yet. When discovered, they will read from the + // external_source_map to determine if there are any external source + // terms that should be applied. + external_point_source_map_[key].push_back(es); } else { // If not a point source, then use the volumetric domain constraints to @@ -1107,42 +1059,25 @@ void FlatSourceDomain::convert_external_sources() if (is->domain_type() == Source::DomainType::MATERIAL) { for (int32_t material_id : domain_ids) { for (int i_cell = 0; i_cell < model::cells.size(); i_cell++) { - apply_external_source_to_cell_and_children( - i_cell, energy, strength_factor, material_id); + apply_external_source_to_cell_and_children(i_cell, es, material_id); } } } else if (is->domain_type() == Source::DomainType::CELL) { for (int32_t cell_id : domain_ids) { int32_t i_cell = model::cell_map[cell_id]; - apply_external_source_to_cell_and_children( - i_cell, energy, strength_factor, C_NONE); + apply_external_source_to_cell_and_children(i_cell, es, C_NONE); } } else if (is->domain_type() == Source::DomainType::UNIVERSE) { for (int32_t universe_id : domain_ids) { int32_t i_universe = model::universe_map[universe_id]; Universe& universe = *model::universes[i_universe]; for (int32_t i_cell : universe.cells_) { - apply_external_source_to_cell_and_children( - i_cell, energy, strength_factor, C_NONE); + apply_external_source_to_cell_and_children(i_cell, es, C_NONE); } } } } } // End loop over external sources - -// Divide the fixed source term by sigma t (to save time when applying each -// iteration) -#pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions(); sr++) { - int material = source_regions_.material(sr); - if (material == MATERIAL_VOID) { - continue; - } - for (int g = 0; g < negroups_; g++) { - double sigma_t = sigma_t_[material * negroups_ + g]; - source_regions_.external_source(sr, g) /= sigma_t; - } - } } void FlatSourceDomain::flux_swap() @@ -1159,13 +1094,23 @@ void FlatSourceDomain::flatten_xs() const int a = 0; n_materials_ = data::mg.macro_xs_.size(); - for (auto& m : data::mg.macro_xs_) { + for (int i = 0; i < n_materials_; i++) { + auto& m = data::mg.macro_xs_[i]; for (int g_out = 0; g_out < negroups_; g_out++) { if (m.exists_in_model) { double sigma_t = m.get_xs(MgxsType::TOTAL, g_out, NULL, NULL, NULL, t, a); sigma_t_.push_back(sigma_t); + if (sigma_t < MINIMUM_MACRO_XS) { + Material* mat = model::materials[i].get(); + warning(fmt::format( + "Material \"{}\" (id: {}) has a group {} total cross section " + "({:.3e}) below the minimum threshold " + "({:.3e}). Material will be treated as pure void.", + mat->name(), mat->id(), g_out, sigma_t, MINIMUM_MACRO_XS)); + } + double nu_sigma_f = m.get_xs(MgxsType::NU_FISSION, g_out, NULL, NULL, NULL, t, a); nu_sigma_f_.push_back(nu_sigma_f); @@ -1206,7 +1151,7 @@ void FlatSourceDomain::flatten_xs() } } -void FlatSourceDomain::set_adjoint_sources(const vector& forward_flux) +void FlatSourceDomain::set_adjoint_sources() { // Set the adjoint external source to 1/forward_flux. If the forward flux is // negative, zero, or extremely close to zero, set the adjoint source to zero, @@ -1220,7 +1165,7 @@ void FlatSourceDomain::set_adjoint_sources(const vector& forward_flux) double max_flux = 0.0; #pragma omp parallel for reduction(max : max_flux) for (int64_t se = 0; se < n_source_elements(); se++) { - double flux = forward_flux[se]; + double flux = source_regions_.scalar_flux_final(se); if (flux > max_flux) { max_flux = flux; } @@ -1230,7 +1175,7 @@ void FlatSourceDomain::set_adjoint_sources(const vector& forward_flux) #pragma omp parallel for for (int64_t sr = 0; sr < n_source_regions(); sr++) { for (int g = 0; g < negroups_; g++) { - double flux = forward_flux[sr * negroups_ + g]; + double flux = source_regions_.scalar_flux_final(sr, g); if (flux <= ZERO_FLUX_CUTOFF * max_flux) { source_regions_.external_source(sr, g) = 0.0; } else { @@ -1239,6 +1184,7 @@ void FlatSourceDomain::set_adjoint_sources(const vector& forward_flux) if (flux > 0.0) { source_regions_.external_source_present(sr) = 1; } + source_regions_.scalar_flux_final(sr, g) = 0.0; } } @@ -1265,7 +1211,6 @@ void FlatSourceDomain::set_adjoint_sources(const vector& forward_flux) source_regions_.external_source_present(sr) = 0; } } - // Divide the fixed source term by sigma t (to save time when applying each // iteration) #pragma omp parallel for @@ -1326,13 +1271,14 @@ void FlatSourceDomain::apply_mesh_to_cell_instances(int32_t i_cell, if ((target_material_id == C_NONE && !is_target_void) || cell_material_id == target_material_id) { int64_t sr = source_region_offsets_[i_cell] + j; - if (source_regions_.mesh(sr) != C_NONE) { - // print out the source region that is broken: + // Check if the key is already present in the mesh_map_ + if (mesh_map_.find(sr) != mesh_map_.end()) { fatal_error(fmt::format("Source region {} already has mesh idx {} " "applied, but trying to apply mesh idx {}", - sr, source_regions_.mesh(sr), mesh_idx)); + sr, mesh_map_[sr], mesh_idx)); } - source_regions_.mesh(sr) = mesh_idx; + // If the SR has not already been assigned, then we can write to it + mesh_map_[sr] = mesh_idx; } } } @@ -1402,18 +1348,9 @@ void FlatSourceDomain::apply_meshes() } } -void FlatSourceDomain::prepare_base_source_regions() -{ - std::swap(source_regions_, base_source_regions_); - source_regions_.negroups() = base_source_regions_.negroups(); - source_regions_.is_linear() = base_source_regions_.is_linear(); -} - SourceRegionHandle FlatSourceDomain::get_subdivided_source_region_handle( - int64_t sr, int mesh_bin, Position r, double dist, Direction u) + SourceRegionKey sr_key, Position r, Direction u) { - SourceRegionKey sr_key {sr, mesh_bin}; - // Case 1: Check if the source region key is already present in the permanent // map. This is the most common condition, as any source region visited in a // previous power iteration will already be present in the permanent map. If @@ -1475,9 +1412,8 @@ SourceRegionHandle FlatSourceDomain::get_subdivided_source_region_handle( gs.r() = r + TINY_BIT * u; gs.u() = {1.0, 0.0, 0.0}; exhaustive_find_cell(gs); - int gs_i_cell = gs.lowest_coord().cell(); - int64_t sr_found = source_region_offsets_[gs_i_cell] + gs.cell_instance(); - if (sr_found != sr) { + int64_t sr_found = lookup_base_source_region_idx(gs); + if (sr_found != sr_key.base_source_region_id) { discovered_source_regions_.unlock(sr_key); SourceRegionHandle handle; handle.is_numerical_fp_artifact_ = true; @@ -1485,9 +1421,9 @@ SourceRegionHandle FlatSourceDomain::get_subdivided_source_region_handle( } // Sanity check on mesh bin - int mesh_idx = base_source_regions_.mesh(sr); + int mesh_idx = lookup_mesh_idx(sr_key.base_source_region_id); if (mesh_idx == C_NONE) { - if (mesh_bin != 0) { + if (sr_key.mesh_bin != 0) { discovered_source_regions_.unlock(sr_key); SourceRegionHandle handle; handle.is_numerical_fp_artifact_ = true; @@ -1496,7 +1432,7 @@ SourceRegionHandle FlatSourceDomain::get_subdivided_source_region_handle( } else { Mesh* mesh = model::meshes[mesh_idx].get(); int bin_found = mesh->get_bin(r + TINY_BIT * u); - if (bin_found != mesh_bin) { + if (bin_found != sr_key.mesh_bin) { discovered_source_regions_.unlock(sr_key); SourceRegionHandle handle; handle.is_numerical_fp_artifact_ = true; @@ -1508,26 +1444,60 @@ SourceRegionHandle FlatSourceDomain::get_subdivided_source_region_handle( // condition only occurs the first time the source region is discovered // (typically in the first power iteration). In this case, we need to handle // creation of the new source region and its storage into the parallel map. - // The new source region is created by copying the base source region, so as - // to inherit material, external source, and some flux properties etc. We - // also pass the base source region id to allow the new source region to - // know which base source region it is derived from. - SourceRegion* sr_ptr = discovered_source_regions_.emplace( - sr_key, {base_source_regions_.get_source_region_handle(sr), sr}); - discovered_source_regions_.unlock(sr_key); + // Additionally, we need to determine the source region's material, initialize + // the starting scalar flux guess, and apply any known external sources. + + // Call the basic constructor for the source region and store in the parallel + // map. + bool is_linear = RandomRay::source_shape_ != RandomRaySourceShape::FLAT; + SourceRegion* sr_ptr = + discovered_source_regions_.emplace(sr_key, {negroups_, is_linear}); SourceRegionHandle handle {*sr_ptr}; - // Check if the new source region contains a point source and apply it if so - auto it2 = point_source_map_.find(sr_key); - if (it2 != point_source_map_.end()) { - int es = it2->second; - auto s = model::external_sources[es].get(); - auto is = dynamic_cast(s); - auto energy = dynamic_cast(is->energy()); - double strength_factor = is->strength(); - apply_external_source_to_source_region(energy, strength_factor, handle); - int material = handle.material(); - if (material != MATERIAL_VOID) { + // Determine the material + int gs_i_cell = gs.lowest_coord().cell(); + Cell& cell = *model::cells[gs_i_cell]; + int material = cell.material(gs.cell_instance()); + + // If material total XS is extremely low, just set it to void to avoid + // problems with 1/Sigma_t + for (int g = 0; g < negroups_; g++) { + double sigma_t = sigma_t_[material * negroups_ + g]; + if (sigma_t < MINIMUM_MACRO_XS) { + material = MATERIAL_VOID; + break; + } + } + + handle.material() = material; + + // Store the mesh index (if any) assigned to this source region + handle.mesh() = mesh_idx; + + if (settings::run_mode == RunMode::FIXED_SOURCE) { + // Determine if there are any volumetric sources, and apply them. + // Volumetric sources are specifc only to the base SR idx. + auto it_vol = + external_volumetric_source_map_.find(sr_key.base_source_region_id); + if (it_vol != external_volumetric_source_map_.end()) { + const vector& vol_sources = it_vol->second; + for (int src_idx : vol_sources) { + apply_external_source_to_source_region(src_idx, handle); + } + } + + // Determine if there are any point sources, and apply them. + // Point sources are specific to the source region key. + auto it_point = external_point_source_map_.find(sr_key); + if (it_point != external_point_source_map_.end()) { + const vector& point_sources = it_point->second; + for (int src_idx : point_sources) { + apply_external_source_to_source_region(src_idx, handle); + } + } + + // Divide external source term by sigma_t + if (material != C_NONE) { for (int g = 0; g < negroups_; g++) { double sigma_t = sigma_t_[material * negroups_ + g]; handle.external_source(g) /= sigma_t; @@ -1535,6 +1505,21 @@ SourceRegionHandle FlatSourceDomain::get_subdivided_source_region_handle( } } + // Compute the combined source term + update_single_neutron_source(handle); + + // Unlock the parallel map. Note: we may be tempted to release + // this lock earlier, and then just use the source region's lock to protect + // the flux/source initialization stages above. However, the rest of the code + // only protects updates to the new flux and volume fields, and assumes that + // the source is constant for the duration of transport. Thus, using just the + // source region's lock by itself would result in other threads potentially + // reading from the source before it is computed, as they won't use the lock + // when only reading from the SR's source. It would be expensive to protect + // those operations, whereas generating the SR is only done once, so we just + // hold the map's bucket lock until the source region is fully initialized. + discovered_source_regions_.unlock(sr_key); + return handle; } @@ -1620,4 +1605,52 @@ void FlatSourceDomain::apply_transport_stabilization() } } +// Determines the base source region index (i.e., a material filled cell +// instance) that corresponds to a particular location in the geometry. Requires +// that the "gs" object passed in has already been initialized and has called +// find_cell etc. +int64_t FlatSourceDomain::lookup_base_source_region_idx( + const GeometryState& gs) const +{ + int i_cell = gs.lowest_coord().cell(); + int64_t sr = source_region_offsets_[i_cell] + gs.cell_instance(); + return sr; +} + +// Determines the index of the mesh (if any) that has been applied +// to a particular base source region index. +int FlatSourceDomain::lookup_mesh_idx(int64_t sr) const +{ + int mesh_idx = C_NONE; + auto mesh_it = mesh_map_.find(sr); + if (mesh_it != mesh_map_.end()) { + mesh_idx = mesh_it->second; + } + return mesh_idx; +} + +// Determines the source region key that corresponds to a particular location in +// the geometry. This takes into account both the base source region index as +// well as the mesh bin if a mesh is applied to this source region for +// subdivision. +SourceRegionKey FlatSourceDomain::lookup_source_region_key( + const GeometryState& gs) const +{ + int64_t sr = lookup_base_source_region_idx(gs); + int64_t mesh_bin = lookup_mesh_bin(sr, gs.r()); + return SourceRegionKey {sr, mesh_bin}; +} + +// Determines the mesh bin that corresponds to a particular base source region +// index and position. +int64_t FlatSourceDomain::lookup_mesh_bin(int64_t sr, Position r) const +{ + int mesh_idx = lookup_mesh_idx(sr); + int mesh_bin = 0; + if (mesh_idx != C_NONE) { + mesh_bin = model::meshes[mesh_idx]->get_bin(r); + } + return mesh_bin; +} + } // namespace openmc diff --git a/src/random_ray/linear_source_domain.cpp b/src/random_ray/linear_source_domain.cpp index 81412164e..e1ad68e3d 100644 --- a/src/random_ray/linear_source_domain.cpp +++ b/src/random_ray/linear_source_domain.cpp @@ -34,25 +34,18 @@ void LinearSourceDomain::batch_reset() } } -void LinearSourceDomain::update_neutron_source(double k_eff) +void LinearSourceDomain::update_single_neutron_source(SourceRegionHandle& srh) { - simulation::time_update_src.start(); - - double inverse_k_eff = 1.0 / k_eff; - -// Reset all source regions to zero (important for void regions) -#pragma omp parallel for - for (int64_t se = 0; se < n_source_elements(); se++) { - source_regions_.source(se) = 0.0; + // Reset all source regions to zero (important for void regions) + for (int g = 0; g < negroups_; g++) { + srh.source(g) = 0.0; } -#pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions(); sr++) { - int material = source_regions_.material(sr); - if (material == MATERIAL_VOID) { - continue; - } - MomentMatrix invM = source_regions_.mom_matrix(sr).inverse(); + // Add scattering + fission source + int material = srh.material(); + if (material != MATERIAL_VOID) { + double inverse_k_eff = 1.0 / k_eff_; + MomentMatrix invM = srh.mom_matrix().inverse(); for (int g_out = 0; g_out < negroups_; g_out++) { double sigma_t = sigma_t_[material * negroups_ + g_out]; @@ -64,8 +57,8 @@ void LinearSourceDomain::update_neutron_source(double k_eff) for (int g_in = 0; g_in < negroups_; g_in++) { // Handles for the flat and linear components of the flux - double flux_flat = source_regions_.scalar_flux_old(sr, g_in); - MomentArray flux_linear = source_regions_.flux_moments_old(sr, g_in); + double flux_flat = srh.scalar_flux_old(g_in); + MomentArray flux_linear = srh.flux_moments_old(g_in); // Handles for cross sections double sigma_s = @@ -81,7 +74,7 @@ void LinearSourceDomain::update_neutron_source(double k_eff) } // Compute the flat source term - source_regions_.source(sr, g_out) = + srh.source(g_out) = (scatter_flat + fission_flat * inverse_k_eff) / sigma_t; // Compute the linear source terms. In the first 10 iterations when the @@ -91,25 +84,21 @@ void LinearSourceDomain::update_neutron_source(double k_eff) // very small/noisy or have poorly developed spatial moments, so we zero // the source gradients (effectively making this a flat source region // temporarily), so as to improve stability. - if (simulation::current_batch > 10 && - source_regions_.source(sr, g_out) >= 0.0) { - source_regions_.source_gradients(sr, g_out) = + if (simulation::current_batch > 10 && srh.source(g_out) >= 0.0) { + srh.source_gradients(g_out) = invM * ((scatter_linear + fission_linear * inverse_k_eff) / sigma_t); } else { - source_regions_.source_gradients(sr, g_out) = {0.0, 0.0, 0.0}; + srh.source_gradients(g_out) = {0.0, 0.0, 0.0}; } } } + // Add external source if in fixed source mode if (settings::run_mode == RunMode::FIXED_SOURCE) { -// Add external source to flat source term if in fixed source mode -#pragma omp parallel for - for (int64_t se = 0; se < n_source_elements(); se++) { - source_regions_.source(se) += source_regions_.external_source(se); + for (int g = 0; g < negroups_; g++) { + srh.source(g) += srh.external_source(g); } } - - simulation::time_update_src.stop(); } void LinearSourceDomain::normalize_scalar_flux_and_volumes( diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp index 27f674c42..89a91449d 100644 --- a/src/random_ray/random_ray.cpp +++ b/src/random_ray/random_ray.cpp @@ -237,7 +237,6 @@ double RandomRay::distance_inactive_; double RandomRay::distance_active_; unique_ptr RandomRay::ray_source_; RandomRaySourceShape RandomRay::source_shape_ {RandomRaySourceShape::FLAT}; -bool RandomRay::mesh_subdivision_enabled_ {false}; RandomRaySampleMethod RandomRay::sample_method_ {RandomRaySampleMethod::PRNG}; RandomRay::RandomRay() @@ -336,71 +335,60 @@ void RandomRay::event_advance_ray() void RandomRay::attenuate_flux(double distance, bool is_active, double offset) { - // Determine source region index etc. - int i_cell = lowest_coord().cell(); - - // The base source region is the spatial region index - int64_t sr = domain_->source_region_offsets_[i_cell] + cell_instance(); + // Lookup base source region index + int64_t sr = domain_->lookup_base_source_region_idx(*this); // Perform ray tracing across mesh - if (mesh_subdivision_enabled_) { - // Determine the mesh index for the base source region, if any - int mesh_idx = domain_->base_source_regions_.mesh(sr); + // Determine the mesh index for the base source region, if any + int mesh_idx = domain_->lookup_mesh_idx(sr); - if (mesh_idx == C_NONE) { - // If there's no mesh being applied to this cell, then - // we just attenuate the flux as normal, and set - // the mesh bin to 0 - attenuate_flux_inner(distance, is_active, sr, 0, r()); - } else { - // If there is a mesh being applied to this cell, then - // we loop over all the bin crossings and attenuate - // separately. - Mesh* mesh = model::meshes[mesh_idx].get(); - - // We adjust the start and end positions of the ray slightly - // to accomodate for floating point precision issues that tend - // to occur at mesh boundaries that overlap with geometry lattice - // boundaries. - Position start = r() + (offset + TINY_BIT) * u(); - Position end = start + (distance - 2.0 * TINY_BIT) * u(); - double reduced_distance = (end - start).norm(); - - // Ray trace through the mesh and record bins and lengths - mesh_bins_.resize(0); - mesh_fractional_lengths_.resize(0); - mesh->bins_crossed(start, end, u(), mesh_bins_, mesh_fractional_lengths_); - - // Loop over all mesh bins and attenuate flux - for (int b = 0; b < mesh_bins_.size(); b++) { - double physical_length = reduced_distance * mesh_fractional_lengths_[b]; - attenuate_flux_inner( - physical_length, is_active, sr, mesh_bins_[b], start); - start += physical_length * u(); - } - } + if (mesh_idx == C_NONE) { + // If there's no mesh being applied to this cell, then + // we just attenuate the flux as normal, and set + // the mesh bin to 0 + attenuate_flux_inner(distance, is_active, sr, 0, r()); } else { - attenuate_flux_inner(distance, is_active, sr, C_NONE, r()); + // If there is a mesh being applied to this cell, then + // we loop over all the bin crossings and attenuate + // separately. + Mesh* mesh = model::meshes[mesh_idx].get(); + + // We adjust the start and end positions of the ray slightly + // to accomodate for floating point precision issues that tend + // to occur at mesh boundaries that overlap with geometry lattice + // boundaries. + Position start = r() + (offset + TINY_BIT) * u(); + Position end = start + (distance - 2.0 * TINY_BIT) * u(); + double reduced_distance = (end - start).norm(); + + // Ray trace through the mesh and record bins and lengths + mesh_bins_.resize(0); + mesh_fractional_lengths_.resize(0); + mesh->bins_crossed(start, end, u(), mesh_bins_, mesh_fractional_lengths_); + + // Loop over all mesh bins and attenuate flux + for (int b = 0; b < mesh_bins_.size(); b++) { + double physical_length = reduced_distance * mesh_fractional_lengths_[b]; + attenuate_flux_inner( + physical_length, is_active, sr, mesh_bins_[b], start); + start += physical_length * u(); + } } } void RandomRay::attenuate_flux_inner( double distance, bool is_active, int64_t sr, int mesh_bin, Position r) { + SourceRegionKey sr_key {sr, mesh_bin}; SourceRegionHandle srh; - if (mesh_subdivision_enabled_) { - srh = domain_->get_subdivided_source_region_handle( - sr, mesh_bin, r, distance, u()); - if (srh.is_numerical_fp_artifact_) { - return; - } - } else { - srh = domain_->source_regions_.get_source_region_handle(sr); + srh = domain_->get_subdivided_source_region_handle(sr_key, r, u()); + if (srh.is_numerical_fp_artifact_) { + return; } switch (source_shape_) { case RandomRaySourceShape::FLAT: - if (this->material() == MATERIAL_VOID) { + if (srh.material() == MATERIAL_VOID) { attenuate_flux_flat_source_void(srh, distance, is_active, r); } else { attenuate_flux_flat_source(srh, distance, is_active, r); @@ -408,7 +396,7 @@ void RandomRay::attenuate_flux_inner( break; case RandomRaySourceShape::LINEAR: case RandomRaySourceShape::LINEAR_XY: - if (this->material() == MATERIAL_VOID) { + if (srh.material() == MATERIAL_VOID) { attenuate_flux_linear_source_void(srh, distance, is_active, r); } else { attenuate_flux_linear_source(srh, distance, is_active, r); @@ -439,7 +427,7 @@ void RandomRay::attenuate_flux_flat_source( n_event()++; // Get material - int material = this->material(); + int material = srh.material(); // MOC incoming flux attenuation + source contribution/attenuation equation for (int g = 0; g < negroups_; g++) { @@ -490,7 +478,7 @@ void RandomRay::attenuate_flux_flat_source_void( // The number of geometric intersections is counted for reporting purposes n_event()++; - int material = this->material(); + int material = srh.material(); // If ray is in the active phase (not in dead zone), make contributions to // source region bookkeeping @@ -537,7 +525,7 @@ void RandomRay::attenuate_flux_linear_source( // The number of geometric intersections is counted for reporting purposes n_event()++; - int material = this->material(); + int material = srh.material(); Position& centroid = srh.centroid(); Position midpoint = r + u() * (distance / 2.0); @@ -810,27 +798,12 @@ void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain) cell_born() = lowest_coord().cell(); } + SourceRegionKey sr_key = domain_->lookup_source_region_key(*this); + SourceRegionHandle srh = + domain_->get_subdivided_source_region_handle(sr_key, r(), u()); + // Initialize ray's starting angular flux to starting location's isotropic // source - int i_cell = lowest_coord().cell(); - int64_t sr = domain_->source_region_offsets_[i_cell] + cell_instance(); - - SourceRegionHandle srh; - if (mesh_subdivision_enabled_) { - int mesh_idx = domain_->base_source_regions_.mesh(sr); - int mesh_bin; - if (mesh_idx == C_NONE) { - mesh_bin = 0; - } else { - Mesh* mesh = model::meshes[mesh_idx].get(); - mesh_bin = mesh->get_bin(r()); - } - srh = - domain_->get_subdivided_source_region_handle(sr, mesh_bin, r(), 0.0, u()); - } else { - srh = domain_->source_regions_.get_source_region_handle(sr); - } - if (!srh.is_numerical_fp_artifact_) { for (int g = 0; g < negroups_; g++) { angular_flux_[g] = srh.source(g); diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index 388a778b8..d475b2593 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -47,97 +47,82 @@ void openmc_run_random_ray() if (mpi::master) validate_random_ray_inputs(); - // Declare forward flux so that it can be saved for later adjoint simulation - vector forward_flux; - SourceRegionContainer forward_source_regions; - SourceRegionContainer forward_base_source_regions; - std::unordered_map - forward_source_region_map; + // Initialize Random Ray Simulation Object + RandomRaySimulation sim; - { - // Initialize Random Ray Simulation Object - RandomRaySimulation sim; + // Initialize fixed sources, if present + sim.apply_fixed_sources_and_mesh_domains(); - // Initialize fixed sources, if present - sim.apply_fixed_sources_and_mesh_domains(); + // Begin main simulation timer + simulation::time_total.start(); - // Begin main simulation timer - simulation::time_total.start(); + // Execute random ray simulation + sim.simulate(); - // Execute random ray simulation - sim.simulate(); + // End main simulation timer + simulation::time_total.stop(); - // End main simulation timer - simulation::time_total.stop(); - - // Normalize and save the final forward flux - sim.domain()->serialize_final_fluxes(forward_flux); - - double source_normalization_factor = - sim.domain()->compute_fixed_source_normalization_factor() / - (settings::n_batches - settings::n_inactive); + // Normalize and save the final forward flux + double source_normalization_factor = + sim.domain()->compute_fixed_source_normalization_factor() / + (settings::n_batches - settings::n_inactive); #pragma omp parallel for - for (uint64_t i = 0; i < forward_flux.size(); i++) { - forward_flux[i] *= source_normalization_factor; - } - - forward_source_regions = sim.domain()->source_regions_; - forward_source_region_map = sim.domain()->source_region_map_; - forward_base_source_regions = sim.domain()->base_source_regions_; - - // Finalize OpenMC - openmc_simulation_finalize(); - - // Output all simulation results - sim.output_simulation_results(); + for (uint64_t se = 0; se < sim.domain()->n_source_elements(); se++) { + sim.domain()->source_regions_.scalar_flux_final(se) *= + source_normalization_factor; } + // Finalize OpenMC + openmc_simulation_finalize(); + + // Output all simulation results + sim.output_simulation_results(); + ////////////////////////////////////////////////////////// // Run adjoint simulation (if enabled) ////////////////////////////////////////////////////////// - if (adjoint_needed) { - reset_timers(); - - // Configure the domain for adjoint simulation - FlatSourceDomain::adjoint_ = true; - - if (mpi::master) - header("ADJOINT FLUX SOLVE", 3); - - // Initialize OpenMC general data structures - openmc_simulation_init(); - - // Initialize Random Ray Simulation Object - RandomRaySimulation adjoint_sim; - - // Initialize adjoint fixed sources, if present - adjoint_sim.prepare_fixed_sources_adjoint(forward_flux, - forward_source_regions, forward_base_source_regions, - forward_source_region_map); - - // Transpose scattering matrix - adjoint_sim.domain()->transpose_scattering_matrix(); - - // Swap nu_sigma_f and chi - adjoint_sim.domain()->nu_sigma_f_.swap(adjoint_sim.domain()->chi_); - - // Begin main simulation timer - simulation::time_total.start(); - - // Execute random ray simulation - adjoint_sim.simulate(); - - // End main simulation timer - simulation::time_total.stop(); - - // Finalize OpenMC - openmc_simulation_finalize(); - - // Output all simulation results - adjoint_sim.output_simulation_results(); + if (!adjoint_needed) { + return; } + + reset_timers(); + + // Configure the domain for adjoint simulation + FlatSourceDomain::adjoint_ = true; + + if (mpi::master) + header("ADJOINT FLUX SOLVE", 3); + + // Initialize OpenMC general data structures + openmc_simulation_init(); + + sim.domain()->k_eff_ = 1.0; + + // Initialize adjoint fixed sources, if present + sim.prepare_fixed_sources_adjoint(); + + // Transpose scattering matrix + sim.domain()->transpose_scattering_matrix(); + + // Swap nu_sigma_f and chi + sim.domain()->nu_sigma_f_.swap(sim.domain()->chi_); + + // Begin main simulation timer + simulation::time_total.start(); + + // Execute random ray simulation + sim.simulate(); + + // End main simulation timer + simulation::time_total.stop(); + + // Finalize OpenMC + openmc_simulation_finalize(); + + // Output all simulation results + sim.output_simulation_results(); } // Enforces restrictions on inputs in random ray mode. While there are @@ -348,7 +333,6 @@ void validate_random_ray_inputs() // when generating weight windows with FW-CADIS and an overlaid mesh. /////////////////////////////////////////////////////////////////// if (RandomRay::source_shape_ == RandomRaySourceShape::LINEAR && - RandomRay::mesh_subdivision_enabled_ && variance_reduction::weight_windows.size() > 0) { warning( "Linear sources may result in negative fluxes in small source regions " @@ -366,7 +350,6 @@ void openmc_reset_random_ray() FlatSourceDomain::mesh_domain_map_.clear(); RandomRay::ray_source_.reset(); RandomRay::source_shape_ = RandomRaySourceShape::FLAT; - RandomRay::mesh_subdivision_enabled_ = false; RandomRay::sample_method_ = RandomRaySampleMethod::PRNG; } @@ -412,20 +395,11 @@ void RandomRaySimulation::apply_fixed_sources_and_mesh_domains() } } -void RandomRaySimulation::prepare_fixed_sources_adjoint( - vector& forward_flux, SourceRegionContainer& forward_source_regions, - SourceRegionContainer& forward_base_source_regions, - std::unordered_map& - forward_source_region_map) +void RandomRaySimulation::prepare_fixed_sources_adjoint() { + domain_->source_regions_.adjoint_reset(); if (settings::run_mode == RunMode::FIXED_SOURCE) { - if (RandomRay::mesh_subdivision_enabled_) { - domain_->source_regions_ = forward_source_regions; - domain_->source_region_map_ = forward_source_region_map; - domain_->base_source_regions_ = forward_base_source_regions; - domain_->source_regions_.adjoint_reset(); - } - domain_->set_adjoint_sources(forward_flux); + domain_->set_adjoint_sources(); } } @@ -445,22 +419,18 @@ void RandomRaySimulation::simulate() simulation::total_weight = 1.0; // Update source term (scattering + fission) - domain_->update_neutron_source(k_eff_); + domain_->update_all_neutron_sources(); - // Reset scalar fluxes, iteration volume tallies, and region hit flags to - // zero + // Reset scalar fluxes, iteration volume tallies, and region hit flags + // to zero domain_->batch_reset(); - // At the beginning of the simulation, if mesh subvivision is in use, we + // At the beginning of the simulation, if mesh subdivision is in use, we // need to swap the main source region container into the base container, // as the main source region container will be used to hold the true // subdivided source regions. The base container will therefore only // contain the external source region information, the mesh indices, // material properties, and initial guess values for the flux/source. - if (RandomRay::mesh_subdivision_enabled_ && - simulation::current_batch == 1 && !FlatSourceDomain::adjoint_) { - domain_->prepare_base_source_regions(); - } // Start timer for transport simulation::time_transport.start(); @@ -476,11 +446,9 @@ void RandomRaySimulation::simulate() simulation::time_transport.stop(); - // If using mesh subdivision, add any newly discovered source regions - // to the main source region container. - if (RandomRay::mesh_subdivision_enabled_) { - domain_->finalize_discovered_source_regions(); - } + // Add any newly discovered source regions to the main source region + // container. + domain_->finalize_discovered_source_regions(); // Normalize scalar flux and update volumes domain_->normalize_scalar_flux_and_volumes( @@ -494,10 +462,10 @@ void RandomRaySimulation::simulate() if (settings::run_mode == RunMode::EIGENVALUE) { // Compute random ray k-eff - k_eff_ = domain_->compute_k_eff(k_eff_); + domain_->compute_k_eff(); // Store random ray k-eff into OpenMC's native k-eff variable - global_tally_tracklength = k_eff_; + global_tally_tracklength = domain_->k_eff_; } // Execute all tallying tasks, if this is an active batch @@ -507,12 +475,6 @@ void RandomRaySimulation::simulate() // estimate domain_->accumulate_iteration_flux(); - // Generate mapping between source regions and tallies - if (!domain_->mapped_all_tallies_ && - !RandomRay::mesh_subdivision_enabled_) { - domain_->convert_source_regions_to_tallies(0); - } - // Use above mapping to contribute FSR flux data to appropriate // tallies domain_->random_ray_tally(); @@ -522,7 +484,7 @@ void RandomRaySimulation::simulate() domain_->flux_swap(); // Check for any obvious insabilities/nans/infs - instability_check(n_hits, k_eff_, avg_miss_rate_); + instability_check(n_hits, domain_->k_eff_, avg_miss_rate_); } // End MPI master work // Finalize the current batch @@ -571,7 +533,7 @@ void RandomRaySimulation::instability_check( } if (k_eff > 10.0 || k_eff < 0.01 || !(std::isfinite(k_eff))) { - fatal_error("Instability detected"); + fatal_error(fmt::format("Instability detected: k-eff = {:.5f}", k_eff)); } } } diff --git a/src/random_ray/source_region.cpp b/src/random_ray/source_region.cpp index 1205b995a..3b06f0ed0 100644 --- a/src/random_ray/source_region.cpp +++ b/src/random_ray/source_region.cpp @@ -48,7 +48,7 @@ SourceRegion::SourceRegion(int negroups, bool is_linear) } scalar_flux_new_.assign(negroups, 0.0); - source_.resize(negroups); + source_.assign(negroups, 0.0); scalar_flux_final_.assign(negroups, 0.0); tally_task_.resize(negroups); @@ -60,25 +60,6 @@ SourceRegion::SourceRegion(int negroups, bool is_linear) } } -SourceRegion::SourceRegion(const SourceRegionHandle& handle, int64_t parent_sr) - : SourceRegion(handle.negroups_, handle.is_linear_) -{ - material_ = handle.material(); - mesh_ = handle.mesh(); - parent_sr_ = parent_sr; - for (int g = 0; g < scalar_flux_new_.size(); g++) { - scalar_flux_old_[g] = handle.scalar_flux_old(g); - source_[g] = handle.source(g); - } - - if (settings::run_mode == RunMode::FIXED_SOURCE) { - external_source_present_ = handle.external_source_present(); - for (int g = 0; g < scalar_flux_new_.size(); g++) { - external_source_[g] = handle.external_source(g); - } - } -} - //============================================================================== // SourceRegionContainer implementation //============================================================================== @@ -259,9 +240,12 @@ void SourceRegionContainer::adjoint_reset() MomentMatrix {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}); std::fill(mom_matrix_t_.begin(), mom_matrix_t_.end(), MomentMatrix {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}); - std::fill(scalar_flux_old_.begin(), scalar_flux_old_.end(), 0.0); + if (settings::run_mode == RunMode::FIXED_SOURCE) { + std::fill(scalar_flux_old_.begin(), scalar_flux_old_.end(), 0.0); + } else { + std::fill(scalar_flux_old_.begin(), scalar_flux_old_.end(), 1.0); + } std::fill(scalar_flux_new_.begin(), scalar_flux_new_.end(), 0.0); - std::fill(scalar_flux_final_.begin(), scalar_flux_final_.end(), 0.0); std::fill(source_.begin(), source_.end(), 0.0f); std::fill(external_source_.begin(), external_source_.end(), 0.0f); std::fill(source_gradients_.begin(), source_gradients_.end(), diff --git a/src/settings.cpp b/src/settings.cpp index 03d42bb9d..f9a2469db 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -346,7 +346,6 @@ void get_run_parameters(pugi::xml_node node_base) } FlatSourceDomain::mesh_domain_map_[mesh_id].emplace_back( type, domain_id); - RandomRay::mesh_subdivision_enabled_ = true; } } } diff --git a/tests/regression_tests/random_ray_low_density/__init__.py b/tests/regression_tests/random_ray_low_density/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_low_density/inputs_true.dat b/tests/regression_tests/random_ray_low_density/inputs_true.dat new file mode 100644 index 000000000..c4fd06f42 --- /dev/null +++ b/tests/regression_tests/random_ray_low_density/inputs_true.dat @@ -0,0 +1,244 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_low_density/results_true.dat b/tests/regression_tests/random_ray_low_density/results_true.dat new file mode 100644 index 000000000..a4b3ee1bc --- /dev/null +++ b/tests/regression_tests/random_ray_low_density/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +5.973607E-01 +7.155477E-02 +tally 2: +3.206216E-02 +2.063375E-04 +tally 3: +2.096415E-03 +8.804963E-07 diff --git a/tests/regression_tests/random_ray_low_density/test.py b/tests/regression_tests/random_ray_low_density/test.py new file mode 100644 index 000000000..1b4ffb781 --- /dev/null +++ b/tests/regression_tests/random_ray_low_density/test.py @@ -0,0 +1,60 @@ +import os + +import numpy as np +import openmc +from openmc.examples import random_ray_three_region_cube + +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_low_density(): + model = random_ray_three_region_cube() + + # Rebuild the MGXS library to have a material with very + # low macroscopic cross sections + ebins = [1e-5, 20.0e6] + groups = openmc.mgxs.EnergyGroups(group_edges=ebins) + + void_sigma_a = 4.0e-6 + void_sigma_s = 3.0e-4 + void_mat_data = openmc.XSdata('void', groups) + void_mat_data.order = 0 + void_mat_data.set_total([void_sigma_a + void_sigma_s]) + void_mat_data.set_absorption([void_sigma_a]) + void_mat_data.set_scatter_matrix( + np.rollaxis(np.array([[[void_sigma_s]]]), 0, 3)) + + absorber_sigma_a = 0.75 + absorber_sigma_s = 0.25 + absorber_mat_data = openmc.XSdata('absorber', groups) + absorber_mat_data.order = 0 + absorber_mat_data.set_total([absorber_sigma_a + absorber_sigma_s]) + absorber_mat_data.set_absorption([absorber_sigma_a]) + absorber_mat_data.set_scatter_matrix( + np.rollaxis(np.array([[[absorber_sigma_s]]]), 0, 3)) + + multiplier = 0.0000001 + source_sigma_a = void_sigma_a * multiplier + source_sigma_s = void_sigma_s * multiplier + source_mat_data = openmc.XSdata('source', groups) + source_mat_data.order = 0 + source_mat_data.set_total([source_sigma_a + source_sigma_s]) + source_mat_data.set_absorption([source_sigma_a]) + source_mat_data.set_scatter_matrix( + np.rollaxis(np.array([[[source_sigma_s]]]), 0, 3)) + + mg_cross_sections_file = openmc.MGXSLibrary(groups) + mg_cross_sections_file.add_xsdatas( + [source_mat_data, void_mat_data, absorber_mat_data]) + mg_cross_sections_file.export_to_hdf5() + + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_point_source_locator/results_true.dat b/tests/regression_tests/random_ray_point_source_locator/results_true.dat index 1785dda57..8c6f358dd 100644 --- a/tests/regression_tests/random_ray_point_source_locator/results_true.dat +++ b/tests/regression_tests/random_ray_point_source_locator/results_true.dat @@ -1,9 +1,9 @@ tally 1: -2.633900E+00 -2.948207E+00 +2.633923E+00 +2.948228E+00 tally 2: -1.440463E-01 -3.294032E-03 +1.440456E-01 +3.293984E-03 tally 3: 9.425207E-03 1.089748E-05 From 1dacf4fd2bba3582b575b6abf92b5d201eabab37 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Thu, 2 Oct 2025 11:48:02 -0500 Subject: [PATCH 421/671] Add missing documentation on in depletion chain file format (#3590) Co-authored-by: Paul Romano Co-authored-by: April Novak --- docs/source/io_formats/depletion_chain.rst | 21 +++++++++++++++++++++ openmc/data/decay.py | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/source/io_formats/depletion_chain.rst b/docs/source/io_formats/depletion_chain.rst index 89c76525f..74413e7b6 100644 --- a/docs/source/io_formats/depletion_chain.rst +++ b/docs/source/io_formats/depletion_chain.rst @@ -56,6 +56,27 @@ attributes: .. _io_chain_reaction: +-------------------- +```` Element +-------------------- + +The ```` element represents photon and electron sources associated with +the decay of a nuclide and contains information to construct an +:class:`openmc.stats.Univariate` object that represents this emission as an +energy distribution. This element has the following attributes: + + :type: + The type of :class:`openmc.stats.Univariate` source term. + + :particle: + The type of particle emitted, e.g., 'photon' or 'electron' + + :parameters: + The parameters of the source term, e.g., for a + :class:`openmc.stats.Discrete` source, the energies (in [eV]) at which the + particles are emitted and their relative intensities in [Bq/atom] (in other + words, decay constants). + ---------------------- ```` Element ---------------------- diff --git a/openmc/data/decay.py b/openmc/data/decay.py index 1a11d3614..c8a0bb5e7 100644 --- a/openmc/data/decay.py +++ b/openmc/data/decay.py @@ -591,7 +591,7 @@ def decay_photon_energy(nuclide: str) -> Univariate | None: openmc.stats.Univariate or None Distribution of energies in [eV] of photons emitted from decay, or None if no photon source exists. Note that the probabilities represent - intensities, given as [Bq]. + intensities, given as [Bq/atom] (in other words, decay constants). """ if not _DECAY_PHOTON_ENERGY: chain_file = openmc.config.get('chain_file') From 91a19cf2201731017e02e445f2fc5995cfa51c46 Mon Sep 17 00:00:00 2001 From: Perry <100789850+yrrepy@users.noreply.github.com> Date: Thu, 2 Oct 2025 14:50:42 -0700 Subject: [PATCH 422/671] Ensure weight_windows_file information is read from XML (#3587) Co-authored-by: Paul Romano --- openmc/settings.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openmc/settings.py b/openmc/settings.py index 7bbc31c19..dc6fa99f9 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -1641,6 +1641,7 @@ class Settings: if mesh_memo is not None: mesh_memo.add(ww.mesh.id) + def _create_weight_windows_on_subelement(self, root): if self._weight_windows_on is not None: elem = ET.SubElement(root, "weight_windows_on") elem.text = str(self._weight_windows_on).lower() @@ -2074,10 +2075,16 @@ class Settings: ww = WeightWindows.from_xml_element(elem, meshes) self.weight_windows.append(ww) + def _weight_windows_on_from_xml_element(self, root): text = get_text(root, 'weight_windows_on') if text is not None: self.weight_windows_on = text in ('true', '1') + def _weight_windows_file_from_xml_element(self, root): + text = get_text(root, 'weight_windows_file') + if text is not None: + self.weight_windows_file = text + def _weight_window_checkpoints_from_xml_element(self, root): elem = root.find('weight_window_checkpoints') if elem is None: @@ -2214,6 +2221,7 @@ class Settings: self._create_log_grid_bins_subelement(element) self._create_write_initial_source_subelement(element) self._create_weight_windows_subelement(element, mesh_memo) + self._create_weight_windows_on_subelement(element) self._create_weight_window_generators_subelement(element, mesh_memo) self._create_weight_windows_file_element(element) self._create_weight_window_checkpoints_subelement(element) @@ -2324,6 +2332,8 @@ class Settings: settings._log_grid_bins_from_xml_element(elem) settings._write_initial_source_from_xml_element(elem) settings._weight_windows_from_xml_element(elem, meshes) + settings._weight_windows_on_from_xml_element(elem) + settings._weight_windows_file_from_xml_element(elem) settings._weight_window_generators_from_xml_element(elem, meshes) settings._weight_window_checkpoints_from_xml_element(elem) settings._max_history_splits_from_xml_element(elem) From 7806703c2614853be2d2ce49860e673e208145da Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Oct 2025 22:52:39 +0000 Subject: [PATCH 423/671] Fix random ray source region mesh export when using model.export_to_xml() (#3579) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jtramm <1009059+jtramm@users.noreply.github.com> Co-authored-by: Paul Romano --- openmc/settings.py | 20 +++++++++++---- .../inputs_true.dat | 4 +-- .../random_ray_adjoint_k_eff/inputs_true.dat | 4 +-- .../cell/inputs_true.dat | 2 +- .../material/inputs_true.dat | 2 +- .../universe/inputs_true.dat | 2 +- .../linear/inputs_true.dat | 2 +- .../linear_xy/inputs_true.dat | 2 +- .../flat/inputs_true.dat | 2 +- .../linear/inputs_true.dat | 2 +- .../False/inputs_true.dat | 2 +- .../True/inputs_true.dat | 2 +- .../flat/inputs_true.dat | 2 +- .../linear_xy/inputs_true.dat | 2 +- .../random_ray_halton_samples/inputs_true.dat | 2 +- .../random_ray_k_eff/inputs_true.dat | 2 +- .../random_ray_k_eff_mesh/inputs_true.dat | 2 +- .../random_ray_linear/linear/inputs_true.dat | 2 +- .../linear_xy/inputs_true.dat | 2 +- .../random_ray_low_density/inputs_true.dat | 2 +- .../inputs_true.dat | 2 +- .../random_ray_void/flat/inputs_true.dat | 2 +- .../random_ray_void/linear/inputs_true.dat | 2 +- .../hybrid/inputs_true.dat | 2 +- .../naive/inputs_true.dat | 2 +- .../simulation_averaged/inputs_true.dat | 2 +- .../hybrid/inputs_true.dat | 2 +- .../naive/inputs_true.dat | 2 +- .../simulation_averaged/inputs_true.dat | 2 +- .../weightwindows_fw_cadis/inputs_true.dat | 2 +- .../flat/inputs_true.dat | 2 +- .../linear/inputs_true.dat | 2 +- tests/unit_tests/test_settings.py | 25 ++++++++++++++++++- 33 files changed, 72 insertions(+), 39 deletions(-) diff --git a/openmc/settings.py b/openmc/settings.py index dc6fa99f9..8f8aedc93 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -1715,9 +1715,15 @@ class Settings: domain_elem = ET.SubElement(mesh_elem, 'domain') domain_elem.set('id', str(domain.id)) domain_elem.set('type', domain.__class__.__name__.lower()) - if mesh_memo is not None and mesh.id not in mesh_memo: + # See if a element already exists -- if not, add it + path = f"./mesh[@id='{mesh.id}']" + if root.find(path) is None: root.append(mesh.to_xml_element()) - mesh_memo.add(mesh.id) + if mesh_memo is not None: + mesh_memo.add(mesh.id) + elif isinstance(value, bool): + subelement = ET.SubElement(element, key) + subelement.text = str(value).lower() else: subelement = ET.SubElement(element, key) subelement.text = str(value) @@ -2110,7 +2116,7 @@ class Settings: if text is not None: self.max_tracks = int(text) - def _random_ray_from_xml_element(self, root): + def _random_ray_from_xml_element(self, root, meshes=None): elem = root.find('random_ray') if elem is not None: self.random_ray = {} @@ -2137,7 +2143,11 @@ class Settings: elif child.tag == 'source_region_meshes': self.random_ray['source_region_meshes'] = [] for mesh_elem in child.findall('mesh'): - mesh = MeshBase.from_xml_element(mesh_elem) + mesh_id = int(get_text(mesh_elem, 'id')) + if meshes and mesh_id in meshes: + mesh = meshes[mesh_id] + else: + mesh = MeshBase.from_xml_element(mesh_elem) domains = [] for domain_elem in mesh_elem.findall('domain'): domain_id = int(get_text(domain_elem, "id")) @@ -2339,7 +2349,7 @@ class Settings: settings._max_history_splits_from_xml_element(elem) settings._max_tracks_from_xml_element(elem) settings._max_secondaries_from_xml_element(elem) - settings._random_ray_from_xml_element(elem) + settings._random_ray_from_xml_element(elem, meshes) settings._use_decay_photons_from_xml_element(elem) settings._source_rejection_fraction_from_xml_element(elem) diff --git a/tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat b/tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat index 30e62a885..0adfc5488 100644 --- a/tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat +++ b/tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat @@ -212,8 +212,8 @@ 0.0 0.0 0.0 30.0 30.0 30.0 - True - True + true + true naive diff --git a/tests/regression_tests/random_ray_adjoint_k_eff/inputs_true.dat b/tests/regression_tests/random_ray_adjoint_k_eff/inputs_true.dat index cd4e92aa1..073348c41 100644 --- a/tests/regression_tests/random_ray_adjoint_k_eff/inputs_true.dat +++ b/tests/regression_tests/random_ray_adjoint_k_eff/inputs_true.dat @@ -85,8 +85,8 @@ -1.26 -1.26 -1 1.26 1.26 1 - True - True + true + true diff --git a/tests/regression_tests/random_ray_fixed_source_domain/cell/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/cell/inputs_true.dat index 4b8af76aa..9f1987f3a 100644 --- a/tests/regression_tests/random_ray_fixed_source_domain/cell/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_domain/cell/inputs_true.dat @@ -212,7 +212,7 @@ 0.0 0.0 0.0 30.0 30.0 30.0 - True + true diff --git a/tests/regression_tests/random_ray_fixed_source_domain/material/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/material/inputs_true.dat index 82fe48b61..b4f57dbfa 100644 --- a/tests/regression_tests/random_ray_fixed_source_domain/material/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_domain/material/inputs_true.dat @@ -212,7 +212,7 @@ 0.0 0.0 0.0 30.0 30.0 30.0 - True + true diff --git a/tests/regression_tests/random_ray_fixed_source_domain/universe/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/universe/inputs_true.dat index c4fd06f42..ab91f74e5 100644 --- a/tests/regression_tests/random_ray_fixed_source_domain/universe/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_domain/universe/inputs_true.dat @@ -212,7 +212,7 @@ 0.0 0.0 0.0 30.0 30.0 30.0 - True + true diff --git a/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat index 4085b0c7a..220fa7db6 100644 --- a/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat @@ -212,7 +212,7 @@ 0.0 0.0 0.0 30.0 30.0 30.0 - True + true linear diff --git a/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat index ff085d2fb..f8c443085 100644 --- a/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat @@ -212,7 +212,7 @@ 0.0 0.0 0.0 30.0 30.0 30.0 - True + true linear_xy diff --git a/tests/regression_tests/random_ray_fixed_source_mesh/flat/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_mesh/flat/inputs_true.dat index 12c4d74ed..c84e544fc 100644 --- a/tests/regression_tests/random_ray_fixed_source_mesh/flat/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_mesh/flat/inputs_true.dat @@ -212,7 +212,7 @@ 0.0 0.0 0.0 30.0 30.0 30.0 - True + true diff --git a/tests/regression_tests/random_ray_fixed_source_mesh/linear/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_mesh/linear/inputs_true.dat index 07adb1ff5..05c4846e6 100644 --- a/tests/regression_tests/random_ray_fixed_source_mesh/linear/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_mesh/linear/inputs_true.dat @@ -212,7 +212,7 @@ 0.0 0.0 0.0 30.0 30.0 30.0 - True + true diff --git a/tests/regression_tests/random_ray_fixed_source_normalization/False/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_normalization/False/inputs_true.dat index ab077ae8a..0c870e100 100644 --- a/tests/regression_tests/random_ray_fixed_source_normalization/False/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_normalization/False/inputs_true.dat @@ -212,7 +212,7 @@ 0.0 0.0 0.0 30.0 30.0 30.0 - False + false diff --git a/tests/regression_tests/random_ray_fixed_source_normalization/True/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_normalization/True/inputs_true.dat index c4fd06f42..ab91f74e5 100644 --- a/tests/regression_tests/random_ray_fixed_source_normalization/True/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_normalization/True/inputs_true.dat @@ -212,7 +212,7 @@ 0.0 0.0 0.0 30.0 30.0 30.0 - True + true diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/flat/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_subcritical/flat/inputs_true.dat index a124b5e3c..0c05a71df 100644 --- a/tests/regression_tests/random_ray_fixed_source_subcritical/flat/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_subcritical/flat/inputs_true.dat @@ -115,7 +115,7 @@ -1.26 -1.26 -1 1.26 1.26 1 - False + false flat diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/inputs_true.dat index cc557afb6..a67495bf1 100644 --- a/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/inputs_true.dat @@ -115,7 +115,7 @@ -1.26 -1.26 -1 1.26 1.26 1 - False + false linear_xy diff --git a/tests/regression_tests/random_ray_halton_samples/inputs_true.dat b/tests/regression_tests/random_ray_halton_samples/inputs_true.dat index 3f058e45c..36d5f6f22 100644 --- a/tests/regression_tests/random_ray_halton_samples/inputs_true.dat +++ b/tests/regression_tests/random_ray_halton_samples/inputs_true.dat @@ -85,7 +85,7 @@ -1.26 -1.26 -1 1.26 1.26 1 - True + true halton diff --git a/tests/regression_tests/random_ray_k_eff/inputs_true.dat b/tests/regression_tests/random_ray_k_eff/inputs_true.dat index 33c9cac33..545bd1d45 100644 --- a/tests/regression_tests/random_ray_k_eff/inputs_true.dat +++ b/tests/regression_tests/random_ray_k_eff/inputs_true.dat @@ -85,7 +85,7 @@ -1.26 -1.26 -1 1.26 1.26 1 - True + true diff --git a/tests/regression_tests/random_ray_k_eff_mesh/inputs_true.dat b/tests/regression_tests/random_ray_k_eff_mesh/inputs_true.dat index f0822d9f9..98badea18 100644 --- a/tests/regression_tests/random_ray_k_eff_mesh/inputs_true.dat +++ b/tests/regression_tests/random_ray_k_eff_mesh/inputs_true.dat @@ -85,7 +85,7 @@ -1.26 -1.26 -1 1.26 1.26 1 - True + true diff --git a/tests/regression_tests/random_ray_linear/linear/inputs_true.dat b/tests/regression_tests/random_ray_linear/linear/inputs_true.dat index 006996557..a43a66e71 100644 --- a/tests/regression_tests/random_ray_linear/linear/inputs_true.dat +++ b/tests/regression_tests/random_ray_linear/linear/inputs_true.dat @@ -85,7 +85,7 @@ -1.26 -1.26 -1 1.26 1.26 1 - True + true linear diff --git a/tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat b/tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat index 81527c479..7f76f2fd1 100644 --- a/tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat +++ b/tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat @@ -85,7 +85,7 @@ -1.26 -1.26 -1 1.26 1.26 1 - True + true linear_xy diff --git a/tests/regression_tests/random_ray_low_density/inputs_true.dat b/tests/regression_tests/random_ray_low_density/inputs_true.dat index c4fd06f42..ab91f74e5 100644 --- a/tests/regression_tests/random_ray_low_density/inputs_true.dat +++ b/tests/regression_tests/random_ray_low_density/inputs_true.dat @@ -212,7 +212,7 @@ 0.0 0.0 0.0 30.0 30.0 30.0 - True + true diff --git a/tests/regression_tests/random_ray_point_source_locator/inputs_true.dat b/tests/regression_tests/random_ray_point_source_locator/inputs_true.dat index 82ad979da..088f803bf 100644 --- a/tests/regression_tests/random_ray_point_source_locator/inputs_true.dat +++ b/tests/regression_tests/random_ray_point_source_locator/inputs_true.dat @@ -211,7 +211,7 @@ 0.0 0.0 0.0 30.0 30.0 30.0 - True + true diff --git a/tests/regression_tests/random_ray_void/flat/inputs_true.dat b/tests/regression_tests/random_ray_void/flat/inputs_true.dat index ea8c22f0b..aa28e7b68 100644 --- a/tests/regression_tests/random_ray_void/flat/inputs_true.dat +++ b/tests/regression_tests/random_ray_void/flat/inputs_true.dat @@ -212,7 +212,7 @@ 0.0 0.0 0.0 30.0 30.0 30.0 - True + true flat diff --git a/tests/regression_tests/random_ray_void/linear/inputs_true.dat b/tests/regression_tests/random_ray_void/linear/inputs_true.dat index a089604ef..e4b2f22fa 100644 --- a/tests/regression_tests/random_ray_void/linear/inputs_true.dat +++ b/tests/regression_tests/random_ray_void/linear/inputs_true.dat @@ -212,7 +212,7 @@ 0.0 0.0 0.0 30.0 30.0 30.0 - True + true linear diff --git a/tests/regression_tests/random_ray_volume_estimator/hybrid/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator/hybrid/inputs_true.dat index 089534d74..8e8a8ed9b 100644 --- a/tests/regression_tests/random_ray_volume_estimator/hybrid/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator/hybrid/inputs_true.dat @@ -212,7 +212,7 @@ 0.0 0.0 0.0 30.0 30.0 30.0 - True + true hybrid diff --git a/tests/regression_tests/random_ray_volume_estimator/naive/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator/naive/inputs_true.dat index 56507df04..1e25b97da 100644 --- a/tests/regression_tests/random_ray_volume_estimator/naive/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator/naive/inputs_true.dat @@ -212,7 +212,7 @@ 0.0 0.0 0.0 30.0 30.0 30.0 - True + true naive diff --git a/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/inputs_true.dat index 0331b562c..78c162697 100644 --- a/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/inputs_true.dat @@ -212,7 +212,7 @@ 0.0 0.0 0.0 30.0 30.0 30.0 - True + true simulation_averaged diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/inputs_true.dat index 343833210..47a8a7182 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/inputs_true.dat @@ -212,7 +212,7 @@ 0.0 0.0 0.0 30.0 30.0 30.0 - True + true linear hybrid diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/naive/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/naive/inputs_true.dat index 50be43eb3..80a9ada4d 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/naive/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator_linear/naive/inputs_true.dat @@ -212,7 +212,7 @@ 0.0 0.0 0.0 30.0 30.0 30.0 - True + true linear naive diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/inputs_true.dat index 3d64ab978..4f032a62a 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/inputs_true.dat @@ -212,7 +212,7 @@ 0.0 0.0 0.0 30.0 30.0 30.0 - True + true linear simulation_averaged diff --git a/tests/regression_tests/weightwindows_fw_cadis/inputs_true.dat b/tests/regression_tests/weightwindows_fw_cadis/inputs_true.dat index 448b4b145..5fa6505dd 100644 --- a/tests/regression_tests/weightwindows_fw_cadis/inputs_true.dat +++ b/tests/regression_tests/weightwindows_fw_cadis/inputs_true.dat @@ -227,7 +227,7 @@ 0.0 0.0 0.0 30.0 30.0 30.0 - True + true naive diff --git a/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/inputs_true.dat b/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/inputs_true.dat index d82aa6fae..ceb89e6e3 100644 --- a/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/inputs_true.dat +++ b/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/inputs_true.dat @@ -227,7 +227,7 @@ 0.0 0.0 0.0 30.0 30.0 30.0 - True + true diff --git a/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/inputs_true.dat b/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/inputs_true.dat index 9e4b21d27..c7691e950 100644 --- a/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/inputs_true.dat +++ b/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/inputs_true.dat @@ -227,7 +227,7 @@ 0.0 0.0 0.0 30.0 30.0 30.0 - True + true diff --git a/tests/unit_tests/test_settings.py b/tests/unit_tests/test_settings.py index 6611e4227..a6abca41a 100644 --- a/tests/unit_tests/test_settings.py +++ b/tests/unit_tests/test_settings.py @@ -59,12 +59,23 @@ def test_export_to_xml(run_in_tmpdir): s.electron_treatment = 'led' s.write_initial_source = True s.weight_window_checkpoints = {'surface': True, 'collision': False} + source_region_mesh = openmc.RegularMesh() + source_region_mesh.dimension = [2, 2, 2] + source_region_mesh.lower_left = [-2, -2, -2] + source_region_mesh.upper_right = [2, 2, 2] + root_universe = openmc.Universe() s.random_ray = { 'distance_inactive': 10.0, 'distance_active': 100.0, 'ray_source': openmc.IndependentSource( space=openmc.stats.Box((-1., -1., -1.), (1., 1., 1.)) - ) + ), + 'source_region_meshes': [(source_region_mesh, [root_universe])], + 'volume_estimator': 'hybrid', + 'source_shape': 'linear', + 'volume_normalized_flux_tallies': True, + 'adjoint': False, + 'sample_method': 'halton' } s.max_particle_events = 100 s.max_secondaries = 1_000_000 @@ -145,5 +156,17 @@ def test_export_to_xml(run_in_tmpdir): assert s.random_ray['distance_active'] == 100.0 assert s.random_ray['ray_source'].space.lower_left == [-1., -1., -1.] assert s.random_ray['ray_source'].space.upper_right == [1., 1., 1.] + assert 'source_region_meshes' in s.random_ray + assert len(s.random_ray['source_region_meshes']) == 1 + mesh_and_domains = s.random_ray['source_region_meshes'][0] + recovered_mesh = mesh_and_domains[0] + assert recovered_mesh.dimension == (2, 2, 2) + assert recovered_mesh.lower_left == [-2., -2., -2.] + assert recovered_mesh.upper_right == [2., 2., 2.] + assert s.random_ray['volume_estimator'] == 'hybrid' + assert s.random_ray['source_shape'] == 'linear' + assert s.random_ray['volume_normalized_flux_tallies'] + assert not s.random_ray['adjoint'] + assert s.random_ray['sample_method'] == 'halton' assert s.max_secondaries == 1_000_000 assert s.source_rejection_fraction == 0.01 From 8a62e7e3235f75810cf59fc80bd69951e0d99fda Mon Sep 17 00:00:00 2001 From: Thomas Kittelmann Date: Fri, 3 Oct 2025 01:27:36 +0200 Subject: [PATCH 424/671] Fix caching issue when using NCrystal materials (#3538) Co-authored-by: Jose Ignacio Marquez Damian <22483345+marquezj@users.noreply.github.com> Co-authored-by: Paul Romano --- include/openmc/particle_data.h | 7 ++++--- openmc/plotter.py | 2 +- src/particle.cpp | 4 +++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index afcd56476..5383487d4 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -154,9 +154,10 @@ struct NuclideMicroXS { // Energy and temperature last used to evaluate these cross sections. If // these values have changed, then the cross sections must be re-evaluated. - double last_E {0.0}; //!< Last evaluated energy - double last_sqrtkT {0.0}; //!< Last temperature in sqrt(Boltzmann constant - //!< * temperature (eV)) + double last_E {0.0}; //!< Last evaluated energy + double last_sqrtkT {0.0}; //!< Last temperature in sqrt(Boltzmann constant + //!< * temperature (eV)) + double ncrystal_xs {-1.0}; //!< NCrystal cross section }; //============================================================================== diff --git a/openmc/plotter.py b/openmc/plotter.py index 693cdaca4..abd8ab6dd 100644 --- a/openmc/plotter.py +++ b/openmc/plotter.py @@ -501,7 +501,7 @@ def _calculate_cexs_nuclide(this, types, temperature=294., sab_name=None, elif ncrystal_cfg: import NCrystal nc_scatter = NCrystal.createScatter(ncrystal_cfg) - nc_func = nc_scatter.crossSectionNonOriented + nc_func = nc_scatter.xsect nc_emax = 5 # eV # this should be obtained from NCRYSTAL_MAX_ENERGY energy_grid = np.union1d(np.geomspace(min(energy_grid), 1.1*nc_emax, diff --git a/src/particle.cpp b/src/particle.cpp index 402af2498..06ccf462a 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -855,10 +855,12 @@ void Particle::update_neutron_xs( // If the cache doesn't match, recalculate micro xs if (this->E() != micro.last_E || this->sqrtkT() != micro.last_sqrtkT || - i_sab != micro.index_sab || sab_frac != micro.sab_frac) { + i_sab != micro.index_sab || sab_frac != micro.sab_frac || + ncrystal_xs != micro.ncrystal_xs) { data::nuclides[i_nuclide]->calculate_xs(i_sab, i_grid, sab_frac, *this); // If NCrystal is being used, update micro cross section cache + micro.ncrystal_xs = ncrystal_xs; if (ncrystal_xs >= 0.0) { data::nuclides[i_nuclide]->calculate_elastic_xs(*this); ncrystal_update_micro(ncrystal_xs, micro); From 50071aa3bd29b78f28fe4c8efe211cb5f65778ee Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Fri, 3 Oct 2025 17:09:57 +0200 Subject: [PATCH 425/671] Speed up time correction factors (#3592) Co-authored-by: Jon Shimwell Co-authored-by: Paul Romano --- openmc/deplete/d1s.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/openmc/deplete/d1s.py b/openmc/deplete/d1s.py index 311bc2d69..f51dea416 100644 --- a/openmc/deplete/d1s.py +++ b/openmc/deplete/d1s.py @@ -108,14 +108,15 @@ def time_correction_factors( # Create a 2D array for the time correction factors h = np.zeros((n_timesteps, n_nuclides)) - for i, (dt, rate) in enumerate(zip(timesteps, source_rates)): - # Precompute the exponential terms. Since (1 - exp(-x)) is susceptible to - # roundoff error, use expm1 instead (which computes exp(x) - 1) - g = np.exp(-decay_rate*dt) - one_minus_g = -np.expm1(-decay_rate*dt) + # Precompute all exponential terms with same shape as h + decay_dt = decay_rate[np.newaxis, :] * timesteps[:, np.newaxis] + g = np.exp(-decay_dt) + one_minus_g = -np.expm1(-decay_dt) + # Apply recurrence relation step by step + for i in range(len(timesteps)): # Eq. (4) in doi:10.1016/j.fusengdes.2019.111399 - h[i + 1] = rate*one_minus_g + h[i]*g + h[i + 1] = source_rates[i] * one_minus_g[i] + h[i] * g[i] return {nuclides[i]: h[:, i] for i in range(n_nuclides)} From 2c15480cc9f1f7407b2fecd8b47a56f976b7bd02 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 6 Oct 2025 07:45:26 -0500 Subject: [PATCH 426/671] Add user setting for free gas threshold (#3593) --- docs/source/io_formats/settings.rst | 10 ++++++++++ include/openmc/physics.h | 7 ------- include/openmc/settings.h | 2 ++ openmc/settings.py | 28 ++++++++++++++++++++++++++++ src/finalize.cpp | 1 + src/physics.cpp | 2 +- src/settings.cpp | 5 +++++ tests/unit_tests/test_settings.py | 2 ++ 8 files changed, 49 insertions(+), 8 deletions(-) diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 26673faac..720846c85 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -178,6 +178,16 @@ history-based parallelism. *Default*: false +-------------------------------- +```` Element +-------------------------------- + +The ```` element specifies the energy multiplier, expressed +in units of :math:`kT`, that determines when the free gas scattering approach is +used for elastic scattering. Values must be positive. + + *Default*: 400.0 + ----------------------------------- ```` Element ----------------------------------- diff --git a/include/openmc/physics.h b/include/openmc/physics.h index f62f43a02..2472d9799 100644 --- a/include/openmc/physics.h +++ b/include/openmc/physics.h @@ -10,13 +10,6 @@ namespace openmc { -//============================================================================== -// Constants -//============================================================================== - -// Monoatomic ideal-gas scattering treatment threshold -constexpr double FREE_GAS_THRESHOLD {400.0}; - //============================================================================== // Non-member functions //============================================================================== diff --git a/include/openmc/settings.h b/include/openmc/settings.h index 999b1b83f..78bfa088e 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -147,6 +147,8 @@ extern std::unordered_set source_write_surf_id; //!< Surface ids where sources will be written extern double source_rejection_fraction; //!< Minimum fraction of source sites //!< that must be accepted +extern double free_gas_threshold; //!< Threshold multiplier for free gas + //!< scattering treatment extern int max_history_splits; //!< maximum number of particle splits for weight windows diff --git a/openmc/settings.py b/openmc/settings.py index 8f8aedc93..2f8a2b124 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -84,6 +84,10 @@ class Settings: history-based parallelism. .. versionadded:: 0.12 + free_gas_threshold : float + Energy multiplier (in units of :math:`kT`) below which the free gas + scattering treatment is applied for elastic scattering. If not + specified, a value of 400.0 is used. generations_per_batch : int Number of generations per batch ifp_n_generation : int @@ -376,6 +380,7 @@ class Settings: self._seed = None self._stride = None self._survival_biasing = None + self._free_gas_threshold = None # Shannon entropy mesh self._entropy_mesh = None @@ -1255,6 +1260,17 @@ class Settings: cv.check_less_than('source_rejection_fraction', source_rejection_fraction, 1) self._source_rejection_fraction = source_rejection_fraction + @property + def free_gas_threshold(self) -> float | None: + return self._free_gas_threshold + + @free_gas_threshold.setter + def free_gas_threshold(self, free_gas_threshold: float | None): + if free_gas_threshold is not None: + cv.check_type('free gas threshold', free_gas_threshold, Real) + cv.check_greater_than('free gas threshold', free_gas_threshold, 0.0) + self._free_gas_threshold = free_gas_threshold + def _create_run_mode_subelement(self, root): elem = ET.SubElement(root, "run_mode") elem.text = self._run_mode.value @@ -1733,6 +1749,11 @@ class Settings: element = ET.SubElement(root, "source_rejection_fraction") element.text = str(self._source_rejection_fraction) + def _create_free_gas_threshold_subelement(self, root): + if self._free_gas_threshold is not None: + element = ET.SubElement(root, "free_gas_threshold") + element.text = str(self._free_gas_threshold) + def _eigenvalue_from_xml_element(self, root): elem = root.find('eigenvalue') if elem is not None: @@ -2171,6 +2192,11 @@ class Settings: if text is not None: self.source_rejection_fraction = float(text) + def _free_gas_threshold_from_xml_element(self, root): + text = get_text(root, 'free_gas_threshold') + if text is not None: + self.free_gas_threshold = float(text) + def to_xml_element(self, mesh_memo=None): """Create a 'settings' element to be written to an XML file. @@ -2241,6 +2267,7 @@ class Settings: self._create_random_ray_subelement(element, mesh_memo) self._create_use_decay_photons_subelement(element) self._create_source_rejection_fraction_subelement(element) + self._create_free_gas_threshold_subelement(element) # Clean the indentation in the file to be user-readable clean_indentation(element) @@ -2352,6 +2379,7 @@ class Settings: settings._random_ray_from_xml_element(elem, meshes) settings._use_decay_photons_from_xml_element(elem) settings._source_rejection_fraction_from_xml_element(elem) + settings._free_gas_threshold_from_xml_element(elem) return settings diff --git a/src/finalize.cpp b/src/finalize.cpp index 25b471a8b..76babbb96 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -85,6 +85,7 @@ int openmc_finalize() settings::time_cutoff = {INFTY, INFTY, INFTY, INFTY}; settings::entropy_on = false; settings::event_based = false; + settings::free_gas_threshold = 400.0; settings::gen_per_batch = 1; settings::legendre_to_tabular = true; settings::legendre_to_tabular_points = -1; diff --git a/src/physics.cpp b/src/physics.cpp index e947fecbb..3a17077b3 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -851,7 +851,7 @@ Direction sample_target_velocity(const Nuclide& nuc, double E, Direction u, // otherwise, use free gas model } else { - if (E >= FREE_GAS_THRESHOLD * kT && nuc.awr_ > 1.0) { + if (E >= settings::free_gas_threshold * kT && nuc.awr_ > 1.0) { return {}; } else { sampling_method = ResScatMethod::cxs; diff --git a/src/settings.cpp b/src/settings.cpp index f9a2469db..325256cdc 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -126,6 +126,7 @@ SolverType solver_type {SolverType::MONTE_CARLO}; std::unordered_set sourcepoint_batch; std::unordered_set statepoint_batch; double source_rejection_fraction {0.05}; +double free_gas_threshold {400.0}; std::unordered_set source_write_surf_id; int64_t ssw_max_particles; int64_t ssw_max_files; @@ -651,6 +652,10 @@ void read_settings_xml(pugi::xml_node root) std::stod(get_node_value(root, "source_rejection_fraction")); } + if (check_for_node(root, "free_gas_threshold")) { + free_gas_threshold = std::stod(get_node_value(root, "free_gas_threshold")); + } + // Survival biasing if (check_for_node(root, "survival_biasing")) { survival_biasing = get_node_value_bool(root, "survival_biasing"); diff --git a/tests/unit_tests/test_settings.py b/tests/unit_tests/test_settings.py index a6abca41a..fe618fd2d 100644 --- a/tests/unit_tests/test_settings.py +++ b/tests/unit_tests/test_settings.py @@ -80,6 +80,7 @@ def test_export_to_xml(run_in_tmpdir): s.max_particle_events = 100 s.max_secondaries = 1_000_000 s.source_rejection_fraction = 0.01 + s.free_gas_threshold = 800.0 # Make sure exporting XML works s.export_to_xml() @@ -170,3 +171,4 @@ def test_export_to_xml(run_in_tmpdir): assert s.random_ray['sample_method'] == 'halton' assert s.max_secondaries == 1_000_000 assert s.source_rejection_fraction == 0.01 + assert s.free_gas_threshold == 800.0 From 3dfa34d2c6a239cad327baa5565cd71f158fc299 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 6 Oct 2025 21:14:37 -0500 Subject: [PATCH 427/671] Switch to using coveralls github action for reporting (#3594) --- .github/workflows/ci.yml | 35 +++++++++++++++++++++++++++++++---- pyproject.toml | 4 ++-- tools/ci/gha-script.sh | 2 +- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38a49b602..d75a64d66 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,6 +75,7 @@ jobs: LIBMESH: ${{ matrix.libmesh }} NPY_DISABLE_CPU_FEATURES: "AVX512F AVX512_SKX" OPENBLAS_NUM_THREADS: 1 + PYTEST_ADDOPTS: --cov=openmc --cov-report=lcov:coverage-python.lcov # libfabric complains about fork() as a result of using Python multiprocessing. # We can work around it with RDMAV_FORK_SAFE=1 in libfabric < 1.13 and with # FI_EFA_FORK_SAFE=1 in more recent versions. @@ -171,11 +172,37 @@ jobs: uses: mxschmitt/action-tmate@v3 timeout-minutes: 10 - - name: after_success + - name: Generate C++ coverage (gcovr) shell: bash run: | - cpp-coveralls -i src -i include -e src/external --exclude-pattern "/usr/*" --dump cpp_cov.json - coveralls --merge=cpp_cov.json --service=github + # Produce LCOV directly from gcov data in the build tree + gcovr \ + --root "$GITHUB_WORKSPACE" \ + --object-directory "$GITHUB_WORKSPACE/build" \ + --filter "$GITHUB_WORKSPACE/src" \ + --filter "$GITHUB_WORKSPACE/include" \ + --exclude "$GITHUB_WORKSPACE/src/external/.*" \ + --exclude "$GITHUB_WORKSPACE/src/include/openmc/external/.*" \ + --gcov-ignore-errors source_not_found \ + --gcov-ignore-errors output_error \ + --gcov-ignore-parse-errors suspicious_hits.warn \ + --print-summary \ + --lcov -o coverage-cpp.lcov || true + + - name: Merge C++ and Python coverage + shell: bash + run: | + # Merge C++ and Python LCOV into a single file for upload + cat coverage-cpp.lcov coverage-python.lcov > coverage.lcov + + - name: Upload coverage to Coveralls + if: ${{ hashFiles('coverage.lcov') != '' }} + uses: coverallsapp/github-action@v2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + parallel: true + flag-name: C++ and Python + path-to-lcov: coverage.lcov finish: needs: main @@ -184,5 +211,5 @@ jobs: - name: Coveralls Finished uses: coverallsapp/github-action@v2 with: - github-token: ${{ secrets.github_token }} + github-token: ${{ secrets.GITHUB_TOKEN }} parallel-finished: true diff --git a/pyproject.toml b/pyproject.toml index 7705de8da..53d429d94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,8 +48,8 @@ docs = [ "sphinxcontrib-svg2pdfconverter", "sphinx-rtd-theme" ] -test = ["packaging", "pytest", "pytest-cov", "colorama", "openpyxl"] -ci = ["cpp-coveralls", "coveralls"] +test = ["packaging", "pytest", "pytest-cov>=4.0", "colorama", "openpyxl"] +ci = ["coverage>=7.4", "gcovr>=7.2"] vtk = ["vtk"] [project.urls] diff --git a/tools/ci/gha-script.sh b/tools/ci/gha-script.sh index c1d292137..b40238ffb 100755 --- a/tools/ci/gha-script.sh +++ b/tools/ci/gha-script.sh @@ -15,7 +15,7 @@ if [[ $EVENT == 'y' ]]; then fi # Run unit tests and then regression tests -pytest --cov=openmc -v $args \ +pytest -v $args \ tests/test_matplotlib_import.py \ tests/unit_tests \ tests/regression_tests From 58c7fbeac67a3e6db0e26a9dc2db421da3abf933 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 14 Oct 2025 07:47:26 -0500 Subject: [PATCH 428/671] Re-run flaky tests when needed (#3604) --- pyproject.toml | 9 ++++++++- tests/unit_tests/test_deplete_activation.py | 1 + tests/unit_tests/test_stats.py | 10 ++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 53d429d94..2d67e8340 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,14 @@ docs = [ "sphinxcontrib-svg2pdfconverter", "sphinx-rtd-theme" ] -test = ["packaging", "pytest", "pytest-cov>=4.0", "colorama", "openpyxl"] +test = [ + "packaging", + "pytest", + "pytest-cov>=4.0", + "pytest-rerunfailures", + "colorama", + "openpyxl", +] ci = ["coverage>=7.4", "gcovr>=7.2"] vtk = ["vtk"] diff --git a/tests/unit_tests/test_deplete_activation.py b/tests/unit_tests/test_deplete_activation.py index cbe00d680..eace1976e 100644 --- a/tests/unit_tests/test_deplete_activation.py +++ b/tests/unit_tests/test_deplete_activation.py @@ -49,6 +49,7 @@ ENERGIES = np.logspace(log10(1e-5), log10(2e7), 100) ("flux", {'energies': ENERGIES, 'reactions': ['(n,gamma)']}, 1e-5), ("flux", {'energies': ENERGIES, 'reactions': ['(n,gamma)'], 'nuclides': ['W186', 'H3']}, 1e-2), ]) +@pytest.mark.flaky(reruns=1) def test_activation(run_in_tmpdir, model, reaction_rate_mode, reaction_rate_opts, tolerance): # Determine (n.gamma) reaction rate using initial run sp = model.run() diff --git a/tests/unit_tests/test_stats.py b/tests/unit_tests/test_stats.py index 386181f34..abf143f12 100644 --- a/tests/unit_tests/test_stats.py +++ b/tests/unit_tests/test_stats.py @@ -16,6 +16,7 @@ def assert_sample_mean(samples, expected_mean): assert np.abs(expected_mean - samples.mean()) < 4*std_dev +@pytest.mark.flaky(reruns=1) def test_discrete(): x = [0.0, 1.0, 10.0] p = [0.3, 0.2, 0.5] @@ -104,6 +105,7 @@ def test_clip_discrete(): d.clip(5) +@pytest.mark.flaky(reruns=1) def test_uniform(): a, b = 10.0, 20.0 d = openmc.stats.Uniform(a, b) @@ -127,6 +129,7 @@ def test_uniform(): assert_sample_mean(samples, exp_mean) +@pytest.mark.flaky(reruns=1) def test_powerlaw(): a, b, n = 10.0, 100.0, 2.0 d = openmc.stats.PowerLaw(a, b, n) @@ -148,6 +151,7 @@ def test_powerlaw(): assert_sample_mean(samples, exp_mean) +@pytest.mark.flaky(reruns=1) def test_maxwell(): theta = 1.2895e6 d = openmc.stats.Maxwell(theta) @@ -171,6 +175,7 @@ def test_maxwell(): assert samples_2.mean() != samples.mean() +@pytest.mark.flaky(reruns=1) def test_watt(): a, b = 0.965e6, 2.29e-6 d = openmc.stats.Watt(a, b) @@ -194,6 +199,7 @@ def test_watt(): assert_sample_mean(samples, exp_mean) +@pytest.mark.flaky(reruns=1) def test_tabular(): # test linear-linear sampling x = np.array([0.0, 5.0, 7.0, 10.0]) @@ -270,6 +276,7 @@ def test_legendre(): d.to_xml_element('distribution') +@pytest.mark.flaky(reruns=1) def test_mixture(): d1 = openmc.stats.Uniform(0, 5) d2 = openmc.stats.Uniform(3, 7) @@ -425,6 +432,7 @@ def test_point(): assert d.xyz == pytest.approx(p) +@pytest.mark.flaky(reruns=1) def test_normal(): mean = 10.0 std_dev = 2.0 @@ -444,6 +452,7 @@ def test_normal(): assert_sample_mean(samples, mean) +@pytest.mark.flaky(reruns=1) def test_muir(): mean = 10.0 mass = 5.0 @@ -463,6 +472,7 @@ def test_muir(): assert_sample_mean(samples, mean) +@pytest.mark.flaky(reruns=1) def test_combine_distributions(): # Combine two discrete (same data as in test_merge_discrete) x1 = [0.0, 1.0, 10.0] From e9077b1372fa1e3ee4adbcfcbfaeaf04a340c8dd Mon Sep 17 00:00:00 2001 From: jiankai-yu Date: Tue, 14 Oct 2025 16:21:11 -0400 Subject: [PATCH 429/671] Allow V0 in atomic_mass function (for ENDF/B-VII.0 data) (#3607) --- openmc/data/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmc/data/data.py b/openmc/data/data.py index 2142a5dc9..5ecadd37b 100644 --- a/openmc/data/data.py +++ b/openmc/data/data.py @@ -324,7 +324,7 @@ def atomic_mass(isotope): # isotopes of their element (e.g. C0), calculate the atomic mass as # the sum of the atomic mass times the natural abundance of the isotopes # that make up the element. - for element in ['C', 'Zn', 'Pt', 'Os', 'Tl']: + for element in ['C', 'Zn', 'Pt', 'Os', 'Tl', 'V']: isotope_zero = element.lower() + '0' _ATOMIC_MASS[isotope_zero] = 0. for iso, abundance in isotopes(element): From b94b49611365a7eb9bd1a107861e5ae5c44b977d Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Thu, 16 Oct 2025 01:51:59 +0200 Subject: [PATCH 430/671] Ability to source electron/positrons directly (#3404) Co-authored-by: Paul Romano --- .../methods/charged_particles_physics.rst | 362 ++++++++++++++++++ docs/source/methods/index.rst | 3 +- docs/source/methods/photon_physics.rst | 338 ---------------- include/openmc/nuclide.h | 4 +- openmc/source.py | 7 +- src/finalize.cpp | 4 +- src/nuclide.cpp | 4 +- src/particle.cpp | 2 +- src/simulation.cpp | 21 +- src/source.cpp | 6 + .../electron_heating/__init__.py | 0 .../electron_heating/inputs_true.dat | 32 ++ .../electron_heating/results_true.dat | 3 + .../regression_tests/electron_heating/test.py | 40 ++ 14 files changed, 472 insertions(+), 354 deletions(-) create mode 100644 docs/source/methods/charged_particles_physics.rst create mode 100644 tests/regression_tests/electron_heating/__init__.py create mode 100644 tests/regression_tests/electron_heating/inputs_true.dat create mode 100644 tests/regression_tests/electron_heating/results_true.dat create mode 100644 tests/regression_tests/electron_heating/test.py diff --git a/docs/source/methods/charged_particles_physics.rst b/docs/source/methods/charged_particles_physics.rst new file mode 100644 index 000000000..5d763074f --- /dev/null +++ b/docs/source/methods/charged_particles_physics.rst @@ -0,0 +1,362 @@ +.. _methods_charged_particle_physics: + +======================== +Charged Particle Physics +======================== + +OpenMC neglects the spatial transport of charged particles (electrons and +positrons), assuming they deposit all their energy locally and produce +bremsstrahlung photons at their birth location. This approximation, called +thick-target bremsstrahlung (TTB) approximation is justified by the fact that +charged particles have much shorter stopping ranges compared to neutrons and +photons, especially in high-density materials. + +----------------------------- +Charged Particle Interactions +----------------------------- + +Bremsstrahlung +-------------- + +When a charged particle is decelerated in the field of an atom, some of its +kinetic energy is converted into electromagnetic radiation known as +bremsstrahlung, or 'braking radiation'. In each event, an electron or positron +with kinetic energy :math:`T` generates a photon with an energy :math:`E` +between :math:`0` and :math:`T`. Bremsstrahlung is described by a cross section +that is differential in photon energy, in the direction of the emitted photon, +and in the final direction of the charged particle. However, in Monte Carlo +simulations it is typical to integrate over the angular variables to obtain a +single differential cross section with respect to photon energy, which is often +expressed in the form + +.. math:: + :label: bremsstrahlung-dcs + + \frac{d\sigma_{\text{br}}}{dE} = \frac{Z^2}{\beta^2} \frac{1}{E} + \chi(Z, T, \kappa), + +where :math:`\kappa = E/T` is the reduced photon energy and :math:`\chi(Z, T, +\kappa)` is the scaled bremsstrahlung cross section, which is experimentally +measured. + +Because electrons are attracted to atomic nuclei whereas positrons are +repulsed, the cross section for positrons is smaller, though it approaches that +of electrons in the high energy limit. To obtain the positron cross section, we +multiply :eq:`bremsstrahlung-dcs` by the :math:`\kappa`-independent factor used +in Salvat_, + +.. math:: + :label: positron-factor + + \begin{aligned} + F_{\text{p}}(Z,T) = + & 1 - \text{exp}(-1.2359\times 10^{-1}t + 6.1274\times 10^{-2}t^2 - 3.1516\times 10^{-2}t^3 \\ + & + 7.7446\times 10^{-3}t^4 - 1.0595\times 10^{-3}t^5 + 7.0568\times 10^{-5}t^6 \\ + & - 1.8080\times 10^{-6}t^7), + \end{aligned} + +where + +.. math:: + :label: positron-factor-t + + t = \ln\left(1 + \frac{10^6}{Z^2}\frac{T}{\text{m}_\text{e}c^2} \right). + +:math:`F_{\text{p}}(Z,T)` is the ratio of the radiative stopping powers for +positrons and electrons. Stopping power describes the average energy loss per +unit path length of a charged particle as it passes through matter: + +.. math:: + :label: stopping-power + + -\frac{dT}{ds} = n \int E \frac{d\sigma}{dE} dE \equiv S(T), + +where :math:`n` is the number density of the material and :math:`d\sigma/dE` is +the cross section differential in energy loss. The total stopping power +:math:`S(T)` can be separated into two components: the radiative stopping +power :math:`S_{\text{rad}}(T)`, which refers to energy loss due to +bremsstrahlung, and the collision stopping power :math:`S_{\text{col}}(T)`, +which refers to the energy loss due to inelastic collisions with bound +electrons in the material that result in ionization and excitation. The +radiative stopping power for electrons is given by + +.. math:: + :label: radiative-stopping-power + + S_{\text{rad}}(T) = n \frac{Z^2}{\beta^2} T \int_0^1 \chi(Z,T,\kappa) + d\kappa. + + +To obtain the radiative stopping power for positrons, +:eq:`radiative-stopping-power` is multiplied by :eq:`positron-factor`. + +While the models for photon interactions with matter described above can safely +assume interactions occur with free atoms, sampling the target atom based on +the macroscopic cross sections, molecular effects cannot necessarily be +disregarded for charged particle treatment. For compounds and mixtures, the +bremsstrahlung cross section is calculated using Bragg's additivity rule as + +.. math:: + :label: material-bremsstrahlung-dcs + + \frac{d\sigma_{\text{br}}}{dE} = \frac{1}{\beta^2 E} \sum_i \gamma_i Z^2_i + \chi(Z_i, T, \kappa), + +where the sum is over the constituent elements and :math:`\gamma_i` is the +atomic fraction of the :math:`i`-th element. Similarly, the radiative stopping +power is calculated using Bragg's additivity rule as + +.. math:: + :label: material-radiative-stopping-power + + S_{\text{rad}}(T) = \sum_i w_i S_{\text{rad},i}(T), + +where :math:`w_i` is the mass fraction of the :math:`i`-th element and +:math:`S_{\text{rad},i}(T)` is found for element :math:`i` using +:eq:`radiative-stopping-power`. The collision stopping power, however, is a +function of certain quantities such as the mean excitation energy :math:`I` and +the density effect correction :math:`\delta_F` that depend on molecular +properties. These quantities cannot simply be summed over constituent elements +in a compound, but should instead be calculated for the material. The Bethe +formula can be used to find the collision stopping power of the material: + +.. math:: + :label: material-collision-stopping-power + + S_{\text{col}}(T) = \frac{2 \pi r_e^2 m_e c^2}{\beta^2} N_A \frac{Z}{A_M} + [\ln(T^2/I^2) + \ln(1 + \tau/2) + F(\tau) - \delta_F(T)], + +where :math:`N_A` is Avogadro's number, :math:`A_M` is the molar mass, +:math:`\tau = T/m_e`, and :math:`F(\tau)` depends on the particle type. For +electrons, + +.. math:: + :label: F-electron + + F_{-}(\tau) = (1 - \beta^2)[1 + \tau^2/8 - (2\tau + 1) \ln2], + +while for positrons + +.. math:: + :label: F-positron + + F_{+}(\tau) = 2\ln2 - (\beta^2/12)[23 + 14/(\tau + 2) + 10/(\tau + 2)^2 + + 4/(\tau + 2)^3]. + +The density effect correction :math:`\delta_F` takes into account the reduction +of the collision stopping power due to the polarization of the material the +charged particle is passing through by the electric field of the particle. +It can be evaluated using the method described by Sternheimer_, where the +equation for :math:`\delta_F` is + +.. math:: + :label: density-effect-correction + + \delta_F(\beta) = \sum_{i=1}^n f_i \ln[(l_i^2 + l^2)/l_i^2] - + l^2(1-\beta^2). + +Here, :math:`f_i` is the oscillator strength of the :math:`i`-th transition, +given by :math:`f_i = n_i/Z`, where :math:`n_i` is the number of electrons in +the :math:`i`-th subshell. The frequency :math:`l` is the solution of the +equation + +.. math:: + :label: density-effect-l + + \frac{1}{\beta^2} - 1 = \sum_{i=1}^{n} \frac{f_i}{\bar{\nu}_i^2 + l^2}, + +where :math:`\bar{v}_i` is defined as + +.. math:: + :label: density-effect-nubar + + \bar{\nu}_i = h\nu_i \rho / h\nu_p. + +The plasma energy :math:`h\nu_p` of the medium is given by + +.. math:: + :label: plasma-frequency + + h\nu_p = \sqrt{\frac{(hc)^2 r_e \rho_m N_A Z}{\pi A}}, + +where :math:`A` is the atomic weight and :math:`\rho_m` is the density of the +material. In :eq:`density-effect-nubar`, :math:`h\nu_i` is the oscillator +energy, and :math:`\rho` is an adjustment factor introduced to give agreement +between the experimental values of the oscillator energies and the mean +excitation energy. The :math:`l_i` in :eq:`density-effect-correction` are +defined as + +.. math:: + :label: density-effect-li + + \begin{aligned} + l_i &= (\bar{\nu}_i^2 + 2/3f_i)^{1/2} ~~~~&\text{for}~~ \bar{\nu}_i > 0 \\ + l_n &= f_n^{1/2} ~~~~&\text{for}~~ \bar{\nu}_n = 0, + \end{aligned} + +where the second case applies to conduction electrons. For a conductor, +:math:`f_n` is given by :math:`n_c/Z`, where :math:`n_c` is the effective +number of conduction electrons, and :math:`v_n = 0`. The adjustment factor +:math:`\rho` is determined using the equation for the mean excitation energy: + +.. math:: + :label: mean-excitation-energy + + \ln I = \sum_{i=1}^{n-1} f_i \ln[(h\nu_i\rho)^2 + 2/3f_i(h\nu_p)^2]^{1/2} + + f_n \ln (h\nu_pf_n^{1/2}). + +.. _ttb: + + +Thick-Target Bremsstrahlung Approximation ++++++++++++++++++++++++++++++++++++++++++ + +Since charged particles lose their energy on a much shorter distance scale than +neutral particles, not much error should be introduced by neglecting to +transport electrons. However, the bremsstrahlung emitted from high energy +electrons and positrons can travel far from the interaction site. Thus, even +without a full electron transport mode it is necessary to model bremsstrahlung. +We use a thick-target bremsstrahlung (TTB) approximation based on the models in +Salvat_ and Kaltiaisenaho_ for generating bremsstrahlung photons, which assumes +the charged particle loses all its energy in a single homogeneous material +region. + +To model bremsstrahlung using the TTB approximation, we need to know the number +of photons emitted by the charged particle and the energy distribution of the +photons. These quantities can be calculated using the continuous slowing down +approximation (CSDA). The CSDA assumes charged particles lose energy +continuously along their trajectory with a rate of energy loss equal to the +total stopping power, ignoring fluctuations in the energy loss. The +approximation is useful for expressing average quantities that describe how +charged particles slow down in matter. For example, the CSDA range approximates +the average path length a charged particle travels as it slows to rest: + +.. math:: + :label: csda-range + + R(T) = \int^T_0 \frac{dT'}{S(T')}. + +Actual path lengths will fluctuate around :math:`R(T)`. The average number of +photons emitted per unit path length is given by the inverse bremsstrahlung +mean free path: + +.. math:: + :label: inverse-bremsstrahlung-mfp + + \lambda_{\text{br}}^{-1}(T,E_{\text{cut}}) + = n\int_{E_{\text{cut}}}^T\frac{d\sigma_{\text{br}}}{dE}dE + = n\frac{Z^2}{\beta^2}\int_{\kappa_{\text{cut}}}^1\frac{1}{\kappa} + \chi(Z,T,\kappa)d\kappa. + +The lower limit of the integral in :eq:`inverse-bremsstrahlung-mfp` is non-zero +because the bremsstrahlung differential cross section diverges for small photon +energies but is finite for photon energies above some cutoff energy +:math:`E_{\text{cut}}`. The mean free path +:math:`\lambda_{\text{br}}^{-1}(T,E_{\text{cut}})` is used to calculate the +photon number yield, defined as the average number of photons emitted with +energy greater than :math:`E_{\text{cut}}` as the charged particle slows down +from energy :math:`T` to :math:`E_{\text{cut}}`. The photon number yield is +given by + +.. math:: + :label: photon-number-yield + + Y(T,E_{\text{cut}}) = \int^{R(T)}_{R(E_{\text{cut}})} + \lambda_{\text{br}}^{-1}(T',E_{\text{cut}})ds = \int_{E_{\text{cut}}}^T + \frac{\lambda_{\text{br}}^{-1}(T',E_{\text{cut}})}{S(T')}dT'. + +:math:`Y(T,E_{\text{cut}})` can be used to construct the energy spectrum of +bremsstrahlung photons: the number of photons created with energy between +:math:`E_1` and :math:`E_2` by a charged particle with initial kinetic energy +:math:`T` as it comes to rest is given by :math:`Y(T,E_1) - Y(T,E_2)`. + +To simulate the emission of bremsstrahlung photons, the total stopping power +and bremsstrahlung differential cross section for positrons and electrons must +be calculated for a given material using :eq:`material-bremsstrahlung-dcs` and +:eq:`material-radiative-stopping-power`. These quantities are used to build the +tabulated bremsstrahlung energy PDF and CDF for that material for each incident +energy :math:`T_k` on the energy grid. The following algorithm is then applied +to sample the photon energies: + +1. For an incident charged particle with energy :math:`T`, sample the number of + emitted photons as + + .. math:: + + N = \lfloor Y(T,E_{\text{cut}}) + \xi_1 \rfloor. + +2. Rather than interpolate the PDF between indices :math:`k` and :math:`k+1` + for which :math:`T_k < T < T_{k+1}`, which is computationally expensive, use + the composition method and sample from the PDF at either :math:`k` or + :math:`k+1`. Using linear interpolation on a logarithmic scale, the PDF can + be expressed as + + .. math:: + + p_{\text{br}}(T,E) = \pi_k p_{\text{br}}(T_k,E) + \pi_{k+1} + p_{\text{br}}(T_{k+1},E), + + where the interpolation weights are + + .. math:: + + \pi_k = \frac{\ln T_{k+1} - \ln T}{\ln T_{k+1} - \ln T_k},~~~ + \pi_{k+1} = \frac{\ln T - \ln T_k}{\ln T_{k+1} - \ln T_k}. + + Sample either the index :math:`i = k` or :math:`i = k+1` according to the + point probabilities :math:`\pi_{k}` and :math:`\pi_{k+1}`. + +3. Determine the maximum value of the CDF :math:`P_{\text{br,max}}`. + +3. Sample the photon energies using the inverse transform method with the + tabulated CDF :math:`P_{\text{br}}(T_i, E)` i.e., + + .. math:: + + E = E_j \left[ (1 + a_j) \frac{\xi_2 P_{\text{br,max}} - + P_{\text{br}}(T_i, E_j)} {E_j p_{\text{br}}(T_i, E_j)} + 1 + \right]^{\frac{1}{1 + a_j}} + + where the interpolation factor :math:`a_j` is given by + + .. math:: + + a_j = \frac{\ln p_{\text{br}}(T_i,E_{j+1}) - \ln p_{\text{br}}(T_i,E_j)} + {\ln E_{j+1} - \ln E_j} + + and :math:`P_{\text{br}}(T_i, E_j) \le \xi_2 P_{\text{br,max}} \le + P_{\text{br}}(T_i, E_{j+1})`. + +We ignore the range of the electron or positron, i.e., the bremsstrahlung +photons are produced in the same location that the charged particle was +created. The direction of the photons is assumed to be the same as the +direction of the incident charged particle, which is a reasonable approximation +at higher energies when the bremsstrahlung radiation is emitted at small +angles. + + +Electron-Positron Annihilation +------------------------------ + +When a positron collides with an electron, both particles are annihilated and +generally two photons with equal energy are created. If the kinetic energy of +the positron is high enough, the two photons can have different energies, and +the higher-energy photon is emitted preferentially in the direction of flight +of the positron. It is also possible to produce a single photon if the +interaction occurs with a bound electron, and in some cases three (or, rarely, +even more) photons can be emitted. However, the annihilation cross section is +largest for low-energy positrons, and as the positron energy decreases, the +angular distribution of the emitted photons becomes isotropic. + +In OpenMC, we assume the most likely case in which a low-energy positron (which +has already lost most of its energy to bremsstrahlung radiation) interacts with +an electron which is free and at rest. Two photons with energy equal to the +electron rest mass energy :math:`m_e c^2 = 0.511` MeV are emitted isotropically +in opposite directions. + + +.. _Kaltiaisenaho: https://aaltodoc.aalto.fi/bitstream/handle/123456789/21004/master_Kaltiaisenaho_Toni_2016.pdf + +.. _Salvat: https://doi.org/10.1787/32da5043-en + +.. _Sternheimer: https://doi.org/10.1103/PhysRevB.26.6067 diff --git a/docs/source/methods/index.rst b/docs/source/methods/index.rst index 75c421c87..121d04b1d 100644 --- a/docs/source/methods/index.rst +++ b/docs/source/methods/index.rst @@ -14,6 +14,7 @@ Theory and Methodology random_numbers neutron_physics photon_physics + charged_particles_physics tallies eigenvalue depletion @@ -21,4 +22,4 @@ Theory and Methodology parallelization cmfd variance_reduction - random_ray \ No newline at end of file + random_ray diff --git a/docs/source/methods/photon_physics.rst b/docs/source/methods/photon_physics.rst index 22d2c7f26..d2bd3ac76 100644 --- a/docs/source/methods/photon_physics.rst +++ b/docs/source/methods/photon_physics.rst @@ -667,342 +667,6 @@ and Auger electrons: 5. Repeat from step 1 for vacancy left by the transition electron. -Electron-Positron Annihilation ------------------------------- - -When a positron collides with an electron, both particles are annihilated and -generally two photons with equal energy are created. If the kinetic energy of -the positron is high enough, the two photons can have different energies, and -the higher-energy photon is emitted preferentially in the direction of flight -of the positron. It is also possible to produce a single photon if the -interaction occurs with a bound electron, and in some cases three (or, rarely, -even more) photons can be emitted. However, the annihilation cross section is -largest for low-energy positrons, and as the positron energy decreases, the -angular distribution of the emitted photons becomes isotropic. - -In OpenMC, we assume the most likely case in which a low-energy positron (which -has already lost most of its energy to bremsstrahlung radiation) interacts with -an electron which is free and at rest. Two photons with energy equal to the -electron rest mass energy :math:`m_e c^2 = 0.511` MeV are emitted isotropically -in opposite directions. - -Bremsstrahlung --------------- - -When a charged particle is decelerated in the field of an atom, some of its -kinetic energy is converted into electromagnetic radiation known as -bremsstrahlung, or 'braking radiation'. In each event, an electron or positron -with kinetic energy :math:`T` generates a photon with an energy :math:`E` -between :math:`0` and :math:`T`. Bremsstrahlung is described by a cross section -that is differential in photon energy, in the direction of the emitted photon, -and in the final direction of the charged particle. However, in Monte Carlo -simulations it is typical to integrate over the angular variables to obtain a -single differential cross section with respect to photon energy, which is often -expressed in the form - -.. math:: - :label: bremsstrahlung-dcs - - \frac{d\sigma_{\text{br}}}{dE} = \frac{Z^2}{\beta^2} \frac{1}{E} - \chi(Z, T, \kappa), - -where :math:`\kappa = E/T` is the reduced photon energy and :math:`\chi(Z, T, -\kappa)` is the scaled bremsstrahlung cross section, which is experimentally -measured. - -Because electrons are attracted to atomic nuclei whereas positrons are -repulsed, the cross section for positrons is smaller, though it approaches that -of electrons in the high energy limit. To obtain the positron cross section, we -multiply :eq:`bremsstrahlung-dcs` by the :math:`\kappa`-independent factor used -in Salvat_, - -.. math:: - :label: positron-factor - - \begin{aligned} - F_{\text{p}}(Z,T) = - & 1 - \text{exp}(-1.2359\times 10^{-1}t + 6.1274\times 10^{-2}t^2 - 3.1516\times 10^{-2}t^3 \\ - & + 7.7446\times 10^{-3}t^4 - 1.0595\times 10^{-3}t^5 + 7.0568\times 10^{-5}t^6 \\ - & - 1.8080\times 10^{-6}t^7), - \end{aligned} - -where - -.. math:: - :label: positron-factor-t - - t = \ln\left(1 + \frac{10^6}{Z^2}\frac{T}{\text{m}_\text{e}c^2} \right). - -:math:`F_{\text{p}}(Z,T)` is the ratio of the radiative stopping powers for -positrons and electrons. Stopping power describes the average energy loss per -unit path length of a charged particle as it passes through matter: - -.. math:: - :label: stopping-power - - -\frac{dT}{ds} = n \int E \frac{d\sigma}{dE} dE \equiv S(T), - -where :math:`n` is the number density of the material and :math:`d\sigma/dE` is -the cross section differential in energy loss. The total stopping power -:math:`S(T)` can be separated into two components: the radiative stopping -power :math:`S_{\text{rad}}(T)`, which refers to energy loss due to -bremsstrahlung, and the collision stopping power :math:`S_{\text{col}}(T)`, -which refers to the energy loss due to inelastic collisions with bound -electrons in the material that result in ionization and excitation. The -radiative stopping power for electrons is given by - -.. math:: - :label: radiative-stopping-power - - S_{\text{rad}}(T) = n \frac{Z^2}{\beta^2} T \int_0^1 \chi(Z,T,\kappa) - d\kappa. - - -To obtain the radiative stopping power for positrons, -:eq:`radiative-stopping-power` is multiplied by :eq:`positron-factor`. - -While the models for photon interactions with matter described above can safely -assume interactions occur with free atoms, sampling the target atom based on -the macroscopic cross sections, molecular effects cannot necessarily be -disregarded for charged particle treatment. For compounds and mixtures, the -bremsstrahlung cross section is calculated using Bragg's additivity rule as - -.. math:: - :label: material-bremsstrahlung-dcs - - \frac{d\sigma_{\text{br}}}{dE} = \frac{1}{\beta^2 E} \sum_i \gamma_i Z^2_i - \chi(Z_i, T, \kappa), - -where the sum is over the constituent elements and :math:`\gamma_i` is the -atomic fraction of the :math:`i`-th element. Similarly, the radiative stopping -power is calculated using Bragg's additivity rule as - -.. math:: - :label: material-radiative-stopping-power - - S_{\text{rad}}(T) = \sum_i w_i S_{\text{rad},i}(T), - -where :math:`w_i` is the mass fraction of the :math:`i`-th element and -:math:`S_{\text{rad},i}(T)` is found for element :math:`i` using -:eq:`radiative-stopping-power`. The collision stopping power, however, is a -function of certain quantities such as the mean excitation energy :math:`I` and -the density effect correction :math:`\delta_F` that depend on molecular -properties. These quantities cannot simply be summed over constituent elements -in a compound, but should instead be calculated for the material. The Bethe -formula can be used to find the collision stopping power of the material: - -.. math:: - :label: material-collision-stopping-power - - S_{\text{col}}(T) = \frac{2 \pi r_e^2 m_e c^2}{\beta^2} N_A \frac{Z}{A_M} - [\ln(T^2/I^2) + \ln(1 + \tau/2) + F(\tau) - \delta_F(T)], - -where :math:`N_A` is Avogadro's number, :math:`A_M` is the molar mass, -:math:`\tau = T/m_e`, and :math:`F(\tau)` depends on the particle type. For -electrons, - -.. math:: - :label: F-electron - - F_{-}(\tau) = (1 - \beta^2)[1 + \tau^2/8 - (2\tau + 1) \ln2], - -while for positrons - -.. math:: - :label: F-positron - - F_{+}(\tau) = 2\ln2 - (\beta^2/12)[23 + 14/(\tau + 2) + 10/(\tau + 2)^2 + - 4/(\tau + 2)^3]. - -The density effect correction :math:`\delta_F` takes into account the reduction -of the collision stopping power due to the polarization of the material the -charged particle is passing through by the electric field of the particle. -It can be evaluated using the method described by Sternheimer_, where the -equation for :math:`\delta_F` is - -.. math:: - :label: density-effect-correction - - \delta_F(\beta) = \sum_{i=1}^n f_i \ln[(l_i^2 + l^2)/l_i^2] - - l^2(1-\beta^2). - -Here, :math:`f_i` is the oscillator strength of the :math:`i`-th transition, -given by :math:`f_i = n_i/Z`, where :math:`n_i` is the number of electrons in -the :math:`i`-th subshell. The frequency :math:`l` is the solution of the -equation - -.. math:: - :label: density-effect-l - - \frac{1}{\beta^2} - 1 = \sum_{i=1}^{n} \frac{f_i}{\bar{\nu}_i^2 + l^2}, - -where :math:`\bar{v}_i` is defined as - -.. math:: - :label: density-effect-nubar - - \bar{\nu}_i = h\nu_i \rho / h\nu_p. - -The plasma energy :math:`h\nu_p` of the medium is given by - -.. math:: - :label: plasma-frequency - - h\nu_p = \sqrt{\frac{(hc)^2 r_e \rho_m N_A Z}{\pi A}}, - -where :math:`A` is the atomic weight and :math:`\rho_m` is the density of the -material. In :eq:`density-effect-nubar`, :math:`h\nu_i` is the oscillator -energy, and :math:`\rho` is an adjustment factor introduced to give agreement -between the experimental values of the oscillator energies and the mean -excitation energy. The :math:`l_i` in :eq:`density-effect-correction` are -defined as - -.. math:: - :label: density-effect-li - - \begin{aligned} - l_i &= (\bar{\nu}_i^2 + 2/3f_i)^{1/2} ~~~~&\text{for}~~ \bar{\nu}_i > 0 \\ - l_n &= f_n^{1/2} ~~~~&\text{for}~~ \bar{\nu}_n = 0, - \end{aligned} - -where the second case applies to conduction electrons. For a conductor, -:math:`f_n` is given by :math:`n_c/Z`, where :math:`n_c` is the effective -number of conduction electrons, and :math:`v_n = 0`. The adjustment factor -:math:`\rho` is determined using the equation for the mean excitation energy: - -.. math:: - :label: mean-excitation-energy - - \ln I = \sum_{i=1}^{n-1} f_i \ln[(h\nu_i\rho)^2 + 2/3f_i(h\nu_p)^2]^{1/2} + - f_n \ln (h\nu_pf_n^{1/2}). - -.. _ttb: - -Thick-Target Bremsstrahlung Approximation -+++++++++++++++++++++++++++++++++++++++++ - -Since charged particles lose their energy on a much shorter distance scale than -neutral particles, not much error should be introduced by neglecting to -transport electrons. However, the bremsstrahlung emitted from high energy -electrons and positrons can travel far from the interaction site. Thus, even -without a full electron transport mode it is necessary to model bremsstrahlung. -We use a thick-target bremsstrahlung (TTB) approximation based on the models in -Salvat_ and Kaltiaisenaho_ for generating bremsstrahlung photons, which assumes -the charged particle loses all its energy in a single homogeneous material -region. - -To model bremsstrahlung using the TTB approximation, we need to know the number -of photons emitted by the charged particle and the energy distribution of the -photons. These quantities can be calculated using the continuous slowing down -approximation (CSDA). The CSDA assumes charged particles lose energy -continuously along their trajectory with a rate of energy loss equal to the -total stopping power, ignoring fluctuations in the energy loss. The -approximation is useful for expressing average quantities that describe how -charged particles slow down in matter. For example, the CSDA range approximates -the average path length a charged particle travels as it slows to rest: - -.. math:: - :label: csda-range - - R(T) = \int^T_0 \frac{dT'}{S(T')}. - -Actual path lengths will fluctuate around :math:`R(T)`. The average number of -photons emitted per unit path length is given by the inverse bremsstrahlung -mean free path: - -.. math:: - :label: inverse-bremsstrahlung-mfp - - \lambda_{\text{br}}^{-1}(T,E_{\text{cut}}) - = n\int_{E_{\text{cut}}}^T\frac{d\sigma_{\text{br}}}{dE}dE - = n\frac{Z^2}{\beta^2}\int_{\kappa_{\text{cut}}}^1\frac{1}{\kappa} - \chi(Z,T,\kappa)d\kappa. - -The lower limit of the integral in :eq:`inverse-bremsstrahlung-mfp` is non-zero -because the bremsstrahlung differential cross section diverges for small photon -energies but is finite for photon energies above some cutoff energy -:math:`E_{\text{cut}}`. The mean free path -:math:`\lambda_{\text{br}}^{-1}(T,E_{\text{cut}})` is used to calculate the -photon number yield, defined as the average number of photons emitted with -energy greater than :math:`E_{\text{cut}}` as the charged particle slows down -from energy :math:`T` to :math:`E_{\text{cut}}`. The photon number yield is -given by - -.. math:: - :label: photon-number-yield - - Y(T,E_{\text{cut}}) = \int^{R(T)}_{R(E_{\text{cut}})} - \lambda_{\text{br}}^{-1}(T',E_{\text{cut}})ds = \int_{E_{\text{cut}}}^T - \frac{\lambda_{\text{br}}^{-1}(T',E_{\text{cut}})}{S(T')}dT'. - -:math:`Y(T,E_{\text{cut}})` can be used to construct the energy spectrum of -bremsstrahlung photons: the number of photons created with energy between -:math:`E_1` and :math:`E_2` by a charged particle with initial kinetic energy -:math:`T` as it comes to rest is given by :math:`Y(T,E_1) - Y(T,E_2)`. - -To simulate the emission of bremsstrahlung photons, the total stopping power -and bremsstrahlung differential cross section for positrons and electrons must -be calculated for a given material using :eq:`material-bremsstrahlung-dcs` and -:eq:`material-radiative-stopping-power`. These quantities are used to build the -tabulated bremsstrahlung energy PDF and CDF for that material for each incident -energy :math:`T_k` on the energy grid. The following algorithm is then applied -to sample the photon energies: - -1. For an incident charged particle with energy :math:`T`, sample the number of - emitted photons as - - .. math:: - - N = \lfloor Y(T,E_{\text{cut}}) + \xi_1 \rfloor. - -2. Rather than interpolate the PDF between indices :math:`k` and :math:`k+1` - for which :math:`T_k < T < T_{k+1}`, which is computationally expensive, use - the composition method and sample from the PDF at either :math:`k` or - :math:`k+1`. Using linear interpolation on a logarithmic scale, the PDF can - be expressed as - - .. math:: - - p_{\text{br}}(T,E) = \pi_k p_{\text{br}}(T_k,E) + \pi_{k+1} - p_{\text{br}}(T_{k+1},E), - - where the interpolation weights are - - .. math:: - - \pi_k = \frac{\ln T_{k+1} - \ln T}{\ln T_{k+1} - \ln T_k},~~~ - \pi_{k+1} = \frac{\ln T - \ln T_k}{\ln T_{k+1} - \ln T_k}. - - Sample either the index :math:`i = k` or :math:`i = k+1` according to the - point probabilities :math:`\pi_{k}` and :math:`\pi_{k+1}`. - -3. Determine the maximum value of the CDF :math:`P_{\text{br,max}}`. - -3. Sample the photon energies using the inverse transform method with the - tabulated CDF :math:`P_{\text{br}}(T_i, E)` i.e., - - .. math:: - - E = E_j \left[ (1 + a_j) \frac{\xi_2 P_{\text{br,max}} - - P_{\text{br}}(T_i, E_j)} {E_j p_{\text{br}}(T_i, E_j)} + 1 - \right]^{\frac{1}{1 + a_j}} - - where the interpolation factor :math:`a_j` is given by - - .. math:: - - a_j = \frac{\ln p_{\text{br}}(T_i,E_{j+1}) - \ln p_{\text{br}}(T_i,E_j)} - {\ln E_{j+1} - \ln E_j} - - and :math:`P_{\text{br}}(T_i, E_j) \le \xi_2 P_{\text{br,max}} \le - P_{\text{br}}(T_i, E_{j+1})`. - -We ignore the range of the electron or positron, i.e., the bremsstrahlung -photons are produced in the same location that the charged particle was -created. The direction of the photons is assumed to be the same as the -direction of the incident charged particle, which is a reasonable approximation -at higher energies when the bremsstrahlung radiation is emitted at small -angles. .. _photon_production: @@ -1070,5 +734,3 @@ emitted photon. .. _Kaltiaisenaho: https://aaltodoc.aalto.fi/bitstream/handle/123456789/21004/master_Kaltiaisenaho_Toni_2016.pdf .. _Salvat: https://doi.org/10.1787/32da5043-en - -.. _Sternheimer: https://doi.org/10.1103/PhysRevB.26.6067 diff --git a/include/openmc/nuclide.h b/include/openmc/nuclide.h index 329c776d0..60b88a153 100644 --- a/include/openmc/nuclide.h +++ b/include/openmc/nuclide.h @@ -164,8 +164,8 @@ namespace data { // Minimum/maximum transport energy for each particle type. Order corresponds to // that of the ParticleType enum -extern array energy_min; -extern array energy_max; +extern array energy_min; +extern array energy_max; //! Minimum temperature in [K] that nuclide data is available at extern double temperature_min; diff --git a/openmc/source.py b/openmc/source.py index c463ccb27..9b730cf1d 100644 --- a/openmc/source.py +++ b/openmc/source.py @@ -260,7 +260,7 @@ class IndependentSource(SourceBase): time distribution of source sites strength : float Strength of the source - particle : {'neutron', 'photon'} + particle : {'neutron', 'photon', 'electron', 'positron'} Source particle type domains : iterable of openmc.Cell, openmc.Material, or openmc.Universe Domains to reject based on, i.e., if a sampled spatial location is not @@ -299,7 +299,7 @@ class IndependentSource(SourceBase): .. versionadded:: 0.14.0 - particle : {'neutron', 'photon'} + particle : {'neutron', 'photon', 'electron', 'positron'} Source particle type constraints : dict Constraints on sampled source particles. Valid keys include @@ -404,7 +404,8 @@ class IndependentSource(SourceBase): @particle.setter def particle(self, particle): - cv.check_value('source particle', particle, ['neutron', 'photon']) + cv.check_value('source particle', particle, + ['neutron', 'photon', 'electron', 'positron']) self._particle = particle def populate_xml_element(self, element): diff --git a/src/finalize.cpp b/src/finalize.cpp index 76babbb96..659f390b3 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -156,8 +156,8 @@ int openmc_finalize() simulation::entropy_mesh = nullptr; simulation::ufs_mesh = nullptr; - data::energy_max = {INFTY, INFTY}; - data::energy_min = {0.0, 0.0}; + data::energy_max = {INFTY, INFTY, INFTY, INFTY}; + data::energy_min = {0.0, 0.0, 0.0, 0.0}; data::temperature_min = 0.0; data::temperature_max = INFTY; model::root_universe = -1; diff --git a/src/nuclide.cpp b/src/nuclide.cpp index 7cb84640d..5ae6e30ee 100644 --- a/src/nuclide.cpp +++ b/src/nuclide.cpp @@ -31,8 +31,8 @@ namespace openmc { //============================================================================== namespace data { -array energy_min {0.0, 0.0}; -array energy_max {INFTY, INFTY}; +array energy_min {0.0, 0.0, 0.0, 0.0}; +array energy_max {INFTY, INFTY, INFTY, INFTY}; double temperature_min {INFTY}; double temperature_max {0.0}; std::unordered_map nuclide_map; diff --git a/src/particle.cpp b/src/particle.cpp index 06ccf462a..6b4c332a8 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -232,7 +232,7 @@ void Particle::event_advance() // Sample a distance to collision if (type() == ParticleType::electron || type() == ParticleType::positron) { - collision_distance() = 0.0; + collision_distance() = material() == MATERIAL_VOID ? INFINITY : 0.0; } else if (macro_xs().total == 0.0) { collision_distance() = INFINITY; } else { diff --git a/src/simulation.cpp b/src/simulation.cpp index b9986f473..f55546c4c 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -674,8 +674,9 @@ void calculate_work() void initialize_data() { // Determine minimum/maximum energy for incident neutron/photon data - data::energy_max = {INFTY, INFTY}; - data::energy_min = {0.0, 0.0}; + data::energy_max = {INFTY, INFTY, INFTY, INFTY}; + data::energy_min = {0.0, 0.0, 0.0, 0.0}; + for (const auto& nuc : data::nuclides) { if (nuc->grid_.size() >= 1) { int neutron = static_cast(ParticleType::neutron); @@ -703,11 +704,21 @@ void initialize_data() // than the current minimum/maximum if (data::ttb_e_grid.size() >= 1) { int photon = static_cast(ParticleType::photon); + int electron = static_cast(ParticleType::electron); + int positron = static_cast(ParticleType::positron); int n_e = data::ttb_e_grid.size(); + + const std::vector charged = {electron, positron}; + for (auto t : charged) { + data::energy_min[t] = std::exp(data::ttb_e_grid(1)); + data::energy_max[t] = std::exp(data::ttb_e_grid(n_e - 1)); + } + data::energy_min[photon] = - std::max(data::energy_min[photon], std::exp(data::ttb_e_grid(1))); - data::energy_max[photon] = std::min( - data::energy_max[photon], std::exp(data::ttb_e_grid(n_e - 1))); + std::max(data::energy_min[photon], data::energy_min[electron]); + + data::energy_max[photon] = + std::min(data::energy_max[photon], data::energy_max[electron]); } } } diff --git a/src/source.cpp b/src/source.cpp index 12323f7bd..f6aa665eb 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -290,6 +290,12 @@ IndependentSource::IndependentSource(pugi::xml_node node) : Source(node) } else if (temp_str == "photon") { particle_ = ParticleType::photon; settings::photon_transport = true; + } else if (temp_str == "electron") { + particle_ = ParticleType::electron; + settings::photon_transport = true; + } else if (temp_str == "positron") { + particle_ = ParticleType::positron; + settings::photon_transport = true; } else { fatal_error(std::string("Unknown source particle type: ") + temp_str); } diff --git a/tests/regression_tests/electron_heating/__init__.py b/tests/regression_tests/electron_heating/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/electron_heating/inputs_true.dat b/tests/regression_tests/electron_heating/inputs_true.dat new file mode 100644 index 000000000..ec8e5a837 --- /dev/null +++ b/tests/regression_tests/electron_heating/inputs_true.dat @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + fixed source + 10000 + 1 + + + 10000000.0 1.0 + + + + 1000.0 + + + + + heating + + + diff --git a/tests/regression_tests/electron_heating/results_true.dat b/tests/regression_tests/electron_heating/results_true.dat new file mode 100644 index 000000000..4f54ceaa4 --- /dev/null +++ b/tests/regression_tests/electron_heating/results_true.dat @@ -0,0 +1,3 @@ +tally 1: +1.000000E+07 +1.000000E+14 diff --git a/tests/regression_tests/electron_heating/test.py b/tests/regression_tests/electron_heating/test.py new file mode 100644 index 000000000..e7a58560c --- /dev/null +++ b/tests/regression_tests/electron_heating/test.py @@ -0,0 +1,40 @@ +import pytest +import openmc + +from tests.testing_harness import PyAPITestHarness + + +@pytest.fixture +def water_model(): + # Define materals and geometry + water = openmc.Material() + water.add_nuclide("H1", 2.0) + water.add_nuclide("O16", 1.0) + water.set_density("g/cc", 1.0) + sphere = openmc.Sphere(r=1.0, boundary_type="reflective") + sph = openmc.Cell(fill=water, region=-sphere) + geometry = openmc.Geometry([sph]) + source = openmc.IndependentSource( + energy=openmc.stats.delta_function(10.0e6), + particle="electron" + ) + + # Define settings + settings = openmc.Settings() + settings.particles = 10000 + settings.batches = 1 + settings.cutoff = {"energy_photon": 1000.0} + settings.run_mode = "fixed source" + settings.source = source + + # Define tallies + tally = openmc.Tally() + tally.scores = ["heating"] + tallies = openmc.Tallies([tally]) + + return openmc.Model(geometry=geometry, settings=settings, tallies=tallies) + + +def test_electron_heating_calc(water_model): + harness = PyAPITestHarness("statepoint.1.h5", water_model) + harness.main() From 055ea15a2ddf40f6fff4eef52990e79b37652f64 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 17 Oct 2025 18:09:37 -0500 Subject: [PATCH 431/671] Clip mixture distributions based on mean times integral (#3603) --- openmc/stats/univariate.py | 76 ++++++++++++++++++++++++++++++++------ 1 file changed, 64 insertions(+), 12 deletions(-) diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index d6cf19f2b..c48cc0075 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -295,6 +295,20 @@ class Discrete(Univariate): """ return np.sum(self.p) + def mean(self) -> float: + """Return mean of the discrete distribution + + The mean is the weighted average of the discrete values. + + .. versionadded:: 0.15.3 + + Returns + ------- + float + Mean of discrete distribution + """ + return np.sum(self.x * self.p) / np.sum(self.p) + def clip(self, tolerance: float = 1e-6, inplace: bool = False) -> Discrete: r"""Remove low-importance points from discrete distribution. @@ -413,6 +427,18 @@ class Uniform(Univariate): rng = np.random.RandomState(seed) return rng.uniform(self.a, self.b, n_samples) + def mean(self) -> float: + """Return mean of the uniform distribution + + .. versionadded:: 0.15.3 + + Returns + ------- + float + Mean of uniform distribution + """ + return 0.5 * (self.a + self.b) + def to_xml_element(self, element_name: str): """Return XML representation of the uniform distribution @@ -1123,7 +1149,7 @@ class Tabular(Univariate): """ interpolation = get_text(elem, 'interpolation') - params = get_elem_list(elem, "parameters", float) + params = get_elem_list(elem, "parameters", float) m = (len(params) + 1)//2 # +1 for when len(params) is odd x = params[:m] p = params[m:] @@ -1347,6 +1373,30 @@ class Mixture(Univariate): for p, dist in zip(self.probability, self.distribution) ]) + def mean(self) -> float: + """Return mean of the mixture distribution + + The mean is the weighted average of the means of the component + distributions, weighted by probability * integral. + + .. versionadded:: 0.15.3 + + Returns + ------- + float + Mean of the mixture distribution + """ + # Weight each component by its probability and integral + weights = [p*dist.integral() for p, dist in + zip(self.probability, self.distribution)] + total_weight = sum(weights) + + if total_weight == 0: + return 0.0 + + return sum([w*dist.mean() for w, dist in + zip(weights, self.distribution)]) / total_weight + def clip(self, tolerance: float = 1e-6, inplace: bool = False) -> Mixture: r"""Remove low-importance points / distributions @@ -1369,14 +1419,14 @@ class Mixture(Univariate): Distribution with low-importance points / distributions removed """ - # Determine integral of original distribution to compare later - original_integral = self.integral() + # Calculate mean * integral for original distribution to compare later. + original_mean_integral = self.mean() * self.integral() # Determine indices for any distributions that contribute non-negligibly - # to overall intensity - intensities = [prob*dist.integral() for prob, dist in - zip(self.probability, self.distribution)] - indices = _intensity_clip(intensities, tolerance=tolerance) + # to overall mean * integral + mean_integrals = [prob*dist.mean()*dist.integral() for prob, dist in + zip(self.probability, self.distribution)] + indices = _intensity_clip(mean_integrals, tolerance=tolerance) # Clip mixture of distributions probability = self.probability[indices] @@ -1397,12 +1447,14 @@ class Mixture(Univariate): # Create new distribution new_dist = type(self)(probability, distribution) - # Show warning if integral of new distribution is not within - # tolerance of original - diff = (original_integral - new_dist.integral())/original_integral + # Show warning if mean * integral of new distribution is not within + # tolerance of original. For energy distributions, mean * integral + # represents total energy. + new_mean_integral = new_dist.mean() * new_dist.integral() + diff = (original_mean_integral - new_mean_integral)/original_mean_integral if diff > tolerance: - warn("Clipping mixture distribution resulted in an integral that is " - f"lower by a fraction of {diff} when tolerance={tolerance}.") + warn("Clipping mixture distribution resulted in a mean*integral " + f"that is lower by a fraction of {diff} when tolerance={tolerance}.") return new_dist From 3ac5d6f8f540a0bfbe53b8264ae1d691093853ec Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Mon, 20 Oct 2025 18:45:14 +0200 Subject: [PATCH 432/671] Allow Path objects in MGXSLibrary.export_to_hdf5 (#3608) Co-authored-by: Paul Romano --- openmc/mgxs_library.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openmc/mgxs_library.py b/openmc/mgxs_library.py index b840563cf..ce86f97df 100644 --- a/openmc/mgxs_library.py +++ b/openmc/mgxs_library.py @@ -11,7 +11,7 @@ import openmc import openmc.mgxs from openmc.mgxs import SCATTER_TABULAR, SCATTER_LEGENDRE, SCATTER_HISTOGRAM from .checkvalue import check_type, check_value, check_greater_than, \ - check_iterable_type, check_less_than, check_filetype_version + check_iterable_type, check_less_than, check_filetype_version, PathLike ROOM_TEMPERATURE_KELVIN = 294.0 @@ -2506,7 +2506,7 @@ class MGXSLibrary: Parameters ---------- - filename : str + filename : str or PathLike Filename of file, default is mgxs.h5. libver : {'earliest', 'latest'} Compatibility mode for the HDF5 file. 'latest' will produce files @@ -2514,7 +2514,7 @@ class MGXSLibrary: """ - check_type('filename', filename, str) + check_type('filename', filename, PathLike) # Create and write to the HDF5 file file = h5py.File(filename, "w", libver=libver) From c31032cf25f5a8c839c20fdb1017cbbf6de88278 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Wed, 22 Oct 2025 02:03:27 +0300 Subject: [PATCH 433/671] load mesh objects from weight_windows.h5 file (#3598) Co-authored-by: Patrick Shriwise --- include/openmc/mesh.h | 22 ++ src/mesh.cpp | 369 ++++++++++++++++++++++------ src/weight_windows.cpp | 4 + tests/cpp_unit_tests/CMakeLists.txt | 1 + tests/cpp_unit_tests/test_mesh.cpp | 257 +++++++++++++++++++ 5 files changed, 579 insertions(+), 74 deletions(-) create mode 100644 tests/cpp_unit_tests/test_mesh.cpp diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index a56705c9e..5c9272e93 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -132,8 +132,14 @@ public: // Constructors and destructor Mesh() = default; Mesh(pugi::xml_node node); + Mesh(hid_t group); virtual ~Mesh() = default; + // Factory method for creating meshes from either an XML node or HDF5 group + template + static const std::unique_ptr& create( + T dataset, const std::string& mesh_type, const std::string& mesh_library); + // Methods //! Perform any preparation needed to support point location within the mesh virtual void prepare_for_point_location() {}; @@ -258,6 +264,7 @@ class StructuredMesh : public Mesh { public: StructuredMesh() = default; StructuredMesh(pugi::xml_node node) : Mesh {node} {}; + StructuredMesh(hid_t group) : Mesh {group} {}; virtual ~StructuredMesh() = default; using MeshIndex = std::array; @@ -423,6 +430,7 @@ class PeriodicStructuredMesh : public StructuredMesh { public: PeriodicStructuredMesh() = default; PeriodicStructuredMesh(pugi::xml_node node) : StructuredMesh {node} {}; + PeriodicStructuredMesh(hid_t group) : StructuredMesh {group} {}; Position local_coords(const Position& r) const override { @@ -442,6 +450,7 @@ public: // Constructors RegularMesh() = default; RegularMesh(pugi::xml_node node); + RegularMesh(hid_t group); // Overridden methods int get_index_in_direction(double r, int i) const override; @@ -481,6 +490,8 @@ public: //! Return the volume for a given mesh index double volume(const MeshIndex& ijk) const override; + int set_grid(); + // Data members double volume_frac_; //!< Volume fraction of each mesh element double element_volume_; //!< Volume of each mesh element @@ -492,6 +503,7 @@ public: // Constructors RectilinearMesh() = default; RectilinearMesh(pugi::xml_node node); + RectilinearMesh(hid_t group); // Overridden methods int get_index_in_direction(double r, int i) const override; @@ -534,6 +546,7 @@ public: // Constructors CylindricalMesh() = default; CylindricalMesh(pugi::xml_node node); + CylindricalMesh(hid_t group); // Overridden methods virtual MeshIndex get_indices(Position r, bool& in_mesh) const override; @@ -598,6 +611,7 @@ public: // Constructors SphericalMesh() = default; SphericalMesh(pugi::xml_node node); + SphericalMesh(hid_t group); // Overridden methods virtual MeshIndex get_indices(Position r, bool& in_mesh) const override; @@ -668,6 +682,7 @@ public: // Constructors UnstructuredMesh() { n_dimension_ = 3; }; UnstructuredMesh(pugi::xml_node node); + UnstructuredMesh(hid_t group); static const std::string mesh_type; virtual std::string get_mesh_type() const override; @@ -774,6 +789,7 @@ public: // Constructors MOABMesh() = default; MOABMesh(pugi::xml_node); + MOABMesh(hid_t group); MOABMesh(const std::string& filename, double length_multiplier = 1.0); MOABMesh(std::shared_ptr external_mbi); @@ -943,6 +959,7 @@ class LibMesh : public UnstructuredMesh { public: // Constructors LibMesh(pugi::xml_node node); + LibMesh(hid_t group); LibMesh(const std::string& filename, double length_multiplier = 1.0); LibMesh(libMesh::MeshBase& input_mesh, double length_multiplier = 1.0); @@ -1069,6 +1086,11 @@ private: //! \param[in] root XML node void read_meshes(pugi::xml_node root); +//! Read meshes from an HDF5 file +// +//! \param[in] group HDF5 group ("meshes" group) +void read_meshes(hid_t group); + //! Write mesh data to an HDF5 group // //! \param[in] group HDF5 group diff --git a/src/mesh.cpp b/src/mesh.cpp index 58d218b9c..610d057cf 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -230,6 +230,42 @@ void MaterialVolumes::add_volume_unsafe( // Mesh implementation //============================================================================== +template +const std::unique_ptr& Mesh::create( + T dataset, const std::string& mesh_type, const std::string& mesh_library) +{ + // Determine mesh type. Add to model vector and map + if (mesh_type == RegularMesh::mesh_type) { + model::meshes.push_back(make_unique(dataset)); + } else if (mesh_type == RectilinearMesh::mesh_type) { + model::meshes.push_back(make_unique(dataset)); + } else if (mesh_type == CylindricalMesh::mesh_type) { + model::meshes.push_back(make_unique(dataset)); + } else if (mesh_type == SphericalMesh::mesh_type) { + model::meshes.push_back(make_unique(dataset)); +#ifdef OPENMC_DAGMC_ENABLED + } else if (mesh_type == UnstructuredMesh::mesh_type && + mesh_library == MOABMesh::mesh_lib_type) { + model::meshes.push_back(make_unique(dataset)); +#endif +#ifdef OPENMC_LIBMESH_ENABLED + } else if (mesh_type == UnstructuredMesh::mesh_type && + mesh_library == LibMesh::mesh_lib_type) { + model::meshes.push_back(make_unique(dataset)); +#endif + } else if (mesh_type == UnstructuredMesh::mesh_type) { + fatal_error("Unstructured mesh support is not enabled or the mesh " + "library is invalid."); + } else { + fatal_error(fmt::format("Invalid mesh type: {}", mesh_type)); + } + + // Map ID to position in vector + model::mesh_map[model::meshes.back()->id_] = model::meshes.size() - 1; + + return model::meshes.back(); +} + Mesh::Mesh(pugi::xml_node node) { // Read mesh id @@ -238,6 +274,17 @@ Mesh::Mesh(pugi::xml_node node) name_ = get_node_value(node, "name"); } +Mesh::Mesh(hid_t group) +{ + // Read mesh ID + read_attribute(group, "id", id_); + + // Read mesh name + if (object_exists(group, "name")) { + read_dataset(group, "name", name_); + } +} + void Mesh::set_id(int32_t id) { assert(id >= 0 || id == C_NONE); @@ -265,7 +312,13 @@ void Mesh::set_id(int32_t id) // Update ID and entry in the mesh map id_ = id; - model::mesh_map[id] = model::meshes.size() - 1; + + // find the index of this mesh in the model::meshes vector + // (search in reverse because this mesh was likely just added to the vector) + auto it = std::find_if(model::meshes.rbegin(), model::meshes.rend(), + [this](const std::unique_ptr& mesh) { return mesh.get() == this; }); + + model::mesh_map[id] = std::distance(model::meshes.begin(), it.base()) - 1; } vector Mesh::volumes() const @@ -627,6 +680,46 @@ UnstructuredMesh::UnstructuredMesh(pugi::xml_node node) : Mesh(node) } } +UnstructuredMesh::UnstructuredMesh(hid_t group) : Mesh(group) +{ + n_dimension_ = 3; + + // check the mesh type + if (object_exists(group, "type")) { + std::string temp; + read_dataset(group, "type", temp); + if (temp != mesh_type) { + fatal_error(fmt::format("Invalid mesh type: {}", temp)); + } + } + + // check if a length unit multiplier was specified + if (object_exists(group, "length_multiplier")) { + read_dataset(group, "length_multiplier", length_multiplier_); + } + + // get the filename of the unstructured mesh to load + if (object_exists(group, "filename")) { + read_dataset(group, "filename", filename_); + if (!file_exists(filename_)) { + fatal_error("Mesh file '" + filename_ + "' does not exist!"); + } + } else { + fatal_error(fmt::format( + "No filename supplied for unstructured mesh with ID: {}", id_)); + } + + if (attribute_exists(group, "options")) { + read_attribute(group, "options", options_); + } + + // check if mesh tally data should be written with + // statepoint files + if (attribute_exists(group, "output")) { + read_attribute(group, "output", output_); + } +} + void UnstructuredMesh::determine_bounds() { double xmin = INFTY; @@ -1086,6 +1179,72 @@ void StructuredMesh::surface_bins_crossed( // RegularMesh implementation //============================================================================== +int RegularMesh::set_grid() +{ + auto shape = xt::adapt(shape_, {n_dimension_}); + + // Check that dimensions are all greater than zero + if (xt::any(shape <= 0)) { + set_errmsg("All entries for a regular mesh dimensions " + "must be positive."); + return OPENMC_E_INVALID_ARGUMENT; + } + + // Make sure lower_left and dimension match + if (lower_left_.size() != n_dimension_) { + set_errmsg("Number of entries in lower_left must be the same " + "as the regular mesh dimensions."); + return OPENMC_E_INVALID_ARGUMENT; + } + if (width_.size() > 0) { + + // Check to ensure width has same dimensions + if (width_.size() != n_dimension_) { + set_errmsg("Number of entries on width must be the same as " + "the regular mesh dimensions."); + return OPENMC_E_INVALID_ARGUMENT; + } + + // Check for negative widths + if (xt::any(width_ < 0.0)) { + 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_); + + } else if (upper_right_.size() > 0) { + + // Check to ensure upper_right_ has same dimensions + if (upper_right_.size() != n_dimension_) { + set_errmsg("Number of entries on upper_right must be the " + "same as the regular mesh dimensions."); + return OPENMC_E_INVALID_ARGUMENT; + } + + // Check that upper-right is above lower-left + if (xt::any(upper_right_ < lower_left_)) { + set_errmsg( + "The upper_right coordinates of a regular mesh must be greater than " + "the lower_left coordinates."); + return OPENMC_E_INVALID_ARGUMENT; + } + + // Set width + width_ = xt::eval((upper_right_ - lower_left_) / shape); + } + + // Set material volumes + volume_frac_ = 1.0 / xt::prod(shape)(); + + element_volume_ = 1.0; + for (int i = 0; i < n_dimension_; i++) { + element_volume_ *= width_[i]; + } + return 0; +} + RegularMesh::RegularMesh(pugi::xml_node node) : StructuredMesh {node} { // Determine number of dimensions for mesh @@ -1100,12 +1259,6 @@ RegularMesh::RegularMesh(pugi::xml_node node) : StructuredMesh {node} } std::copy(shape.begin(), shape.end(), shape_.begin()); - // Check that dimensions are all greater than zero - if (xt::any(shape <= 0)) { - fatal_error("All entries on the element for a tally " - "mesh must be positive."); - } - // Check for lower-left coordinates if (check_for_node(node, "lower_left")) { // Read mesh lower-left corner location @@ -1114,12 +1267,6 @@ RegularMesh::RegularMesh(pugi::xml_node node) : StructuredMesh {node} fatal_error("Must specify on a mesh."); } - // Make sure lower_left and dimension match - if (shape.size() != lower_left_.size()) { - fatal_error("Number of entries on must be the same " - "as the number of entries on ."); - } - if (check_for_node(node, "width")) { // Make sure one of upper-right or width were specified if (check_for_node(node, "upper_right")) { @@ -1128,49 +1275,52 @@ RegularMesh::RegularMesh(pugi::xml_node node) : StructuredMesh {node} width_ = get_node_xarray(node, "width"); - // Check to ensure width has same dimensions - auto n = width_.size(); - if (n != lower_left_.size()) { - fatal_error("Number of entries on must be the same as " - "the number of entries on ."); - } - - // Check for negative widths - if (xt::any(width_ < 0.0)) { - fatal_error("Cannot have a negative on a tally mesh."); - } - - // Set width and upper right coordinate - upper_right_ = xt::eval(lower_left_ + shape * width_); - } else if (check_for_node(node, "upper_right")) { + upper_right_ = get_node_xarray(node, "upper_right"); - // Check to ensure width has same dimensions - auto n = upper_right_.size(); - if (n != lower_left_.size()) { - fatal_error("Number of entries on must be the " - "same as the number of entries on ."); - } - - // Check that upper-right is above lower-left - if (xt::any(upper_right_ < lower_left_)) { - fatal_error("The coordinates must be greater than " - "the coordinates on a tally mesh."); - } - - // Set width - width_ = xt::eval((upper_right_ - lower_left_) / shape); } else { fatal_error("Must specify either or on a mesh."); } - // Set material volumes - volume_frac_ = 1.0 / xt::prod(shape)(); + if (int err = set_grid()) { + fatal_error(openmc_err_msg); + } +} - element_volume_ = 1.0; - for (int i = 0; i < n_dimension_; i++) { - element_volume_ *= width_[i]; +RegularMesh::RegularMesh(hid_t group) : StructuredMesh {group} +{ + // Determine number of dimensions for mesh + if (!object_exists(group, "dimension")) { + fatal_error("Must specify on a regular mesh."); + } + + xt::xtensor shape; + read_dataset(group, "dimension", shape); + int n = n_dimension_ = shape.size(); + if (n != 1 && n != 2 && n != 3) { + fatal_error("Mesh must be one, two, or three dimensions."); + } + std::copy(shape.begin(), shape.end(), shape_.begin()); + + // Check for lower-left coordinates + if (object_exists(group, "lower_left")) { + // Read mesh lower-left corner location + read_dataset(group, "lower_left", lower_left_); + } else { + fatal_error("Must specify lower_left dataset on a mesh."); + } + + if (object_exists(group, "upper_right")) { + + read_dataset(group, "upper_right", upper_right_); + + } else { + fatal_error("Must specify either upper_right dataset on a mesh."); + } + + if (int err = set_grid()) { + fatal_error(openmc_err_msg); } } @@ -1343,6 +1493,19 @@ RectilinearMesh::RectilinearMesh(pugi::xml_node node) : StructuredMesh {node} } } +RectilinearMesh::RectilinearMesh(hid_t group) : StructuredMesh {group} +{ + n_dimension_ = 3; + + read_dataset(group, "x_grid", grid_[0]); + read_dataset(group, "y_grid", grid_[1]); + read_dataset(group, "z_grid", grid_[2]); + + if (int err = set_grid()) { + fatal_error(openmc_err_msg); + } +} + const std::string RectilinearMesh::mesh_type = "rectilinear"; std::string RectilinearMesh::get_mesh_type() const @@ -1478,6 +1641,19 @@ CylindricalMesh::CylindricalMesh(pugi::xml_node node) } } +CylindricalMesh::CylindricalMesh(hid_t group) : PeriodicStructuredMesh {group} +{ + n_dimension_ = 3; + read_dataset(group, "r_grid", grid_[0]); + read_dataset(group, "phi_grid", grid_[1]); + read_dataset(group, "z_grid", grid_[2]); + read_dataset(group, "origin", origin_); + + if (int err = set_grid()) { + fatal_error(openmc_err_msg); + } +} + const std::string CylindricalMesh::mesh_type = "cylindrical"; std::string CylindricalMesh::get_mesh_type() const @@ -1756,6 +1932,20 @@ SphericalMesh::SphericalMesh(pugi::xml_node node) } } +SphericalMesh::SphericalMesh(hid_t group) : PeriodicStructuredMesh {group} +{ + n_dimension_ = 3; + + read_dataset(group, "r_grid", grid_[0]); + read_dataset(group, "theta_grid", grid_[1]); + read_dataset(group, "phi_grid", grid_[2]); + read_dataset(group, "origin", origin_); + + if (int err = set_grid()) { + fatal_error(openmc_err_msg); + } +} + const std::string SphericalMesh::mesh_type = "spherical"; std::string SphericalMesh::get_mesh_type() const @@ -2520,6 +2710,11 @@ MOABMesh::MOABMesh(pugi::xml_node node) : UnstructuredMesh(node) initialize(); } +MOABMesh::MOABMesh(hid_t group) : UnstructuredMesh(group) +{ + initialize(); +} + MOABMesh::MOABMesh(const std::string& filename, double length_multiplier) : UnstructuredMesh() { @@ -3228,6 +3423,15 @@ LibMesh::LibMesh(pugi::xml_node node) : UnstructuredMesh(node) initialize(); } +LibMesh::LibMesh(hid_t group) : UnstructuredMesh(group) +{ + // filename_ and length_multiplier_ will already be set by the + // UnstructuredMesh constructor + set_mesh_pointer_from_filename(filename_); + set_length_multiplier(length_multiplier_); + initialize(); +} + // create the mesh from a pointer to a libMesh Mesh LibMesh::LibMesh(libMesh::MeshBase& input_mesh, double length_multiplier) { @@ -3618,34 +3822,51 @@ void read_meshes(pugi::xml_node root) mesh_lib = get_node_value(node, "library", true, true); } - // Read mesh and add to vector - if (mesh_type == RegularMesh::mesh_type) { - model::meshes.push_back(make_unique(node)); - } else if (mesh_type == RectilinearMesh::mesh_type) { - model::meshes.push_back(make_unique(node)); - } else if (mesh_type == CylindricalMesh::mesh_type) { - model::meshes.push_back(make_unique(node)); - } else if (mesh_type == SphericalMesh::mesh_type) { - model::meshes.push_back(make_unique(node)); -#ifdef OPENMC_DAGMC_ENABLED - } else if (mesh_type == UnstructuredMesh::mesh_type && - mesh_lib == MOABMesh::mesh_lib_type) { - model::meshes.push_back(make_unique(node)); -#endif -#ifdef OPENMC_LIBMESH_ENABLED - } else if (mesh_type == UnstructuredMesh::mesh_type && - mesh_lib == LibMesh::mesh_lib_type) { - model::meshes.push_back(make_unique(node)); -#endif - } else if (mesh_type == UnstructuredMesh::mesh_type) { - fatal_error("Unstructured mesh support is not enabled or the mesh " - "library is invalid."); - } else { - fatal_error("Invalid mesh type: " + mesh_type); + Mesh::create(node, mesh_type, mesh_lib); + } +} + +void read_meshes(hid_t group) +{ + std::unordered_set mesh_ids; + + std::vector ids; + read_attribute(group, "ids", ids); + + for (auto id : ids) { + + // Check to make sure multiple meshes in the same file don't share IDs + if (contains(mesh_ids, id)) { + fatal_error(fmt::format("Two or more meshes use the same unique ID " + "'{}' in the same HDF5 input file", + id)); + } + mesh_ids.insert(id); + + // If we've already read a mesh with the same ID in a *different* file, + // assume it is the same here + if (model::mesh_map.find(id) != model::mesh_map.end()) { + warning(fmt::format("Mesh with ID={} appears in multiple files.", id)); + continue; } - // Map ID to position in vector - model::mesh_map[model::meshes.back()->id_] = model::meshes.size() - 1; + std::string name = fmt::format("mesh {}", id); + hid_t mesh_group = open_group(group, name.c_str()); + + std::string mesh_type; + if (object_exists(mesh_group, "type")) { + read_dataset(mesh_group, "type", mesh_type); + } else { + mesh_type = "regular"; + } + + // determine the mesh library to use + std::string mesh_lib; + if (object_exists(mesh_group, "library")) { + read_dataset(mesh_group, "library", mesh_lib); + } + + Mesh::create(mesh_group, mesh_type, mesh_lib); } } diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index 26762ad18..0d648d333 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -1328,6 +1328,10 @@ extern "C" int openmc_weight_windows_import(const char* filename) hid_t weight_windows_group = open_group(ww_file, "weight_windows"); + hid_t mesh_group = open_group(ww_file, "meshes"); + + read_meshes(mesh_group); + std::vector names = group_names(weight_windows_group); for (const auto& name : names) { diff --git a/tests/cpp_unit_tests/CMakeLists.txt b/tests/cpp_unit_tests/CMakeLists.txt index 8fedc2daa..5f87db9ea 100644 --- a/tests/cpp_unit_tests/CMakeLists.txt +++ b/tests/cpp_unit_tests/CMakeLists.txt @@ -5,6 +5,7 @@ set(TEST_NAMES test_interpolate test_math test_mcpl_stat_sum + test_mesh # Add additional unit test files here ) diff --git a/tests/cpp_unit_tests/test_mesh.cpp b/tests/cpp_unit_tests/test_mesh.cpp new file mode 100644 index 000000000..24c4f7737 --- /dev/null +++ b/tests/cpp_unit_tests/test_mesh.cpp @@ -0,0 +1,257 @@ +#include +#include +#include + +#include +#include + +#include "openmc/hdf5_interface.h" +#include "openmc/mesh.h" + +using namespace openmc; + +TEST_CASE("Test mesh hdf5 roundtrip - regular") +{ + // The XML data as a string + std::string xml_string = R"( + + 3 4 5 + -2 -3 -5 + 2 3 5 + + )"; + + // Create a pugixml document object + pugi::xml_document doc; + + // Load the XML from the string + pugi::xml_parse_result result = doc.load_string(xml_string.c_str()); + + pugi::xml_node root = doc.child("mesh"); + + auto mesh = RegularMesh(root); + + hid_t file_id = file_open("mesh.h5", 'w'); + + mesh.to_hdf5(file_id); + + file_close(file_id); + + hid_t file_id2 = file_open("mesh.h5", 'r'); + + hid_t group = open_group(file_id2, "mesh 1"); + + auto mesh2 = RegularMesh(group); + + file_close(file_id2); + + remove("mesh.h5"); + + REQUIRE(mesh2.shape_ == mesh.shape_); + + REQUIRE(mesh2.lower_left() == mesh.lower_left()); + + REQUIRE(mesh2.upper_right() == mesh.upper_right()); +} + +TEST_CASE("Test mesh hdf5 roundtrip - rectilinear") +{ + // The XML data as a string + std::string xml_string = R"( + + 0.0 1.0 5.0 10.0 + -10.0 -5.0 0.0 + -100.0 0.0 100.0 + + )"; + + // Create a pugixml document object + pugi::xml_document doc; + + // Load the XML from the string + pugi::xml_parse_result result = doc.load_string(xml_string.c_str()); + + pugi::xml_node root = doc.child("mesh"); + + auto mesh = RectilinearMesh(root); + + hid_t file_id = file_open("mesh.h5", 'w'); + + mesh.to_hdf5(file_id); + + file_close(file_id); + + hid_t file_id2 = file_open("mesh.h5", 'r'); + + hid_t group = open_group(file_id2, "mesh 1"); + + auto mesh2 = RectilinearMesh(group); + + file_close(file_id2); + + remove("mesh.h5"); + + REQUIRE(mesh2.shape_ == mesh.shape_); + + REQUIRE(mesh2.grid_ == mesh.grid_); +} + +TEST_CASE("Test mesh hdf5 roundtrip - cylindrical") +{ + // The XML data as a string + std::string xml_string = R"( + + 0.1 0.2 0.5 1.0 + 0.0 6.283185307179586 + 0.1 0.2 0.4 0.6 1.0 + 0 0 0 + + )"; + + // Create a pugixml document object + pugi::xml_document doc; + + // Load the XML from the string + pugi::xml_parse_result result = doc.load_string(xml_string.c_str()); + + pugi::xml_node root = doc.child("mesh"); + + auto mesh = CylindricalMesh(root); + + hid_t file_id = file_open("mesh.h5", 'w'); + + mesh.to_hdf5(file_id); + + file_close(file_id); + + hid_t file_id2 = file_open("mesh.h5", 'r'); + + hid_t group = open_group(file_id2, "mesh 1"); + + auto mesh2 = CylindricalMesh(group); + + file_close(file_id2); + + remove("mesh.h5"); + + REQUIRE(mesh2.shape_ == mesh.shape_); + + REQUIRE(mesh2.grid_ == mesh.grid_); +} + +TEST_CASE("Test mesh hdf5 roundtrip - spherical") +{ + // The XML data as a string + std::string xml_string = R"( + + 0.1 0.2 0.5 1.0 + 0.0 3.141592653589793 + 0.0 6.283185307179586 + 0.0 0.0 0.0 + ' + )"; + + // Create a pugixml document object + pugi::xml_document doc; + + // Load the XML from the string + pugi::xml_parse_result result = doc.load_string(xml_string.c_str()); + + pugi::xml_node root = doc.child("mesh"); + + auto mesh = SphericalMesh(root); + + hid_t file_id = file_open("mesh.h5", 'w'); + + mesh.to_hdf5(file_id); + + file_close(file_id); + + hid_t file_id2 = file_open("mesh.h5", 'r'); + + hid_t group = open_group(file_id2, "mesh 1"); + + auto mesh2 = SphericalMesh(group); + + file_close(file_id2); + + remove("mesh.h5"); + + REQUIRE(mesh2.shape_ == mesh.shape_); + + REQUIRE(mesh2.grid_ == mesh.grid_); +} + +TEST_CASE("Test multiple meshes HDF5 roundtrip - spherical") +{ + // The XML data as a string + std::string xml_string = R"( + + + 0.1 0.2 0.5 1.0 + 0.0 3.141592653589793 + 0.0 6.283185307179586 + 0.0 0.0 0.0 + + + 3 4 5 + -2 -3 -5 + 2 3 5 + + + )"; + + // Create a pugixml document object + pugi::xml_document doc; + + // Load the XML from the string + pugi::xml_parse_result result = doc.load_string(xml_string.c_str()); + + pugi::xml_node root = doc.child("meshes"); + + read_meshes(root); + + const auto spherical_mesh_xml = + dynamic_cast(model::meshes[0].get()); + const auto regular_mesh_xml = + dynamic_cast(model::meshes[1].get()); + + hid_t file_id = file_open("meshes.h5", 'w'); + + hid_t root_group = create_group(file_id, "root"); + + open_group(file_id, "root"); + + meshes_to_hdf5(root_group); + + close_group(root_group); + + file_close(file_id); + + hid_t file_id2 = file_open("meshes.h5", 'r'); + + hid_t root_group_read = open_group(file_id2, "root"); + + hid_t mesh_group_read = open_group(root_group_read, "meshes"); + + read_meshes(mesh_group_read); + + // increment mesh IDs to avoid collision during read + for (auto& mesh : model::meshes) { + mesh->set_id(mesh->id() + 10); + } + + const auto spherical_mesh_hdf5 = dynamic_cast( + model::meshes[model::mesh_map[spherical_mesh_xml->id_]].get()); + const auto regular_mesh_hdf5 = dynamic_cast( + model::meshes[model::mesh_map[regular_mesh_xml->id_]].get()); + + remove("meshes.h5"); + + REQUIRE(spherical_mesh_hdf5->shape_ == spherical_mesh_xml->shape_); + REQUIRE(spherical_mesh_hdf5->grid_ == spherical_mesh_xml->grid_); + + REQUIRE(regular_mesh_hdf5->shape_ == regular_mesh_xml->shape_); + REQUIRE(regular_mesh_hdf5->lower_left() == regular_mesh_xml->lower_left()); + REQUIRE(regular_mesh_hdf5->upper_right() == regular_mesh_xml->upper_right()); +} From 70b52546624cb3a654189856d8c9e012624c7672 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Mon, 27 Oct 2025 03:30:54 -0500 Subject: [PATCH 434/671] Random Ray Geometry Debug Mode Fix (#3615) --- src/random_ray/random_ray.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp index 89a91449d..c19d136a4 100644 --- a/src/random_ray/random_ray.cpp +++ b/src/random_ray/random_ray.cpp @@ -278,6 +278,10 @@ uint64_t RandomRay::transport_history_based_single_ray() // Transports ray across a single source region void RandomRay::event_advance_ray() { + // If geometry debug mode is on, check for cell overlaps + if (settings::check_overlaps) + check_cell_overlap(*this); + // Find the distance to the nearest boundary boundary() = distance_to_boundary(*this); double distance = boundary().distance(); From 230db28d39ae1e3011e3ad85cb8b5090693fe862 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Tue, 28 Oct 2025 03:22:37 -0500 Subject: [PATCH 435/671] FW-CADIS Disregard Max Realizations Setting (#3616) --- docs/source/usersguide/variance_reduction.rst | 3 +-- src/simulation.cpp | 7 ++----- src/weight_windows.cpp | 15 +++++++++++---- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/docs/source/usersguide/variance_reduction.rst b/docs/source/usersguide/variance_reduction.rst index 369e33e2d..5c2485158 100644 --- a/docs/source/usersguide/variance_reduction.rst +++ b/docs/source/usersguide/variance_reduction.rst @@ -133,8 +133,7 @@ random ray mode can be found in the :ref:`Random Ray User Guide `. # we used for source region decomposition wwg = openmc.WeightWindowGenerator( method='fw_cadis', - mesh=mesh, - max_realizations=settings.batches + mesh=mesh ) # Add generator to openmc.settings object diff --git a/src/simulation.cpp b/src/simulation.cpp index f55546c4c..05d554526 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -400,11 +400,8 @@ void finalize_batch() simulation::time_tallies.stop(); // update weight windows if needed - if (settings::solver_type != SolverType::RANDOM_RAY || - simulation::current_batch == settings::n_batches) { - for (const auto& wwg : variance_reduction::weight_windows_generators) { - wwg->update(); - } + for (const auto& wwg : variance_reduction::weight_windows_generators) { + wwg->update(); } // Reset global tally results diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index 0d648d333..674fae49c 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -26,6 +26,7 @@ #include "openmc/random_ray/flat_source_domain.h" #include "openmc/search.h" #include "openmc/settings.h" +#include "openmc/simulation.h" #include "openmc/tallies/filter_energy.h" #include "openmc/tallies/filter_mesh.h" #include "openmc/tallies/filter_particle.h" @@ -966,11 +967,17 @@ void WeightWindowsGenerator::update() const Tally* tally = model::tallies[tally_idx_].get(); - // if we're beyond the number of max realizations or not at the corrrect - // update interval, skip the update - if (max_realizations_ < tally->n_realizations_ || - tally->n_realizations_ % update_interval_ != 0) + // If in random ray mode, only update on the last batch + if (settings::solver_type == SolverType::RANDOM_RAY) { + if (simulation::current_batch != settings::n_batches) { + return; + } + // If in Monte Carlo mode and beyond the number of max realizations or + // not at the correct update interval, skip the update + } else if (max_realizations_ < tally->n_realizations_ || + tally->n_realizations_ % update_interval_ != 0) { return; + } wws->update_weights(tally, tally_value_, threshold_, ratio_, method_); From f10d7d9f67375acb088d01e82bab957617f18e39 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 28 Oct 2025 03:23:16 -0500 Subject: [PATCH 436/671] Speed up `apply_time_correction` by reducing file I/O and deepcopies (#3617) --- openmc/deplete/d1s.py | 27 ++++++++++++++++----------- tests/unit_tests/test_d1s.py | 14 ++++++++++++++ 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/openmc/deplete/d1s.py b/openmc/deplete/d1s.py index f51dea416..bc99fc42d 100644 --- a/openmc/deplete/d1s.py +++ b/openmc/deplete/d1s.py @@ -5,7 +5,7 @@ shutdown dose rate calculations. """ -from copy import deepcopy +from copy import copy from typing import Sequence from math import log, prod @@ -164,8 +164,12 @@ def apply_time_correction( radionuclides = [str(x) for x in tally.filters[i_filter].bins] tcf = np.array([time_correction_factors[x][index] for x in radionuclides]) - # Create copy of tally - new_tally = deepcopy(tally) + # Force tally results to be read and std_dev to be computed + tally.std_dev + + # Create shallow copy of tally + new_tally = copy(tally) + new_tally._filters = copy(tally._filters) # Determine number of bins in other filters n_bins_before = prod([f.num_bins for f in tally.filters[:i_filter]]) @@ -177,32 +181,33 @@ def apply_time_correction( shape = (n_bins_before, n_radionuclides, n_bins_after, n_nuclides, n_scores) tally_sum = new_tally.sum.reshape(shape) tally_sum_sq = new_tally.sum_sq.reshape(shape) + tally_mean = new_tally.mean.reshape(shape) + tally_std_dev = new_tally.std_dev.reshape(shape) # Apply TCF, broadcasting to the correct dimensions tcf.shape = (1, -1, 1, 1, 1) new_tally._sum = tally_sum * tcf new_tally._sum_sq = tally_sum_sq * (tcf*tcf) - new_tally._mean = None - new_tally._std_dev = None + new_tally._mean = tally_mean * tcf + new_tally._std_dev = tally_std_dev * tcf shape = (-1, n_nuclides, n_scores) if sum_nuclides: - # Query the mean and standard deviation - mean = new_tally.mean - std_dev = new_tally.std_dev - # Sum over parent nuclides (note that when combining different bins for # parent nuclide, we can't work directly on sum_sq) - new_tally._mean = mean.sum(axis=1).reshape(shape) - new_tally._std_dev = np.linalg.norm(std_dev, axis=1).reshape(shape) + new_tally._mean = new_tally.mean.sum(axis=1).reshape(shape) + new_tally._std_dev = np.linalg.norm(new_tally.std_dev, axis=1).reshape(shape) new_tally._derived = True # Remove ParentNuclideFilter new_tally.filters.pop(i_filter) else: + # Change shape back to (filter combinations, nuclides, scores) new_tally._sum.shape = shape new_tally._sum_sq.shape = shape + new_tally._mean.shape = shape + new_tally._std_dev.shape = shape return new_tally diff --git a/tests/unit_tests/test_d1s.py b/tests/unit_tests/test_d1s.py index 9410f2da2..8f3b62f40 100644 --- a/tests/unit_tests/test_d1s.py +++ b/tests/unit_tests/test_d1s.py @@ -120,6 +120,13 @@ def test_apply_time_correction(run_in_tmpdir): tally = sp.tallies[tally.id] flux = tally.mean.flatten() + # Copy attributes from original tally + tally_filters = list(tally.filters) + tally_sum = tally.sum.copy() + tally_sum_sq = tally.sum_sq.copy() + tally_mean = tally.mean.copy() + tally_std_dev = tally.std_dev.copy() + # Apply TCF and make sure results are consistent result = d1s.apply_time_correction(tally, factors, sum_nuclides=False) tcf = np.array([factors[nuc][-1] for nuc in nuclides]) @@ -129,6 +136,13 @@ def test_apply_time_correction(run_in_tmpdir): result_summed = d1s.apply_time_correction(tally, factors) assert result_summed.mean.flatten()[0] == pytest.approx(result.mean.sum()) + # Make sure original tally is unchanged + assert tally.filters == tally_filters + assert np.all(tally.sum == tally_sum) + assert np.all(tally.sum_sq == tally_sum_sq) + assert np.all(tally.mean == tally_mean) + assert np.all(tally.std_dev == tally_std_dev) + # Make sure various tally methods work result.get_values() result_summed.get_values() From a74c1424a8a883066b2c0afe4b23f20c84cbb815 Mon Sep 17 00:00:00 2001 From: Makarand More <40858007+Jarvis2001@users.noreply.github.com> Date: Tue, 28 Oct 2025 20:20:11 +0530 Subject: [PATCH 437/671] Update `check_type` calls to accept both `str` and `os.PathLike` objects. (#3618) Co-authored-by: Paul Romano --- openmc/checkvalue.py | 2 +- openmc/mgxs/library.py | 7 ++-- openmc/mgxs/mdgxs.py | 3 +- openmc/mgxs/mgxs.py | 3 +- openmc/mgxs_library.py | 5 ++- openmc/plots.py | 2 +- tests/unit_tests/test_pathlike_simple.py | 46 ++++++++++++++++++++++++ 7 files changed, 58 insertions(+), 10 deletions(-) create mode 100644 tests/unit_tests/test_pathlike_simple.py diff --git a/openmc/checkvalue.py b/openmc/checkvalue.py index 4fa205b14..5ff2cf9ac 100644 --- a/openmc/checkvalue.py +++ b/openmc/checkvalue.py @@ -37,7 +37,7 @@ def check_type(name, value, expected_type, expected_iter_type=None, *, none_ok=F [t.__name__ for t in expected_type])) else: msg = (f'Unable to set "{name}" to "{value}" which is not of type "' - f'{expected_type.__name__}"') + f'{expected_type}"') raise TypeError(msg) if expected_iter_type: diff --git a/openmc/mgxs/library.py b/openmc/mgxs/library.py index 12a4630bd..f782cbe9e 100644 --- a/openmc/mgxs/library.py +++ b/openmc/mgxs/library.py @@ -10,6 +10,7 @@ import numpy as np import openmc import openmc.mgxs import openmc.checkvalue as cv +from openmc.checkvalue import PathLike from ..tallies import ESTIMATOR_TYPES @@ -851,7 +852,7 @@ class Library: 'since a statepoint has not yet been loaded' raise ValueError(msg) - cv.check_type('filename', filename, str) + cv.check_type('filename', filename, (str, PathLike)) cv.check_type('directory', directory, str) import h5py @@ -894,7 +895,7 @@ class Library: """ - cv.check_type('filename', filename, str) + cv.check_type('filename', filename, (str, PathLike)) cv.check_type('directory', directory, str) # Make directory if it does not exist @@ -930,7 +931,7 @@ class Library: """ - cv.check_type('filename', filename, str) + cv.check_type('filename', filename, (str, PathLike)) cv.check_type('directory', directory, str) # Make directory if it does not exist diff --git a/openmc/mgxs/mdgxs.py b/openmc/mgxs/mdgxs.py index b95a4fbc0..c12c1a9ab 100644 --- a/openmc/mgxs/mdgxs.py +++ b/openmc/mgxs/mdgxs.py @@ -7,6 +7,7 @@ import numpy as np import openmc import openmc.checkvalue as cv +from openmc.checkvalue import PathLike from openmc.mgxs import MGXS from .mgxs import _DOMAIN_TO_FILTER @@ -722,7 +723,7 @@ class MDGXS(MGXS): """ - cv.check_type('filename', filename, str) + cv.check_type('filename', filename, (str, PathLike)) cv.check_type('directory', directory, str) cv.check_value('format', format, ['csv', 'excel', 'pickle', 'latex']) cv.check_value('xs_type', xs_type, ['macro', 'micro']) diff --git a/openmc/mgxs/mgxs.py b/openmc/mgxs/mgxs.py index b8f2b8d3f..533ab0ad3 100644 --- a/openmc/mgxs/mgxs.py +++ b/openmc/mgxs/mgxs.py @@ -10,6 +10,7 @@ import numpy as np import openmc from openmc.data import REACTION_MT, REACTION_NAME, FISSION_MTS import openmc.checkvalue as cv +from openmc.checkvalue import PathLike from ..tallies import ESTIMATOR_TYPES from . import EnergyGroups @@ -1982,7 +1983,7 @@ class MGXS: """ - cv.check_type('filename', filename, str) + cv.check_type('filename', filename, (str, PathLike)) cv.check_type('directory', directory, str) cv.check_value('format', format, ['csv', 'excel', 'pickle', 'latex']) cv.check_value('xs_type', xs_type, ['macro', 'micro']) diff --git a/openmc/mgxs_library.py b/openmc/mgxs_library.py index ce86f97df..4bc2d4a5a 100644 --- a/openmc/mgxs_library.py +++ b/openmc/mgxs_library.py @@ -2514,8 +2514,7 @@ class MGXSLibrary: """ - check_type('filename', filename, PathLike) - + check_type('filename', filename, (str, PathLike)) # Create and write to the HDF5 file file = h5py.File(filename, "w", libver=libver) file.attrs['filetype'] = np.bytes_(_FILETYPE_MGXS_LIBRARY) @@ -2554,7 +2553,7 @@ class MGXSLibrary: raise ValueError("Either path or openmc.config['mg_cross_sections']" "must be set") - check_type('filename', filename, str) + check_type('filename', filename, (str, PathLike)) file = h5py.File(filename, 'r') # Check filetype and version diff --git a/openmc/plots.py b/openmc/plots.py index e9130f0be..a0bde3f00 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -439,7 +439,7 @@ class PlotBase(IDManagerMixin): @filename.setter def filename(self, filename): - cv.check_type('filename', filename, str) + cv.check_type('filename', filename, (str, PathLike)) self._filename = filename @property diff --git a/tests/unit_tests/test_pathlike_simple.py b/tests/unit_tests/test_pathlike_simple.py new file mode 100644 index 000000000..e0116bf01 --- /dev/null +++ b/tests/unit_tests/test_pathlike_simple.py @@ -0,0 +1,46 @@ +"""Simple test for PathLike filename support""" + +from pathlib import Path + +import pytest +import openmc +from openmc.checkvalue import check_type, PathLike + + +def test_pathlike_type_checking(): + """Test that PathLike type checking works correctly""" + + # Test with string (should work) + check_type('filename', 'test.txt', PathLike) + + # Test with Path object (should work) + path_obj = Path('test.txt') + check_type('filename', path_obj, PathLike) + + # Test with Path object containing subdirectories (should work) + path_with_subdir = Path('subdir') / 'test.txt' + check_type('filename', path_with_subdir, PathLike) + + # Test with invalid type (should raise TypeError) + with pytest.raises(TypeError): + check_type('filename', 123, PathLike) + + +def test_plot_filename_pathlike(): + """Test that plot filename accepts Path objects""" + + plot = openmc.Plot() + + # Test with string (should still work) + plot.filename = "test_plot" + assert plot.filename == "test_plot" + + # Test with Path object + path_obj = Path("test_plot_path") + plot.filename = path_obj + assert plot.filename == path_obj + + # Test with Path object containing subdirectories + path_with_subdir = Path("subdir") / "test_plot" + plot.filename = path_with_subdir + assert plot.filename == path_with_subdir From 4c4176661124fe92f5304d20d1b6e7675c44d455 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 28 Oct 2025 11:36:03 -0500 Subject: [PATCH 438/671] Criticality search method on the Model class (#3569) --- openmc/model/model.py | 270 ++++++++++++++++++++++++++++++++- src/settings.cpp | 5 - tests/unit_tests/test_lib.py | 3 - tests/unit_tests/test_model.py | 57 +++++++ 4 files changed, 326 insertions(+), 9 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index c1ffafafd..7963751d5 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1,6 +1,7 @@ from __future__ import annotations -from collections.abc import Iterable, Sequence +from collections.abc import Callable, Iterable, Sequence import copy +from dataclasses import dataclass, field from functools import cache from pathlib import Path import math @@ -8,11 +9,13 @@ from numbers import Integral, Real import random import re from tempfile import NamedTemporaryFile, TemporaryDirectory +from typing import Any, Protocol import warnings import h5py import lxml.etree as ET import numpy as np +from scipy.optimize import curve_fit import openmc import openmc._xml as xml @@ -24,6 +27,12 @@ from openmc.plots import add_plot_params, _BASIS_INDICES from openmc.utility_funcs import change_directory +# Protocol for a function that is passed to search_keff +class ModelModifier(Protocol): + def __call__(self, val: float, **kwargs: Any) -> None: + ... + + class Model: """Model container. @@ -2196,3 +2205,262 @@ class Model: # Take a wild guess as to how many rays are needed self.settings.particles = 2 * int(max_length) + + def keff_search( + self, + func: ModelModifier, + x0: float, + x1: float, + target: float = 1.0, + k_tol: float = 1e-4, + sigma_final: float = 3e-4, + p: float = 0.5, + q: float = 0.95, + memory: int = 4, + x_min: float | None = None, + x_max: float | None = None, + b0: int | None = None, + b_min: int = 20, + b_max: int | None = None, + maxiter: int = 50, + output: bool = False, + func_kwargs: dict[str, Any] | None = None, + run_kwargs: dict[str, Any] | None = None, + ) -> SearchResult: + r"""Perform a keff search on a model parametrized by a single variable. + + This method uses the GRsecant method described in a paper by `Price and + Roskoff `_. The GRsecant + method is a modification of the secant method that accounts for + uncertainties in the function evaluations. The method uses a weighted + linear fit of the most recent function evaluations to predict the next + point to evaluate. It also adaptively changes the number of batches to + meet the target uncertainty value at each iteration. + + The target uncertainty for iteration :math:`n+1` is determined by the + following equation (following Eq. (8) in the paper): + + .. math:: + \sigma_{i+1} = q \sigma_\text{final} \left ( \frac{ \min \left \{ + \left\lvert k_i - k_\text{target} \right\rvert : k=0,1,\dots,n + \right \} }{k_\text{tol}} \right )^p + + where :math:`q` is a multiplicative factor less than 1, given as the + ``sigma_factor`` parameter below. + + Parameters + ---------- + func : ModelModifier + Function that takes the parameter to be searched and makes a + modification to the model. + x0 : float + First guess for the parameter passed to `func` + x1 : float + Second guess for the parameter passed to `func` + target : float, optional + keff value to search for + k_tol : float, optional + Stopping criterion on the function value; the absolute value must be + within ``k_tol`` of zero to be accepted. + sigma_final : float, optional + Maximum accepted k-effective uncertainty for the stopping criterion. + p : float, optional + Exponent used in the stopping criterion. + q : float, optional + Multiplicative factor used in the stopping criterion. + memory : int, optional + Number of most-recent points used in the weighted linear fit of + ``f(x) = a + b x`` to predict the next point. + x_min : float, optional + Minimum allowed value for the parameter ``x``. + x_max : float, optional + Maximum allowed value for the parameter ``x``. + b0 : int, optional + Number of active batches to use for the initial function + evaluations. If None, uses the model's current setting. + b_min : int, optional + Minimum number of active batches to use in a function evaluation. + b_max : int, optional + Maximum number of active batches to use in a function evaluation. + maxiter : int, optional + Maximum number of iterations to perform. + output : bool, optional + Whether or not to display output showing iteration progress. + func_kwargs : dict, optional + Keyword-based arguments to pass to the `func` function. + run_kwargs : dict, optional + Keyword arguments to pass to :meth:`openmc.Model.run` or + :meth:`openmc.lib.run`. + + Returns + ------- + SearchResult + Result object containing the estimated root (parameter value) and + evaluation history (parameters, means, standard deviations, and + batches), plus convergence status and termination reason. + + """ + import openmc.lib + + check_type('model modifier', func, Callable) + check_type('target', target, Real) + if memory < 2: + raise ValueError("memory must be ≥ 2") + func_kwargs = {} if func_kwargs is None else dict(func_kwargs) + run_kwargs = {} if run_kwargs is None else dict(run_kwargs) + run_kwargs.setdefault('output', False) + + # Create lists to store the history of evaluations + xs: list[float] = [] + fs: list[float] = [] + ss: list[float] = [] + gs: list[int] = [] + count = 0 + + # Helper function to evaluate f and store results + def eval_at(x: float, batches: int) -> tuple[float, float]: + # Modify the model with the current guess + func(x, **func_kwargs) + + # Change the number of batches and run the model + batches += self.settings.inactive + if openmc.lib.is_initialized: + openmc.lib.settings.set_batches(batches) + openmc.lib.reset() + openmc.lib.run(**run_kwargs) + sp_filepath = f'statepoint.{batches}.h5' + else: + self.settings.batches = batches + sp_filepath = self.run(**run_kwargs) + + # Extract keff and its uncertainty + with openmc.StatePoint(sp_filepath) as sp: + keff = sp.keff + + if output: + nonlocal count + count += 1 + print(f'Iteration {count}: {batches=}, {x=:.6g}, {keff=:.5f}') + + xs.append(float(x)) + fs.append(float(keff.n - target)) + ss.append(float(keff.s)) + gs.append(int(batches)) + return fs[-1], ss[-1] + + # Default b0 to current model settings if not explicitly provided + if b0 is None: + b0 = self.settings.batches - self.settings.inactive + + # Perform the search (inlined GRsecant) in a temporary directory + with TemporaryDirectory() as tmpdir: + if not openmc.lib.is_initialized: + run_kwargs.setdefault('cwd', tmpdir) + + # ---- Seed with two evaluations + f0, s0 = eval_at(x0, b0) + if abs(f0) <= k_tol and s0 <= sigma_final: + return SearchResult(x0, xs, fs, ss, gs, True, "converged") + f1, s1 = eval_at(x1, b0) + if abs(f1) <= k_tol and s1 <= sigma_final: + return SearchResult(x1, xs, fs, ss, gs, True, "converged") + + for _ in range(maxiter - 2): + # ------ Step 1: propose next x via GRsecant + m = min(memory, len(xs)) + + # Perform a curve fit on f(x) = a + bx accounting for + # uncertainties. This is equivalent to minimizing the function + # in Equation (A.14) + (a, b), _ = curve_fit( + lambda x, a, b: a + b*x, + xs[-m:], fs[-m:], sigma=ss[-m:], absolute_sigma=True + ) + x_new = float(-a / b) + + # Clamp x_new to the bounds if provided + if x_min is not None: + x_new = max(x_new, x_min) + if x_max is not None: + x_new = min(x_new, x_max) + + # ------ Step 2: choose target σ for next run (Eq. 8 + clamp) + + min_abs_f = float(np.min(np.abs(fs))) + base = q * sigma_final + ratio = min_abs_f / k_tol if k_tol > 0 else 1.0 + sig = base * (ratio ** p) + sig_target = max(sig, base) + + # ------ Step 3: choose generations to hit σ_target (Appendix C) + + # Use at least two past points for regression + if len(gs) >= 2 and np.var(np.log(gs)) > 0.0: + # Perform a curve fit based on Eq. (C.3) to solve for ln(k). + # Note that unlike in the paper, we do not leave r as an + # undetermined parameter and choose r=0.5. + (ln_k,), _ = curve_fit( + lambda ln_b, ln_k: ln_k - 0.5*ln_b, + np.log(gs[-4:]), np.log(ss[-4:]), + ) + k = float(np.exp(ln_k)) + else: + k = float(ss[-1] * math.sqrt(gs[-1])) + + b_new = (k / sig_target) ** 2 + + # Clamp and round up to integer + b_new = max(b_min, math.ceil(b_new)) + if b_max is not None: + b_new = min(b_new, b_max) + + # Evaluate at proposed x with batches determined above + f_new, s_new = eval_at(x_new, b_new) + + # Termination based on both criteria (|f| and σ) + if abs(f_new) <= k_tol and s_new <= sigma_final: + return SearchResult(x_new, xs, fs, ss, gs, True, "converged") + + return SearchResult(xs[-1], xs, fs, ss, gs, False, "maxiter") + + +@dataclass +class SearchResult: + """Result of a GRsecant keff search. + + Attributes + ---------- + root : float + Estimated parameter value where f(x) = 0 at termination. + parameters : list[float] + Parameter values (x) evaluated during the search, in order. + keffs : list[float] + Estimated keff values for each evaluation. + stdevs : list[float] + One-sigma uncertainties of keff for each evaluation. + batches : list[int] + Number of active batches used for each evaluation. + converged : bool + Whether both |f| <= k_tol and sigma <= sigma_final were met. + flag : str + Reason for termination (e.g., "converged", "maxiter"). + """ + root: float + parameters: list[float] = field(repr=False) + means: list[float] = field(repr=False) + stdevs: list[float] = field(repr=False) + batches: list[int] = field(repr=False) + converged: bool + flag: str + + @property + def function_calls(self) -> int: + """Number of function evaluations performed.""" + return len(self.parameters) + + @property + def total_batches(self) -> int: + """Total number of active batches used across all evaluations.""" + return sum(self.batches) + + diff --git a/src/settings.cpp b/src/settings.cpp index 325256cdc..13b91b0e4 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -1220,11 +1220,6 @@ extern "C" int openmc_set_n_batches( return OPENMC_E_INVALID_ARGUMENT; } - if (simulation::current_batch >= n_batches) { - set_errmsg("Number of batches must be greater than current batch."); - return OPENMC_E_INVALID_ARGUMENT; - } - if (!settings::trigger_on) { // Set n_batches and n_max_batches to same value settings::n_batches = n_batches; diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index 43bc5a8f6..eb4dc3dce 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -496,9 +496,6 @@ def test_set_n_batches(lib_run): for i in range(7): openmc.lib.next_batch() - # Setting n_batches less than current_batch should raise error - with pytest.raises(exc.InvalidArgumentError): - settings.set_batches(6) # n_batches should stay the same assert settings.get_batches() == 10 diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 60f8b1a25..f4f94a47c 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -901,6 +901,7 @@ def test_id_map_aligned_model(): assert tr_instance == 3, f"Expected cell instance 3 at top-right corner, got {tr_instance}" assert tr_material == 5, f"Expected material ID 5 at top-right corner, got {tr_material}" + def test_setter_from_list(): mat = openmc.Material() model = openmc.Model(materials=[mat]) @@ -913,3 +914,59 @@ def test_setter_from_list(): plot = openmc.Plot() model = openmc.Model(plots=[plot]) assert isinstance(model.plots, openmc.Plots) + + +def test_keff_search(run_in_tmpdir): + """Test the Model.keff_search method""" + + # Create model of a sphere of U235 + mat = openmc.Material() + mat.set_density('g/cm3', 18.9) + mat.add_nuclide('U235', 1.0) + sphere = openmc.Sphere(r=10.0, boundary_type='vacuum') + cell = openmc.Cell(fill=mat, region=-sphere) + geometry = openmc.Geometry([cell]) + settings = openmc.Settings(particles=1000, inactive=10, batches=30) + model = openmc.Model(geometry=geometry, settings=settings) + + # Define function to modify sphere radius + def modify_radius(radius): + sphere.r = radius + + # Perform keff search + k_tol = 4e-3 + sigma_final = 2e-3 + result = model.keff_search( + func=modify_radius, + x0=6.0, + x1=9.0, + k_tol=k_tol, + sigma_final=sigma_final, + output=True, + ) + + final_keff = result.means[-1] + 1.0 # Add back target since means are (keff - target) + final_sigma = result.stdevs[-1] + + # Check for convergence and that tolerances are met + assert result.converged, "keff_search did not converge" + assert abs(final_keff - 1.0) <= k_tol, \ + f"Final keff {final_keff:.5f} not within k_tol {k_tol}" + assert final_sigma <= sigma_final, \ + f"Final uncertainty {final_sigma:.5f} exceeds sigma_final {sigma_final}" + + # Check type of result + assert isinstance(result, openmc.model.SearchResult) + + # Check that we have function evaluation history + assert len(result.parameters) >= 2 + assert len(result.means) == len(result.parameters) + assert len(result.stdevs) == len(result.parameters) + assert len(result.batches) == len(result.parameters) + + # Check that function_calls property works + assert result.function_calls == len(result.parameters) + + # Check that total_batches property works + assert result.total_batches == sum(result.batches) + assert result.total_batches > 0 From 5fc289b99d6ace24d6bcf736a77cfef4c59f6a82 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 31 Oct 2025 19:15:25 -0500 Subject: [PATCH 439/671] Automate workflow for mesh- or cell-based R2S calculations (#3508) Co-authored-by: Ethan Peterson Co-authored-by: Jonathan Shimwell --- docs/source/pythonapi/deplete.rst | 10 + docs/source/usersguide/decay_sources.rst | 205 ++++++- openmc/deplete/__init__.py | 1 + openmc/deplete/microxs.py | 113 +++- openmc/deplete/r2s.py | 680 +++++++++++++++++++++++ openmc/mesh.py | 18 +- openmc/model/model.py | 23 + openmc/utility_funcs.py | 19 + tests/unit_tests/test_r2s.py | 152 +++++ 9 files changed, 1173 insertions(+), 48 deletions(-) create mode 100644 openmc/deplete/r2s.py create mode 100644 tests/unit_tests/test_r2s.py diff --git a/docs/source/pythonapi/deplete.rst b/docs/source/pythonapi/deplete.rst index f112cf8cc..25fcd898f 100644 --- a/docs/source/pythonapi/deplete.rst +++ b/docs/source/pythonapi/deplete.rst @@ -287,6 +287,16 @@ the following abstract base classes: abc.SIIntegrator abc.DepSystemSolver +R2S Automation +-------------- + +.. autosummary:: + :toctree: generated + :nosignatures: + :template: myclass.rst + + R2SManager + D1S Functions ------------- diff --git a/docs/source/usersguide/decay_sources.rst b/docs/source/usersguide/decay_sources.rst index d5a078135..398680e74 100644 --- a/docs/source/usersguide/decay_sources.rst +++ b/docs/source/usersguide/decay_sources.rst @@ -6,42 +6,189 @@ Decay Sources Through the :ref:`depletion ` capabilities in OpenMC, it is possible to simulate radiation emitted from the decay of activated materials. -For fusion energy systems, this is commonly done using what is known as the -`rigorous 2-step `_ (R2S) method. -In this method, a neutron transport calculation is used to determine the neutron -flux and reaction rates over a cell- or mesh-based spatial discretization of the -model. Then, the neutron flux in each discrete region is used to predict the -activated material composition using a depletion solver. Finally, a photon -transport calculation with a source based on the activity and energy spectrum of -the activated materials is used to determine a desired physical response (e.g., -a dose rate) at one or more locations of interest. +For fusion energy systems, this is commonly done using either the `rigorous +2-step `_ (R2S) method or the +`direct 1-step `_ (D1S) method. +In the R2S method, a neutron transport calculation is used to determine the +neutron flux and reaction rates over a cell- or mesh-based spatial +discretization of the model. Then, the neutron flux in each discrete region is +used to predict the activated material composition using a depletion solver. +Finally, a photon transport calculation with a source based on the activity and +energy spectrum of the activated materials is used to determine a desired +physical response (e.g., a dose rate) at one or more locations of interest. +OpenMC includes automation for both the R2S and D1S methods as described in the +following sections. -Once a depletion simulation has been completed in OpenMC, the intrinsic decay -source can be determined as follows. First the activated material composition -can be determined using the :class:`openmc.deplete.Results` object. Indexing an -instance of this class with the timestep index returns a -:class:`~openmc.deplete.StepResult` object, which itself has a -:meth:`~openmc.deplete.StepResult.get_material` method. Once the activated -:class:`~openmc.Material` has been obtained, the -:meth:`~openmc.Material.get_decay_photon_energy` method will give the energy -spectrum of the decay photon source. The integral of the spectrum also indicates -the intensity of the source in units of [Bq]. Altogether, the workflow looks as -follows:: +Rigorous 2-Step (R2S) Calculations +================================== +OpenMC includes an :class:`openmc.deplete.R2SManager` class that fully automates +cell- and mesh-based R2S calculations. Before we describe this class, it is +useful to understand the basic mechanics of how an R2S calculation works. +Generally, it involves the following steps: + +1. The :meth:`openmc.deplete.get_microxs_and_flux` function is called to run a + neutron transport calculation that determines fluxes and microscopic cross + sections in each activation region. +2. The :class:`openmc.deplete.IndependentOperator` and + :class:`openmc.deplete.PredictorIntegrator` classes are used to carry out a + depletion (activation) calculation in order to determine predicted material + compositions based on a set of timesteps and source rates. +3. The activated material composition is determined using the + :class:`openmc.deplete.Results` class. Indexing an instance of this class + with the timestep index returns a :class:`~openmc.deplete.StepResult` object, + which itself has a :meth:`~openmc.deplete.StepResult.get_material` method + returning an activated material. +4. The :meth:`openmc.Material.get_decay_photon_energy` method is used to obtain + the energy spectrum of the decay photon source. The integral of the spectrum + also indicates the intensity of the source in units of [Bq]. +5. A new photon source is defined using one of OpenMC's source classes with the + energy distribution set equal to the object returned by the + :meth:`openmc.Material.get_decay_photon_energy` method. The source is then + assigned to a photon :class:`~openmc.Model`. +6. A photon transport calculation is run with ``model.run()``. + +Altogether, the workflow looks as follows:: + + # Run neutron transport calculation + fluxes, micros = openmc.deplete.get_microxs_and_flux(model, domains) + + # Run activation calculation + op = openmc.deplete.IndependentOperator(mats, fluxes, micros) + timesteps = ... + source_rates = ... + integrator = openmc.deplete.Integrator(op, timesteps, source_rates) + integrator.integrate() + + # Get decay photon source at last timestep results = openmc.deplete.Results("depletion_results.h5") - - # Get results at last timestep step = results[-1] - - # Get activated material composition for ID=1 activated_mat = step.get_material('1') - - # Determine photon source photon_energy = activated_mat.get_decay_photon_energy() + photon_source = openmc.IndependentSource( + space=..., + energy=photon_energy, + particle='photon', + strength=photon_energy.integral() + ) -By default, the :meth:`~openmc.Material.get_decay_photon_energy` method will -eliminate spectral lines with very low intensity, but this behavior can be -configured with the ``clip_tolerance`` argument. + # Run photon transport calculation + model.settings.source = photon_source + model.run() + +Note that by default, the :meth:`~openmc.Material.get_decay_photon_energy` +method will eliminate spectral lines with very low intensity, but this behavior +can be configured with the ``clip_tolerance`` argument. + +Cell-based R2S +-------------- + +In practice, users do not need to manually go through each of the steps in an R2S +calculation described above. The :class:`~openmc.deplete.R2SManager` fully +automates the execution of neutron transport, depletion, decay source +generation, and photon transport. For a cell-based R2S calculation, once you +have a :class:`~openmc.Model` that has been defined, simply create an instance +of :class:`~openmc.deplete.R2SManager` by passing the model and a list of cells +to activate:: + + r2s = openmc.deplete.R2SManager(model, [cell1, cell2, cell3]) + +Note that the ``volume`` attribute must be set for any cell that is to be +activated. The :class:`~openmc.deplete.R2SManager` class allows you to +optionally specify a separate photon model; if not given as an argument, it will +create a shallow copy of the original neutron model (available as the +``neutron_model`` attribute) and store it in the ``photon_model`` attribute. We +can use this to define tallies specific to the photon model:: + + dose_tally = openmc.Tally() + ... + r2s.photon_model.tallies = [dose_tally] + +Next, define the timesteps and source rates for the activation calculation:: + + timesteps = [(3.0, 'd'), (5.0, 'h')] + source_rates = [1e12, 0.0] + +In this case, the model is irradiated for 3 days with a source rate of +:math:`10^{12}` neutron/sec and then the source is turned off and the activated +materials are allowed to decay for 5 hours. These parameters should be passed to +the :meth:`~openmc.deplete.R2SManager.run` method to execute the full R2S +calculation. Before we can do that though, for a cell-based calculation, the one +other piece of information that is needed is bounding boxes of the activated +cells:: + + bounding_boxes = { + cell1.id: cell1.bounding_box, + cell2.id: cell2.bounding_box, + cell3.id: cell3.bounding_box + } + +Note that calling the ``bounding_box`` attribute may not work for all +constructive solid geometry regions (for example, a cell that uses a +non-axis-aligned plane). In these cases, the bounding box will need to be +specified manually. Once you have a set of bounding boxes, the R2S calculation +can be run:: + + r2s.run(timesteps, source_rates, bounding_boxes=bounding_boxes) + +If not specified otherwise, a photon transport calculation is run at each time +in the depletion schedule. That means in the case above, we would see three +photon transport calculations. To specify specific times at which photon +transport calculations should be run, pass the ``photon_time_indices`` argument. +For example, if we wanted to run a photon transport calculation only on the last +time (after the 5 hour decay), we would run:: + + r2s.run(timesteps, source_rates, bounding_boxes=bounding_boxes, + photon_time_indices=[2]) + +After an R2S calculation has been run, the :class:`~openmc.deplete.R2SManager` +instance will have a ``results`` dictionary that allows you to directly access +results from each of the steps. It will also write out all the output files into +a directory that is named "r2s_/". The ``output_dir`` argument to the +:meth:`~openmc.deplete.R2SManager.run` method enables you to override the +default output directory name if desired. + +The :meth:`~openmc.deplete.R2SManager.run` method actually runs three +lower-level methods under the hood:: + + r2s.step1_neutron_transport(...) + r2s.step2_activation(...) + r2s.step3_photon_transport(...) + +For users looking for more control over the calculation, these lower-level +methods can be used in lieu of the :meth:`openmc.deplete.R2SManager.run` method. + +Mesh-based R2S +-------------- + +Executing a mesh-based R2S calculation looks nearly identical to the cell-based +R2S workflow described above. The only difference is that instead of passing a +list of cells to the ``domains`` argument of +:class:`~openmc.deplete.R2SManager`, you need to define a mesh object and pass +that instead. This might look like the following:: + + # Define a regular Cartesian mesh + mesh = openmc.RegularMesh() + mesh.lower_left = (-50., -50., 0.) + mesh.upper_right = (50., 50., 75.) + mesh.dimension = (10, 10, 5) + + r2s = openmc.deplete.R2SManager(model, mesh) + +Executing the R2S calculation is then performed by adding photon tallies and +calling the :meth:`~openmc.deplete.R2SManager.run` method with the appropriate +timesteps and source rates. Note that in this case we do not need to define cell +volumes or bounding boxes as is required for a cell-based R2S calculation. +Instead, during the neutron transport step, OpenMC will run a raytracing +calculation to determine material volume fractions within each mesh element +using the :meth:`openmc.MeshBase.material_volumes` method. Arguments to this +method can be customized via the ``mat_vol_kwargs`` argument to the +:meth:`~openmc.deplete.R2SManager.run` method. Most often, this would involve +customizing the number of rays traced to obtain better estimates of volumes. As +an example, if we wanted to run the raytracing calculation with 10 million rays, +we would run:: + + r2s.run(timesteps, source_rates, mat_vol_kwargs={'n_samples': 10_000_000}) Direct 1-Step (D1S) Calculations ================================ diff --git a/openmc/deplete/__init__.py b/openmc/deplete/__init__.py index 8a9509e90..052e22459 100644 --- a/openmc/deplete/__init__.py +++ b/openmc/deplete/__init__.py @@ -17,6 +17,7 @@ from .stepresult import * from .results import * from .integrators import * from .transfer_rates import * +from .r2s import * from . import abc from . import cram from . import helpers diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index 4ce199f0c..e351c923d 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -8,8 +8,9 @@ from __future__ import annotations from collections.abc import Sequence import shutil from tempfile import TemporaryDirectory -from typing import Union, TypeAlias +from typing import Union, TypeAlias, Self +import h5py import pandas as pd import numpy as np @@ -20,6 +21,7 @@ from openmc.data import REACTION_MT import openmc from .chain import Chain, REACTIONS, _get_chain from .coupled_operator import _find_cross_sections, _get_nuclides_with_data +from ..utility_funcs import h5py_file_or_group import openmc.lib from openmc.mpi import comm @@ -47,6 +49,7 @@ def get_microxs_and_flux( reaction_rate_mode: str = 'direct', chain_file: PathLike | Chain | None = None, path_statepoint: PathLike | None = None, + path_input: PathLike | None = None, run_kwargs=None ) -> tuple[list[np.ndarray], list[MicroXS]]: """Generate microscopic cross sections and fluxes for multiple domains. @@ -59,7 +62,7 @@ def get_microxs_and_flux( .. versionadded:: 0.14.0 .. versionchanged:: 0.15.3 - Added `reaction_rate_mode` and `path_statepoint` arguments. + Added `reaction_rate_mode`, `path_statepoint`, `path_input` arguments. Parameters ---------- @@ -90,6 +93,10 @@ def get_microxs_and_flux( Path to write the statepoint file from the neutron transport solve to. By default, The statepoint file is written to a temporary directory and is not kept. + path_input : path-like, optional + Path to write the model XML file from the neutron transport solve to. + By default, the model XML file is written to a temporary directory and + not kept. run_kwargs : dict, optional Keyword arguments passed to :meth:`openmc.Model.run` @@ -108,7 +115,7 @@ def get_microxs_and_flux( check_value('reaction_rate_mode', reaction_rate_mode, {'direct', 'flux'}) # Save any original tallies on the model - original_tallies = model.tallies + original_tallies = list(model.tallies) # Determine what reactions and nuclides are available in chain chain = _get_chain(chain_file) @@ -178,6 +185,10 @@ def get_microxs_and_flux( shutil.move(statepoint_path, path_statepoint) statepoint_path = path_statepoint + # Export the model to path_input if provided + if path_input is not None: + model.export_to_model_xml(path_input) + with StatePoint(statepoint_path) as sp: if reaction_rate_mode == 'direct': rr_tally = sp.tallies[rr_tally.id] @@ -383,8 +394,7 @@ class MicroXS: MicroXS """ - if 'float_precision' not in kwargs: - kwargs['float_precision'] = 'round_trip' + kwargs.setdefault('float_precision', 'round_trip') df = pd.read_csv(csv_file, **kwargs) df.set_index(['nuclides', 'reactions', 'groups'], inplace=True) @@ -419,3 +429,96 @@ class MicroXS: ) df = pd.DataFrame({'xs': self.data.flatten()}, index=multi_index) df.to_csv(*args, **kwargs) + + def to_hdf5(self, group_or_filename: h5py.Group | PathLike, **kwargs): + """Export microscopic cross section data to HDF5 format + + Parameters + ---------- + group_or_filename : h5py.Group or path-like + HDF5 group or filename to write to + kwargs : dict, optional + Keyword arguments to pass to :meth:`h5py.Group.create_dataset`. + Defaults to {'compression': 'lzf'}. + + """ + kwargs.setdefault('compression', 'lzf') + + with h5py_file_or_group(group_or_filename, 'w') as group: + # Store cross section data as 3D dataset + group.create_dataset('data', data=self.data, **kwargs) + + # Store metadata as datasets using string encoding + group.create_dataset('nuclides', data=np.array(self.nuclides, dtype='S')) + group.create_dataset('reactions', data=np.array(self.reactions, dtype='S')) + + @classmethod + def from_hdf5(cls, group_or_filename: h5py.Group | PathLike) -> Self: + """Load data from an HDF5 file + + Parameters + ---------- + group_or_filename : h5py.Group or str or PathLike + HDF5 group or path to HDF5 file. If given as an h5py.Group, the + data is read from that group. If given as a string, it is assumed + to be the filename for the HDF5 file. + + Returns + ------- + MicroXS + """ + + with h5py_file_or_group(group_or_filename, 'r') as group: + # Read data from HDF5 group + data = group['data'][:] + nuclides = [nuc.decode('utf-8') for nuc in group['nuclides'][:]] + reactions = [rxn.decode('utf-8') for rxn in group['reactions'][:]] + + return cls(data, nuclides, reactions) + + +def write_microxs_hdf5( + micros: Sequence[MicroXS], + filename: PathLike, + names: Sequence[str] | None = None, + **kwargs +): + """Write multiple MicroXS objects to an HDF5 file + + Parameters + ---------- + micros : list of MicroXS + List of MicroXS objects + filename : PathLike + Output HDF5 filename + names : list of str, optional + Names for each MicroXS object. If None, uses 'domain_0', 'domain_1', + etc. + **kwargs + Additional keyword arguments passed to :meth:`h5py.Group.create_dataset` + """ + if names is None: + names = [f'domain_{i}' for i in range(len(micros))] + + # Open file once and write all domains using group interface + with h5py.File(filename, 'w') as f: + for microxs, name in zip(micros, names): + group = f.create_group(name) + microxs.to_hdf5(group, **kwargs) + + +def read_microxs_hdf5(filename: PathLike) -> dict[str, MicroXS]: + """Read multiple MicroXS objects from an HDF5 file + + Parameters + ---------- + filename : path-like + HDF5 filename + + Returns + ------- + dict + Dictionary mapping domain names to MicroXS objects + """ + with h5py.File(filename, 'r') as f: + return {name: MicroXS.from_hdf5(group) for name, group in f.items()} diff --git a/openmc/deplete/r2s.py b/openmc/deplete/r2s.py new file mode 100644 index 000000000..7b5deddc2 --- /dev/null +++ b/openmc/deplete/r2s.py @@ -0,0 +1,680 @@ +from __future__ import annotations +from collections.abc import Sequence +import copy +from datetime import datetime +import json +from pathlib import Path + +import numpy as np +import openmc +from . import IndependentOperator, PredictorIntegrator +from .microxs import get_microxs_and_flux, write_microxs_hdf5, read_microxs_hdf5 +from .results import Results +from ..checkvalue import PathLike + + +def get_activation_materials( + model: openmc.Model, mmv: openmc.MeshMaterialVolumes +) -> openmc.Materials: + """Get a list of activation materials for each mesh element/material. + + When performing a mesh-based R2S calculation, a unique material is needed + for each activation region, which is a combination of a mesh element and a + material within that mesh element. This function generates a list of such + materials, each with a unique name and volume corresponding to the mesh + element and material. + + Parameters + ---------- + model : openmc.Model + The full model containing the geometry and materials. + mmv : openmc.MeshMaterialVolumes + The mesh material volumes object containing the materials and their + volumes for each mesh element. + + Returns + ------- + openmc.Materials + A list of materials, each corresponding to a unique mesh element and + material combination. + + """ + # Get the material ID, volume, and element index for each element-material + # combination + mat_ids = mmv._materials[mmv._materials > -1] + volumes = mmv._volumes[mmv._materials > -1] + elems, _ = np.where(mmv._materials > -1) + + # Get all materials in the model + material_dict = model._get_all_materials() + + # Create a new activation material for each element-material combination + materials = openmc.Materials() + for elem, mat_id, vol in zip(elems, mat_ids, volumes): + mat = material_dict[mat_id] + new_mat = mat.clone() + new_mat.depletable = True + new_mat.name = f'Element {elem}, Material {mat_id}' + new_mat.volume = vol + materials.append(new_mat) + + return materials + + +class R2SManager: + """Manager for Rigorous 2-Step (R2S) method calculations. + + This class is responsible for managing the materials and sources needed for + mesh-based or cell-based R2S calculations. It provides methods to get + activation materials and decay photon sources based on the mesh/cells and + materials in the OpenMC model. + + This class supports the use of a different models for the neutron and photon + transport calculation. However, for cell-based calculations, it assumes that + the only changes in the model are material assignments. For mesh-based + calculations, it checks material assignments in the photon model and any + element--material combinations that don't appear in the photon model are + skipped. + + Parameters + ---------- + neutron_model : openmc.Model + The OpenMC model to use for neutron transport. + domains : openmc.MeshBase or Sequence[openmc.Cell] + The mesh or a sequence of cells that represent the spatial units over + which the R2S calculation will be performed. + photon_model : openmc.Model, optional + The OpenMC model to use for photon transport calculations. If None, a + shallow copy of the neutron_model will be created and used. + + Attributes + ---------- + domains : openmc.MeshBase or Sequence[openmc.Cell] + The mesh or a sequence of cells that represent the spatial units over + which the R2S calculation will be performed. + neutron_model : openmc.Model + The OpenMC model used for neutron transport. + photon_model : openmc.Model + The OpenMC model used for photon transport calculations. + method : {'mesh-based', 'cell-based'} + Indicates whether the R2S calculation uses mesh elements ('mesh-based') + as the spatial discetization or a list of a cells ('cell-based'). + results : dict + A dictionary that stores results from the R2S calculation. + + """ + def __init__( + self, + neutron_model: openmc.Model, + domains: openmc.MeshBase | Sequence[openmc.Cell], + photon_model: openmc.Model | None = None, + ): + self.neutron_model = neutron_model + if photon_model is None: + # Create a shallow copy of the neutron model for photon transport + self.photon_model = openmc.Model( + geometry=copy.copy(neutron_model.geometry), + materials=copy.copy(neutron_model.materials), + settings=copy.copy(neutron_model.settings), + tallies=copy.copy(neutron_model.tallies), + plots=copy.copy(neutron_model.plots), + ) + else: + self.photon_model = photon_model + if isinstance(domains, openmc.MeshBase): + self.method = 'mesh-based' + else: + self.method = 'cell-based' + self.domains = domains + self.results = {} + + def run( + self, + timesteps: Sequence[float] | Sequence[tuple[float, str]], + source_rates: float | Sequence[float], + timestep_units: str = 's', + photon_time_indices: Sequence[int] | None = None, + output_dir: PathLike | None = None, + bounding_boxes: dict[int, openmc.BoundingBox] | None = None, + chain_file: PathLike | None = None, + micro_kwargs: dict | None = None, + mat_vol_kwargs: dict | None = None, + run_kwargs: dict | None = None, + operator_kwargs: dict | None = None, + ): + """Run the R2S calculation. + + Parameters + ---------- + timesteps : Sequence[float] or Sequence[tuple[float, str]] + Sequence of timesteps. Note that values are not cumulative. The + units are specified by the `timestep_units` argument when + `timesteps` is an iterable of float. Alternatively, units can be + specified for each step by passing an iterable of (value, unit) + tuples. + source_rates : float or Sequence[float] + Source rate in [neutron/sec] for each interval in `timesteps`. + timestep_units : {'s', 'min', 'h', 'd', 'a'}, optional + Units for values specified in the `timesteps` argument when passing + float values. 's' means seconds, 'min' means minutes, 'h' means + hours, 'd' means days, and 'a' means years (Julian). + photon_time_indices : Sequence[int], optional + Sequence of time indices at which photon transport should be run; + represented as indices into the array of times formed by the + timesteps. For example, if two timesteps are specified, the array of + times would contain three entries, and [2] would indicate computing + photon results at the last time. A value of None indicates to run + photon transport for each time. + output_dir : PathLike, optional + Path to directory where R2S calculation outputs will be saved. If + not provided, a timestamped directory 'r2s_YYYY-MM-DDTHH-MM-SS' is + created. Subdirectories will be created for the neutron transport, + activation, and photon transport steps. + bounding_boxes : dict[int, openmc.BoundingBox], optional + Dictionary mapping cell IDs to bounding boxes used for spatial + source sampling in cell-based R2S calculations. Required if method + is 'cell-based'. + chain_file : PathLike, optional + Path to the depletion chain XML file to use during activation. If + not provided, the default configured chain file will be used. + micro_kwargs : dict, optional + Additional keyword arguments passed to + :func:`openmc.deplete.get_microxs_and_flux` during the neutron + transport step. + mat_vol_kwargs : dict, optional + Additional keyword arguments passed to + :meth:`openmc.MeshBase.material_volumes`. + run_kwargs : dict, optional + Additional keyword arguments passed to :meth:`openmc.Model.run` + during the neutron and photon transport step. By default, output is + disabled. + operator_kwargs : dict, optional + Additional keyword arguments passed to + :class:`openmc.deplete.IndependentOperator`. + + Returns + ------- + Path + Path to the output directory containing all calculation results + """ + + if output_dir is None: + stamp = datetime.now().strftime('%Y-%m-%dT%H-%M-%S') + output_dir = Path(f'r2s_{stamp}') + + # Set run_kwargs for the neutron transport step + if micro_kwargs is None: + micro_kwargs = {} + if run_kwargs is None: + run_kwargs = {} + if operator_kwargs is None: + operator_kwargs = {} + run_kwargs.setdefault('output', False) + micro_kwargs.setdefault('run_kwargs', run_kwargs) + # If a chain file is provided, prefer it for steps 1 and 2 + if chain_file is not None: + micro_kwargs.setdefault('chain_file', chain_file) + operator_kwargs.setdefault('chain_file', chain_file) + + self.step1_neutron_transport( + output_dir / 'neutron_transport', mat_vol_kwargs, micro_kwargs + ) + self.step2_activation( + timesteps, source_rates, timestep_units, output_dir / 'activation', + operator_kwargs=operator_kwargs + ) + self.step3_photon_transport( + photon_time_indices, bounding_boxes, output_dir / 'photon_transport', + mat_vol_kwargs=mat_vol_kwargs, run_kwargs=run_kwargs + ) + + return output_dir + + def step1_neutron_transport( + self, + output_dir: PathLike = "neutron_transport", + mat_vol_kwargs: dict | None = None, + micro_kwargs: dict | None = None + ): + """Run the neutron transport step. + + This step computes the material volume fractions on the mesh, creates a + mesh-material filter, and retrieves the fluxes and microscopic cross + sections for each mesh/material combination. This step will populate the + 'fluxes' and 'micros' keys in the results dictionary. For a mesh-based + calculation, it will also populate the 'mesh_material_volumes' key. + + Parameters + ---------- + output_dir : PathLike, optional + The directory where the results will be saved. + mat_vol_kwargs : dict, optional + Additional keyword arguments based to + :meth:`openmc.MeshBase.material_volumes`. + micro_kwargs : dict, optional + Additional keyword arguments passed to + :func:`openmc.deplete.get_microxs_and_flux`. + + """ + + output_dir = Path(output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + if self.method == 'mesh-based': + # Compute material volume fractions on the mesh + if mat_vol_kwargs is None: + mat_vol_kwargs = {} + self.results['mesh_material_volumes'] = mmv = \ + self.domains.material_volumes(self.neutron_model, **mat_vol_kwargs) + + # Save results to file + mmv.save(output_dir / 'mesh_material_volumes.npz') + + # Create mesh-material filter based on what combos were found + domains = openmc.MeshMaterialFilter.from_volumes(self.domains, mmv) + else: + domains: Sequence[openmc.Cell] = self.domains + + # Check to make sure that each cell is filled with a material and + # that the volume has been set + + # TODO: If volumes are not set, run volume calculation for cells + for cell in domains: + if cell.fill is None: + raise ValueError( + f"Cell {cell.id} is not filled with a materials. " + "Please set the fill material for each cell before " + "running the R2S calculation." + ) + if cell.volume is None: + raise ValueError( + f"Cell {cell.id} does not have a volume set. " + "Please set the volume for each cell before running " + "the R2S calculation." + ) + + # Set default keyword arguments for microxs and flux calculation + if micro_kwargs is None: + micro_kwargs = {} + micro_kwargs.setdefault('path_statepoint', output_dir / 'statepoint.h5') + micro_kwargs.setdefault('path_input', output_dir / 'model.xml') + + # Run neutron transport and get fluxes and micros + self.results['fluxes'], self.results['micros'] = get_microxs_and_flux( + self.neutron_model, domains, **micro_kwargs) + + # Save flux and micros to file + np.save(output_dir / 'fluxes.npy', self.results['fluxes']) + write_microxs_hdf5(self.results['micros'], output_dir / 'micros.h5') + + def step2_activation( + self, + timesteps: Sequence[float] | Sequence[tuple[float, str]], + source_rates: float | Sequence[float], + timestep_units: str = 's', + output_dir: PathLike = 'activation', + operator_kwargs: dict | None = None, + ): + """Run the activation step. + + This step creates a unique copy of each activation material based on the + mesh elements or cells, then solves the depletion equations for each + material using the fluxes and microscopic cross sections obtained in the + neutron transport step. This step will populate the 'depletion_results' + and 'activation_materials' keys in the results dictionary. + + Parameters + ---------- + timesteps : Sequence[float] or Sequence[tuple[float, str]] + Sequence of timesteps. Note that values are not cumulative. The + units are specified by the `timestep_units` argument when + `timesteps` is an iterable of float. Alternatively, units can be + specified for each step by passing an iterable of (value, unit) + tuples. + source_rates : float | Sequence[float] + Source rate in [neutron/sec] for each interval in `timesteps`. + timestep_units : {'s', 'min', 'h', 'd', 'a'}, optional + Units for values specified in the `timesteps` argument when passing + float values. 's' means seconds, 'min' means minutes, 'h' means + hours, 'd' means days, and 'a' means years (Julian). + output_dir : PathLike, optional + Path to directory where activation calculation outputs will be + saved. + operator_kwargs : dict, optional + Additional keyword arguments passed to + :class:`openmc.deplete.IndependentOperator`. + """ + + if self.method == 'mesh-based': + # Get unique material for each (mesh, material) combination + mmv = self.results['mesh_material_volumes'] + self.results['activation_materials'] = get_activation_materials(self.neutron_model, mmv) + else: + # Create unique material for each cell + activation_mats = openmc.Materials() + for cell in self.domains: + mat = cell.fill.clone() + mat.name = f'Cell {cell.id}' + mat.depletable = True + mat.volume = cell.volume + activation_mats.append(mat) + self.results['activation_materials'] = activation_mats + + # Save activation materials to file + output_dir = Path(output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + self.results['activation_materials'].export_to_xml( + output_dir / 'materials.xml') + + # Create depletion operator for the activation materials + if operator_kwargs is None: + operator_kwargs = {} + operator_kwargs.setdefault('normalization_mode', 'source-rate') + op = IndependentOperator( + self.results['activation_materials'], + self.results['fluxes'], + self.results['micros'], + **operator_kwargs + ) + + # Create time integrator and solve depletion equations + integrator = PredictorIntegrator( + op, timesteps, source_rates=source_rates, timestep_units=timestep_units + ) + output_path = output_dir / 'depletion_results.h5' + integrator.integrate(final_step=False, path=output_path) + + # Get depletion results + self.results['depletion_results'] = Results(output_path) + + def step3_photon_transport( + self, + time_indices: Sequence[int] | None = None, + bounding_boxes: dict[int, openmc.BoundingBox] | None = None, + output_dir: PathLike = 'photon_transport', + mat_vol_kwargs: dict | None = None, + run_kwargs: dict | None = None, + ): + """Run the photon transport step. + + This step performs photon transport calculations using decay photon + sources created from the activated materials. For each specified time, + it creates appropriate photon sources and runs a transport calculation. + In mesh-based mode, the sources are created using the mesh material + volumes, while in cell-based mode, they are created using bounding boxes + for each cell. This step will populate the 'photon_tallies' key in the + results dictionary. + + Parameters + ---------- + time_indices : Sequence[int], optional + Sequence of time indices at which photon transport should be run; + represented as indices into the array of times formed by the + timesteps. For example, if two timesteps are specified, the array of + times would contain three entries, and [2] would indicate computing + photon results at the last time. A value of None indicates to run + photon transport for each time. + bounding_boxes : dict[int, openmc.BoundingBox], optional + Dictionary mapping cell IDs to bounding boxes used for spatial + source sampling in cell-based R2S calculations. Required if method + is 'cell-based'. + output_dir : PathLike, optional + Path to directory where photon transport outputs will be saved. + mat_vol_kwargs : dict, optional + Additional keyword arguments passed to + :meth:`openmc.MeshBase.material_volumes`. + run_kwargs : dict, optional + Additional keyword arguments passed to :meth:`openmc.Model.run` + during the photon transport step. By default, output is disabled. + """ + + # TODO: Automatically determine bounding box for each cell + if bounding_boxes is None and self.method == 'cell-based': + raise ValueError("bounding_boxes must be provided for cell-based " + "R2S calculations.") + + # Set default run arguments if not provided + if run_kwargs is None: + run_kwargs = {} + run_kwargs.setdefault('output', False) + + # Write out JSON file with tally IDs that can be used for loading + # results + output_dir = Path(output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + # Get default time indices if not provided + if time_indices is None: + n_steps = len(self.results['depletion_results']) + time_indices = list(range(n_steps)) + + # Check whether the photon model is different + neutron_univ = self.neutron_model.geometry.root_universe + photon_univ = self.photon_model.geometry.root_universe + different_photon_model = (neutron_univ != photon_univ) + + # For mesh-based calculations, compute material volume fractions for the + # photon model if it is different from the neutron model to account for + # potential material changes + if self.method == 'mesh-based' and different_photon_model: + self.results['mesh_material_volumes_photon'] = photon_mmv = \ + self.domains.material_volumes(self.photon_model, **mat_vol_kwargs) + + # Save photon MMV results to file + photon_mmv.save(output_dir / 'mesh_material_volumes.npz') + + tally_ids = [tally.id for tally in self.photon_model.tallies] + with open(output_dir / 'tally_ids.json', 'w') as f: + json.dump(tally_ids, f) + + self.results['photon_tallies'] = {} + + # Get dictionary of cells in the photon model + if different_photon_model: + photon_cells = self.photon_model.geometry.get_all_cells() + + for time_index in time_indices: + # Create decay photon source + if self.method == 'mesh-based': + self.photon_model.settings.source = \ + self.get_decay_photon_source_mesh(time_index) + else: + sources = [] + results = self.results['depletion_results'] + for cell, original_mat in zip(self.domains, self.results['activation_materials']): + # Skip if the cell is not in the photon model or the + # material has changed + if different_photon_model: + if cell.id not in photon_cells or \ + cell.fill.id != photon_cells[cell.id].fill.id: + continue + + # Get bounding box for the cell + bounding_box = bounding_boxes[cell.id] + + # Get activated material composition + activated_mat = results[time_index].get_material(str(original_mat.id)) + + # Create decay photon source source + space = openmc.stats.Box(*bounding_box) + energy = activated_mat.get_decay_photon_energy() + strength = energy.integral() if energy is not None else 0.0 + source = openmc.IndependentSource( + space=space, + energy=energy, + particle='photon', + strength=strength, + constraints={'domains': [cell]} + ) + sources.append(source) + self.photon_model.settings.source = sources + + # Convert time_index (which may be negative) to a normal index + if time_index < 0: + time_index = len(self.results['depletion_results']) + time_index + + # Run photon transport calculation + run_kwargs['cwd'] = Path(output_dir) / f'time_{time_index}' + statepoint_path = self.photon_model.run(**run_kwargs) + + # Store tally results + with openmc.StatePoint(statepoint_path) as sp: + self.results['photon_tallies'][time_index] = [ + sp.tallies[tally.id] for tally in self.photon_model.tallies + ] + + def get_decay_photon_source_mesh( + self, + time_index: int = -1 + ) -> list[openmc.MeshSource]: + """Create decay photon source for a mesh-based calculation. + + This function creates N :class:`MeshSource` objects where N is the + maximum number of unique materials that appears in a single mesh + element. For each mesh element-material combination, and + IndependentSource instance is created with a spatial constraint limited + the sampled decay photons to the correct region. + + When the photon transport model is different from the neutron model, the + photon MeshMaterialVolumes is used to determine whether an (element, + material) combination exists in the photon model. + + Parameters + ---------- + time_index : int, optional + Time index for the decay photon source. Default is -1 (last time). + + Returns + ------- + list of openmc.MeshSource + A list of MeshSource objects, each containing IndependentSource + instances for the decay photons in the corresponding mesh element. + + """ + mat_dict = self.neutron_model._get_all_materials() + + # Some MeshSource objects will have empty positions; create a "null source" + # that is used for this case + null_source = openmc.IndependentSource(particle='photon', strength=0.0) + + # List to hold sources for each MeshSource (length = N) + source_lists = [] + + # Index in the overall list of activated materials + index_mat = 0 + + # Get various results from previous steps + mat_vols = self.results['mesh_material_volumes'] + materials = self.results['activation_materials'] + results = self.results['depletion_results'] + photon_mat_vols = self.results.get('mesh_material_volumes_photon') + + # Total number of mesh elements + n_elements = mat_vols.num_elements + + for index_elem in range(n_elements): + # Determine which materials exist in the photon model for this element + if photon_mat_vols is not None: + photon_materials = { + mat_id + for mat_id, _ in photon_mat_vols.by_element(index_elem) + if mat_id is not None + } + + for j, (mat_id, _) in enumerate(mat_vols.by_element(index_elem)): + # Skip void volume + if mat_id is None: + continue + + # Skip if this material doesn't exist in photon model + if photon_mat_vols is not None and mat_id not in photon_materials: + index_mat += 1 + continue + + # Check whether a new MeshSource object is needed + if j >= len(source_lists): + source_lists.append([null_source]*n_elements) + + # Get activated material composition + original_mat = materials[index_mat] + activated_mat = results[time_index].get_material(str(original_mat.id)) + + # Create decay photon source source + energy = activated_mat.get_decay_photon_energy() + if energy is not None: + strength = energy.integral() + source_lists[j][index_elem] = openmc.IndependentSource( + energy=energy, + particle='photon', + strength=strength, + constraints={'domains': [mat_dict[mat_id]]} + ) + + # Increment index of activated material + index_mat += 1 + + # Return list of mesh sources + return [openmc.MeshSource(self.domains, sources) for sources in source_lists] + + def load_results(self, path: PathLike): + """Load results from a previous R2S calculation. + + Parameters + ---------- + path : PathLike + Path to the directory containing the R2S calculation results. + + """ + path = Path(path) + + # Load neutron transport results + neutron_dir = path / 'neutron_transport' + if self.method == 'mesh-based': + mmv_file = neutron_dir / 'mesh_material_volumes.npz' + if mmv_file.exists(): + self.results['mesh_material_volumes'] = \ + openmc.MeshMaterialVolumes.from_npz(mmv_file) + fluxes_file = neutron_dir / 'fluxes.npy' + if fluxes_file.exists(): + self.results['fluxes'] = list(np.load(fluxes_file, allow_pickle=True)) + micros_dict = read_microxs_hdf5(neutron_dir / 'micros.h5') + self.results['micros'] = [ + micros_dict[f'domain_{i}'] for i in range(len(micros_dict)) + ] + + # Load activation results + activation_dir = path / 'activation' + activation_results = activation_dir / 'depletion_results.h5' + if activation_results.exists(): + self.results['depletion_results'] = Results(activation_results) + activation_mats_file = activation_dir / 'materials.xml' + if activation_mats_file.exists(): + self.results['activation_materials'] = \ + openmc.Materials.from_xml(activation_mats_file) + + # Load photon transport results + photon_dir = path / 'photon_transport' + + # Load photon mesh material volumes if they exist (for mesh-based calculations) + if self.method == 'mesh-based': + photon_mmv_file = photon_dir / 'mesh_material_volumes.npz' + if photon_mmv_file.exists(): + self.results['mesh_material_volumes_photon'] = \ + openmc.MeshMaterialVolumes.from_npz(photon_mmv_file) + + # Load tally IDs from JSON file + tally_ids_path = photon_dir / 'tally_ids.json' + if tally_ids_path.exists(): + with tally_ids_path.open('r') as f: + tally_ids = json.load(f) + self.results['photon_tallies'] = {} + + # For each photon transport calc, load the statepoint and get the + # tally results based on tally_ids + for time_dir in photon_dir.glob('time_*'): + time_index = int(time_dir.name.split('_')[1]) + for sp_path in time_dir.glob('statepoint.*.h5'): + with openmc.StatePoint(sp_path) as sp: + self.results['photon_tallies'][time_index] = [ + sp.tallies[tally_id] for tally_id in tally_ids + ] diff --git a/openmc/mesh.py b/openmc/mesh.py index 9601207e9..3d93d87a1 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -325,20 +325,10 @@ class MeshBase(IDManagerMixin, ABC): vols = material_volumes mat_volume_by_element = [vols.by_element(i) for i in range(vols.num_elements)] + # Get dictionary of all materials + materials = model._get_all_materials() + # Create homogenized material for each element - materials = model.geometry.get_all_materials() - - # Account for materials in DAGMC universes - # TODO: This should really get incorporated in lower-level calls to - # get_all_materials, but right now it requires information from the - # Model object - for cell in model.geometry.get_all_cells().values(): - if isinstance(cell.fill, openmc.DAGMCUniverse): - names = cell.fill.material_names - materials.update({ - mat.id: mat for mat in model.materials if mat.name in names - }) - homogenized_materials = [] for mat_volume_list in mat_volume_by_element: material_ids, volumes = [list(x) for x in zip(*mat_volume_list)] @@ -410,7 +400,7 @@ class MeshBase(IDManagerMixin, ABC): # In order to get mesh into model, we temporarily replace the # tallies with a single mesh tally using the current mesh - original_tallies = model.tallies + original_tallies = list(model.tallies) new_tally = openmc.Tally() new_tally.filters = [openmc.MeshFilter(self)] new_tally.scores = ['flux'] diff --git a/openmc/model/model.py b/openmc/model/model.py index 7963751d5..64294c23c 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -212,6 +212,29 @@ class Model: result[mat.name].add(mat) return result + # TODO: This should really get incorporated in lower-level calls to + # get_all_materials, but right now it requires information from the Model object + def _get_all_materials(self) -> dict[int, openmc.Material]: + """Get all materials including those in DAGMC universes + + Returns + ------- + dict + Dictionary mapping material ID to material instances + """ + # Get all materials from the Geometry object + materials = self.geometry.get_all_materials() + + # Account for materials in DAGMC universes + for cell in self.geometry.get_all_cells().values(): + if isinstance(cell.fill, openmc.DAGMCUniverse): + names = cell.fill.material_names + materials.update({ + mat.id: mat for mat in self.materials if mat.name in names + }) + + return materials + def add_kinetics_parameters_tallies(self, num_groups: int | None = None): """Add tallies for calculating kinetics parameters using the IFP method. diff --git a/openmc/utility_funcs.py b/openmc/utility_funcs.py index da9f73b16..935a58985 100644 --- a/openmc/utility_funcs.py +++ b/openmc/utility_funcs.py @@ -3,6 +3,8 @@ import os from pathlib import Path from tempfile import TemporaryDirectory +import h5py + import openmc from .checkvalue import PathLike @@ -57,3 +59,20 @@ def input_path(filename: PathLike) -> Path: return Path(filename).resolve() else: return Path(filename) + + +@contextmanager +def h5py_file_or_group(group_or_filename: PathLike | h5py.Group, *args, **kwargs): + """Context manager for opening an HDF5 file or using an existing group + + Parameters + ---------- + group_or_filename : path-like or h5py.Group + Path to HDF5 file, or group from an existing HDF5 file + + """ + if isinstance(group_or_filename, h5py.Group): + yield group_or_filename + else: + with h5py.File(group_or_filename, *args, **kwargs) as f: + yield f diff --git a/tests/unit_tests/test_r2s.py b/tests/unit_tests/test_r2s.py new file mode 100644 index 000000000..a94f85c8c --- /dev/null +++ b/tests/unit_tests/test_r2s.py @@ -0,0 +1,152 @@ +from pathlib import Path + +import pytest +import openmc +from openmc.deplete import Chain, R2SManager + + +@pytest.fixture +def simple_model_and_mesh(tmp_path): + # Define two materials: water and Ni + h2o = openmc.Material() + h2o.add_nuclide("H1", 2.0) + h2o.add_nuclide("O16", 1.0) + h2o.set_density("g/cm3", 1.0) + nickel = openmc.Material() + nickel.add_element("Ni", 1.0) + nickel.set_density("g/cm3", 4.0) + + # Geometry: two half-spaces split by x=0 plane + left = openmc.XPlane(0.0) + x_min = openmc.XPlane(-10.0, boundary_type='vacuum') + x_max = openmc.XPlane(10.0, boundary_type='vacuum') + y_min = openmc.YPlane(-10.0, boundary_type='vacuum') + y_max = openmc.YPlane(10.0, boundary_type='vacuum') + z_min = openmc.ZPlane(-10.0, boundary_type='vacuum') + z_max = openmc.ZPlane(10.0, boundary_type='vacuum') + + c1 = openmc.Cell(fill=h2o, region=+x_min & -left & +y_min & -y_max & +z_min & -z_max) + c2 = openmc.Cell(fill=nickel, region=+left & -x_max & +y_min & -y_max & +z_min & -z_max) + c1.volume = 4000.0 + c2.volume = 4000.0 + geometry = openmc.Geometry([c1, c2]) + + # Simple settings with a point source + settings = openmc.Settings() + settings.batches = 10 + settings.particles = 1000 + settings.run_mode = 'fixed source' + settings.source = openmc.IndependentSource() + model = openmc.Model(geometry, settings=settings) + + mesh = openmc.RegularMesh() + mesh.lower_left = (-10.0, -10.0, -10.0) + mesh.upper_right = (10.0, 10.0, 10.0) + mesh.dimension = (1, 1, 1) + return model, (c1, c2), mesh + + +def test_r2s_mesh_expected_output(simple_model_and_mesh, tmp_path): + model, (c1, c2), mesh = simple_model_and_mesh + + # Use mesh-based domains + r2s = R2SManager(model, mesh) + + # Use custom reduced chain file for Ni + chain = Chain.from_xml(Path(__file__).parents[1] / "chain_ni.xml") + + # Run R2S calculation + outdir = r2s.run( + timesteps=[(1.0, 'd')], + source_rates=[1.0], + photon_time_indices=[1], + output_dir=tmp_path, + chain_file=chain, + ) + + # Check directories and files exist + nt = Path(outdir) / 'neutron_transport' + assert (nt / 'fluxes.npy').exists() + assert (nt / 'micros.h5').exists() + assert (nt / 'mesh_material_volumes.npz').exists() + act = Path(outdir) / 'activation' + assert (act / 'depletion_results.h5').exists() + pt = Path(outdir) / 'photon_transport' + assert (pt / 'tally_ids.json').exists() + assert (pt / 'time_1' / 'statepoint.10.h5').exists() + + # Basic results structure checks + assert len(r2s.results['fluxes']) == 2 + assert len(r2s.results['micros']) == 2 + assert len(r2s.results['mesh_material_volumes']) == 2 + assert len(r2s.results['activation_materials']) == 2 + assert len(r2s.results['depletion_results']) == 2 + + # Check activation materials + amats = r2s.results['activation_materials'] + assert all(m.depletable for m in amats) + # Volumes preserved + assert {m.volume for m in amats} == {c1.volume, c2.volume} + + # Check loading results + r2s_loaded = R2SManager(model, mesh) + r2s_loaded.load_results(outdir) + assert len(r2s_loaded.results['fluxes']) == 2 + assert len(r2s_loaded.results['micros']) == 2 + assert len(r2s_loaded.results['mesh_material_volumes']) == 2 + assert len(r2s_loaded.results['activation_materials']) == 2 + assert len(r2s_loaded.results['depletion_results']) == 2 + + +def test_r2s_cell_expected_output(simple_model_and_mesh, tmp_path): + model, (c1, c2), _ = simple_model_and_mesh + + # Use cell-based domains + r2s = R2SManager(model, [c1, c2]) + + # Use custom reduced chain file for Ni + chain = Chain.from_xml(Path(__file__).parents[1] / "chain_ni.xml") + + # Run R2S calculation + bounding_boxes = {c1.id: c1.bounding_box, c2.id: c2.bounding_box} + outdir = r2s.run( + timesteps=[(1.0, 'd')], + source_rates=[1.0], + photon_time_indices=[1], + output_dir=tmp_path, + bounding_boxes=bounding_boxes, + chain_file=chain + ) + + # Check directories and files exist + nt = Path(outdir) / 'neutron_transport' + assert (nt / 'fluxes.npy').exists() + assert (nt / 'micros.h5').exists() + act = Path(outdir) / 'activation' + assert (act / 'depletion_results.h5').exists() + pt = Path(outdir) / 'photon_transport' + assert (pt / 'tally_ids.json').exists() + assert (pt / 'time_1' / 'statepoint.10.h5').exists() + + # Basic results structure checks + assert len(r2s.results['fluxes']) == 2 + assert len(r2s.results['micros']) == 2 + assert len(r2s.results['activation_materials']) == 2 + assert len(r2s.results['depletion_results']) == 2 + + # Check activation materials + amats = r2s.results['activation_materials'] + assert all(m.depletable for m in amats) + # Names include cell IDs + assert any(f"Cell {c1.id}" in m.name for m in amats) + assert any(f"Cell {c2.id}" in m.name for m in amats) + # Volumes preserved + assert {m.volume for m in amats} == {c1.volume, c2.volume} + + # Check loading results + r2s_loaded = R2SManager(model, [c1, c2]) + r2s_loaded.load_results(outdir) + assert len(r2s_loaded.results['fluxes']) == 2 + assert len(r2s_loaded.results['micros']) == 2 + assert len(r2s_loaded.results['activation_materials']) == 2 + assert len(r2s_loaded.results['depletion_results']) == 2 From fd964bc9b04dcf2090b212b8307551b32fea810b Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Mon, 3 Nov 2025 09:42:59 +0200 Subject: [PATCH 440/671] Enable specifying reference direction for azimuthal angle in PolarAzimuthal distribution (#3582) Co-authored-by: shimwell --- include/openmc/distribution_multi.h | 2 ++ openmc/stats/multivariate.py | 25 +++++++++++++++- src/distribution_multi.cpp | 15 +++++++++- tests/regression_tests/source/inputs_true.dat | 2 +- .../regression_tests/source/results_true.dat | 2 +- tests/unit_tests/test_stats.py | 30 +++++++++++++++++++ 6 files changed, 72 insertions(+), 4 deletions(-) diff --git a/include/openmc/distribution_multi.h b/include/openmc/distribution_multi.h index 9e84d03d5..75126593f 100644 --- a/include/openmc/distribution_multi.h +++ b/include/openmc/distribution_multi.h @@ -51,6 +51,8 @@ public: Distribution* phi() const { return phi_.get(); } private: + Direction v_ref_ {1.0, 0.0, 0.0}; //!< reference direction + Direction w_ref_; UPtrDist mu_; //!< Distribution of polar angle UPtrDist phi_; //!< Distribution of azimuthal angle }; diff --git a/openmc/stats/multivariate.py b/openmc/stats/multivariate.py index 222d2d18a..1ce998758 100644 --- a/openmc/stats/multivariate.py +++ b/openmc/stats/multivariate.py @@ -79,6 +79,9 @@ class PolarAzimuthal(UnitSphere): reference_uvw : Iterable of float Direction from which polar angle is measured. Defaults to the positive z-direction. + reference_vwu : Iterable of float + Direction from which azimuthal angle is measured. Defaults to the positive + x-direction. Attributes ---------- @@ -89,8 +92,9 @@ class PolarAzimuthal(UnitSphere): """ - def __init__(self, mu=None, phi=None, reference_uvw=(0., 0., 1.)): + def __init__(self, mu=None, phi=None, reference_uvw=(0., 0., 1.), reference_vwu=(1., 0., 0.)): super().__init__(reference_uvw) + self.reference_vwu = reference_vwu if mu is not None: self.mu = mu else: @@ -100,6 +104,20 @@ class PolarAzimuthal(UnitSphere): self.phi = phi else: self.phi = Uniform(0., 2*pi) + + @property + def reference_vwu(self): + return self._reference_vwu + + @reference_vwu.setter + def reference_vwu(self, vwu): + cv.check_type('reference v direction', vwu, Iterable, Real) + vwu = np.asarray(vwu) + uvw = self.reference_uvw + cv.check_greater_than('reference v direction must not be parallel to reference u direction', np.linalg.norm(np.cross(vwu,uvw)), 1e-6*np.linalg.norm(vwu)) + vwu -= vwu.dot(uvw)*uvw + cv.check_less_than('reference v direction must be orthogonal to reference u direction', np.abs(vwu.dot(uvw)), 1e-6) + self._reference_vwu = vwu/np.linalg.norm(vwu) @property def mu(self): @@ -132,6 +150,8 @@ class PolarAzimuthal(UnitSphere): element.set("type", "mu-phi") if self.reference_uvw is not None: element.set("reference_uvw", ' '.join(map(str, self.reference_uvw))) + if self.reference_vwu is not None: + element.set("reference_vwu", ' '.join(map(str, self.reference_vwu))) element.append(self.mu.to_xml_element('mu')) element.append(self.phi.to_xml_element('phi')) return element @@ -155,6 +175,9 @@ class PolarAzimuthal(UnitSphere): uvw = get_elem_list(elem, "reference_uvw", float) if uvw is not None: mu_phi.reference_uvw = uvw + vwu = get_elem_list(elem, "reference_vwu", float) + if vwu is not None: + mu_phi.reference_vwu = vwu mu_phi.mu = Univariate.from_xml_element(elem.find('mu')) mu_phi.phi = Univariate.from_xml_element(elem.find('phi')) return mu_phi diff --git a/src/distribution_multi.cpp b/src/distribution_multi.cpp index b7b3efe52..cdb33adc2 100644 --- a/src/distribution_multi.cpp +++ b/src/distribution_multi.cpp @@ -58,6 +58,15 @@ PolarAzimuthal::PolarAzimuthal(Direction u, UPtrDist mu, UPtrDist phi) PolarAzimuthal::PolarAzimuthal(pugi::xml_node node) : UnitSphereDistribution {node} { + // Read reference directional unit vector + if (check_for_node(node, "reference_vwu")) { + auto v_ref = get_node_array(node, "reference_vwu"); + if (v_ref.size() != 3) + fatal_error("Angular distribution reference v direction must have " + "three parameters specified."); + v_ref_ = Direction(v_ref.data()); + } + w_ref_ = u_ref_.cross(v_ref_); if (check_for_node(node, "mu")) { pugi::xml_node node_dist = node.child("mu"); mu_ = distribution_from_xml(node_dist); @@ -79,11 +88,15 @@ Direction PolarAzimuthal::sample(uint64_t* seed) const double mu = mu_->sample(seed); if (mu == 1.0) return u_ref_; + if (mu == -1.0) + return -u_ref_; // Sample azimuthal angle double phi = phi_->sample(seed); - return rotate_angle(u_ref_, mu, &phi, seed); + double f = std::sqrt(1 - mu * mu); + + return mu * u_ref_ + f * std::cos(phi) * v_ref_ + f * std::sin(phi) * w_ref_; } //============================================================================== diff --git a/tests/regression_tests/source/inputs_true.dat b/tests/regression_tests/source/inputs_true.dat index 9f10b79d6..0c3764ba6 100644 --- a/tests/regression_tests/source/inputs_true.dat +++ b/tests/regression_tests/source/inputs_true.dat @@ -25,7 +25,7 @@ -2.0 0.0 2.0 0.2 0.3 0.2 - + -1.0 0.0 1.0 0.5 0.25 0.25 diff --git a/tests/regression_tests/source/results_true.dat b/tests/regression_tests/source/results_true.dat index 951075bbb..7d03c696d 100644 --- a/tests/regression_tests/source/results_true.dat +++ b/tests/regression_tests/source/results_true.dat @@ -1,2 +1,2 @@ k-combined: -3.034717E-01 2.799386E-03 +3.080655E-01 4.837707E-03 diff --git a/tests/unit_tests/test_stats.py b/tests/unit_tests/test_stats.py index abf143f12..998d4b984 100644 --- a/tests/unit_tests/test_stats.py +++ b/tests/unit_tests/test_stats.py @@ -516,3 +516,33 @@ def test_combine_distributions(): # uncertainty of the expected value samples = combined.sample(10_000) assert_sample_mean(samples, 0.25) + +def test_reference_vwu_projection(): + """When a non-orthogonal vector is provided, the setter should project out + any component along reference_uvw so the stored vector is orthogonal. + """ + pa = openmc.stats.PolarAzimuthal() # default reference_uvw == (0, 0, 1) + + # Provide a vector that is not orthogonal to (0,0,1) + pa.reference_vwu = (2.0, 0.5, 0.3) + + reference_v = np.asarray(pa.reference_vwu) + reference_u = np.asarray(pa.reference_uvw) + + # reference_v should be orthogonal to reference_u + assert abs(np.dot(reference_v, reference_u)) < 1e-6 + + +def test_reference_vwu_normalization(): + """When a non-normalized vector is provided, the setter should normalize + the projected vector to unit length. + """ + pa = openmc.stats.PolarAzimuthal() # default reference_uvw == (0, 0, 1) + + # Provide a vector that is neither orthogonal to (0,0,1) nor unit-length + pa.reference_vwu = (2.0, 0.5, 0.3) + + reference_v = np.asarray(pa.reference_vwu) + + # reference_v should be unit length + assert np.isclose(np.linalg.norm(reference_v), 1.0, atol=1e-12) From 2d8e006c3de9c8fbc0b010357ebf5a870909c8bc Mon Sep 17 00:00:00 2001 From: Perry <100789850+yrrepy@users.noreply.github.com> Date: Mon, 3 Nov 2025 09:10:30 -0800 Subject: [PATCH 441/671] Enable nuclide filters with get_decay_photon_energy (#3614) Co-authored-by: Paul Romano --- openmc/material.py | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/openmc/material.py b/openmc/material.py index c7b954b66..1609da05a 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -290,11 +290,13 @@ class Material(IDManagerMixin): return self.get_decay_photon_energy(0.0) def get_decay_photon_energy( - self, - clip_tolerance: float = 1e-6, - units: str = 'Bq', - volume: float | None = None - ) -> Univariate | None: + self, + clip_tolerance: float = 1e-6, + units: str = 'Bq', + volume: float | None = None, + exclude_nuclides: list[str] | None = None, + include_nuclides: list[str] | None = None + ) -> Univariate | None: r"""Return energy distribution of decay photons from unstable nuclides. .. versionadded:: 0.14.0 @@ -302,22 +304,31 @@ class Material(IDManagerMixin): Parameters ---------- clip_tolerance : float - Maximum fraction of :math:`\sum_i x_i p_i` for discrete - distributions that will be discarded. + Maximum fraction of :math:`\sum_i x_i p_i` for discrete distributions + that will be discarded. units : {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3'} Specifies the units on the integral of the distribution. volume : float, optional Volume of the material. If not passed, defaults to using the :attr:`Material.volume` attribute. + exclude_nuclides : list of str, optional + Nuclides to exclude from the photon source calculation. + include_nuclides : list of str, optional + Nuclides to include in the photon source calculation. If specified, + only these nuclides are used. Returns ------- Univariate or None - Decay photon energy distribution. The integral of this distribution - is the total intensity of the photon source in the requested units. + Decay photon energy distribution. The integral of this distribution is + the total intensity of the photon source in the requested units. """ cv.check_value('units', units, {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3'}) + + if exclude_nuclides is not None and include_nuclides is not None: + raise ValueError("Cannot specify both exclude_nuclides and include_nuclides") + if units == 'Bq': multiplier = volume if volume is not None else self.volume if multiplier is None: @@ -332,6 +343,11 @@ class Material(IDManagerMixin): dists = [] probs = [] for nuc, atoms_per_bcm in self.get_nuclide_atom_densities().items(): + if exclude_nuclides is not None and nuc in exclude_nuclides: + continue + if include_nuclides is not None and nuc not in include_nuclides: + continue + source_per_atom = openmc.data.decay_photon_energy(nuc) if source_per_atom is not None and atoms_per_bcm > 0.0: dists.append(source_per_atom) From bd76fc056651e33d138d18dbee061c0a7bc83823 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 3 Nov 2025 11:11:57 -0600 Subject: [PATCH 442/671] Fix bug in normalization of tally results with no_reduce (#3619) --- src/tallies/tally.cpp | 7 +++++- tests/unit_tests/test_no_reduce.py | 40 ++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 tests/unit_tests/test_no_reduce.py diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index b9c615ecb..12ee3427e 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -825,9 +825,14 @@ void Tally::accumulate() total_source = 1.0; } + // Determine number of particles contributing to tally + double contributing_particles = settings::reduce_tallies + ? settings::n_particles + : simulation::work_per_rank; + // Account for number of source particles in normalization double norm = - total_source / (settings::n_particles * settings::gen_per_batch); + total_source / (contributing_particles * settings::gen_per_batch); if (settings::solver_type == SolverType::RANDOM_RAY) { norm = 1.0; diff --git a/tests/unit_tests/test_no_reduce.py b/tests/unit_tests/test_no_reduce.py new file mode 100644 index 000000000..00ddb5a95 --- /dev/null +++ b/tests/unit_tests/test_no_reduce.py @@ -0,0 +1,40 @@ +"""Test the settings.no_reduce feature to ensure tallies are correctly +reduced across MPI processes.""" + +import openmc +import pytest + +from tests.testing_harness import config + + +@pytest.mark.parametrize('no_reduce', [True, False]) +def test_no_reduce(no_reduce, run_in_tmpdir): + """Test that tally results are correct with and without no_reduce.""" + + # Create simple sphere model with vacuum + model = openmc.Model() + sphere = openmc.Sphere(r=1.0, boundary_type='vacuum') + cell = openmc.Cell(region=-sphere) + model.geometry = openmc.Geometry([cell]) + model.settings.run_mode = 'fixed source' + model.settings.batches = 10 + model.settings.particles = 100 + model.settings.source = openmc.IndependentSource(space=openmc.stats.Point()) + model.settings.no_reduce = no_reduce + + # Tally: surface current on vacuum boundary + surf_filter = openmc.SurfaceFilter(sphere) + tally = openmc.Tally() + tally.filters = [surf_filter] + tally.scores = ['current'] + model.tallies = [tally] + + # Run OpenMC with proper MPI arguments if needed + kwargs = {'apply_tally_results': True, 'openmc_exec': config['exe']} + if config['mpi']: + kwargs['mpi_args'] = [config['mpiexec'], '-n', config['mpi_np']] + model.run(**kwargs) + + # The tally should be ~1.0 (every particle crosses the surface once) + tally_mean = tally.mean.flatten()[0] + assert tally_mean == pytest.approx(1.0) From e5c7d0ca88983cdd37bb9870cd1fccc8e4301736 Mon Sep 17 00:00:00 2001 From: Jon Shimwell Date: Wed, 5 Nov 2025 17:03:20 +0100 Subject: [PATCH 443/671] Adding vtkhdf option to write vtk data (#3252) Co-authored-by: shimwell Co-authored-by: Jonathan Shimwell Co-authored-by: rherrero-pf <156206440+rherrero-pf@users.noreply.github.com> Co-authored-by: Patrick Shriwise Co-authored-by: Paul Romano --- openmc/mesh.py | 166 ++++++++++++++++-- .../test_mesh_dagmc_tets.vtk | 159 +++++++++++++++++ tests/unit_tests/test_mesh.py | 69 ++++++++ tests/unit_tests/test_mesh_dagmc_tets.vtk | 1 + 4 files changed, 377 insertions(+), 18 deletions(-) create mode 100644 tests/regression_tests/unstructured_mesh/test_mesh_dagmc_tets.vtk create mode 120000 tests/unit_tests/test_mesh_dagmc_tets.vtk diff --git a/openmc/mesh.py b/openmc/mesh.py index 3d93d87a1..ce5218b5e 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -5,6 +5,7 @@ from collections.abc import Iterable, Sequence, Mapping from functools import wraps from math import pi, sqrt, atan2 from numbers import Integral, Real +from pathlib import Path from typing import Protocol import h5py @@ -2443,6 +2444,7 @@ class UnstructuredMesh(MeshBase): _UNSUPPORTED_ELEM = -1 _LINEAR_TET = 0 _LINEAR_HEX = 1 + _VTK_TETRA = 10 def __init__(self, filename: PathLike, library: str, mesh_id: int | None = None, name: str = '', length_multiplier: float = 1.0, @@ -2652,7 +2654,8 @@ class UnstructuredMesh(MeshBase): warnings.warn( "The 'UnstructuredMesh.write_vtk_mesh' method has been renamed " "to 'write_data_to_vtk' and will be removed in a future version " - " of OpenMC.", FutureWarning + " of OpenMC.", + FutureWarning, ) self.write_data_to_vtk(**kwargs) @@ -2670,9 +2673,10 @@ class UnstructuredMesh(MeshBase): Parameters ---------- filename : str or pathlib.Path - Name of the VTK file to write. If the filename ends in '.vtu' then a - binary VTU format file will be written, if the filename ends in - '.vtk' then a legacy VTK file will be written. + Name of the VTK file to write. If the filename ends in '.vtkhdf' + then a VTKHDF format file will be written. If the filename ends in + '.vtu' then a binary VTU format file will be written. If the + filename ends in '.vtk' then a legacy VTK file will be written. datasets : dict Dictionary whose keys are the data labels and values are numpy appropriately sized arrays of the data @@ -2680,6 +2684,35 @@ class UnstructuredMesh(MeshBase): Whether or not to normalize the data by the volume of the mesh elements """ + + if Path(filename).suffix == ".vtkhdf": + + self._write_data_to_vtk_hdf5_format( + filename=filename, + datasets=datasets, + volume_normalization=volume_normalization, + ) + + elif Path(filename).suffix == ".vtk" or Path(filename).suffix == ".vtu": + + self._write_data_to_vtk_ascii_format( + filename=filename, + datasets=datasets, + volume_normalization=volume_normalization, + ) + + else: + raise ValueError( + "Unsupported file extension, The filename must end with " + "'.vtkhdf', '.vtu' or '.vtk'" + ) + + def _write_data_to_vtk_ascii_format( + self, + filename: PathLike | None = None, + datasets: dict | None = None, + volume_normalization: bool = True, + ): from vtkmodules.util import numpy_support from vtkmodules import vtkCommonCore from vtkmodules import vtkCommonDataModel @@ -2687,9 +2720,7 @@ class UnstructuredMesh(MeshBase): from vtkmodules import vtkIOXML if self.connectivity is None or self.vertices is None: - raise RuntimeError( - "This mesh has not been loaded from a statepoint file." - ) + raise RuntimeError("This mesh has not been loaded from a statepoint file.") if filename is None: filename = f"mesh_{self.id}.vtk" @@ -2771,29 +2802,128 @@ class UnstructuredMesh(MeshBase): writer.Write() + def _write_data_to_vtk_hdf5_format( + self, + filename: PathLike | None = None, + datasets: dict | None = None, + volume_normalization: bool = True, + ): + def append_dataset(dset, array): + """Convenience function to append data to an HDF5 dataset""" + origLen = dset.shape[0] + dset.resize(origLen + array.shape[0], axis=0) + dset[origLen:] = array + + if self.library != "moab": + raise NotImplementedError("VTKHDF output is only supported for MOAB meshes") + + # the self.connectivity contains arrays of length 8 to support hex + # elements as well, in the case of tetrahedra mesh elements, the + # last 4 values are -1 and are removed + trimmed_connectivity = [] + for cell in self.connectivity: + # Find the index of the first -1 value, if any + first_negative_index = np.where(cell == -1)[0] + if first_negative_index.size > 0: + # Slice the array up to the first -1 value + trimmed_connectivity.append(cell[: first_negative_index[0]]) + else: + # No -1 values, append the whole cell + trimmed_connectivity.append(cell) + trimmed_connectivity = np.array(trimmed_connectivity, dtype="int32").flatten() + + # MOAB meshes supports tet elements only so we know it has 4 points per cell + points_per_cell = 4 + + # offsets are the indices of the first point of each cell in the array of points + offsets = np.arange(0, self.n_elements * points_per_cell + 1, points_per_cell) + + for name, data in datasets.items(): + if data.shape != self.dimension: + raise ValueError( + f'Cannot apply dataset "{name}" with ' + f"shape {data.shape} to mesh {self.id} " + f"with dimensions {self.dimension}" + ) + + with h5py.File(filename, "w") as f: + + root = f.create_group("VTKHDF") + vtk_file_format_version = (2, 1) + root.attrs["Version"] = vtk_file_format_version + ascii_type = "UnstructuredGrid".encode("ascii") + root.attrs.create( + "Type", + ascii_type, + dtype=h5py.string_dtype("ascii", len(ascii_type)), + ) + + # create hdf5 file structure + root.create_dataset("NumberOfPoints", (0,), maxshape=(None,), dtype="i8") + root.create_dataset("Types", (0,), maxshape=(None,), dtype="uint8") + root.create_dataset("Points", (0, 3), maxshape=(None, 3), dtype="f") + root.create_dataset( + "NumberOfConnectivityIds", (0,), maxshape=(None,), dtype="i8" + ) + root.create_dataset("NumberOfCells", (0,), maxshape=(None,), dtype="i8") + root.create_dataset("Offsets", (0,), maxshape=(None,), dtype="i8") + root.create_dataset("Connectivity", (0,), maxshape=(None,), dtype="i8") + + append_dataset(root["NumberOfPoints"], np.array([len(self.vertices)])) + append_dataset(root["Points"], self.vertices) + append_dataset( + root["NumberOfConnectivityIds"], + np.array([len(trimmed_connectivity)]), + ) + append_dataset(root["Connectivity"], trimmed_connectivity) + append_dataset(root["NumberOfCells"], np.array([self.n_elements])) + append_dataset(root["Offsets"], offsets) + + append_dataset( + root["Types"], np.full(self.n_elements, self._VTK_TETRA, dtype="uint8") + ) + + cell_data_group = root.create_group("CellData") + + for name, data in datasets.items(): + + cell_data_group.create_dataset( + name, (0,), maxshape=(None,), dtype="float64", chunks=True + ) + + if volume_normalization: + data /= self.volumes + append_dataset(cell_data_group[name], data) + @classmethod def from_hdf5(cls, group: h5py.Group, mesh_id: int, name: str): - filename = group['filename'][()].decode() - library = group['library'][()].decode() - if 'options' in group.attrs: + filename = group["filename"][()].decode() + library = group["library"][()].decode() + if "options" in group.attrs: options = group.attrs['options'].decode() else: options = None - mesh = cls(filename=filename, library=library, mesh_id=mesh_id, name=name, options=options) + mesh = cls( + filename=filename, + library=library, + mesh_id=mesh_id, + name=name, + options=options, + ) mesh._has_statepoint_data = True - vol_data = group['volumes'][()] + vol_data = group["volumes"][()] mesh.volumes = np.reshape(vol_data, (vol_data.shape[0],)) mesh.n_elements = mesh.volumes.size - vertices = group['vertices'][()] + vertices = group["vertices"][()] mesh._vertices = vertices.reshape((-1, 3)) - connectivity = group['connectivity'][()] + connectivity = group["connectivity"][()] mesh._connectivity = connectivity.reshape((-1, 8)) - mesh._element_types = group['element_types'][()] + mesh._element_types = group["element_types"][()] - if 'length_multiplier' in group: - mesh.length_multiplier = group['length_multiplier'][()] + if "length_multiplier" in group: + mesh.length_multiplier = group["length_multiplier"][()] return mesh @@ -2812,7 +2942,7 @@ class UnstructuredMesh(MeshBase): element.set("library", self._library) if self.options is not None: - element.set('options', self.options) + element.set("options", self.options) subelement = ET.SubElement(element, "filename") subelement.text = str(self.filename) diff --git a/tests/regression_tests/unstructured_mesh/test_mesh_dagmc_tets.vtk b/tests/regression_tests/unstructured_mesh/test_mesh_dagmc_tets.vtk new file mode 100644 index 000000000..2ddd228cf --- /dev/null +++ b/tests/regression_tests/unstructured_mesh/test_mesh_dagmc_tets.vtk @@ -0,0 +1,159 @@ +# vtk DataFile Version 2.0 +made_with_cad_to_dagmc_package, Created by Gmsh 4.12.1 +ASCII +DATASET UNSTRUCTURED_GRID +POINTS 14 double +-0.5 -0.5 0.5 +-0.5 -0.5 -0.5 +-0.5 0.5 0.5 +-0.5 0.5 -0.5 +0.5 -0.5 0.5 +0.5 -0.5 -0.5 +0.5 0.5 0.5 +0.5 0.5 -0.5 +-0.5 0 0 +0.5 0 0 +0 -0.5 0 +0 0.5 0 +0 0 -0.5 +0 0 0.5 + +CELLS 68 268 +1 0 +1 1 +1 2 +1 3 +1 4 +1 5 +1 6 +1 7 +2 1 0 +2 0 2 +2 3 2 +2 1 3 +2 5 4 +2 4 6 +2 7 6 +2 5 7 +2 1 5 +2 0 4 +2 3 7 +2 2 6 +3 1 0 8 +3 0 2 8 +3 3 1 8 +3 2 3 8 +3 5 9 4 +3 4 9 6 +3 7 9 5 +3 6 9 7 +3 0 1 10 +3 4 0 10 +3 1 5 10 +3 5 4 10 +3 2 11 3 +3 6 11 2 +3 3 11 7 +3 7 11 6 +3 1 3 12 +3 5 1 12 +3 3 7 12 +3 7 5 12 +3 0 13 2 +3 4 13 0 +3 2 13 6 +3 6 13 4 +4 13 8 12 10 +4 11 8 12 13 +4 12 11 13 9 +4 10 12 13 9 +4 12 3 11 7 +4 13 2 8 0 +4 8 12 1 3 +4 11 3 8 2 +4 10 8 1 0 +4 0 10 13 4 +4 1 12 10 5 +4 11 2 13 6 +4 4 9 13 6 +4 6 9 11 7 +4 10 9 4 5 +4 7 9 12 5 +4 3 8 12 11 +4 1 12 8 10 +4 8 11 2 13 +4 10 13 8 0 +4 13 10 9 4 +4 11 13 9 6 +4 12 11 9 7 +4 9 10 12 5 + +CELL_TYPES 68 +1 +1 +1 +1 +1 +1 +1 +1 +3 +3 +3 +3 +3 +3 +3 +3 +3 +3 +3 +3 +5 +5 +5 +5 +5 +5 +5 +5 +5 +5 +5 +5 +5 +5 +5 +5 +5 +5 +5 +5 +5 +5 +5 +5 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 +10 diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index f0f289408..9aca8b596 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -2,6 +2,7 @@ from math import pi from tempfile import TemporaryDirectory from pathlib import Path +import h5py import numpy as np from scipy.stats import chi2 import pytest @@ -486,6 +487,74 @@ def test_umesh(run_in_tmpdir, simple_umesh, export_type): with pytest.raises(ValueError, match='Cannot apply dataset "mean"'): simple_umesh.write_data_to_vtk(datasets={'mean': ref_data[:-2]}, filename=filename) + +@pytest.mark.skipif(not openmc.lib._dagmc_enabled(), reason="DAGMC not enabled.") +def test_write_vtkhdf(request, run_in_tmpdir): + """Performs a minimal UnstructuredMesh simulation, reads in the resulting + statepoint file and writes the mesh data to vtk and vtkhdf files. It is + necessary to read in the unstructured mesh from a statepoint file to ensure + it has all the required attributes + """ + model = openmc.Model() + + surf1 = openmc.Sphere(r=1000.0, boundary_type="vacuum") + cell1 = openmc.Cell(region=-surf1) + model.geometry = openmc.Geometry([cell1]) + + umesh = openmc.UnstructuredMesh( + request.path.parent / "test_mesh_dagmc_tets.vtk", + "moab", + mesh_id = 1 + ) + mesh_filter = openmc.MeshFilter(umesh) + + # Create flux mesh tally to score alpha production + mesh_tally = openmc.Tally(name="test_tally") + mesh_tally.filters = [mesh_filter] + mesh_tally.scores = ["flux"] + + model.tallies = [mesh_tally] + + model.settings.run_mode = "fixed source" + model.settings.batches = 2 + model.settings.particles = 10 + + statepoint_file = model.run() + + with openmc.StatePoint(statepoint_file) as statepoint: + my_tally = statepoint.get_tally(name="test_tally") + + umesh_from_sp = statepoint.meshes[umesh.id] + + datasets={ + "mean": my_tally.mean.flatten(), + "std_dev": my_tally.std_dev.flatten() + } + + umesh_from_sp.write_data_to_vtk(datasets=datasets, filename="test_mesh.vtkhdf") + umesh_from_sp.write_data_to_vtk(datasets=datasets, filename="test_mesh.vtk") + + with pytest.raises(ValueError, match="Unsupported file extension"): + # Supported file extensions are vtk or vtkhdf, not hdf5, so this should raise an error + umesh_from_sp.write_data_to_vtk( + datasets=datasets, + filename="test_mesh.hdf5", + ) + with pytest.raises(ValueError, match="Cannot apply dataset"): + # The shape of the data should match the shape of the mesh, so this should raise an error + umesh_from_sp.write_data_to_vtk( + datasets={'incorrectly_shaped_data': np.array(([1,2,3]))}, + filename="test_mesh_incorrect_shape.vtkhdf", + ) + + assert Path("test_mesh.vtk").exists() + assert Path("test_mesh.vtkhdf").exists() + + # just ensure we can open the file without error + with h5py.File("test_mesh.vtkhdf", "r"): + ... + + def test_mesh_get_homogenized_materials(): """Test the get_homogenized_materials method""" # Simple model with 1 cm of Fe56 next to 1 cm of H1 diff --git a/tests/unit_tests/test_mesh_dagmc_tets.vtk b/tests/unit_tests/test_mesh_dagmc_tets.vtk new file mode 120000 index 000000000..9f7000175 --- /dev/null +++ b/tests/unit_tests/test_mesh_dagmc_tets.vtk @@ -0,0 +1 @@ +../regression_tests/unstructured_mesh/test_mesh_dagmc_tets.vtk \ No newline at end of file From c0f302db6845e2d303625317430b6ee26ff2cec7 Mon Sep 17 00:00:00 2001 From: Perry <100789850+yrrepy@users.noreply.github.com> Date: Fri, 7 Nov 2025 08:58:40 -0800 Subject: [PATCH 444/671] Add energy group structure: SCALE-999 (#3564) --- openmc/mgxs/__init__.py | 208 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 205 insertions(+), 3 deletions(-) diff --git a/openmc/mgxs/__init__.py b/openmc/mgxs/__init__.py index 5de85afb0..682b5d550 100644 --- a/openmc/mgxs/__init__.py +++ b/openmc/mgxs/__init__.py @@ -13,8 +13,8 @@ GROUP_STRUCTURES = {} - "XMAS-172_" designed for LWR analysis ([SAR1990]_, [SAN2004]_) - "SHEM-361_" designed for LWR analysis to eliminate self-shielding calculations of thermal resonances ([HFA2005]_, [SAN2007]_, [HEB2008]_) -- "SCALE-X" (where X is 44 which is designed for criticality analysis - and 252 is designed for thermal reactors) for the SCALE code suite +- "SCALE-X" (where X is 44 which is designed for criticality analysis, 252 is designed + for thermal reactors and 999 for multipurpose activation) for the SCALE code suite ([ZAL1999]_ and [REARDEN2013]_) - "MPACT-X" (where X is 51 (PWR), 60 (BWR), 69 (Magnox)) from the MPACT_ reactor physics code ([KIM2019]_ and [KIM2020]_) @@ -29,6 +29,7 @@ GROUP_STRUCTURES = {} .. _SCALE44: https://www-nds.iaea.org/publications/indc/indc-czr-0001.pdf .. _ECCO-33: https://serpent.vtt.fi/mediawiki/index.php/ECCO_33-group_structure .. _SCALE252: https://oecd-nea.org/science/wpncs/amct/workingarea/meeting2013/EGAMCT2013_08.pdf +.. _SCALE999: https://info.ornl.gov/sites/publications/Files/Pub67728.pdf, https://www.nrc.gov/docs/ML1218/ML12184A002.pdf .. _MPACT: https://vera.ornl.gov/mpact/ .. _XMAS-172: https://www-nds.iaea.org/wimsd/energy.htm .. _SHEM-361: http://merlin.polymtl.ca/downloads/FP214.pdf @@ -593,7 +594,208 @@ GROUP_STRUCTURES['CCFE-709'] = np.array([ 2.4000e8, 2.8000e8, 3.2000e8, 3.6000e8, 4.0000e8, 4.4000e8, 4.8000e8, 5.2000e8, 5.6000e8, 6.0000e8, 6.4000e8, 6.8000e8, 7.2000e8, 7.6000e8, 8.0000e8, - 8.4000e8, 8.8000e8, 9.2000e8, 9.6000e8, 1.0000e9,]) + 8.4000e8, 8.8000e8, 9.2000e8, 9.6000e8, 1.0000e9]) +GROUP_STRUCTURES['SCALE-999'] = np.array([ + 1.000e-5, 1.000e-4, 5.000e-4, 7.500e-4, 1.000e-3, + 1.200e-3, 1.500e-3, 2.000e-3, 2.500e-3, 3.000e-3, + 4.000e-3, 5.000e-3, 7.500e-3, 1.000e-2, 1.450e-2, + 1.850e-2, 2.100e-2, 2.530e-2, 3.000e-2, 4.000e-2, + 5.000e-2, 6.000e-2, 7.000e-2, 8.000e-2, 9.000e-2, + 1.000e-1, 1.250e-1, 1.500e-1, 1.750e-1, 1.840e-1, + 2.000e-1, 2.250e-1, 2.500e-1, 2.750e-1, 3.000e-1, + 3.250e-1, 3.500e-1, 3.668e-1, 3.750e-1, 4.000e-1, + 4.140e-1, 4.500e-1, 5.000e-1, 5.316e-1, 5.500e-1, + 6.000e-1, 6.250e-1, 6.500e-1, 6.826e-1, 7.000e-1, + 7.500e-1, 8.000e-1, 8.500e-1, 8.764e-1, 9.000e-1, + 9.250e-1, 9.500e-1, 9.750e-1, 1.000e+0, 1.010e+0, + 1.020e+0, 1.030e+0, 1.040e+0, 1.050e+0, 1.060e+0, + 1.070e+0, 1.080e+0, 1.090e+0, 1.100e+0, 1.110e+0, + 1.120e+0, 1.130e+0, 1.140e+0, 1.150e+0, 1.175e+0, + 1.200e+0, 1.225e+0, 1.250e+0, 1.300e+0, 1.350e+0, + 1.400e+0, 1.450e+0, 1.500e+0, 1.545e+0, 1.590e+0, + 1.635e+0, 1.680e+0, 1.725e+0, 1.770e+0, 1.815e+0, + 1.860e+0, 1.900e+0, 1.940e+0, 1.970e+0, 2.000e+0, + 2.060e+0, 2.120e+0, 2.165e+0, 2.210e+0, 2.255e+0, + 2.300e+0, 2.340e+0, 2.380e+0, 2.425e+0, 2.470e+0, + 2.520e+0, 2.570e+0, 2.620e+0, 2.670e+0, 2.720e+0, + 2.770e+0, 2.820e+0, 2.870e+0, 2.920e+0, 2.970e+0, + 3.000e+0, 3.100e+0, 3.200e+0, 3.300e+0, 3.500e+0, + 3.620e+0, 3.730e+0, 3.830e+0, 3.928e+0, 4.000e+0, + 4.100e+0, 4.300e+0, 4.500e+0, 4.750e+0, 4.875e+0, + 5.000e+0, 5.044e+0, 5.250e+0, 5.400e+0, 5.550e+0, + 5.700e+0, 5.850e+0, 6.000e+0, 6.250e+0, 6.375e+0, + 6.500e+0, 6.625e+0, 6.750e+0, 6.875e+0, 7.000e+0, + 7.075e+0, 7.150e+0, 7.625e+0, 8.100e+0, 8.208e+0, + 8.315e+0, 8.708e+0, 9.100e+0, 9.550e+0, 1.000e+1, + 1.034e+1, 1.068e+1, 1.109e+1, 1.150e+1, 1.170e+1, + 1.190e+1, 1.240e+1, 1.290e+1, 1.333e+1, 1.375e+1, + 1.408e+1, 1.440e+1, 1.475e+1, 1.510e+1, 1.555e+1, + 1.600e+1, 1.650e+1, 1.700e+1, 1.730e+1, 1.760e+1, + 1.805e+1, 1.850e+1, 1.875e+1, 1.900e+1, 1.940e+1, + 2.000e+1, 2.050e+1, 2.100e+1, 2.175e+1, 2.250e+1, + 2.375e+1, 2.500e+1, 2.625e+1, 2.750e+1, 2.826e+1, + 2.902e+1, 2.951e+1, 3.000e+1, 3.063e+1, 3.125e+1, + 3.150e+1, 3.175e+1, 3.250e+1, 3.325e+1, 3.350e+1, + 3.375e+1, 3.418e+1, 3.460e+1, 3.500e+1, 3.550e+1, + 3.600e+1, 3.700e+1, 3.713e+1, 3.727e+1, 3.763e+1, + 3.800e+1, 3.855e+1, 3.910e+1, 3.935e+1, 3.960e+1, + 4.030e+1, 4.100e+1, 4.170e+1, 4.240e+1, 4.320e+1, + 4.400e+1, 4.460e+1, 4.520e+1, 4.610e+1, 4.700e+1, + 4.743e+1, 4.785e+1, 4.808e+1, 4.830e+1, 4.875e+1, + 4.920e+1, 4.990e+1, 5.060e+1, 5.130e+1, 5.200e+1, + 5.270e+1, 5.340e+1, 5.620e+1, 5.800e+1, 6.000e+1, + 6.100e+1, 6.122e+1, 6.144e+1, 6.300e+1, 6.500e+1, + 6.625e+1, 6.750e+1, 6.975e+1, 7.200e+1, 7.400e+1, + 7.600e+1, 7.745e+1, 7.889e+1, 7.945e+1, 8.000e+1, + 8.170e+1, 8.200e+1, 8.400e+1, 8.600e+1, 8.800e+1, + 9.000e+1, 9.250e+1, 9.500e+1, 9.700e+1, 1.000e+2, + 1.012e+2, 1.038e+2, 1.050e+2, 1.080e+2, 1.105e+2, + 1.130e+2, 1.160e+2, 1.175e+2, 1.190e+2, 1.205e+2, + 1.220e+2, 1.240e+2, 1.260e+2, 1.280e+2, 1.301e+2, + 1.325e+2, 1.350e+2, 1.375e+2, 1.400e+2, 1.430e+2, + 1.450e+2, 1.475e+2, 1.500e+2, 1.525e+2, 1.550e+2, + 1.575e+2, 1.600e+2, 1.625e+2, 1.650e+2, 1.670e+2, + 1.700e+2, 1.716e+2, 1.739e+2, 1.763e+2, 1.786e+2, + 1.800e+2, 1.877e+2, 1.885e+2, 1.915e+2, 1.930e+2, + 1.962e+2, 1.999e+2, 2.020e+2, 2.074e+2, 2.088e+2, + 2.095e+2, 2.122e+2, 2.145e+2, 2.175e+2, 2.200e+2, + 2.237e+2, 2.269e+2, 2.301e+2, 2.333e+2, 2.367e+2, + 2.400e+2, 2.442e+2, 2.484e+2, 2.527e+2, 2.571e+2, + 2.615e+2, 2.661e+2, 2.707e+2, 2.754e+2, 2.801e+2, + 2.850e+2, 2.899e+2, 2.948e+2, 2.999e+2, 3.050e+2, + 3.107e+2, 3.165e+2, 3.224e+2, 3.284e+2, 3.345e+2, + 3.408e+2, 3.471e+2, 3.536e+2, 3.591e+2, 3.648e+2, + 3.705e+2, 3.764e+2, 3.823e+2, 3.883e+2, 3.944e+2, + 4.007e+2, 4.070e+2, 4.134e+2, 4.199e+2, 4.265e+2, + 4.332e+2, 4.400e+2, 4.470e+2, 4.540e+2, 4.595e+2, + 4.650e+2, 4.706e+2, 4.763e+2, 4.821e+2, 4.879e+2, + 4.937e+2, 4.997e+2, 5.057e+2, 5.118e+2, 5.180e+2, + 5.243e+2, 5.306e+2, 5.370e+2, 5.435e+2, 5.500e+2, + 5.581e+2, 5.662e+2, 5.745e+2, 5.830e+2, 5.932e+2, + 6.036e+2, 6.142e+2, 6.250e+2, 6.359e+2, 6.471e+2, + 6.585e+2, 6.700e+2, 6.765e+2, 6.830e+2, 6.909e+2, + 6.988e+2, 7.069e+2, 7.150e+2, 7.232e+2, 7.316e+2, + 7.400e+2, 7.485e+2, 7.598e+2, 7.712e+2, 7.827e+2, + 7.945e+2, 8.064e+2, 8.185e+2, 8.308e+2, 8.433e+2, + 8.559e+2, 8.688e+2, 8.818e+2, 8.950e+2, 9.085e+2, + 9.221e+2, 9.360e+2, 9.500e+2, 9.555e+2, 9.611e+2, + 9.720e+2, 9.829e+2, 9.940e+2, 1.005e+3, 1.017e+3, + 1.028e+3, 1.040e+3, 1.051e+3, 1.063e+3, 1.075e+3, + 1.087e+3, 1.100e+3, 1.112e+3, 1.125e+3, 1.137e+3, + 1.150e+3, 1.171e+3, 1.191e+3, 1.213e+3, 1.234e+3, + 1.249e+3, 1.265e+3, 1.280e+3, 1.296e+3, 1.312e+3, + 1.328e+3, 1.344e+3, 1.361e+3, 1.377e+3, 1.394e+3, + 1.411e+3, 1.429e+3, 1.446e+3, 1.464e+3, 1.482e+3, + 1.500e+3, 1.525e+3, 1.550e+3, 1.585e+3, 1.610e+3, + 1.636e+3, 1.662e+3, 1.689e+3, 1.716e+3, 1.744e+3, + 1.772e+3, 1.800e+3, 1.828e+3, 1.856e+3, 1.885e+3, + 1.914e+3, 1.943e+3, 1.973e+3, 2.004e+3, 2.035e+3, + 2.075e+3, 2.116e+3, 2.158e+3, 2.200e+3, 2.250e+3, + 2.290e+3, 2.337e+3, 2.386e+3, 2.435e+3, 2.500e+3, + 2.532e+3, 2.580e+3, 2.613e+3, 2.679e+3, 2.747e+3, + 2.808e+3, 2.871e+3, 2.935e+3, 3.000e+3, 3.035e+3, + 3.112e+3, 3.191e+3, 3.272e+3, 3.355e+3, 3.440e+3, + 3.527e+3, 3.616e+3, 3.707e+3, 3.740e+3, 3.819e+3, + 3.900e+3, 3.998e+3, 4.099e+3, 4.202e+3, 4.307e+3, + 4.400e+3, 4.500e+3, 4.620e+3, 4.740e+3, 4.850e+3, + 4.960e+3, 5.100e+3, 5.250e+3, 5.400e+3, 5.531e+3, + 5.645e+3, 5.700e+3, 5.879e+3, 6.000e+3, 6.128e+3, + 6.258e+3, 6.392e+3, 6.528e+3, 6.667e+3, 6.809e+3, + 6.954e+3, 7.102e+3, 7.212e+3, 7.323e+3, 7.437e+3, + 7.552e+3, 7.669e+3, 7.787e+3, 7.908e+3, 8.030e+3, + 8.159e+3, 8.289e+3, 8.422e+3, 8.557e+3, 8.694e+3, + 8.834e+3, 8.975e+3, 9.119e+3, 9.307e+3, 9.500e+3, + 9.763e+3, 1.003e+4, 1.031e+4, 1.060e+4, 1.086e+4, + 1.114e+4, 1.142e+4, 1.171e+4, 1.202e+4, 1.234e+4, + 1.266e+4, 1.300e+4, 1.324e+4, 1.348e+4, 1.373e+4, + 1.398e+4, 1.424e+4, 1.450e+4, 1.476e+4, 1.503e+4, + 1.527e+4, 1.550e+4, 1.574e+4, 1.599e+4, 1.623e+4, + 1.649e+4, 1.674e+4, 1.700e+4, 1.727e+4, 1.755e+4, + 1.783e+4, 1.812e+4, 1.841e+4, 1.870e+4, 1.900e+4, + 1.931e+4, 1.965e+4, 2.000e+4, 2.045e+4, 2.092e+4, + 2.139e+4, 2.188e+4, 2.229e+4, 2.271e+4, 2.314e+4, + 2.358e+4, 2.388e+4, 2.418e+4, 2.450e+4, 2.479e+4, + 2.500e+4, 2.520e+4, 2.552e+4, 2.580e+4, 2.606e+4, + 2.653e+4, 2.700e+4, 2.737e+4, 2.774e+4, 2.812e+4, + 2.850e+4, 2.887e+4, 2.924e+4, 2.962e+4, 3.000e+4, + 3.045e+4, 3.090e+4, 3.136e+4, 3.183e+4, 3.243e+4, + 3.304e+4, 3.367e+4, 3.431e+4, 3.468e+4, 3.507e+4, + 3.545e+4, 3.584e+4, 3.624e+4, 3.663e+4, 3.704e+4, + 3.744e+4, 3.786e+4, 3.827e+4, 3.869e+4, 3.912e+4, + 3.955e+4, 3.998e+4, 4.042e+4, 4.087e+4, 4.136e+4, + 4.186e+4, 4.237e+4, 4.288e+4, 4.340e+4, 4.393e+4, + 4.446e+4, 4.500e+4, 4.565e+4, 4.631e+4, 4.721e+4, + 4.812e+4, 4.905e+4, 5.000e+4, 5.099e+4, 5.200e+4, + 5.248e+4, 5.347e+4, 5.448e+4, 5.551e+4, 5.656e+4, + 5.740e+4, 5.826e+4, 5.912e+4, 6.000e+4, 6.088e+4, + 6.177e+4, 6.267e+4, 6.358e+4, 6.451e+4, 6.545e+4, + 6.641e+4, 6.738e+4, 6.851e+4, 6.965e+4, 7.081e+4, + 7.200e+4, 7.300e+4, 7.399e+4, 7.500e+4, 7.610e+4, + 7.722e+4, 7.835e+4, 7.950e+4, 8.074e+4, 8.200e+4, + 8.250e+4, 8.374e+4, 8.500e+4, 8.652e+4, 8.788e+4, + 8.926e+4, 9.067e+4, 9.210e+4, 9.355e+4, 9.502e+4, + 9.652e+4, 9.804e+4, 1.000e+5, 1.013e+5, 1.027e+5, + 1.040e+5, 1.054e+5, 1.068e+5, 1.082e+5, 1.096e+5, + 1.111e+5, 1.125e+5, 1.139e+5, 1.153e+5, 1.168e+5, + 1.183e+5, 1.197e+5, 1.213e+5, 1.228e+5, 1.241e+5, + 1.255e+5, 1.269e+5, 1.283e+5, 1.291e+5, 1.307e+5, + 1.323e+5, 1.340e+5, 1.357e+5, 1.374e+5, 1.391e+5, + 1.409e+5, 1.426e+5, 1.445e+5, 1.463e+5, 1.481e+5, + 1.490e+5, 1.519e+5, 1.538e+5, 1.557e+5, 1.576e+5, + 1.596e+5, 1.616e+5, 1.637e+5, 1.657e+5, 1.678e+5, + 1.699e+5, 1.721e+5, 1.742e+5, 1.764e+5, 1.786e+5, + 1.809e+5, 1.832e+5, 1.855e+5, 1.878e+5, 1.902e+5, + 1.926e+5, 1.962e+5, 2.000e+5, 2.024e+5, 2.050e+5, + 2.076e+5, 2.102e+5, 2.128e+5, 2.155e+5, 2.182e+5, + 2.209e+5, 2.237e+5, 2.265e+5, 2.294e+5, 2.323e+5, + 2.352e+5, 2.381e+5, 2.411e+5, 2.442e+5, 2.472e+5, + 2.500e+5, 2.527e+5, 2.555e+5, 2.584e+5, 2.612e+5, + 2.641e+5, 2.670e+5, 2.700e+5, 2.732e+5, 2.767e+5, + 2.802e+5, 2.837e+5, 2.873e+5, 2.909e+5, 2.945e+5, + 2.972e+5, 2.985e+5, 3.020e+5, 3.053e+5, 3.088e+5, + 3.122e+5, 3.157e+5, 3.192e+5, 3.228e+5, 3.264e+5, + 3.300e+5, 3.337e+5, 3.379e+5, 3.422e+5, 3.465e+5, + 3.508e+5, 3.553e+5, 3.597e+5, 3.643e+5, 3.688e+5, + 3.735e+5, 3.782e+5, 3.829e+5, 3.877e+5, 3.938e+5, + 4.000e+5, 4.076e+5, 4.138e+5, 4.200e+5, 4.249e+5, + 4.299e+5, 4.349e+5, 4.400e+5, 4.452e+5, 4.505e+5, + 4.553e+5, 4.601e+5, 4.650e+5, 4.700e+5, 4.772e+5, + 4.845e+5, 4.920e+5, 4.995e+5, 5.054e+5, 5.113e+5, + 5.173e+5, 5.234e+5, 5.299e+5, 5.365e+5, 5.432e+5, + 5.500e+5, 5.557e+5, 5.614e+5, 5.672e+5, 5.730e+5, + 5.784e+5, 5.891e+5, 6.000e+5, 6.081e+5, 6.158e+5, + 6.235e+5, 6.313e+5, 6.393e+5, 6.468e+5, 6.545e+5, + 6.622e+5, 6.700e+5, 6.790e+5, 6.926e+5, 7.065e+5, + 7.154e+5, 7.244e+5, 7.335e+5, 7.427e+5, 7.500e+5, + 7.576e+5, 7.653e+5, 7.730e+5, 7.808e+5, 7.904e+5, + 8.002e+5, 8.100e+5, 8.200e+5, 8.301e+5, 8.403e+5, + 8.506e+5, 8.611e+5, 8.750e+5, 8.874e+5, 9.000e+5, + 9.072e+5, 9.200e+5, 9.400e+5, 9.616e+5, 9.800e+5, + 1.003e+6, 1.010e+6, 1.040e+6, 1.070e+6, 1.100e+6, + 1.108e+6, 1.136e+6, 1.165e+6, 1.200e+6, 1.225e+6, + 1.250e+6, 1.287e+6, 1.317e+6, 1.356e+6, 1.400e+6, + 1.423e+6, 1.461e+6, 1.500e+6, 1.536e+6, 1.572e+6, + 1.612e+6, 1.653e+6, 1.695e+6, 1.738e+6, 1.782e+6, + 1.827e+6, 1.850e+6, 1.921e+6, 1.969e+6, 2.019e+6, + 2.070e+6, 2.123e+6, 2.176e+6, 2.231e+6, 2.307e+6, + 2.354e+6, 2.365e+6, 2.385e+6, 2.466e+6, 2.479e+6, + 2.535e+6, 2.592e+6, 2.658e+6, 2.725e+6, 2.794e+6, + 2.865e+6, 2.932e+6, 3.000e+6, 3.080e+6, 3.166e+6, + 3.247e+6, 3.329e+6, 3.413e+6, 3.499e+6, 3.588e+6, + 3.679e+6, 3.772e+6, 3.867e+6, 3.965e+6, 4.066e+6, + 4.183e+6, 4.304e+6, 4.398e+6, 4.493e+6, 4.607e+6, + 4.724e+6, 4.800e+6, 4.882e+6, 4.966e+6, 5.092e+6, + 5.221e+6, 5.353e+6, 5.488e+6, 5.627e+6, 5.770e+6, + 5.916e+6, 6.065e+6, 6.219e+6, 6.376e+6, 6.434e+6, + 6.592e+6, 6.703e+6, 6.873e+6, 7.047e+6, 7.225e+6, + 7.408e+6, 7.596e+6, 7.788e+6, 7.985e+6, 8.187e+6, + 8.395e+6, 8.607e+6, 8.825e+6, 9.048e+6, 9.278e+6, + 9.512e+6, 9.753e+6, 1.000e+7, 1.025e+7, 1.051e+7, + 1.078e+7, 1.105e+7, 1.133e+7, 1.162e+7, 1.191e+7, + 1.221e+7, 1.252e+7, 1.284e+7, 1.317e+7, 1.350e+7, + 1.384e+7, 1.419e+7, 1.455e+7, 1.492e+7, 1.530e+7, + 1.568e+7, 1.608e+7, 1.649e+7, 1.691e+7, 1.733e+7, + 1.790e+7, 1.845e+7, 1.900e+7, 1.964e+7, 2.000e+7]) GROUP_STRUCTURES['UKAEA-1102'] = np.array([ 1.0000e-5, 1.0471e-5, 1.0965e-5, 1.1482e-5, 1.2023e-5, 1.2589e-5, 1.3183e-5, 1.3804e-5, 1.4454e-5, 1.5136e-5, From 8cd3911cbe0fa277b889effa1c5c1566b0683f55 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Wed, 12 Nov 2025 00:01:18 -0600 Subject: [PATCH 445/671] Reset DAGMC history when reviving from source. (#3601) Co-authored-by: Paul Romano --- src/particle.cpp | 3 + .../weightwindows/dagmc/__init__.py | 0 .../dagmc/nested_shell_geometry.h5m | Bin 0 -> 58680 bytes tests/unit_tests/weightwindows/dagmc/test.py | 98 ++++++++++++++++++ 4 files changed, 101 insertions(+) create mode 100644 tests/unit_tests/weightwindows/dagmc/__init__.py create mode 100644 tests/unit_tests/weightwindows/dagmc/nested_shell_geometry.h5m create mode 100644 tests/unit_tests/weightwindows/dagmc/test.py diff --git a/src/particle.cpp b/src/particle.cpp index 6b4c332a8..6ba8ebf12 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -121,6 +121,9 @@ void Particle::from_source(const SourceSite* src) fission() = false; zero_flux_derivs(); lifetime() = 0.0; +#ifdef OPENMC_DAGMC_ENABLED + history().reset(); +#endif // Copy attributes from source bank site type() = src->particle; diff --git a/tests/unit_tests/weightwindows/dagmc/__init__.py b/tests/unit_tests/weightwindows/dagmc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit_tests/weightwindows/dagmc/nested_shell_geometry.h5m b/tests/unit_tests/weightwindows/dagmc/nested_shell_geometry.h5m new file mode 100644 index 0000000000000000000000000000000000000000..af1d5563d9255b518d42b551b7527cdf45eeaaf3 GIT binary patch literal 58680 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 ZKJ@mkzRs4Wo;}NVH#PV5?Swso{}1hu%5MMw literal 0 HcmV?d00001 diff --git a/tests/unit_tests/weightwindows/dagmc/test.py b/tests/unit_tests/weightwindows/dagmc/test.py new file mode 100644 index 000000000..ed01a93ed --- /dev/null +++ b/tests/unit_tests/weightwindows/dagmc/test.py @@ -0,0 +1,98 @@ +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_dagmc_weight_windows_near_boundary(run_in_tmpdir, request): + """Ensure splitting near a boundary doesn't lose particles due to + a stale DAGMC history on the particle object.""" + + # DAGMC model overview: + # * Three nested cubes; innermost cube contains a fusion neutron source. + # * Two outer cubes filled with tungsten. Weight windows defined on a mesh + # cause particles to split moving outward. + # Outer cubes are similar in size (outer slightly larger) so particles + # frequently cross the problem boundary immediately after splitting. No lost + # particles are allowed to the correct DAGMC history after splitting is used. + model = openmc.Model() + + dagmc_file = request.path.parent / 'nested_shell_geometry.h5m' + dagmc_univ = openmc.DAGMCUniverse(dagmc_file) + model.geometry = openmc.Geometry(dagmc_univ) + + tungsten = openmc.Material(name='shell') + tungsten.add_element('W', 1.0) + tungsten.set_density('g/cm3', 7.8) + materials = openmc.Materials([tungsten]) + model.materials = materials + + settings = openmc.Settings() + settings.output = {'tallies': False, 'summary': False} + + source = openmc.IndependentSource() + source.space = openmc.stats.Point((0.0, 0.0, 0.0)) + source.angle = openmc.stats.Isotropic() + source.energy = openmc.stats.Discrete([14.1e6], [1.0]) + settings.source = source + + settings.batches = 2 + settings.particles = 500 + settings.run_mode = 'fixed source' + settings.survival_biasing = False + settings.max_lost_particles = 1 + settings.max_history_splits = 10_000_000 + + settings.weight_window_checkpoints = { + 'surface': True, + 'collision': True, + } + + mesh = openmc.RegularMesh() + mesh.lower_left = (-60.0, -60.0, -60.0) + mesh.upper_right = (60.0, 60.0, 60.0) + mesh.dimension = (24, 1, 1) + + weight_windows_lower = [ + 0.030750733294361156, + 0.056110505674355333, + 0.08187875047968339, + 0.1101743496347699, + 0.13982370013053508, + 0.17443799246829372, + 0.21576286623367483, + 0.26416659508033646, + 0.318574932646899, + 0.3804031702117963, + 0.42899359749256355, + 0.4954283294279403, + 0.49999999999999994, + 0.43432341070872266, + 0.38302303850488206, + 0.32148375935490886, + 0.2637416945702018, + 0.21498369367288853, + 0.17163611765361744, + 0.13832102142074995, + 0.10717772257151495, + 0.07986176041282561, + 0.05499644859408233, + 0.03058023506703803, + ] + + weight_windows = openmc.WeightWindows( + mesh, + lower_ww_bounds=weight_windows_lower, + upper_bound_ratio=5.0, + ) + weight_windows.max_lower_bound_ratio = 1.0 + settings.weight_windows = weight_windows + settings.weight_windows_on = True + model.settings = settings + + model.run() \ No newline at end of file From 5c2bfe771eb94c759923c63be5ba3eb01a6b3ec9 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Wed, 12 Nov 2025 00:29:19 -0600 Subject: [PATCH 446/671] Write particle states as separate lines in track VTK files. (#3628) --- openmc/tracks.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/openmc/tracks.py b/openmc/tracks.py index 61e5a7244..81646e7d2 100644 --- a/openmc/tracks.py +++ b/openmc/tracks.py @@ -296,16 +296,16 @@ class Tracks(list): for state in pt.states: points.InsertNextPoint(state['r']) - # Create VTK line and assign points to line. - n = pt.states.size - line = vtk.vtkPolyLine() - line.GetPointIds().SetNumberOfIds(n) - for i in range(n): - line.GetPointIds().SetId(i, point_offset + i) - point_offset += n + # Create VTK line and assign points to line. + n = pt.states.size + line = vtk.vtkPolyLine() + line.GetPointIds().SetNumberOfIds(n) + for i in range(n): + line.GetPointIds().SetId(i, point_offset + i) + point_offset += n - # Add line to cell array - cells.InsertNextCell(line) + # Add line to cell array + cells.InsertNextCell(line) data = vtk.vtkPolyData() data.SetPoints(points) From e5348d3f62a821f84513a026977eef0f8bef01ad Mon Sep 17 00:00:00 2001 From: Marco De Pietri Date: Wed, 12 Nov 2025 05:35:07 -0500 Subject: [PATCH 447/671] Avoid divide-by-zero in `from_multigroup_flux` when flux is zero (#3624) Co-authored-by: Paul Romano --- openmc/deplete/microxs.py | 12 ++++++++---- tests/unit_tests/test_deplete_microxs.py | 13 +++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index e351c923d..d7624955e 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -356,13 +356,17 @@ class MicroXS: reactions = chain.reactions mts = [REACTION_MT[name] for name in reactions] - # Normalize multigroup flux - multigroup_flux = np.array(multigroup_flux) - multigroup_flux /= multigroup_flux.sum() - # Create 3D array for microscopic cross sections microxs_arr = np.zeros((len(nuclides), len(mts), 1)) + # If flux is zero, safely return zero cross sections + multigroup_flux = np.array(multigroup_flux) + if (flux_sum := multigroup_flux.sum()) == 0.0: + return cls(microxs_arr, nuclides, reactions) + + # Normalize multigroup flux + multigroup_flux /= flux_sum + # Compute microscopic cross sections within a temporary session with openmc.lib.TemporarySession(**init_kwargs): # For each nuclide and reaction, compute the flux-averaged xs diff --git a/tests/unit_tests/test_deplete_microxs.py b/tests/unit_tests/test_deplete_microxs.py index 073b3f162..5762a8511 100644 --- a/tests/unit_tests/test_deplete_microxs.py +++ b/tests/unit_tests/test_deplete_microxs.py @@ -111,3 +111,16 @@ def test_multigroup_flux_same(): energies=energies, multigroup_flux=flux, chain_file=chain_file) assert microxs_4g.data == pytest.approx(microxs_2g.data) + + +def test_microxs_zero_flux(): + chain_file = Path(__file__).parents[1] / 'chain_simple.xml' + + # Generate micro XS based on zero flux + energies = [0., 6.25e-1, 5.53e3, 8.21e5, 2.e7] + flux = [0.0, 0.0, 0.0, 0.0] + microxs = MicroXS.from_multigroup_flux( + energies=energies, multigroup_flux=flux, chain_file=chain_file) + + # All microscopic cross sections should be zero + assert np.all(microxs.data == 0.0) From 2d77544b0c045f7e693146dc466e5cc290fbdc49 Mon Sep 17 00:00:00 2001 From: Gregoire Biot Date: Wed, 12 Nov 2025 11:41:37 -0500 Subject: [PATCH 448/671] Adding variance of variance and normality tests for tally statistics (#3454) Co-authored-by: Ethan Peterson Co-authored-by: Paul Romano --- docs/source/io_formats/statepoint.rst | 2 + docs/source/methods/tallies.rst | 107 +++++- include/openmc/constants.h | 2 +- include/openmc/hdf5_interface.h | 10 +- include/openmc/tallies/tally.h | 5 + openmc/statepoint.py | 4 + openmc/tallies.py | 524 +++++++++++++++++++++++++- src/hdf5_interface.cpp | 16 +- src/state_point.cpp | 17 +- src/tallies/tally.cpp | 42 ++- src/weight_windows.cpp | 8 +- tests/unit_tests/test_tallies.py | 213 +++++++++++ 12 files changed, 915 insertions(+), 35 deletions(-) diff --git a/docs/source/io_formats/statepoint.rst b/docs/source/io_formats/statepoint.rst index 3b1031769..2309643dc 100644 --- a/docs/source/io_formats/statepoint.rst +++ b/docs/source/io_formats/statepoint.rst @@ -149,6 +149,8 @@ The current version of the statepoint file format is 18.1. tallies will have a value of 0 unless otherwise instructed. - **multiply_density** (*int*) -- Flag indicating whether reaction rates should be multiplied by atom density (1) or not (0). + - **higher_moments** (*int*) -- Flag indicating whether + higher-order tally moments are enabled (1) or not (0). :Datasets: - **n_realizations** (*int*) -- Number of realizations. - **n_filters** (*int*) -- Number of filters used. diff --git a/docs/source/methods/tallies.rst b/docs/source/methods/tallies.rst index 79a63fbdd..27a3f873a 100644 --- a/docs/source/methods/tallies.rst +++ b/docs/source/methods/tallies.rst @@ -387,6 +387,101 @@ of this is that the longer you run a simulation, the better you know your results. Therefore, by running a simulation long enough, it is possible to reduce the stochastic uncertainty to arbitrarily low levels. +Skewness +++++++++ + +The `skewness`_ of a population quantifies the asymmetry of the probability +distribution around its mean. Positive and negative skewness indicate a +longer/heavier right and left tail respectively. Let :math:`x_1,\ldots,x_n` be +the per-realization values for a bin, with sample mean :math:`\bar{x}` and +sample central moments: + +.. math:: + + m_k \;=\; \frac{1}{n}\sum_{i=1}^{n}\bigl(x_i-\bar{x}\bigr)^k. + +OpenMC reports the *adjusted Fisher-Pearson skewness* (defined for :math:`n \ge +3`), which is commonly used in many statistical packages: + +.. math:: + + G_1 \;=\; \frac{\sqrt{n \cdot (n-1)}}{\,n-2\,}\cdot\frac{m_3}{m_2^{3/2}}. + +where :math:`m_2` and :math:`m_3` correspond to the biased sample second and +third central moment respectively. + +Kurtosis +++++++++ + +The `kurtosis`_ of a population quantifies tail weight (also called tailedness) +of the probability distribution relative to a normal distribution. Positive +excess kurtosis indicates *heavier tails* whereas negative excess kurtosis +indicates *lighter tails*. Kurtosis is especially useful for identifying bins +where occasional extreme scores dominate uncertainty. OpenMC reports the +*adjusted excess kurtosis* (defined for :math:`n \ge 4`): + +.. math:: + + G_2 \;=\; \frac{(n-1)}{(n-2)(n-3)} + \left[(n+1)\,\frac{m_4}{m_2^{2}} \;-\; 3(n-1)\right]. + +where :math:`m_2` and :math:`m_4` correspond to the biased sample second and +fourth central moment respectively. For a perfectly normal distribution, the +excess kurtosis is :math:`0`. + +Variance of Variance +++++++++++++++++++++ + +The variance of the variance (also known as the coefficient of variation +squared) measures *stability of the sample variance* :math:`s^2` and, by +extension, the reliability of reported relative errors. High VOV means that +error bars themselves are noisy—often due to heavy tails, skewness, or too few +realizations. + +.. math:: + + VOV = \frac{s^2(s_{\bar{X}}^2)}{s_{\bar{X}}^4 } = \frac{m_4}{m_2^2} - \frac{1}{n} + +where :math:`s_{\bar{X}}^2` is the estimated variance of the mean and +:math:`s^2(s_{\bar{X}}^2)` is the estimated variance in :math:`s_{\bar{X}}^2`. +The MCNP manual suggests a hard threshold such that :math:`VOV < 0.1` to improve +the probability of forming a reliable confidence interval. However, OpenMC does +not enforce an universal cut-off because the suitability of any single threshold +depends strongly on problem specifics (estimator choice, variance-reduction +settings, tally binning, or even effective sample size). + + +Normality Tests (D'Agostino-Pearson) +++++++++++++++++++++++++++++++++++++ + +These normality test verify the hypothesis that fluctuations are *approximately +normal*, a working assumption behind many Monte Carlo diagnostics and +`confidence-interval heuristics`_. Tests are provided for: (i) skewness-only, +(ii) kurtosis-only, and (iii) the *omnibus* combination. OpenMC uses the +finite-sample-adjusted skewness :math:`G_1` and excess kurtosis :math:`G_2` +above to construct standardized normal scores :math:`Z_1` (from :math:`G_1`) and +:math:`Z_2` (from :math:`G_2`) via the D'Agostino-Pearson transformations. The +omnibus statistic is + +.. math:: + + K^2 \;=\; Z_1^{\,2} \;+\; Z_2^{\,2} + \;\sim\; \chi^2_{(2)} \quad \text{under } H_0:\ \text{normality}. + +OpenMC reports :math:`Z_1`, :math:`Z_2`, :math:`K^2`, and their p-values when +prerequisites are met (skewness for :math:`n\ge 3`, kurtosis and omnibus for +:math:`n\ge 4`). Given a user-chosen significance level :math:`\alpha` (default +is :math:`0.05`), reject :math:`H_0` if :math:`\text{p-value}<\alpha`; otherwise +fail to reject. OpenMC leaves the interpretation to the user, who should +consider VOV together with skewness, kurtosis, and normality tests results when +judging whether reported confidence intervals are credible for their application +[#norm-tests]_. + +.. [#norm-tests] + Higher-moments accumulation must be enabled with ``higher_moments = True`` + for running these diagnostics including the skewness, kurtosis, and normality + tests. + Figure of Merit +++++++++++++++ @@ -405,14 +500,16 @@ defined as .. math:: :label: relative_error - r = \frac{s_\bar{X}}{\bar{x}}. + r = \frac{s_{\bar{X}}}{\bar{x}}. Based on this definition, one can see that a higher FOM is desirable. The FOM is useful as a comparative tool. For example, if a variance reduction technique is being applied to a simulation, the FOM with variance reduction can be compared to the FOM without variance reduction to ascertain whether the reduction in variance outweighs the potential increase in execution time (e.g., due to -particle splitting). +particle splitting). It is important to note that MCNP reports the FOM using CPU +time (wall-clock time multiplied by the number of threads/cores), whereas OpenMC +reports the FOM using only the wall-clock time :math:`t`. Confidence Intervals ++++++++++++++++++++ @@ -521,6 +618,8 @@ improve the estimate of the percentile. .. rubric:: References +.. _confidence-interval heuristics: https://doi.org/10.1080/00031305.1990.10475751 + .. _following approximation: https://doi.org/10.1080/03610918708812641 .. _Bessel's correction: https://en.wikipedia.org/wiki/Bessel's_correction @@ -541,6 +640,10 @@ improve the estimate of the percentile. .. _converges in distribution: https://en.wikipedia.org/wiki/Convergence_of_random_variables#Convergence_in_distribution +.. _skewness: https://en.wikipedia.org/wiki/Skewness + +.. _kurtosis: https://en.wikipedia.org/wiki/Kurtosis + .. _confidence intervals: https://en.wikipedia.org/wiki/Confidence_interval .. _Student's t-distribution: https://en.wikipedia.org/wiki/Student%27s_t-distribution diff --git a/include/openmc/constants.h b/include/openmc/constants.h index a0d164613..b66193481 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -291,7 +291,7 @@ enum class MgxsType { // ============================================================================ // TALLY-RELATED CONSTANTS -enum class TallyResult { VALUE, SUM, SUM_SQ, SIZE }; +enum class TallyResult { VALUE, SUM, SUM_SQ, SUM_THIRD, SUM_FOURTH }; enum class TallyType { VOLUME, MESH_SURFACE, SURFACE, PULSE_HEIGHT }; diff --git a/include/openmc/hdf5_interface.h b/include/openmc/hdf5_interface.h index 0092c08f8..28b0d2b11 100644 --- a/include/openmc/hdf5_interface.h +++ b/include/openmc/hdf5_interface.h @@ -100,8 +100,8 @@ void read_llong(hid_t obj_id, const char* name, long long* buffer, bool indep); void read_string( hid_t obj_id, const char* name, size_t slen, char* buffer, bool indep); -void read_tally_results( - hid_t group_id, hsize_t n_filter, hsize_t n_score, double* results); +void read_tally_results(hid_t group_id, hsize_t n_filter, hsize_t n_score, + hsize_t n_results, double* results); void write_attr_double(hid_t obj_id, int ndim, const hsize_t* dims, const char* name, const double* buffer); void write_attr_int(hid_t obj_id, int ndim, const hsize_t* dims, @@ -114,9 +114,9 @@ void write_int(hid_t group_id, int ndim, const hsize_t* dims, const char* name, void write_llong(hid_t group_id, int ndim, const hsize_t* dims, const char* name, const long long* buffer, bool indep); void write_string(hid_t group_id, int ndim, const hsize_t* dims, size_t slen, - const char* name, char const* buffer, bool indep); -void write_tally_results( - hid_t group_id, hsize_t n_filter, hsize_t n_score, const double* results); + const char* name, const char* buffer, bool indep); +void write_tally_results(hid_t group_id, hsize_t n_filter, hsize_t n_score, + hsize_t n_results, const double* results); } // extern "C" //============================================================================== diff --git a/include/openmc/tallies/tally.h b/include/openmc/tallies/tally.h index 3beeb9d5a..374daff92 100644 --- a/include/openmc/tallies/tally.h +++ b/include/openmc/tallies/tally.h @@ -106,6 +106,8 @@ public: bool writable() const { return writable_; } + bool higher_moments() const { return higher_moments_; } + //---------------------------------------------------------------------------- // Other methods. @@ -190,6 +192,9 @@ private: //! Whether to multiply by atom density for reaction rates bool multiply_density_ {true}; + //! Whether to accumulate higher moments (third and fourth) + bool higher_moments_ {false}; + int64_t index_; }; diff --git a/openmc/statepoint.py b/openmc/statepoint.py index a763db397..11986841f 100644 --- a/openmc/statepoint.py +++ b/openmc/statepoint.py @@ -434,6 +434,10 @@ class StatePoint: if "multiply_density" in group.attrs: tally.multiply_density = group.attrs["multiply_density"].item() > 0 + # Check if tally has higher_moments attribute + if 'higher_moments' in group.attrs: + tally.higher_moments = bool(group.attrs['higher_moments'][()]) + # Read the number of realizations n_realizations = group['n_realizations'][()] diff --git a/openmc/tallies.py b/openmc/tallies.py index 075b1e991..25ec29a58 100644 --- a/openmc/tallies.py +++ b/openmc/tallies.py @@ -3,6 +3,7 @@ from collections.abc import Iterable, MutableSequence import copy from functools import partial, reduce, wraps from itertools import product +from math import sqrt, log from numbers import Integral, Real import operator from pathlib import Path @@ -12,6 +13,7 @@ import h5py import numpy as np import pandas as pd import scipy.sparse as sps +from scipy.stats import chi2, norm import openmc import openmc.checkvalue as cv @@ -91,10 +93,20 @@ class Tally(IDManagerMixin): sum_sq : numpy.ndarray An array containing the sum of each independent realization squared for each bin + sum_third : numpy.ndarray + An array containing the sum of each independent realization to the third power for + each bin + sum_fourth : numpy.ndarray + An array containing the sum of each independent realization to the fourth power for + each bin mean : numpy.ndarray An array containing the sample mean for each bin std_dev : numpy.ndarray An array containing the sample standard deviation for each bin + vov : numpy.ndarray + An array containing the variance of the variance for each tally bin + higher_moments : bool + Whether or not the tally accumulates the sums third and fourth to compute higher-order moments figure_of_merit : numpy.ndarray An array containing the figure of merit for each bin @@ -129,8 +141,12 @@ class Tally(IDManagerMixin): self._sum = None self._sum_sq = None + self._sum_third = None + self._sum_fourth = None self._mean = None self._std_dev = None + self._vov = None + self._higher_moments = False self._simulation_time = None self._with_batch_statistics = False self._derived = False @@ -221,6 +237,15 @@ class Tally(IDManagerMixin): cv.check_type('multiply density', value, bool) self._multiply_density = value + @property + def higher_moments(self) -> bool: + return self._higher_moments + + @higher_moments.setter + def higher_moments(self, value): + cv.check_type("higher_moments", value, bool) + self._higher_moments = value + @property def filters(self): return self._filters @@ -371,6 +396,11 @@ class Tally(IDManagerMixin): # Update nuclides nuclide_names = group['nuclides'][()] self._nuclides = [name.decode().strip() for name in nuclide_names] + # Check for higher_moments attribute + if "higher_moments" in group.attrs: + self._higher_moments = bool(group.attrs["higher_moments"][()]) + else: + self._higher_moments = False # Extract Tally data from the file data = group['results'] @@ -385,10 +415,25 @@ class Tally(IDManagerMixin): self._sum = sum_ self._sum_sq = sum_sq + if self._higher_moments: + # Extract additional Tally data when higher moments enabled + sum_third = data[:, :, 2] + sum_fourth = data[:, :, 3] + + # Reshape the results arrays + sum_third = np.reshape(sum_third, self.shape) + sum_fourth = np.reshape(sum_fourth, self.shape) + + # Set the additional data for this Tally + self._sum_third = sum_third + self._sum_fourth = sum_fourth + # Convert NumPy arrays to SciPy sparse LIL matrices if self.sparse: self._sum = sps.lil_matrix(self._sum.flatten(), self._sum.shape) self._sum_sq = sps.lil_matrix(self._sum_sq.flatten(), self._sum_sq.shape) + self._sum_third = sps.lil_matrix(self._sum_third.flatten(), self._sum_third.shape) + self._sum_fourth = sps.lil_matrix(self.sum_fourth.flatten(), self._sum_fourth.shape) # Read simulation time (needed for figure of merit) self._simulation_time = f["runtime"]["simulation"][()] @@ -428,6 +473,52 @@ class Tally(IDManagerMixin): cv.check_type('sum_sq', sum_sq, Iterable) self._sum_sq = sum_sq + @property + @ensure_results + def sum_third(self): + if not self._higher_moments: + raise ValueError( + "Higher moments have not been enabled for this tally. To make " + "higher moments available, set the higher_moments attribute to " + "True before running a simulation." + ) + + if not self._sp_filename or self.derived: + return None + + if self.sparse: + return np.reshape(self._sum_third.toarray(), self.shape) + else: + return self._sum_third + + @sum_third.setter + def sum_third(self, sum_third): + cv.check_type("sum_third", sum_third, Iterable) + self._sum_third = sum_third + + @property + @ensure_results + def sum_fourth(self): + if not self._higher_moments: + raise ValueError( + "Higher moments have not been enabled for this tally. To make " + "higher moments available, set the higher_moments attribute to " + "True before running a simulation." + ) + + if not self._sp_filename or self.derived: + return None + + if self.sparse: + return np.reshape(self._sum_fourth.toarray(), self.shape) + else: + return self._sum_fourth + + @sum_fourth.setter + def sum_fourth(self, sum_fourth): + cv.check_type("sum_fourth", sum_fourth, Iterable) + self._sum_fourth = sum_fourth + @property def mean(self): if self._mean is None: @@ -470,14 +561,370 @@ class Tally(IDManagerMixin): else: return self._std_dev + @property + def vov(self): + if self._vov is None: + n = self.num_realizations + sum1 = self.sum + sum2 = self.sum_sq + sum3 = self.sum_third + sum4 = self.sum_fourth + self._vov = np.zeros_like(sum1, dtype=float) + + # Calculate the variance of the variance (Eq. 2.232 in + # https://doi.org/10.2172/2372634) + numerator = (sum4 - (4.0*sum3*sum1)/n + + (6.0*sum2*(sum1**2))/(n**2) + - (3.0*(sum1)**4)/(n**3)) + denominator = (sum2 - (1.0/n)*(sum1**2))**2 + + mask = denominator > 0.0 + + self._vov[mask] = numerator[mask]/denominator[mask] - 1.0/n + + if self.sparse: + self._vov = sps.lil_matrix(self._vov.flatten(), self._vov.shape) + + if self.sparse: + return np.reshape(self._vov.toarray(), self.shape) + else: + return self._vov + + @property + def m2(self): + n = self.num_realizations + return self.sum_sq/n - self.mean**2 + + @property + def m3(self): + n = self.num_realizations + mean = self.mean + sum2 = self.sum_sq/n + sum3 = self.sum_third/n + + return sum3 - 3.0*mean*sum2 + 2.0*mean**3 + + @property + def m4(self): + n = self.num_realizations + mean = self.mean + sum2 = self.sum_sq/n + sum3 = self.sum_third/n + sum4 = self.sum_fourth/n + + return sum4 - 4.0*mean*sum3 + 6.0*(mean**2)*sum2 - 3.0*mean**4 + + def skew(self, bias=False) -> np.ndarray: + """Return the sample skewness of each tally bin. + + This method computes and returns the unadjusted or adjusted + Fisher-Pearson coefficient of skewness. + + Parameters + ---------- + bias : bool + If False, calculations are corrected for bias and the adjusted + Fisher-Pearson skewness (:math:`G_1`) is returned. If True, + calculations are not corrected for bias and the unadjusted skewness + (:math:`g_1`) is returned. + + Returns + ------- + float + The skewness of each tally bin + """ + n = self.num_realizations + m2 = self.m2 + m3 = self.m3 + + with np.errstate(divide="ignore", invalid="ignore"): + g1 = np.where(m2 > 0.0, m3/(m2**1.5), 0.0) + + if bias: + return g1 + else: + if n <= 2: + raise ValueError("Insufficient number of independent realizations" + f"for bias-corrected skewness: need n >= 3, got {n=}.") + else: + return sqrt(n*(n - 1))/(n - 2)*g1 + + def kurtosis(self, fisher=True, bias=False) -> np.ndarray: + r"""Return the sample kurtosis of each tally bin. + + This method computes and returns the sample kurtosis using either + Pearson's or Fisher's definition, with or without finite-sample bias + correction. The value returned depends on the `bias` and `fisher` + arguments as follows: + + - **bias=True, fisher=False**: Returns :math:`b_2` (Pearson's kurtosis) + This is the raw fourth standardized moment: :math:`m_4/m_2^2`. For a + normal distribution, :math:`b_2\approx 3`. + + - **bias=True, fisher=True**: Returns :math:`g_2` (excess kurtosis) This + is :math:`b_2 - 3`, centered at 0 for normal distributions. Positive + values indicate heavier tails, negative values lighter tails. + + - **bias=False, fisher=True** (default): Returns :math:`G_2` (adjusted + excess kurtosis). This applies finite-sample bias correction to + :math:`g_2`. This is the recommended estimator for statistical + inference. + + - **bias=False, fisher=False**: Returns bias-corrected Pearson's + kurtosis. This is :math:`G_2 + 3`. + + Parameters + ---------- + fisher : bool, optional + If True (default), Fisher's definition is used (excess kurtosis). If + False, Pearson's definition is used. + bias : bool, optional + If False (default), calculations are corrected for statistical bias + using finite-sample adjustments. If True, calculations use the + biased estimator (population formulas). + + Returns + ------- + numpy.ndarray + The kurtosis of each tally bin + + """ + n = self.num_realizations + m2 = self.m2 + m4 = self.m4 + + with np.errstate(divide="ignore", invalid="ignore"): + b2 = np.where(m2 > 0.0, m4/(m2**2), 0.0) + g2 = b2 - 3.0 + + if bias: + # Biased estimator (g2 or b2) + return g2 if fisher else b2 + else: + # Unbiased estimator with finite-sample correction + if n <= 3: + raise ValueError("Insufficient number of independent realizations" + f"for bias-corrected kurtosis: need n >= 4, got {n=}.") + else: + G2 = ((n - 1)/((n - 2)*(n - 3)))*((n + 1)*g2 + 6.0) + return G2 if fisher else G2 + 3.0 + + def skewtest(self, alternative: str = "two-sided"): + """Perform D'Agostino and Pearson's test for skewness. + + This method tests the null hypothesis that the skewness of the + population that the sample was drawn from is the same as that of a + corresponding normal distribution. + + Parameters + ---------- + alternative : {'two-sided', 'less', 'greater'}, optional + Defines the alternative hypothesis. The following options are + available: + + * 'two-sided': the skewness of the distribution is different from + that of the normal distribution (i.e., non-zero) + * 'less': the skewness of the distribution is less than that of the + normal distribution + * 'greater': the skewness of the distribution is greater than that + of the normal distribution + + Returns + ------- + statistic : np.ndarray + The computed z-score for the skewness test for each tally bin + pvalue : np.ndarray + The p-value for the hypothesis test for each tally bin + + Notes + ----- + This test is based on D'Agostino and Pearson's test [1]_. The test + requires at least 8 realizations to produce valid results. + + References + ---------- + .. [1] D'Agostino, R. B. (1971), "An omnibus test of normality for + moderate and large sample size", Biometrika, 58, 341-348 + + """ + n = self.num_realizations + if n < 8: + raise ValueError("Skewness test is not well-defined for n < 8.") + + g1 = self.skew(bias=True) + + # --- Z1 (skewness) --- + y = g1 * sqrt(((n + 1.0)*(n + 3.0))/(6.0*(n - 2.0))) + beta2 = (3.0*(n**2 + 27.0*n - 70.0)*(n + 1.0)*(n + 3.0) + )/((n - 2.0)*(n + 5.0)*(n + 7.0)*(n + 9.0)) + W2 = -1.0 + sqrt(2.0*(beta2 - 1.0)) + delta = 1.0 / sqrt(log(sqrt(W2))) + alpha = sqrt(2.0 / (W2 - 1.0)) + Zb1 = np.where( + y >= 0.0, + delta*np.log((y/alpha) + np.sqrt((y/alpha)**2 + 1.0)), + -delta*np.log((-y/alpha) + np.sqrt((y/alpha)**2 + 1.0)) + ) + + # p-value + if alternative == "two-sided": + p = 2.0 * (1.0 - norm.cdf(np.abs(Zb1))) + elif alternative == "greater": + p = 1.0 - norm.cdf(Zb1) + elif alternative == "less": + p = norm.cdf(Zb1) + else: + raise ValueError("alternative must be 'two-sided', 'greater', or 'less'") + + return Zb1, p + + def kurtosistest(self, alternative: str = "two-sided"): + """Perform D'Agostino and Pearson's test for kurtosis. + + This method tests the null hypothesis that the kurtosis of the + population that the sample was drawn from is the same as that of a + corresponding normal distribution. + + Parameters + ---------- + alternative : {'two-sided', 'less', 'greater'}, optional + Defines the alternative hypothesis. Default is 'two-sided'. + The following options are available: + + * 'two-sided': the kurtosis of the distribution is different from + that of the normal distribution + * 'less': the kurtosis of the distribution is less than that of the + normal distribution + * 'greater': the kurtosis of the distribution is greater than that + of the normal distribution + + Returns + ------- + statistic : np.ndarray + The computed z-score for the kurtosis test for each tally bin + pvalue : np.ndarray + The p-value for the hypothesis test for each tally bin + + Raises + ------ + ValueError + If the number of realizations is less than 20, or if an invalid + alternative hypothesis is specified. + + Notes + ----- + This test is based on D'Agostino and Pearson's test [1]_. The test + is typically recommended for at least 20 realizations to produce + valid results. + + References + ---------- + .. [1] D'Agostino, R. B. (1971), "An omnibus test of normality for + moderate and large sample size", Biometrika, 58, 341-348 + + """ + n = self.num_realizations + if n < 20: + raise ValueError("Kurtosis test is typically recommended for n >= 20.") + + b2 = self.kurtosis(bias=True, fisher=False) + + # --- Z2 (kurtosis) --- + mean_b2 = 3.0 * (n - 1.0) / (n + 1.0) + var_b2 = (24.0*n*(n - 2.0)*(n - 3.0)/( + (n + 1.0)**2*(n + 3.0)*(n + 5.0))) + x = (b2 - mean_b2)/np.sqrt(var_b2) + moment = ((6.0*(n**2 - 5.0*n + 2.0))/((n + 7.0)*(n + 9.0)) + )*sqrt((6.0*(n + 3.0)*(n + 5.0))/(n*(n - 2.0)*(n - 3.0))) + A = 6.0 + (8.0/moment)*((2.0/moment) + sqrt(1.0 + 4.0/(moment**2))) + Zb2 = (1.0- 2.0/(9.0*A) - ((1.0 - 2.0/A) / (1.0 + (x + )*sqrt(2.0/(A - 4.0))))**(1.0/3.0)) / sqrt(2.0/(9.0*A)) + + # p-value + if alternative == "two-sided": + p = 2.0 * (1.0 - norm.cdf(np.abs(Zb2))) + elif alternative == "greater": + p = 1.0 - norm.cdf(Zb2) + elif alternative == "less": + p = norm.cdf(Zb2) + else: + raise ValueError("alternative must be 'two-sided', 'greater', or 'less'") + + return Zb2, p + + def normaltest(self, alternative: str = "two-sided"): + """Perform D'Agostino and Pearson's omnibus test for normality. + + This method tests the null hypothesis that a sample comes from a + normal distribution. It combines skewness and kurtosis to produce an + omnibus test of normality. + + Parameters + ---------- + alternative : {'two-sided', 'less', 'greater'}, optional + Defines the alternative hypothesis used for the component skewness + and kurtosis tests. Default is 'two-sided'. The following options + are available: + + * 'two-sided': the distribution is different from normal + * 'less': used for the component tests + * 'greater': used for the component tests + + Returns + ------- + statistic : np.ndarray + The computed z-score for the normality test for each tally bin + pvalue : np.ndarray + The p-value for the hypothesis test for each tally bin + + Raises + ------ + ValueError + If the number of realizations is less than 20, or if an invalid + alternative hypothesis is specified. + + Notes + ----- + This test combines a test for skewness and a test for kurtosis to + produce an omnibus test [1]_. The test statistic is: + + .. math:: + + K^2 = Z_1^2 + Z_2^2 + + where :math:`Z_1` is the z-score from the skewness test and + :math:`Z_2` is the z-score from the kurtosis test. This statistic + follows a chi-square distribution with 2 degrees of freedom. + + The test requires at least 20 realizations to produce valid results. + + References + ---------- + .. [1] D'Agostino, R. B. and Pearson, E. S. (1973), "Tests for + departure from normality", Biometrika, 60, 613-622 + + """ + n = self.num_realizations + if n < 20: + raise ValueError("normaltest requires n >= 20 (per D'Agostino-Pearson).") + + # Use the component tests + Z1, _ = self.skewtest(alternative) + Z2, _ = self.kurtosistest(alternative) + + # Combine as chi-square with df=2 since we have skewness and kurtosis + K2 = Z1*Z1 + Z2*Z2 + p = chi2.sf(K2, df=2) + return K2, p + @property def figure_of_merit(self): mean = self.mean std_dev = self.std_dev fom = np.zeros_like(mean) nonzero = np.abs(mean) > 0 - fom[nonzero] = 1.0 / ( - (std_dev[nonzero] / mean[nonzero])**2 * self._simulation_time) + rel_err = std_dev[nonzero] / mean[nonzero] + fom[nonzero] = 1.0 / (rel_err**2 * self._simulation_time) return fom @property @@ -528,6 +975,12 @@ class Tally(IDManagerMixin): if self._sum_sq is not None: self._sum_sq = sps.lil_matrix(self._sum_sq.flatten(), self._sum_sq.shape) + if self._sum_third is not None: + self._sum_third = sps.lil_matrix(self._sum_third.flatten(), + self._sum_third.shape) + if self._sum_fourth is not None: + self._sum_fourth = sps.lil_matrix(self._sum_fourth.flatten(), + self._sum_fourth.shape) if self._mean is not None: self._mean = sps.lil_matrix(self._mean.flatten(), self._mean.shape) @@ -543,6 +996,10 @@ class Tally(IDManagerMixin): self._sum = np.reshape(self._sum.toarray(), self.shape) if self._sum_sq is not None: self._sum_sq = np.reshape(self._sum_sq.toarray(), self.shape) + if self._sum_third is not None: + self._sum_third = np.reshape(self._sum_third.toarray(), self.shape) + if self._sum_fourth is not None: + self._sum_fourth = np.reshape(self._sum_fourth.toarray(), self.shape) if self._mean is not None: self._mean = np.reshape(self._mean.toarray(), self.shape) if self._std_dev is not None: @@ -869,6 +1326,34 @@ class Tally(IDManagerMixin): merged_tally._sum_sq = np.reshape(merged_sum_sq, merged_tally.shape) + # Concatenate sum_third arrays if present in both tallies + if self._sum_third is not None and other._sum_third is not None: + self_sum_third = self.get_reshaped_data(value="sum_third") + other_sum_third = other_copy.get_reshaped_data(value="sum_third") + + if join_right: + merged_sum_third = np.concatenate((self_sum_third, other_sum_third), + axis=merge_axis) + else: + merged_sum_third = np.concatenate((other_sum_third, self_sum_third), + axis=merge_axis) + + merged_tally._sum_third = np.reshape(merged_sum_third, merged_tally.shape) + + # Concatenate sum_fourth arrays if present in both tallies + if self._sum_fourth is not None and other._sum_fourth is not None: + self_sum_fourth = self.get_reshaped_data(value="sum_fourth") + other_sum_fourth = other_copy.get_reshaped_data(value="sum_fourth") + + if join_right: + merged_sum_fourth = np.concatenate((self_sum_fourth, other_sum_fourth), + axis=merge_axis) + else: + merged_sum_fourth = np.concatenate((other_sum_fourth, self_sum_fourth), + axis=merge_axis) + + merged_tally._sum_fourth = np.reshape(merged_sum_fourth, merged_tally.shape) + # Concatenate mean arrays if present in both tallies if self.mean is not None and other.mean is not None: self_mean = self.get_reshaped_data(value='mean') @@ -958,6 +1443,11 @@ class Tally(IDManagerMixin): subelement = ET.SubElement(element, "derivative") subelement.text = str(self.derivative.id) + # Optional higher moments accumulation + if self.higher_moments: + subelement = ET.SubElement(element, "higher_moments") + subelement.text = str(self.higher_moments).lower() + return element def add_results(self, statepoint: cv.PathLike | openmc.StatePoint): @@ -984,8 +1474,12 @@ class Tally(IDManagerMixin): # point are based on the current statepoint file self._sum = None self._sum_sq = None + self._sum_third = None + self._sum_fourth = None self._mean = None self._std_dev = None + self._vov = None + self._higher_moments = False self._num_realizations = 0 self._results_read = False @@ -1355,7 +1849,9 @@ class Tally(IDManagerMixin): (value == 'std_dev' and self.std_dev is None) or \ (value == 'rel_err' and self.mean is None) or \ (value == 'sum' and self.sum is None) or \ - (value == 'sum_sq' and self.sum_sq is None): + (value == 'sum_sq' and self.sum_sq is None) or \ + (value == "sum_third" and self.sum_third is None) or \ + (value == "sum_fourth" and self.sum_fourth is None): msg = f'The Tally ID="{self.id}" has no data to return' raise ValueError(msg) @@ -1378,10 +1874,14 @@ class Tally(IDManagerMixin): data = self.sum[indices] elif value == 'sum_sq': data = self.sum_sq[indices] + elif value == "sum_third": + data = self.sum_third[indices] + elif value == "sum_fourth": + data = self.sum_fourth[indices] else: msg = f'Unable to return results from Tally ID="{value}" since ' \ f'the requested value "{self.id}" is not \'mean\', ' \ - '\'std_dev\', \'rel_err\', \'sum\', or \'sum_sq\'' + '\'std_dev\', \'rel_err\', \'sum\', \'sum_sq\', \'sum_third\' or \'sum_fourth\'' raise LookupError(msg) return data @@ -2711,6 +3211,16 @@ class Tally(IDManagerMixin): new_sum_sq = self.get_values(scores, filters, filter_bins, nuclides, 'sum_sq') new_tally.sum_sq = new_sum_sq + if not self.derived and self._sum_third is not None: + new_sum_third = self.get_values( + scores, filters, filter_bins, nuclides, "sum_third" + ) + new_tally._sum_third = new_sum_third + if not self.derived and self._sum_fourth is not None: + new_sum_fourth = self.get_values( + scores, filters, filter_bins, nuclides, "sum_fourth" + ) + new_tally._sum_fourth = new_sum_fourth if self.mean is not None: new_mean = self.get_values(scores, filters, filter_bins, nuclides, 'mean') @@ -3151,6 +3661,12 @@ class Tally(IDManagerMixin): if not self.derived and self.sum_sq is not None: new_tally._sum_sq = np.zeros(new_tally.shape, dtype=np.float64) new_tally._sum_sq[diag_indices, :, :] = self.sum_sq + if not self.derived and self._sum_third is not None: + new_tally._sum_third = np.zeros(new_tally.shape, dtype=np.float64) + new_tally._sum_third[diag_indices, :, :] = self.sum_third + if not self.derived and self._sum_fourth is not None: + new_tally._sum_fourth = np.zeros(new_tally.shape, dtype=np.float64) + new_tally._sum_fourth[diag_indices, :, :] = self.sum_fourth if self.mean is not None: new_tally._mean = np.zeros(new_tally.shape, dtype=np.float64) new_tally._mean[diag_indices, :, :] = self.mean diff --git a/src/hdf5_interface.cpp b/src/hdf5_interface.cpp index bf1f79549..c56d485e2 100644 --- a/src/hdf5_interface.cpp +++ b/src/hdf5_interface.cpp @@ -536,14 +536,14 @@ void read_complex( H5Tclose(complex_id); } -void read_tally_results( - hid_t group_id, hsize_t n_filter, hsize_t n_score, double* results) +void read_tally_results(hid_t group_id, hsize_t n_filter, hsize_t n_score, + hsize_t n_results, double* results) { // Create dataspace for hyperslab in memory constexpr int ndim = 3; - hsize_t dims[ndim] {n_filter, n_score, 3}; + hsize_t dims[ndim] {n_filter, n_score, n_results}; hsize_t start[ndim] {0, 0, 1}; - hsize_t count[ndim] {n_filter, n_score, 2}; + hsize_t count[ndim] {n_filter, n_score, n_results - 1}; hid_t memspace = H5Screate_simple(ndim, dims, nullptr); H5Sselect_hyperslab(memspace, H5S_SELECT_SET, start, nullptr, count, nullptr); @@ -686,15 +686,15 @@ void write_string( group_id, 0, nullptr, buffer.length(), name, buffer.c_str(), indep); } -void write_tally_results( - hid_t group_id, hsize_t n_filter, hsize_t n_score, const double* results) +void write_tally_results(hid_t group_id, hsize_t n_filter, hsize_t n_score, + hsize_t n_results, const double* results) { // Set dimensions of sum/sum_sq hyperslab to store constexpr int ndim = 3; - hsize_t count[ndim] {n_filter, n_score, 2}; + hsize_t count[ndim] {n_filter, n_score, n_results - 1}; // Set dimensions of results array - hsize_t dims[ndim] {n_filter, n_score, 3}; + hsize_t dims[ndim] {n_filter, n_score, n_results}; hsize_t start[ndim] {0, 0, 1}; hid_t memspace = H5Screate_simple(ndim, dims, nullptr); H5Sselect_hyperslab(memspace, H5S_SELECT_SET, start, nullptr, count, nullptr); diff --git a/src/state_point.cpp b/src/state_point.cpp index 8195c4865..0b0fed132 100644 --- a/src/state_point.cpp +++ b/src/state_point.cpp @@ -201,6 +201,12 @@ extern "C" int openmc_statepoint_write(const char* filename, bool* write_source) write_attribute(tally_group, "multiply_density", 0); } + if (tally->higher_moments()) { + write_attribute(tally_group, "higher_moments", 1); + } else { + write_attribute(tally_group, "higher_moments", 0); + } + if (tally->estimator_ == TallyEstimator::ANALOG) { write_dataset(tally_group, "estimator", "analog"); } else if (tally->estimator_ == TallyEstimator::TRACKLENGTH) { @@ -264,12 +270,13 @@ extern "C" int openmc_statepoint_write(const char* filename, bool* write_source) for (const auto& tally : model::tallies) { if (!tally->writable_) continue; - // Write sum and sum_sq for each bin + + // Write results for each bin 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.data()); + results.shape()[1], results.shape()[2], results.data()); close_group(tally_group); } } else { @@ -509,7 +516,8 @@ extern "C" int openmc_statepoint_load(const char* filename) } else { auto& results = tally->results_; read_tally_results(tally_group, results.shape()[0], - results.shape()[1], results.data()); + results.shape()[1], results.shape()[2], results.data()); + read_dataset(tally_group, "n_realizations", tally->n_realizations_); close_group(tally_group); } @@ -1001,7 +1009,8 @@ void write_tally_results_nr(hid_t file_id) // Write reduced tally results to file auto shape = results_copy.shape(); - write_tally_results(tally_group, shape[0], shape[1], results_copy.data()); + write_tally_results( + tally_group, shape[0], shape[1], shape[2], results_copy.data()); close_group(tally_group); } else { diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index 12ee3427e..9daeb1d69 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -107,6 +107,9 @@ Tally::Tally(pugi::xml_node node) multiply_density_ = get_node_value_bool(node, "multiply_density"); } + if (check_for_node(node, "higher_moments")) { + higher_moments_ = get_node_value_bool(node, "higher_moments"); + } // ======================================================================= // READ DATA FOR FILTERS @@ -800,7 +803,11 @@ void Tally::init_triggers(pugi::xml_node node) void Tally::init_results() { int n_scores = scores_.size() * nuclides_.size(); - results_ = xt::empty({n_filter_bins_, n_scores, 3}); + if (higher_moments_) { + results_ = xt::empty({n_filter_bins_, n_scores, 5}); + } else { + results_ = xt::empty({n_filter_bins_, n_scores, 3}); + } } void Tally::reset() @@ -838,14 +845,33 @@ void Tally::accumulate() norm = 1.0; } -// Accumulate each result + // Accumulate each result + if (higher_moments_) { #pragma omp parallel for - for (int i = 0; i < results_.shape()[0]; ++i) { - 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; - results_(i, j, TallyResult::SUM_SQ) += val * val; + // filter bins (specific cell, energy bins) + 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) { + double val = results_(i, j, TallyResult::VALUE) * norm; + double val2 = val * val; + results_(i, j, TallyResult::VALUE) = 0.0; + results_(i, j, TallyResult::SUM) += val; + results_(i, j, TallyResult::SUM_SQ) += val2; + results_(i, j, TallyResult::SUM_THIRD) += val2 * val; + results_(i, j, TallyResult::SUM_FOURTH) += val2 * val2; + } + } + } else { +#pragma omp parallel for + // filter bins (specific cell, energy bins) + 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) { + double val = results_(i, j, TallyResult::VALUE) * norm; + results_(i, j, TallyResult::VALUE) = 0.0; + results_(i, j, TallyResult::SUM) += val; + results_(i, j, TallyResult::SUM_SQ) += val * val; + } } } } diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index 674fae49c..9333800bc 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -547,8 +547,10 @@ void WeightWindows::update_weights(const Tally* tally, const std::string& value, // build a shape for a view of the tally results, this will always be // dimension 5 (3 filter dimensions, 1 score dimension, 1 results dimension) - std::array shape = { - 1, 1, 1, tally->n_scores(), static_cast(TallyResult::SIZE)}; + // 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]); + std::array shape = {1, 1, 1, tally->n_scores(), results_dim}; // set the shape for the filters applied on the tally for (int i = 0; i < tally->filters().size(); i++) { @@ -586,7 +588,7 @@ void WeightWindows::update_weights(const Tally* tally, const std::string& value, // get a fully reshaped view of the tally according to tally ordering of // filters - auto tally_values = xt::reshape_view(tally->results(), shape); + 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); diff --git a/tests/unit_tests/test_tallies.py b/tests/unit_tests/test_tallies.py index c38f067d5..7b1bf0a2f 100644 --- a/tests/unit_tests/test_tallies.py +++ b/tests/unit_tests/test_tallies.py @@ -1,6 +1,8 @@ +from math import sqrt import numpy as np import pytest import openmc +import scipy.stats as sps def test_xml_roundtrip(run_in_tmpdir): @@ -163,3 +165,214 @@ def test_tally_application(sphere_model, run_in_tmpdir): assert (sp_tally.std_dev == tally.std_dev).all() assert (sp_tally.mean == tally.mean).all() assert sp_tally.nuclides == tally.nuclides + +def _tally_from_data(x, *, higher_moments=True, normality=True): + t = openmc.Tally() + t.scores = ["flux"] # 1 score + t.nuclides = [openmc.Nuclide("H1")] # 1 nuclide + t._sp_filename = "dummy.h5" # mark "results available" + t._results_read = True # don't try to read from disk + t._num_realizations = int(len(x)) # n + t.higher_moments = bool(higher_moments) + + x = np.asarray(x, dtype=float) + # (num_filter_bins=1, num_nuclides=1, num_scores=1) -> (1,1,1) arrays + t._sum = np.array([[[np.sum(x)]]], dtype=float) + t._sum_sq = np.array([[[np.sum(x**2)]]], dtype=float) + if higher_moments: + t._sum_third = np.array([[[np.sum(x**3)]]], dtype=float) + t._sum_fourth = np.array([[[np.sum(x**4)]]], dtype=float) + return t + +@pytest.mark.parametrize( + "x, skew_true, kurt_true", + [ # Rademacher distribution + (np.array([1.0, -1.0] * 200), 0.0, 1.0), + # Two-point {0,3} with p(0)=3/4, p(3)=1/4 + (np.concatenate([np.zeros(600), np.full(200, 3.0)]), 2.0 / sqrt(3.0), 7.0 / 3.0), + # Bernoulli distribution + (np.concatenate([np.ones(300), np.zeros(700)]), (1 - 2 * 0.3) / sqrt(0.3 * 0.7), (1 - 3 * 0.3 + 3 * 0.3**2) / (0.3 * 0.7)), + ], +) +def test_b1_b2_analytical_against_tally(x, skew_true, kurt_true): + t = _tally_from_data(x, higher_moments=True, normality=False) + + g1 = t.skew(bias=True)[0, 0, 0] + b2 = t.kurtosis(bias=True, fisher=False)[0, 0, 0] + + assert np.isclose(g1, skew_true, rtol=0, atol=1e-12) + assert np.isclose(b2, kurt_true, rtol=0, atol=1e-12) + +@pytest.mark.parametrize( + "draw, skew_true, kurt_true", + [(lambda rng, n: rng.normal(0, 1, n), 0.0, 3.0), # Normal + (lambda rng, n: rng.random(n), 0.0, 1.8), # Uniform(0,1) + (lambda rng, n: rng.exponential(1.0, n), 2.0, 9.0), # Exp(1) + (lambda rng, n: (rng.random(n) < 0.3).astype(float), + (1 - 2 * 0.3) / sqrt(0.3 * 0.7), + (1 - 3 * 0.3 + 3 * 0.3**2) / (0.3 * 0.7),),],) + +def test_b1_b2_scipy_and_theory(draw, skew_true, kurt_true): + rng = np.random.default_rng(12345) + N = 200_000 + x = draw(rng, N) + + # Tally outputs + t = _tally_from_data(x, higher_moments=True, normality=False) + g1_t = t.skew(bias=True)[0, 0, 0] + b2_t = t.kurtosis(bias=True, fisher=False)[0, 0, 0] + + # SciPy (population, bias=True to match population-moment style) + skew_sp = sps.skew(x, bias=True) + kurt_sp = sps.kurtosis(x, fisher=False, bias=True) + + # Compare to SciPy numerically + assert np.isclose(g1_t, skew_sp, rtol=0, atol=5e-3) + assert np.isclose(b2_t, kurt_sp, rtol=0, atol=5e-3) + + # Compare to analytical targets with size-dependent tolerances + tol_skew = 0.02 if abs(skew_true) < 0.5 else 0.05 + tol_kurt = 0.03 if kurt_true < 4 else 0.1 + assert abs(g1_t - skew_true) < tol_skew + assert abs(b2_t - kurt_true) < tol_kurt + + +def test_kurtosis_bias_fisher_combinations(): + """Test that all combinations of bias and fisher match scipy.stats.kurtosis""" + rng = np.random.default_rng(42) + x = rng.normal(0, 1, 10000) + + t = _tally_from_data(x, higher_moments=True, normality=False) # Test all four combinations + # 1. bias=True, fisher=False (Pearson's kurtosis, b2) + b2_tally = t.kurtosis(bias=True, fisher=False)[0, 0, 0] + b2_scipy = sps.kurtosis(x, fisher=False, bias=True) + assert np.isclose(b2_tally, b2_scipy, rtol=0, atol=1e-10) + assert np.isclose(b2_tally, 3.0, rtol=0.05, atol=0.1) # Should be ~3 for normal + + # 2. bias=True, fisher=True (excess kurtosis, g2) + g2_tally = t.kurtosis(bias=True, fisher=True)[0, 0, 0] + g2_scipy = sps.kurtosis(x, fisher=True, bias=True) + assert np.isclose(g2_tally, g2_scipy, rtol=0, atol=1e-10) + assert np.isclose(g2_tally, 0.0, rtol=0, atol=0.1) # Should be ~0 for normal + assert np.isclose(g2_tally, b2_tally - 3.0, rtol=0, atol=1e-10) # g2 = b2 - 3 + + # 3. bias=False, fisher=True (adjusted excess kurtosis, G2) + G2_tally = t.kurtosis(bias=False, fisher=True)[0, 0, 0] + G2_tally_default = t.kurtosis()[0, 0, 0] # Should be same as default + G2_scipy = sps.kurtosis(x, fisher=True, bias=False) + assert np.isclose(G2_tally, G2_tally_default, rtol=0, atol=1e-10) + assert np.isclose(G2_tally, G2_scipy, rtol=0, atol=1e-10) + assert np.isclose(G2_tally, 0.0, rtol=0, atol=0.1) # Should be ~0 for normal + + # 4. bias=False, fisher=False (adjusted Pearson's kurtosis) + adj_b2_tally = t.kurtosis(bias=False, fisher=False)[0, 0, 0] + adj_b2_scipy = sps.kurtosis(x, fisher=False, bias=False) + assert np.isclose(adj_b2_tally, adj_b2_scipy, rtol=0, atol=1e-10) + assert np.isclose(adj_b2_tally, 3.0, rtol=0.05, atol=0.1) # Should be ~3 for normal + assert np.isclose(adj_b2_tally, G2_tally + 3.0, rtol=0, atol=1e-10) # adj_b2 = G2 + 3 + + +def test_ztests_scipy_comparison(): + rng = np.random.default_rng(987) + x_norm = rng.normal(size=50_000) + x_exp = rng.exponential(size=50_000) + + # -------- Normal dataset (should not reject) -------- + t0 = _tally_from_data(x_norm, higher_moments=True, normality=True) + Zb1_0, p_skew_0 = t0.skewtest(alternative="two-sided") + Zb2_0, p_kurt_0 = t0.kurtosistest(alternative="two-sided") + K2_0, p_omni_0 = t0.normaltest(alternative="two-sided") + + Zb1_0 = Zb1_0.ravel()[0] + p_skew_0 = p_skew_0.ravel()[0] + Zb2_0 = Zb2_0.ravel()[0] + p_kurt_0 = p_kurt_0.ravel()[0] + K2_0 = K2_0.ravel()[0] + p_omni_0 = p_omni_0.ravel()[0] + + z_skew_sp0, p_skew_sp0 = sps.skewtest(x_norm) + z_kurt_sp0, p_kurt_sp0 = sps.kurtosistest(x_norm) + k2_sp0, p_omni_sp0 = sps.normaltest(x_norm) + + assert np.isclose(Zb1_0, z_skew_sp0, atol=0.15) + assert np.isclose(Zb2_0, z_kurt_sp0, atol=0.15) + assert np.isclose(K2_0, k2_sp0, atol=0.30) + assert np.isclose(p_skew_0, p_skew_sp0, atol=5e-3) + assert np.isclose(p_kurt_0, p_kurt_sp0, atol=5e-3) + assert np.isclose(p_omni_0, p_omni_sp0, atol=5e-3) + + # -------- Exponential dataset (should strongly reject) -------- + t1 = _tally_from_data(x_exp, higher_moments=True, normality=True) + + Zb1_1, p_skew_1 = t1.skewtest(alternative="two-sided") + Zb2_1, p_kurt_1 = t1.kurtosistest(alternative="two-sided") + K2_1, p_omni_1 = t1.normaltest(alternative="two-sided") + + Zb1_1 = Zb1_1.ravel()[0] + p_skew_1 = p_skew_1.ravel()[0] + Zb2_1 = Zb2_1.ravel()[0] + p_kurt_1 = p_kurt_1.ravel()[0] + K2_1 = K2_1.ravel()[0] + p_omni_1 = p_omni_1.ravel()[0] + + z_skew_sp1, p_skew_sp1 = sps.skewtest(x_exp) + z_kurt_sp1, p_kurt_sp1 = sps.kurtosistest(x_exp) + k2_sp1, p_omni_sp1 = sps.normaltest(x_exp) + + # Both pipelines should reject very strongly + assert p_skew_1 < 1e-6 and p_skew_sp1 < 1e-6 + assert p_kurt_1 < 1e-6 and p_kurt_sp1 < 1e-6 + assert p_omni_1 < 1e-6 and p_omni_sp1 < 1e-6 + + # Right-skewed and heavy-tailed → large positive Z-statistics + assert Zb1_1 > 30 and z_skew_sp1 > 30 + assert Zb2_1 > 30 and z_kurt_sp1 > 30 + assert K2_1 > 2000 and k2_sp1 > 2000 + +def test_vov_stochastic(sphere_model, run_in_tmpdir): + tally = openmc.Tally(name="test tally") + ef = openmc.EnergyFilter([0.0, 0.1, 1.0, 10.0e6]) + mesh = openmc.RegularMesh.from_domain(sphere_model.geometry, (2, 2, 2)) + mf = openmc.MeshFilter(mesh) + tally.filters = [ef, mf] + tally.scores = ["flux", "absorption", "fission", "scatter"] + tally.higher_moments = True + sphere_model.tallies = [tally] + + sp_file = sphere_model.run(apply_tally_results=True) + + assert tally._mean is None + assert tally._std_dev is None + assert tally._sum is None + assert tally._sum_sq is None + assert tally._sum_third is None + assert tally._sum_fourth is None + assert tally._num_realizations == 0 + assert tally._sp_filename == sp_file + + with openmc.StatePoint(sp_file) as sp: + assert tally in sp.tallies.values() + sp_tally = sp.tallies[tally.id] + + assert np.all(sp_tally.std_dev == tally.std_dev) + assert np.all(sp_tally.mean == tally.mean) + assert np.all(sp_tally.vov == tally.vov) + assert sp_tally.nuclides == tally.nuclides + + n = sp_tally.num_realizations + mean = sp_tally.mean + sum_ = sp_tally._sum + sum_sq = sp_tally._sum_sq + sum_third = sp_tally._sum_third + sum_fourth = sp_tally._sum_fourth + + expected_vov = np.zeros_like(mean) + nonzero = np.abs(mean) > 0 + + num = (sum_fourth - (4.0*sum_third*sum_)/n + (6.0*sum_sq*sum_**2)/(n**2) + - (3.0*sum_**4)/(n**3)) + den = (sum_sq - (1.0/n)*sum_**2)**2 + + expected_vov[nonzero] = num[nonzero]/den[nonzero] - 1.0/n + + assert np.allclose(expected_vov, sp_tally.vov, rtol=1e-7, atol=0.0) From 4e24c2d933dcf92b4ddb51dfca362a20ef82f2f6 Mon Sep 17 00:00:00 2001 From: April Novak Date: Wed, 12 Nov 2025 15:59:45 -0600 Subject: [PATCH 449/671] Update documentation for particle tracks (#3627) Co-authored-by: Paul Romano --- docs/source/usersguide/processing.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/source/usersguide/processing.rst b/docs/source/usersguide/processing.rst index 8b5ae53fa..fe6ab0826 100644 --- a/docs/source/usersguide/processing.rst +++ b/docs/source/usersguide/processing.rst @@ -68,17 +68,17 @@ generation, and particle number of the desired particle. For example, to create a track file for particle 4 of batch 1 and generation 2:: settings = openmc.Settings() - settings.track = (1, 2, 4) + settings.track = [(1, 2, 4)] -To specify multiple particles, the length of the iterable should be a multiple -of three, e.g., if we wanted particles 3 and 4 from batch 1 and generation 2:: +To specify multiple particles, specify a list of tuples, e.g., if we wanted +particles 3 and 4 from batch 1 and generation 2:: - settings.track = (1, 2, 3, 1, 2, 4) + settings.track = [(1, 2, 3), (1, 2, 4)] -After running OpenMC, the working directory will contain a file of the form -"track_(batch #)_(generation #)_(particle #).h5" for each particle tracked. -These track files can be converted into VTK poly data files with the -:class:`openmc.Tracks` class. +After running OpenMC (now, without the ``-t`` argument), the working directory +will contain a file named `tracks.h5`, which contains a collection of particle +tracks. These track files can be converted into VTK poly data files or +matplotlib plots with the :class:`openmc.Tracks` class. ---------------------- Source Site Processing From 50bb0df191053e6fc58b9d779022402ac159115e Mon Sep 17 00:00:00 2001 From: Lorenzo Chierici Date: Wed, 12 Nov 2025 23:45:48 +0100 Subject: [PATCH 450/671] depletion-thermochemistry: Redox control transfer rates (#2783) Co-authored-by: Gavin Ridley Co-authored-by: Paul Romano --- openmc/deplete/abc.py | 30 ++++++++++ openmc/deplete/chain.py | 54 ++++++++++++++++++ openmc/deplete/pool.py | 13 +++++ openmc/deplete/transfer_rates.py | 43 +++++++++++++- .../ref_depletion_with_ext_source.h5 | Bin 37328 -> 37328 bytes .../ref_depletion_with_feed.h5 | Bin 37328 -> 37328 bytes .../ref_depletion_with_redox.h5 | Bin 0 -> 37328 bytes .../ref_depletion_with_removal.h5 | Bin 37328 -> 37328 bytes .../ref_depletion_with_removal_and_redox.h5 | Bin 0 -> 37328 bytes .../ref_depletion_with_transfer.h5 | Bin 37328 -> 37328 bytes .../ref_depletion_with_transfer_and_redox.h5 | Bin 0 -> 37328 bytes .../ref_no_depletion_only_feed.h5 | Bin 37328 -> 37328 bytes .../ref_no_depletion_only_removal.h5 | Bin 37328 -> 37328 bytes .../ref_no_depletion_with_ext_source.h5 | Bin 37328 -> 37328 bytes .../ref_no_depletion_with_transfer.h5 | Bin 37328 -> 37328 bytes .../deplete_with_transfer_rates/test.py | 22 ++++--- .../unit_tests/test_deplete_transfer_rates.py | 32 +++++++++++ 17 files changed, 185 insertions(+), 9 deletions(-) create mode 100644 tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_redox.h5 create mode 100644 tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_removal_and_redox.h5 create mode 100644 tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_transfer_and_redox.h5 diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index c5a6219c7..fb18d86af 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -1013,6 +1013,36 @@ class Integrator(ABC): material, composition, rate, rate_units, timesteps) + def add_redox(self, material, buffer, oxidation_states, timesteps=None): + """Add redox control to depletable material. + + Parameters + ---------- + material : openmc.Material or str or int + Depletable material + buffer : dict + Dictionary of buffer nuclides used to maintain redox balance. Keys + are nuclide names (strings) and values are their respective + fractions (float) that collectively sum to 1. + oxidation_states : dict + User-defined oxidation states for elements. Keys are element symbols + (e.g., 'H', 'He'), and values are their corresponding oxidation + states as integers (e.g., +1, 0). + timesteps : list of int, optional + List of timestep indices where to set external source rates. + Defaults to None, which means the external source rate is set for + all timesteps. + """ + if self.transfer_rates is None: + if hasattr(self.operator, 'model'): + materials = self.operator.model.materials + elif hasattr(self.operator, 'materials'): + materials = self.operator.materials + self.transfer_rates = TransferRates( + self.operator, materials, len(self.timesteps)) + + self.transfer_rates.set_redox(material, buffer, oxidation_states, timesteps) + @add_params class SIIntegrator(Integrator): r"""Abstract class for the Stochastic Implicit Euler integrators diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index ac5c02aa5..f34416d56 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -7,6 +7,7 @@ loaded from an .xml file and all the nuclides are linked together. from io import StringIO from itertools import chain import math +import numpy as np import re from collections import defaultdict, namedtuple from collections.abc import Mapping, Iterable @@ -714,6 +715,59 @@ class Chain: # Return CSC representation instead of DOK return sp.csc_matrix((vals, (rows, cols)), shape=(n, n)) + def add_redox_term(self, matrix, buffer, oxidation_states): + """Adds a redox term to the depletion matrix from data contained in + the matrix itself and a few user-inputs. + + The redox term to add to the buffer nuclide :math:`N_j` can be written + as: :math:`\frac{dN_j(t)}{dt} = + \cdots - \frac{1}{OS_j}\sum_i N_i a_{ij} \cdot OS_i ` + + where :math:`OS` is the oxidation states vector and `a_{ij}` the + corresponding term in the Bateman matrix. + + Parameters + ---------- + matrix : scipy.sparse.csc_matrix + Sparse matrix representing depletion + buffer : dict + Dictionary of buffer nuclides used to maintain anoins net balance. + Keys are nuclide names (strings) and values are their respective + fractions (float) that collectively sum to 1. + oxidation_states : dict + User-defined oxidation states for elements. Keys are element symbols + (e.g., 'H', 'He'), and values are their corresponding oxidation + states as integers (e.g., +1, 0). + Returns + ------- + matrix : scipy.sparse.csc_matrix + Sparse matrix with redox term added + """ + # Elements list with the same size as self.nuclides + elements = [re.split(r'\d+', nuc.name)[0] for nuc in self.nuclides] + + # Match oxidation states with all elements and add 0 if not data + os = np.array([oxidation_states[elm] if elm in oxidation_states else 0 + for elm in elements]) + + # Buffer idx with nuclide index as value + buffer_idx = {nuc: self.nuclide_dict[nuc] for nuc in buffer} + array = matrix.toarray() + redox_change = np.array([]) + + # calculate the redox array + for i in range(len(self)): + # Net redox impact of reaction: multiply the i-th column of the + # depletion matrix by the oxidation states + redox_change = np.append(redox_change, sum(array[:, i]*os)) + + # Subtract redox vector to the buffer nuclides in the matrix scaling by + # their respective oxidation states + for nuc, idx in buffer_idx.items(): + array[idx] -= redox_change * buffer[nuc] / os[idx] + + return sp.csc_matrix(array) + def form_rr_term(self, tr_rates, current_timestep, mats): """Function to form the transfer rate term matrices. diff --git a/openmc/deplete/pool.py b/openmc/deplete/pool.py index 03b050af3..aa348c02a 100644 --- a/openmc/deplete/pool.py +++ b/openmc/deplete/pool.py @@ -109,6 +109,13 @@ def deplete(func, chain, n, rates, dt, current_timestep=None, matrix_func=None, matrices = [matrix - transfer for (matrix, transfer) in zip(matrices, transfers)] + if transfer_rates.redox: + for mat_idx, mat_id in enumerate(transfer_rates.local_mats): + if mat_id in transfer_rates.redox: + matrices[mat_idx] = chain.add_redox_term(matrices[mat_idx], + transfer_rates.redox[mat_id][0], + transfer_rates.redox[mat_id][1]) + if current_timestep in transfer_rates.index_transfer: # Gather all on comm.rank 0 matrices = comm.gather(matrices) @@ -125,6 +132,12 @@ def deplete(func, chain, n, rates, dt, current_timestep=None, matrix_func=None, transfer_matrix = chain.form_rr_term(transfer_rates, current_timestep, mat_pair) + + # check if destination material has a redox control + if mat_pair[0] in transfer_rates.redox: + transfer_matrix = chain.add_redox_term(transfer_matrix, + transfer_rates.redox[mat_pair[0]][0], + transfer_rates.redox[mat_pair[0]][1]) transfer_pair[mat_pair] = transfer_matrix # Combine all matrices together in a single matrix of matrices diff --git a/openmc/deplete/transfer_rates.py b/openmc/deplete/transfer_rates.py index 4f2b9aba5..ea9fc9185 100644 --- a/openmc/deplete/transfer_rates.py +++ b/openmc/deplete/transfer_rates.py @@ -49,9 +49,10 @@ class ExternalRates: self.local_mats = operator.local_mats self.number_of_timesteps = number_of_timesteps - # initialize transfer rates container dict + #initialize transfer rates container dict self.external_rates = {mat: defaultdict(list) for mat in self.burnable_mats} self.external_timesteps = [] + self.redox = {} def _get_material_id(self, val): """Helper method for getting material id from Material obj or name. @@ -300,6 +301,46 @@ class TransferRates(ExternalRates): self.external_timesteps = np.unique(np.concatenate( [self.external_timesteps, timesteps])) + def set_redox(self, material, buffer, oxidation_states, timesteps=None): + """Add redox control to depletable material. + + Parameters + ---------- + material : openmc.Material or str or int + Depletable material + buffer : dict + Dictionary of buffer nuclides used to maintain redox balance. + Keys are nuclide names (strings) and values are their respective + fractions (float) that collectively sum to 1. + oxidation_states : dict + User-defined oxidation states for elements. + Keys are element symbols (e.g., 'H', 'He'), and values are their + corresponding oxidation states as integers (e.g., +1, 0). + timesteps : list of int, optional + List of timestep indices where to set external source rates. + Defaults to None, which means the external source rate is set for + all timesteps. + + """ + material_id = self._get_material_id(material) + if timesteps is not None: + for timestep in timesteps: + check_value('timestep', timestep, range(self.number_of_timesteps)) + timesteps = np.array(timesteps) + else: + timesteps = np.arange(self.number_of_timesteps) + #Check nuclides in buffer exist + for nuc in buffer: + if nuc not in self.chain_nuclides: + raise ValueError(f'{nuc} is not a valid nuclide.') + # Checks element in oxidation states exist + for elm in oxidation_states: + if elm not in ELEMENT_SYMBOL.values(): + raise ValueError(f'{elm} is not a valid element.') + + self.redox[material_id] = (buffer, oxidation_states) + self.external_timesteps = np.unique(np.concatenate( + [self.external_timesteps, timesteps])) class ExternalSourceRates(ExternalRates): """Class for defining external source rates. diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_ext_source.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_ext_source.h5 index 3ed38f3d6ce3973fd63e7967b1246c8012ebf063..2f9951a42aaa0613c0b4a20351ae2c0df985b0bc 100644 GIT binary patch delta 569 zcmcbxnCZe|rVS2#j53oQ`{L`jC9l$uo6Bd%00mo5bLh;i`vB*6-8Fg^a%@Z|^%tzWFI=h?g8OIAU8Q+5L;d0Wiy{t}+CheB5fg=BG-o^RH!sw#G-cKV@?+ lvt52=hS{y(9<~z?PT;!2+W+$S!`t>#eokIBQHcfQX8^fN!^Qvr delta 569 zcmcbxnCZe|rVS2#jIxs*`{L_&C$G|wo6Bd%00p~FbLh;i`vB+9zH9U>A<9GwvP;sMMu~xzzyKY&yn6*$7~OC!0S~X{%IR5 zfIH~(R?pA-SLnmlFa6CcVRLUEoFD6wD}C|0DV%?T@qMWGIZr5Ghk>DNxAxX|6D;8h z!uRO(8|Rq7`3BtGg7OzW*zbHj`BcA>Pr~V`_9Ai?hL(RfIj?-IXu)%AiL?06R;z-| zJDfKbB~3rKWrnlezFmg858|97_9uO*KcDM-K)2w9eprA@^!-^6e!oB9@-EZjXwU$|5)1ozM82TJp1hWf+#f#MFA+A<9Gwl@uqMMu~xzztx@&yn6*$7~OC0RQR_|FjJj zz#SyJ&GYmA75Z@X7k=|f*xcI(=U2MqN?*Kg3g_=&d>`t4&J)VlVPL4(t-bZ#1WULA z?L9jE#yKW%zAtyTp!|go_8VVMKGm;eYMy)4p7roO@nbax&i1Xxp0O+IIp?XaT-X2G z-&si`evf9Gw)3wSZ=X!ra>~idzshk<{vD^2lNBe#*FWgkJSnRZ5!7DGvL?Kcc@MWp zd+k@Q#?^?R{&hwBn8;rdxct>wS&HXgE{F3E1^K6nY*B*qt=xn)Z+)<~W1IU>{{7^C zw(F$#N!OoJx4W+YZrzO+z@S!>jLbeY@4xMJnS~F(zr1SurDjo);Q>Xs0cWOPU+YrE z4iD;j&zaoGrw~D1U8cVxayP=ly?f^ssVzJN*YBUTQRZ1TBB)g*n)PzcgQ4=kptjt2 mSHW(N1zbbsbFtQv79%*n?DLVpyHy|US4U4?HBpHL6x0C2(a??n delta 688 zcmcbxnCZe|rVS2#jIxs*`{L{8C9l$uo6Bd%00nDKbLh;i`vB+fxNGz*A<9Gw&x6vMMu~xzzyKc&yn6*$7~OCz|~bB{%IR5 zfIH~sR?pA-SLnmlXaDAvu(`Jn&Np<)mA-i06wW`*_&(J8oF|m8!@$tGTYKxf36^jL zA$xTCjdM)kd~fb5d`FEVoPga}|Uw^G*^Q5dwL{Jwl%bM^)<~`gZ z$F*O%8doEN`oR_LVY`dXJN zc6d-1dCufcK7|PCiZcBbk-HHV?%q4ENNwRExPHH^jWW-&5kW03(X5wi9t@QS2DQt^ ny9#!DEZ`c-pNqAYv>3tp)t`?9-mUs*?+`Y5)kGy0P*4K^K)BOB diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_redox.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_redox.h5 new file mode 100644 index 0000000000000000000000000000000000000000..c7f002ba14a29133887bb7b7d06cc0a6378a71ec GIT binary patch literal 37328 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 literal 0 HcmV?d00001 diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_removal.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_removal.h5 index 3d48105d7b444cc16f5b6438f97f897d2c1657fd..c3121a9535097cbd3bc2457b664ff774ca654e2c 100644 GIT binary patch delta 670 zcmcbxnCZe|rVS2#j53oQ`{L_&B(Ktto6Bd%00o;)bLh;i`vB)Jx@+_-A<9Gws#GVMMu~xzzyKa&yn6*$7~OC0Qc$-|FjJj zz#a5utLNwaEA-*&zy9Wxu(`Jn&i8Q1mA-i06wcqn_&(J8oF|m8!@y9!TYKxf36^jL zv3qp-jdM)k{BZ7WLHP?G>`y$Ke5zl`luz@NJT(XoTCHz?k32oIA3}(w0l2|Vr!&ni1-^&# z&9=`ztNUUC+-Db=>$Yl5mx9ZGh{~C^=j3uYKVa88i=i7=GEVh&%zk>SYbyTR{^~xxdBVvnwgz4i2di9_;0BcayLV^9 zOg4B>Uv^^fW6N3ycTl?2aigtQRpIItk4CJtQ920cPht(=UOL|j&gV63vJXBP0_6)c fFhrcY#JKz_BBA<9GwjT_RMMu~xzztx@&yn6*$7~OCz^7Fo{%IR5 zfIH~;R?pA-SLnmlxBupqu(`Jn&JTCVmA-i06wZIk_&(J8oF|m8!@#g;xAxX|6D;8h zTK4Gl8|Rq7`L^8Mg7OzW*!Mr0e5zl`bi>_A_H2jmi65&maE=b-yPF`V;(YJLj7yfU zteis+&*nYhrsDkO#oH%Sww!V@@UL=QlYhtQ{A9%m@%8btXQeu%1mFhioX#}66!;#_ zSKmJStnP~maG%{{uG^|LT?#ILEh=Z)o|DVre3M=8ELL)B!}+yOo-wv;x3-(OB2r_L z*I(N|ExJ2Bk80S}&0fJ`EaOyf$LFWFx~Ag4?eFf>nOD>jer#C_;SOq-I&QS}sw!N)=+TIkHcAKK`~ubh?xpjs;Cwa1Ci~!%AyB?B f1B1r7ON`5}B7!=0^2CYaO#D)lS4~u60R=1or!~mt diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_removal_and_redox.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_removal_and_redox.h5 new file mode 100644 index 0000000000000000000000000000000000000000..80ce771fceae774cd606fef7888c325128b7462f GIT binary patch literal 37328 zcmeHPeQ;D)6~Avcp$h~EErk?n`cj#8O;Xu>&?bc4Y}gPJ93>E_tL?O#k1eE=FOw_= zMa)R6adafK&NSlSq|TtBEp7VcfOPCu`jOfW6Dn3lfr(>Bqr=oy+YzgP@4b7@d)b?P zyX+%NxAg2Ed*8YD+;h+Uz4JTo-nZ}VcWSF^=FM3;hp8WjgJrO6?GV2%!e@LBuR$5x z18^WgIRNE|jUPygMYSyl>a)SVZ08lDP`_#8+O>??7~01Yn%UqiepCh(^@_Io325K+ zb+scBz##z9d+LwQ$EI zi8BM}phP(TAYZV|VxZ4vEBzhpzSg#2@Aj@BsI%)7#f6H1`YpjOML`)+?hf{Z+Io9H zQBpKi9poGOe-8MT13cn%WLs7ISG6lh`a}{o-J)9W6mM=T2L{2-DAE zJo8EH8n#I3x2>}+)UK>j7Rr?{gqM}hWu+H|JlnjZsqXlZKUSLMB^TsZ%1Z*^B*sL* zEH9lDc#ipErt%WQZ+j%K5d({3D@Apis;_YY`hEa55-ya%S%r!

    %x-v&8GFh?j9+>{@M|+SuFKE_a05 zn}Xdku36O?nXOx48LIU4Y7>`~Rjaz*?G83J^V?^Qy}>D#64^1!R}naGl&>_H=_>&O zX8CFe;t0EAewe9zg*+4UjlmCVJj(=!4LjC&hIY(&=KlmAhqJ;(FKMhc%v|*M=Qy zJVQHXJaaDR8pJb1ipqEhnDK1H^I_@FFw#<-iQ`}*jUucxSF zr6PQ$rD&!4le}8qp_FyJnr$vF(SEU4N>xpjwFkQVL9U>dYCrTZpem){gU|Uo!S>cX zet_eO`%D+;jl;Sa^~7~De>#n^GBAvK;&t8&_J$!Zp`MthYC+Gjf*+us3*>s?yn^}_ zEAaOm(H|o(TFbBao`PY=T7E@43Ewv;=aAv=-d7Aj8YnFg5c8%CUX8$WMd+lnqi{=O zdwb)jpyIyP9__swYN}Mt0k*iA~&T)v>AgsHX8UryM4`qT)uy?{?)e%kI|3HhfRHBh=j1+7d)je;yMn z_#>W7ua~OT`1@4e>zvGVmDrnt_LEE=}>^=TH3K1H{4l{B08t8uOpR zgMq{+?Fwq(`v6`Tv%WI^;7(|E3hR6Ey_&6{=kmq37l3AYO?=$UCkWNuRz7PV003kpK5CVh%A&?jWvvp&l=fWmn-H3X4-&heb1G)c;IZ~qTvS_-#D17w>S4!4&8O?r@HJpeDoK8Snbs1)%S1z=Zi0__H-XE zeDlIn7u-dU^t@LSae96jnJqp1T9v1%;nDJQFI{wZeygG>{LtI(f4E+~%U>z${cq@O zTlV};yZ$dviyn_$C~yty{#^I-x_`YmvO?E??Z5MKR-gH$F7K&sUv~U=D|Pv-^!Lr5 zJyxg7Uq4dt{JVqYx?KK3;qH>QQeAGJ``v{%jYlerCZCz8IPzoN4-K!J_q|#f4r=AH zidQP_N6##Ib%)F6-!T7lAa}X%<$qh{+jR6t^TukH``#}I-!t&1M}6+~U#)tg z^VFKxk4bgAl=JI&`ah=*|Jz+X`oX&EADO}PyZO%T%Ql{!r_X2a{&i=5zxJ#yKRHo- zXz7_j9nZ0X-Z=~M7whf23O0OjVApuxSSzs7TCaKk(6pSkGX zdwVFa>T#!MZ`p(Y``M8y&zExZpKUnCJO|(TitpvWyzTx?z9*~ni#O~2Z`t0I_vzbj#Uk?pXmO0pZ!AM+Z&DX)-3JnoL}@Sz5h+`<(2Gn7VC29yDck1HJ{VvhVrb6 r6H6*|x&K7To8Nn5l`e-o3%>A<9Gwl55hMMu~xzzq<{&yn6*$7~OCfZFO0|FjJj zz#ZhZ&GYmA75Z@X8-Mdk*xcI(=TCFVmA-i06wd$6_&(J8oF|m8&A?E&TYKxf36^jL z!FzQ2jdM)kd_V4PLHP?G?4M6Q*{^JxAW&`3eE6RDu^I#C@Ib!1334jVH(t!RWckX< zIrQ*s-V<&r&VOFKeKKXsDJLucD#tbXcbra6R-7O&m>}J^Zs&pTPPN${3vX`Man_w2 zI6=SOJ>FEg%MYsX17rIU?4rhUff8Ci)C;mT2gum`8`#JKgT5$F2L_>AAg&&0T zw`t!p_?Br5=if<`KCk~K5X#qKU@%&myZ1l^BK)o8?kKz}H-u|=wB~g4EP;>qYmQD{ KHBpHL6afHS{MF3> delta 709 zcmcbxnCZe|rVS2#jIxs*`{L`DC9l$uo6Bd%00p~FbLh;i`vB)}x@+_-cf;rNC>vkUa?o^xYvGC@09cPot zffMxWo&RKSGXBL6x9x6RlTzGVMEEP$?Rw6*aRS`C^9xHBepVNStKam}H}24d6>z?w zn@N+`a&JF5Ax5ReE~DLlJJkhVxw~7KgLI!@u~v!mHixfRxP;thoYgn+rkgR z`75+<8GOq$hV%C&N}tz%6A0z&Ffc?c&E0#T0ulbMa(5J7l^en}JY93Td6vLOdxH~` LS4~u60Yv}+O6u9M diff --git a/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_transfer_and_redox.h5 b/tests/regression_tests/deplete_with_transfer_rates/ref_depletion_with_transfer_and_redox.h5 new file mode 100644 index 0000000000000000000000000000000000000000..6847fd7dfe6960da89a0e4fdf0be9f35eb56d7f4 GIT binary patch literal 37328 zcmeHPeQ;FO6~AvcfyFEkD9BbUJ_?mZLRm8)#x@Bn7nTz}OARLSi=`%~yj} zWTv!@b_OCXL#Apc>GUJoVpFTqG%0LliV+ zn|)+4#j}6xedpeD&pr2d&+okZ-oCewNjs(w~jFzL;F}lGi#o}kFuenP|;RDLG7Eq zZgd3l*X0(fbHsSTI1BodJM(D}uQ(pYwUgy*153=ooJw>zglcNbt14N#L!pt@!X1+& z&J3V~65;%Ve8JMmK%Y%g`a9V6@^GkbXLShF*{2l6X^MdQrJ-s?K^al53Dwqz>uNz! zQZ!T@NkLIb&yB6eImuWr@BsZ3d4cncOy%}aZvbS+bY7ng!})!& z$Njik&Ca|1Y22e`aE#{xW|{{+F8^uV@qvJt2ard8I6t75$P?rv)>|!k9Kr6D!v4%@#7(|0nS z`6M=x%~1L^RE6s+lwHa+IR}RDv#d0hbydi-B@b>ZIN9}=9J9RSf&5B&NdTP05fL!U zOML>*F<*>TUP3;Jc_mRkp0sJ3iyxp~ z5y*|7=JhVnYq=#_F9LG^t-M|aJvJv=&kb_Eu7HZ;v&8eNh?j9+Y`V5jEv~DokSpse zwuNeB+_S1PGTXPpHdN{B*CsA0yH<6-TN5fS;kVC<>q0{;#j|6UuLfb=C|_wX(^nh> z%<@$W#1VGK{4iGe3V9~x8-pL#c$Nwd8+NSm4DFcltmrd*AmSND2$iuAFyk46IKsZk z;+c59HuzzUXDQ&YVaFQJ(2g0;A~GL{c!m)|Wh?~DcvdDy_evJe#Qm1 zMSO&T#k@55VeNc|ej0YH@eJ*l@hpSnc6S*N~x-;vi3k%QHU$3rP>eu3#v*<_~3KCZm?Z8 zmmlD`;&rA6^m<`mjC$fenLnMzST-0&J@Gv62YaoMmrzg4Q~99hSi%ob&jWIyu&$tf z-3t6YNA$$-qdDF9+*P}m0AUAq>{U_}k&yONri9WB3xS2iQI0*RS`cqy(#Hbtz0kgc)BJdP> zjBz!RtwidO4`N~a`mUaK3>6Z-7n{N>aQ}z;C{F^X@jY*|cDwQ>tJ-Gveo3nG zET7tLv)eQi4kl}s?Y7jG_1clvtf?rXIxM8{u|kv&g5SgODAJmwRL#9%@BfN;pd1eu z)Jq&6eqm&{P3YguomL$iijQg4}%^ABb#lvS36~9B)C1^OE#G~POa0DezH$9#)9uWtW6Q92e z=5Me5I-eKDCd|DtiL;GwdbD_8ydfSaUjrWP-!$-0&5ct$`1*_CMV;1Jb9^4JhZef2fzE@KQdY(XRdqHTH*T&Wx@2%?oSsLO0V44$j zJbf?kfc=n%@yrbFi`Kw?SYm#|eaw)6jP6JhAwUQa0)zk|KnM^5ga9Ex2oM5^f4$3%LZnXoc%Z?4PKa-FP7i<6?g-#p_Cx>H0Fkg(7uAfDj-A2mwNX5Fi8y0YZQf zAOr{jLf{$@FuR|SK1!Pr_+1(G@w(rvd+TG-EvSzr-5-d?ha^IP5Fi8y0YZQfAOr{j zLSQ^0VD=zTlsxU8^W+pC+UtJBk@4iN zL0vv_e%afbU*4zxYvPejKS+7_&>X$JYdbic%CD|?GY{5&uzt|(dwg(`^!=A|ePx^4R$lzWRqyuitlk#cf5!Wc=lGVQ99i$b zq$)i7`CWGXU!HFJ#o(1io>twT87~yPf3$sxuK&h=C%e{MIHJo9c@?uyzP? zo-e*upvzmk7Crxu=9Rjpykxczpo11d+hPaJ?rNOK3WvHuj#LCf$R+r zei+}f94lM242tgqwOAD!#`(3{tB>8_avhpg|0*?XVqjC{Cu&)4$X-Z-2Q`0UK^ z37hx*wc5C&_&RaaSvc~m+!B*)R6zg8sFK*x6a;uius)TPcP`YYoX8n!k;T#-MPN0KR@|I z%A3p=E=@bM=dCl|3tRiVoBHSK{g2*v<&I}-Q*>Pa^K|#04sL1F*LU9OS(n}mF4pxA zyu0}Ji3?xU Date: Thu, 13 Nov 2025 21:35:33 +0100 Subject: [PATCH 451/671] Addition of a collision tracking feature (#3417) Co-authored-by: Paul Romano --- CMakeLists.txt | 1 + docs/source/io_formats/collision_track.rst | 46 ++++ docs/source/io_formats/index.rst | 1 + docs/source/io_formats/settings.rst | 79 ++++++ docs/source/pythonapi/base.rst | 3 + docs/source/usersguide/settings.rst | 56 ++++ include/openmc/bank.h | 2 + include/openmc/bank_io.h | 103 +++++++ include/openmc/collision_track.h | 23 ++ include/openmc/constants.h | 1 + include/openmc/mcpl_interface.h | 15 ++ include/openmc/message_passing.h | 1 + include/openmc/particle_data.h | 19 ++ include/openmc/settings.h | 20 ++ include/openmc/simulation.h | 1 + include/openmc/urr.h | 8 +- openmc/settings.py | 246 ++++++++++++++--- openmc/source.py | 175 ++++++++++-- src/bank.cpp | 3 + src/collision_track.cpp | 238 ++++++++++++++++ src/finalize.cpp | 5 + src/initialize.cpp | 31 +++ src/mcpl_interface.cpp | 155 +++++++++++ src/message_passing.cpp | 1 + src/particle.cpp | 6 + src/physics.cpp | 5 +- src/settings.cpp | 76 +++++- src/simulation.cpp | 13 +- src/state_point.cpp | 90 +------ .../collision_track/__init__.py | 0 .../case_1_Reactions/inputs_true.dat | 58 ++++ .../case_1_Reactions/results_true.dat | 2 + .../case_2_Cell_ID/inputs_true.dat | 58 ++++ .../case_2_Cell_ID/results_true.dat | 2 + .../case_3_Material_ID/inputs_true.dat | 58 ++++ .../case_3_Material_ID/results_true.dat | 2 + .../case_4_Nuclide_ID/inputs_true.dat | 58 ++++ .../case_4_Nuclide_ID/results_true.dat | 2 + .../case_5_Universe_ID/inputs_true.dat | 59 ++++ .../case_5_Universe_ID/results_true.dat | 2 + .../inputs_true.dat | 58 ++++ .../results_true.dat | 2 + .../inputs_true.dat | 63 +++++ .../results_true.dat | 2 + .../case_8_2threads/inputs_true.dat | 57 ++++ .../case_8_2threads/results_true.dat | 2 + .../regression_tests/collision_track/test.py | 255 ++++++++++++++++++ tests/testing_harness.py | 126 ++++++++- tests/unit_tests/test_collision_track.py | 127 +++++++++ 49 files changed, 2268 insertions(+), 148 deletions(-) create mode 100644 docs/source/io_formats/collision_track.rst create mode 100644 include/openmc/bank_io.h create mode 100644 include/openmc/collision_track.h create mode 100644 src/collision_track.cpp create mode 100644 tests/regression_tests/collision_track/__init__.py create mode 100644 tests/regression_tests/collision_track/case_1_Reactions/inputs_true.dat create mode 100644 tests/regression_tests/collision_track/case_1_Reactions/results_true.dat create mode 100644 tests/regression_tests/collision_track/case_2_Cell_ID/inputs_true.dat create mode 100644 tests/regression_tests/collision_track/case_2_Cell_ID/results_true.dat create mode 100644 tests/regression_tests/collision_track/case_3_Material_ID/inputs_true.dat create mode 100644 tests/regression_tests/collision_track/case_3_Material_ID/results_true.dat create mode 100644 tests/regression_tests/collision_track/case_4_Nuclide_ID/inputs_true.dat create mode 100644 tests/regression_tests/collision_track/case_4_Nuclide_ID/results_true.dat create mode 100644 tests/regression_tests/collision_track/case_5_Universe_ID/inputs_true.dat create mode 100644 tests/regression_tests/collision_track/case_5_Universe_ID/results_true.dat create mode 100644 tests/regression_tests/collision_track/case_6_deposited_energy_threshold/inputs_true.dat create mode 100644 tests/regression_tests/collision_track/case_6_deposited_energy_threshold/results_true.dat create mode 100644 tests/regression_tests/collision_track/case_7_all_parameters_used_together/inputs_true.dat create mode 100644 tests/regression_tests/collision_track/case_7_all_parameters_used_together/results_true.dat create mode 100644 tests/regression_tests/collision_track/case_8_2threads/inputs_true.dat create mode 100644 tests/regression_tests/collision_track/case_8_2threads/results_true.dat create mode 100644 tests/regression_tests/collision_track/test.py create mode 100644 tests/unit_tests/test_collision_track.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 474451c8a..87b8789d1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -338,6 +338,7 @@ list(APPEND libopenmc_SOURCES src/cell.cpp src/chain.cpp src/cmfd_solver.cpp + src/collision_track.cpp src/cross_sections.cpp src/dagmc.cpp src/distribution.cpp diff --git a/docs/source/io_formats/collision_track.rst b/docs/source/io_formats/collision_track.rst new file mode 100644 index 000000000..a36459754 --- /dev/null +++ b/docs/source/io_formats/collision_track.rst @@ -0,0 +1,46 @@ +.. _io_collision_track: + +=========================== +Collision Track File Format +=========================== + +When collision tracking is enabled with ``mcpl=false`` (the default), OpenMC +writes binary data to an HDF5 file named ``collision_track.h5``. The same data +may also be written after each batch when multiple files are requested +(``collision_track.N.h5``) or when the run is performed in parallel. The file +contains the information needed to reconstruct each recorded collision. + +The current revision of the collision track file format is 1.0. + +**/** + +:Attributes: + - **filetype** (*char[]*) -- String indicating the type of file. + For collision-track files the value is ``"collision_track"``. + +:Datasets: + + - **collision_track_bank** (Compound type) -- Collision information + for each stored event. Each entry in the dataset corresponds to one + collision and contains the following fields: + + - ``r`` (*double[3]*) -- Position of the collision in [cm]. + - ``u`` (*double[3]*) -- Direction unit vector immediately after the collision. + - ``E`` (*double*) -- Incident particle energy before the collision in [eV]. + - ``dE`` (*double*) -- Energy loss over the collision (:math:`E_\text{before} - E_\text{after}`) in [eV]. + - ``time`` (*double*) -- Time of the collision in [s]. + - ``wgt`` (*double*) -- Particle weight at the collision. + - ``event_mt`` (*int*) -- ENDF MT number identifying the reaction. + - ``delayed_group`` (*int*) -- Delayed neutron group index (non-zero for delayed events). + - ``cell_id`` (*int*) -- ID of the cell in which the collision occurred. + - ``nuclide_id`` (*int*) -- ZA identifier of the nuclide (ZZZAAAM format). + - ``material_id`` (*int*) -- ID of the material containing the collision site. + - ``universe_id`` (*int*) -- ID of the universe containing the collision site. + - ``n_collision`` (*int*) -- Collision counter for the particle history. + - ``particle`` (*int*) -- Particle type (0=neutron, 1=photon, 2=electron, 3=positron). + - ``parent_id`` (*int64*) -- Unique ID of the parent particle. + - ``progeny_id`` (*int64*) -- Progeny ID of the particle. + +In an MPI run, OpenMC writes the combined dataset by gathering collision-track +entries from all ranks before flushing them to disk, so the final file appears +as though it were produced serially. diff --git a/docs/source/io_formats/index.rst b/docs/source/io_formats/index.rst index 4bbaa961a..5b4efea66 100644 --- a/docs/source/io_formats/index.rst +++ b/docs/source/io_formats/index.rst @@ -44,6 +44,7 @@ Output Files statepoint source + collision_track summary properties depletion_results diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 720846c85..b7874fcf2 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -20,6 +20,85 @@ source neutrons. *Default*: None +----------------------------- +```` Element +----------------------------- + +The ```` element indicates to track information about particle +collisions based on a set of criteria and store these events in a file named +``collision_track.h5``. This file records details such as the position of the +interaction, direction of the incoming particle, incident energy and deposited +energy, weight, time of the interaction, and the delayed neutron group (0 for +prompt neutrons). Additional information such as the cell ID, material ID, +universe ID, nuclide ZAID, particle type, and event MT number are also stored. +Users can specify one or more criterion to filter collisions. If no criteria are +specified, it defaults to tracking all collisions across the model. + +.. warning:: + Storing all collisions can be very memory intensive. For more targeted + tracking, users can employ a variety of parameters such as ``cell_ids``, + ``reactions``, ``universe_ids``, ``material_ids``, ``nuclides``, and + ``deposited_E_threshold`` to refine the selection of particle interactions + to be banked. + +This element can contain one or more of the following attributes or +sub-elements: + + :max_collisions: + An integer indicating the maximum number of collisions to be banked per file. + + *Default*: 1000 + + :max_collision_track_files: + An integer indicating the number of collision_track files to be used. + + *Default*: 1 + + :mcpl: + An optional boolean to enable MCPL_-format instead of the native HDF5-based + format. If activated, the output file name and type is changed to + ``collision_track.mcpl``. + + *Default*: false + + .. _MCPL: https://mctools.github.io/mcpl/mcpl.pdf + + :cell_ids: + A list of integers representing cell IDs to define specific cells in which + collisions are to be banked. + + *Default*: None + + :universe_ids: + A list of integers representing the universe IDs to define specific + universes in which collisions are to be banked. + + *Default*: None + + :material_ids: + A list of integers representing the material IDs to define specific + materials in which collisions are to be banked. + + *Default*: None + + :nuclides: + A list of strings representing the nuclide, to define specific + define specific target nuclide collisions to be banked. + + *Default*: None + + :reactions: + A list of integers representing the ENDF-6 format MT numbers or strings + (e.g. (n,fission)) to define specific reaction types to be banked. + + *Default*: None + + :deposited_E_threshold: + A float defining the minimum deposited energy per collision (in eV) to + trigger banking. + + *Default*: 0.0 + ---------------------------------- ```` Element ---------------------------------- diff --git a/docs/source/pythonapi/base.rst b/docs/source/pythonapi/base.rst index 2a9d0876c..ce2f6f0f8 100644 --- a/docs/source/pythonapi/base.rst +++ b/docs/source/pythonapi/base.rst @@ -216,6 +216,9 @@ Post-processing :nosignatures: :template: myfunction.rst + openmc.read_collision_track_file + openmc.read_collision_track_hdf5 + openmc.read_collision_track_mcpl openmc.voxel_to_vtk The following classes and functions are used for functional expansion reconstruction. diff --git a/docs/source/usersguide/settings.rst b/docs/source/usersguide/settings.rst index 1b2d4bc1a..f973f1465 100644 --- a/docs/source/usersguide/settings.rst +++ b/docs/source/usersguide/settings.rst @@ -756,6 +756,62 @@ instance, whereas the :meth:`openmc.Track.filter` method returns a new track_files = [f"tracks_p{rank}.h5" for rank in range(32)] openmc.Tracks.combine(track_files, "tracks.h5") +Collision Track File +--------------------- + +OpenMC can generate a collision track file that contains detailed collision +information (position, direction, energy, deposited energy, time, weight, cell +ID, material ID, universe ID, nuclide ZAID, particle type, particle delayed +group and particle ID) for each particle collision depending on user-defined +parameters. To invoke this feature, set the +:attr:`~openmc.Settings.collision_track` attribute as shown in this example:: + + settings.collision_track = { + "max_collisions": 300, + "reactions": ["(n,fission)", "(n,2n)"], + "material_ids": [1,2], + "nuclides": ["U238", "O16"], + "cell_ids": [5, 12] + } + +In this example, collision track information is written to the +collision_track.h5 file at the end of the simulation. The file contains +300 recorded collisions that occurred in materials with IDs 1 or 2, involving +fission or (n,2n) reactions on the nuclides U-238 or O-16, within cells +with IDs 5 and 12. +The file can be read using :func:`openmc.read_collision_track_file`. +The example below shows how to extract the data from the collision_track +feature and displays the fields stored in the file: + +>>> data = openmc.read_collision_track_file('collision_track.h5') +>>> data.dtype + dtype([('r', [('x', ' source_bank; extern SharedArray surf_source_bank; +extern SharedArray collision_track_bank; + extern SharedArray fission_bank; extern vector> ifp_source_delayed_group_bank; diff --git a/include/openmc/bank_io.h b/include/openmc/bank_io.h new file mode 100644 index 000000000..90ffd820f --- /dev/null +++ b/include/openmc/bank_io.h @@ -0,0 +1,103 @@ +#ifndef OPENMC_BANK_IO_H +#define OPENMC_BANK_IO_H + +#include "hdf5.h" + +#include "openmc/message_passing.h" +#include "openmc/span.h" +#include "openmc/vector.h" + +#include + +#ifdef OPENMC_MPI +#include +#endif + +namespace openmc { + +template +void write_bank_dataset(const char* dataset_name, hid_t group_id, + span bank, const vector& bank_index, hid_t banktype +#ifdef OPENMC_MPI + , + MPI_Datatype mpi_dtype +#endif +) +{ + int64_t dims_size = bank_index.back(); + int64_t count_size = bank_index[mpi::rank + 1] - bank_index[mpi::rank]; + +#ifdef PHDF5 + hsize_t dims[] {static_cast(dims_size)}; + hid_t dspace = H5Screate_simple(1, dims, nullptr); + hid_t dset = H5Dcreate(group_id, dataset_name, banktype, dspace, H5P_DEFAULT, + H5P_DEFAULT, H5P_DEFAULT); + + hsize_t count[] {static_cast(count_size)}; + hid_t memspace = H5Screate_simple(1, count, nullptr); + + hsize_t start[] {static_cast(bank_index[mpi::rank])}; + H5Sselect_hyperslab(dspace, H5S_SELECT_SET, start, nullptr, count, nullptr); + + hid_t plist = H5Pcreate(H5P_DATASET_XFER); + H5Pset_dxpl_mpio(plist, H5FD_MPIO_COLLECTIVE); + + H5Dwrite(dset, banktype, memspace, dspace, plist, bank.data()); + + H5Sclose(dspace); + H5Sclose(memspace); + H5Dclose(dset); + H5Pclose(plist); +#else + if (mpi::master) { + hsize_t dims[] {static_cast(dims_size)}; + hid_t dspace = H5Screate_simple(1, dims, nullptr); + hid_t dset = H5Dcreate(group_id, dataset_name, banktype, dspace, + H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); + +#ifdef OPENMC_MPI + vector temp_bank {bank.begin(), bank.end()}; +#endif + + for (int i = 0; i < mpi::n_procs; ++i) { + hsize_t count[] {static_cast(bank_index[i + 1] - bank_index[i])}; + hid_t memspace = H5Screate_simple(1, count, nullptr); + +#ifdef OPENMC_MPI + if (i > 0) { + MPI_Recv(bank.data(), count[0], mpi_dtype, i, i, mpi::intracomm, + MPI_STATUS_IGNORE); + } +#endif + + hid_t dspace_rank = H5Dget_space(dset); + hsize_t start[] {static_cast(bank_index[i])}; + H5Sselect_hyperslab( + dspace_rank, H5S_SELECT_SET, start, nullptr, count, nullptr); + + H5Dwrite(dset, banktype, memspace, dspace_rank, H5P_DEFAULT, bank.data()); + + H5Sclose(memspace); + H5Sclose(dspace_rank); + } + + H5Dclose(dset); + +#ifdef OPENMC_MPI + std::copy(temp_bank.begin(), temp_bank.end(), bank.begin()); +#endif + } +#ifdef OPENMC_MPI + else { + if (!bank.empty()) { + MPI_Send( + bank.data(), bank.size(), mpi_dtype, 0, mpi::rank, mpi::intracomm); + } + } +#endif +#endif +} + +} // namespace openmc + +#endif // OPENMC_BANK_IO_H diff --git a/include/openmc/collision_track.h b/include/openmc/collision_track.h new file mode 100644 index 000000000..208b8f6e1 --- /dev/null +++ b/include/openmc/collision_track.h @@ -0,0 +1,23 @@ +#ifndef OPENMC_COLLISION_TRACK_H +#define OPENMC_COLLISION_TRACK_H + +#include + +namespace openmc { + +class Particle; + +//! Reserve space in the collision track bank according to user settings. +void collision_track_reserve_bank(); + +//! Write collision track data to disk when the bank is full or the batch ends. +void collision_track_flush_bank(); + +//! Record the current particle as a collision-track entry when applicable. +//! +//! \param particle Particle whose collision should be recorded if eligible +void collision_track_record(Particle& particle); + +} // namespace openmc + +#endif // OPENMC_COLLISION_TRACK_H diff --git a/include/openmc/constants.h b/include/openmc/constants.h index b66193481..bba260c3a 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -34,6 +34,7 @@ constexpr array VERSION_VOXEL {2, 0}; constexpr array VERSION_MGXS_LIBRARY {1, 0}; constexpr array VERSION_PROPERTIES {1, 1}; constexpr array VERSION_WEIGHT_WINDOWS {1, 0}; +constexpr array VERSION_COLLISION_TRACK {1, 0}; // ============================================================================ // ADJUSTABLE PARAMETERS diff --git a/include/openmc/mcpl_interface.h b/include/openmc/mcpl_interface.h index a76d72e64..a9cce3e69 100644 --- a/include/openmc/mcpl_interface.h +++ b/include/openmc/mcpl_interface.h @@ -38,6 +38,21 @@ vector mcpl_source_sites(std::string path); void write_mcpl_source_point(const char* filename, span source_bank, const vector& bank_index); +//! Write an MCPL collision track file +//! +//! This function writes collision track data to an MCPL file. Additional +//! collision-specific metadata (such as energy deposition, material info, etc.) +//! is stored in the file header as blob data. +//! +//! \param[in] filename Path to MCPL file +//! \param[in] collision_track_bank Vector of CollisionTrackSites to write to +//! file for this MPI rank. +//! \param[in] bank_index Pointer to vector of site index ranges over all +//! MPI ranks. +void write_mcpl_collision_track(const char* filename, + span collision_track_bank, + const vector& bank_index); + //! Check if MCPL functionality is available bool is_mcpl_interface_available(); diff --git a/include/openmc/message_passing.h b/include/openmc/message_passing.h index a1641a906..ce993776b 100644 --- a/include/openmc/message_passing.h +++ b/include/openmc/message_passing.h @@ -18,6 +18,7 @@ extern bool master; #ifdef OPENMC_MPI extern MPI_Datatype source_site; +extern MPI_Datatype collision_track_site; extern MPI_Comm intracomm; #endif diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index 5383487d4..fdacfa765 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -56,6 +56,25 @@ struct SourceSite { int64_t progeny_id; }; +struct CollisionTrackSite { + Position r; + Direction u; + double E; + double dE; + double time {0.0}; + double wgt {1.0}; + int event_mt {0}; + int delayed_group {0}; + int cell_id {0}; + int nuclide_id; + int material_id {0}; + int universe_id {0}; + int n_collision {0}; + ParticleType particle; + int64_t parent_id; + int64_t progeny_id; +}; + //! State of a particle used for particle track files struct TrackState { Position r; //!< Position in [cm] diff --git a/include/openmc/settings.h b/include/openmc/settings.h index 78bfa088e..b369c99fe 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -32,6 +32,24 @@ enum class IFPParameter { GenerationTime, }; +struct CollisionTrackConfig { + bool mcpl_write {false}; //!< Write collision tracks using MCPL? + std::unordered_set + cell_ids; //!< Cell ids where collisions will be written + std::unordered_set + mt_numbers; //!< MT Numbers where collisions will be written + std::unordered_set + universe_ids; //!< Universe IDs where collisions will be written + std::unordered_set + material_ids; //!< Material IDs where collisions will be written + std::unordered_set + nuclides; //!< Nuclides where collisions will be written + double deposited_energy_threshold {0.0}; //!< Minimum deposited energy [eV] + int64_t max_collisions { + 1000}; //!< Maximum events recorded per collision track file + int64_t max_files {1}; //!< Maximum number of collision track files +}; + //============================================================================== // Global variable declarations //============================================================================== @@ -41,6 +59,7 @@ namespace settings { // Boolean flags extern bool assume_separate; //!< assume tallies are spatially separate? extern bool check_overlaps; //!< check overlaps in geometry? +extern bool collision_track; //!< flag to use collision track feature? extern bool confidence_intervals; //!< use confidence intervals for results? extern bool create_fission_neutrons; //!< create fission neutrons (fixed source)? @@ -145,6 +164,7 @@ extern std::unordered_set statepoint_batch; //!< Batches when state should be written extern std::unordered_set source_write_surf_id; //!< Surface ids where sources will be written +extern CollisionTrackConfig collision_track_config; extern double source_rejection_fraction; //!< Minimum fraction of source sites //!< that must be accepted extern double free_gas_threshold; //!< Threshold multiplier for free gas diff --git a/include/openmc/simulation.h b/include/openmc/simulation.h index 3e4e24e1d..9a6cf1b21 100644 --- a/include/openmc/simulation.h +++ b/include/openmc/simulation.h @@ -22,6 +22,7 @@ constexpr int STATUS_EXIT_ON_TRIGGER {2}; namespace simulation { +extern int ct_current_file; //!< current collision track file index extern "C" int current_batch; //!< current batch extern "C" int current_gen; //!< current fission generation extern "C" bool initialized; //!< has simulation been initialized? diff --git a/include/openmc/urr.h b/include/openmc/urr.h index 3978c7b86..1e6037158 100644 --- a/include/openmc/urr.h +++ b/include/openmc/urr.h @@ -27,10 +27,10 @@ public: double heating; }; - Interpolation interp_; //!< interpolation type - int inelastic_flag_; //!< inelastic competition flag - int absorption_flag_; //!< other absorption flag - bool multiply_smooth_; //!< multiply by smooth cross section? + Interpolation interp_; //!< interpolation type + int inelastic_flag_; //!< inelastic competition flag + int absorption_flag_; //!< other absorption flag + bool multiply_smooth_; //!< multiply by smooth cross section? vector energy_; //!< incident energies auto n_energy() const { return energy_.size(); } diff --git a/openmc/settings.py b/openmc/settings.py index 2f8a2b124..43c1fe069 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -6,7 +6,7 @@ from numbers import Integral, Real from pathlib import Path import lxml.etree as ET - +import warnings import openmc import openmc.checkvalue as cv from openmc.checkvalue import PathLike @@ -47,6 +47,21 @@ class Settings: half-width of the 95% two-sided confidence interval. If False, uncertainties on tally results will be reported as the sample standard deviation. + collision_track : dict + Options for writing collision information. Acceptable keys are: + + :max_collisions: Maximum number of collisions to be banked per file. (int) + :max_collision_track_files: Maximum number of collision_track files. (int) + :mcpl: Output in the form of an MCPL-file. (bool) + :cell_ids: List of cell IDs to define cells in which collisions should be banked. (list of int) + :universe_ids: List of universe IDs to define universes in which collisions should be banked. (list of int) + :material_ids: List of material IDs to define materials in which collisions should be banked. (list of int) + :nuclides: List of nuclides to define nuclides in which collisions should be banked. + (ex: ["I135m", "U233"] ). (list of str) + :reactions: List of reaction to define specific reactions that should be banked + (ex: ["(n,fission)", 2, "(n,2n)"] ). (list of str or int) + :deposited_E_threshold: Number to define the minimum deposited energy during + per collision to trigger banking. (float) create_fission_neutrons : bool Indicate whether fission neutrons should be created or not. cutoff : dict @@ -395,6 +410,9 @@ class Settings: # Iterated Fission Probability self._ifp_n_generation = None + # Collision track feature + self._collision_track = {} + # Output options self._statepoint = {} self._sourcepoint = {} @@ -434,7 +452,8 @@ class Settings: self._max_particle_events = None self._write_initial_source = None self._weight_windows = WeightWindowsList() - self._weight_window_generators = cv.CheckedList(WeightWindowGenerator, 'weight window generators') + self._weight_window_generators = cv.CheckedList( + WeightWindowGenerator, 'weight window generators') self._weight_windows_on = None self._weight_windows_file = None self._weight_window_checkpoints = {} @@ -475,8 +494,9 @@ class Settings: @generations_per_batch.setter def generations_per_batch(self, generations_per_batch: int): - cv.check_type('generations per patch', generations_per_batch, Integral) - cv.check_greater_than('generations per batch', generations_per_batch, 0) + cv.check_type('generations per batch', generations_per_batch, Integral) + cv.check_greater_than('generations per batch', + generations_per_batch, 0) self._generations_per_batch = generations_per_batch @property @@ -506,7 +526,8 @@ class Settings: @rel_max_lost_particles.setter def rel_max_lost_particles(self, rel_max_lost_particles: float): cv.check_type('rel_max_lost_particles', rel_max_lost_particles, Real) - cv.check_greater_than('rel_max_lost_particles', rel_max_lost_particles, 0) + cv.check_greater_than('rel_max_lost_particles', + rel_max_lost_particles, 0) cv.check_less_than('rel_max_lost_particles', rel_max_lost_particles, 1) self._rel_max_lost_particles = rel_max_lost_particles @@ -516,8 +537,10 @@ class Settings: @max_write_lost_particles.setter def max_write_lost_particles(self, max_write_lost_particles: int): - cv.check_type('max_write_lost_particles', max_write_lost_particles, Integral) - cv.check_greater_than('max_write_lost_particles', max_write_lost_particles, 0) + cv.check_type('max_write_lost_particles', + max_write_lost_particles, Integral) + cv.check_greater_than('max_write_lost_particles', + max_write_lost_particles, 0) self._max_write_lost_particles = max_write_lost_particles @property @@ -538,12 +561,12 @@ class Settings: def keff_trigger(self, keff_trigger: dict): if not isinstance(keff_trigger, dict): msg = f'Unable to set a trigger on keff from "{keff_trigger}" ' \ - 'which is not a Python dictionary' + 'which is not a Python dictionary' raise ValueError(msg) elif 'type' not in keff_trigger: msg = f'Unable to set a trigger on keff from "{keff_trigger}" ' \ - 'which does not have a "type" key' + 'which does not have a "type" key' raise ValueError(msg) elif keff_trigger['type'] not in ['variance', 'std_dev', 'rel_err']: @@ -553,7 +576,7 @@ class Settings: elif 'threshold' not in keff_trigger: msg = f'Unable to set a trigger on keff from "{keff_trigger}" ' \ - 'which does not have a "threshold" key' + 'which does not have a "threshold" key' raise ValueError(msg) elif not isinstance(keff_trigger['threshold'], Real): @@ -570,7 +593,7 @@ class Settings: @energy_mode.setter def energy_mode(self, energy_mode: str): cv.check_value('energy mode', energy_mode, - ['continuous-energy', 'multi-group']) + ['continuous-energy', 'multi-group']) self._energy_mode = energy_mode @property @@ -593,7 +616,8 @@ class Settings: def source(self, source: SourceBase | Iterable[SourceBase]): if not isinstance(source, MutableSequence): source = [source] - self._source = cv.CheckedList(SourceBase, 'source distributions', source) + self._source = cv.CheckedList( + SourceBase, 'source distributions', source) @property def confidence_intervals(self) -> bool: @@ -610,7 +634,8 @@ class Settings: @electron_treatment.setter def electron_treatment(self, electron_treatment: str): - cv.check_value('electron treatment', electron_treatment, ['led', 'ttb']) + cv.check_value('electron treatment', + electron_treatment, ['led', 'ttb']) self._electron_treatment = electron_treatment @property @@ -704,7 +729,8 @@ class Settings: @trigger_max_batches.setter def trigger_max_batches(self, trigger_max_batches: int): cv.check_type('trigger maximum batches', trigger_max_batches, Integral) - cv.check_greater_than('trigger maximum batches', trigger_max_batches, 0) + cv.check_greater_than('trigger maximum batches', + trigger_max_batches, 0) self._trigger_max_batches = trigger_max_batches @property @@ -713,8 +739,10 @@ class Settings: @trigger_batch_interval.setter def trigger_batch_interval(self, trigger_batch_interval: int): - cv.check_type('trigger batch interval', trigger_batch_interval, Integral) - cv.check_greater_than('trigger batch interval', trigger_batch_interval, 0) + cv.check_type('trigger batch interval', + trigger_batch_interval, Integral) + cv.check_greater_than('trigger batch interval', + trigger_batch_interval, 0) self._trigger_batch_interval = trigger_batch_interval @property @@ -798,19 +826,22 @@ class Settings: @surf_source_write.setter def surf_source_write(self, surf_source_write: dict): - cv.check_type("surface source writing options", surf_source_write, Mapping) + cv.check_type("surface source writing options", + surf_source_write, Mapping) for key, value in surf_source_write.items(): cv.check_value( "surface source writing key", key, - ("surface_ids", "max_particles", "max_source_files", "mcpl", "cell", "cellfrom", "cellto"), + ("surface_ids", "max_particles", "max_source_files", + "mcpl", "cell", "cellfrom", "cellto"), ) if key == "surface_ids": cv.check_type( "surface ids for source banking", value, Iterable, Integral ) for surf_id in value: - cv.check_greater_than("surface id for source banking", surf_id, 0) + cv.check_greater_than( + "surface id for source banking", surf_id, 0) elif key == "mcpl": cv.check_type("write to an MCPL-format file", value, bool) @@ -827,6 +858,79 @@ class Settings: self._surf_source_write = surf_source_write + @property + def collision_track(self) -> dict: + return self._collision_track + + @collision_track.setter + def collision_track(self, collision_track: dict): + cv.check_type('Collision tracking options', collision_track, Mapping) + for key, value in collision_track.items(): + cv.check_value('collision_track key', key, + ('cell_ids', 'reactions', 'universe_ids', 'material_ids', 'nuclides', + 'deposited_E_threshold', 'max_collisions', 'max_collision_track_files', 'mcpl')) + if key == 'cell_ids': + cv.check_type('cell ids for collision tracking data banking', value, + Iterable, Integral) + for cell_id in value: + cv.check_greater_than('cell id for collision tracking data banking', + cell_id, 0) + elif key == 'reactions': + cv.check_type('MT numbers for collision tracking data banking', value, + Iterable) + for reaction in value: + if isinstance(reaction, int): + cv.check_greater_than( + 'MT number for collision tracking data banking', reaction, 0 + ) + elif isinstance(reaction, str): + # check against allowed strings? so far let C++ code handle it + pass + else: + raise TypeError( + f"MT number for collision tracking data banking must be a positive int or string, " + f"got {type(reaction).__name__}") + elif key == 'universe_ids': + cv.check_type('universe ids for collision tracking data banking', value, + Iterable, Integral) + for universe_id in value: + cv.check_greater_than('universe id for collision tracking data banking', + universe_id, 0) + elif key == 'material_ids': + cv.check_type('material ids for collision tracking data banking', value, + Iterable, Integral) + for material_id in value: + cv.check_greater_than('material id for collision tracking data banking', + material_id, 0) + elif key == 'nuclides': + cv.check_type('nuclides for collision tracking data banking', value, + Iterable, str) + for nuclide in value: + # If nuclide name doesn't look valid, give a warning + try: + openmc.data.zam(nuclide) + except ValueError: + warnings.warn(f"Nuclide {nuclide} is not valid") + elif key == 'deposited_E_threshold': + cv.check_type('Deposited Energy Threshold for collision tracking data banking', + value, Real) + cv.check_greater_than('Deposited Energy Threshold for collision tracking data banking', + value, 0) + elif key == 'max_collisions': + cv.check_type('maximum collisions banks per file', + value, Integral) + cv.check_greater_than('maximum collisions banks in collision tracking', + value, 0) + elif key == 'max_collision_track_files': + cv.check_type('maximum collisions banks', + value, Integral) + cv.check_greater_than('maximum number of collision_track files ', + value, 0) + elif key == 'mcpl': + cv.check_type('write to an MCPL-format file', value, bool) + + self._collision_track = collision_track + @property def no_reduce(self) -> bool: return self._no_reduce @@ -943,7 +1047,7 @@ class Settings: def cutoff(self, cutoff: dict): if not isinstance(cutoff, Mapping): msg = f'Unable to set cutoff from "{cutoff}" which is not a '\ - 'Python dictionary' + 'Python dictionary' raise ValueError(msg) for key in cutoff: if key == 'weight': @@ -961,7 +1065,7 @@ class Settings: cv.check_greater_than('energy cutoff', cutoff[key], 0.0) else: msg = f'Unable to set cutoff to "{key}" which is unsupported ' \ - 'by OpenMC' + 'by OpenMC' self._cutoff = cutoff @@ -1130,12 +1234,14 @@ class Settings: @weight_window_checkpoints.setter def weight_window_checkpoints(self, weight_window_checkpoints: dict): for key in weight_window_checkpoints.keys(): - cv.check_value('weight_window_checkpoints', key, ('collision', 'surface')) + cv.check_value('weight_window_checkpoints', + key, ('collision', 'surface')) self._weight_window_checkpoints = weight_window_checkpoints @property def max_splits(self): - raise AttributeError('max_splits has been deprecated. Please use max_history_splits instead') + raise AttributeError( + 'max_splits has been deprecated. Please use max_history_splits instead') @property def max_history_splits(self) -> int: @@ -1184,7 +1290,8 @@ class Settings: def weight_window_generators(self, wwgs): if not isinstance(wwgs, MutableSequence): wwgs = [wwgs] - self._weight_window_generators = cv.CheckedList(WeightWindowGenerator, 'weight window generators', wwgs) + self._weight_window_generators = cv.CheckedList( + WeightWindowGenerator, 'weight window generators', wwgs) @property def random_ray(self) -> dict: @@ -1221,7 +1328,8 @@ class Settings: for mesh, domains in value: cv.check_type('mesh', mesh, MeshBase) cv.check_type('domains', domains, Iterable) - valid_types = (openmc.Material, openmc.Cell, openmc.Universe) + valid_types = (openmc.Material, + openmc.Cell, openmc.Universe) for domain in domains: if not isinstance(domain, valid_types): raise ValueError( @@ -1255,9 +1363,12 @@ class Settings: @source_rejection_fraction.setter def source_rejection_fraction(self, source_rejection_fraction: float): - cv.check_type('source_rejection_fraction', source_rejection_fraction, Real) - cv.check_greater_than('source_rejection_fraction', source_rejection_fraction, 0) - cv.check_less_than('source_rejection_fraction', source_rejection_fraction, 1) + cv.check_type('source_rejection_fraction', + source_rejection_fraction, Real) + cv.check_greater_than('source_rejection_fraction', + source_rejection_fraction, 0) + cv.check_less_than('source_rejection_fraction', + source_rejection_fraction, 1) self._source_rejection_fraction = source_rejection_fraction @property @@ -1422,6 +1533,45 @@ class Settings: subelement = ET.SubElement(element, key) subelement.text = str(self._surf_source_write[key]) + def _create_collision_track_subelement(self, root): + if self._collision_track: + element = ET.SubElement(root, "collision_track") + if 'cell_ids' in self._collision_track: + subelement = ET.SubElement(element, "cell_ids") + subelement.text = ' '.join( + str(x) for x in self._collision_track['cell_ids']) + if 'reactions' in self._collision_track: + subelement = ET.SubElement(element, "reactions") + subelement.text = ' '.join( + str(x) for x in self._collision_track['reactions']) + if 'universe_ids' in self._collision_track: + subelement = ET.SubElement(element, "universe_ids") + subelement.text = ' '.join( + str(x) for x in self._collision_track['universe_ids']) + if 'material_ids' in self._collision_track: + subelement = ET.SubElement(element, "material_ids") + subelement.text = ' '.join( + str(x) for x in self._collision_track['material_ids']) + if 'nuclides' in self._collision_track: + subelement = ET.SubElement(element, "nuclides") + subelement.text = ' '.join( + str(x) for x in self._collision_track['nuclides']) + if 'deposited_E_threshold' in self._collision_track: + subelement = ET.SubElement(element, "deposited_E_threshold") + subelement.text = str( + self._collision_track['deposited_E_threshold']) + if 'max_collisions' in self._collision_track: + subelement = ET.SubElement(element, "max_collisions") + subelement.text = str(self._collision_track['max_collisions']) + if 'max_collision_track_files' in self._collision_track: + subelement = ET.SubElement( + element, "max_collision_track_files") + subelement.text = str( + self._collision_track['max_collision_track_files']) + if 'mcpl' in self._collision_track: + subelement = ET.SubElement(element, "mcpl") + subelement.text = str(self._collision_track['mcpl']).lower() + def _create_confidence_intervals(self, root): if self._confidence_intervals is not None: element = ET.SubElement(root, "confidence_intervals") @@ -1477,8 +1627,8 @@ class Settings: # use default heuristic for entropy mesh if not set by user if self.entropy_mesh.dimension is None: if self.particles is None: - raise RuntimeError("Number of particles must be set in order to " \ - "use entropy mesh dimension heuristic") + raise RuntimeError("Number of particles must be set in order to " + "use entropy mesh dimension heuristic") else: n = ceil((self.particles / 20.0)**(1.0 / 3.0)) d = len(self.entropy_mesh.lower_left) @@ -1568,7 +1718,8 @@ class Settings: path = f"./mesh[@id='{self.ufs_mesh.id}']" if root.find(path) is None: root.append(self.ufs_mesh.to_xml_element()) - if mesh_memo is not None: mesh_memo.add(self.ufs_mesh.id) + if mesh_memo is not None: + mesh_memo.add(self.ufs_mesh.id) def _create_use_decay_photons_subelement(self, root): if self._use_decay_photons is not None: @@ -1694,11 +1845,13 @@ class Settings: if 'collision' in self._weight_window_checkpoints: subelement = ET.SubElement(element, "collision") - subelement.text = str(self._weight_window_checkpoints['collision']).lower() + subelement.text = str( + self._weight_window_checkpoints['collision']).lower() if 'surface' in self._weight_window_checkpoints: subelement = ET.SubElement(element, "surface") - subelement.text = str(self._weight_window_checkpoints['surface']).lower() + subelement.text = str( + self._weight_window_checkpoints['surface']).lower() def _create_max_history_splits_subelement(self, root): if self._max_history_splits is not None: @@ -1730,6 +1883,9 @@ class Settings: for domain in domains: domain_elem = ET.SubElement(mesh_elem, 'domain') domain_elem.set('id', str(domain.id)) + domain_elem.set( + 'type', domain.__class__.__name__.lower()) + if mesh_memo is not None and mesh.id not in mesh_memo: domain_elem.set('type', domain.__class__.__name__.lower()) # See if a element already exists -- if not, add it path = f"./mesh[@id='{mesh.id}']" @@ -1881,6 +2037,25 @@ class Settings: value = int(value) self.surf_source_write[key] = value + def _collision_track_from_xml_element(self, root): + elem = root.find('collision_track') + if elem is not None: + for key in ('cell_ids', 'reactions', 'universe_ids', 'material_ids', 'nuclides', + 'deposited_E_threshold', 'max_collisions', "max_collision_track_files", 'mcpl'): + value = get_text(elem, key) + if value is not None: + if key in ('cell_ids', 'universe_ids', 'material_ids'): + value = [int(x) for x in value.split()] + elif key in ('reactions', 'nuclides'): + value = value.split() + elif key in ('max_collisions', 'max_collision_track_files'): + value = int(value) + elif key == 'deposited_E_threshold': + value = float(value) + elif key == 'mcpl': + value = value in ('true', '1') + self.collision_track[key] = value + def _confidence_intervals_from_xml_element(self, root): text = get_text(root, 'confidence_intervals') if text is not None: @@ -2180,7 +2355,8 @@ class Settings: elif domain_type == 'universe': domain = openmc.Universe(domain_id) domains.append(domain) - self.random_ray['source_region_meshes'].append((mesh, domains)) + self.random_ray['source_region_meshes'].append( + (mesh, domains)) def _use_decay_photons_from_xml_element(self, root): text = get_text(root, 'use_decay_photons') @@ -2223,6 +2399,7 @@ class Settings: self._create_sourcepoint_subelement(element) self._create_surf_source_read_subelement(element) self._create_surf_source_write_subelement(element) + self._create_collision_track_subelement(element) self._create_confidence_intervals(element) self._create_electron_treatment_subelement(element) self._create_energy_mode_subelement(element) @@ -2336,6 +2513,7 @@ class Settings: settings._sourcepoint_from_xml_element(elem) settings._surf_source_read_from_xml_element(elem) settings._surf_source_write_from_xml_element(elem) + settings._collision_track_from_xml_element(elem) settings._confidence_intervals_from_xml_element(elem) settings._electron_treatment_from_xml_element(elem) settings._energy_mode_from_xml_element(elem) diff --git a/openmc/source.py b/openmc/source.py index 9b730cf1d..84d8a9619 100644 --- a/openmc/source.py +++ b/openmc/source.py @@ -6,7 +6,6 @@ from numbers import Real from pathlib import Path import warnings from typing import Any -from pathlib import Path import lxml.etree as ET import numpy as np @@ -107,10 +106,12 @@ class SourceBase(ABC): cv.check_type('fissionable', value, bool) self._constraints['fissionable'] = value elif key == 'rejection_strategy': - cv.check_value('rejection strategy', value, ('resample', 'kill')) + cv.check_value('rejection strategy', + value, ('resample', 'kill')) self._constraints['rejection_strategy'] = value else: - raise ValueError(f'Unknown key in constraints dictionary: {key}') + raise ValueError( + f'Unknown key in constraints dictionary: {key}') @abstractmethod def populate_xml_element(self, element): @@ -144,13 +145,16 @@ class SourceBase(ABC): dt_elem = ET.SubElement(constraints_elem, "domain_type") dt_elem.text = constraints["domain_type"] id_elem = ET.SubElement(constraints_elem, "domain_ids") - id_elem.text = ' '.join(str(uid) for uid in constraints["domain_ids"]) + id_elem.text = ' '.join(str(uid) + for uid in constraints["domain_ids"]) if "time_bounds" in constraints: dt_elem = ET.SubElement(constraints_elem, "time_bounds") - dt_elem.text = ' '.join(str(t) for t in constraints["time_bounds"]) + dt_elem.text = ' '.join(str(t) + for t in constraints["time_bounds"]) if "energy_bounds" in constraints: dt_elem = ET.SubElement(constraints_elem, "energy_bounds") - dt_elem.text = ' '.join(str(E) for E in constraints["energy_bounds"]) + dt_elem.text = ' '.join(str(E) + for E in constraints["energy_bounds"]) if "fissionable" in constraints: dt_elem = ET.SubElement(constraints_elem, "fissionable") dt_elem.text = str(constraints["fissionable"]).lower() @@ -199,7 +203,8 @@ class SourceBase(ABC): elif source_type == 'mesh': return MeshSource.from_xml_element(elem, meshes) else: - raise ValueError(f'Source type {source_type} is not recognized') + raise ValueError( + f'Source type {source_type} is not recognized') @staticmethod def _get_constraints(elem: ET.Element) -> dict[str, Any]: @@ -316,7 +321,8 @@ class IndependentSource(SourceBase): time: openmc.stats.Univariate | None = None, strength: float = 1.0, particle: str = 'neutron', - domains: Sequence[openmc.Cell | openmc.Material | openmc.Universe] | None = None, + domains: Sequence[openmc.Cell | openmc.Material | + openmc.Universe] | None = None, constraints: dict[str, Any] | None = None ): if domains is not None: @@ -527,11 +533,12 @@ class MeshSource(SourceBase): 'fissionable', and 'rejection_strategy'. """ + def __init__( self, mesh: MeshBase, sources: Sequence[SourceBase], - constraints: dict[str, Any] | None = None, + constraints: dict[str, Any] | None = None, ): super().__init__(strength=None, constraints=constraints) self.mesh = mesh @@ -577,7 +584,8 @@ class MeshSource(SourceBase): elif isinstance(self.mesh, UnstructuredMesh): if s.ndim > 1: - raise ValueError('Sources must be a 1-D array for unstructured mesh') + raise ValueError( + 'Sources must be a 1-D array for unstructured mesh') self._sources = s for src in self._sources: @@ -646,7 +654,8 @@ class MeshSource(SourceBase): mesh_id = int(get_text(elem, 'mesh')) mesh = meshes[mesh_id] - sources = [SourceBase.from_xml_element(e) for e in elem.iterchildren('source')] + sources = [SourceBase.from_xml_element( + e) for e in elem.iterchildren('source')] constraints = cls._get_constraints(elem) return cls(mesh, sources, constraints=constraints) @@ -656,7 +665,8 @@ def Source(*args, **kwargs): A function for backward compatibility of sources. Will be removed in the future. Please update to IndependentSource. """ - warnings.warn("This class is deprecated in favor of 'IndependentSource'", FutureWarning) + warnings.warn( + "This class is deprecated in favor of 'IndependentSource'", FutureWarning) return openmc.IndependentSource(*args, **kwargs) @@ -703,6 +713,7 @@ class CompiledSource(SourceBase): 'fissionable', and 'rejection_strategy'. """ + def __init__( self, library: PathLike, @@ -914,7 +925,8 @@ class ParticleType(IntEnum): try: return cls[value.upper()] except KeyError: - raise ValueError(f"Invalid string for creation of {cls.__name__}: {value}") + raise ValueError( + f"Invalid string for creation of {cls.__name__}: {value}") @classmethod def from_pdg_number(cls, pdg_number: int) -> ParticleType: @@ -984,6 +996,7 @@ class SourceParticle: Type of the particle """ + def __init__( self, r: Iterable[float] = (0., 0., 0.), @@ -1042,7 +1055,8 @@ def write_source_file( openmc.SourceParticle """ - cv.check_iterable_type("source particles", source_particles, SourceParticle) + cv.check_iterable_type( + "source particles", source_particles, SourceParticle) pl = ParticleList(source_particles) pl.export_to_hdf5(filename, **kwargs) @@ -1104,7 +1118,8 @@ class ParticleList(list): for particle in f.particles: # Determine particle type based on the PDG number try: - particle_type = ParticleType.from_pdg_number(particle.pdgcode) + particle_type = ParticleType.from_pdg_number( + particle.pdgcode) except ValueError: particle_type = "UNKNOWN" @@ -1241,3 +1256,133 @@ def read_source_file(filename: PathLike) -> ParticleList: return ParticleList.from_hdf5(filename) else: return ParticleList.from_mcpl(filename) + + +def read_collision_track_hdf5(filename): + """Read a collision track file in HDF5 format. + + Parameters + ---------- + filename : str or path-like + Path to the HDF5 collision track file. + + Returns + ------- + numpy.ndarray + Structured array containing collision track data. + + See Also + -------- + read_collision_track_mcpl + read_collision_track_file + """ + + with h5py.File(filename, 'r') as file: + data = file['collision_track_bank'][:] + + return data + + +def read_collision_track_mcpl(file_path): + """Read a collision track file in MCPL format. + + Parameters + ---------- + file_path : str or path-like + Path to the MCPL collision track file. + + Returns + ------- + numpy.ndarray + Structured array of particle collision track information, including + position, direction, energy, weight, reaction data, and identifiers. + + See Also + -------- + read_collision_track_hdf5 + read_collision_track_file + """ + import mcpl + myfile = mcpl.MCPLFile(file_path) + data = { + 'r': [], # for position (x, y, z) + 'u': [], # for direction (ux, uy, uz) + 'E': [], 'dE': [], 'time': [], + 'wgt': [], 'event_mt': [], 'delayed_group': [], + 'cell_id': [], 'nuclide_id': [], 'material_id': [], + 'universe_id': [], 'n_collision': [], 'particle': [], + 'parent_id': [], 'progeny_id': [] + } + + # Read and collect data from the MCPL file + for i, p in enumerate(myfile.particles): + if f'blob_{i}' in myfile.blobs: + blob_data = myfile.blobs[f'blob_{i}'] + decoded_str = blob_data.decode('utf-8') + pairs = decoded_str.split(';') + values_dict = {k.strip(): v.strip() + for k, v in (pair.split(':') for pair in pairs if pair.strip())} + + data['r'].append((p.x, p.y, p.z)) # Append as tuple + data['u'].append((p.ux, p.uy, p.uz)) # Append as tuple + data['E'].append(p.ekin * 1e6) + data['dE'].append(float(values_dict.get('dE', 0))) + data['time'].append(p.time * 1e-3) + data['wgt'].append(p.weight) + data['event_mt'].append(int(values_dict.get('event_mt', 0))) + data['delayed_group'].append( + int(values_dict.get('delayed_group', 0))) + data['cell_id'].append(int(values_dict.get('cell_id', 0))) + data['nuclide_id'].append(int(values_dict.get('nuclide_id', 0))) + data['material_id'].append(int(values_dict.get('material_id', 0))) + data['universe_id'].append(int(values_dict.get('universe_id', 0))) + data['n_collision'].append(int(values_dict.get('n_collision', 0))) + data['particle'].append(ParticleType.from_pdg_number(p.pdgcode)) + data['parent_id'].append(int(values_dict.get('parent_id', 0))) + data['progeny_id'].append(int(values_dict.get('progeny_id', 0))) + + dtypes = [ + ('r', [('x', 'f8'), ('y', 'f8'), ('z', 'f8')]), + ('u', [('x', 'f8'), ('y', 'f8'), ('z', 'f8')]), + ('E', 'f8'), ('dE', 'f8'), ('time', 'f8'), ('wgt', 'f8'), + ('event_mt', 'f8'), ('delayed_group', 'i4'), ('cell_id', 'i4'), + ('nuclide_id', 'i4'), ('material_id', 'i4'), ('universe_id', 'i4'), + ('n_collision', 'i4'), ('particle', 'i4'), + ('parent_id', 'i8'), ('progeny_id', 'i8') + ] + + structured_array = np.zeros(len(data['r']), dtype=dtypes) + for key in data: + structured_array[key] = data[key] # Assign data + + return structured_array + + +def read_collision_track_file(filename): + """Read a collision track file (HDF5 or MCPL) and return its data. + + Parameters + ---------- + filename : str or path-like + Path to the collision track file to read. Must end with + ``.h5`` or ``.mcpl``. + + Returns + ------- + numpy.ndarray + Structured array containing collision track data. + + See Also + -------- + read_collision_track_hdf5 + read_collision_track_mcpl + """ + + filename = Path(filename) + if filename.suffix not in ('.h5', '.mcpl'): + raise ValueError('Collision track file must have a .h5 or .mcpl extension.') + + if filename.suffix == '.h5': + return read_collision_track_hdf5(filename) + else: + return read_collision_track_mcpl(filename) diff --git a/src/bank.cpp b/src/bank.cpp index 3e806b3c0..33790379b 100644 --- a/src/bank.cpp +++ b/src/bank.cpp @@ -20,6 +20,8 @@ vector source_bank; SharedArray surf_source_bank; +SharedArray collision_track_bank; + // The fission bank is allocated as a SharedArray, rather than a vector, as it // will be shared by all threads in the simulation. It will be allocated to a // fixed maximum capacity in the init_fission_bank() function. Then, Elements @@ -50,6 +52,7 @@ void free_memory_bank() { simulation::source_bank.clear(); simulation::surf_source_bank.clear(); + simulation::collision_track_bank.clear(); simulation::fission_bank.clear(); simulation::progeny_per_particle.clear(); simulation::ifp_source_delayed_group_bank.clear(); diff --git a/src/collision_track.cpp b/src/collision_track.cpp new file mode 100644 index 000000000..75e56574a --- /dev/null +++ b/src/collision_track.cpp @@ -0,0 +1,238 @@ +#include "openmc/collision_track.h" + +#include +#include + +#include + +#include "openmc/bank.h" +#include "openmc/bank_io.h" +#include "openmc/cell.h" +#include "openmc/constants.h" +#include "openmc/error.h" +#include "openmc/file_utils.h" +#include "openmc/hdf5_interface.h" +#include "openmc/material.h" +#include "openmc/mcpl_interface.h" +#include "openmc/message_passing.h" +#include "openmc/nuclide.h" +#include "openmc/output.h" +#include "openmc/particle.h" +#include "openmc/settings.h" +#include "openmc/simulation.h" +#include "openmc/universe.h" + +#ifdef OPENMC_MPI +#include +#endif + +namespace openmc { + +namespace { + +hid_t h5_collision_track_banktype() +{ + hid_t postype = H5Tcreate(H5T_COMPOUND, sizeof(Position)); + H5Tinsert(postype, "x", HOFFSET(Position, x), H5T_NATIVE_DOUBLE); + H5Tinsert(postype, "y", HOFFSET(Position, y), H5T_NATIVE_DOUBLE); + H5Tinsert(postype, "z", HOFFSET(Position, z), H5T_NATIVE_DOUBLE); + + hid_t banktype = H5Tcreate(H5T_COMPOUND, sizeof(CollisionTrackSite)); + + H5Tinsert(banktype, "r", HOFFSET(CollisionTrackSite, r), postype); + H5Tinsert(banktype, "u", HOFFSET(CollisionTrackSite, u), postype); + H5Tinsert(banktype, "E", HOFFSET(CollisionTrackSite, E), H5T_NATIVE_DOUBLE); + H5Tinsert(banktype, "dE", HOFFSET(CollisionTrackSite, dE), H5T_NATIVE_DOUBLE); + H5Tinsert( + banktype, "time", HOFFSET(CollisionTrackSite, time), H5T_NATIVE_DOUBLE); + H5Tinsert( + banktype, "wgt", HOFFSET(CollisionTrackSite, wgt), H5T_NATIVE_DOUBLE); + H5Tinsert(banktype, "event_mt", HOFFSET(CollisionTrackSite, event_mt), + H5T_NATIVE_INT); + H5Tinsert(banktype, "delayed_group", + HOFFSET(CollisionTrackSite, delayed_group), H5T_NATIVE_INT); + H5Tinsert( + banktype, "cell_id", HOFFSET(CollisionTrackSite, cell_id), H5T_NATIVE_INT); + H5Tinsert(banktype, "nuclide_id", HOFFSET(CollisionTrackSite, nuclide_id), + H5T_NATIVE_INT); + H5Tinsert(banktype, "material_id", HOFFSET(CollisionTrackSite, material_id), + H5T_NATIVE_INT); + H5Tinsert(banktype, "universe_id", HOFFSET(CollisionTrackSite, universe_id), + H5T_NATIVE_INT); + H5Tinsert(banktype, "n_collision", HOFFSET(CollisionTrackSite, n_collision), + H5T_NATIVE_INT); + H5Tinsert(banktype, "particle", HOFFSET(CollisionTrackSite, particle), + H5T_NATIVE_INT); + H5Tinsert(banktype, "parent_id", HOFFSET(CollisionTrackSite, parent_id), + H5T_NATIVE_INT64); + H5Tinsert(banktype, "progeny_id", HOFFSET(CollisionTrackSite, progeny_id), + H5T_NATIVE_INT64); + H5Tclose(postype); + return banktype; +} + +void write_collision_track_bank(hid_t group_id, + openmc::span collision_track_bank, + const openmc::vector& bank_index) +{ + hid_t banktype = h5_collision_track_banktype(); +#ifdef OPENMC_MPI + write_bank_dataset("collision_track_bank", group_id, collision_track_bank, + bank_index, banktype, mpi::collision_track_site); +#else + write_bank_dataset("collision_track_bank", group_id, collision_track_bank, + bank_index, banktype); +#endif + + H5Tclose(banktype); +} + +void write_h5_collision_track(const char* filename, + openmc::span collision_track_bank, + const openmc::vector& bank_index) +{ +#ifdef PHDF5 + bool parallel = true; +#else + bool parallel = false; +#endif + + if (!filename) + fatal_error("write_h5_collision_track filename needs a nonempty name."); + + std::string filename_(filename); + const auto extension = get_file_extension(filename_); + if (extension.empty()) { + filename_.append(".h5"); + } else if (extension != "h5") { + warning("write_h5_collision_track was passed a file extension differing " + "from .h5, but an hdf5 file will be written."); + } + + hid_t file_id; + if (mpi::master || parallel) { + file_id = file_open(filename_.c_str(), 'w', true); + + // Write filetype and version info + write_attribute(file_id, "filetype", "collision_track"); + write_attribute(file_id, "version", VERSION_COLLISION_TRACK); + } + + write_collision_track_bank(file_id, collision_track_bank, bank_index); + + if (mpi::master || parallel) + file_close(file_id); +} + +} // namespace + +bool should_record_event(int id_cell, int mt_event, const std::string& nuclide, + int id_universe, int id_material, double energy_loss) +{ + auto matches_filter = [](const auto& filter_set, const auto& value) { + return filter_set.empty() || filter_set.count(value) > 0; + }; + + const auto& cfg = settings::collision_track_config; + return simulation::current_batch > settings::n_inactive && + !simulation::collision_track_bank.full() && + matches_filter(cfg.cell_ids, id_cell) && + matches_filter(cfg.mt_numbers, mt_event) && + matches_filter(cfg.universe_ids, id_universe) && + matches_filter(cfg.material_ids, id_material) && + matches_filter(cfg.nuclides, nuclide) && + (cfg.deposited_energy_threshold == 0 || + cfg.deposited_energy_threshold < energy_loss); +} + +void collision_track_reserve_bank() +{ + simulation::collision_track_bank.reserve( + settings::collision_track_config.max_collisions); +} + +void collision_track_flush_bank() +{ + const auto& cfg = settings::collision_track_config; + if (simulation::ct_current_file > cfg.max_files) + return; + + bool last_batch = (simulation::current_batch == settings::n_batches); + if (!simulation::collision_track_bank.full() && !last_batch) + return; + + auto size = simulation::collision_track_bank.size(); + if (size == 0 && !last_batch) + return; + + auto collision_track_work_index = mpi::calculate_parallel_index_vector(size); + openmc::span collisiontrackbankspan( + simulation::collision_track_bank.begin(), size); + + std::string ext = cfg.mcpl_write ? "mcpl" : "h5"; + auto filename = fmt::format("{}collision_track.{}.{}", settings::path_output, + simulation::ct_current_file, ext); + + if (cfg.max_files == 1 || (simulation::ct_current_file == 1 && last_batch)) { + filename = settings::path_output + "collision_track." + ext; + } + write_message("Creating {}...", filename, 4); + + if (cfg.mcpl_write) { + write_mcpl_collision_track( + filename.c_str(), collisiontrackbankspan, collision_track_work_index); + } else { + write_h5_collision_track( + filename.c_str(), collisiontrackbankspan, collision_track_work_index); + } + + simulation::collision_track_bank.clear(); + if (!last_batch && cfg.max_files >= 1) { + collision_track_reserve_bank(); + } + ++simulation::ct_current_file; +} + +void collision_track_record(Particle& particle) +{ + int cell_index = particle.lowest_coord().cell(); + if (cell_index == C_NONE) + return; + + int cell_id = model::cells[cell_index]->id_; + const auto* nuclide_ptr = data::nuclides[particle.event_nuclide()].get(); + std::string nuclide = nuclide_ptr->name_; + int universe_id = model::universes[particle.lowest_coord().universe()]->id_; + double delta_E = particle.E_last() - particle.E(); + int material_index = particle.material(); + if (material_index == C_NONE) + return; + + int material_id = model::materials[material_index]->id_; + + if (!should_record_event(cell_id, particle.event_mt(), nuclide, universe_id, + material_id, delta_E)) + return; + + CollisionTrackSite site; + site.r = particle.r(); + site.u = particle.u(); + site.E = particle.E_last(); + site.dE = delta_E; + site.time = particle.time(); + site.wgt = particle.wgt(); + site.event_mt = particle.event_mt(); + site.delayed_group = particle.delayed_group(); + site.cell_id = cell_id; + site.nuclide_id = + 10000 * nuclide_ptr->Z_ + 10 * nuclide_ptr->A_ + nuclide_ptr->metastable_; + site.material_id = material_id; + site.universe_id = universe_id; + site.n_collision = particle.n_collision(); + site.particle = particle.type(); + site.parent_id = particle.id(); + site.progeny_id = particle.n_progeny(); + simulation::collision_track_bank.thread_safe_append(site); +} + +} // namespace openmc diff --git a/src/finalize.cpp b/src/finalize.cpp index 659f390b3..9ee943409 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -3,6 +3,7 @@ #include "openmc/bank.h" #include "openmc/capi.h" #include "openmc/cmfd_solver.h" +#include "openmc/collision_track.h" #include "openmc/constants.h" #include "openmc/cross_sections.h" #include "openmc/dagmc.h" @@ -76,6 +77,7 @@ int openmc_finalize() // Reset global variables settings::assume_separate = false; settings::check_overlaps = false; + settings::collision_track_config = CollisionTrackConfig {}; settings::confidence_intervals = false; settings::create_fission_neutrons = true; settings::create_delayed_neutrons = true; @@ -177,6 +179,9 @@ int openmc_finalize() if (mpi::source_site != MPI_DATATYPE_NULL) { MPI_Type_free(&mpi::source_site); } + if (mpi::collision_track_site != MPI_DATATYPE_NULL) { + MPI_Type_free(&mpi::collision_track_site); + } #endif openmc_reset_random_ray(); diff --git a/src/initialize.cpp b/src/initialize.cpp index 36f326116..e2a5b9743 100644 --- a/src/initialize.cpp +++ b/src/initialize.cpp @@ -178,6 +178,37 @@ void initialize_mpi(MPI_Comm intracomm) MPI_DOUBLE, MPI_INT, MPI_INT, MPI_INT, MPI_INT, MPI_LONG, MPI_LONG}; MPI_Type_create_struct(11, blocks, disp, types, &mpi::source_site); MPI_Type_commit(&mpi::source_site); + + CollisionTrackSite bc; + MPI_Aint dispc[16]; + MPI_Get_address(&bc.r, &dispc[0]); // double + MPI_Get_address(&bc.u, &dispc[1]); // double + MPI_Get_address(&bc.E, &dispc[2]); // double + MPI_Get_address(&bc.dE, &dispc[3]); // double + MPI_Get_address(&bc.time, &dispc[4]); // double + MPI_Get_address(&bc.wgt, &dispc[5]); // double + MPI_Get_address(&bc.event_mt, &dispc[6]); // int + MPI_Get_address(&bc.delayed_group, &dispc[7]); // int + MPI_Get_address(&bc.cell_id, &dispc[8]); // int + MPI_Get_address(&bc.nuclide_id, &dispc[9]); // int + MPI_Get_address(&bc.material_id, &dispc[10]); // int + MPI_Get_address(&bc.universe_id, &dispc[11]); // int + MPI_Get_address(&bc.n_collision, &dispc[12]); // int + MPI_Get_address(&bc.particle, &dispc[13]); // int + MPI_Get_address(&bc.parent_id, &dispc[14]); // int64_t + MPI_Get_address(&bc.progeny_id, &dispc[15]); // int64_t + for (int i = 15; i >= 0; --i) { + dispc[i] -= dispc[0]; + } + + int blocksc[] = {3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; + MPI_Datatype typesc[] = {MPI_DOUBLE, MPI_DOUBLE, MPI_DOUBLE, MPI_DOUBLE, + MPI_DOUBLE, MPI_DOUBLE, MPI_INT, MPI_INT, MPI_INT, MPI_INT, MPI_INT, + MPI_INT, MPI_INT, MPI_INT, MPI_INT64_T, MPI_INT64_T}; + + MPI_Type_create_struct( + 16, blocksc, dispc, typesc, &mpi::collision_track_site); + MPI_Type_commit(&mpi::collision_track_site); } #endif // OPENMC_MPI diff --git a/src/mcpl_interface.cpp b/src/mcpl_interface.cpp index b8e780710..129407301 100644 --- a/src/mcpl_interface.cpp +++ b/src/mcpl_interface.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -61,6 +62,8 @@ using mcpl_hdr_nparticles_fpt = uint64_t (*)(mcpl_file_t* file_handle); using mcpl_read_fpt = const mcpl_particle_repr_t* (*)(mcpl_file_t* file_handle); using mcpl_close_file_fpt = void (*)(mcpl_file_t* file_handle); +using mcpl_hdr_add_data_fpt = void (*)(mcpl_outfile_t* file_handle, + const char* key, int32_t ldata, const char* data); using mcpl_create_outfile_fpt = mcpl_outfile_t* (*)(const char* filename); using mcpl_hdr_set_srcname_fpt = void (*)( mcpl_outfile_t* outfile_handle, const char* srcname); @@ -110,6 +113,7 @@ struct McplApi { mcpl_close_file_fpt close_file; mcpl_create_outfile_fpt create_outfile; mcpl_hdr_set_srcname_fpt hdr_set_srcname; + mcpl_hdr_add_data_fpt hdr_add_data; mcpl_add_particle_fpt add_particle; mcpl_close_outfile_fpt close_outfile; mcpl_hdr_add_stat_sum_fpt hdr_add_stat_sum; @@ -146,6 +150,8 @@ struct McplApi { load_symbol_platform("mcpl_create_outfile")); hdr_set_srcname = reinterpret_cast( load_symbol_platform("mcpl_hdr_set_srcname")); + hdr_add_data = reinterpret_cast( + load_symbol_platform("mcpl_hdr_add_data")); add_particle = reinterpret_cast( load_symbol_platform("mcpl_add_particle")); close_outfile = reinterpret_cast( @@ -545,4 +551,153 @@ void write_mcpl_source_point(const char* filename, span source_bank, } } +// Collision track feature with MCPL +void write_mcpl_collision_track_internal(mcpl_outfile_t* file_id, + span collision_track_bank, + const vector& bank_index_all_ranks) +{ + if (mpi::master) { + if (!file_id) { + fatal_error("MCPL: Internal error - master rank called " + "write_mcpl_source_bank_internal with null file_id."); + } + vector receive_buffer; + vector all_sites; + all_sites.reserve(static_cast(bank_index_all_ranks.back())); + vector all_blobs; + all_blobs.reserve(static_cast(bank_index_all_ranks.back())); + + for (int rank_idx = 0; rank_idx < mpi::n_procs; ++rank_idx) { + size_t num_sites_on_rank = static_cast( + bank_index_all_ranks[rank_idx + 1] - bank_index_all_ranks[rank_idx]); + if (num_sites_on_rank == 0) + continue; + + span sites_to_process; +#ifdef OPENMC_MPI + if (rank_idx == mpi::rank) { + sites_to_process = openmc::span( + collision_track_bank.data(), num_sites_on_rank); + } else { + receive_buffer.resize(num_sites_on_rank); + MPI_Recv(receive_buffer.data(), num_sites_on_rank, + mpi::collision_track_site, rank_idx, rank_idx, mpi::intracomm, + MPI_STATUS_IGNORE); + sites_to_process = openmc::span( + receive_buffer.data(), num_sites_on_rank); + } +#else + sites_to_process = openmc::span( + collision_track_bank.data(), num_sites_on_rank); +#endif + + for (const auto& site : sites_to_process) { + std::ostringstream custom_data_stream; + custom_data_stream << " dE : " << site.dE + << " ; event_mt : " << site.event_mt + << " ; delayed_group : " << site.delayed_group + << " ; cell_id : " << site.cell_id + << " ; nuclide_id : " << site.nuclide_id + << " ; material_id : " << site.material_id + << " ; universe_id : " << site.universe_id + << " ; n_collision : " << site.n_collision + << " ; parent_id : " << site.parent_id + << " ; progeny_id : " << site.progeny_id; + + all_blobs.push_back(custom_data_stream.str()); + all_sites.push_back(site); + } + } + + for (size_t idx = 0; idx < all_blobs.size(); ++idx) { + const auto& blob = all_blobs[idx]; + std::string key = "blob_" + std::to_string(idx); + g_mcpl_api->hdr_add_data(file_id, key.c_str(), blob.size(), blob.c_str()); + } + + for (const auto& site : all_sites) { + mcpl_particle_repr_t p_repr {}; + p_repr.position[0] = site.r.x; + p_repr.position[1] = site.r.y; + p_repr.position[2] = site.r.z; + p_repr.direction[0] = site.u.x; + p_repr.direction[1] = site.u.y; + p_repr.direction[2] = site.u.z; + p_repr.ekin = site.E * 1e-6; + p_repr.time = site.time * 1e3; + p_repr.weight = site.wgt; + switch (site.particle) { + case ParticleType::neutron: + p_repr.pdgcode = 2112; + break; + case ParticleType::photon: + p_repr.pdgcode = 22; + break; + case ParticleType::electron: + p_repr.pdgcode = 11; + break; + case ParticleType::positron: + p_repr.pdgcode = -11; + break; + default: + continue; + } + g_mcpl_api->add_particle(file_id, &p_repr); + } + } else { +#ifdef OPENMC_MPI + if (!collision_track_bank.empty()) { + MPI_Send(collision_track_bank.data(), collision_track_bank.size(), + mpi::collision_track_site, 0, mpi::rank, mpi::intracomm); + } +#endif + } +} + +void write_mcpl_collision_track(const char* filename, + span collision_track_bank, + const vector& bank_index) +{ + ensure_mcpl_ready_or_fatal(); + + std::string filename_(filename); + const auto extension = get_file_extension(filename_); + if (extension.empty()) { + filename_.append(".mcpl"); + } else if (extension != "mcpl") { + warning(fmt::format("Specified filename '{}' has an extension '.{}', but " + "an MCPL file (.mcpl) will be written using this name.", + filename, extension)); + } + + mcpl_outfile_t* file_id = nullptr; + + if (mpi::master) { + file_id = g_mcpl_api->create_outfile(filename_.c_str()); + if (!file_id) { + fatal_error(fmt::format( + "MCPL: Failed to create output file '{}'. Check permissions and path.", + filename_)); + } + std::string src_line; + if (VERSION_DEV) { + src_line = fmt::format("OpenMC {}.{}.{}-dev{}", VERSION_MAJOR, + VERSION_MINOR, VERSION_RELEASE, VERSION_COMMIT_COUNT); + } else { + src_line = fmt::format( + "OpenMC {}.{}.{}", VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE); + } + + g_mcpl_api->hdr_set_srcname(file_id, src_line.c_str()); + } + write_mcpl_collision_track_internal( + file_id, collision_track_bank, bank_index); + + if (mpi::master) { + if (file_id) { + g_mcpl_api->close_outfile(file_id); + } + } +} + } // namespace openmc diff --git a/src/message_passing.cpp b/src/message_passing.cpp index 374c1aa72..a160f6d73 100644 --- a/src/message_passing.cpp +++ b/src/message_passing.cpp @@ -10,6 +10,7 @@ bool master {true}; #ifdef OPENMC_MPI MPI_Comm intracomm {MPI_COMM_NULL}; MPI_Datatype source_site {MPI_DATATYPE_NULL}; +MPI_Datatype collision_track_site {MPI_DATATYPE_NULL}; #endif extern "C" bool openmc_master() diff --git a/src/particle.cpp b/src/particle.cpp index 6ba8ebf12..2d70d715e 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -8,6 +8,7 @@ #include "openmc/bank.h" #include "openmc/capi.h" #include "openmc/cell.h" +#include "openmc/collision_track.h" #include "openmc/constants.h" #include "openmc/dagmc.h" #include "openmc/error.h" @@ -351,6 +352,11 @@ void Particle::event_collide() collision_mg(*this); } + // Collision track feature to recording particle interaction + if (settings::collision_track) { + collision_track_record(*this); + } + // Score collision estimator tallies -- this is done after a collision // has occurred rather than before because we need information on the // outgoing energy for any tallies with an outgoing energy filter diff --git a/src/physics.cpp b/src/physics.cpp index 3a17077b3..41509af97 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -122,6 +122,7 @@ void sample_neutron_reaction(Particle& p) "with k-effective close to or greater than one."); } } + p.event_mt() = rx.mt_; } // Create secondary photons @@ -663,7 +664,9 @@ void absorption(Particle& p, int i_nuclide) p.wgt() = 0.0; p.event() = TallyEvent::ABSORB; - p.event_mt() = N_DISAPPEAR; + if (!p.fission()) { + p.event_mt() = N_DISAPPEAR; + } } } } diff --git a/src/settings.cpp b/src/settings.cpp index 13b91b0e4..9dcf7c8db 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -11,6 +11,7 @@ #endif #include "openmc/capi.h" +#include "openmc/collision_track.h" #include "openmc/constants.h" #include "openmc/container_util.h" #include "openmc/distribution.h" @@ -26,6 +27,7 @@ #include "openmc/plot.h" #include "openmc/random_lcg.h" #include "openmc/random_ray/random_ray.h" +#include "openmc/reaction.h" #include "openmc/simulation.h" #include "openmc/source.h" #include "openmc/string_utils.h" @@ -45,6 +47,7 @@ namespace settings { // Default values for boolean flags bool assume_separate {false}; bool check_overlaps {false}; +bool collision_track {false}; bool cmfd_run {false}; bool confidence_intervals {false}; bool create_delayed_neutrons {true}; @@ -128,6 +131,7 @@ std::unordered_set statepoint_batch; double source_rejection_fraction {0.05}; double free_gas_threshold {400.0}; std::unordered_set source_write_surf_id; +CollisionTrackConfig collision_track_config {}; int64_t ssw_max_particles; int64_t ssw_max_files; int64_t ssw_cell_id {C_NONE}; @@ -925,8 +929,72 @@ void read_settings_xml(pugi::xml_node root) } } - // If source is not separate and is to be written out in the statepoint file, - // make sure that the sourcepoint batch numbers are contained in the + // Check if the user has specified to write specific collisions + if (check_for_node(root, "collision_track")) { + settings::collision_track = true; + // Get collision track node + xml_node node_ct = root.child("collision_track"); + collision_track_config = CollisionTrackConfig {}; + + // Determine cell ids at which crossing particles are to be banked + if (check_for_node(node_ct, "cell_ids")) { + auto temp = get_node_array(node_ct, "cell_ids"); + for (const auto& b : temp) { + collision_track_config.cell_ids.insert(b); + } + } + 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); + if (reaction_int > 0) { + collision_track_config.mt_numbers.insert(reaction_int); + } + } + } + if (check_for_node(node_ct, "universe_ids")) { + auto temp = get_node_array(node_ct, "universe_ids"); + for (const auto& b : temp) { + collision_track_config.universe_ids.insert(b); + } + } + if (check_for_node(node_ct, "material_ids")) { + auto temp = get_node_array(node_ct, "material_ids"); + for (const auto& b : temp) { + collision_track_config.material_ids.insert(b); + } + } + if (check_for_node(node_ct, "nuclides")) { + auto temp = get_node_array(node_ct, "nuclides"); + for (const auto& b : temp) { + collision_track_config.nuclides.insert(b); + } + } + if (check_for_node(node_ct, "deposited_E_threshold")) { + collision_track_config.deposited_energy_threshold = + std::stod(get_node_value(node_ct, "deposited_E_threshold")); + } + // Get maximum number of particles to be banked per collision + if (check_for_node(node_ct, "max_collisions")) { + collision_track_config.max_collisions = + std::stoll(get_node_value(node_ct, "max_collisions")); + } else { + warning("A maximum number of collisions needs to be specified. " + "By default the code sets 'max_collisions' parameter equals to " + "1000."); + } + // Get maximum number of collision_track files to be created + if (check_for_node(node_ct, "max_collision_track_files")) { + collision_track_config.max_files = + std::stoll(get_node_value(node_ct, "max_collision_track_files")); + } + if (check_for_node(node_ct, "mcpl")) { + collision_track_config.mcpl_write = get_node_value_bool(node_ct, "mcpl"); + } + } + + // If source is not separate and is to be written out in the statepoint + // file, make sure that the sourcepoint batch numbers are contained in the // statepoint list if (!source_separate) { for (const auto& b : sourcepoint_batch) { @@ -1171,8 +1239,8 @@ void read_settings_xml(pugi::xml_node root) variance_reduction::weight_windows_generators.emplace_back( std::make_unique(node_wwg)); } - // if any of the weight windows are intended to be generated otf, make sure - // they're applied + // if any of the weight windows are intended to be generated otf, make + // sure they're applied for (const auto& wwg : variance_reduction::weight_windows_generators) { if (wwg->on_the_fly_) { settings::weight_windows_on = true; diff --git a/src/simulation.cpp b/src/simulation.cpp index 05d554526..b536ae588 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -2,6 +2,7 @@ #include "openmc/bank.h" #include "openmc/capi.h" +#include "openmc/collision_track.h" #include "openmc/container_util.h" #include "openmc/eigenvalue.h" #include "openmc/error.h" @@ -9,7 +10,6 @@ #include "openmc/geometry_aux.h" #include "openmc/ifp.h" #include "openmc/material.h" -#include "openmc/mcpl_interface.h" #include "openmc/message_passing.h" #include "openmc/nuclide.h" #include "openmc/output.h" @@ -118,6 +118,7 @@ int openmc_simulation_init() // Reset global variables -- this is done before loading state point (as that // will potentially populate k_generation and entropy) simulation::current_batch = 0; + simulation::ct_current_file = 1; simulation::ssw_current_file = 1; simulation::k_generation.clear(); simulation::entropy.clear(); @@ -297,6 +298,7 @@ namespace openmc { namespace simulation { +int ct_current_file; int current_batch; int current_gen; bool initialized {false}; @@ -347,6 +349,11 @@ void allocate_banks() // Allocate surface source bank simulation::surf_source_bank.reserve(settings::ssw_max_particles); } + + if (settings::collision_track) { + // Allocate collision track bank + collision_track_reserve_bank(); + } } void initialize_batch() @@ -490,6 +497,10 @@ void finalize_batch() ++simulation::ssw_current_file; } } + // Write collision track file if requested + if (settings::collision_track) { + collision_track_flush_bank(); + } } void initialize_generation() diff --git a/src/state_point.cpp b/src/state_point.cpp index 0b0fed132..47296da3a 100644 --- a/src/state_point.cpp +++ b/src/state_point.cpp @@ -9,6 +9,7 @@ #include #include "openmc/bank.h" +#include "openmc/bank_io.h" #include "openmc/capi.h" #include "openmc/constants.h" #include "openmc/eigenvalue.h" @@ -642,91 +643,12 @@ void write_source_bank(hid_t group_id, span source_bank, { hid_t banktype = h5banktype(); - // Set total and individual process dataspace sizes for source bank - int64_t dims_size = bank_index.back(); - int64_t count_size = bank_index[mpi::rank + 1] - bank_index[mpi::rank]; - -#ifdef PHDF5 - // Set size of total dataspace for all procs and rank - hsize_t dims[] {static_cast(dims_size)}; - hid_t dspace = H5Screate_simple(1, dims, nullptr); - hid_t dset = H5Dcreate(group_id, "source_bank", banktype, dspace, H5P_DEFAULT, - H5P_DEFAULT, H5P_DEFAULT); - - // Create another data space but for each proc individually - hsize_t count[] {static_cast(count_size)}; - hid_t memspace = H5Screate_simple(1, count, nullptr); - - // Select hyperslab for this dataspace - hsize_t start[] {static_cast(bank_index[mpi::rank])}; - H5Sselect_hyperslab(dspace, H5S_SELECT_SET, start, nullptr, count, nullptr); - - // Set up the property list for parallel writing - hid_t plist = H5Pcreate(H5P_DATASET_XFER); - H5Pset_dxpl_mpio(plist, H5FD_MPIO_COLLECTIVE); - - // Write data to file in parallel - H5Dwrite(dset, banktype, memspace, dspace, plist, source_bank.data()); - - // Free resources - H5Sclose(dspace); - H5Sclose(memspace); - H5Dclose(dset); - H5Pclose(plist); - +#ifdef OPENMC_MPI + write_bank_dataset("source_bank", group_id, source_bank, bank_index, banktype, + mpi::source_site); #else - - if (mpi::master) { - // Create dataset big enough to hold all source sites - hsize_t dims[] {static_cast(dims_size)}; - hid_t dspace = H5Screate_simple(1, dims, nullptr); - hid_t dset = H5Dcreate(group_id, "source_bank", banktype, dspace, - H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); - - // Save source bank sites since the array is overwritten below -#ifdef OPENMC_MPI - vector temp_source {source_bank.begin(), source_bank.end()}; -#endif - - for (int i = 0; i < mpi::n_procs; ++i) { - // Create memory space - hsize_t count[] {static_cast(bank_index[i + 1] - bank_index[i])}; - hid_t memspace = H5Screate_simple(1, count, nullptr); - -#ifdef OPENMC_MPI - // Receive source sites from other processes - if (i > 0) - MPI_Recv(source_bank.data(), count[0], mpi::source_site, i, i, - mpi::intracomm, MPI_STATUS_IGNORE); -#endif - - // Select hyperslab for this dataspace - dspace = H5Dget_space(dset); - hsize_t start[] {static_cast(bank_index[i])}; - H5Sselect_hyperslab( - dspace, H5S_SELECT_SET, start, nullptr, count, nullptr); - - // Write data to hyperslab - H5Dwrite( - dset, banktype, memspace, dspace, H5P_DEFAULT, source_bank.data()); - - H5Sclose(memspace); - H5Sclose(dspace); - } - - // Close all ids - H5Dclose(dset); - -#ifdef OPENMC_MPI - // Restore state of source bank - std::copy(temp_source.begin(), temp_source.end(), source_bank.begin()); -#endif - } else { -#ifdef OPENMC_MPI - MPI_Send(source_bank.data(), count_size, mpi::source_site, 0, mpi::rank, - mpi::intracomm); -#endif - } + write_bank_dataset( + "source_bank", group_id, source_bank, bank_index, banktype); #endif H5Tclose(banktype); diff --git a/tests/regression_tests/collision_track/__init__.py b/tests/regression_tests/collision_track/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/collision_track/case_1_Reactions/inputs_true.dat b/tests/regression_tests/collision_track/case_1_Reactions/inputs_true.dat new file mode 100644 index 000000000..7533616c0 --- /dev/null +++ b/tests/regression_tests/collision_track/case_1_Reactions/inputs_true.dat @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + true + + + + (n,fission) 101 + 300 + + 1 + + diff --git a/tests/regression_tests/collision_track/case_1_Reactions/results_true.dat b/tests/regression_tests/collision_track/case_1_Reactions/results_true.dat new file mode 100644 index 000000000..d4d1d1e5a --- /dev/null +++ b/tests/regression_tests/collision_track/case_1_Reactions/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_2_Cell_ID/inputs_true.dat b/tests/regression_tests/collision_track/case_2_Cell_ID/inputs_true.dat new file mode 100644 index 000000000..55fb835de --- /dev/null +++ b/tests/regression_tests/collision_track/case_2_Cell_ID/inputs_true.dat @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + true + + + + 22 + 300 + + 1 + + diff --git a/tests/regression_tests/collision_track/case_2_Cell_ID/results_true.dat b/tests/regression_tests/collision_track/case_2_Cell_ID/results_true.dat new file mode 100644 index 000000000..d4d1d1e5a --- /dev/null +++ b/tests/regression_tests/collision_track/case_2_Cell_ID/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_3_Material_ID/inputs_true.dat b/tests/regression_tests/collision_track/case_3_Material_ID/inputs_true.dat new file mode 100644 index 000000000..61890414b --- /dev/null +++ b/tests/regression_tests/collision_track/case_3_Material_ID/inputs_true.dat @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + true + + + + 1 + 300 + + 1 + + diff --git a/tests/regression_tests/collision_track/case_3_Material_ID/results_true.dat b/tests/regression_tests/collision_track/case_3_Material_ID/results_true.dat new file mode 100644 index 000000000..d4d1d1e5a --- /dev/null +++ b/tests/regression_tests/collision_track/case_3_Material_ID/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_4_Nuclide_ID/inputs_true.dat b/tests/regression_tests/collision_track/case_4_Nuclide_ID/inputs_true.dat new file mode 100644 index 000000000..8960dde5c --- /dev/null +++ b/tests/regression_tests/collision_track/case_4_Nuclide_ID/inputs_true.dat @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + true + + + + O16 U235 + 300 + + 1 + + diff --git a/tests/regression_tests/collision_track/case_4_Nuclide_ID/results_true.dat b/tests/regression_tests/collision_track/case_4_Nuclide_ID/results_true.dat new file mode 100644 index 000000000..d4d1d1e5a --- /dev/null +++ b/tests/regression_tests/collision_track/case_4_Nuclide_ID/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_5_Universe_ID/inputs_true.dat b/tests/regression_tests/collision_track/case_5_Universe_ID/inputs_true.dat new file mode 100644 index 000000000..8c0d7aa8e --- /dev/null +++ b/tests/regression_tests/collision_track/case_5_Universe_ID/inputs_true.dat @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + true + + + + 22 + 77 + 300 + + 1 + + diff --git a/tests/regression_tests/collision_track/case_5_Universe_ID/results_true.dat b/tests/regression_tests/collision_track/case_5_Universe_ID/results_true.dat new file mode 100644 index 000000000..d4d1d1e5a --- /dev/null +++ b/tests/regression_tests/collision_track/case_5_Universe_ID/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/inputs_true.dat b/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/inputs_true.dat new file mode 100644 index 000000000..5173dc35c --- /dev/null +++ b/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/inputs_true.dat @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + true + + + + 550000.0 + 300 + + 1 + + diff --git a/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/results_true.dat b/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/results_true.dat new file mode 100644 index 000000000..d4d1d1e5a --- /dev/null +++ b/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_7_all_parameters_used_together/inputs_true.dat b/tests/regression_tests/collision_track/case_7_all_parameters_used_together/inputs_true.dat new file mode 100644 index 000000000..005d9feb2 --- /dev/null +++ b/tests/regression_tests/collision_track/case_7_all_parameters_used_together/inputs_true.dat @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + true + + + + 22 33 + elastic 18 (n,disappear) + 77 + 1 11 + U238 U235 H1 U234 + 100000.0 + 300 + + 1 + + diff --git a/tests/regression_tests/collision_track/case_7_all_parameters_used_together/results_true.dat b/tests/regression_tests/collision_track/case_7_all_parameters_used_together/results_true.dat new file mode 100644 index 000000000..d4d1d1e5a --- /dev/null +++ b/tests/regression_tests/collision_track/case_7_all_parameters_used_together/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_8_2threads/inputs_true.dat b/tests/regression_tests/collision_track/case_8_2threads/inputs_true.dat new file mode 100644 index 000000000..514932c1a --- /dev/null +++ b/tests/regression_tests/collision_track/case_8_2threads/inputs_true.dat @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + true + + + + 200 + + 1 + + diff --git a/tests/regression_tests/collision_track/case_8_2threads/results_true.dat b/tests/regression_tests/collision_track/case_8_2threads/results_true.dat new file mode 100644 index 000000000..d4d1d1e5a --- /dev/null +++ b/tests/regression_tests/collision_track/case_8_2threads/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/test.py b/tests/regression_tests/collision_track/test.py new file mode 100644 index 000000000..00e1e3de4 --- /dev/null +++ b/tests/regression_tests/collision_track/test.py @@ -0,0 +1,255 @@ +"""Test the 'collision_track' setting. + +Results +------- + +All results are generated using only 1 MPI process. + +All results are generated using 1 thread except for "test_consistency_low_realization_number". +This specific test verifies that when the number of realization (i.e., point being candidate +to be stored) is lower than the capacity, results are reproducible even with multiple +threads (i.e., there is no potential thread competition that would produce different +results in that case). + +All results are generated using the history-based mode except for cases e01 to e03. + +All results are visually verified using the '_visualize.py' script in the regression test folder. + +OpenMC models +------------- + +Four OpenMC models with CSG-only geometries are used to cover the transmission, vacuum, +reflective and periodic Boundary Conditions (BC): + +- model_1: cylindrical core in 2 boxes (vacuum and transmission BC), + +# Test cases for simulation parameters using CSG-only geometries +# ============================================================ +# Each test case is defined by a combination of folder name, model name, and specific parameters. +# Below is a summary of the parameters used in the test cases: +# +# - max_collisions: Maximum number of particles to track in the simulation. +# - reactions: List of MT numbers (reaction types- 2 for scattering, 18 for fission, 101 for absorbtion). +# - cell_ids: IDs of specific cells in the model. +# - mat_ids: Material IDs for filtering particles. +# - nuclides: Nuclides for filtering particles. +# - univ_ids: Universe IDs for filtering particles. +# - E_threshold: Energy threshold for filtering particles (optional). +# +# The test cases are designed to validate the behavior of the simulation under various configurations. + +*: BC stands for Boundary Conditions, T for Transmission, R for Reflective, and V for Vacuum. + +An additional case, called 'case-a01', is used to check that the results are comparable when +the number of threads is set to 2 if the number of realization is lower than the capacity. + + +*: BC stands for Boundary Conditions, T for Transmission, and V for Vacuum. + +Notes: + +- The test cases list is non-exhaustive compared to the number of possible combinations. + Test cases have been selected based on use and internal code logic. + + + +TODO: + +- Test with a lattice. + +""" + +import os + +import openmc +import openmc.lib +import pytest + +from tests.testing_harness import CollisionTrackTestHarness +from tests.regression_tests import config + + +@pytest.fixture(scope="function") +def two_threads(monkeypatch): + """Set the number of OMP threads to 2 for the test.""" + monkeypatch.setenv("OMP_NUM_THREADS", "2") + + +@pytest.fixture(scope="function") +def single_process(monkeypatch): + """Set the number of MPI process to 1 for the test.""" + monkeypatch.setitem(config, "mpi_np", "1") + + +@pytest.fixture(scope="module") +def model_1(): + """Cylindrical core contained in a first box which is contained in a larger box. + A lower universe is used to describe the interior of the first box which + contains the core and its surrounding space. + + """ + openmc.reset_auto_ids() + model = openmc.Model() + + # ============================================================================= + # Materials + # ============================================================================= + + fuel = openmc.Material(material_id=1) + fuel.add_nuclide("U234", 0.0004524) + fuel.add_nuclide("U235", 0.0506068) + fuel.add_nuclide("U238", 0.9487090) + fuel.add_nuclide("U236", 0.0002318) + fuel.add_nuclide("O16", 2.0) + fuel.set_density("g/cm3", 11.0) + + water = openmc.Material(material_id=11) + water.add_nuclide("H1", 2.0) + water.add_nuclide("O16", 1.0) + water.set_density("g/cm3", 1.0) + + # ============================================================================= + # Geometry + # ============================================================================= + + # ----------------------------------------------------------------------------- + # Cylindrical core + # ----------------------------------------------------------------------------- + + # Parameters + core_radius = 2.0 + core_height = 4.0 + + # Surfaces + core_cylinder = openmc.ZCylinder(r=core_radius) + core_lower_plane = openmc.ZPlane(-core_height / 2.0) + core_upper_plane = openmc.ZPlane(core_height / 2.0) + + # Region + core_region = -core_cylinder & +core_lower_plane & -core_upper_plane + + # Cells + core = openmc.Cell(fill=fuel, region=core_region, cell_id=22) + outside_core_region = +core_cylinder | -core_lower_plane | +core_upper_plane + outside_core = openmc.Cell( + fill=water, region=outside_core_region, cell_id=33) + + # Universe + inside_box1_universe = openmc.Universe( + cells=[core, outside_core], universe_id=77) + + # ----------------------------------------------------------------------------- + # Box 1 + # ----------------------------------------------------------------------------- + + # Parameters + box1_size = 6.0 + + # Surfaces + box1_rpp = openmc.model.RectangularParallelepiped( + -box1_size / 2.0, box1_size / 2.0, + -box1_size / 2.0, box1_size / 2.0, + -box1_size / 2.0, box1_size / 2.0, + ) + + # Cell + box1 = openmc.Cell(fill=inside_box1_universe, region=-box1_rpp, cell_id=5) + + # ----------------------------------------------------------------------------- + # Box 2 + # ----------------------------------------------------------------------------- + + # Parameters + box2_size = 8 + + # Surfaces + box2_rpp = openmc.model.RectangularParallelepiped( + -box2_size / 2.0, box2_size / 2.0, + -box2_size / 2.0, box2_size / 2.0, + -box2_size / 2.0, box2_size / 2.0, + boundary_type="vacuum" + ) + + # Cell + box2 = openmc.Cell(fill=water, region=-box2_rpp & +box1_rpp, cell_id=8) + + # Register geometry + model.geometry = openmc.Geometry([box1, box2]) + + # ============================================================================= + # Settings + # ============================================================================= + + model.settings = openmc.Settings() + model.settings.particles = 100 + model.settings.batches = 5 + model.settings.inactive = 1 + model.settings.seed = 1 + + bounds = [ + -core_radius, + -core_radius, + -core_height / 2.0, + core_radius, + core_radius, + core_height / 2.0, + ] + distribution = openmc.stats.Box(bounds[:3], bounds[3:]) + model.settings.source = openmc.IndependentSource( + space=distribution, constraints={'fissionable': True}) + + return model + + +@pytest.mark.parametrize( + "folder, model_name, parameter", + [("case_1_Reactions", "model_1", {"max_collisions": 300, "reactions": ["(n,fission)", 101]}), + ("case_2_Cell_ID", "model_1", { + "max_collisions": 300, "cell_ids": [22]}), + ("case_3_Material_ID", "model_1", { + "max_collisions": 300, "material_ids": [1]}), + ("case_4_Nuclide_ID", "model_1", { + "max_collisions": 300, "nuclides": ["O16", "U235"]}), + ("case_5_Universe_ID", "model_1", { + "max_collisions": 300, "cell_ids": [22], "universe_ids": [77]}), + ("case_6_deposited_energy_threshold", "model_1", { + "max_collisions": 300, "deposited_E_threshold": 5.5e5}), + ("case_7_all_parameters_used_together", "model_1", { + "max_collisions": 300, + "reactions": ["elastic", 18, "(n,disappear)"], + "material_ids": [1, 11], + "universe_ids": [77], + "nuclides": ["U238", "U235", "H1", "U234"], + "cell_ids": [22, 33], + "deposited_E_threshold": 1e5}) + ], +) +def test_collision_track_several_cases( + folder, model_name, parameter, request +): + # Since for these tests the actual number of collisions recorded is < max_collisions, + # we can run them with 1 or 2 threads, and in history or event mode. + model = request.getfixturevalue(model_name) + model.settings.collision_track = parameter + harness = CollisionTrackTestHarness( + "statepoint.5.h5", model=model, workdir=folder + ) + harness.main() + + +@pytest.mark.skipif(config["event"], reason="Results from history-based mode.") +def test_collision_track_2threads(model_1, two_threads, single_process): + # This test checks that the `max_collisions` setting is honored: + # no collisions beyond the specified limit should be recorded. + # + # For the result to be reproducible, the number of threads and + # the transport mode (history vs. event) must remain fixed. + assert os.environ["OMP_NUM_THREADS"] == "2" + assert config["mpi_np"] == "1" + model_1.settings.collision_track = { + "max_collisions": 200 + } + harness = CollisionTrackTestHarness( + "statepoint.5.h5", model=model_1, workdir="case_8_2threads" + ) + harness.main() diff --git a/tests/testing_harness.py b/tests/testing_harness.py index 11ced5b38..1ad91b7a8 100644 --- a/tests/testing_harness.py +++ b/tests/testing_harness.py @@ -68,7 +68,7 @@ class TestHarness: if config['mpi']: mpi_args = [config['mpiexec'], '-n', config['mpi_np']] openmc.run(openmc_exec=config['exe'], mpi_args=mpi_args, - event_based=config['event']) + event_based=config['event']) else: openmc.run(openmc_exec=config['exe'], event_based=config['event']) @@ -305,9 +305,12 @@ class PyAPITestHarness(TestHarness): else: self.execute_test() - def execute_test(self): + def execute_test(self, change_dir=False): """Build input XMLs, run OpenMC, and verify correct results.""" + base_dir = os.getcwd() if change_dir else None try: + if change_dir: + os.chdir(self.workdir) self._build_inputs() inputs = self._get_inputs() self._write_inputs(inputs) @@ -319,10 +322,15 @@ class PyAPITestHarness(TestHarness): self._compare_results() finally: self._cleanup() + if base_dir: + os.chdir(base_dir) - def update_results(self): + def update_results(self, change_dir=False): """Update results_true.dat and inputs_true.dat""" + base_dir = os.getcwd() if change_dir else None try: + if change_dir: + os.chdir(self.workdir) self._build_inputs() inputs = self._get_inputs() self._write_inputs(inputs) @@ -334,6 +342,8 @@ class PyAPITestHarness(TestHarness): self._overwrite_results() finally: self._cleanup() + if base_dir: + os.chdir(base_dir) def _build_inputs(self): """Write input XML files.""" @@ -371,7 +381,8 @@ class PyAPITestHarness(TestHarness): """Delete XMLs, statepoints, tally, and test files.""" super()._cleanup() output = ['materials.xml', 'geometry.xml', 'settings.xml', - 'tallies.xml', 'plots.xml', 'inputs_test.dat', 'model.xml'] + 'tallies.xml', 'plots.xml', 'inputs_test.dat', 'model.xml', + 'collision_track.h5', 'collision_track.mcpl'] for f in output: if os.path.exists(f): os.remove(f) @@ -389,6 +400,7 @@ class TolerantPyAPITestHarness(PyAPITestHarness): due to single precision usage (e.g., as in the random ray solver). """ + def _are_files_equal(self, actual_path, expected_path, tolerance): def isfloat(value): try: @@ -428,7 +440,8 @@ class TolerantPyAPITestHarness(PyAPITestHarness): def _compare_results(self): """Make sure the current results agree with the reference.""" - compare = self._are_files_equal('results_test.dat', 'results_true.dat', 1e-6) + compare = self._are_files_equal( + 'results_test.dat', 'results_true.dat', 1e-6) if not compare: expected = open('results_true.dat').readlines() actual = open('results_test.dat').readlines() @@ -476,6 +489,7 @@ class WeightWindowPyAPITestHarness(PyAPITestHarness): class PlotTestHarness(TestHarness): """Specialized TestHarness for running OpenMC plotting tests.""" + def __init__(self, plot_names, voxel_convert_checks=[]): super().__init__(None) self._plot_names = plot_names @@ -523,3 +537,105 @@ class PlotTestHarness(TestHarness): outstr = sha512.hexdigest() return outstr + + +class CollisionTrackTestHarness(PyAPITestHarness): + def __init__(self, statepoint_name, model=None, inputs_true=None, workdir=None): + super().__init__(statepoint_name, model, inputs_true) + self.workdir = workdir + + def _test_output_created(self): + """Make sure collision_track.h5 has also been created.""" + super()._test_output_created() + if self._model.settings.collision_track: + assert os.path.exists( + "collision_track.h5" + ), "collision_track file has not been created." + + def _compare_output(self): + """Compare collision_track.h5 files.""" + if self._model.settings.collision_track: + collision_track_true = self._return_collision_track_data( + "collision_track_true.h5") + collision_track_test = self._return_collision_track_data( + "collision_track.h5") + np.testing.assert_allclose( + collision_track_true, collision_track_test, rtol=1e-07) + + def main(self): + """Accept commandline arguments and either run or update tests.""" + if config["build_inputs"]: + self.build_inputs() + elif config["update"]: + self.update_results(change_dir=True) + else: + self.execute_test(change_dir=True) + + def build_inputs(self): + """Build inputs.""" + base_dir = os.getcwd() + try: + os.chdir(self.workdir) + self._build_inputs() + finally: + os.chdir(base_dir) + + def _overwrite_results(self): + """Also add the 'collision_track.h5' file during overwriting.""" + super()._overwrite_results() + if os.path.exists("collision_track.h5"): + shutil.copyfile("collision_track.h5", "collision_track_true.h5") + + @staticmethod + def _return_collision_track_data(filepath): + """ + Read a collision_track file and return a sorted array composed + of flatten arrays of collision information. + + Parameters + ---------- + filepath : str + Path to the collision_track file + + Returns + ------- + data : np.array + Sorted array composed of flatten arrays of collision_track data for + each collision information + """ + data = [] + keys = [] + + # Read source file + source = openmc.read_collision_track_file(filepath) + for src in source: + r = src['r'] + u = src['u'] + e = src['E'] + de = src['dE'] + time = src['time'] + wgt = src['wgt'] + delayed_group = src['delayed_group'] + cell_id = src['cell_id'] + nuclide_id = src['nuclide_id'] + material_id = src['material_id'] + universe_id = src['universe_id'] + n_collision = src['n_collision'] + event_mt = src['event_mt'] + key = ( + f"{r[0]:.10e} {r[1]:.10e} {r[2]:.10e} {u[0]:.10e} {u[1]:.10e} {u[2]:.10e}" + f"{e:.10e} {de:.10e} {time:.10e} {wgt:.10e} {event_mt} {delayed_group} {cell_id}" + f"{nuclide_id} {material_id} {universe_id} {n_collision} " + ) + keys.append(key) + values = [*r, *u, e, de, time, wgt, event_mt, + delayed_group, cell_id, nuclide_id, material_id, + universe_id, n_collision] + assert len(values) == 17 + data.append(values) + + data = np.array(data) + keys = np.array(keys) + sorted_idx = np.argsort(keys, kind='stable') + + return data[sorted_idx] diff --git a/tests/unit_tests/test_collision_track.py b/tests/unit_tests/test_collision_track.py new file mode 100644 index 000000000..9bc6a8c15 --- /dev/null +++ b/tests/unit_tests/test_collision_track.py @@ -0,0 +1,127 @@ +"""Test the 'collision_track' setting used to store particle information +during specified collision conditions in a file for a given simulation.""" + +import openmc +import pytest +import h5py +import numpy as np + +from tests.testing_harness import CollisionTrackTestHarness as ctt + + +@pytest.fixture(scope="module") +def geometry(): + """Simple hydrogen sphere geometry""" + openmc.reset_auto_ids() + material = openmc.Material(name="H1") + material.add_element("H", 1.0) + sphere = openmc.Sphere(r=1.0, boundary_type="vacuum") + cell = openmc.Cell(region=-sphere, fill=material) + return openmc.Geometry([cell]) + + +@pytest.mark.parametrize( + "parameter", + [ + {"max_collisions": 200}, + {"max_collisions": 200, "reactions": ["(n,disappear)"]}, + {"max_collisions": 200, "cell_ids": [1]}, + {"max_collisions": 200, "material_ids": [1]}, + {"max_collisions": 200, "universe_ids": [1]}, + {"max_collisions": 200, "nuclides": ["H1"]}, + {"max_collisions": 200, "deposited_E_threshold": 200000.0}, + {"max_collisions": 200, "mcpl": True} + + ], +) +def test_xml_serialization(parameter, run_in_tmpdir): + """Check that the different use cases can be written and read in XML.""" + settings = openmc.Settings() + settings.collision_track = parameter + settings.export_to_xml() + + read_settings = openmc.Settings.from_xml() + assert read_settings.collision_track == parameter + + +@pytest.fixture(scope="module") +def model(): + """Simple hydrogen sphere divided in two hemispheres + by a z-plane to form 2 cells.""" + openmc.reset_auto_ids() + model = openmc.Model() + + # Material + material = openmc.Material(name="H1") + material.add_element("H", 1.0) + + # Geometry + radius = 1.0 + sphere = openmc.Sphere(r=radius, boundary_type="reflective") + plane = openmc.ZPlane(0.0) + cell_1 = openmc.Cell(region=-sphere & -plane, fill=material, cell_id=1) + cell_2 = openmc.Cell(region=-sphere & +plane, fill=material, cell_id=2) + root = openmc.Universe(cells=[cell_1, cell_2]) + model.geometry = openmc.Geometry(root) + + # Settings + model.settings = openmc.Settings() + model.settings.run_mode = "fixed source" + model.settings.particles = 1 + model.settings.batches = 1 + model.settings.seed = 2 + + bounds = [-radius, -radius, -radius, radius, radius, radius] + distribution = openmc.stats.Box(bounds[:3], bounds[3:]) + model.settings.source = openmc.IndependentSource(space=distribution) + + return model + + +def test_particle_location(run_in_tmpdir, model): + """Test the location of particles with respected to the "cell_ids" + and the location x, y, z of the particle itself. the upper sphere will + have positive z component and the bottom sphere a negative z compnent. + + """ + model.settings.collision_track = { + "max_collisions": 200, + "reactions": ["elastic"], + "cell_ids": [1, 2] + } + model.run() + + with h5py.File("collision_track.h5", "r") as f: + source = f["collision_track_bank"] + + assert len(source) == 60 + + # We want to verify that the collisions happenening are in the right cells + # and the position of the particle is either positive or negative relative + # to the z plane. In this case, we track the position of the particle + # relative to the cell_id already set. + for point in source: + if point['cell_id'] == 1: + assert point['r'][2] < 0.0 # z component negative + elif point['cell_id'] == 2: + assert point["r"][2] > 0.0 # z component positive + else: + assert False + + +def test_format_similarity(run_in_tmpdir, model): + model.settings.collision_track = {"max_collisions": 200, "reactions": ['elastic'], + "cell_ids": [1, 2], "mcpl": False} + model.run() + data_h5 = ctt._return_collision_track_data('collision_track.h5') + + model.settings.collision_track["mcpl"] = True + model.run() + data_mcpl = ctt._return_collision_track_data('collision_track.mcpl') + + assert len(data_h5) == 60 + assert len(data_mcpl) == 60 + + np.testing.assert_allclose(data_h5, data_mcpl, rtol=1e-05) + # tolerance not that low due to the strings that is saved in MCPL, + # not enough precision! From 8d618716b55d92e892314d966ac75ffe5b19b7d7 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 13 Nov 2025 17:14:39 -0600 Subject: [PATCH 452/671] Avoid multiprocessing Pool when running depletion tests with MPI (#3633) --- tests/conftest.py | 21 +++++++++++++++++-- .../deplete_no_transport/test.py | 4 ++++ .../deplete_with_transport/test.py | 2 ++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index fa6718502..71dd5ebf5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -29,12 +29,29 @@ def run_in_tmpdir(tmpdir): yield finally: orig.chdir() - + @pytest.fixture(scope="module") def endf_data(): - return os.environ['OPENMC_ENDF_DATA'] + return os.environ['OPENMC_ENDF_DATA'] @pytest.fixture(scope='session', autouse=True) def resolve_paths(): with openmc.config.patch('resolve_paths', False): yield + + +@pytest.fixture(scope='session', autouse=True) +def disable_depletion_multiprocessing_under_mpi(): + """Fork-based depletion multiprocessing may deadlock if MPI is active.""" + if not regression_config['mpi']: + yield + return + + from openmc.deplete import pool + + original_setting = pool.USE_MULTIPROCESSING + pool.USE_MULTIPROCESSING = False + try: + yield + finally: + pool.USE_MULTIPROCESSING = original_setting diff --git a/tests/regression_tests/deplete_no_transport/test.py b/tests/regression_tests/deplete_no_transport/test.py index 63ae584e1..5550ad048 100644 --- a/tests/regression_tests/deplete_no_transport/test.py +++ b/tests/regression_tests/deplete_no_transport/test.py @@ -76,6 +76,8 @@ def test_against_self(run_in_tmpdir, dt = [360] # single step # Perform simulation using the predictor algorithm + if config['mpi'] and multiproc: + pytest.skip("Multiprocessing depletion is disabled when MPI is enabled.") openmc.deplete.pool.USE_MULTIPROCESSING = multiproc openmc.deplete.PredictorIntegrator(op, dt, @@ -135,6 +137,8 @@ def test_against_coupled(run_in_tmpdir, dt = [dt] # single step # Perform simulation using the predictor algorithm + if config['mpi'] and multiproc: + pytest.skip("Multiprocessing depletion is disabled when MPI is enabled.") openmc.deplete.pool.USE_MULTIPROCESSING = multiproc openmc.deplete.PredictorIntegrator( op, dt, power=174, timestep_units=time_units).integrate() diff --git a/tests/regression_tests/deplete_with_transport/test.py b/tests/regression_tests/deplete_with_transport/test.py index 0f7ebf00f..a2be22c56 100644 --- a/tests/regression_tests/deplete_with_transport/test.py +++ b/tests/regression_tests/deplete_with_transport/test.py @@ -65,6 +65,8 @@ def test_full(run_in_tmpdir, problem, multiproc): power = 2.337e15*4*JOULE_PER_EV*1e6 # MeV/second cm from CASMO # Perform simulation using the predictor algorithm + if config['mpi'] and multiproc: + pytest.skip("Multiprocessing depletion is disabled when MPI is enabled.") openmc.deplete.pool.USE_MULTIPROCESSING = multiproc openmc.deplete.PredictorIntegrator(op, dt, power).integrate() From 7815d3a680de21362e7fec64c871ecbab1150421 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Thu, 13 Nov 2025 23:27:17 -0600 Subject: [PATCH 453/671] Fix typo in DAGMC lost particle test (#3634) --- tests/unit_tests/dagmc/test_lost_particles.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit_tests/dagmc/test_lost_particles.py b/tests/unit_tests/dagmc/test_lost_particles.py index 502bd795e..3a4166009 100644 --- a/tests/unit_tests/dagmc/test_lost_particles.py +++ b/tests/unit_tests/dagmc/test_lost_particles.py @@ -70,12 +70,12 @@ def test_lost_particles(run_in_tmpdir, broken_dagmc_model): openmc.run() # run this again, but with the dagmc universe as the root unvierse + # to ensure that lost particles are still caught in this case for univ in broken_dagmc_model.geometry.get_all_universes().values(): if isinstance(univ, openmc.DAGMCUniverse): - broken_dagmc_model.geometry.root_unvierse = univ + broken_dagmc_model.geometry.root_universe = univ break broken_dagmc_model.export_to_xml() with pytest.raises(RuntimeError, match='Maximum number of lost particles has been reached.'): openmc.run() - From 5c63e0df21a25e2c9a25f8be6cfd23a09d6007b3 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 18 Nov 2025 17:57:12 -0600 Subject: [PATCH 454/671] Fix a few warnings, rename add_to_tallies_file (#3639) --- docs/source/usersguide/random_ray.rst | 2 +- openmc/deplete/chain.py | 13 +++--- openmc/mgxs/library.py | 18 +++++--- openmc/mgxs/mgxs.py | 4 +- openmc/model/model.py | 6 +-- openmc/tallies.py | 45 +++++++------------ .../mgxs_library_ce_to_mg/test.py | 2 +- .../mgxs_library_ce_to_mg_nuclides/test.py | 2 +- .../mgxs_library_condense/test.py | 2 +- .../mgxs_library_correction/test.py | 2 +- .../mgxs_library_distribcell/test.py | 2 +- .../mgxs_library_hdf5/test.py | 2 +- .../mgxs_library_histogram/test.py | 2 +- .../mgxs_library_mesh/test.py | 2 +- .../mgxs_library_no_nuclides/test.py | 2 +- .../mgxs_library_nuclides/test.py | 2 +- .../mgxs_library_specific_nuclides/test.py | 2 +- 17 files changed, 54 insertions(+), 56 deletions(-) diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index 138ae910c..d5d752a83 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -765,7 +765,7 @@ energy decomposition:: # Create a "tallies.xml" file for the MGXS Library tallies = openmc.Tallies() - mgxs_lib.add_to_tallies_file(tallies, merge=True) + mgxs_lib.add_to_tallies(tallies, merge=True) # Export tallies.export_to_xml() diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index f34416d56..873d7ca89 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -630,7 +630,7 @@ class Chain: n = len(self) - # we accumulate indices and value entries for everything and create the matrix + # we accumulate indices and value entries for everything and create the matrix # in one step at the end to avoid expensive index checks scipy otherwise does. rows, cols, vals = [], [], [] def setval(i, j, val): @@ -716,14 +716,17 @@ class Chain: return sp.csc_matrix((vals, (rows, cols)), shape=(n, n)) def add_redox_term(self, matrix, buffer, oxidation_states): - """Adds a redox term to the depletion matrix from data contained in + r"""Adds a redox term to the depletion matrix from data contained in the matrix itself and a few user-inputs. The redox term to add to the buffer nuclide :math:`N_j` can be written - as: :math:`\frac{dN_j(t)}{dt} = - \cdots - \frac{1}{OS_j}\sum_i N_i a_{ij} \cdot OS_i ` + as: - where :math:`OS` is the oxidation states vector and `a_{ij}` the + .. math:: + \frac{dN_j(t)}{dt} = \cdots - \frac{1}{OS_j}\sum_i N_i a_{ij} + \cdot OS_i + + where :math:`OS` is the oxidation states vector and :math:`a_{ij}` the corresponding term in the Bateman matrix. Parameters diff --git a/openmc/mgxs/library.py b/openmc/mgxs/library.py index f782cbe9e..b476de902 100644 --- a/openmc/mgxs/library.py +++ b/openmc/mgxs/library.py @@ -556,14 +556,14 @@ class Library: self.all_mgxs[domain.id][mgxs_type] = mgxs - def add_to_tallies_file(self, tallies_file, merge=True): - """Add all tallies from all MGXS objects to a tallies file. + def add_to_tallies(self, tallies, merge=True): + """Add tallies from all MGXS objects to a tallies object. NOTE: This assumes that :meth:`Library.build_library` has been called Parameters ---------- - tallies_file : openmc.Tallies + tallies : openmc.Tallies A Tallies collection to add each MGXS' tallies to generate a 'tallies.xml' input file for OpenMC merge : bool @@ -572,7 +572,7 @@ class Library: """ - cv.check_type('tallies_file', tallies_file, openmc.Tallies) + cv.check_type('tallies', tallies, openmc.Tallies) # Add tallies from each MGXS for each domain and mgxs type for domain in self.domains: @@ -587,7 +587,15 @@ class Library: = list(range(1, self.num_delayed_groups + 1)) for tally in mgxs.tallies.values(): - tallies_file.append(tally, merge=merge) + tallies.append(tally, merge=merge) + + def add_to_tallies_file(self, tallies_file, merge=True): + warn( + "The Library.add_to_tallies_file(...) method has been renamed to" + "add_to_tallies(...) and will be removed in a future version of " + "OpenMC.", FutureWarning + ) + self.add_to_tallies(tallies_file, merge=merge) def load_from_statepoint(self, statepoint): """Extracts tallies in an OpenMC StatePoint with the data needed to diff --git a/openmc/mgxs/mgxs.py b/openmc/mgxs/mgxs.py index 533ab0ad3..f621db092 100644 --- a/openmc/mgxs/mgxs.py +++ b/openmc/mgxs/mgxs.py @@ -2127,8 +2127,8 @@ class MGXS: df['std. dev.'] /= np.tile(densities, tile_factor) # Replace NaNs by zeros (happens if nuclide density is zero) - df['mean'].replace(np.nan, 0.0, inplace=True) - df['std. dev.'].replace(np.nan, 0.0, inplace=True) + df['mean'] = df['mean'].replace(np.nan, 0.0) + df['std. dev.'] = df['std. dev.'].replace(np.nan, 0.0) # Sort the dataframe by domain type id (e.g., distribcell id) and # energy groups such that data is from fast to thermal diff --git a/openmc/model/model.py b/openmc/model/model.py index 64294c23c..10e5cbfc7 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1791,7 +1791,7 @@ class Model: mgxs_lib.build_library() # Create a "tallies.xml" file for the MGXS Library - mgxs_lib.add_to_tallies_file(model.tallies, merge=True) + mgxs_lib.add_to_tallies(model.tallies, merge=True) # Run statepoint_filename = model.run(cwd=directory) @@ -1980,7 +1980,7 @@ class Model: mgxs_lib.build_library() # Create a "tallies.xml" file for the MGXS Library - mgxs_lib.add_to_tallies_file(model.tallies, merge=True) + mgxs_lib.add_to_tallies(model.tallies, merge=True) # Run statepoint_filename = model.run(cwd=directory) @@ -2075,7 +2075,7 @@ class Model: mgxs_lib.build_library() # Create a "tallies.xml" file for the MGXS Library - mgxs_lib.add_to_tallies_file(model.tallies, merge=True) + mgxs_lib.add_to_tallies(model.tallies, merge=True) # Run statepoint_filename = model.run(cwd=directory) diff --git a/openmc/tallies.py b/openmc/tallies.py index 25ec29a58..add356579 100644 --- a/openmc/tallies.py +++ b/openmc/tallies.py @@ -738,13 +738,9 @@ class Tally(IDManagerMixin): Notes ----- - This test is based on D'Agostino and Pearson's test [1]_. The test - requires at least 8 realizations to produce valid results. - - References - ---------- - .. [1] D'Agostino, R. B. (1971), "An omnibus test of normality for - moderate and large sample size", Biometrika, 58, 341-348 + This test is based on `D'Agostino and Pearson's test + `_. The test requires at least + 8 realizations to produce valid results. """ n = self.num_realizations @@ -788,8 +784,8 @@ class Tally(IDManagerMixin): Parameters ---------- alternative : {'two-sided', 'less', 'greater'}, optional - Defines the alternative hypothesis. Default is 'two-sided'. - The following options are available: + Defines the alternative hypothesis. Default is 'two-sided'. The + following options are available: * 'two-sided': the kurtosis of the distribution is different from that of the normal distribution @@ -813,14 +809,9 @@ class Tally(IDManagerMixin): Notes ----- - This test is based on D'Agostino and Pearson's test [1]_. The test - is typically recommended for at least 20 realizations to produce - valid results. - - References - ---------- - .. [1] D'Agostino, R. B. (1971), "An omnibus test of normality for - moderate and large sample size", Biometrika, 58, 341-348 + This test is based on `D'Agostino and Pearson's test + `_. The test is typically + recommended for at least 20 realizations to produce valid results. """ n = self.num_realizations @@ -855,9 +846,9 @@ class Tally(IDManagerMixin): def normaltest(self, alternative: str = "two-sided"): """Perform D'Agostino and Pearson's omnibus test for normality. - This method tests the null hypothesis that a sample comes from a - normal distribution. It combines skewness and kurtosis to produce an - omnibus test of normality. + This method tests the null hypothesis that a sample comes from a normal + distribution. It combines skewness and kurtosis to produce an omnibus + test of normality. Parameters ---------- @@ -886,23 +877,19 @@ class Tally(IDManagerMixin): Notes ----- This test combines a test for skewness and a test for kurtosis to - produce an omnibus test [1]_. The test statistic is: + produce an `omnibus test `_. + The test statistic is: .. math:: K^2 = Z_1^2 + Z_2^2 - where :math:`Z_1` is the z-score from the skewness test and - :math:`Z_2` is the z-score from the kurtosis test. This statistic - follows a chi-square distribution with 2 degrees of freedom. + where :math:`Z_1` is the z-score from the skewness test and :math:`Z_2` + is the z-score from the kurtosis test. This statistic follows a + chi-square distribution with 2 degrees of freedom. The test requires at least 20 realizations to produce valid results. - References - ---------- - .. [1] D'Agostino, R. B. and Pearson, E. S. (1973), "Tests for - departure from normality", Biometrika, 60, 613-622 - """ n = self.num_realizations if n < 20: diff --git a/tests/regression_tests/mgxs_library_ce_to_mg/test.py b/tests/regression_tests/mgxs_library_ce_to_mg/test.py index 48a715997..075167f58 100644 --- a/tests/regression_tests/mgxs_library_ce_to_mg/test.py +++ b/tests/regression_tests/mgxs_library_ce_to_mg/test.py @@ -28,7 +28,7 @@ class MGXSTestHarness(PyAPITestHarness): self.mgxs_lib.build_library() # Initialize a tallies file - self.mgxs_lib.add_to_tallies_file(self._model.tallies, merge=False) + self.mgxs_lib.add_to_tallies(self._model.tallies, merge=False) def _run_openmc(self): # Initial run diff --git a/tests/regression_tests/mgxs_library_ce_to_mg_nuclides/test.py b/tests/regression_tests/mgxs_library_ce_to_mg_nuclides/test.py index a77ad2429..489105f8f 100644 --- a/tests/regression_tests/mgxs_library_ce_to_mg_nuclides/test.py +++ b/tests/regression_tests/mgxs_library_ce_to_mg_nuclides/test.py @@ -28,7 +28,7 @@ class MGXSTestHarness(PyAPITestHarness): self.mgxs_lib.build_library() # Initialize a tallies file - self.mgxs_lib.add_to_tallies_file(self._model.tallies, merge=False) + self.mgxs_lib.add_to_tallies(self._model.tallies, merge=False) def _run_openmc(self): # Initial run diff --git a/tests/regression_tests/mgxs_library_condense/test.py b/tests/regression_tests/mgxs_library_condense/test.py index a7e60617f..bbc4c11bf 100644 --- a/tests/regression_tests/mgxs_library_condense/test.py +++ b/tests/regression_tests/mgxs_library_condense/test.py @@ -36,7 +36,7 @@ class MGXSTestHarness(PyAPITestHarness): self.mgxs_lib.build_library() # Add tallies - self.mgxs_lib.add_to_tallies_file(self._model.tallies, merge=False) + self.mgxs_lib.add_to_tallies(self._model.tallies, merge=False) def _get_results(self, hash_output=False): """Digest info in the statepoint and return as a string.""" diff --git a/tests/regression_tests/mgxs_library_correction/test.py b/tests/regression_tests/mgxs_library_correction/test.py index 05eedfef8..64e638e44 100644 --- a/tests/regression_tests/mgxs_library_correction/test.py +++ b/tests/regression_tests/mgxs_library_correction/test.py @@ -29,7 +29,7 @@ class MGXSTestHarness(PyAPITestHarness): self.mgxs_lib.build_library() # Add tallies - self.mgxs_lib.add_to_tallies_file(self._model.tallies, merge=False) + self.mgxs_lib.add_to_tallies(self._model.tallies, merge=False) def _get_results(self, hash_output=False): """Digest info in the statepoint and return as a string.""" diff --git a/tests/regression_tests/mgxs_library_distribcell/test.py b/tests/regression_tests/mgxs_library_distribcell/test.py index fd6c8e938..464b309c0 100644 --- a/tests/regression_tests/mgxs_library_distribcell/test.py +++ b/tests/regression_tests/mgxs_library_distribcell/test.py @@ -36,7 +36,7 @@ class MGXSTestHarness(PyAPITestHarness): self.mgxs_lib.build_library() # Add tallies - self.mgxs_lib.add_to_tallies_file(self._model.tallies, merge=False) + self.mgxs_lib.add_to_tallies(self._model.tallies, merge=False) self._model.tallies.export_to_xml() def _get_results(self, hash_output=False): diff --git a/tests/regression_tests/mgxs_library_hdf5/test.py b/tests/regression_tests/mgxs_library_hdf5/test.py index 06625c25f..4fb4bf093 100644 --- a/tests/regression_tests/mgxs_library_hdf5/test.py +++ b/tests/regression_tests/mgxs_library_hdf5/test.py @@ -40,7 +40,7 @@ class MGXSTestHarness(PyAPITestHarness): self.mgxs_lib.build_library() # Add tallies - self.mgxs_lib.add_to_tallies_file(self._model.tallies, merge=False) + self.mgxs_lib.add_to_tallies(self._model.tallies, merge=False) def _get_results(self, hash_output=False): """Digest info in the statepoint and return as a string.""" diff --git a/tests/regression_tests/mgxs_library_histogram/test.py b/tests/regression_tests/mgxs_library_histogram/test.py index b9905910a..42fc1957a 100644 --- a/tests/regression_tests/mgxs_library_histogram/test.py +++ b/tests/regression_tests/mgxs_library_histogram/test.py @@ -30,7 +30,7 @@ class MGXSTestHarness(PyAPITestHarness): self.mgxs_lib.build_library() # Add tallies - self.mgxs_lib.add_to_tallies_file(self._model.tallies, merge=False) + self.mgxs_lib.add_to_tallies(self._model.tallies, merge=False) def _get_results(self, hash_output=False): """Digest info in the statepoint and return as a string.""" diff --git a/tests/regression_tests/mgxs_library_mesh/test.py b/tests/regression_tests/mgxs_library_mesh/test.py index 89c68a75a..c1a5980b5 100644 --- a/tests/regression_tests/mgxs_library_mesh/test.py +++ b/tests/regression_tests/mgxs_library_mesh/test.py @@ -57,7 +57,7 @@ def model(): model.mgxs_lib.build_library() # Add tallies - model.mgxs_lib.add_to_tallies_file(model.tallies, merge=False) + model.mgxs_lib.add_to_tallies(model.tallies, merge=False) return model diff --git a/tests/regression_tests/mgxs_library_no_nuclides/test.py b/tests/regression_tests/mgxs_library_no_nuclides/test.py index af14a5dc8..a02086af3 100644 --- a/tests/regression_tests/mgxs_library_no_nuclides/test.py +++ b/tests/regression_tests/mgxs_library_no_nuclides/test.py @@ -39,7 +39,7 @@ class MGXSTestHarness(PyAPITestHarness): self.mgxs_lib.build_library() # Add tallies - self.mgxs_lib.add_to_tallies_file(self._model.tallies, merge=False) + self.mgxs_lib.add_to_tallies(self._model.tallies, merge=False) def _get_results(self, hash_output=False): """Digest info in the statepoint and return as a string.""" diff --git a/tests/regression_tests/mgxs_library_nuclides/test.py b/tests/regression_tests/mgxs_library_nuclides/test.py index 8a7673565..a10070358 100644 --- a/tests/regression_tests/mgxs_library_nuclides/test.py +++ b/tests/regression_tests/mgxs_library_nuclides/test.py @@ -36,7 +36,7 @@ class MGXSTestHarness(PyAPITestHarness): self.mgxs_lib.build_library() # Add tallies - self.mgxs_lib.add_to_tallies_file(self._model.tallies, merge=False) + self.mgxs_lib.add_to_tallies(self._model.tallies, merge=False) def _get_results(self, hash_output=True): """Digest info in the statepoint and return as a string.""" diff --git a/tests/regression_tests/mgxs_library_specific_nuclides/test.py b/tests/regression_tests/mgxs_library_specific_nuclides/test.py index 61910e539..0ccbb83bd 100644 --- a/tests/regression_tests/mgxs_library_specific_nuclides/test.py +++ b/tests/regression_tests/mgxs_library_specific_nuclides/test.py @@ -37,7 +37,7 @@ class MGXSTestHarness(PyAPITestHarness): self.mgxs_lib.build_library() # Add tallies - self.mgxs_lib.add_to_tallies_file(self._model.tallies, merge=True) + self.mgxs_lib.add_to_tallies(self._model.tallies, merge=True) def _get_results(self, hash_output=True): """Digest info in the statepoint and return as a string.""" From 028f4404485407b01a31c44f30249852601a29c8 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 19 Nov 2025 11:19:27 -0600 Subject: [PATCH 455/671] Support MPI parallelism in R2SManager (#3632) --- openmc/deplete/microxs.py | 31 +++++++++++++------------ openmc/deplete/r2s.py | 48 ++++++++++++++++++++++++--------------- openmc/lib/core.py | 45 ++++++++++++++++++++++++++++-------- 3 files changed, 82 insertions(+), 42 deletions(-) diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index d7624955e..f4bdc6795 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -170,13 +170,14 @@ def get_microxs_and_flux( # Reinitialize with tallies openmc.lib.init(intracomm=comm) - # create temporary run with TemporaryDirectory() as temp_dir: - if run_kwargs is None: - run_kwargs = {} - else: - run_kwargs = dict(run_kwargs) - run_kwargs.setdefault('cwd', temp_dir) + # Indicate to run in temporary directory unless being executed through + # openmc.lib, in which case we don't need to specify the cwd + run_kwargs = dict(run_kwargs) if run_kwargs else {} + if not openmc.lib.is_initialized: + run_kwargs.setdefault('cwd', temp_dir) + + # Run transport simulation statepoint_path = model.run(**run_kwargs) if comm.rank == 0: @@ -189,15 +190,18 @@ def get_microxs_and_flux( if path_input is not None: model.export_to_model_xml(path_input) - with StatePoint(statepoint_path) as sp: - if reaction_rate_mode == 'direct': - rr_tally = sp.tallies[rr_tally.id] - rr_tally._read_results() - flux_tally = sp.tallies[flux_tally.id] - flux_tally._read_results() + # Broadcast updated statepoint path to all ranks + statepoint_path = comm.bcast(statepoint_path) + + # Read in tally results (on all ranks) + with StatePoint(statepoint_path) as sp: + if reaction_rate_mode == 'direct': + rr_tally = sp.tallies[rr_tally.id] + rr_tally._read_results() + flux_tally = sp.tallies[flux_tally.id] + flux_tally._read_results() # Get flux values and make energy groups last dimension - flux_tally = comm.bcast(flux_tally) flux = flux_tally.get_reshaped_data() # (domains, groups, 1, 1) flux = np.moveaxis(flux, 1, -1) # (domains, 1, 1, groups) @@ -206,7 +210,6 @@ def get_microxs_and_flux( if reaction_rate_mode == 'direct': # Get reaction rates - rr_tally = comm.bcast(rr_tally) reaction_rates = rr_tally.get_reshaped_data() # (domains, groups, nuclides, reactions) # Make energy groups last dimension diff --git a/openmc/deplete/r2s.py b/openmc/deplete/r2s.py index 7b5deddc2..7f3e94edd 100644 --- a/openmc/deplete/r2s.py +++ b/openmc/deplete/r2s.py @@ -11,6 +11,9 @@ from . import IndependentOperator, PredictorIntegrator from .microxs import get_microxs_and_flux, write_microxs_hdf5, read_microxs_hdf5 from .results import Results from ..checkvalue import PathLike +from ..mpi import comm +from openmc.lib import TemporarySession +from openmc.utility_funcs import change_directory def get_activation_materials( @@ -199,8 +202,10 @@ class R2SManager: """ if output_dir is None: + # Create timestamped output directory and broadcast to all ranks for + # consistency (different ranks may have slightly different times) stamp = datetime.now().strftime('%Y-%m-%dT%H-%M-%S') - output_dir = Path(f'r2s_{stamp}') + output_dir = Path(comm.bcast(f'r2s_{stamp}')) # Set run_kwargs for the neutron transport step if micro_kwargs is None: @@ -257,18 +262,19 @@ class R2SManager: """ - output_dir = Path(output_dir) + output_dir = Path(output_dir).resolve() output_dir.mkdir(parents=True, exist_ok=True) if self.method == 'mesh-based': # Compute material volume fractions on the mesh if mat_vol_kwargs is None: mat_vol_kwargs = {} - self.results['mesh_material_volumes'] = mmv = \ - self.domains.material_volumes(self.neutron_model, **mat_vol_kwargs) + self.results['mesh_material_volumes'] = mmv = comm.bcast( + self.domains.material_volumes(self.neutron_model, **mat_vol_kwargs)) # Save results to file - mmv.save(output_dir / 'mesh_material_volumes.npz') + if comm.rank == 0: + mmv.save(output_dir / 'mesh_material_volumes.npz') # Create mesh-material filter based on what combos were found domains = openmc.MeshMaterialFilter.from_volumes(self.domains, mmv) @@ -299,13 +305,16 @@ class R2SManager: micro_kwargs.setdefault('path_statepoint', output_dir / 'statepoint.h5') micro_kwargs.setdefault('path_input', output_dir / 'model.xml') - # Run neutron transport and get fluxes and micros - self.results['fluxes'], self.results['micros'] = get_microxs_and_flux( - self.neutron_model, domains, **micro_kwargs) + # Run neutron transport and get fluxes and micros. Run via openmc.lib to + # maintain a consistent parallelism strategy with the activation step. + with TemporarySession(): + self.results['fluxes'], self.results['micros'] = get_microxs_and_flux( + self.neutron_model, domains, **micro_kwargs) # Save flux and micros to file - np.save(output_dir / 'fluxes.npy', self.results['fluxes']) - write_microxs_hdf5(self.results['micros'], output_dir / 'micros.h5') + if comm.rank == 0: + np.save(output_dir / 'fluxes.npy', self.results['fluxes']) + write_microxs_hdf5(self.results['micros'], output_dir / 'micros.h5') def step2_activation( self, @@ -457,15 +466,17 @@ class R2SManager: # photon model if it is different from the neutron model to account for # potential material changes if self.method == 'mesh-based' and different_photon_model: - self.results['mesh_material_volumes_photon'] = photon_mmv = \ - self.domains.material_volumes(self.photon_model, **mat_vol_kwargs) + self.results['mesh_material_volumes_photon'] = photon_mmv = comm.bcast( + self.domains.material_volumes(self.photon_model, **mat_vol_kwargs)) # Save photon MMV results to file - photon_mmv.save(output_dir / 'mesh_material_volumes.npz') + if comm.rank == 0: + photon_mmv.save(output_dir / 'mesh_material_volumes.npz') - tally_ids = [tally.id for tally in self.photon_model.tallies] - with open(output_dir / 'tally_ids.json', 'w') as f: - json.dump(tally_ids, f) + if comm.rank == 0: + tally_ids = [tally.id for tally in self.photon_model.tallies] + with open(output_dir / 'tally_ids.json', 'w') as f: + json.dump(tally_ids, f) self.results['photon_tallies'] = {} @@ -514,8 +525,9 @@ class R2SManager: time_index = len(self.results['depletion_results']) + time_index # Run photon transport calculation - run_kwargs['cwd'] = Path(output_dir) / f'time_{time_index}' - statepoint_path = self.photon_model.run(**run_kwargs) + photon_dir = Path(output_dir) / f'time_{time_index}' + with TemporarySession(self.photon_model, cwd=photon_dir): + statepoint_path = self.photon_model.run(**run_kwargs) # Store tally results with openmc.StatePoint(statepoint_path) as sp: diff --git a/openmc/lib/core.py b/openmc/lib/core.py index 9f8db69d5..cfccecef2 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -13,6 +13,7 @@ from numpy.ctypeslib import as_array from . import _dll from .error import _error_handler +from ..mpi import comm from openmc.checkvalue import PathLike import openmc.lib import openmc @@ -632,6 +633,9 @@ class TemporarySession: model : openmc.Model, optional OpenMC model to use for the session. If None, a minimal working model is created. + cwd : PathLike, optional + Working directory in which to run OpenMC. If None, a temporary directory + is created and deleted automatically. **init_kwargs Keyword arguments to pass to :func:`openmc.lib.init`. @@ -639,10 +643,13 @@ class TemporarySession: ---------- model : openmc.Model The OpenMC model used for the session. + comm : mpi4py.MPI.Intracomm + The MPI intracommunicator used for the session. """ - def __init__(self, model=None, **init_kwargs): - self.init_kwargs = init_kwargs + def __init__(self, model=None, cwd=None, **init_kwargs): + self.init_kwargs = dict(init_kwargs) + self.cwd = cwd if model is None: surf = openmc.Sphere(boundary_type="vacuum") cell = openmc.Cell(region=-surf) @@ -652,6 +659,10 @@ class TemporarySession: particles=1, batches=1, output={'summary': False}) self.model = model + # Determine MPI intercommunicator + self.init_kwargs.setdefault('intracomm', comm) + self.comm = self.init_kwargs['intracomm'] + def __enter__(self): """Initialize the OpenMC library in a temporary directory.""" # If already initialized, the context manager is a no-op @@ -662,14 +673,24 @@ class TemporarySession: # Store original working directory self.orig_dir = Path.cwd() - # Set up temporary directory - self.tmp_dir = TemporaryDirectory() - working_dir = Path(self.tmp_dir.name) - working_dir.mkdir(parents=True, exist_ok=True) - os.chdir(working_dir) + if self.cwd is None: + # Set up temporary directory on rank 0 + if self.comm.rank == 0: + self._tmp_dir = TemporaryDirectory() + self.cwd = self._tmp_dir.name - # Export model and initialize OpenMC - self.model.export_to_model_xml() + # Broadcast the path so that all ranks use the same directory + self.cwd = self.comm.bcast(self.cwd) + + # Create and change to specified directory + self.cwd = Path(self.cwd) + self.cwd.mkdir(parents=True, exist_ok=True) + os.chdir(self.cwd) + + # Export model on first rank and initialize OpenMC + if self.comm.rank == 0: + self.model.export_to_model_xml() + self.comm.barrier() openmc.lib.init(**self.init_kwargs) return self @@ -683,7 +704,11 @@ class TemporarySession: finalize() finally: os.chdir(self.orig_dir) - self.tmp_dir.cleanup() + + # Make sure all ranks have finalized before deleting temporary dir + self.comm.barrier() + if hasattr(self, '_tmp_dir'): + self._tmp_dir.cleanup() class _DLLGlobal: From f544d02e499248281050ef2272d042c4a7933858 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 19 Nov 2025 11:26:57 -0600 Subject: [PATCH 456/671] Don't write reaction rates in depletion results by default, remove per-stage data for multistage integrators (#3609) --- docs/source/io_formats/depletion_results.rst | 26 +-- openmc/deplete/abc.py | 153 ++++++++----- openmc/deplete/integrators.py | 85 +++---- openmc/deplete/results.py | 8 +- openmc/deplete/stepresult.py | 222 +++++++++++-------- tests/unit_tests/test_deplete_continue.py | 8 +- tests/unit_tests/test_deplete_integrator.py | 93 +++++--- tests/unit_tests/test_deplete_restart.py | 14 +- 8 files changed, 336 insertions(+), 273 deletions(-) diff --git a/docs/source/io_formats/depletion_results.rst b/docs/source/io_formats/depletion_results.rst index 7035fc9c9..b2a726dad 100644 --- a/docs/source/io_formats/depletion_results.rst +++ b/docs/source/io_formats/depletion_results.rst @@ -4,7 +4,7 @@ Depletion Results File Format ============================= -The current version of the depletion results file format is 1.1. +The current version of the depletion results file format is 1.2. **/** @@ -12,22 +12,20 @@ The current version of the depletion results file format is 1.1. - **version** (*int[2]*) -- Major and minor version of the statepoint file format. -:Datasets: - **eigenvalues** (*double[][][2]*) -- k-eigenvalues at each - time/stage. This array has shape (number of timesteps, number of - stages, value). The last axis contains the eigenvalue and the - associated uncertainty - - **number** (*double[][][][]*) -- Total number of atoms. This array - has shape (number of timesteps, number of stages, number of +:Datasets: - **eigenvalues** (*double[][2]*) -- k-eigenvalues at each timestep. + This array has shape (number of timesteps, 2). The second axis + contains the eigenvalue and its associated uncertainty. + - **number** (*double[][][]*) -- Total number of atoms at each + timestep. This array has shape (number of timesteps, number of materials, number of nuclides). - - **reaction rates** (*double[][][][][]*) -- Reaction rates used to - build depletion matrices. This array has shape (number of - timesteps, number of stages, number of materials, number of - nuclides, number of reactions). + - **reaction rates** (*double[][][][]*) -- Reaction rates at each + timestep. This array has shape (number of timesteps, number of + materials, number of nuclides, number of reactions). Only stored if + write_rates=True. - **time** (*double[][2]*) -- Time in [s] at beginning/end of each step. - - **source_rate** (*double[][]*) -- Power in [W] or source rate in - [neutron/sec]. This array has shape (number of timesteps, number - of stages). + - **source_rate** (*double[]*) -- Power in [W] or source rate in + [neutron/sec] for each timestep. - **depletion time** (*double[]*) -- Average process time in [s] spent depleting a material across all burnable materials and, if applicable, MPI processes. diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index fb18d86af..32f468306 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -631,17 +631,7 @@ class Integrator(ABC): solver: str = "cram48", continue_timesteps: bool = False, ): - # Check number of stages previously used - if operator.prev_res is not None: - res = operator.prev_res[-1] - if res.data.shape[0] != self._num_stages: - raise ValueError( - "{} incompatible with previous restart calculation. " - "Previous scheme used {} intermediate solutions, while " - "this uses {}".format( - self.__class__.__name__, res.data.shape[0], - self._num_stages)) - elif continue_timesteps: + if continue_timesteps and operator.prev_res is None: raise ValueError("Continuation run requires passing prev_results.") self.operator = operator self.chain = operator.chain @@ -775,12 +765,8 @@ class Integrator(ABC): ------- proc_time : float Time spent in CRAM routines for all materials in [s] - n_list : list of list of numpy.ndarray - Concentrations at each of the intermediate points with - the final concentration as the last element - op_results : list of openmc.deplete.OperatorResult - Eigenvalue and reaction rates from intermediate transport - simulations + n_end : list of numpy.ndarray + Concentrations at end of timestep """ @property @@ -811,9 +797,9 @@ class Integrator(ABC): """Get beginning of step concentrations, reaction rates from restart""" res = self.operator.prev_res[-1] # Depletion methods expect list of arrays - bos_conc = list(res.data[0]) - rates = res.rates[0] - k = ufloat(res.k[0, 0], res.k[0, 1]) + bos_conc = list(res.data) + rates = res.rates + k = ufloat(res.k[0], res.k[1]) if res.source_rate != 0.0: # Scale reaction rates by ratio of source rates @@ -855,7 +841,8 @@ class Integrator(ABC): self, final_step: bool = True, output: bool = True, - path: PathLike = 'depletion_results.h5' + path: PathLike = 'depletion_results.h5', + write_rates: bool = False ): """Perform the entire depletion process across all steps @@ -874,6 +861,11 @@ class Integrator(ABC): Path to file to write. Defaults to 'depletion_results.h5'. .. versionadded:: 0.15.0 + write_rates : bool, optional + Whether reaction rates should be written to the results file for + each step. Defaults to ``False`` to reduce file size. + + .. versionadded:: 0.15.3 """ with change_directory(self.operator.output_dir): n = self.operator.initial_condition() @@ -890,18 +882,22 @@ class Integrator(ABC): n, res = self._get_bos_data_from_restart(source_rate, n) # Solve Bateman equations over time interval - proc_time, n_list, res_list = self(n, res.rates, dt, source_rate, i) + proc_time, n_end = self(n, res.rates, dt, source_rate, i) - # Insert BOS concentration, transport results - n_list.insert(0, n) - res_list.insert(0, res) - - # Remove actual EOS concentration for next step - n = n_list.pop() - - StepResult.save(self.operator, n_list, res_list, [t, t + dt], - source_rate, self._i_res + i, proc_time, path) + StepResult.save( + self.operator, + n, + res, + [t, t + dt], + source_rate, + self._i_res + i, + proc_time, + write_rates=write_rates, + path=path + ) + # Update for next step + n = n_end t += dt # Final simulation -- in the case that final_step is False, a zero @@ -910,9 +906,18 @@ class Integrator(ABC): # solve) if output and final_step and comm.rank == 0: print(f"[openmc.deplete] t={t} (final operator evaluation)") - res_list = [self.operator(n, source_rate if final_step else 0.0)] - StepResult.save(self.operator, [n], res_list, [t, t], - source_rate, self._i_res + len(self), proc_time, path) + res_final = self.operator(n, source_rate if final_step else 0.0) + StepResult.save( + self.operator, + n, + res_final, + [t, t], + source_rate, + self._i_res + len(self), + proc_time, + write_rates=write_rates, + path=path + ) self.operator.write_bos_data(len(self) + self._i_res) self.operator.finalize() @@ -1171,10 +1176,40 @@ class SIIntegrator(Integrator): self.operator.settings.particles //= self.n_steps return inherited + @abstractmethod + def __call__(self, n, rates, dt, source_rate, i): + """Perform the integration across one time step + + Parameters + ---------- + n : list of numpy.ndarray + List of atom number arrays for each material. Each array has + shape ``(n_nucs,)`` where ``n_nucs`` is the number of nuclides + rates : openmc.deplete.ReactionRates + Reaction rates (from transport operator) + dt : float + Time step in [s] + source_rate : float + Power in [W] or source rate in [neutron/sec] + i : int + Current time step index + + Returns + ------- + proc_time : float + Time spent in transport simulation + n_end : list of numpy.ndarray + Updated atom number densities for each material + op_result : OperatorResult + Eigenvalue and reaction rates resulting from transport simulation + + """ + def integrate( self, output: bool = True, - path: PathLike = "depletion_results.h5" + path: PathLike = "depletion_results.h5", + write_rates: bool = False ): """Perform the entire depletion process across all steps @@ -1186,11 +1221,17 @@ class SIIntegrator(Integrator): Path to file to write. Defaults to 'depletion_results.h5'. .. versionadded:: 0.15.0 + write_rates : bool, optional + Whether reaction rates should be written to the results file for + each step. Defaults to ``False`` to reduce file size. + + .. versionadded:: 0.15.3 """ with change_directory(self.operator.output_dir): n = self.operator.initial_condition() t, self._i_res = self._get_start_data() + res_end = None # Will be set in first iteration for i, (dt, p) in enumerate(self): if output: print(f"[openmc.deplete] t={t} s, dt={dt} s, source={p}") @@ -1200,28 +1241,38 @@ class SIIntegrator(Integrator): n, res = self._get_bos_data_from_operator(i, p, n) else: n, res = self._get_bos_data_from_restart(p, n) - else: - # Pull rates, k from previous iteration w/o - # re-running transport - res = res_list[-1] # defined in previous i iteration - proc_time, n_list, res_list = self(n, res.rates, dt, p, i) + proc_time, n_end, res_end = self(n, res.rates, dt, p, i) - # Insert BOS concentration, transport results - n_list.insert(0, n) - res_list.insert(0, res) - - # Remove actual EOS concentration for next step - n = n_list.pop() - - StepResult.save(self.operator, n_list, res_list, [t, t + dt], - p, self._i_res + i, proc_time, path) + StepResult.save( + self.operator, + n, + res, + [t, t + dt], + p, + self._i_res + i, + proc_time, + write_rates=write_rates, + path=path + ) + # Update for next step + n = n_end + res = res_end t += dt # No final simulation for SIE, use last iteration results - StepResult.save(self.operator, [n], [res_list[-1]], [t, t], - p, self._i_res + len(self), proc_time, path) + StepResult.save( + self.operator, + n, + res_end, + [t, t], + p, + self._i_res + len(self), + proc_time, + write_rates=write_rates, + path=path + ) self.operator.write_bos_data(self._i_res + len(self)) self.operator.finalize() diff --git a/openmc/deplete/integrators.py b/openmc/deplete/integrators.py index 7c543a6cb..25e64cb2e 100644 --- a/openmc/deplete/integrators.py +++ b/openmc/deplete/integrators.py @@ -46,15 +46,12 @@ class PredictorIntegrator(Integrator): ------- proc_time : float Time spent in CRAM routines for all materials in [s] - n_list : list of list of numpy.ndarray + n_end : list of numpy.ndarray Concentrations at end of interval - op_results : empty list - Kept for consistency with API. No intermediate calls to operator - with predictor """ proc_time, n_end = self._timed_deplete(n, rates, dt, _i) - return proc_time, [n_end], [] + return proc_time, n_end @add_params @@ -98,11 +95,8 @@ class CECMIntegrator(Integrator): ------- proc_time : float Time spent in CRAM routines for all materials in [s] - n_list : list of list of numpy.ndarray - Concentrations at each of the intermediate points with - the final concentration as the last element - op_results : list of openmc.deplete.OperatorResult - Eigenvalue and reaction rates from transport simulations + n_end : list of numpy.ndarray + Concentrations at end of interval """ # deplete across first half of interval time0, n_middle = self._timed_deplete(n, rates, dt / 2, _i) @@ -112,7 +106,7 @@ class CECMIntegrator(Integrator): # MOS reaction rates time1, n_end = self._timed_deplete(n, res_middle.rates, dt, _i) - return time0 + time1, [n_middle, n_end], [res_middle] + return time0 + time1, n_end @add_params @@ -162,12 +156,8 @@ class CF4Integrator(Integrator): ------- proc_time : float Time spent in CRAM routines for all materials in [s] - n_list : list of numpy.ndarray - Concentrations at each of the intermediate points with - the final concentration as the last element - op_results : list of openmc.deplete.OperatorResult - Eigenvalue and reaction rates from intermediate transport - simulations + n_end : list of numpy.ndarray + Concentrations at end of interval """ # Step 1: deplete with matrix 1/2*A(y0) time1, n_eos1 = self._timed_deplete( @@ -192,9 +182,7 @@ class CF4Integrator(Integrator): time5, n_eos5 = self._timed_deplete( n_inter, list_rates, dt, _i, matrix_func=cf4_f4) - return (time1 + time2 + time3 + time4 + time5, - [n_eos1, n_eos2, n_eos3, n_eos5], - [res1, res2, res3]) + return time1 + time2 + time3 + time4 + time5, n_eos5 @add_params @@ -240,12 +228,8 @@ class CELIIntegrator(Integrator): ------- proc_time : float Time spent in CRAM routines for all materials in [s] - n_list : list of list of numpy.ndarray - Concentrations at each of the intermediate points with - the final concentration as the last element - op_results : list of openmc.deplete.OperatorResult - Eigenvalue and reaction rates from intermediate transport - simulation + n_end : list of numpy.ndarray + Concentrations at end of interval """ # deplete to end using BOS rates proc_time, n_ce = self._timed_deplete(n_bos, rates, dt, _i) @@ -260,7 +244,7 @@ class CELIIntegrator(Integrator): time_le2, n_end = self._timed_deplete( n_inter, list_rates, dt, _i, matrix_func=celi_f2) - return proc_time + time_le1 + time_le1, [n_ce, n_end], [res_ce] + return proc_time + time_le1 + time_le2, n_end @add_params @@ -306,12 +290,8 @@ class EPCRK4Integrator(Integrator): ------- proc_time : float Time spent in CRAM routines for all materials in [s] - n_list : list of list of numpy.ndarray - Concentrations at each of the intermediate points with - the final concentration as the last element - op_results : list of openmc.deplete.OperatorResult - Eigenvalue and reaction rates from intermediate transport - simulations + n_end : list of numpy.ndarray + Concentrations at end of interval """ # Step 1: deplete with matrix A(y0) / 2 @@ -330,7 +310,7 @@ class EPCRK4Integrator(Integrator): list_rates = list(zip(rates, res1.rates, res2.rates, res3.rates)) time4, n4 = self._timed_deplete(n, list_rates, dt, _i, matrix_func=rk4_f4) - return (time1 + time2 + time3 + time4, [n1, n2, n3, n4], [res1, res2, res3]) + return time1 + time2 + time3 + time4, n4 @add_params @@ -388,12 +368,8 @@ class LEQIIntegrator(Integrator): ------- proc_time : float Time spent in CRAM routines for all materials in [s] - n_list : list of list of numpy.ndarray - Concentrations at each of the intermediate points with - the final concentration as the last element - op_results : list of openmc.deplete.OperatorResult - Eigenvalue and reaction rates from intermediate transport - simulation + n_end : list of numpy.ndarray + Concentrations at end of interval """ if i == 0: if self._i_res < 1: # need at least previous transport solution @@ -402,7 +378,7 @@ class LEQIIntegrator(Integrator): self, n_bos, bos_rates, dt, source_rate, i) prev_res = self.operator.prev_res[-2] prev_dt = self.timesteps[i] - prev_res.time[0] - self._prev_rates = prev_res.rates[0] + self._prev_rates = prev_res.rates else: prev_dt = self.timesteps[i - 1] @@ -431,9 +407,7 @@ class LEQIIntegrator(Integrator): # store updated rates self._prev_rates = copy.deepcopy(bos_res.rates) - return ( - time1 + time2 + time3 + time4, [n_eos0, n_eos1], - [bos_res, res_inter]) + return time1 + time2 + time3 + time4, n_eos1 @add_params @@ -470,10 +444,9 @@ class SICELIIntegrator(SIIntegrator): ------- proc_time : float Time spent in CRAM routines for all materials in [s] - n_bos_list : list of list of numpy.ndarray - Concentrations at each of the intermediate points with - the final concentration as the last element - op_results : list of openmc.deplete.OperatorResult + n_end : list of numpy.ndarray + Concentrations at end of interval + op_result : openmc.deplete.OperatorResult Eigenvalue and reaction rates from intermediate transport simulations """ @@ -499,7 +472,7 @@ class SICELIIntegrator(SIIntegrator): proc_time += time1 + time2 # end iteration - return proc_time, [n_eos, n_inter], [res_bar] + return proc_time, n_inter, res_bar @add_params @@ -536,10 +509,9 @@ class SILEQIIntegrator(SIIntegrator): ------- proc_time : float Time spent in CRAM routines for all materials in [s] - n_list : list of list of numpy.ndarray - Concentrations at each of the intermediate points with - the final concentration as the last element - op_results : list of openmc.deplete.OperatorResult + n_end : list of numpy.ndarray + Concentrations at end of interval + op_result : openmc.deplete.OperatorResult Eigenvalue and reaction rates from intermediate transport simulation """ @@ -551,7 +523,7 @@ class SILEQIIntegrator(SIIntegrator): self, n_bos, bos_rates, dt, source_rate, i) prev_res = self.operator.prev_res[-2] prev_dt = self.timesteps[i] - prev_res.time[0] - self._prev_rates = prev_res.rates[0] + self._prev_rates = prev_res.rates else: prev_dt = self.timesteps[i - 1] @@ -584,7 +556,10 @@ class SILEQIIntegrator(SIIntegrator): n_inter, inputs, dt, i, matrix_func=leqi_f4) proc_time += time1 + time2 - return proc_time, [n_eos, n_inter], [res_bar] + # Store updated rates for next step + self._prev_rates = copy.deepcopy(bos_rates) + + return proc_time, n_inter, res_bar integrator_by_name = { diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index 7427abd73..e1fcb26b6 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -203,7 +203,7 @@ class Results(list): # Evaluate value in each region for i, result in enumerate(self): times[i] = result.time[0] - concentrations[i] = result[0, mat_id, nuc] + concentrations[i] = result[mat_id, nuc] # Unit conversions times = _get_time_as(times, time_units) @@ -363,7 +363,7 @@ class Results(list): # Evaluate value in each region for i, result in enumerate(self): times[i] = result.time[0] - rates[i] = result.rates[0].get(mat_id, nuc, rx) * result[0, mat, nuc] + rates[i] = result.rates.get(mat_id, nuc, rx) * result[mat, nuc] return times, rates @@ -397,7 +397,7 @@ class Results(list): # Get time/eigenvalue at each point for i, result in enumerate(self): times[i] = result.time[0] - eigenvalues[i] = result.k[0] + eigenvalues[i] = result.k # Convert time units if necessary times = _get_time_as(times, time_units) @@ -630,7 +630,7 @@ class Results(list): for nuc in result.index_nuc: if nuc not in available_cross_sections: continue - atoms = result[0, mat_id, nuc] + atoms = result[mat_id, nuc] if atoms > 0.0: atoms_per_barn_cm = 1e-24 * atoms / mat.volume mat.remove_nuclide(nuc) # Replace if it's there diff --git a/openmc/deplete/stepresult.py b/openmc/deplete/stepresult.py index 1a26cbe34..ff39e9acb 100644 --- a/openmc/deplete/stepresult.py +++ b/openmc/deplete/stepresult.py @@ -16,7 +16,7 @@ from openmc.mpi import comm, MPI from openmc.checkvalue import PathLike from .reaction_rates import ReactionRates -VERSION_RESULTS = (1, 1) +VERSION_RESULTS = (1, 2) __all__ = ["StepResult"] @@ -30,8 +30,8 @@ class StepResult: Attributes ---------- - k : list of (float, float) - Eigenvalue and uncertainty for each substep. + k : tuple of (float, float) + Eigenvalue and uncertainty at end of step. time : list of float Time at beginning, end of step, in seconds. source_rate : float @@ -40,8 +40,8 @@ class StepResult: Number of mats. n_nuc : int Number of nuclides. - rates : list of ReactionRates - The reaction rates for each substep. + rates : ReactionRates + The reaction rates at end of step. volume : dict of str to float Dictionary mapping mat id to volume. index_mat : dict of str to int @@ -52,10 +52,8 @@ class StepResult: A dictionary mapping mat ID as string to global index. n_hdf5_mats : int Number of materials in entire geometry. - n_stages : int - Number of stages in simulation. data : numpy.ndarray - Atom quantity, stored by stage, mat, then by nuclide. + Atom quantity, stored by mat, then by nuclide. proc_time : int Average time spent depleting a material across all materials and processes @@ -86,17 +84,17 @@ class StepResult: Parameters ---------- pos : tuple - A three-length tuple containing a stage index, mat index and a nuc - index. All can be integers or slices. The second two can be + A two-length tuple containing a mat index and a nuc + index. Both can be integers or slices, or can be strings corresponding to their respective dictionary. Returns ------- float - The atoms for stage, mat, nuc + The atoms for mat, nuc """ - stage, mat, nuc = pos + mat, nuc = pos if isinstance(mat, openmc.Material): mat = str(mat.id) if isinstance(mat, str): @@ -104,7 +102,7 @@ class StepResult: if isinstance(nuc, str): nuc = self.index_nuc[nuc] - return self.data[stage, mat, nuc] + return self.data[mat, nuc] def __setitem__(self, pos, val): """Sets an item from results. @@ -112,21 +110,21 @@ class StepResult: Parameters ---------- pos : tuple - A three-length tuple containing a stage index, mat index and a nuc - index. All can be integers or slices. The second two can be + A two-length tuple containing a mat index and a nuc + index. Both can be integers or slices, or can be strings corresponding to their respective dictionary. val : float The value to set data to. """ - stage, mat, nuc = pos + mat, nuc = pos if isinstance(mat, str): mat = self.index_mat[mat] if isinstance(nuc, str): nuc = self.index_nuc[nuc] - self.data[stage, mat, nuc] = val + self.data[mat, nuc] = val @property def n_mat(self): @@ -140,11 +138,7 @@ class StepResult: def n_hdf5_mats(self): return len(self.mat_to_hdf5_ind) - @property - def n_stages(self): - return self.data.shape[0] - - def allocate(self, volume, nuc_list, burn_list, full_burn_list, stages): + def allocate(self, volume, nuc_list, burn_list, full_burn_list): """Allocate memory for depletion step data Parameters @@ -157,8 +151,6 @@ class StepResult: A list of all mat IDs to be burned. Used for sorting the simulation. full_burn_list : list of str List of all burnable material IDs - stages : int - Number of stages in simulation. """ self.volume = copy.deepcopy(volume) @@ -167,7 +159,7 @@ class StepResult: self.mat_to_hdf5_ind = {mat: i for i, mat in enumerate(full_burn_list)} # Create storage array - self.data = np.zeros((stages, self.n_mat, self.n_nuc)) + self.data = np.zeros((self.n_mat, self.n_nuc)) def distribute(self, local_materials, ranges): """Create a new object containing data for distributed materials @@ -196,8 +188,8 @@ class StepResult: for attr in direct_attrs: setattr(new, attr, getattr(self, attr)) # Get applicable slice of data - new.data = self.data[:, ranges] - new.rates = [r[ranges] for r in self.rates] + new.data = self.data[ranges] + new.rates = self.rates[ranges] return new def get_material(self, mat_id): @@ -232,7 +224,7 @@ class StepResult: f'values are {list(self.volume.keys())}' ) from e for nuc, _ in sorted(self.index_nuc.items(), key=lambda x: x[1]): - atoms = self[0, mat_id, nuc] + atoms = self[mat_id, nuc] if atoms <= 0.0: continue atom_per_bcm = atoms / vol * 1e-24 @@ -240,7 +232,7 @@ class StepResult: material.volume = vol return material - def export_to_hdf5(self, filename, step): + def export_to_hdf5(self, filename, step, write_rates: bool = False): """Export results to an HDF5 file Parameters @@ -249,6 +241,8 @@ class StepResult: The filename to write to step : int What step is this? + write_rates : bool, optional + Whether to include reaction rate datasets in the results file. """ # Write new file if first time step, else add to existing file @@ -259,7 +253,8 @@ class StepResult: kwargs['driver'] = 'mpio' kwargs['comm'] = comm with h5py.File(filename, **kwargs) as handle: - self._to_hdf5(handle, step, parallel=True) + self._to_hdf5(handle, step, parallel=True, + write_rates=write_rates) else: # Gather results at root process all_results = comm.gather(self) @@ -268,15 +263,18 @@ class StepResult: if comm.rank == 0: with h5py.File(filename, **kwargs) as handle: for res in all_results: - res._to_hdf5(handle, step, parallel=False) + res._to_hdf5(handle, step, parallel=False, + write_rates=write_rates) - def _write_hdf5_metadata(self, handle): + def _write_hdf5_metadata(self, handle, write_rates): """Writes result metadata in HDF5 file Parameters ---------- handle : h5py.File or h5py.Group An hdf5 file or group type to store this in. + write_rates : bool + Whether reaction rate datasets are being written. """ # Create and save the 5 dictionaries: @@ -284,8 +282,8 @@ class StepResult: # self.index_mat -> self.volume (TODO: support for changing volumes) # self.index_nuc # reactions - # self.rates[0].index_nuc (can be different from above, above is superset) - # self.rates[0].index_rx + # self.rates.index_nuc (can be different from above, above is superset) + # self.rates.index_rx # these are shared by every step of the simulation, and should be deduplicated. # Store concentration mat and nuclide dictionaries (along with volumes) @@ -295,13 +293,19 @@ class StepResult: mat_list = sorted(self.mat_to_hdf5_ind, key=int) nuc_list = sorted(self.index_nuc) - rxn_list = sorted(self.rates[0].index_rx) + + include_rates = ( + write_rates + and self.rates is not None + and bool(self.rates.index_nuc) + and bool(self.rates.index_rx) + ) + rxn_list = sorted(self.rates.index_rx) if include_rates else [] n_mats = self.n_hdf5_mats n_nuc_number = len(nuc_list) - n_nuc_rxn = len(self.rates[0].index_nuc) + n_nuc_rxn = len(self.rates.index_nuc) if include_rates else 0 n_rxn = len(rxn_list) - n_stages = self.n_stages mat_group = handle.create_group("materials") @@ -315,41 +319,44 @@ class StepResult: for nuc in nuc_list: nuc_single_group = nuc_group.create_group(nuc) nuc_single_group.attrs["atom number index"] = self.index_nuc[nuc] - if nuc in self.rates[0].index_nuc: - nuc_single_group.attrs["reaction rate index"] = self.rates[0].index_nuc[nuc] + if include_rates and nuc in self.rates.index_nuc: + nuc_single_group.attrs["reaction rate index"] = ( + self.rates.index_nuc[nuc]) - rxn_group = handle.create_group("reactions") + if include_rates: + rxn_group = handle.create_group("reactions") - for rxn in rxn_list: - rxn_single_group = rxn_group.create_group(rxn) - rxn_single_group.attrs["index"] = self.rates[0].index_rx[rxn] + for rxn in rxn_list: + rxn_single_group = rxn_group.create_group(rxn) + rxn_single_group.attrs["index"] = ( + self.rates.index_rx[rxn]) # Construct array storage - handle.create_dataset("number", (1, n_stages, n_mats, n_nuc_number), - maxshape=(None, n_stages, n_mats, n_nuc_number), + handle.create_dataset("number", (1, n_mats, n_nuc_number), + maxshape=(None, n_mats, n_nuc_number), chunks=True, dtype='float64') - if n_nuc_rxn > 0 and n_rxn > 0: - handle.create_dataset("reaction rates", (1, n_stages, n_mats, n_nuc_rxn, n_rxn), - maxshape=(None, n_stages, n_mats, n_nuc_rxn, n_rxn), - chunks=True, - dtype='float64') + if include_rates and n_nuc_rxn > 0 and n_rxn > 0: + handle.create_dataset( + "reaction rates", (1, n_mats, n_nuc_rxn, n_rxn), + maxshape=(None, n_mats, n_nuc_rxn, n_rxn), + chunks=True, dtype='float64') - handle.create_dataset("eigenvalues", (1, n_stages, 2), - maxshape=(None, n_stages, 2), dtype='float64') + handle.create_dataset("eigenvalues", (1, 2), + maxshape=(None, 2), dtype='float64') handle.create_dataset("time", (1, 2), maxshape=(None, 2), dtype='float64') - handle.create_dataset("source_rate", (1, n_stages), maxshape=(None, n_stages), + handle.create_dataset("source_rate", (1,), maxshape=(None,), dtype='float64') handle.create_dataset( "depletion time", (1,), maxshape=(None,), dtype="float64") - def _to_hdf5(self, handle, index, parallel=False): + def _to_hdf5(self, handle, index, parallel=False, write_rates: bool = False): """Converts results object into an hdf5 object. Parameters @@ -360,12 +367,14 @@ class StepResult: What step is this? parallel : bool Being called with parallel HDF5? + write_rates : bool, optional + Whether reaction rate datasets are being written. """ if "/number" not in handle: if parallel: comm.barrier() - self._write_hdf5_metadata(handle) + self._write_hdf5_metadata(handle, write_rates) if parallel: comm.barrier() @@ -417,18 +426,14 @@ class StepResult: return # Add data - # Note, for the last step, self.n_stages = 1, even if n_stages != 1. - n_stages = self.n_stages inds = [self.mat_to_hdf5_ind[mat] for mat in self.index_mat] low = min(inds) high = max(inds) - for i in range(n_stages): - number_dset[index, i, low:high+1] = self.data[i] - if has_reactions: - rxn_dset[index, i, low:high+1] = self.rates[i] - if comm.rank == 0: - eigenvalues_dset[index, i] = self.k[i] + number_dset[index, low:high+1] = self.data + if has_reactions: + rxn_dset[index, low:high+1] = self.rates if comm.rank == 0: + eigenvalues_dset[index] = self.k time_dset[index] = self.time source_rate_dset[index] = self.source_rate if self.proc_time is not None: @@ -459,10 +464,24 @@ class StepResult: # Older versions used "power" instead of "source_rate" source_rate_dset = handle["/power"] - results.data = number_dset[step, :, :, :] - results.k = eigenvalues_dset[step, :] + # Check if this is an old format file (with stages dimension) or new format + # Old format: number has shape (n_steps, n_stages, n_mats, n_nucs) + # New format: number has shape (n_steps, n_mats, n_nucs) + has_stages = len(number_dset.shape) == 4 + + if has_stages: + # Old format - extract data from first stage (index 0) + results.data = number_dset[step, 0, :, :] + results.k = eigenvalues_dset[step, 0, :] + # source_rate had shape (n_steps, n_stages) in old format + results.source_rate = source_rate_dset[step, 0] + else: + # New format - no stages dimension + results.data = number_dset[step, :, :] + results.k = eigenvalues_dset[step, :] + results.source_rate = source_rate_dset[step] + results.time = time_dset[step, :] - results.source_rate = source_rate_dset[step, 0] if "depletion time" in handle: proc_time_dset = handle["/depletion time"] @@ -493,33 +512,45 @@ class StepResult: if "reaction rate index" in nuc_handle.attrs: rxn_nuc_to_ind[nuc] = nuc_handle.attrs["reaction rate index"] - for rxn, rxn_handle in handle["/reactions"].items(): - rxn_to_ind[rxn] = rxn_handle.attrs["index"] + if "reactions" in handle: + for rxn, rxn_handle in handle["/reactions"].items(): + rxn_to_ind[rxn] = rxn_handle.attrs["index"] - results.rates = [] - # Reconstruct reactions - for i in range(results.n_stages): - rate = ReactionRates(results.index_mat, rxn_nuc_to_ind, rxn_to_ind, True) - - if "reaction rates" in handle: - rate[:] = handle["/reaction rates"][step, i, :, :, :] - results.rates.append(rate) + # Reconstruct reaction rates + rate = ReactionRates(results.index_mat, rxn_nuc_to_ind, rxn_to_ind, True) + if "reaction rates" in handle: + if has_stages: + # Old format: (n_steps, n_stages, n_mats, n_nucs, n_rxns) + rate[:] = handle["/reaction rates"][step, 0, :, :, :] + else: + # New format: (n_steps, n_mats, n_nucs, n_rxns) + rate[:] = handle["/reaction rates"][step, :, :, :] + results.rates = rate return results @staticmethod - def save(op, x, op_results, t, source_rate, step_ind, proc_time=None, - path: PathLike = "depletion_results.h5"): + def save( + op, + x, + op_results, + t, + source_rate, + step_ind, + proc_time=None, + write_rates: bool = False, + path: PathLike = "depletion_results.h5" + ): """Creates and writes depletion results to disk Parameters ---------- op : openmc.deplete.abc.TransportOperator The operator used to generate these results. - x : list of list of numpy.array - The prior x vectors. Indexed [i][cell] using the above equation. - op_results : list of openmc.deplete.OperatorResult - Results of applying transport operator + x : numpy.array + End-of-step concentrations for each material + op_results : openmc.deplete.OperatorResult + Result of applying transport operator at end of step t : list of float Time indices. source_rate : float @@ -530,7 +561,8 @@ class StepResult: Total process time spent depleting materials. This may be process-dependent and will be reduced across MPI processes. - + write_rates : bool, optional + Whether reaction rates should be written to the results file. path : PathLike Path to file to write. Defaults to 'depletion_results.h5'. @@ -539,26 +571,20 @@ class StepResult: # Get indexing terms vol_dict, nuc_list, burn_list, full_burn_list = op.get_results_info() - stages = len(x) - # Create results results = StepResult() - results.allocate(vol_dict, nuc_list, burn_list, full_burn_list, stages) + results.allocate(vol_dict, nuc_list, burn_list, full_burn_list) n_mat = len(burn_list) - for i in range(stages): - for mat_i in range(n_mat): - results[i, mat_i, :] = x[i][mat_i] + for mat_i in range(n_mat): + results[mat_i, :] = x[mat_i] - ks = [] - for r in op_results: - if isinstance(r.k, type(None)): - ks += [(None, None)] - else: - ks += [(r.k.nominal_value, r.k.std_dev)] - results.k = ks - results.rates = [r.rates for r in op_results] + if isinstance(op_results.k, type(None)): + results.k = (None, None) + else: + results.k = (op_results.k.nominal_value, op_results.k.std_dev) + results.rates = op_results.rates results.time = t results.source_rate = source_rate results.proc_time = proc_time @@ -567,7 +593,7 @@ class StepResult: if not Path(path).is_file(): Path(path).parent.mkdir(parents=True, exist_ok=True) - results.export_to_hdf5(path, step_ind) + results.export_to_hdf5(path, step_ind, write_rates) def transfer_volumes(self, model): """Transfers volumes from depletion results to geometry diff --git a/tests/unit_tests/test_deplete_continue.py b/tests/unit_tests/test_deplete_continue.py index 637c9d5e4..1b6eac238 100644 --- a/tests/unit_tests/test_deplete_continue.py +++ b/tests/unit_tests/test_deplete_continue.py @@ -17,7 +17,7 @@ def test_continue(run_in_tmpdir): operator = dummy_operator.DummyOperator() # initial depletion - bundle.solver(operator, [1.0, 2.0], [1.0, 2.0]).integrate() + bundle.solver(operator, [1.0, 2.0], [1.0, 2.0]).integrate(write_rates=True) # set up continue run prev_res = openmc.deplete.Results(operator.output_dir / "depletion_results.h5") @@ -25,7 +25,7 @@ def test_continue(run_in_tmpdir): # if continue run happens, test passes bundle.solver(operator, [1.0, 2.0, 3.0, 4.0], [1.0, 2.0, 3.0, 4.0], - continue_timesteps=True).integrate() + continue_timesteps=True).integrate(write_rates=True) final_res = openmc.deplete.Results(operator.output_dir / "depletion_results.h5") @@ -42,7 +42,7 @@ def test_continue_continue(run_in_tmpdir): operator = dummy_operator.DummyOperator() # initial depletion - bundle.solver(operator, [1.0, 2.0], [1.0, 2.0]).integrate() + bundle.solver(operator, [1.0, 2.0], [1.0, 2.0]).integrate(write_rates=True) # set up continue run prev_res = openmc.deplete.Results(operator.output_dir / "depletion_results.h5") @@ -50,7 +50,7 @@ def test_continue_continue(run_in_tmpdir): # first continue run bundle.solver(operator, [1.0, 2.0, 3.0, 4.0], [1.0, 2.0, 3.0, 4.0], - continue_timesteps=True).integrate() + continue_timesteps=True).integrate(write_rates=True) prev_res = openmc.deplete.Results(operator.output_dir / "depletion_results.h5") # second continue run diff --git a/tests/unit_tests/test_deplete_integrator.py b/tests/unit_tests/test_deplete_integrator.py index b1d2cb950..6463eaa20 100644 --- a/tests/unit_tests/test_deplete_integrator.py +++ b/tests/unit_tests/test_deplete_integrator.py @@ -10,6 +10,7 @@ import copy from random import uniform from unittest.mock import MagicMock +import h5py import numpy as np from uncertainties import ufloat import pytest @@ -38,8 +39,6 @@ INTEGRATORS = [ def test_results_save(run_in_tmpdir): """Test data save module""" - stages = 3 - rng = np.random.RandomState(comm.rank) # Mock geometry @@ -63,31 +62,22 @@ def test_results_save(run_in_tmpdir): op.get_results_info.return_value = ( vol_dict, nuc_list, burn_list, full_burn_list) - # Construct x - x1 = [] - x2 = [] + # Construct end-of-step concentrations + x1 = [rng.random(2), rng.random(2)] + x2 = [rng.random(2), rng.random(2)] - for i in range(stages): - x1.append([rng.random(2), rng.random(2)]) - x2.append([rng.random(2), rng.random(2)]) - - # Construct r + # Construct reaction rates r1 = ReactionRates(burn_list, ["na", "nb"], ["ra", "rb"]) r1[:] = rng.random((2, 2, 2)) + rate1 = copy.deepcopy(r1) - rate1 = [] - rate2 = [] + r2 = ReactionRates(burn_list, ["na", "nb"], ["ra", "rb"]) + r2[:] = rng.random((2, 2, 2)) + rate2 = copy.deepcopy(r2) - for i in range(stages): - rate1.append(copy.deepcopy(r1)) - r1[:] = rng.random((2, 2, 2)) - rate2.append(copy.deepcopy(r1)) - r1[:] = rng.random((2, 2, 2)) - - # Create global terms - # Col 0: eig, Col 1: uncertainty - eigvl1 = rng.random((stages, 2)) - eigvl2 = rng.random((stages, 2)) + # Create global terms (eigenvalue and uncertainty) + eigvl1 = rng.random(2) + eigvl2 = rng.random(2) eigvl1 = comm.bcast(eigvl1, root=0) eigvl2 = comm.bcast(eigvl2, root=0) @@ -95,29 +85,35 @@ def test_results_save(run_in_tmpdir): t1 = [0.0, 1.0] t2 = [1.0, 2.0] - op_result1 = [OperatorResult(ufloat(*k), rates) - for k, rates in zip(eigvl1, rate1)] - op_result2 = [OperatorResult(ufloat(*k), rates) - for k, rates in zip(eigvl2, rate2)] + op_result1 = OperatorResult(ufloat(*eigvl1), rate1) + op_result2 = OperatorResult(ufloat(*eigvl2), rate2) # saves within a subdirectory - StepResult.save(op, x1, op_result1, t1, 0, 0, path='out/put/depletion.h5') + StepResult.save( + op, + x1, + op_result1, + t1, + 0, + 0, + write_rates=True, + path='out/put/depletion.h5' + ) res = Results('out/put/depletion.h5') # saves with default filename - StepResult.save(op, x1, op_result1, t1, 0, 0) - StepResult.save(op, x2, op_result2, t2, 0, 1) + StepResult.save(op, x1, op_result1, t1, 0, 0, write_rates=True) + StepResult.save(op, x2, op_result2, t2, 0, 1, write_rates=True) # Load the files res = Results("depletion_results.h5") - for i in range(stages): - for mat_i, mat in enumerate(burn_list): - for nuc_i, nuc in enumerate(nuc_list): - assert res[0][i, mat, nuc] == x1[i][mat_i][nuc_i] - assert res[1][i, mat, nuc] == x2[i][mat_i][nuc_i] - np.testing.assert_array_equal(res[0].rates[i], rate1[i]) - np.testing.assert_array_equal(res[1].rates[i], rate2[i]) + for mat_i, mat in enumerate(burn_list): + for nuc_i, nuc in enumerate(nuc_list): + assert res[0][mat, nuc] == x1[mat_i][nuc_i] + assert res[1][mat, nuc] == x2[mat_i][nuc_i] + np.testing.assert_array_equal(res[0].rates, rate1) + np.testing.assert_array_equal(res[1].rates, rate2) np.testing.assert_array_equal(res[0].k, eigvl1) np.testing.assert_array_equal(res[0].time, t1) @@ -126,6 +122,31 @@ def test_results_save(run_in_tmpdir): np.testing.assert_array_equal(res[1].time, t2) +def test_results_save_without_rates(run_in_tmpdir): + """StepResult.save skips reaction-rate datasets by default""" + + op = MagicMock() + op.prev_res = None + vol_dict = {"0": 1.0} + nuc_list = ["na"] + burn_list = ["0"] + op.get_results_info.return_value = (vol_dict, nuc_list, burn_list, burn_list) + + x = [np.array([1.0])] + rates = ReactionRates(burn_list, nuc_list, ["ra"]) + rates[:] = np.array([[[2.0]]]) + op_result = OperatorResult(ufloat(1.0, 0.1), rates) + + StepResult.save(op, x, op_result, [0.0, 1.0], 0.0, 0) + + with h5py.File('depletion_results.h5', 'r') as handle: + assert 'reaction rates' not in handle + assert 'reactions' not in handle + + res = Results('depletion_results.h5') + assert res[0].rates.size == 0 + + def test_bad_integrator_inputs(): """Test failure modes for Integrator inputs""" diff --git a/tests/unit_tests/test_deplete_restart.py b/tests/unit_tests/test_deplete_restart.py index e8bfc062a..1cbff30a5 100644 --- a/tests/unit_tests/test_deplete_restart.py +++ b/tests/unit_tests/test_deplete_restart.py @@ -21,7 +21,7 @@ def test_restart_predictor_cecm(run_in_tmpdir): # Perform simulation using the predictor algorithm dt = [0.75] power = 1.0 - openmc.deplete.PredictorIntegrator(op, dt, power).integrate() + openmc.deplete.PredictorIntegrator(op, dt, power).integrate(write_rates=True) # Load the files prev_res = openmc.deplete.Results(op.output_dir / "depletion_results.h5") @@ -30,10 +30,6 @@ def test_restart_predictor_cecm(run_in_tmpdir): op = dummy_operator.DummyOperator(prev_res) op.output_dir = output_dir - # check ValueError is raised, indicating previous and current stages - with pytest.raises(ValueError, match="incompatible.* 1.*2"): - openmc.deplete.CECMIntegrator(op, dt, power) - def test_restart_cecm_predictor(run_in_tmpdir): """Integral regression test of integrator algorithm using CE/CM for the @@ -47,7 +43,7 @@ def test_restart_cecm_predictor(run_in_tmpdir): dt = [0.75] power = 1.0 cecm = openmc.deplete.CECMIntegrator(op, dt, power) - cecm.integrate() + cecm.integrate(write_rates=True) # Load the files prev_res = openmc.deplete.Results(op.output_dir / "depletion_results.h5") @@ -56,10 +52,6 @@ def test_restart_cecm_predictor(run_in_tmpdir): op = dummy_operator.DummyOperator(prev_res) op.output_dir = output_dir - # check ValueError is raised, indicating previous and current stages - with pytest.raises(ValueError, match="incompatible.* 2.*1"): - openmc.deplete.PredictorIntegrator(op, dt, power) - @pytest.mark.parametrize("scheme", dummy_operator.SCHEMES) def test_restart(run_in_tmpdir, scheme): @@ -70,7 +62,7 @@ def test_restart(run_in_tmpdir, scheme): operator = dummy_operator.DummyOperator() # take first step - bundle.solver(operator, [0.75], 1.0).integrate() + bundle.solver(operator, [0.75], 1.0).integrate(write_rates=True) # restart prev_res = openmc.deplete.Results( From d217efa007760dbe927af25063ab6d68990779cc Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 20 Nov 2025 12:10:04 -0600 Subject: [PATCH 457/671] Add two MPI barriers in R2S workflow (#3646) --- openmc/deplete/microxs.py | 3 ++- openmc/deplete/r2s.py | 1 + openmc/lib/core.py | 10 ++++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index f4bdc6795..879a2d4ee 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -177,8 +177,9 @@ def get_microxs_and_flux( if not openmc.lib.is_initialized: run_kwargs.setdefault('cwd', temp_dir) - # Run transport simulation + # Run transport simulation and synchronize statepoint_path = model.run(**run_kwargs) + comm.barrier() if comm.rank == 0: # Move the statepoint file if it is being saved to a specific path diff --git a/openmc/deplete/r2s.py b/openmc/deplete/r2s.py index 7f3e94edd..97277cbda 100644 --- a/openmc/deplete/r2s.py +++ b/openmc/deplete/r2s.py @@ -392,6 +392,7 @@ class R2SManager: ) output_path = output_dir / 'depletion_results.h5' integrator.integrate(final_step=False, path=output_path) + comm.barrier() # Get depletion results self.results['depletion_results'] = Results(output_path) diff --git a/openmc/lib/core.py b/openmc/lib/core.py index cfccecef2..02c7784d1 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -7,6 +7,7 @@ import os from pathlib import Path from random import getrandbits from tempfile import TemporaryDirectory +import traceback as tb import numpy as np from numpy.ctypeslib import as_array @@ -700,6 +701,15 @@ class TemporarySession: if self.already_initialized: return + # If an exception occurred, abort all ranks immediately + if exc_type is not None: + # Print exception info on the rank that failed + tb.print_exception(exc_type, exc_value, traceback) + sys.stdout.flush() + + # Abort all MPI processes + self.comm.Abort(1) + try: finalize() finally: From 27e38e894697bb32a1dac7848d2618818b6b8daf Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Sat, 22 Nov 2025 23:06:17 +0100 Subject: [PATCH 458/671] Add release notes for 0.15.3 (#3644) --- docs/source/releasenotes/0.15.3.rst | 226 ++++++++++++++++++ docs/source/releasenotes/index.rst | 1 + docs/source/usersguide/variance_reduction.rst | 4 +- openmc/model/model.py | 6 +- openmc/settings.py | 9 +- 5 files changed, 240 insertions(+), 6 deletions(-) create mode 100644 docs/source/releasenotes/0.15.3.rst diff --git a/docs/source/releasenotes/0.15.3.rst b/docs/source/releasenotes/0.15.3.rst new file mode 100644 index 000000000..c50958104 --- /dev/null +++ b/docs/source/releasenotes/0.15.3.rst @@ -0,0 +1,226 @@ +==================== +What's New in 0.15.3 +==================== + +.. currentmodule:: openmc + +------- +Summary +------- + +This release of OpenMC includes many bug fixes, performance improvements, and +several notable new features. The major highlights of this release include a new +:class:`~openmc.deplete.R2SManager` class that automates the workflow for +rigorous 2-step (R2S) shutdown dose rate calculations, the ability to collect +higher moments for tally results that can be used to test normality, a new +uncertainty-aware criticality search method, a new collision tracking feature +that enables detailed tracking of particle interactions, support for distributed +cell densities, and several new tally filters. The random ray solver also +continues to receive significant updates, including automatic setup +capabilities, improved geometry handling, and better weight window support. +Depletion capabilities have been expanded with thermochemical redox control, +external transfer rates, and improved performance. + +------------------------------------ +Compatibility Notes and Deprecations +------------------------------------ + +MCPL has been changed from a build-time dependency to a runtime optional +dependency, which means OpenMC will attempt to load the MCPL library at +runtime when needed rather than requiring it at build time. + +The ``openmc.mgxs.Library.add_to_tallies_file`` method has been renamed to +:meth:`openmc.mgxs.Library.add_to_tallies`. + +------------ +New Features +------------ + +- A new collision tracking feature enables detailed tracking of particle + interactions (`#3417 `_) +- Added :meth:`~openmc.model.Model.keff_search` method for automated criticality + searches (`#3569 `_) +- Introduced automated workflow for mesh- or cell-based R2S calculations + (`#3508 `_) +- Ability to source electron/positrons directly for charged particle + simulations (`#3404 `_) +- Multi-group capability for kinetics parameter calculations with Iterated + Fission Probability (`#3425 + `_) +- Introduced a new :class:`openmc.MeshMaterialFilter` class (`#3406 + `_) +- Added support for distributed cell densities (`#3546 + `_) +- Implemented a :class:`openmc.WeightWindowsList` class that enables export to + HDF5 (`#3456 `_) +- Added :meth:`openmc.Material.mean_free_path` method (`#3469 + `_) +- Introduced :func:`openmc.lib.TemporarySession` context manager (`#3475 + `_) +- Added material depletion function for tracking individual material depletion + (`#3420 `_) +- Added methods on :class:`~openmc.Material` class for waste disposal rating / + classification (`#3366 `_, + `#3376 `_) +- Support for thermochemical redox control transfer rates in depletion + (`#2783 `_) +- Support for external transfer rates source term in depletion (`#3088 + `_) +- Added combing capability for fission site sampling and delayed neutron + emission time (`#2992 `_) +- Ability to specify reference direction for azimuthal angle in + :class:`~openmc.stats.PolarAzimuthal` distribution (`#3582 + `_) +- Allow spatial constraints on element sources within + :class:`~openmc.MeshSource` (`#3431 + `_) +- Added VTK HDF (.vtkhdf) format support for writing VTK data (`#3252 + `_) +- Implemented filter weight capability (`#3345 + `_) +- Optionally collect higher moments for tallies (`#3363 + `_) +- Several random ray solver enhancements: + + - Random Ray AutoMagic Setup for automatic configuration (`#3351 `_) + - Point source locator for random ray mode (`#3360 `_) + - Support for DAGMC geometries (`#3374 `_) + - Optimized mapping of source regions to tallies (`#3465 `_) + - Base source region refactor (`#3576 `_) + +--------------------------- +Bug Fixes and Small Changes +--------------------------- + +- Add two MPI barriers in R2S workflow (`#3646 `_) +- Fix a few warnings, rename add_to_tallies_file (`#3639 `_) +- Fix typo in DAGMC lost particle test (`#3634 `_) +- Avoid multiprocessing Pool when running depletion tests with MPI (`#3633 `_) +- Support MPI parallelism in R2SManager (`#3632 `_) +- Update documentation for particle tracks (`#3627 `_) +- Adding variance of variance and normality tests for tally statistics (`#3454 `_) +- Avoid divide-by-zero in ``from_multigroup_flux`` when flux is zero (`#3624 `_) +- Write particle states as separate lines in track VTK files (`#3628 `_) +- Reset DAGMC history when reviving from source (`#3601 `_) +- Add energy group structure: SCALE-999 (`#3564 `_) +- Fix bug in normalization of tally results with no_reduce (`#3619 `_) +- Enable nuclide filters with get_decay_photon_energy (`#3614 `_) +- Update ``check_type`` calls to accept both ``str`` and ``os.PathLike`` objects (`#3618 `_) +- Speed up ``apply_time_correction`` by reducing file I/O and deepcopies (`#3617 `_) +- FW-CADIS Disregard Max Realizations Setting (`#3616 `_) +- Random Ray Geometry Debug Mode Fix (`#3615 `_) +- Don't write reaction rates in depletion results by default (`#3609 `_) +- Allow Path objects in MGXSLibrary.export_to_hdf5 (`#3608 `_) +- Clip mixture distributions based on mean times integral (`#3603 `_) +- Allow V0 in atomic_mass function (for ENDF/B-VII.0 data) (`#3607 `_) +- Re-run flaky tests when needed (`#3604 `_) +- Ability to load mesh objects from weight_windows.h5 file (`#3598 `_) +- Switch to using coveralls github action for reporting (`#3594 `_) +- Add user setting for free gas threshold (`#3593 `_) +- Speed up time correction factors (`#3592 `_) +- Fix caching issue when using NCrystal materials (`#3538 `_) +- Fix random ray source region mesh export when using model.export_to_xml() (`#3579 `_) +- Ensure weight_windows_file information is read from XML (`#3587 `_) +- Add missing documentation on in depletion chain file format (`#3590 `_) +- Adding tally filter type option to statepoint get_tally (`#3584 `_) +- Optional separation of mesh-material-volume calc from get_homogenized_materials (`#3581 `_) +- Fix IFP implementation (`#3580 `_) +- Remove several TODOs related to C++17 support (`#3574 `_) +- Fix performance regression in libMesh unstructured mesh tallies (`#3577 `_) +- Update find_package calls in OpenMCConfig.cmake (`#3572 `_) +- Ensure ``n_dimension_`` attribute is set for unstructured meshes (`#3575 `_) +- Allow newer Sphinx version and fix docbuild warnings (`#3571 `_) +- Fixed a bug when combining TimeFilter, MeshFilter, and tracklength estimator (`#3525 `_) +- PowerLaw raises an error if sampling interval contains negative values (`#3542 `_) +- depletion: fix performance of chain matrix construction (`#3567 `_) +- Do not apply boundary conditions when initialized in volume calculation mode (`#3562 `_) +- Bump up tolerance for flaky activation test (`#3560 `_) +- Fixed a bug in plotting cross sections with S(a,b) data (`#3558 `_) +- Change test order to run unit tests first (`#3533 `_) +- adding ecco 33 (`#3556 `_) +- Refactor endf_data to be a fixture (`#3539 `_) +- Revert "fix broken CI" (`#3554 `_) +- fix broken CI (`#3551 `_) +- Leverage particle.move_distance in event advance (`#3544 `_) +- fix tests that accidentaly got broken (`#3543 `_) +- not printing nuclides with 0 percent to terminal (option 2 ) (`#3448 `_) +- Fix a bug in time cutoff behavior (`#3526 `_) +- Avoid duplicate materials written to XML (`#3536 `_) +- Use cached property for openmc.data.Decay.sources (`#3535 `_) +- more helpful error message for dose_coefficients (`#3534 `_) +- Adding 616 group structure (`#3531 `_) +- Remove unused special accessors for tallies (`#3527 `_) +- Consistent XML parsing using functions from _xml module (`#3517 `_) +- Add stat:sum field to MCPL files for proper weight normalization (`#3522 `_) +- Remove reorder_attributes from openmc._xml (`#3519 `_) +- fixed a bug in MeshMaterialFilter.from_volumes (`#3520 `_) +- Fixed a bug in distribcell offsets logic (`#3424 `_) +- Add test for FW-CADIS based WW generation on a DAGMC model (`#3504 `_) +- Fix for Weight Window Scaling Bug (`#3511 `_) +- Fix: ``materials``, ``plots``, and ``tallies`` cannot be passed as lists (`#3513 `_) +- Allow already-initialized openmc.lib in TemporarySession (`#3505 `_) +- Update DAGMC and libMesh precompiler definitions (`#3510 `_) +- Avoid adding ParentNuclideFilter twice when calling prepare_tallies (`#3506 `_) +- Enabling MCPL source files to be read when using surf_source_read (`#3472 `_) +- Boundary info accessors (`#3496 `_) +- automatically finding appropriate dimension when making regular mesh from domain (`#3468 `_) +- Add accessor methods for LocalCoord (`#3494 `_) +- Make MCPL a Runtime Optional Dependency (`#3429 `_) +- Use auto-chunking for StepResult HDF5 writing (`#3498 `_) +- Provide a way to get ID maps from plot parameters on the Model class (`#3481 `_) +- Update OSX install instructions to point to x64 platform (`#3501 `_) +- Update conda install instructions for macOS Apple silicon (`#3488 `_) +- Only show warning if in restart mode (`#3478 `_) +- Add flag to CMakeLists to use submodules instead of searching (`#3480 `_) +- Added citation metadata file (`#3409 `_) +- fix zam parsing (`#3484 `_) +- Support flux collapse method in ``get_microxs_and_flux`` (`#3466 `_) +- Stabilize Adjoint Source (`#3476 `_) +- Refactor and Harden Configuration Management (`#3461 `_) +- Updated Docs to Not Give Specific Python Version Requirement (`#3473 `_) +- Parallelization of Weight Window Update (`#3467 `_) +- Limit Random Ray Weight Window Generation to Final Batch (`#3464 `_) +- Fix Dockerfile DAGMC build (`#3463 `_) +- Fix Weight Window Infinite Loop Bug (`#3457 `_) +- Weight Window Birth Scaling (`#3459 `_) +- Adding checks to geometry.plot to avoid material name overlaps (`#3458 `_) +- Fixing crash when calling Geometry.plot when DAGMCUniverse in geometry (`#3455 `_) +- fixing expansion of elemental Ta bug (`#3443 `_) +- Prevent Adjoint Sources from Trending towards Infinity (`#3449 `_) +- adding plot function to DAGMCUnvierse (`#3451 `_) +- Allow specifying number of equiprobable angles for thermal scattering data generation (`#3346 `_) +- Change Dockerfile from debian:bookworm-slim to ubuntu:24.04 (`#3442 `_) +- Fix Resetting of Auto IDs When Generating MGXS (`#3437 `_) +- Allowing chain_file to be chain object to save reloading time (`#3436 `_) +- update units for flux (`#3441 `_) +- Fix raytrace infinite loop (`#3423 `_) +- Apply Max Number of Events Check to Random Rays (`#3438 `_) +- Add user setting for source rejection fraction (`#3433 `_) +- Adding fix and tests for spherical mesh as spatial distribution (`#3428 `_) +- Random Ray Missed Cell Policy Change for Adjoint Mode (`#3434 `_) +- Random Ray External Source Plotting Fix (`#3430 `_) +- Avoid negative heating values during pair production and bremsstrahlung (`#3426 `_) +- Fix no serialization of periodic_surface_id bug (`#3421 `_) +- Update _get_start_data to always grab the beginning of timestep time (`#3414 `_) +- Fixed a bug in charged particle energy deposition (`#3416 `_) +- Fix bug where the same mesh is written multiple times to settings.xml (`#3418 `_) +- small typo - spelling of Debian (`#3411 `_) +- added test for dagmc geometry plot (`#3375 `_) +- Random Ray Misc Memory Error Fixes (`#3405 `_) +- added type hints to model file (`#3399 `_) +- Apply resolve paths to path values in ``config`` (`#3400 `_) +- Fixing an incorrect computation of CDF of bremsstrahlung photons (`#3396 `_) +- Fix weight modification for uniform source sampling (`#3395 `_) +- Updates to VTK data checks (`#3371 `_) +- Map Compton subshell data to atomic relaxation data (`#3392 `_) +- Skip atomic relaxation if binding energy is larger than photon energy (`#3391 `_) +- Fix extremely large yields from Bremsstrahlung (`#3386 `_) +- corrected tally name in D1S example (`#3383 `_) +- Install MCPL using same build type as OpenMC in CI (`#3388 `_) +- using reduce chain level to remove need for reduce chain (`#3377 `_) +- Fix negative distances from bins_crossed for CylindricalMesh (`#3370 `_) +- Add check for equal value bins in an EnergyFilter (`#3372 `_) +- Fix for Issue Loading MGXS Data Files with LLVM 20 or Newer (`#3368 `_) +- Report plot ID instead of index for unsupported plot types in random ray mode (`#3361 `_) +- Handle Missing Tags in Versioning by Setting Default to 0 (`#3359 `_) +- added kg units to doc string in results class (`#3358 `_) diff --git a/docs/source/releasenotes/index.rst b/docs/source/releasenotes/index.rst index d24b83f9e..1292599ba 100644 --- a/docs/source/releasenotes/index.rst +++ b/docs/source/releasenotes/index.rst @@ -7,6 +7,7 @@ Release Notes .. toctree:: :maxdepth: 1 + 0.15.3 0.15.2 0.15.1 0.15.0 diff --git a/docs/source/usersguide/variance_reduction.rst b/docs/source/usersguide/variance_reduction.rst index 5c2485158..93321e107 100644 --- a/docs/source/usersguide/variance_reduction.rst +++ b/docs/source/usersguide/variance_reduction.rst @@ -51,7 +51,7 @@ With the :class:`~openmc.WeightWindowGenerator` instance added to the :attr:`~openmc.Settings`, the rest of the problem can be defined as normal. When running, note that the second iteration and beyond may be several orders of magnitude slower than the first. As the weight windows are applied in each -iteration, particles may be agressively split, resulting in a large number of +iteration, particles may be aggressively split, resulting in a large number of secondary (split) particles being generated per initial source particle. This is not necessarily a bad thing, as the split particles are much more efficient at exploring low flux regions of phase space as compared to initial particles. @@ -161,7 +161,7 @@ solver, the Python input just needs to load the h5 file:: settings.weight_window_checkpoints = {'collision': True, 'surface': True} settings.survival_biasing = False - settings.weight_windows = openmc.WeightWindowsList.from_hdf5('weight_windows.h5') + settings.weight_windows_file = "weight_windows.h5" settings.weight_windows_on = True The :class:`~openmc.WeightWindowGenerator` instance is not needed to load an diff --git a/openmc/model/model.py b/openmc/model/model.py index 10e5cbfc7..48ae9f0b9 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -2112,8 +2112,12 @@ class Model: groups : openmc.mgxs.EnergyGroups or str, optional Energy group structure for the MGXS or the name of the group structure (based on keys from openmc.mgxs.GROUP_STRUCTURES). + nparticles : int, optional + Number of particles to simulate per batch when generating MGXS. + overwrite_mgxs_library : bool, optional + Whether to overwrite an existing MGXS library file. mgxs_path : str, optional - Filename of the mgxs.h5 library file. + Path to the mgxs.h5 library file. correction : str, optional Transport correction to apply to the MGXS. Options are None and "P0". diff --git a/openmc/settings.py b/openmc/settings.py index 43c1fe069..289fb35b7 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -1278,9 +1278,12 @@ class Settings: return self._weight_windows_file @weight_windows_file.setter - def weight_windows_file(self, value: PathLike): - cv.check_type('weight windows file', value, PathLike) - self._weight_windows_file = input_path(value) + def weight_windows_file(self, value: PathLike | None): + if value is None: + self._weight_windows_file = None + else: + cv.check_type('weight windows file', value, PathLike) + self._weight_windows_file = input_path(value) @property def weight_window_generators(self) -> list[WeightWindowGenerator]: From dcb7443711a5ae761bcc686fdfa6703937d51ab1 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Thu, 27 Nov 2025 17:03:38 +0100 Subject: [PATCH 459/671] added test to check dagmc name is in xml (#3657) --- openmc/dagmc.py | 2 ++ tests/unit_tests/dagmc/test_model.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/openmc/dagmc.py b/openmc/dagmc.py index d1265be26..3cb48ddf1 100644 --- a/openmc/dagmc.py +++ b/openmc/dagmc.py @@ -302,6 +302,8 @@ class DAGMCUniverse(openmc.UniverseBase): dagmc_element = ET.Element('dagmc_universe') dagmc_element.set('id', str(self.id)) + if self.name: + dagmc_element.set('name', self.name) if self.auto_geom_ids: dagmc_element.set('auto_geom_ids', 'true') if self.auto_mat_ids: diff --git a/tests/unit_tests/dagmc/test_model.py b/tests/unit_tests/dagmc/test_model.py index 0de4f6092..0917f5b23 100644 --- a/tests/unit_tests/dagmc/test_model.py +++ b/tests/unit_tests/dagmc/test_model.py @@ -31,7 +31,7 @@ def model(request): p = Path(request.fspath).parent / "dagmc.h5m" - daguniv = openmc.DAGMCUniverse(p, auto_geom_ids=True) + daguniv = openmc.DAGMCUniverse(p, name='simple-dagmc', auto_geom_ids=True) lattice = openmc.RectLattice() lattice.dimension = [2, 2] @@ -232,6 +232,7 @@ def test_dagmc_xml(model): dagmc_ele = root.find('dagmc_universe') assert dagmc_ele.get('id') == str(dag_univ.id) + assert dagmc_ele.get('name') == str(dag_univ.name) assert dagmc_ele.get('filename') == str(dag_univ.filename) assert dagmc_ele.get('auto_geom_ids') == str(dag_univ.auto_geom_ids).lower() From 7ab92ec142470da4cc1f685f0434777772c5343a Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 28 Nov 2025 16:39:08 +0100 Subject: [PATCH 460/671] Fix typo in description of distance_inactive for random ray setting (#3663) --- openmc/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmc/settings.py b/openmc/settings.py index 289fb35b7..76e191c5e 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -182,7 +182,7 @@ class Settings: Options for configuring the random ray solver. Acceptable keys are: :distance_inactive: - Indicates the total active distance in [cm] a ray should travel + Indicates the total inactive distance in [cm] a ray should travel :distance_active: Indicates the total active distance in [cm] a ray should travel :ray_source: From ef22558f4a037585c4fdd96a1a64dd2781a72c37 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Sat, 29 Nov 2025 17:28:18 +0200 Subject: [PATCH 461/671] fix a bug in borated_water temperature assignment (#3662) --- openmc/model/funcs.py | 9 +++------ tests/unit_tests/test_material.py | 1 + 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/openmc/model/funcs.py b/openmc/model/funcs.py index 41aa920ea..e076b080a 100644 --- a/openmc/model/funcs.py +++ b/openmc/model/funcs.py @@ -39,8 +39,8 @@ def borated_water(boron_ppm, temperature=293., pressure=0.1013, temp_unit='K', press_unit : {'MPa', 'psi'} The units used for the `pressure` argument. density : float - Water density in [g / cm^3]. If specified, this value overrides the - temperature and pressure arguments. + Water density in [g / cm^3]. If specified, this value overrides + the value that is computed from the temperature and pressure arguments. **kwargs All keyword arguments are passed to the created Material object. @@ -95,10 +95,7 @@ def borated_water(boron_ppm, temperature=293., pressure=0.1013, temp_unit='K', frac_B = boron_ppm * 1e-6 / M_B # Build the material. - if density is None: - out = openmc.Material(temperature=T, **kwargs) - else: - out = openmc.Material(**kwargs) + out = openmc.Material(temperature=T, **kwargs) out.add_element('H', frac_H, 'ao') out.add_element('O', frac_O, 'ao') out.add_element('B', frac_B, 'ao') diff --git a/tests/unit_tests/test_material.py b/tests/unit_tests/test_material.py index 2e3724272..ce58339d7 100644 --- a/tests/unit_tests/test_material.py +++ b/tests/unit_tests/test_material.py @@ -481,6 +481,7 @@ def test_borated_water(): # Test the density override m = openmc.model.borated_water(975, 566.5, 15.51, density=0.9) assert m.density == pytest.approx(0.9, 1e-3) + assert m.temperature == pytest.approx(566.5) def test_from_xml(run_in_tmpdir): From 9e9ab848337bc1ba208c72ecbb3fb548f19bb2a1 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 2 Dec 2025 10:11:56 -0600 Subject: [PATCH 462/671] Update C++/CMake policy based on Ubuntu 22.04 (#3666) --- docs/source/devguide/policies.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/devguide/policies.rst b/docs/source/devguide/policies.rst index 2cf319987..3644ae822 100644 --- a/docs/source/devguide/policies.rst +++ b/docs/source/devguide/policies.rst @@ -21,8 +21,8 @@ C++ code in OpenMC must conform to the most recent C++ standard that is fully supported in the `version of the gcc compiler `_ that is distributed with the oldest version of Ubuntu that is still within its `standard support period -`_. Ubuntu 20.04 LTS will be supported -through April 2025 and is distributed with gcc 9.3.0, which fully supports the +`_. Ubuntu 22.04 LTS will be supported +through April 2027 and is distributed with gcc 11.4.0, which fully supports the C++17 standard. -------------------- @@ -31,5 +31,5 @@ CMake Version Policy Similar to the C++ standard policy, the minimum supported version of CMake corresponds to whatever version is distributed with the oldest version of Ubuntu -still within its standard support period. Ubuntu 20.04 LTS is distributed with -CMake 3.16. +still within its standard support period. Ubuntu 22.04 LTS is distributed with +CMake 3.22. From 10706510bfb7e89555bb89bccc849bfde36be409 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Tue, 2 Dec 2025 22:00:32 -0600 Subject: [PATCH 463/671] Random Ray Eigenvalue Flux Normalization Change (#3595) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../openmc/random_ray/flat_source_domain.h | 3 + src/eigenvalue.cpp | 10 + src/random_ray/flat_source_domain.cpp | 32 +- src/random_ray/linear_source_domain.cpp | 6 +- .../random_ray_adjoint_k_eff/results_true.dat | 244 +++++++-------- .../infinite_medium/results_true.dat | 2 +- .../material_wise/results_true.dat | 2 +- .../stochastic_slab/results_true.dat | 2 +- .../random_ray_auto_convert/test.py | 2 +- .../results_true.dat | 2 +- .../random_ray_diagonal_stabilization/test.py | 2 +- .../results_true.dat | 280 ++++++++--------- .../random_ray_k_eff/results_true.dat | 282 +++++++++--------- .../random_ray_k_eff_mesh/results_true.dat | 280 ++++++++--------- .../random_ray_linear/linear/results_true.dat | 280 ++++++++--------- .../linear_xy/results_true.dat | 280 ++++++++--------- 16 files changed, 874 insertions(+), 835 deletions(-) diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index d4e802734..4df4e5d8d 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -170,6 +170,9 @@ protected: simulation_volume_; // Total physical volume of the simulation domain, as // defined by the 3D box of the random ray source + double + fission_rate_; // The system's fission rate (per cm^3), in eigenvalue mode + // 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 diff --git a/src/eigenvalue.cpp b/src/eigenvalue.cpp index a2120a006..8412cbd3b 100644 --- a/src/eigenvalue.cpp +++ b/src/eigenvalue.cpp @@ -403,6 +403,16 @@ void calculate_average_keff() t_value * std::sqrt( (simulation::k_sum[1] / n - std::pow(simulation::keff, 2)) / (n - 1)); + + // In some cases (such as an infinite medium problem), random ray + // may estimate k exactly and in an unvarying manner between iterations. + // In this case, the floating point roundoff between the division and the + // power operations may cause an extremely small negative value to occur + // inside the sqrt operation, leading to NaN. If this occurs, we check for + // it and set the std dev to zero. + if (!std::isfinite(simulation::keff_std)) { + simulation::keff_std = 0.0; + } } } } diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 1bf27e1ed..ec14795dd 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -124,7 +124,9 @@ void FlatSourceDomain::update_single_neutron_source(SourceRegionHandle& srh) double chi = chi_[material * negroups_ + g_out]; scatter_source += sigma_s * scalar_flux; - fission_source += nu_sigma_f * scalar_flux * chi; + if (settings::create_fission_neutrons) { + fission_source += nu_sigma_f * scalar_flux * chi; + } } srh.source(g_out) = (scatter_source + fission_source * inverse_k_eff) / sigma_t; @@ -369,6 +371,7 @@ void FlatSourceDomain::compute_k_eff() // Adds entropy value to shared entropy vector in openmc namespace. simulation::entropy.push_back(H); + fission_rate_ = fission_rate_new; k_eff_ = k_eff_new; } @@ -519,12 +522,33 @@ void FlatSourceDomain::reset_tally_volumes() // simulation double FlatSourceDomain::compute_fixed_source_normalization_factor() const { - // If we are not in fixed source mode, then there are no external sources - // so no normalization is needed. - if (settings::run_mode != RunMode::FIXED_SOURCE || adjoint_) { + // Eigenvalue mode normalization + if (settings::run_mode == RunMode::EIGENVALUE) { + // Normalize fluxes by total number of fission neutrons produced. This + // ensures consistent scaling of the eigenvector such that its magnitude is + // comparable to the eigenvector produced by the Monte Carlo solver. + // Multiplying by the eigenvalue is unintuitive, but it is necessary. + // If the eigenvalue is 1.2, per starting source neutron, you will + // generate 1.2 neutrons. Thus if we normalize to generating only ONE + // neutron in total for the whole domain, then we don't actually have enough + // flux to generate the required 1.2 neutrons. We only know the flux + // required to generate 1 neutron (which would have required less than one + // starting neutron). Thus, you have to scale the flux up by the eigenvalue + // such that 1.2 neutrons are generated, so as to be consistent with the + // bookkeeping in MC which is all done per starting source neutron (not per + // neutron produced). + return k_eff_ / (fission_rate_ * simulation_volume_); + } + + // If we are in adjoint mode of a fixed source problem, the external + // source is already normalized, such that all resulting fluxes are + // also normalized. + if (adjoint_) { return 1.0; } + // Fixed source mode normalization + // Step 1 is to sum over all source regions and energy groups to get the // total external source strength in the simulation. double simulation_external_source_strength = 0.0; diff --git a/src/random_ray/linear_source_domain.cpp b/src/random_ray/linear_source_domain.cpp index e1ad68e3d..47ffbb727 100644 --- a/src/random_ray/linear_source_domain.cpp +++ b/src/random_ray/linear_source_domain.cpp @@ -68,9 +68,11 @@ void LinearSourceDomain::update_single_neutron_source(SourceRegionHandle& srh) // Compute source terms for flat and linear components of the flux scatter_flat += sigma_s * flux_flat; - fission_flat += nu_sigma_f * flux_flat * chi; scatter_linear += sigma_s * flux_linear; - fission_linear += nu_sigma_f * flux_linear * chi; + if (settings::create_fission_neutrons) { + fission_flat += nu_sigma_f * flux_flat * chi; + fission_linear += nu_sigma_f * flux_linear * chi; + } } // Compute the flat source term diff --git a/tests/regression_tests/random_ray_adjoint_k_eff/results_true.dat b/tests/regression_tests/random_ray_adjoint_k_eff/results_true.dat index 657c841b5..dfef53cd2 100644 --- a/tests/regression_tests/random_ray_adjoint_k_eff/results_true.dat +++ b/tests/regression_tests/random_ray_adjoint_k_eff/results_true.dat @@ -1,171 +1,171 @@ k-combined: 1.006640E+00 1.812969E-03 tally 1: -6.684129E+00 -8.939821E+00 -2.685967E+00 -1.443592E+00 +1.208044E+00 +2.920182E-01 +4.854426E-01 +4.715453E-02 0.000000E+00 0.000000E+00 -6.358774E+00 -8.091444E+00 -9.687217E-01 -1.878029E-01 +1.149242E+00 +2.643067E-01 +1.750801E-01 +6.134563E-03 0.000000E+00 0.000000E+00 -5.963160E+00 -7.117108E+00 -1.932332E-01 -7.473914E-03 +1.077743E+00 +2.324814E-01 +3.492371E-02 +2.441363E-04 0.000000E+00 0.000000E+00 -5.137593E+00 -5.283310E+00 -1.714616E-01 -5.884834E-03 -1.086218E-06 -2.361752E-13 -4.857253E+00 -4.719856E+00 -5.689580E-02 -6.476286E-04 -2.989356E-03 -1.787808E-06 -4.830516E+00 -4.666801E+00 -7.203015E-03 -1.037676E-05 -3.620020E+00 -2.620927E+00 -5.161382E+00 -5.328124E+00 -6.786255E-02 -9.210763E-04 -5.531943E+00 -6.120553E+00 -5.414034E+00 -5.864661E+00 +9.285362E-01 +1.725808E-01 +3.098889E-02 +1.922297E-04 +1.963161E-07 +7.714727E-15 +8.778641E-01 +1.541719E-01 +1.028293E-02 +2.115448E-05 +5.402741E-04 +5.839789E-08 +8.730274E-01 +1.524358E-01 +1.301813E-03 +3.389450E-07 +6.542525E-01 +8.560964E-02 +9.328247E-01 +1.740366E-01 +1.226491E-02 +3.008584E-05 +9.997969E-01 +1.999204E-01 +9.784958E-01 +1.915688E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.632338E+00 -6.347626E+00 +1.017952E+00 +2.073461E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.682608E+00 -6.462382E+00 +1.027039E+00 +2.110955E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.310716E+00 -5.645180E+00 +9.598240E-01 +1.844004E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -4.945409E+00 -4.893171E+00 +8.937969E-01 +1.598332E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -4.842688E+00 -4.690352E+00 +8.752275E-01 +1.532052E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.117198E+00 -5.237280E+00 +9.248400E-01 +1.710699E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -6.938711E+00 -9.633345E+00 -2.835258E+00 -1.608212E+00 +1.254054E+00 +3.146708E-01 +5.124223E-01 +5.253093E-02 0.000000E+00 0.000000E+00 -6.549505E+00 -8.584036E+00 -1.015138E+00 -2.061993E-01 +1.183712E+00 +2.803961E-01 +1.834683E-01 +6.735381E-03 0.000000E+00 0.000000E+00 -6.050651E+00 -7.327711E+00 -1.992816E-01 -7.948424E-03 +1.093555E+00 +2.393604E-01 +3.601678E-02 +2.596341E-04 0.000000E+00 0.000000E+00 -5.113981E+00 -5.234801E+00 -1.732323E-01 -6.006619E-03 -1.097435E-06 -2.410627E-13 -4.837033E+00 -4.680541E+00 -5.760042E-02 -6.637112E-04 -3.026377E-03 -1.832205E-06 -4.827049E+00 -4.660105E+00 -7.319913E-03 -1.071647E-05 -3.678770E+00 -2.706730E+00 -5.175337E+00 -5.356957E+00 -6.923046E-02 -9.586177E-04 -5.643451E+00 -6.370016E+00 -6.693323E+00 -8.964322E+00 -2.753307E+00 -1.516683E+00 +9.242694E-01 +1.709967E-01 +3.130894E-02 +1.962084E-04 +1.983437E-07 +7.874400E-15 +8.742100E-01 +1.528879E-01 +1.041026E-02 +2.167970E-05 +5.469644E-04 +5.984780E-08 +8.724009E-01 +1.522171E-01 +1.322938E-03 +3.500386E-07 +6.648691E-01 +8.841161E-02 +9.353464E-01 +1.749781E-01 +1.251209E-02 +3.131171E-05 +1.019947E+00 +2.080664E-01 +1.209708E+00 +2.928214E-01 +4.976146E-01 +4.954258E-02 0.000000E+00 0.000000E+00 -6.358384E+00 -8.090233E+00 -9.912008E-01 -1.965868E-01 +1.149174E+00 +2.642694E-01 +1.791431E-01 +6.421530E-03 0.000000E+00 0.000000E+00 -5.957484E+00 -7.103246E+00 -1.974033E-01 -7.798286E-03 +1.076718E+00 +2.320295E-01 +3.567737E-02 +2.547314E-04 0.000000E+00 0.000000E+00 -5.130744E+00 -5.268844E+00 -1.749233E-01 -6.123348E-03 -1.108148E-06 -2.457474E-13 -4.857340E+00 -4.720019E+00 -5.816659E-02 -6.768049E-04 -3.056125E-03 -1.868351E-06 -4.830629E+00 -4.667018E+00 -7.366289E-03 -1.085264E-05 -3.702077E+00 -2.741125E+00 -5.164864E+00 -5.335279E+00 -6.947917E-02 -9.655086E-04 -5.663725E+00 -6.415806E+00 +9.272977E-01 +1.721077E-01 +3.161443E-02 +2.000179E-04 +2.002789E-07 +8.027290E-15 +8.778794E-01 +1.541769E-01 +1.051257E-02 +2.210729E-05 +5.523400E-04 +6.102818E-08 +8.730477E-01 +1.524429E-01 +1.331320E-03 +3.544869E-07 +6.690816E-01 +8.953516E-02 +9.334544E-01 +1.742707E-01 +1.255707E-02 +3.153710E-05 +1.023613E+00 +2.095641E-01 diff --git a/tests/regression_tests/random_ray_auto_convert/infinite_medium/results_true.dat b/tests/regression_tests/random_ray_auto_convert/infinite_medium/results_true.dat index c7584ab64..ee396fa74 100644 --- a/tests/regression_tests/random_ray_auto_convert/infinite_medium/results_true.dat +++ b/tests/regression_tests/random_ray_auto_convert/infinite_medium/results_true.dat @@ -1,2 +1,2 @@ k-combined: -7.797820E-01 1.054725E-02 +7.797252E-01 1.055731E-02 diff --git a/tests/regression_tests/random_ray_auto_convert/material_wise/results_true.dat b/tests/regression_tests/random_ray_auto_convert/material_wise/results_true.dat index d544a27df..3bade01e0 100644 --- a/tests/regression_tests/random_ray_auto_convert/material_wise/results_true.dat +++ b/tests/regression_tests/random_ray_auto_convert/material_wise/results_true.dat @@ -1,2 +1,2 @@ k-combined: -7.356667E-01 6.637270E-03 +7.372542E-01 6.967831E-03 diff --git a/tests/regression_tests/random_ray_auto_convert/stochastic_slab/results_true.dat b/tests/regression_tests/random_ray_auto_convert/stochastic_slab/results_true.dat index 75a10a224..86e08a710 100644 --- a/tests/regression_tests/random_ray_auto_convert/stochastic_slab/results_true.dat +++ b/tests/regression_tests/random_ray_auto_convert/stochastic_slab/results_true.dat @@ -1,2 +1,2 @@ k-combined: -7.551716E-01 8.117378E-03 +7.496641E-01 8.282032E-03 diff --git a/tests/regression_tests/random_ray_auto_convert/test.py b/tests/regression_tests/random_ray_auto_convert/test.py index fa7f2f17f..99a931dce 100644 --- a/tests/regression_tests/random_ray_auto_convert/test.py +++ b/tests/regression_tests/random_ray_auto_convert/test.py @@ -27,7 +27,7 @@ def test_random_ray_auto_convert(method): # Convert to a multi-group model model.convert_to_multigroup( - method=method, groups='CASMO-2', nparticles=30, + method=method, groups='CASMO-2', nparticles=100, overwrite_mgxs_library=False, mgxs_path="mgxs.h5" ) diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/results_true.dat b/tests/regression_tests/random_ray_diagonal_stabilization/results_true.dat index f27ad46b4..034d7f7c6 100644 --- a/tests/regression_tests/random_ray_diagonal_stabilization/results_true.dat +++ b/tests/regression_tests/random_ray_diagonal_stabilization/results_true.dat @@ -1,2 +1,2 @@ k-combined: -7.201808E-01 1.506596E-02 +7.134473E-01 1.422763E-02 diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/test.py b/tests/regression_tests/random_ray_diagonal_stabilization/test.py index c7a1c9f7c..8d36e1d25 100644 --- a/tests/regression_tests/random_ray_diagonal_stabilization/test.py +++ b/tests/regression_tests/random_ray_diagonal_stabilization/test.py @@ -23,7 +23,7 @@ def test_random_ray_diagonal_stabilization(): # MGXS data with some negatives on the diagonal, in order # to trigger diagonal correction. model.convert_to_multigroup( - method='material_wise', groups='CASMO-70', nparticles=30, + method='material_wise', groups='CASMO-70', nparticles=13, overwrite_mgxs_library=True, mgxs_path="mgxs.h5", correction='P0' ) diff --git a/tests/regression_tests/random_ray_halton_samples/results_true.dat b/tests/regression_tests/random_ray_halton_samples/results_true.dat index b62398935..256f8a744 100644 --- a/tests/regression_tests/random_ray_halton_samples/results_true.dat +++ b/tests/regression_tests/random_ray_halton_samples/results_true.dat @@ -1,171 +1,171 @@ k-combined: 8.388051E-01 7.383265E-03 tally 1: -5.033308E+00 -5.072162E+00 -1.917335E+00 -7.360725E-01 -4.666410E+00 -4.360038E+00 -2.851812E+00 -1.629362E+00 -4.365590E-01 -3.818884E-02 -1.062497E+00 -2.262071E-01 -1.697621E+00 -5.829333E-01 -5.639912E-02 -6.427568E-04 -1.372642E-01 -3.807294E-03 -2.376683E+00 -1.151027E+00 -8.060903E-02 -1.323179E-03 -1.961862E-01 -7.837693E-03 -7.145452E+00 -1.037540E+01 -8.551803E-02 -1.486269E-03 -2.081363E-01 -8.803955E-03 -2.053205E+01 -8.469498E+01 -3.235618E-02 -2.102891E-04 -8.006311E-02 -1.287559E-03 -1.326545E+01 -3.519484E+01 -1.867471E-01 -6.975133E-03 -5.194275E-01 -5.396284E-02 -7.558115E+00 -1.142535E+01 +1.065839E+00 +2.274384E-01 +4.060094E-01 +3.300592E-02 +9.881457E-01 +1.955066E-01 +6.038893E-01 +7.306038E-02 +9.244411E-02 +1.712380E-03 +2.249905E-01 +1.014308E-02 +3.594916E-01 +2.614145E-02 +1.194318E-02 +2.882412E-05 +2.906731E-02 +1.707363E-04 +5.032954E-01 +5.161897E-02 +1.707007E-02 +5.933921E-05 +4.154514E-02 +3.514889E-04 +1.513147E+00 +4.652938E-01 +1.810962E-02 +6.665306E-05 +4.407572E-02 +3.948212E-04 +4.347893E+00 +3.798064E+00 +6.851785E-03 +9.430195E-06 +1.695426E-02 +5.773923E-05 +2.809071E+00 +1.578195E+00 +3.954523E-02 +3.127754E-04 +1.099931E-01 +2.419775E-03 +1.600493E+00 +5.123291E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -3.386211E+00 -2.294414E+00 +7.170555E-01 +1.028835E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.827274E+00 -6.782305E-01 +3.869491E-01 +3.041553E-02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.702858E+00 -1.489752E+00 +5.723683E-01 +6.680970E-02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -7.475537E+00 -1.133971E+01 +1.583046E+00 +5.085372E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.828685E+01 -6.719606E+01 +3.872447E+00 +3.013344E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.143600E+01 -2.615734E+01 +2.421670E+00 +1.172938E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -4.590713E+00 -4.224107E+00 -1.705847E+00 -5.831967E-01 -4.151691E+00 -3.454497E+00 -2.730853E+00 -1.495413E+00 -4.072633E-01 -3.325944E-02 -9.911973E-01 -1.970084E-01 -1.664732E+00 -5.598668E-01 -5.385645E-02 -5.858353E-04 -1.310758E-01 -3.470126E-03 -2.312239E+00 -1.088485E+00 -7.662778E-02 -1.195422E-03 -1.864967E-01 -7.080943E-03 -7.105766E+00 -1.025960E+01 -8.287512E-02 -1.396474E-03 -2.017039E-01 -8.272053E-03 -2.099024E+01 -8.854251E+01 -3.191885E-02 -2.048368E-04 -7.898095E-02 -1.254175E-03 -1.355820E+01 -3.676862E+01 -1.815102E-01 -6.590727E-03 -5.048614E-01 -5.098890E-02 -5.093659E+00 -5.192360E+00 -1.874632E+00 -7.031793E-01 -4.562478E+00 -4.165199E+00 -2.870214E+00 -1.650126E+00 -4.244069E-01 -3.608745E-02 -1.032921E+00 -2.137598E-01 -1.703400E+00 -5.873029E-01 -5.464557E-02 -6.042855E-04 -1.329964E-01 -3.579413E-03 -2.389118E+00 -1.163674E+00 -7.832237E-02 -1.251254E-03 -1.906210E-01 -7.411654E-03 -7.162707E+00 -1.042515E+01 -8.273831E-02 -1.391799E-03 -2.013709E-01 -8.244359E-03 -2.043145E+01 -8.383557E+01 -3.096158E-02 -1.924116E-04 -7.661226E-02 -1.178098E-03 -1.314148E+01 -3.454143E+01 -1.771732E-01 -6.279887E-03 -4.927984E-01 -4.858410E-02 +9.721128E-01 +1.894086E-01 +3.612242E-01 +2.615053E-02 +8.791475E-01 +1.548996E-01 +5.782742E-01 +6.705354E-02 +8.624040E-02 +1.491336E-03 +2.098919E-01 +8.833753E-03 +3.525265E-01 +2.510689E-02 +1.140473E-02 +2.627142E-05 +2.775683E-02 +1.556157E-04 +4.896481E-01 +4.881407E-02 +1.622698E-02 +5.360976E-05 +3.949322E-02 +3.175511E-04 +1.504743E+00 +4.601003E-01 +1.754995E-02 +6.262618E-05 +4.271358E-02 +3.709679E-04 +4.444922E+00 +3.970609E+00 +6.759184E-03 +9.185746E-06 +1.672513E-02 +5.624252E-05 +2.871068E+00 +1.648778E+00 +3.843643E-02 +2.955428E-04 +1.069090E-01 +2.286455E-03 +1.078620E+00 +2.328291E-01 +3.969671E-01 +3.153110E-02 +9.661384E-01 +1.867708E-01 +6.077864E-01 +7.399164E-02 +8.987086E-02 +1.618157E-03 +2.187277E-01 +9.584966E-03 +3.607158E-01 +2.633746E-02 +1.157185E-02 +2.709895E-05 +2.816358E-02 +1.605174E-04 +5.059289E-01 +5.218618E-02 +1.658585E-02 +5.611375E-05 +4.036663E-02 +3.323832E-04 +1.516801E+00 +4.675245E-01 +1.752096E-02 +6.241641E-05 +4.264304E-02 +3.697253E-04 +4.326588E+00 +3.759517E+00 +6.556454E-03 +8.628460E-06 +1.622349E-02 +5.283037E-05 +2.782815E+00 +1.548888E+00 +3.751781E-02 +2.815972E-04 +1.043539E-01 +2.178566E-03 diff --git a/tests/regression_tests/random_ray_k_eff/results_true.dat b/tests/regression_tests/random_ray_k_eff/results_true.dat index 37eca77f3..ace18df8c 100644 --- a/tests/regression_tests/random_ray_k_eff/results_true.dat +++ b/tests/regression_tests/random_ray_k_eff/results_true.dat @@ -1,171 +1,171 @@ k-combined: -8.400321E-01 8.023358E-03 +8.400321E-01 8.023357E-03 tally 1: -5.086559E+00 -5.180935E+00 -1.885166E+00 -7.115503E-01 -4.588116E+00 -4.214784E+00 -2.860400E+00 -1.639328E+00 -4.245221E-01 -3.610929E-02 -1.033202E+00 -2.138892E-01 -1.692631E+00 -5.793966E-01 -5.445818E-02 -5.996625E-04 -1.325403E-01 -3.552030E-03 -2.372248E+00 -1.146944E+00 -7.808142E-02 -1.242278E-03 -1.900346E-01 -7.358491E-03 -7.134949E+00 -1.034824E+01 -8.272647E-02 -1.391871E-03 -2.013421E-01 -8.244788E-03 -2.043539E+01 -8.389902E+01 -3.099367E-02 -1.930673E-04 -7.669167E-02 -1.182113E-03 -1.313212E+01 -3.449537E+01 -1.764293E-01 -6.225586E-03 -4.907293E-01 -4.816400E-02 -7.567715E+00 -1.145439E+01 +1.075769E+00 +2.317354E-01 +3.986991E-01 +3.182682E-02 +9.703538E-01 +1.885224E-01 +6.049423E-01 +7.331941E-02 +8.978161E-02 +1.614997E-03 +2.185105E-01 +9.566247E-03 +3.579852E-01 +2.591721E-02 +1.151770E-02 +2.682363E-05 +2.803176E-02 +1.588866E-04 +5.017326E-01 +5.130808E-02 +1.651428E-02 +5.557274E-05 +4.019245E-02 +3.291786E-04 +1.509054E+00 +4.629325E-01 +1.749679E-02 +6.226587E-05 +4.258421E-02 +3.688336E-04 +4.322054E+00 +3.753085E+00 +6.555113E-03 +8.636537E-06 +1.622017E-02 +5.287982E-05 +2.777351E+00 +1.542942E+00 +3.731363E-02 +2.784662E-04 +1.037860E-01 +2.154343E-03 +1.600522E+00 +5.123477E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -3.383194E+00 -2.290468E+00 +7.155116E-01 +1.024445E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.819672E+00 -6.726158E-01 +3.848568E-01 +3.008769E-02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.693683E+00 -1.480961E+00 +5.697160E-01 +6.624996E-02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -7.453758E+00 -1.128171E+01 +1.576480E+00 +5.046890E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.823561E+01 -6.681652E+01 +3.856827E+00 +2.989000E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.137517E+01 -2.588512E+01 +2.405807E+00 +1.157889E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -4.601916E+00 -4.242624E+00 -1.719723E+00 -5.923465E-01 -4.185462E+00 -3.508695E+00 -2.730305E+00 -1.494324E+00 -4.108214E-01 -3.383938E-02 -9.998572E-01 -2.004436E-01 -1.660852E+00 -5.570432E-01 -5.428709E-02 -5.947848E-04 -1.321239E-01 -3.523137E-03 -2.306069E+00 -1.082855E+00 -7.697031E-02 -1.206036E-03 -1.873303E-01 -7.143815E-03 -7.075194E+00 -1.017519E+01 -8.322052E-02 -1.408294E-03 -2.025446E-01 -8.342071E-03 -2.094832E+01 -8.816715E+01 -3.234739E-02 -2.101889E-04 -8.004135E-02 -1.286945E-03 -1.357413E+01 -3.685983E+01 -1.861680E-01 -6.934827E-03 -5.178169E-01 -5.365102E-02 -5.072149E+00 -5.151429E+00 -1.916643E+00 -7.358710E-01 -4.664726E+00 -4.358845E+00 -2.859464E+00 -1.638250E+00 -4.332944E-01 -3.763170E-02 -1.054552E+00 -2.229070E-01 -1.693008E+00 -5.796671E-01 -5.561096E-02 -6.247543E-04 -1.353459E-01 -3.700658E-03 -2.368860E+00 -1.143296E+00 -7.951819E-02 -1.286690E-03 -1.935314E-01 -7.621557E-03 -7.119587E+00 -1.030023E+01 -8.428175E-02 -1.442931E-03 -2.051274E-01 -8.547243E-03 -2.046758E+01 -8.418768E+01 -3.181946E-02 -2.034766E-04 -7.873502E-02 -1.245847E-03 -1.325834E+01 -3.515919E+01 -1.832838E-01 -6.720555E-03 -5.097947E-01 -5.199331E-02 +9.732717E-01 +1.897670E-01 +3.637108E-01 +2.649544E-02 +8.851992E-01 +1.569426E-01 +5.774286E-01 +6.683395E-02 +8.688408E-02 +1.513473E-03 +2.114585E-01 +8.964883E-03 +3.512640E-01 +2.491729E-02 +1.148149E-02 +2.660532E-05 +2.794365E-02 +1.575935E-04 +4.877362E-01 +4.844138E-02 +1.627932E-02 +5.395212E-05 +3.962062E-02 +3.195790E-04 +1.496416E+00 +4.551920E-01 +1.760130E-02 +6.300084E-05 +4.283856E-02 +3.731872E-04 +4.430519E+00 +3.943947E+00 +6.841363E-03 +9.402132E-06 +1.692847E-02 +5.756741E-05 +2.870816E+00 +1.648662E+00 +3.937267E-02 +3.101707E-04 +1.095131E-01 +2.399624E-03 +1.072714E+00 +2.304093E-01 +4.053505E-01 +3.291277E-02 +9.865420E-01 +1.949549E-01 +6.047420E-01 +7.327008E-02 +9.163611E-02 +1.683034E-03 +2.230240E-01 +9.969255E-03 +3.580639E-01 +2.592902E-02 +1.176143E-02 +2.794541E-05 +2.862497E-02 +1.655313E-04 +5.010143E-01 +5.114429E-02 +1.681803E-02 +5.755790E-05 +4.093173E-02 +3.409375E-04 +1.505803E+00 +4.607828E-01 +1.782566E-02 +6.454911E-05 +4.338462E-02 +3.823584E-04 +4.328879E+00 +3.766055E+00 +6.729800E-03 +9.102372E-06 +1.665242E-02 +5.573204E-05 +2.804070E+00 +1.572690E+00 +3.876396E-02 +3.006259E-04 +1.078200E-01 +2.325780E-03 diff --git a/tests/regression_tests/random_ray_k_eff_mesh/results_true.dat b/tests/regression_tests/random_ray_k_eff_mesh/results_true.dat index 83209044b..2ae8fad85 100644 --- a/tests/regression_tests/random_ray_k_eff_mesh/results_true.dat +++ b/tests/regression_tests/random_ray_k_eff_mesh/results_true.dat @@ -1,171 +1,171 @@ k-combined: 8.379203E-01 8.057199E-03 tally 1: -5.080172E+00 -5.167984E+00 -1.880341E+00 -7.079266E-01 -4.576373E+00 -4.193319E+00 -2.859914E+00 -1.638769E+00 -4.243332E-01 -3.607732E-02 -1.032742E+00 -2.136998E-01 -1.692643E+00 -5.794069E-01 -5.445214E-02 -5.995213E-04 -1.325256E-01 -3.551193E-03 -2.372336E+00 -1.147031E+00 -7.807378E-02 -1.242019E-03 -1.900160E-01 -7.356955E-03 -7.135636E+00 -1.035026E+01 -8.273225E-02 -1.392069E-03 -2.013562E-01 -8.245961E-03 -2.044034E+01 -8.394042E+01 -3.100485E-02 -1.932097E-04 -7.671934E-02 -1.182985E-03 -1.313652E+01 -3.451846E+01 -1.764978E-01 -6.230420E-03 -4.909196E-01 -4.820140E-02 -7.585874E+00 -1.150936E+01 +1.073897E+00 +2.309328E-01 +3.974859E-01 +3.163415E-02 +9.674011E-01 +1.873811E-01 +6.045463E-01 +7.322367E-02 +8.969819E-02 +1.612011E-03 +2.183075E-01 +9.548557E-03 +3.578121E-01 +2.589198E-02 +1.151076E-02 +2.679073E-05 +2.801489E-02 +1.586917E-04 +5.015038E-01 +5.126076E-02 +1.650453E-02 +5.550572E-05 +4.016872E-02 +3.287816E-04 +1.508456E+00 +4.625615E-01 +1.748939E-02 +6.221267E-05 +4.256620E-02 +3.685184E-04 +4.320984E+00 +3.751237E+00 +6.554265E-03 +8.634389E-06 +1.621807E-02 +5.286667E-05 +2.776930E+00 +1.542475E+00 +3.730995E-02 +2.784113E-04 +1.037757E-01 +2.153918E-03 +1.603583E+00 +5.143065E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -3.386790E+00 -2.295327E+00 +7.159242E-01 +1.025623E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.820058E+00 -6.729239E-01 +3.847488E-01 +3.007151E-02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.694013E+00 -1.481363E+00 +5.695050E-01 +6.620177E-02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -7.453818E+00 -1.128202E+01 +1.575716E+00 +5.042003E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.823642E+01 -6.682239E+01 +3.855109E+00 +2.986316E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.137903E+01 -2.590265E+01 +2.405453E+00 +1.157547E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -4.589529E+00 -4.219985E+00 -1.712446E+00 -5.873675E-01 -4.167750E+00 -3.479202E+00 -2.728285E+00 -1.492132E+00 -4.104063E-01 -3.377132E-02 -9.988468E-01 -2.000405E-01 -1.660506E+00 -5.567967E-01 -5.427391E-02 -5.944708E-04 -1.320918E-01 -3.521277E-03 -2.305889E+00 -1.082691E+00 -7.695793E-02 -1.205644E-03 -1.873002E-01 -7.141490E-03 -7.076637E+00 -1.017946E+01 -8.323139E-02 -1.408687E-03 -2.025710E-01 -8.344398E-03 -2.095897E+01 -8.825741E+01 -3.236513E-02 -2.104220E-04 -8.008525E-02 -1.288373E-03 -1.358006E+01 -3.689205E+01 -1.862562E-01 -6.941428E-03 -5.180621E-01 -5.370209E-02 -5.067386E+00 -5.141748E+00 -1.912704E+00 -7.328484E-01 -4.655140E+00 -4.340941E+00 -2.858992E+00 -1.637705E+00 -4.331050E-01 -3.759875E-02 -1.054091E+00 -2.227118E-01 -1.692974E+00 -5.796396E-01 -5.560487E-02 -6.246120E-04 -1.353311E-01 -3.699815E-03 -2.368737E+00 -1.143184E+00 -7.950086E-02 -1.286135E-03 -1.934892E-01 -7.618269E-03 -7.119767E+00 -1.030095E+01 -8.427628E-02 -1.442764E-03 -2.051141E-01 -8.546249E-03 -2.047651E+01 -8.426275E+01 -3.183786E-02 -2.037156E-04 -7.878057E-02 -1.247311E-03 -1.326415E+01 -3.519010E+01 -1.833688E-01 -6.726832E-03 -5.100312E-01 -5.204188E-02 +9.701818E-01 +1.885724E-01 +3.619962E-01 +2.624740E-02 +8.810264E-01 +1.554734E-01 +5.767221E-01 +6.667165E-02 +8.675429E-02 +1.508976E-03 +2.111426E-01 +8.938242E-03 +3.510185E-01 +2.488161E-02 +1.147307E-02 +2.656498E-05 +2.792316E-02 +1.573545E-04 +4.874578E-01 +4.838573E-02 +1.626869E-02 +5.388082E-05 +3.959473E-02 +3.191567E-04 +1.495984E+00 +4.549294E-01 +1.759493E-02 +6.295564E-05 +4.282306E-02 +3.729194E-04 +4.430601E+00 +3.944093E+00 +6.841763E-03 +9.403281E-06 +1.692946E-02 +5.757445E-05 +2.870670E+00 +1.648496E+00 +3.937215E-02 +3.101637E-04 +1.095116E-01 +2.399569E-03 +1.071187E+00 +2.297540E-01 +4.043214E-01 +3.274592E-02 +9.840375E-01 +1.939666E-01 +6.043491E-01 +7.317500E-02 +9.155169E-02 +1.679939E-03 +2.228185E-01 +9.950920E-03 +3.578809E-01 +2.590208E-02 +1.175437E-02 +2.791136E-05 +2.860779E-02 +1.653296E-04 +5.007414E-01 +5.108824E-02 +1.680608E-02 +5.747571E-05 +4.090264E-02 +3.404506E-04 +1.505100E+00 +4.603562E-01 +1.781573E-02 +6.447733E-05 +4.336044E-02 +3.819332E-04 +4.328647E+00 +3.765700E+00 +6.730396E-03 +9.104085E-06 +1.665389E-02 +5.574253E-05 +2.803934E+00 +1.572540E+00 +3.876306E-02 +3.006136E-04 +1.078175E-01 +2.325685E-03 diff --git a/tests/regression_tests/random_ray_linear/linear/results_true.dat b/tests/regression_tests/random_ray_linear/linear/results_true.dat index 4c0e14370..77d41f373 100644 --- a/tests/regression_tests/random_ray_linear/linear/results_true.dat +++ b/tests/regression_tests/random_ray_linear/linear/results_true.dat @@ -1,171 +1,171 @@ k-combined: 1.095967E+00 1.543581E-02 tally 1: -2.548108E+01 -3.269093E+01 -9.271804E+00 -4.327275E+00 -2.256572E+01 -2.563210E+01 -1.816107E+01 -1.653421E+01 -2.659203E+00 -3.544951E-01 -6.471969E+00 -2.099810E+00 -1.364193E+01 -9.308675E+00 -4.362828E-01 -9.521133E-03 -1.061825E+00 -5.639730E-02 -1.746102E+01 -1.524680E+01 -5.733016E-01 -1.643671E-02 -1.395301E+00 -9.736092E-02 -4.539598E+01 -1.030472E+02 -5.263055E-01 -1.385088E-02 -1.280938E+00 -8.204609E-02 -9.945736E+01 -4.946716E+02 -1.505228E-01 -1.133424E-03 -3.724582E-01 -6.939732E-03 -5.324914E+01 -1.418809E+02 -7.219589E-01 -2.614875E-02 -2.008092E+00 -2.022988E-01 -4.188246E+01 -8.843469E+01 +5.425537E+00 +1.482137E+00 +1.974189E+00 +1.961888E-01 +4.804781E+00 +1.162101E+00 +3.866915E+00 +7.496163E-01 +5.662059E-01 +1.607180E-02 +1.378032E+00 +9.519943E-02 +2.904666E+00 +4.220197E-01 +9.289413E-02 +4.316514E-04 +2.260857E-01 +2.556836E-03 +3.717829E+00 +6.912286E-01 +1.220682E-01 +7.451721E-04 +2.970897E-01 +4.413940E-03 +9.665773E+00 +4.671720E+00 +1.120617E-01 +6.279393E-04 +2.727390E-01 +3.719615E-03 +2.117656E+01 +2.242614E+01 +3.204951E-02 +5.138445E-05 +7.930426E-02 +3.146169E-04 +1.133784E+01 +6.432186E+00 +1.537206E-01 +1.185474E-03 +4.275660E-01 +9.171376E-03 +8.917756E+00 +4.009380E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.224363E+01 -2.481131E+01 +4.736166E+00 +1.124858E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.436097E+01 -1.031496E+01 +3.057755E+00 +4.676344E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.901248E+01 -1.807657E+01 +4.048156E+00 +8.195089E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -4.559337E+01 -1.039424E+02 +9.707791E+00 +4.712281E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -8.802992E+01 -3.874925E+02 +1.874344E+01 +1.756722E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -4.506602E+01 -1.016497E+02 +9.595520E+00 +4.608366E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.207833E+01 -2.455068E+01 -8.139772E+00 -3.337974E+00 -1.981058E+01 -1.977210E+01 -1.710407E+01 -1.466648E+01 -2.542287E+00 -3.240467E-01 -6.187418E+00 -1.919453E+00 -1.342690E+01 -9.017908E+00 -4.362263E-01 -9.518695E-03 -1.061688E+00 -5.638286E-02 -1.713924E+01 -1.469065E+01 -5.714028E-01 -1.632839E-02 -1.390680E+00 -9.671931E-02 -4.539193E+01 -1.030326E+02 -5.345254E-01 -1.428719E-02 -1.300944E+00 -8.463057E-02 -1.013270E+02 -5.134168E+02 -1.555517E-01 -1.210145E-03 -3.849017E-01 -7.409480E-03 -5.377836E+01 -1.446537E+02 -7.374053E-01 -2.722908E-02 -2.051056E+00 -2.106567E-01 -2.522726E+01 -3.202007E+01 -9.366659E+00 -4.412278E+00 -2.279657E+01 -2.613561E+01 -1.803368E+01 -1.629756E+01 -2.696490E+00 -3.643066E-01 -6.562716E+00 -2.157928E+00 -1.357447E+01 -9.216150E+00 -4.437305E-01 -9.847287E-03 -1.079951E+00 -5.832923E-02 -1.735848E+01 -1.506775E+01 -5.825173E-01 -1.696803E-02 -1.417731E+00 -1.005081E-01 -4.519297E+01 -1.021280E+02 -5.357712E-01 -1.435322E-02 -1.303976E+00 -8.502167E-02 -9.934508E+01 -4.935314E+02 -1.538761E-01 -1.184229E-03 -3.807556E-01 -7.250804E-03 -5.335839E+01 -1.424221E+02 -7.404823E-01 -2.747396E-02 -2.059614E+00 -2.125512E-01 +4.701002E+00 +1.113069E+00 +1.733152E+00 +1.513360E-01 +4.218145E+00 +8.964208E-01 +3.641849E+00 +6.649342E-01 +5.413112E-01 +1.469131E-02 +1.317443E+00 +8.702223E-02 +2.858878E+00 +4.088359E-01 +9.288202E-02 +4.315389E-04 +2.260562E-01 +2.556170E-03 +3.649312E+00 +6.660131E-01 +1.216639E-01 +7.402611E-04 +2.961057E-01 +4.384849E-03 +9.664912E+00 +4.671059E+00 +1.138118E-01 +6.477188E-04 +2.769985E-01 +3.836780E-03 +2.157464E+01 +2.327600E+01 +3.312018E-02 +5.486221E-05 +8.195357E-02 +3.359106E-04 +1.145052E+01 +6.557893E+00 +1.570084E-01 +1.234420E-03 +4.367109E-01 +9.550040E-03 +5.371474E+00 +1.451702E+00 +1.994382E+00 +2.000414E-01 +4.853928E+00 +1.184921E+00 +3.839778E+00 +7.388780E-01 +5.741437E-01 +1.651648E-02 +1.397351E+00 +9.783345E-02 +2.890296E+00 +4.178220E-01 +9.447974E-02 +4.464346E-04 +2.299448E-01 +2.644403E-03 +3.695989E+00 +6.831063E-01 +1.240303E-01 +7.692555E-04 +3.018649E-01 +4.556594E-03 +9.622545E+00 +4.630042E+00 +1.140770E-01 +6.507108E-04 +2.776440E-01 +3.854503E-03 +2.115266E+01 +2.237450E+01 +3.276343E-02 +5.368742E-05 +8.107083E-02 +3.287176E-04 +1.136109E+01 +6.456700E+00 +1.576636E-01 +1.245524E-03 +4.385334E-01 +9.635950E-03 diff --git a/tests/regression_tests/random_ray_linear/linear_xy/results_true.dat b/tests/regression_tests/random_ray_linear/linear_xy/results_true.dat index abfd03c06..052608b42 100644 --- a/tests/regression_tests/random_ray_linear/linear_xy/results_true.dat +++ b/tests/regression_tests/random_ray_linear/linear_xy/results_true.dat @@ -1,171 +1,171 @@ k-combined: 1.104727E+00 1.593303E-02 tally 1: -2.566934E+01 -3.317503E+01 -9.417202E+00 -4.465518E+00 -2.291958E+01 -2.645097E+01 -1.823903E+01 -1.667438E+01 -2.679931E+00 -3.600420E-01 -6.522415E+00 -2.132667E+00 -1.365623E+01 -9.327448E+00 -4.370682E-01 -9.554968E-03 -1.063736E+00 -5.659772E-02 -1.750634E+01 -1.532609E+01 -5.762870E-01 -1.660889E-02 -1.402567E+00 -9.838082E-02 -4.543609E+01 -1.032286E+02 -5.271287E-01 -1.389456E-02 -1.282941E+00 -8.230483E-02 -9.881678E+01 -4.882586E+02 -1.487616E-01 -1.106634E-03 -3.681003E-01 -6.775702E-03 -5.260781E+01 -1.384126E+02 -7.018594E-01 -2.464953E-02 -1.952187E+00 -1.907001E-01 -4.184779E+01 -8.826530E+01 +5.465547E+00 +1.504031E+00 +2.005120E+00 +2.024490E-01 +4.880060E+00 +1.199182E+00 +3.883466E+00 +7.559482E-01 +5.706123E-01 +1.632280E-02 +1.388756E+00 +9.668619E-02 +2.907681E+00 +4.228621E-01 +9.306046E-02 +4.331768E-04 +2.264905E-01 +2.565871E-03 +3.727441E+00 +6.948089E-01 +1.227027E-01 +7.529631E-04 +2.986338E-01 +4.460088E-03 +9.674219E+00 +4.679846E+00 +1.122358E-01 +6.299070E-04 +2.731629E-01 +3.731271E-03 +2.103996E+01 +2.213497E+01 +3.167421E-02 +5.016895E-05 +7.837561E-02 +3.071747E-04 +1.120119E+01 +6.274853E+00 +1.494396E-01 +1.117486E-03 +4.156586E-01 +8.645390E-03 +8.910289E+00 +4.001626E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.226932E+01 -2.486554E+01 +4.741600E+00 +1.127303E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.441107E+01 -1.038682E+01 +3.068401E+00 +4.708885E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.909194E+01 -1.822767E+01 +4.065045E+00 +8.263510E-01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -4.579319E+01 -1.048559E+02 +9.750251E+00 +4.753623E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -8.833276E+01 -3.901410E+02 +1.880773E+01 +1.768690E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -4.528911E+01 -1.025740E+02 +9.642921E+00 +4.650165E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.218278E+01 -2.477434E+01 -8.203467E+00 -3.389915E+00 -1.996560E+01 -2.007976E+01 -1.714818E+01 -1.473927E+01 -2.551034E+00 -3.262450E-01 -6.208707E+00 -1.932474E+00 -1.343470E+01 -9.027502E+00 -4.363259E-01 -9.522121E-03 -1.061930E+00 -5.640315E-02 -1.717034E+01 -1.474381E+01 -5.729720E-01 -1.641841E-02 -1.394499E+00 -9.725251E-02 -4.542715E+01 -1.031896E+02 -5.348128E-01 -1.430232E-02 -1.301643E+00 -8.472019E-02 -1.009076E+02 -5.091264E+02 -1.543547E-01 -1.191320E-03 -3.819398E-01 -7.294216E-03 -5.342751E+01 -1.427427E+02 -7.255554E-01 -2.633014E-02 -2.018096E+00 -2.037021E-01 -2.540053E+01 -3.247261E+01 -9.483956E+00 -4.526477E+00 -2.308205E+01 -2.681205E+01 -1.812760E+01 -1.646855E+01 -2.717374E+00 -3.700301E-01 -6.613545E+00 -2.191830E+00 -1.362672E+01 -9.286907E+00 -4.458292E-01 -9.940508E-03 -1.085059E+00 -5.888142E-02 -1.744511E+01 -1.521876E+01 -5.866632E-01 -1.721091E-02 -1.427821E+00 -1.019468E-01 -4.538426E+01 -1.029941E+02 -5.385934E-01 -1.450495E-02 -1.310845E+00 -8.592045E-02 -9.921424E+01 -4.921945E+02 -1.532444E-01 -1.174272E-03 -3.791926E-01 -7.189835E-03 -5.301633E+01 -1.405571E+02 -7.285745E-01 -2.655093E-02 -2.026493E+00 -2.054102E-01 +4.723189E+00 +1.123179E+00 +1.746692E+00 +1.536860E-01 +4.251098E+00 +9.103405E-01 +3.651202E+00 +6.682179E-01 +5.431675E-01 +1.479058E-02 +1.321961E+00 +8.761024E-02 +2.860513E+00 +4.092640E-01 +9.290237E-02 +4.316867E-04 +2.261058E-01 +2.557045E-03 +3.655900E+00 +6.684112E-01 +1.219969E-01 +7.443277E-04 +2.969160E-01 +4.408937E-03 +9.672316E+00 +4.678082E+00 +1.138719E-01 +6.483917E-04 +2.771448E-01 +3.840766E-03 +2.148514E+01 +2.308100E+01 +3.286501E-02 +5.400774E-05 +8.132216E-02 +3.306788E-04 +1.137571E+01 +6.471140E+00 +1.544841E-01 +1.193653E-03 +4.296897E-01 +9.234647E-03 +5.408307E+00 +1.472183E+00 +2.019332E+00 +2.052123E-01 +4.914651E+00 +1.215551E+00 +3.859737E+00 +7.466141E-01 +5.785840E-01 +1.677554E-02 +1.408158E+00 +9.936796E-02 +2.901397E+00 +4.210234E-01 +9.492573E-02 +4.506531E-04 +2.310302E-01 +2.669390E-03 +3.714401E+00 +6.899411E-01 +1.249118E-01 +7.802521E-04 +3.040104E-01 +4.621732E-03 +9.663183E+00 +4.669215E+00 +1.146768E-01 +6.575767E-04 +2.791038E-01 +3.895173E-03 +2.112461E+01 +2.231346E+01 +3.262864E-02 +5.323508E-05 +8.073730E-02 +3.259479E-04 +1.128817E+01 +6.372070E+00 +1.551272E-01 +1.203670E-03 +4.314785E-01 +9.312148E-03 From ad5a876bee9f3e3b96ddc28867fdec0028cb7587 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Wed, 3 Dec 2025 01:48:38 -0600 Subject: [PATCH 464/671] Improved automatic MGXS generation for random ray (#3658) Co-authored-by: Paul Romano --- docs/source/usersguide/random_ray.rst | 28 ++- openmc/model/model.py | 186 +++++++++++++++--- .../infinite_medium/results_true.dat | 2 +- .../stochastic_slab/results_true.dat | 2 +- .../__init__.py | 0 .../infinite_medium/model/inputs_true.dat | 61 ++++++ .../infinite_medium/model/results_true.dat | 2 + .../infinite_medium/user/inputs_true.dat | 64 ++++++ .../infinite_medium/user/results_true.dat | 2 + .../stochastic_slab/model/inputs_true.dat | 61 ++++++ .../stochastic_slab/model/results_true.dat | 2 + .../stochastic_slab/user/inputs_true.dat | 64 ++++++ .../stochastic_slab/user/results_true.dat | 2 + .../test.py | 66 +++++++ 14 files changed, 511 insertions(+), 31 deletions(-) create mode 100644 tests/regression_tests/random_ray_auto_convert_source_energy/__init__.py create mode 100644 tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/results_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/results_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/results_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/results_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert_source_energy/test.py diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index d5d752a83..881498a0a 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -644,7 +644,8 @@ model to use these multigroup cross sections. An example is given below:: nparticles=2000, overwrite_mgxs_library=False, mgxs_path="mgxs.h5", - correction=None + correction=None, + source_energy=None ) The most important parameter to set is the ``method`` parameter, which can be @@ -706,6 +707,31 @@ generation and use an existing library file. with a :math:`\rho` default value of 1.0, which can be adjusted with the ``settings.random_ray['diagonal_stabilization_rho']`` parameter. +When generating MGXS data with either the ``stochastic_slab`` or +``infinite_medium`` methods, by default the simulation will use a uniform source +distribution spread evenly over all energy groups. This ensures that all energy +groups receive tallies and therefore produce non-zero total multigroup cross +sections. Additionally, the function will convert any sources in the model into +simplified spatial sources that retain the original energy distributions. If +sources are present, they will be used 99% of the time to sample source energies +during MGXS generation. The other 1% of the time, energies will be sampled +uniformly over all energy groups to ensure that all groups receive some tallies. +However, the user may wish to specify a different source energy spectrum (for +instance, if they are using a FileSource, such that the energy distribution +cannot be extracted from the python source object). This can be done by +providing a :class:`openmc.stats.Univariate` distribution as the +``source_energy`` parameter of the :meth:`openmc.Model.convert_to_multigroup` +method. If provided, it will override any sources present in the model and will +be used 99% of the time to sample source energies during MGXS generation. The +other 1% of the time, energies will be sampled uniformly over all energy groups +to ensure that all groups receive some tallies. + +For instance, a D-D fusion simulation may involve a complex file source. In this +case, the user may wish to provide a discrete 2.45 MeV energy source +distribution for MGXS generation as:: + + source_energy = openmc.stats.delta_function(2.45e6) + Ultimately, the methods described above are all just approximations. Approximations in the generated MGXS data will fundamentally limit the potential accuracy of the random ray solver. However, the methods described above are all diff --git a/openmc/model/model.py b/openmc/model/model.py index 48ae9f0b9..299724759 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1687,6 +1687,91 @@ class Model: self.geometry.get_all_materials().values() ) + def _create_mgxs_sources( + self, + groups: openmc.mgxs.EnergyGroups, + spatial_dist: openmc.stats.Spatial, + source_energy: openmc.stats.Univariate | None = None, + ) -> list[openmc.IndependentSource]: + """Create a list of independent sources to use with MGXS generation. + + Note that in all cases, a discrete source that is uniform over all + energy groups is created (strength = 0.01) to ensure that total cross + sections are generated for all energy groups. In the case that the user + has provided a source_energy distribution as an argument, an additional + source (strength = 0.99) is created using that energy distribution. If + the user has not provided a source_energy distribution, but the model + has sources defined, and all of those sources are of IndependentSource + type, then additional sources are created based on the model's existing + sources, keeping their energy distributions but replacing their + spatial/angular distributions, with their combined strength being 0.99. + If the user has not provided a source_energy distribution and no sources + are defined on the model and the run mode is 'eigenvalue', then a + default Watt spectrum source (strength = 0.99) is added. + + Parameters + ---------- + groups : openmc.mgxs.EnergyGroups + Energy group structure for the MGXS. + spatial_dist : openmc.stats.Spatial + Spatial distribution to use for all sources. + source_energy : openmc.stats.Univariate, optional + Energy distribution to use when generating MGXS data, replacing any + existing sources in the model. + + Returns + ------- + list[openmc.IndependentSource] + A list of independent sources to use for MGXS generation. + """ + # Make a discrete source that is uniform over the bins of the group structure + midpoints = [] + strengths = [] + for i in range(groups.num_groups): + bounds = groups.get_group_bounds(i+1) + midpoints.append((bounds[0] + bounds[1]) / 2.0) + strengths.append(1.0) + + uniform_energy = openmc.stats.Discrete(x=midpoints, p=strengths) + uniform_distribution = openmc.IndependentSource(spatial_dist, energy=uniform_energy, strength=0.01) + sources = [uniform_distribution] + + # If the user provided an energy distribution, use that + if source_energy is not None: + user_energy = openmc.IndependentSource( + space=spatial_dist, energy=source_energy, strength=0.99) + sources.append(user_energy) + + # If the user did not provide an energy distribution, create sources + # based on what is in their model, keeping the energy spectrum but + # replacing the spatial/angular distributions. We only do this if ALL + # sources are of IndependentSource type, as we can't pull the energy + # distribution from e.g. CompiledSource or FileSource types. + else: + if self.settings.source is not None: + for src in self.settings.source: + if not isinstance(src, openmc.IndependentSource): + break + else: + n_user_sources = len(self.settings.source) + for src in self.settings.source: + # Create a new IndependentSource with adjusted strength, space, and angle + user_source = openmc.IndependentSource( + space=spatial_dist, + energy=src.energy, + strength=0.99 / n_user_sources + ) + sources.append(user_source) + else: + # No user sources defined. If we are in eigenvalue mode, then use the default Watt spectrum. + if self.settings.run_mode == 'eigenvalue': + watt_energy = openmc.stats.Watt() + watt_source = openmc.IndependentSource( + space=spatial_dist, energy=watt_energy, strength=0.99) + sources.append(watt_source) + + return sources + def _generate_infinite_medium_mgxs( self, groups: openmc.mgxs.EnergyGroups, @@ -1694,6 +1779,7 @@ class Model: mgxs_path: PathLike, correction: str | None, directory: PathLike, + source_energy: openmc.stats.Univariate | None = None, ): """Generate a MGXS library by running multiple OpenMC simulations, each representing an infinite medium simulation of a single isolated @@ -1702,6 +1788,20 @@ class Model: method that ignores all spatial self shielding effects and all resonance shielding effects between materials. + Note that in all cases, a discrete source that is uniform over all + energy groups is created (strength = 0.01) to ensure that total cross + sections are generated for all energy groups. In the case that the user + has provided a source_energy distribution as an argument, an additional + source (strength = 0.99) is created using that energy distribution. If + the user has not provided a source_energy distribution, but the model + has sources defined, and all of those sources are of IndependentSource + type, then additional sources are created based on the model's existing + sources, keeping their energy distributions but replacing their + spatial/angular distributions, with their combined strength being 0.99. + If the user has not provided a source_energy distribution and no sources + are defined on the model and the run mode is 'eigenvalue', then a + default Watt spectrum source (strength = 0.99) is added. + Parameters ---------- groups : openmc.mgxs.EnergyGroups @@ -1715,9 +1815,10 @@ class Model: "P0". directory : str Directory to run the simulation in, so as to contain XML files. + source_energy : openmc.stats.Univariate, optional + Energy distribution to use when generating MGXS data, replacing any + existing sources in the model. """ - warnings.warn("The infinite medium method of generating MGXS may hang " - "if a material has a k-infinity > 1.0.") mgxs_sets = [] for material in self.materials: model = openmc.Model() @@ -1728,20 +1829,16 @@ class Model: # Settings model.settings.batches = 100 model.settings.particles = nparticles + + model.settings.source = self._create_mgxs_sources( + groups, + spatial_dist=openmc.stats.Point(), + source_energy=source_energy + ) + model.settings.run_mode = 'fixed source' + model.settings.create_fission_neutrons = False - # Make a discrete source that is uniform over the bins of the group structure - n_groups = groups.num_groups - midpoints = [] - strengths = [] - for i in range(n_groups): - bounds = groups.get_group_bounds(i+1) - midpoints.append((bounds[0] + bounds[1]) / 2.0) - strengths.append(1.0) - - energy_distribution = openmc.stats.Discrete(x=midpoints, p=strengths) - model.settings.source = openmc.IndependentSource( - space=openmc.stats.Point(), energy=energy_distribution) model.settings.output = {'summary': True, 'tallies': False} # Geometry @@ -1891,6 +1988,7 @@ class Model: mgxs_path: PathLike, correction: str | None, directory: PathLike, + source_energy: openmc.stats.Univariate | None = None, ) -> None: """Generate MGXS assuming a stochastic "sandwich" of materials in a layered slab geometry. While geometry-specific spatial shielding effects are not @@ -1915,6 +2013,23 @@ class Model: "P0". directory : str Directory to run the simulation in, so as to contain XML files. + source_energy : openmc.stats.Univariate, optional + Energy distribution to use when generating MGXS data, replacing any + existing sources in the model. In all cases, a discrete source that + is uniform over all energy groups is created (strength = 0.01) to + ensure that total cross sections are generated for all energy + groups. In the case that the user has provided a source_energy + distribution as an argument, an additional source (strength = 0.99) + is created using that energy distribution. If the user has not + provided a source_energy distribution, but the model has sources + defined, and all of those sources are of IndependentSource type, + then additional sources are created based on the model's existing + sources, keeping their energy distributions but replacing their + spatial/angular distributions, with their combined strength being + 0.99. If the user has not provided a source_energy distribution and + no sources are defined on the model and the run mode is + 'eigenvalue', then a default Watt spectrum source (strength = 0.99) + is added. """ model = openmc.Model() model.materials = self.materials @@ -1924,24 +2039,20 @@ class Model: model.settings.inactive = 100 model.settings.particles = nparticles model.settings.output = {'summary': True, 'tallies': False} - model.settings.run_mode = self.settings.run_mode # Stochastic slab geometry model.geometry, spatial_distribution = Model._create_stochastic_slab_geometry( model.materials) - # Make a discrete source that is uniform over the bins of the group structure - n_groups = groups.num_groups - midpoints = [] - strengths = [] - for i in range(n_groups): - bounds = groups.get_group_bounds(i+1) - midpoints.append((bounds[0] + bounds[1]) / 2.0) - strengths.append(1.0) + # Define the sources + model.settings.source = self._create_mgxs_sources( + groups, + spatial_dist=spatial_distribution, + source_energy=source_energy + ) - energy_distribution = openmc.stats.Discrete(x=midpoints, p=strengths) - model.settings.source = [openmc.IndependentSource( - space=spatial_distribution, energy=energy_distribution, strength=1.0)] + model.settings.run_mode = 'fixed source' + model.settings.create_fission_neutrons = False model.settings.output = {'summary': True, 'tallies': False} @@ -2099,6 +2210,7 @@ class Model: overwrite_mgxs_library: bool = False, mgxs_path: PathLike = "mgxs.h5", correction: str | None = None, + source_energy: openmc.stats.Univariate | None = None, ): """Convert all materials from continuous energy to multigroup. @@ -2121,6 +2233,24 @@ class Model: correction : str, optional Transport correction to apply to the MGXS. Options are None and "P0". + source_energy : openmc.stats.Univariate, optional + Energy distribution to use when generating MGXS data, replacing any + existing sources in the model. In all cases, a discrete source that + is uniform over all energy groups is created (strength = 0.01) to + ensure that total cross sections are generated for all energy + groups. In the case that the user has provided a source_energy + distribution as an argument, an additional source (strength = 0.99) + is created using that energy distribution. If the user has not + provided a source_energy distribution, but the model has sources + defined, and all of those sources are of IndependentSource type, + then additional sources are created based on the model's existing + sources, keeping their energy distributions but replacing their + spatial/angular distributions, with their combined strength being + 0.99. If the user has not provided a source_energy distribution and + no sources are defined on the model and the run mode is + 'eigenvalue', then a default Watt spectrum source (strength = 0.99) + is added. Note that this argument is only used when using the + "stochastic_slab" or "infinite_medium" MGXS generation methods. """ if isinstance(groups, str): groups = openmc.mgxs.EnergyGroups(groups) @@ -2150,13 +2280,13 @@ class Model: if not Path(mgxs_path).is_file() or overwrite_mgxs_library: if method == "infinite_medium": self._generate_infinite_medium_mgxs( - groups, nparticles, mgxs_path, correction, tmpdir) + groups, nparticles, mgxs_path, correction, tmpdir, source_energy) elif method == "material_wise": self._generate_material_wise_mgxs( groups, nparticles, mgxs_path, correction, tmpdir) elif method == "stochastic_slab": self._generate_stochastic_slab_mgxs( - groups, nparticles, mgxs_path, correction, tmpdir) + groups, nparticles, mgxs_path, correction, tmpdir, source_energy) else: raise ValueError( f'MGXS generation method "{method}" not recognized') diff --git a/tests/regression_tests/random_ray_auto_convert/infinite_medium/results_true.dat b/tests/regression_tests/random_ray_auto_convert/infinite_medium/results_true.dat index ee396fa74..f984f3718 100644 --- a/tests/regression_tests/random_ray_auto_convert/infinite_medium/results_true.dat +++ b/tests/regression_tests/random_ray_auto_convert/infinite_medium/results_true.dat @@ -1,2 +1,2 @@ k-combined: -7.797252E-01 1.055731E-02 +7.479770E-01 1.624548E-02 diff --git a/tests/regression_tests/random_ray_auto_convert/stochastic_slab/results_true.dat b/tests/regression_tests/random_ray_auto_convert/stochastic_slab/results_true.dat index 86e08a710..674dee4aa 100644 --- a/tests/regression_tests/random_ray_auto_convert/stochastic_slab/results_true.dat +++ b/tests/regression_tests/random_ray_auto_convert/stochastic_slab/results_true.dat @@ -1,2 +1,2 @@ k-combined: -7.496641E-01 8.282032E-03 +6.413334E-01 2.083132E-02 diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/__init__.py b/tests/regression_tests/random_ray_auto_convert_source_energy/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/inputs_true.dat new file mode 100644 index 000000000..80a166c67 --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/inputs_true.dat @@ -0,0 +1,61 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + + + 7000000.0 1.0 + + + multi-group + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 + 150.0 + + + + + + linear + + + 2 2 + -0.63 -0.63 + 0.63 0.63 + + + diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/results_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/results_true.dat new file mode 100644 index 000000000..1fb09fd68 --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +7.657815E-01 2.317564E-02 diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/inputs_true.dat new file mode 100644 index 000000000..464c89a5d --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/inputs_true.dat @@ -0,0 +1,64 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + + + -0.63 -0.63 -1 0.63 0.63 1 + + + true + + + multi-group + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 + 150.0 + + + + + + linear + + + 2 2 + -0.63 -0.63 + 0.63 0.63 + + + diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/results_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/results_true.dat new file mode 100644 index 000000000..073c5c99f --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +7.827784E-01 2.062954E-02 diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/inputs_true.dat new file mode 100644 index 000000000..80a166c67 --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/inputs_true.dat @@ -0,0 +1,61 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + + + 7000000.0 1.0 + + + multi-group + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 + 150.0 + + + + + + linear + + + 2 2 + -0.63 -0.63 + 0.63 0.63 + + + diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/results_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/results_true.dat new file mode 100644 index 000000000..c5cdf8e29 --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +7.479571E-01 2.398563E-02 diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/inputs_true.dat new file mode 100644 index 000000000..464c89a5d --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/inputs_true.dat @@ -0,0 +1,64 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + + + -0.63 -0.63 -1 0.63 0.63 1 + + + true + + + multi-group + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 + 150.0 + + + + + + linear + + + 2 2 + -0.63 -0.63 + 0.63 0.63 + + + diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/results_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/results_true.dat new file mode 100644 index 000000000..c6cce2e39 --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +7.620306E-01 2.175179E-02 diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/test.py b/tests/regression_tests/random_ray_auto_convert_source_energy/test.py new file mode 100644 index 000000000..bb9119d89 --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/test.py @@ -0,0 +1,66 @@ +import os + +import openmc +from openmc.examples import pwr_pin_cell +from openmc import RegularMesh +from openmc.utility_funcs import change_directory +import pytest + +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) + + +@pytest.mark.parametrize("source_type", ["model", "user"]) +@pytest.mark.parametrize("method", ["stochastic_slab", "infinite_medium"]) +def test_random_ray_auto_convert_source_energy(method, source_type): + dirname = f"{method}/{source_type}" + with change_directory(dirname): + openmc.reset_auto_ids() + + # Start with a normal continuous energy model + model = pwr_pin_cell() + + # Define the source energy distribution, using different methods + source_energy = None + if source_type == "model": + model.settings.source = openmc.IndependentSource( + energy=openmc.stats.delta_function(7.0e6) + ) + elif source_type == "user": + source_energy = openmc.stats.delta_function(1.0e4) + + # Convert to a multi-group model + model.convert_to_multigroup( + method=method, groups='CASMO-8', nparticles=100, + overwrite_mgxs_library=False, mgxs_path="mgxs.h5", + source_energy=source_energy + ) + + # Convert to a random ray model + model.convert_to_random_ray() + + # Set the number of particles + model.settings.particles = 100 + + # Overlay a basic 2x2 mesh + n = 2 + mesh = RegularMesh() + mesh.dimension = (n, n) + bbox = model.geometry.bounding_box + mesh.lower_left = (bbox.lower_left[0], bbox.lower_left[1]) + mesh.upper_right = (bbox.upper_right[0], bbox.upper_right[1]) + model.settings.random_ray['source_region_meshes'] = [ + (mesh, [model.geometry.root_universe])] + + # Set the source shape to linear + model.settings.random_ray['source_shape'] = 'linear' + + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() From db8d4627386c5a5da94d7ddd953d57e93eaa10bb Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 4 Dec 2025 06:53:04 -0600 Subject: [PATCH 465/671] Allow DistribcellFilter to work with apply_tally_results=True (#3667) --- openmc/__init__.py | 2 +- openmc/data/decay.py | 16 ++----- openmc/data/neutron.py | 8 +--- openmc/filter.py | 19 ++++++-- openmc/model/model.py | 11 +++++ openmc/statepoint.py | 2 +- openmc/tallies.py | 37 ++++---------- tests/unit_tests/test_filter_distribcell.py | 53 +++++++++++++++++++++ 8 files changed, 94 insertions(+), 54 deletions(-) create mode 100644 tests/unit_tests/test_filter_distribcell.py diff --git a/openmc/__init__.py b/openmc/__init__.py index bb972b4e6..c204929c8 100644 --- a/openmc/__init__.py +++ b/openmc/__init__.py @@ -37,7 +37,7 @@ from openmc.tracks import * from .config import * # Import a few names from the model module -from openmc.model import Model +from openmc.model import Model, SearchResult from . import examples diff --git a/openmc/data/decay.py b/openmc/data/decay.py index c8a0bb5e7..7cd4bf43d 100644 --- a/openmc/data/decay.py +++ b/openmc/data/decay.py @@ -13,7 +13,7 @@ import openmc.checkvalue as cv from openmc.exceptions import DataError from openmc.mixin import EqualityMixin from openmc.stats import Discrete, Tabular, Univariate, combine_distributions -from .data import ATOMIC_SYMBOL, ATOMIC_NUMBER +from .data import ATOMIC_NUMBER, gnds_name from .function import INTERPOLATION_SCHEME from .endf import Evaluation, get_head_record, get_list_record, get_tab1_record @@ -126,9 +126,7 @@ class FissionProductYields(EqualityMixin): for j in range(n_products): Z, A = divmod(int(values[4*j]), 1000) isomeric_state = int(values[4*j + 1]) - name = ATOMIC_SYMBOL[Z] + str(A) - if isomeric_state > 0: - name += f'_m{isomeric_state}' + name = gnds_name(Z, A, isomeric_state) yield_j = ufloat(values[4*j + 2], values[4*j + 3]) yields[name] = yield_j @@ -256,10 +254,7 @@ class DecayMode(EqualityMixin): A += delta_A Z += delta_Z - if self._daughter_state > 0: - return f'{ATOMIC_SYMBOL[Z]}{A}_m{self._daughter_state}' - else: - return f'{ATOMIC_SYMBOL[Z]}{A}' + return gnds_name(Z, A, self._daughter_state) @property def parent(self): @@ -348,10 +343,7 @@ class Decay(EqualityMixin): self.nuclide['atomic_number'] = Z self.nuclide['mass_number'] = A self.nuclide['isomeric_state'] = metastable - if metastable > 0: - self.nuclide['name'] = f'{ATOMIC_SYMBOL[Z]}{A}_m{metastable}' - else: - self.nuclide['name'] = f'{ATOMIC_SYMBOL[Z]}{A}' + self.nuclide['name'] = gnds_name(Z, A, metastable) self.nuclide['mass'] = items[1] # AWR self.nuclide['excited_state'] = items[2] # State of the original nuclide self.nuclide['stable'] = (items[4] == 1) # Nucleus stability flag diff --git a/openmc/data/neutron.py b/openmc/data/neutron.py index 95a3424ea..628801e5e 100644 --- a/openmc/data/neutron.py +++ b/openmc/data/neutron.py @@ -11,7 +11,7 @@ import h5py from . import HDF5_VERSION, HDF5_VERSION_MAJOR from .ace import Library, Table, get_table, get_metadata -from .data import ATOMIC_SYMBOL, K_BOLTZMANN, EV_PER_MEV +from .data import ATOMIC_SYMBOL, K_BOLTZMANN, EV_PER_MEV, gnds_name from .endf import ( Evaluation, SUM_RULES, get_head_record, get_tab1_record, get_evaluations) from .fission_energy import FissionEnergyRelease @@ -678,11 +678,7 @@ class IncidentNeutron(EqualityMixin): temperature = ev.target['temperature'] # Determine name - element = ATOMIC_SYMBOL[atomic_number] - if metastable > 0: - name = f'{element}{mass_number}_m{metastable}' - else: - name = f'{element}{mass_number}' + name = gnds_name(atomic_number, mass_number, metastable) # Instantiate incident neutron data data = cls(name, atomic_number, mass_number, metastable, diff --git a/openmc/filter.py b/openmc/filter.py index 6a666d2a0..550146f85 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -1839,12 +1839,21 @@ class DistribcellFilter(Filter): @property def paths(self): - return self._paths + if self._paths is None: + if not hasattr(self, '_geometry'): + raise ValueError( + "Model must be exported before the 'paths' attribute is" \ + "available for a DistribcellFilter.") - @paths.setter - def paths(self, paths): - cv.check_iterable_type('paths', paths, str) - self._paths = paths + # Determine paths for cell instances + self._geometry.determine_paths() + + # Get paths for the corresponding cell + cell_id = self.bins[0] + cell = self._geometry.get_all_cells()[cell_id] + self._paths = cell.paths + + return self._paths @Filter.bins.setter def bins(self, bins): diff --git a/openmc/model/model.py b/openmc/model/model.py index 299724759..782725b78 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -546,6 +546,13 @@ class Model: depletion_operator.cleanup_when_done = True depletion_operator.finalize() + def _link_geometry_to_filters(self): + """Establishes a link between distribcell filters and the geometry""" + for tally in self.tallies: + for f in tally.filters: + if isinstance(f, openmc.DistribcellFilter): + f._geometry = self.geometry + def export_to_xml(self, directory: PathLike = '.', remove_surfs: bool = False, nuclides_to_ignore: Iterable[str] | None = None): """Export model to separate XML files. @@ -587,6 +594,8 @@ class Model: if self.plots: self.plots.export_to_xml(d) + self._link_geometry_to_filters() + def export_to_model_xml(self, path: PathLike = 'model.xml', remove_surfs: bool = False, nuclides_to_ignore: Iterable[str] | None = None): """Export model to a single XML file. @@ -666,6 +675,8 @@ class Model: fh.write(ET.tostring(plots_element, encoding="unicode")) fh.write("\n") + self._link_geometry_to_filters() + def import_properties(self, filename: PathLike): """Import physical properties diff --git a/openmc/statepoint.py b/openmc/statepoint.py index 11986841f..a10ec3a83 100644 --- a/openmc/statepoint.py +++ b/openmc/statepoint.py @@ -723,7 +723,7 @@ class StatePoint: cell = cells[cell_id] if not cell._paths: summary.geometry.determine_paths() - tally_filter.paths = cell.paths + tally_filter._paths = cell.paths self._summary = summary diff --git a/openmc/tallies.py b/openmc/tallies.py index add356579..09365e525 100644 --- a/openmc/tallies.py +++ b/openmc/tallies.py @@ -290,7 +290,7 @@ class Tally(IDManagerMixin): @property def num_nuclides(self): - return len(self._nuclides) + return max(len(self._nuclides), 1) @property def scores(self): @@ -393,6 +393,11 @@ class Tally(IDManagerMixin): group = f[f'tallies/tally {self.id}'] self._num_realizations = int(group['n_realizations'][()]) + for filt in self.filters: + if isinstance(filt, openmc.DistribcellFilter): + filter_group = f[f'tallies/filters/filter {filt.id}'] + filt._num_bins = int(filter_group['n_bins'][()]) + # Update nuclides nuclide_names = group['nuclides'][()] self._nuclides = [name.decode().strip() for name in nuclide_names] @@ -3704,43 +3709,17 @@ class Tallies(cv.CheckedList): if possible. Defaults to False. """ - if not isinstance(tally, Tally): - msg = f'Unable to add a non-Tally "{tally}" to the Tallies instance' - raise TypeError(msg) - if merge: - merged = False - # Look for a tally to merge with this one for i, tally2 in enumerate(self): - # If a mergeable tally is found if tally2.can_merge(tally): # Replace tally2 with the merged tally merged_tally = tally2.merge(tally) self[i] = merged_tally - merged = True - break + return - # If no mergeable tally was found, simply add this tally - if not merged: - super().append(tally) - - else: - super().append(tally) - - def insert(self, index, item): - """Insert tally before index - - Parameters - ---------- - index : int - Index in list - item : openmc.Tally - Tally to insert - - """ - super().insert(index, item) + super().append(tally) def merge_tallies(self): """Merge any mergeable tallies together. Note that n-way merges are diff --git a/tests/unit_tests/test_filter_distribcell.py b/tests/unit_tests/test_filter_distribcell.py new file mode 100644 index 000000000..d5734a2c0 --- /dev/null +++ b/tests/unit_tests/test_filter_distribcell.py @@ -0,0 +1,53 @@ +import openmc +import pandas as pd + + +def test_distribcell_filter_apply_tally_results(run_in_tmpdir): + # Reset IDs to ensure consistent paths + openmc.reset_auto_ids() + + mat = openmc.Material() + mat.add_nuclide("U235", 1.0) + mat.set_density("g/cm3", 1.0) + + # Define 2x2 lattice with a cylinder in each universe + cyl = openmc.ZCylinder(r=1.0) + cell1 = openmc.Cell(fill=mat, region=-cyl) + cell2 = openmc.Cell(fill=None, region=+cyl) + univ = openmc.Universe(cells=[cell1, cell2]) + lattice = openmc.RectLattice() + lattice.lower_left = (-3.0, -3.0) + lattice.pitch = (3.0, 3.0) + lattice.universes = [[univ, univ], [univ, univ]] + box = openmc.model.RectangularPrism(6., 6., boundary_type='reflective') + root_cell = openmc.Cell(region=-box, fill=lattice) + geometry = openmc.Geometry([root_cell]) + + # Create model and add tally with distribcell filter + model = openmc.Model(geometry) + model.settings.batches = 10 + model.settings.particles = 1000 + tally = openmc.Tally() + distribcell_filter = openmc.DistribcellFilter(cell1) + tally.filters = [distribcell_filter] + tally.scores = ['flux'] + model.tallies = [tally] + + # Run OpenMC and apply tally results + model.run(apply_tally_results=True) + + # Check that mean and standard deviation are available on tally + assert tally.mean.shape == (4, 1, 1) + assert tally.std_dev.shape == (4, 1, 1) + + # Make sure paths attribute on filter is correct + assert distribcell_filter.paths == [ + 'u3->c3->l2(0,0)->u1->c1', + 'u3->c3->l2(1,0)->u1->c1', + 'u3->c3->l2(0,1)->u1->c1', + 'u3->c3->l2(1,1)->u1->c1', + ] + + # Check that we can get a DataFrame from the tally + df = tally.get_pandas_dataframe() + assert isinstance(df, pd.DataFrame) From f28139250ac8dc7fdef3f8f13b8c65c5dbb82923 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Fri, 5 Dec 2025 18:02:26 +0100 Subject: [PATCH 466/671] Fixed plotting issue by scaling source locations with axis units (#3668) --- openmc/model/model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index 782725b78..538c64ff3 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1287,8 +1287,8 @@ class Model: tol = plane_tolerance for particle in particles: if (slice_value - tol < particle.r[z] < slice_value + tol): - xs.append(particle.r[x]) - ys.append(particle.r[y]) + xs.append(particle.r[x] * axis_scaling_factor[axis_units]) + ys.append(particle.r[y] * axis_scaling_factor[axis_units]) axes.scatter(xs, ys, **source_kwargs) return axes From 9b675adda5c64f8620c11e3d6723c598f443f982 Mon Sep 17 00:00:00 2001 From: Boris Polania Date: Fri, 5 Dec 2025 12:17:09 -0800 Subject: [PATCH 467/671] Introduce SlicePlot and VoxelPlot to replace the Plot class (#3528) Co-authored-by: Paul Romano --- docs/source/pythonapi/base.rst | 3 +- docs/source/usersguide/plots.rst | 46 ++- docs/source/usersguide/random_ray.rst | 6 +- examples/lattice/hexagonal/build_xml.py | 4 +- examples/lattice/nested/build_xml.py | 2 +- examples/lattice/simple/build_xml.py | 2 +- examples/pincell_random_ray/build_xml.py | 3 +- openmc/examples.py | 6 +- openmc/executor.py | 2 +- openmc/model/model.py | 2 +- openmc/plots.py | 324 ++++++++++++++++++--- src/mcpl_interface.cpp | 13 +- tests/regression_tests/distribmat/test.py | 4 +- tests/unit_tests/test_model.py | 6 +- tests/unit_tests/test_plots.py | 34 +-- tests/unit_tests/test_slice_voxel_plots.py | 273 +++++++++++++++++ 16 files changed, 618 insertions(+), 112 deletions(-) create mode 100644 tests/unit_tests/test_slice_voxel_plots.py diff --git a/docs/source/pythonapi/base.rst b/docs/source/pythonapi/base.rst index ce2f6f0f8..dea8c4427 100644 --- a/docs/source/pythonapi/base.rst +++ b/docs/source/pythonapi/base.rst @@ -176,7 +176,8 @@ Geometry Plotting :nosignatures: :template: myclass.rst - openmc.Plot + openmc.SlicePlot + openmc.VoxelPlot openmc.WireframeRayTracePlot openmc.SolidRayTracePlot openmc.Plots diff --git a/docs/source/usersguide/plots.rst b/docs/source/usersguide/plots.rst index da0c69bdd..b5c29a3e8 100644 --- a/docs/source/usersguide/plots.rst +++ b/docs/source/usersguide/plots.rst @@ -6,13 +6,14 @@ Geometry Visualization .. currentmodule:: openmc -OpenMC is capable of producing two-dimensional slice plots of a geometry as well -as three-dimensional voxel plots using the geometry plotting :ref:`run mode -`. The geometry plotting mode relies on the presence of a -:ref:`plots.xml ` file that indicates what plots should be created. To -create this file, one needs to create one or more :class:`openmc.Plot` -instances, add them to a :class:`openmc.Plots` collection, and then use the -:class:`Plots.export_to_xml` method to write the ``plots.xml`` file. +OpenMC is capable of producing two-dimensional slice plots of a geometry, +three-dimensional voxel plots, and three-dimensional raytrace plots using the +geometry plotting :ref:`run mode `. The geometry plotting +mode relies on the presence of a :ref:`plots.xml ` file that indicates +what plots should be created. To create this file, one needs to create one or +more instances of the various plot classes described below, add them to a +:class:`openmc.Plots` collection, and then use the :class:`Plots.export_to_xml` +method to write the ``plots.xml`` file. ----------- Slice Plots @@ -21,15 +22,14 @@ Slice Plots .. image:: ../_images/atr.png :width: 300px -By default, when an instance of :class:`openmc.Plot` is created, it indicates -that a 2D slice plot should be made. You can specify the origin of the plot -(:attr:`Plot.origin`), the width of the plot in each direction -(:attr:`Plot.width`), the number of pixels to use in each direction -(:attr:`Plot.pixels`), and the basis directions for the plot. For example, to -create a :math:`x` - :math:`z` plot centered at (5.0, 2.0, 3.0) with a width of -(50., 50.) and 400x400 pixels:: +The :class:`openmc.SlicePlot` class indicates that a 2D slice plot should be +made. You can specify the origin of the plot (:attr:`SlicePlot.origin`), the +width of the plot in each direction (:attr:`SlicePlot.width`), the number of +pixels to use in each direction (:attr:`SlicePlot.pixels`), and the basis +directions for the plot. For example, to create a :math:`x` - :math:`z` plot +centered at (5.0, 2.0, 3.0) with a width of (50., 50.) and 400x400 pixels:: - plot = openmc.Plot() + plot = openmc.SlicePlot() plot.basis = 'xz' plot.origin = (5.0, 2.0, 3.0) plot.width = (50., 50.) @@ -47,7 +47,7 @@ that location. By default, a unique color will be assigned to each cell in the geometry. If you want your plot to be colored by material instead, change the -:attr:`Plot.color_by` attribute:: +:attr:`SlicePlot.color_by` attribute:: plot.color_by = 'material' @@ -68,8 +68,8 @@ particular cells/materials should be given colors of your choosing:: Note that colors can be given as RGB tuples or by a string indicating a valid `SVG color `_. -When you're done creating your :class:`openmc.Plot` instances, you need to then -assign them to a :class:`openmc.Plots` collection and export it to XML:: +When you're done creating your :class:`openmc.SlicePlot` instances, you need to +then assign them to a :class:`openmc.Plots` collection and export it to XML:: plots = openmc.Plots([plot1, plot2, plot3]) plots.export_to_xml() @@ -97,13 +97,11 @@ Voxel Plots .. image:: ../_images/3dba.png :width: 200px -The :class:`openmc.Plot` class can also be told to generate a 3D voxel plot -instead of a 2D slice plot. Simply change the :attr:`Plot.type` attribute to -'voxel'. In this case, the :attr:`Plot.width` and :attr:`Plot.pixels` attributes -should be three items long, e.g.:: +The :class:`openmc.VoxelPlot` class enables the generation of a 3D voxel plot +instead of a 2D slice plot. In this case, the :attr:`VoxelPlot.width` and +:attr:`VoxelPlot.pixels` attributes should be three items long, e.g.:: - vox_plot = openmc.Plot() - vox_plot.type = 'voxel' + vox_plot = openmc.VoxelPlot() vox_plot.width = (100., 100., 50.) vox_plot.pixels = (400, 400, 200) diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index 881498a0a..382381a9e 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -1131,11 +1131,10 @@ given below: tallies.export_to_xml() # Create voxel plot - plot = openmc.Plot() + plot = openmc.VoxelPlot() plot.origin = [0, 0, 0] plot.width = [2*pitch, 2*pitch, 1] plot.pixels = [1000, 1000, 1] - plot.type = 'voxel' # Instantiate a Plots collection and export to XML plots = openmc.Plots([plot]) @@ -1215,11 +1214,10 @@ given below: tallies.export_to_xml() # Create voxel plot - plot = openmc.Plot() + plot = openmc.VoxelPlot() plot.origin = [0, 0, 0] plot.width = [2*pitch, 2*pitch, 1] plot.pixels = [1000, 1000, 1] - plot.type = 'voxel' # Instantiate a Plots collection and export to XML plots = openmc.Plots([plot]) diff --git a/examples/lattice/hexagonal/build_xml.py b/examples/lattice/hexagonal/build_xml.py index 9485d0aa4..2624e52b4 100644 --- a/examples/lattice/hexagonal/build_xml.py +++ b/examples/lattice/hexagonal/build_xml.py @@ -128,14 +128,14 @@ settings_file.export_to_xml() # Exporting to OpenMC plots.xml file ############################################################################### -plot_xy = openmc.Plot(plot_id=1) +plot_xy = openmc.SlicePlot(plot_id=1) plot_xy.filename = 'plot_xy' plot_xy.origin = [0, 0, 0] plot_xy.width = [6, 6] plot_xy.pixels = [400, 400] plot_xy.color_by = 'material' -plot_yz = openmc.Plot(plot_id=2) +plot_yz = openmc.SlicePlot(plot_id=2) plot_yz.filename = 'plot_yz' plot_yz.basis = 'yz' plot_yz.origin = [0, 0, 0] diff --git a/examples/lattice/nested/build_xml.py b/examples/lattice/nested/build_xml.py index 2db23a46b..a1d9c092d 100644 --- a/examples/lattice/nested/build_xml.py +++ b/examples/lattice/nested/build_xml.py @@ -135,7 +135,7 @@ settings_file.export_to_xml() # Exporting to OpenMC plots.xml file ############################################################################### -plot = openmc.Plot(plot_id=1) +plot = openmc.SlicePlot(plot_id=1) plot.origin = [0, 0, 0] plot.width = [4, 4] plot.pixels = [400, 400] diff --git a/examples/lattice/simple/build_xml.py b/examples/lattice/simple/build_xml.py index 56c466121..44531edd8 100644 --- a/examples/lattice/simple/build_xml.py +++ b/examples/lattice/simple/build_xml.py @@ -128,7 +128,7 @@ settings_file.export_to_xml() # Exporting to OpenMC plots.xml file ############################################################################### -plot = openmc.Plot(plot_id=1) +plot = openmc.SlicePlot(plot_id=1) plot.origin = [0, 0, 0] plot.width = [4, 4] plot.pixels = [400, 400] diff --git a/examples/pincell_random_ray/build_xml.py b/examples/pincell_random_ray/build_xml.py index b3dd8020a..5ff4c0082 100644 --- a/examples/pincell_random_ray/build_xml.py +++ b/examples/pincell_random_ray/build_xml.py @@ -192,11 +192,10 @@ tallies.export_to_xml() # Exporting to OpenMC plots.xml file ############################################################################### -plot = openmc.Plot() +plot = openmc.VoxelPlot() plot.origin = [0, 0, 0] plot.width = [pitch, pitch, pitch] plot.pixels = [1000, 1000, 1] -plot.type = 'voxel' # Instantiate a Plots collection and export to XML plots = openmc.Plots([plot]) diff --git a/openmc/examples.py b/openmc/examples.py index 5578d513e..01dd9d01f 100644 --- a/openmc/examples.py +++ b/openmc/examples.py @@ -83,7 +83,7 @@ def pwr_pin_cell() -> openmc.Model: constraints={'fissionable': True} ) - plot = openmc.Plot.from_geometry(model.geometry) + plot = openmc.SlicePlot.from_geometry(model.geometry) plot.pixels = (300, 300) plot.color_by = 'material' model.plots.append(plot) @@ -429,7 +429,7 @@ def pwr_core() -> openmc.Model: model.settings.source = openmc.IndependentSource(space=openmc.stats.Box( [-160, -160, -183], [160, 160, 183])) - plot = openmc.Plot() + plot = openmc.SlicePlot() plot.origin = (125, 125, 0) plot.width = (250, 250) plot.pixels = (3000, 3000) @@ -544,7 +544,7 @@ def pwr_assembly() -> openmc.Model: constraints={'fissionable': True} ) - plot = openmc.Plot() + plot = openmc.SlicePlot() plot.origin = (0.0, 0.0, 0) plot.width = (21.42, 21.42) plot.pixels = (300, 300) diff --git a/openmc/executor.py b/openmc/executor.py index aacc48b3f..9cd299345 100644 --- a/openmc/executor.py +++ b/openmc/executor.py @@ -164,7 +164,7 @@ def plot_inline(plots, openmc_exec='openmc', cwd='.', path_input=None): Parameters ---------- - plots : Iterable of openmc.Plot + plots : Iterable of openmc.PlotBase Plots to display openmc_exec : str Path to OpenMC executable diff --git a/openmc/model/model.py b/openmc/model/model.py index 538c64ff3..e2ac57eba 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1170,7 +1170,7 @@ class Model: self.settings.plot_seed = seed # Create plot object matching passed arguments - plot = openmc.Plot() + plot = openmc.SlicePlot() plot.origin = origin plot.width = width plot.pixels = pixels diff --git a/openmc/plots.py b/openmc/plots.py index a0bde3f00..cb722abc6 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -2,6 +2,7 @@ from collections.abc import Iterable, Mapping from numbers import Integral, Real from pathlib import Path from textwrap import dedent +import warnings import h5py import lxml.etree as ET @@ -626,14 +627,15 @@ class PlotBase(IDManagerMixin): return element -class Plot(PlotBase): - """Definition of a finite region of space to be plotted. +class SlicePlot(PlotBase): + """Definition of a 2D slice plot of the geometry. - OpenMC is capable of generating two-dimensional slice plots, or - three-dimensional voxel or projection plots. Colors that are used in plots can be given as - RGB tuples, e.g. (255, 255, 255) would be white, or by a string indicating a + Colors that are used in plots can be given as RGB tuples, e.g. + (255, 255, 255) would be white, or by a string indicating a valid `SVG color `_. + .. versionadded:: 0.15.4 + Parameters ---------- plot_id : int @@ -648,7 +650,7 @@ class Plot(PlotBase): name : str Name of the plot pixels : Iterable of int - Number of pixels to use in each direction + Number of pixels to use in each direction (2 values) filename : str Path to write the plot to color_by : {'cell', 'material'} @@ -671,11 +673,9 @@ class Plot(PlotBase): level : int Universe depth to plot at width : Iterable of float - Width of the plot in each basis direction + Width of the plot in each basis direction (2 values) origin : tuple or list of ndarray - Origin (center) of the plot - type : {'slice', 'voxel'} - The type of the plot + Origin (center) of the plot (3 values) basis : {'xy', 'xz', 'yz'} The basis directions for the plot meshlines : dict @@ -688,10 +688,37 @@ class Plot(PlotBase): super().__init__(plot_id, name) self._width = [4.0, 4.0] self._origin = [0., 0., 0.] - self._type = 'slice' self._basis = 'xy' self._meshlines = None + @property + def type(self): + warnings.warn( + "The 'type' attribute is deprecated and will be removed in a future version. " + "This is a SlicePlot instance.", + FutureWarning, stacklevel=2 + ) + return 'slice' + + @type.setter + def type(self, value): + raise TypeError( + "Setting plot.type is no longer supported. " + "Use openmc.SlicePlot() for 2D slice plots or openmc.VoxelPlot() for 3D voxel plots." + ) + + @property + def pixels(self): + return self._pixels + + @pixels.setter + def pixels(self, pixels): + cv.check_type('plot pixels', pixels, Iterable, Integral) + cv.check_length('plot pixels', pixels, 2, 2) + for dim in pixels: + cv.check_greater_than('plot pixels', dim, 0) + self._pixels = pixels + @property def width(self): return self._width @@ -699,7 +726,7 @@ class Plot(PlotBase): @width.setter def width(self, width): cv.check_type('plot width', width, Iterable, Real) - cv.check_length('plot width', width, 2, 3) + cv.check_length('plot width', width, 2, 2) self._width = width @property @@ -712,15 +739,6 @@ class Plot(PlotBase): cv.check_length('plot origin', origin, 3) self._origin = origin - @property - def type(self): - return self._type - - @type.setter - def type(self, plottype): - cv.check_value('plot type', plottype, ['slice', 'voxel']) - self._type = plottype - @property def basis(self): return self._basis @@ -763,11 +781,10 @@ class Plot(PlotBase): self._meshlines = meshlines def __repr__(self): - string = 'Plot\n' + string = 'SlicePlot\n' string += '{: <16}=\t{}\n'.format('\tID', self._id) string += '{: <16}=\t{}\n'.format('\tName', self._name) string += '{: <16}=\t{}\n'.format('\tFilename', self._filename) - string += '{: <16}=\t{}\n'.format('\tType', self._type) string += '{: <16}=\t{}\n'.format('\tBasis', self._basis) string += '{: <16}=\t{}\n'.format('\tWidth', self._width) string += '{: <16}=\t{}\n'.format('\tOrigin', self._origin) @@ -883,7 +900,7 @@ class Plot(PlotBase): self._colors[domain] = (r, g, b) def to_xml_element(self): - """Return XML representation of the slice/voxel plot + """Return XML representation of the slice plot Returns ------- @@ -893,10 +910,8 @@ class Plot(PlotBase): """ element = super().to_xml_element() - element.set("type", self._type) - - if self._type == 'slice': - element.set("basis", self._basis) + element.set("type", "slice") + element.set("basis", self._basis) subelement = ET.SubElement(element, "origin") subelement.text = ' '.join(map(str, self._origin)) @@ -942,8 +957,8 @@ class Plot(PlotBase): Returns ------- - openmc.Plot - Plot object + openmc.SlicePlot + SlicePlot object """ plot_id = int(get_text(elem, "id")) @@ -952,9 +967,7 @@ class Plot(PlotBase): if "filename" in elem.keys(): plot.filename = get_text(elem, "filename") plot.color_by = get_text(elem, "color_by") - plot.type = get_text(elem, "type") - if plot.type == 'slice': - plot.basis = get_text(elem, "basis") + plot.basis = get_text(elem, "basis") plot.origin = tuple(get_elem_list(elem, "origin", float)) plot.width = tuple(get_elem_list(elem, "width", float)) @@ -1036,9 +1049,215 @@ class Plot(PlotBase): # Return produced image return _get_plot_image(self, cwd) + + +class VoxelPlot(PlotBase): + """Definition of a 3D voxel plot of the geometry. + + Colors that are used in plots can be given as RGB tuples, e.g. + (255, 255, 255) would be white, or by a string indicating a + valid `SVG color `_. + + .. versionadded:: 0.15.1 + + Parameters + ---------- + plot_id : int + Unique identifier for the plot + name : str + Name of the plot + + Attributes + ---------- + id : int + Unique identifier + name : str + Name of the plot + pixels : Iterable of int + Number of pixels to use in each direction (3 values) + filename : str + Path to write the plot to + color_by : {'cell', 'material'} + Indicate whether the plot should be colored by cell or by material + background : Iterable of int or str + Color of the background + mask_components : Iterable of openmc.Cell or openmc.Material or int + The cells or materials (or corresponding IDs) to mask + mask_background : Iterable of int or str + Color to apply to all cells/materials listed in mask_components + show_overlaps : bool + Indicate whether or not overlapping regions are shown + overlap_color : Iterable of int or str + Color to apply to overlapping regions + colors : dict + Dictionary indicating that certain cells/materials should be + displayed with a particular color. The keys can be of type + :class:`~openmc.Cell`, :class:`~openmc.Material`, or int (ID for a + cell/material). + level : int + Universe depth to plot at + width : Iterable of float + Width of the plot in each dimension (3 values) + origin : tuple or list of ndarray + Origin (center) of the plot (3 values) + + """ + + def __init__(self, plot_id=None, name=''): + super().__init__(plot_id, name) + self._width = [4.0, 4.0, 4.0] + self._origin = [0., 0., 0.] + self._pixels = [400, 400, 400] + + @property + def pixels(self): + return self._pixels + + @pixels.setter + def pixels(self, pixels): + cv.check_type('plot pixels', pixels, Iterable, Integral) + cv.check_length('plot pixels', pixels, 3, 3) + for dim in pixels: + cv.check_greater_than('plot pixels', dim, 0) + self._pixels = pixels + + @property + def width(self): + return self._width + + @width.setter + def width(self, width): + cv.check_type('plot width', width, Iterable, Real) + cv.check_length('plot width', width, 3, 3) + self._width = width + + @property + def origin(self): + return self._origin + + @origin.setter + def origin(self, origin): + cv.check_type('plot origin', origin, Iterable, Real) + cv.check_length('plot origin', origin, 3) + self._origin = origin + + def __repr__(self): + string = 'VoxelPlot\n' + string += '{: <16}=\t{}\n'.format('\tID', self._id) + string += '{: <16}=\t{}\n'.format('\tName', self._name) + string += '{: <16}=\t{}\n'.format('\tFilename', self._filename) + string += '{: <16}=\t{}\n'.format('\tWidth', self._width) + string += '{: <16}=\t{}\n'.format('\tOrigin', self._origin) + string += '{: <16}=\t{}\n'.format('\tPixels', self._pixels) + string += '{: <16}=\t{}\n'.format('\tColor by', self._color_by) + string += '{: <16}=\t{}\n'.format('\tBackground', self._background) + string += '{: <16}=\t{}\n'.format('\tMask components', + self._mask_components) + string += '{: <16}=\t{}\n'.format('\tMask background', + self._mask_background) + string += '{: <16}=\t{}\n'.format('\tOverlap Color', + self._overlap_color) + string += '{: <16}=\t{}\n'.format('\tColors', self._colors) + string += '{: <16}=\t{}\n'.format('\tLevel', self._level) + return string + + def to_xml_element(self): + """Return XML representation of the voxel plot + + Returns + ------- + element : lxml.etree._Element + XML element containing plot data + + """ + + element = super().to_xml_element() + element.set("type", "voxel") + + subelement = ET.SubElement(element, "origin") + subelement.text = ' '.join(map(str, self._origin)) + + subelement = ET.SubElement(element, "width") + subelement.text = ' '.join(map(str, self._width)) + + if self._colors: + self._colors_to_xml(element) + + if self._show_overlaps: + subelement = ET.SubElement(element, "show_overlaps") + subelement.text = "true" + + if self._overlap_color is not None: + color = self._overlap_color + if isinstance(color, str): + color = _SVG_COLORS[color.lower()] + subelement = ET.SubElement(element, "overlap_color") + subelement.text = ' '.join(str(x) for x in color) + + return element + + @classmethod + def from_xml_element(cls, elem): + """Generate plot object from an XML element + + Parameters + ---------- + elem : lxml.etree._Element + XML element + + Returns + ------- + openmc.VoxelPlot + VoxelPlot object + + """ + plot_id = int(get_text(elem, "id")) + name = get_text(elem, 'name', '') + plot = cls(plot_id, name) + if "filename" in elem.keys(): + plot.filename = get_text(elem, "filename") + plot.color_by = get_text(elem, "color_by") + + plot.origin = tuple(get_elem_list(elem, "origin", float)) + plot.width = tuple(get_elem_list(elem, "width", float)) + plot.pixels = tuple(get_elem_list(elem, "pixels")) + background = get_elem_list(elem, "background") + if background is not None: + plot._background = tuple(background) + + # Set plot colors + colors = {} + for color_elem in elem.findall("color"): + uid = int(get_text(color_elem, "id")) + colors[uid] = tuple(get_elem_list(color_elem, "rgb", int)) + plot.colors = colors + + # Set masking information + mask_elem = elem.find("mask") + if mask_elem is not None: + plot.mask_components = get_elem_list(mask_elem, "components", int) + background = get_elem_list(mask_elem, "background", int) + if background is not None: + plot.mask_background = tuple(background) + + # show overlaps + overlap = get_text(elem, "show_overlaps") + if overlap is not None: + plot.show_overlaps = (overlap in ('true', '1')) + overlap_color = get_elem_list(elem, "overlap_color", int) + if overlap_color is not None: + plot.overlap_color = tuple(overlap_color) + + # Set universe level + level = get_text(elem, "level") + if level is not None: + plot.level = int(level) + + return plot + def to_vtk(self, output: PathLike | None = None, openmc_exec: str = 'openmc', cwd: str = '.'): - """Render plot as an voxel image + """Render plot as a voxel image This method runs OpenMC in plotting mode to produce a .vti file. @@ -1059,10 +1278,6 @@ class Plot(PlotBase): Path of the .vti file produced """ - if self.type != 'voxel': - raise ValueError( - 'Generating a VTK file only works for voxel plots') - # Create plots.xml Plots([self]).export_to_xml(cwd) @@ -1082,6 +1297,20 @@ class Plot(PlotBase): return voxel_to_vtk(h5_voxel_file, output) +def Plot(plot_id=None, name=''): + """Legacy Plot class for backward compatibility. + + .. deprecated:: 0.15.4 + Use :class:`SlicePlot` for 2D slice plots or :class:`VoxelPlot` for 3D voxel plots. + + """ + warnings.warn( + "The Plot class is deprecated. Use SlicePlot for 2D slice plots " + "or VoxelPlot for 3D voxel plots.", FutureWarning + ) + return SlicePlot(plot_id, name) + + class RayTracePlot(PlotBase): """Definition of a camera's view of OpenMC geometry @@ -1737,16 +1966,16 @@ class SolidRayTracePlot(RayTracePlot): class Plots(cv.CheckedList): - """Collection of Plots used for an OpenMC simulation. + """Collection of plots used for an OpenMC simulation. This class corresponds directly to the plots.xml input file. It can be thought of as a normal Python list where each member is inherits from :class:`PlotBase`. It behaves like a list as the following example demonstrates: - >>> xz_plot = openmc.Plot() - >>> big_plot = openmc.Plot() - >>> small_plot = openmc.Plot() + >>> xz_plot = openmc.SlicePlot() + >>> big_plot = openmc.VoxelPlot() + >>> small_plot = openmc.SlicePlot() >>> p = openmc.Plots((xz_plot, big_plot)) >>> p.append(small_plot) >>> small_plot = p.pop() @@ -1782,7 +2011,7 @@ class Plots(cv.CheckedList): ---------- index : int Index in list - plot : openmc.Plot + plot : openmc.PlotBase Plot to insert """ @@ -1903,8 +2132,13 @@ class Plots(cv.CheckedList): plots.append(WireframeRayTracePlot.from_xml_element(e)) elif plot_type == 'solid_raytrace': plots.append(SolidRayTracePlot.from_xml_element(e)) - elif plot_type in ('slice', 'voxel'): - plots.append(Plot.from_xml_element(e)) + elif plot_type == 'slice': + plots.append(SlicePlot.from_xml_element(e)) + elif plot_type == 'voxel': + plots.append(VoxelPlot.from_xml_element(e)) + elif plot_type is None: + # For backward compatibility, assume slice if no type specified + plots.append(SlicePlot.from_xml_element(e)) else: raise ValueError("Unknown plot type: {}".format(plot_type)) return plots diff --git a/src/mcpl_interface.cpp b/src/mcpl_interface.cpp index 129407301..256f3343f 100644 --- a/src/mcpl_interface.cpp +++ b/src/mcpl_interface.cpp @@ -63,7 +63,7 @@ using mcpl_read_fpt = const mcpl_particle_repr_t* (*)(mcpl_file_t* file_handle); using mcpl_close_file_fpt = void (*)(mcpl_file_t* file_handle); using mcpl_hdr_add_data_fpt = void (*)(mcpl_outfile_t* file_handle, - const char* key, int32_t ldata, const char* data); + const char* key, uint32_t datalength, const char* data); using mcpl_create_outfile_fpt = mcpl_outfile_t* (*)(const char* filename); using mcpl_hdr_set_srcname_fpt = void (*)( mcpl_outfile_t* outfile_handle, const char* srcname); @@ -150,13 +150,20 @@ struct McplApi { load_symbol_platform("mcpl_create_outfile")); hdr_set_srcname = reinterpret_cast( load_symbol_platform("mcpl_hdr_set_srcname")); - hdr_add_data = reinterpret_cast( - load_symbol_platform("mcpl_hdr_add_data")); add_particle = reinterpret_cast( load_symbol_platform("mcpl_add_particle")); close_outfile = reinterpret_cast( load_symbol_platform("mcpl_close_outfile")); + // Try to load mcpl_hdr_add_data (available in MCPL >= 2.1.0) + // Set to nullptr if not available for graceful fallback + try { + hdr_add_data = reinterpret_cast( + load_symbol_platform("mcpl_hdr_add_data")); + } catch (const std::runtime_error&) { + hdr_add_data = nullptr; + } + // Try to load mcpl_hdr_add_stat_sum (available in MCPL >= 2.1.0) // Set to nullptr if not available for graceful fallback try { diff --git a/tests/regression_tests/distribmat/test.py b/tests/regression_tests/distribmat/test.py index 02f7e773e..dd09eec36 100644 --- a/tests/regression_tests/distribmat/test.py +++ b/tests/regression_tests/distribmat/test.py @@ -73,7 +73,7 @@ class DistribmatTestHarness(PyAPITestHarness): # Plots #################### - plot1 = openmc.Plot(plot_id=1) + plot1 = openmc.SlicePlot(plot_id=1) plot1.basis = 'xy' plot1.color_by = 'cell' plot1.filename = 'cellplot' @@ -81,7 +81,7 @@ class DistribmatTestHarness(PyAPITestHarness): plot1.width = (7, 7) plot1.pixels = (400, 400) - plot2 = openmc.Plot(plot_id=2) + plot2 = openmc.SlicePlot(plot_id=2) plot2.basis = 'xy' plot2.color_by = 'material' plot2.filename = 'matplot' diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index f4f94a47c..9b28ff1c4 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -73,13 +73,13 @@ def pin_model_attributes(): tal.scores = ['flux', 'fission'] tals.append(tal) - plot1 = openmc.Plot(plot_id=1) + plot1 = openmc.SlicePlot(plot_id=1) plot1.origin = (0., 0., 0.) plot1.width = (pitch, pitch) plot1.pixels = (300, 300) plot1.color_by = 'material' plot1.filename = 'test' - plot2 = openmc.Plot(plot_id=2) + plot2 = openmc.SlicePlot(plot_id=2) plot2.origin = (0., 0., 0.) plot2.width = (pitch, pitch) plot2.pixels = (300, 300) @@ -911,7 +911,7 @@ def test_setter_from_list(): model = openmc.Model(tallies=[tally]) assert isinstance(model.tallies, openmc.Tallies) - plot = openmc.Plot() + plot = openmc.SlicePlot() model = openmc.Model(plots=[plot]) assert isinstance(model.plots, openmc.Plots) diff --git a/tests/unit_tests/test_plots.py b/tests/unit_tests/test_plots.py index fad574ee6..98a93e44b 100644 --- a/tests/unit_tests/test_plots.py +++ b/tests/unit_tests/test_plots.py @@ -9,12 +9,11 @@ from openmc.plots import _SVG_COLORS @pytest.fixture(scope='module') def myplot(): - plot = openmc.Plot(name='myplot') + plot = openmc.SlicePlot(name='myplot') plot.width = (100., 100.) plot.origin = (2., 3., -10.) plot.pixels = (500, 500) plot.filename = './not-a-dir/myplot' - plot.type = 'slice' plot.basis = 'yz' plot.background = 'black' plot.background = (0, 0, 0) @@ -80,8 +79,7 @@ def test_voxel_plot(run_in_tmpdir): geometry.export_to_xml() materials = openmc.Materials() materials.export_to_xml() - vox_plot = openmc.Plot() - vox_plot.type = 'voxel' + vox_plot = openmc.VoxelPlot() vox_plot.id = 12 vox_plot.width = (1500., 1500., 1500.) vox_plot.pixels = (200, 200, 200) @@ -97,8 +95,9 @@ def test_voxel_plot(run_in_tmpdir): assert Path('h5_voxel_plot.h5').is_file() assert Path('another_test_voxel_plot.vti').is_file() - slice_plot = openmc.Plot() - with pytest.raises(ValueError): + # SlicePlot should not have to_vtk method + slice_plot = openmc.SlicePlot() + with pytest.raises(AttributeError): slice_plot.to_vtk('shimmy.vti') @@ -153,14 +152,14 @@ def test_from_geometry(): geom = openmc.Geometry(univ) for basis in ('xy', 'yz', 'xz'): - plot = openmc.Plot.from_geometry(geom, basis) + plot = openmc.SlicePlot.from_geometry(geom, basis) assert plot.origin == pytest.approx((0., 0., 0.)) assert plot.width == pytest.approx((width, width)) assert plot.basis == basis def test_highlight_domains(): - plot = openmc.Plot() + plot = openmc.SlicePlot() plot.color_by = 'material' plots = openmc.Plots([plot]) @@ -179,8 +178,8 @@ def test_xml_element(myplot): assert elem.find('pixels') is not None assert elem.find('background').text == '0 0 0' - newplot = openmc.Plot.from_xml_element(elem) - attributes = ('id', 'color_by', 'filename', 'type', 'basis', 'level', + newplot = openmc.SlicePlot.from_xml_element(elem) + attributes = ('id', 'color_by', 'filename', 'basis', 'level', 'meshlines', 'show_overlaps', 'origin', 'width', 'pixels', 'background', 'mask_background') for attr in attributes: @@ -200,11 +199,11 @@ def test_to_xml_element_proj(myprojectionplot): def test_plots(run_in_tmpdir): - p1 = openmc.Plot(name='plot1') + p1 = openmc.SlicePlot(name='plot1') p1.origin = (5., 5., 5.) p1.colors = {10: (255, 100, 0)} p1.mask_components = [2, 4, 6] - p2 = openmc.Plot(name='plot2') + p2 = openmc.SlicePlot(name='plot2') p2.origin = (-3., -3., -3.) plots = openmc.Plots([p1, p2]) assert len(plots) == 2 @@ -213,7 +212,7 @@ def test_plots(run_in_tmpdir): plots = openmc.Plots([p1, p2, p3]) assert len(plots) == 3 - p4 = openmc.Plot(name='plot4') + p4 = openmc.VoxelPlot(name='plot4') plots.append(p4) assert len(plots) == 4 @@ -230,8 +229,7 @@ def test_plots(run_in_tmpdir): def test_voxel_plot_roundtrip(): # Define a voxel plot and create XML element - plot = openmc.Plot(name='my voxel plot') - plot.type = 'voxel' + plot = openmc.VoxelPlot(name='my voxel plot') plot.filename = 'voxel1' plot.pixels = (50, 50, 50) plot.origin = (0., 0., 0.) @@ -243,7 +241,6 @@ def test_voxel_plot_roundtrip(): new_plot = plot.from_xml_element(elem) assert new_plot.name == plot.name assert new_plot.filename == plot.filename - assert new_plot.type == plot.type assert new_plot.pixels == plot.pixels assert new_plot.origin == plot.origin assert new_plot.width == plot.width @@ -288,10 +285,9 @@ def test_phong_plot_roundtrip(): def test_plot_directory(run_in_tmpdir): pwr_pin = openmc.examples.pwr_pin_cell() - # create a standard plot, expected to work - plot = openmc.Plot() + # create a standard slice plot, expected to work + plot = openmc.SlicePlot() plot.filename = 'plot_1' - plot.type = 'slice' plot.pixels = (10, 10) plot.color_by = 'material' plot.width = (100., 100.) diff --git a/tests/unit_tests/test_slice_voxel_plots.py b/tests/unit_tests/test_slice_voxel_plots.py new file mode 100644 index 000000000..48ca31b7a --- /dev/null +++ b/tests/unit_tests/test_slice_voxel_plots.py @@ -0,0 +1,273 @@ +"""Tests for SlicePlot and VoxelPlot classes + +This module tests the functionality of the new SlicePlot and VoxelPlot +classes that replace the legacy Plot class. +""" +import warnings + +import pytest +import openmc + + +def test_slice_plot_initialization(): + """Test SlicePlot initialization with defaults""" + plot = openmc.SlicePlot() + assert plot.width == [4.0, 4.0] + assert plot.pixels == [400, 400] + assert plot.basis == 'xy' + assert plot.origin == [0., 0., 0.] + + +def test_slice_plot_width_validation(): + """Test that SlicePlot only accepts 2 values for width""" + plot = openmc.SlicePlot() + + # Should accept 2 values + plot.width = [10.0, 20.0] + assert plot.width == [10.0, 20.0] + + # Should reject 1 value + with pytest.raises(ValueError, match='must be of length "2"'): + plot.width = [10.0] + + # Should reject 3 values + with pytest.raises(ValueError, match='must be of length "2"'): + plot.width = [10.0, 20.0, 30.0] + + +def test_slice_plot_pixels_validation(): + """Test that SlicePlot only accepts 2 values for pixels""" + plot = openmc.SlicePlot() + + # Should accept 2 values + plot.pixels = [100, 200] + assert plot.pixels == [100, 200] + + # Should reject 1 value + with pytest.raises(ValueError, match='must be of length "2"'): + plot.pixels = [100] + + # Should reject 3 values + with pytest.raises(ValueError, match='must be of length "2"'): + plot.pixels = [100, 200, 300] + + +def test_slice_plot_basis(): + """Test that SlicePlot has basis attribute""" + plot = openmc.SlicePlot() + + # Test all valid basis values + for basis in ['xy', 'xz', 'yz']: + plot.basis = basis + assert plot.basis == basis + + # Test invalid basis + with pytest.raises(ValueError): + plot.basis = 'invalid' + + +def test_slice_plot_meshlines(): + """Test that SlicePlot has meshlines attribute""" + plot = openmc.SlicePlot() + + meshlines = { + 'type': 'tally', + 'id': 1, + 'linewidth': 2, + 'color': (255, 0, 0) + } + plot.meshlines = meshlines + assert plot.meshlines == meshlines + + +def test_slice_plot_xml_roundtrip(): + """Test SlicePlot XML serialization and deserialization""" + plot = openmc.SlicePlot(name='test_slice') + plot.width = [15.0, 25.0] + plot.pixels = [150, 250] + plot.basis = 'xz' + plot.origin = [1.0, 2.0, 3.0] + plot.color_by = 'material' + plot.filename = 'test_plot' + + # Convert to XML and back + elem = plot.to_xml_element() + new_plot = openmc.SlicePlot.from_xml_element(elem) + + # Check all attributes preserved + assert new_plot.name == plot.name + assert new_plot.width == pytest.approx(plot.width) + assert new_plot.pixels == tuple(plot.pixels) + assert new_plot.basis == plot.basis + assert new_plot.origin == pytest.approx(plot.origin) + assert new_plot.color_by == plot.color_by + assert new_plot.filename == plot.filename + + +def test_slice_plot_from_geometry(): + """Test creating SlicePlot from geometry""" + # Create simple geometry + s = openmc.Sphere(r=10.0, boundary_type='vacuum') + c = openmc.Cell(region=-s) + univ = openmc.Universe(cells=[c]) + geom = openmc.Geometry(univ) + + # Test all basis options + for basis in ['xy', 'xz', 'yz']: + plot = openmc.SlicePlot.from_geometry(geom, basis=basis) + assert plot.basis == basis + assert plot.width == pytest.approx([20.0, 20.0]) + assert plot.origin == pytest.approx([0.0, 0.0, 0.0]) + + +def test_voxel_plot_initialization(): + """Test VoxelPlot initialization with defaults""" + plot = openmc.VoxelPlot() + assert plot.width == [4.0, 4.0, 4.0] + assert plot.pixels == [400, 400, 400] + assert plot.origin == [0., 0., 0.] + + +def test_voxel_plot_width_validation(): + """Test that VoxelPlot only accepts 3 values for width""" + plot = openmc.VoxelPlot() + + # Should accept 3 values + plot.width = [10.0, 20.0, 30.0] + assert plot.width == [10.0, 20.0, 30.0] + + # Should reject 2 values + with pytest.raises(ValueError, match='must be of length "3"'): + plot.width = [10.0, 20.0] + + # Should reject 1 value + with pytest.raises(ValueError, match='must be of length "3"'): + plot.width = [10.0] + + +def test_voxel_plot_pixels_validation(): + """Test that VoxelPlot only accepts 3 values for pixels""" + plot = openmc.VoxelPlot() + + # Should accept 3 values + plot.pixels = [100, 200, 300] + assert plot.pixels == [100, 200, 300] + + # Should reject 2 values + with pytest.raises(ValueError, match='must be of length "3"'): + plot.pixels = [100, 200] + + # Should reject 1 value + with pytest.raises(ValueError, match='must be of length "3"'): + plot.pixels = [100] + + +def test_voxel_plot_xml_roundtrip(): + """Test VoxelPlot XML serialization and deserialization""" + plot = openmc.VoxelPlot(name='test_voxel') + plot.width = [10.0, 20.0, 30.0] + plot.pixels = [100, 200, 300] + plot.origin = [1.0, 2.0, 3.0] + plot.color_by = 'cell' + plot.filename = 'voxel_plot' + + # Convert to XML and back + elem = plot.to_xml_element() + new_plot = openmc.VoxelPlot.from_xml_element(elem) + + # Check all attributes preserved + assert new_plot.name == plot.name + assert new_plot.width == pytest.approx(plot.width) + assert new_plot.pixels == tuple(plot.pixels) + assert new_plot.origin == pytest.approx(plot.origin) + assert new_plot.color_by == plot.color_by + assert new_plot.filename == plot.filename + + +def test_plot_deprecation_warning(): + """Test that Plot class raises deprecation warning""" + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + openmc.Plot() + + assert len(w) == 1 + assert issubclass(w[0].category, FutureWarning) + assert "deprecated" in str(w[0].message).lower() + + +def test_plot_returns_slice_plot(): + """Test that Plot() returns a SlicePlot instance""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + plot = openmc.Plot() + + # Should be an actual SlicePlot instance + assert isinstance(plot, openmc.SlicePlot) + + +def test_plot_type_setter_raises_error(): + """Test that setting plot.type raises a helpful error""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + plot = openmc.Plot() + + with pytest.raises(TypeError, match="no longer supported"): + plot.type = 'voxel' + + with pytest.raises(TypeError, match="no longer supported"): + plot.type = 'slice' + + +def test_plot_type_getter_warns(): + """Test that getting plot.type raises a deprecation warning""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + plot = openmc.Plot() + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + plot_type = plot.type + + assert plot_type == 'slice' + assert len(w) == 1 + assert issubclass(w[0].category, FutureWarning) + assert "deprecated" in str(w[0].message).lower() + + +def test_plots_collection_mixed_types(): + """Test Plots collection with different plot types""" + slice_plot = openmc.SlicePlot(name='slice') + voxel_plot = openmc.VoxelPlot(name='voxel') + wireframe_plot = openmc.WireframeRayTracePlot(name='wireframe') + + plots = openmc.Plots([slice_plot, voxel_plot, wireframe_plot]) + + assert len(plots) == 3 + assert isinstance(plots[0], openmc.SlicePlot) + assert isinstance(plots[1], openmc.VoxelPlot) + assert isinstance(plots[2], openmc.WireframeRayTracePlot) + + +def test_plots_collection_xml_roundtrip(run_in_tmpdir): + """Test XML export and import with new plot types""" + s1 = openmc.SlicePlot(name='slice1') + s1.width = [10.0, 20.0] + s1.basis = 'xz' + + v1 = openmc.VoxelPlot(name='voxel1') + v1.width = [10.0, 20.0, 30.0] + + plots = openmc.Plots([s1, v1]) + plots.export_to_xml() + + # Read back + new_plots = openmc.Plots.from_xml() + + assert len(new_plots) == 2 + assert isinstance(new_plots[0], openmc.SlicePlot) + assert isinstance(new_plots[1], openmc.VoxelPlot) + assert new_plots[0].name == 'slice1' + assert new_plots[1].name == 'voxel1' + assert new_plots[0].basis == 'xz' + assert new_plots[0].width == pytest.approx([10.0, 20.0]) + assert new_plots[1].width == pytest.approx([10.0, 20.0, 30.0]) From f70febb05f5fc32925d08707db48ff2c33dfe493 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Sat, 6 Dec 2025 15:54:30 -0600 Subject: [PATCH 468/671] Update CITATION.cff file (#3671) --- CITATION.cff | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/CITATION.cff b/CITATION.cff index 19b4213a1..ab27d89b8 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,9 +1,43 @@ +cff-version: 1.2.0 +message: "If you use this software, please cite it as below." +title: OpenMC +authors: +- family-names: Romano + given-names: Paul K. + orcid: "https://orcid.org/0000-0002-1147-045X" +- family-names: Shriwise + given-names: Patrick C. + orcid: "https://orcid.org/0000-0002-3979-7665" +- family-names: Shimwell + given-names: Jonathan + orcid: "https://orcid.org/0000-0001-6909-0946" +- family-names: Harper + given-names: Sterling +- family-names: Boyd + given-names: Will +- family-names: Nelson + given-names: Adam G. + orcid: "https://orcid.org/0000-0002-3614-0676" +- family-names: Tramm + given-names: John R. + orcid: "https://orcid.org/0000-0002-5397-4402" +- family-names: Ridley + given-names: Gavin + orcid: "https://orcid.org/0000-0003-1635-8042" +- family-names: Johnson + given-names: Andrew + orcid: "https://orcid.org/0000-0003-2125-8775" +- family-names: Peterson + given-names: Ethan E. + orcid: "https://orcid.org/0000-0002-5694-7194" +- family-names: Herman + given-names: Bryan R. preferred-citation: authors: - family-names: Romano given-names: Paul K. orcid: "https://orcid.org/0000-0002-1147-045X" - - final-names: Horelik + - family-names: Horelik given-names: Nicholas E. - family-names: Herman given-names: Bryan R. From 9b40ea008efb4ca48e51b5ab4569fb09c42c4380 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Mon, 8 Dec 2025 13:39:48 +0200 Subject: [PATCH 469/671] Read IFP settings only in source/eigenvalue run mode (#3673) --- src/settings.cpp | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/settings.cpp b/src/settings.cpp index 9dcf7c8db..d47e9b5e6 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -545,6 +545,20 @@ void read_settings_xml(pugi::xml_node root) } else if (rel_max_lost_particles <= 0.0 || rel_max_lost_particles >= 1.0) { fatal_error("Relative max lost particles must be between zero and one."); } + + // Check for user value for the number of generation of the Iterated Fission + // Probability (IFP) method + if (check_for_node(root, "ifp_n_generation")) { + ifp_n_generation = std::stoi(get_node_value(root, "ifp_n_generation")); + if (ifp_n_generation <= 0) { + fatal_error("'ifp_n_generation' must be greater than 0."); + } + // Avoid tallying 0 if IFP logs are not complete when active cycles start + if (ifp_n_generation > n_inactive) { + fatal_error("'ifp_n_generation' must be lower than or equal to the " + "number of inactive cycles."); + } + } } // Copy plotting random number seed if specified @@ -1130,20 +1144,6 @@ void read_settings_xml(pugi::xml_node root) temperature_range[1] = range.at(1); } - // Check for user value for the number of generation of the Iterated Fission - // Probability (IFP) method - if (check_for_node(root, "ifp_n_generation")) { - ifp_n_generation = std::stoi(get_node_value(root, "ifp_n_generation")); - if (ifp_n_generation <= 0) { - fatal_error("'ifp_n_generation' must be greater than 0."); - } - // Avoid tallying 0 if IFP logs are not complete when active cycles start - if (ifp_n_generation > n_inactive) { - fatal_error("'ifp_n_generation' must be lower than or equal to the " - "number of inactive cycles."); - } - } - // Check for tabular_legendre options if (check_for_node(root, "tabular_legendre")) { // Get pointer to tabular_legendre node From 8e06ed89986394ad46e6dccf7afc5157b6c9b870 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Mon, 8 Dec 2025 16:54:37 +0100 Subject: [PATCH 470/671] Allowing model.id_map to return overlap ID values (#3669) Co-authored-by: Paul Romano --- openmc/lib/plot.py | 12 +++--------- openmc/model/model.py | 6 ++++++ tests/unit_tests/test_model.py | 26 ++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/openmc/lib/plot.py b/openmc/lib/plot.py index f97348b20..68f61821c 100644 --- a/openmc/lib/plot.py +++ b/openmc/lib/plot.py @@ -52,7 +52,7 @@ class _PlotBase(Structure): C-Type Attributes ----------------- - origin : openmc.lib.plot._Position + origin_ : openmc.lib.plot._Position A position defining the origin of the plot. width_ : openmc.lib.plot._Position The width of the plot along the x, y, and z axes, respectively @@ -60,6 +60,8 @@ class _PlotBase(Structure): The axes basis of the plot view. pixels_ : c_size_t[3] The resolution of the plot in the horizontal and vertical dimensions + color_overlaps_ : c_bool + Whether to assign unique IDs (-3) to overlapping regions. level_ : c_int The universe level for the plot view @@ -187,14 +189,6 @@ class _PlotBase(Structure): def color_overlaps(self, color_overlaps): self.color_overlaps_ = color_overlaps - @property - def color_overlaps(self): - return self.color_overlaps_ - - @color_overlaps.setter - def color_overlaps(self, val): - self.color_overlaps_ = val - def __repr__(self): out_str = ["-----", "Plot:", diff --git a/openmc/model/model.py b/openmc/model/model.py index e2ac57eba..a9aaa481d 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1037,6 +1037,7 @@ class Model: width: Sequence[float] | None = None, pixels: int | Sequence[int] = 40000, basis: str = 'xy', + color_overlaps: bool = False, **init_kwargs ) -> np.ndarray: """Generate an ID map for domains based on the plot parameters @@ -1065,6 +1066,10 @@ class Model: total and the image aspect ratio based on the width argument. basis : {'xy', 'yz', 'xz'}, optional Basis of the plot. + color_overlaps : bool, optional + Whether to assign unique IDs (-3) to overlapping regions. If False, + overlapping regions will be assigned the ID of the lowest-numbered + cell that occupies that region. Defaults to False. **init_kwargs Keyword arguments passed to :meth:`Model.init_lib`. @@ -1089,6 +1094,7 @@ class Model: plot_obj.h_res = pixels[0] plot_obj.v_res = pixels[1] plot_obj.basis = basis + plot_obj.color_overlaps = color_overlaps # Silence output by default. Also set arguments to start in volume # calculation mode to avoid loading cross sections diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 9b28ff1c4..d553af53c 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -902,6 +902,32 @@ def test_id_map_aligned_model(): assert tr_material == 5, f"Expected material ID 5 at top-right corner, got {tr_material}" +def test_id_map_model_with_overlaps(): + """Test id_map with a model that has overlaps and color_overlaps option""" + surface1 = openmc.Sphere(r=50, boundary_type="vacuum") + surface2 = openmc.Sphere(r=30) + cell1 = openmc.Cell(region=-surface1) + cell2 = openmc.Cell(region=-surface2) + geometry = openmc.Geometry([cell1, cell2]) + settings = openmc.Settings() + model = openmc.Model(geometry=geometry, settings=settings) + id_slice = model.id_map( + pixels=(10, 10), + basis='xy', + origin=(0, 0, 0), + width=(100, 100), + ) + assert -3 not in id_slice # -3 indicates overlap region + id_slice = model.id_map( + pixels=(10, 10), + basis='xy', + origin=(0, 0, 0), + width=(100, 100), + color_overlaps=True, # enables id_map to return -3 for overlaps + ) + assert -3 in id_slice + + def test_setter_from_list(): mat = openmc.Material() model = openmc.Model(materials=[mat]) From a9dc84f75a8de4499c5bb04bede0f7a65eb844c5 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Mon, 8 Dec 2025 20:43:30 +0100 Subject: [PATCH 471/671] Allowing material making from class constructor (#3649) Co-authored-by: Jon Shimwell Co-authored-by: Paul Romano --- openmc/material.py | 48 ++++++++++++++++++++++++++--- tests/unit_tests/test_material.py | 51 +++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 4 deletions(-) diff --git a/openmc/material.py b/openmc/material.py index 1609da05a..735a05743 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -60,6 +60,26 @@ class Material(IDManagerMixin): temperature : float, optional Temperature of the material in Kelvin. If not specified, the material inherits the default temperature applied to the model. + density : float, optional + Density of the material (units defined separately) + density_units : str + Units used for `density`. Can be one of 'g/cm3', 'g/cc', 'kg/m3', + 'atom/b-cm', 'atom/cm3', 'sum', or 'macro'. The 'macro' unit only + applies in the case of a multi-group calculation. Defaults to 'sum'. + depletable : bool, optional + Indicate whether the material is depletable. Defaults to False. + volume : float, optional + Volume of the material in cm^3. This can either be set manually or + calculated in a stochastic volume calculation and added via the + :meth:`Material.add_volume_information` method. + components : dict of str to float or dict + Dictionary mapping element or nuclide names to their atom or weight + percent. To specify enrichment of an element, the entry of + ``components`` for that element must instead be a dictionary containing + the keyword arguments as well as a value for ``'percent'`` + percent_type : {'ao', 'wo'} + Whether the values in `components` should be interpreted as atom percent + ('ao') or weight percent ('wo'). Attributes ---------- @@ -111,17 +131,28 @@ class Material(IDManagerMixin): next_id = 1 used_ids = set() - def __init__(self, material_id=None, name='', temperature=None): + def __init__( + self, + material_id: int | None = None, + name: str = "", + temperature: float | None = None, + density: float | None = None, + density_units: str = "sum", + depletable: bool | None = False, + volume: float | None = None, + components: dict | None = None, + percent_type: str = "ao", + ): # Initialize class attributes self.id = material_id self.name = name self.temperature = temperature self._density = None - self._density_units = 'sum' - self._depletable = False + self._density_units = density_units + self._depletable = depletable self._paths = None self._num_instances = None - self._volume = None + self._volume = volume self._atoms = {} self._isotropic = [] self._ncrystal_cfg = None @@ -136,6 +167,15 @@ class Material(IDManagerMixin): # If specified, a list of table names self._sab = [] + # Set density if provided + if density is not None: + self.set_density(density_units, density) + + # Add components if provided + if components is not None: + self.add_components(components, percent_type=percent_type) + + def __repr__(self) -> str: string = 'Material\n' string += '{: <16}=\t{}\n'.format('\tID', self._id) diff --git a/tests/unit_tests/test_material.py b/tests/unit_tests/test_material.py index ce58339d7..764c98d41 100644 --- a/tests/unit_tests/test_material.py +++ b/tests/unit_tests/test_material.py @@ -768,3 +768,54 @@ def test_mean_free_path(): mat2.add_nuclide('Pb208', 1.0) mat2.set_density('g/cm3', 11.34) assert mat2.mean_free_path(energy=14e6) == pytest.approx(5.65, abs=1e-2) + + +def test_material_from_constructor(): + # Test that components and percent_type work in the constructor + components = { + 'Li': {'percent': 0.5, 'enrichment': 60.0, 'enrichment_target': 'Li7'}, + 'O16': 1.0, + 'Be': 0.5 + } + mat = openmc.Material( + material_id=123, + name="test-mat", + components=components, + percent_type="ao" + ) + # Check that nuclides were added + nuclide_names = [nuc.name for nuc in mat.nuclides] + assert 'O16' in nuclide_names + assert 'Be9' in nuclide_names + assert 'Li7' in nuclide_names + assert 'Li6' in nuclide_names + assert mat.id == 123 + assert mat.name == "test-mat" + + mat1 = openmc.Material( + **{ + "material_id": 1, + "name": "neutron_star", + "density": 1e17, + "density_units": "kg/m3", + } + ) + assert mat1.id == 1 + assert mat1.name == "neutron_star" + assert mat1._density == 1e17 + assert mat1._density_units == "kg/m3" + assert mat1.nuclides == [] + + mat2 = openmc.Material( + material_id=42, + name="plasma", + temperature=None, + density=1e-7, + density_units="g/cm3", + ) + assert mat2.id == 42 + assert mat2.name == "plasma" + assert mat2.temperature is None + assert mat2.density == 1e-7 + assert mat2.density_units == "g/cm3" + assert mat2.nuclides == [] From bc1348579f30281d1a84961389d87558d64cbaf6 Mon Sep 17 00:00:00 2001 From: Lewis Gross <43077972+lewisgross1296@users.noreply.github.com> Date: Wed, 10 Dec 2025 04:16:04 -0600 Subject: [PATCH 472/671] Generalize RotationalPeriodicBC for X-, Y-, or Z-axis (#3591) Co-authored-by: GuySten <62616591+GuySten@users.noreply.github.com> --- docs/source/io_formats/geometry.rst | 8 +- docs/source/usersguide/geometry.rst | 2 +- include/openmc/boundary_condition.h | 14 ++- openmc/surface.py | 14 +-- src/boundary_condition.cpp | 114 +++++++++--------- src/surface.cpp | 40 +++++- .../periodic_cyls/__init__.py | 0 tests/regression_tests/periodic_cyls/test.py | 91 ++++++++++++++ .../periodic_cyls/xcyl_model/inputs_true.dat | 29 +++++ .../periodic_cyls/xcyl_model/results_true.dat | 2 + .../periodic_cyls/ycyl_model/inputs_true.dat | 29 +++++ .../periodic_cyls/ycyl_model/results_true.dat | 2 + 12 files changed, 265 insertions(+), 80 deletions(-) create mode 100644 tests/regression_tests/periodic_cyls/__init__.py create mode 100644 tests/regression_tests/periodic_cyls/test.py create mode 100644 tests/regression_tests/periodic_cyls/xcyl_model/inputs_true.dat create mode 100644 tests/regression_tests/periodic_cyls/xcyl_model/results_true.dat create mode 100644 tests/regression_tests/periodic_cyls/ycyl_model/inputs_true.dat create mode 100644 tests/regression_tests/periodic_cyls/ycyl_model/results_true.dat diff --git a/docs/source/io_formats/geometry.rst b/docs/source/io_formats/geometry.rst index 6d0a37a24..dda1efa9a 100644 --- a/docs/source/io_formats/geometry.rst +++ b/docs/source/io_formats/geometry.rst @@ -38,11 +38,9 @@ Each ```` element can have the following attributes or sub-elements: :boundary: The boundary condition for the surface. This can be "transmission", - "vacuum", "reflective", or "periodic". Periodic boundary conditions can - only be applied to x-, y-, and z-planes. Only axis-aligned periodicity is - supported, i.e., x-planes can only be paired with x-planes. Specify which - planes are periodic and the code will automatically identify which planes - are paired together. + "vacuum", "reflective", or "periodic". Specify which planes are + periodic and the code will automatically identify which planes are + paired together. *Default*: "transmission" diff --git a/docs/source/usersguide/geometry.rst b/docs/source/usersguide/geometry.rst index 6f14ebfa5..8c68e4851 100644 --- a/docs/source/usersguide/geometry.rst +++ b/docs/source/usersguide/geometry.rst @@ -192,7 +192,7 @@ Otherwise it is necessary to specify pairs explicitly using the Both rotational and translational periodic boundary conditions are specified in the same fashion. If both planes have the same normal vector, a translational periodicity is assumed; rotational periodicity is assumed otherwise. Currently, -only rotations about the :math:`z`-axis are supported. +rotations must be about the :math:`x`-, :math:`y`-, or :math:`z`-axis. For a rotational periodic BC, the normal vectors of each surface must point inwards---towards the valid geometry. For example, a :class:`XPlane` and diff --git a/include/openmc/boundary_condition.h b/include/openmc/boundary_condition.h index af40131f1..5a14239e8 100644 --- a/include/openmc/boundary_condition.h +++ b/include/openmc/boundary_condition.h @@ -138,18 +138,26 @@ protected: //============================================================================== //! A BC that rotates particles about a global axis. // -//! Currently only rotations about the z-axis are supported. +//! Only rotations about the x, y, and z axes are supported. //============================================================================== class RotationalPeriodicBC : public PeriodicBC { public: - RotationalPeriodicBC(int i_surf, int j_surf); - + enum PeriodicAxis { x, y, z }; + RotationalPeriodicBC(int i_surf, int j_surf, PeriodicAxis axis); + double compute_periodic_rotation( + double rise_1, double run_1, double rise_2, double run_2) const; void handle_particle(Particle& p, const Surface& surf) const override; protected: //! Angle about the axis by which particle coordinates will be rotated double angle_; + //! Ensure that choice of axes is right handed. axis_1_idx_ corresponds to the + //! independent axis and axis_2_idx_ corresponds to the dependent axis in the + //! 2D plane perpendicular to the planes' axis of rotation + int zero_axis_idx_; + int axis_1_idx_; + int axis_2_idx_; }; } // namespace openmc diff --git a/openmc/surface.py b/openmc/surface.py index 4839783ff..1fe5fabdf 100644 --- a/openmc/surface.py +++ b/openmc/surface.py @@ -123,9 +123,8 @@ class Surface(IDManagerMixin, ABC): boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles - freely pass through the surface. Note that periodic boundary conditions - can only be applied to x-, y-, and z-planes, and only axis-aligned - periodicity is supported. + freely pass through the surface. Note that only axis-aligned + periodicity is supported around the x-, y-, and z-axes. albedo : float, optional Albedo of the surfaces as a ratio of particle weight after interaction with the surface to the initial weight. Values must be positive. Only @@ -822,8 +821,7 @@ class XPlane(PlaneMixin, Surface): boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles - freely pass through the surface. Only axis-aligned periodicity is - supported, i.e., x-planes can only be paired with x-planes. + freely pass through the surface. albedo : float, optional Albedo of the surfaces as a ratio of particle weight after interaction with the surface to the initial weight. Values must be positive. Only @@ -887,8 +885,7 @@ class YPlane(PlaneMixin, Surface): boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles - freely pass through the surface. Only axis-aligned periodicity is - supported, i.e., y-planes can only be paired with y-planes. + freely pass through the surface. albedo : float, optional Albedo of the surfaces as a ratio of particle weight after interaction with the surface to the initial weight. Values must be positive. Only @@ -952,8 +949,7 @@ class ZPlane(PlaneMixin, Surface): boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles - freely pass through the surface. Only axis-aligned periodicity is - supported, i.e., z-planes can only be paired with z-planes. + freely pass through the surface. albedo : float, optional Albedo of the surfaces as a ratio of particle weight after interaction with the surface to the initial weight. Values must be positive. Only diff --git a/src/boundary_condition.cpp b/src/boundary_condition.cpp index 7216ac896..2840b3c7d 100644 --- a/src/boundary_condition.cpp +++ b/src/boundary_condition.cpp @@ -158,63 +158,44 @@ void TranslationalPeriodicBC::handle_particle( // RotationalPeriodicBC implementation //============================================================================== -RotationalPeriodicBC::RotationalPeriodicBC(int i_surf, int j_surf) +RotationalPeriodicBC::RotationalPeriodicBC( + int i_surf, int j_surf, PeriodicAxis axis) : PeriodicBC(i_surf, j_surf) { Surface& surf1 {*model::surfaces[i_surf_]}; Surface& surf2 {*model::surfaces[j_surf_]}; - // Check the type of the first surface - bool surf1_is_xyplane; - if (const auto* ptr = dynamic_cast(&surf1)) { - surf1_is_xyplane = true; - } else if (const auto* ptr = dynamic_cast(&surf1)) { - surf1_is_xyplane = true; - } else if (const auto* ptr = dynamic_cast(&surf1)) { - surf1_is_xyplane = false; - } else { - throw std::invalid_argument(fmt::format( - "Surface {} is an invalid type for " - "rotational periodic BCs. Only x-planes, y-planes, or general planes " - "(that are perpendicular to z) are supported for these BCs.", - surf1.id_)); - } - - // Check the type of the second surface - bool surf2_is_xyplane; - if (const auto* ptr = dynamic_cast(&surf2)) { - surf2_is_xyplane = true; - } else if (const auto* ptr = dynamic_cast(&surf2)) { - surf2_is_xyplane = true; - } else if (const auto* ptr = dynamic_cast(&surf2)) { - surf2_is_xyplane = false; - } else { - throw std::invalid_argument(fmt::format( - "Surface {} is an invalid type for " - "rotational periodic BCs. Only x-planes, y-planes, or general planes " - "(that are perpendicular to z) are supported for these BCs.", - surf2.id_)); + // below convention for right handed coordinate system + switch (axis) { + case x: + zero_axis_idx_ = 0; // x component of plane must be zero + axis_1_idx_ = 1; // y component independent + axis_2_idx_ = 2; // z component dependent + break; + case y: + // for a right handed coordinate system, z should be the independent axis + // but this would cause the y-rotation case to be different than the other + // two. using a left handed coordinate system and a negative rotation the + // compute angle and rotation matrix behavior mimics that of the x and z + // cases + zero_axis_idx_ = 1; // y component of plane must be zero + axis_1_idx_ = 0; // x component independent + axis_2_idx_ = 2; // z component dependent + break; + case z: + zero_axis_idx_ = 2; // z component of plane must be zero + axis_1_idx_ = 0; // x component independent + axis_2_idx_ = 1; // y component dependent + break; + default: + throw std::invalid_argument( + fmt::format("You've specified an axis that is not x, y, or z.")); } // Compute the surface normal vectors and make sure they are perpendicular - // to the z-axis + // to the correct axis Direction norm1 = surf1.normal({0, 0, 0}); Direction norm2 = surf2.normal({0, 0, 0}); - if (std::abs(norm1.z) > FP_PRECISION) { - throw std::invalid_argument(fmt::format( - "Rotational periodic BCs are only " - "supported for rotations about the z-axis, but surface {} is not " - "perpendicular to the z-axis.", - surf1.id_)); - } - if (std::abs(norm2.z) > FP_PRECISION) { - throw std::invalid_argument(fmt::format( - "Rotational periodic BCs are only " - "supported for rotations about the z-axis, but surface {} is not " - "perpendicular to the z-axis.", - surf2.id_)); - } - // Make sure both surfaces intersect the origin if (std::abs(surf1.evaluate({0, 0, 0})) > FP_COINCIDENT) { throw std::invalid_argument(fmt::format( @@ -231,15 +212,8 @@ RotationalPeriodicBC::RotationalPeriodicBC(int i_surf, int j_surf) surf2.id_)); } - // Compute the BC rotation angle. Here it is assumed that both surface - // normal vectors point inwards---towards the valid geometry region. - // Consequently, the rotation angle is not the difference between the two - // normals, but is instead the difference between one normal and one - // anti-normal. (An incident ray on one surface must be an outgoing ray on - // the other surface after rotation hence the anti-normal.) - double theta1 = std::atan2(norm1.y, norm1.x); - double theta2 = std::atan2(norm2.y, norm2.x) + PI; - angle_ = theta2 - theta1; + angle_ = compute_periodic_rotation(norm1[axis_2_idx_], norm1[axis_1_idx_], + norm2[axis_2_idx_], norm2[axis_1_idx_]); // Warn the user if the angle does not evenly divide a circle double rem = std::abs(std::remainder((2 * PI / angle_), 1.0)); @@ -251,6 +225,20 @@ RotationalPeriodicBC::RotationalPeriodicBC(int i_surf, int j_surf) } } +double RotationalPeriodicBC::compute_periodic_rotation( + double rise_1, double run_1, double rise_2, double run_2) const +{ + // Compute the BC rotation angle. Here it is assumed that both surface + // normal vectors point inwards---towards the valid geometry region. + // Consequently, the rotation angle is not the difference between the two + // normals, but is instead the difference between one normal and one + // anti-normal. (An incident ray on one surface must be an outgoing ray on + // the other surface after rotation hence the anti-normal.) + double theta1 = std::atan2(rise_1, run_1); + double theta2 = std::atan2(rise_2, run_2) + PI; + return theta2 - theta1; +} + void RotationalPeriodicBC::handle_particle( Particle& p, const Surface& surf) const { @@ -278,10 +266,16 @@ void RotationalPeriodicBC::handle_particle( Direction u = p.u(); double cos_theta = std::cos(theta); double sin_theta = std::sin(theta); - Position new_r = { - cos_theta * r.x - sin_theta * r.y, sin_theta * r.x + cos_theta * r.y, r.z}; - Direction new_u = { - cos_theta * u.x - sin_theta * u.y, sin_theta * u.x + cos_theta * u.y, u.z}; + + Position new_r; + new_r[zero_axis_idx_] = r[zero_axis_idx_]; + new_r[axis_1_idx_] = cos_theta * r[axis_1_idx_] - sin_theta * r[axis_2_idx_]; + new_r[axis_2_idx_] = sin_theta * r[axis_1_idx_] + cos_theta * r[axis_2_idx_]; + + Direction new_u; + new_u[zero_axis_idx_] = u[zero_axis_idx_]; + new_u[axis_1_idx_] = cos_theta * u[axis_1_idx_] - sin_theta * u[axis_2_idx_]; + new_u[axis_2_idx_] = sin_theta * u[axis_1_idx_] + cos_theta * u[axis_2_idx_]; // Handle the effects of the surface albedo on the particle's weight. BoundaryCondition::handle_albedo(p, surf); diff --git a/src/surface.cpp b/src/surface.cpp index ea19514a3..09fc0f24f 100644 --- a/src/surface.cpp +++ b/src/surface.cpp @@ -1334,8 +1334,44 @@ void read_surfaces(pugi::xml_node node) surf1.bc_ = make_unique(i_surf, j_surf); surf2.bc_ = make_unique(i_surf, j_surf); } else { - surf1.bc_ = make_unique(i_surf, j_surf); - surf2.bc_ = make_unique(i_surf, j_surf); + // check that both normals have at least one 0 component + if (std::abs(norm1.x) > FP_PRECISION && + std::abs(norm1.y) > FP_PRECISION && + std::abs(norm1.z) > FP_PRECISION) { + fatal_error(fmt::format( + "The normal ({}) of the periodic surface ({}) does not contain any " + "component with a zero value. A RotationalPeriodicBC requires one " + "component which is zero for both plane normals.", + norm1, i_surf)); + } + if (std::abs(norm2.x) > FP_PRECISION && + std::abs(norm2.y) > FP_PRECISION && + std::abs(norm2.z) > FP_PRECISION) { + fatal_error(fmt::format( + "The normal ({}) of the periodic surface ({}) does not contain any " + "component with a zero value. A RotationalPeriodicBC requires one " + "component which is zero for both plane normals.", + norm2, j_surf)); + } + // find common zero component, which indicates the periodic axis + RotationalPeriodicBC::PeriodicAxis axis; + if (std::abs(norm1.x) <= FP_PRECISION && + std::abs(norm2.x) <= FP_PRECISION) { + axis = RotationalPeriodicBC::PeriodicAxis::x; + } else if (std::abs(norm1.y) <= FP_PRECISION && + std::abs(norm2.y) <= FP_PRECISION) { + axis = RotationalPeriodicBC::PeriodicAxis::y; + } else if (std::abs(norm1.z) <= FP_PRECISION && + std::abs(norm2.z) <= FP_PRECISION) { + axis = RotationalPeriodicBC::PeriodicAxis::z; + } else { + fatal_error(fmt::format( + "There is no component which is 0.0 in both normal vectors. This " + "indicates that the two planes are not periodic about the X, Y, or Z " + "axis, which is not supported.")); + } + surf1.bc_ = make_unique(i_surf, j_surf, axis); + surf2.bc_ = make_unique(i_surf, j_surf, axis); } // If albedo data is present in albedo map, set the boundary albedo. diff --git a/tests/regression_tests/periodic_cyls/__init__.py b/tests/regression_tests/periodic_cyls/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/periodic_cyls/test.py b/tests/regression_tests/periodic_cyls/test.py new file mode 100644 index 000000000..a341e3799 --- /dev/null +++ b/tests/regression_tests/periodic_cyls/test.py @@ -0,0 +1,91 @@ +import openmc +import numpy as np +import pytest +from openmc.utility_funcs import change_directory +from tests.testing_harness import PyAPITestHarness + + +@pytest.fixture +def xcyl_model(): + model = openmc.Model() + # Define materials + fuel = openmc.Material() + fuel.add_nuclide('U235', 0.2) + fuel.add_nuclide('U238', 0.8) + fuel.set_density('g/cc', 19.1) + model.materials = openmc.Materials([fuel]) + + # Define geometry + # finite cylinder + x_min = openmc.XPlane(x0=0.0, boundary_type='reflective') + x_max = openmc.XPlane(x0=20.0, boundary_type='reflective') + x_cyl = openmc.XCylinder(r=20.0,boundary_type='vacuum') + # slice cylinder for periodic BC + periodic_bounding_yplane = openmc.YPlane(y0=0, boundary_type='periodic') + periodic_bounding_plane = openmc.Plane( + a=0.0, b=-np.sqrt(3) / 3, c=1, boundary_type='periodic', + ) + sixth_cyl_cell = openmc.Cell(1, fill=fuel, region = + +x_min &- x_max & -x_cyl & +periodic_bounding_yplane & +periodic_bounding_plane) + periodic_bounding_yplane.periodic_surface = periodic_bounding_plane + periodic_bounding_plane.periodic_surface = periodic_bounding_yplane + + model.geometry = openmc.Geometry([sixth_cyl_cell]) + + + # Define settings + model.settings.particles = 1000 + model.settings.batches = 4 + model.settings.inactive = 0 + model.settings.source = openmc.IndependentSource(space=openmc.stats.Box( + (0, 0, 0), (20, 20, 20)) + ) + return model + +@pytest.fixture +def ycyl_model(): + model = openmc.Model() + # Define materials + fuel = openmc.Material() + fuel.add_nuclide('U235', 0.2) + fuel.add_nuclide('U238', 0.8) + fuel.set_density('g/cc', 19.1) + model.materials = openmc.Materials([fuel]) + + # Define geometry + # finite cylinder + y_min = openmc.YPlane(y0=0.0, boundary_type='reflective') + y_max = openmc.YPlane(y0=20.0, boundary_type='reflective') + y_cyl = openmc.YCylinder(r=20.0,boundary_type='vacuum') + # slice cylinder for periodic BC + periodic_bounding_xplane = openmc.XPlane(x0=0, boundary_type='periodic') + periodic_bounding_plane = openmc.Plane( + a=-np.sqrt(3) / 3, b=0.0, c=1, boundary_type='periodic', + ) + sixth_cyl_cell = openmc.Cell(1, fill=fuel, region = + +y_min &- y_max & -y_cyl & +periodic_bounding_xplane & +periodic_bounding_plane) + periodic_bounding_xplane.periodic_surface = periodic_bounding_plane + periodic_bounding_plane.periodic_surface = periodic_bounding_xplane + model.geometry = openmc.Geometry([sixth_cyl_cell]) + + + # Define settings + model.settings.particles = 1000 + model.settings.batches = 4 + model.settings.inactive = 0 + model.settings.source = openmc.IndependentSource(space=openmc.stats.Box( + (0, 0, 0), (20, 20, 20)) + ) + return model + +def test_xcyl(xcyl_model): + with change_directory("xcyl_model"): + openmc.reset_auto_ids() + harness = PyAPITestHarness('statepoint.4.h5', xcyl_model) + harness.main() + +def test_ycyl(ycyl_model): + with change_directory("ycyl_model"): + openmc.reset_auto_ids() + harness = PyAPITestHarness('statepoint.4.h5', ycyl_model) + harness.main() \ No newline at end of file diff --git a/tests/regression_tests/periodic_cyls/xcyl_model/inputs_true.dat b/tests/regression_tests/periodic_cyls/xcyl_model/inputs_true.dat new file mode 100644 index 000000000..7d1ecf426 --- /dev/null +++ b/tests/regression_tests/periodic_cyls/xcyl_model/inputs_true.dat @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + eigenvalue + 1000 + 4 + 0 + + + 0 0 0 20 20 20 + + + + diff --git a/tests/regression_tests/periodic_cyls/xcyl_model/results_true.dat b/tests/regression_tests/periodic_cyls/xcyl_model/results_true.dat new file mode 100644 index 000000000..c7e3eb670 --- /dev/null +++ b/tests/regression_tests/periodic_cyls/xcyl_model/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +1.082283E+00 6.676373E-02 diff --git a/tests/regression_tests/periodic_cyls/ycyl_model/inputs_true.dat b/tests/regression_tests/periodic_cyls/ycyl_model/inputs_true.dat new file mode 100644 index 000000000..f3ca7e0f4 --- /dev/null +++ b/tests/regression_tests/periodic_cyls/ycyl_model/inputs_true.dat @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + eigenvalue + 1000 + 4 + 0 + + + 0 0 0 20 20 20 + + + + diff --git a/tests/regression_tests/periodic_cyls/ycyl_model/results_true.dat b/tests/regression_tests/periodic_cyls/ycyl_model/results_true.dat new file mode 100644 index 000000000..562467f2c --- /dev/null +++ b/tests/regression_tests/periodic_cyls/ycyl_model/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +1.082652E+00 3.316031E-02 From d09fbc61b5e164e9a434de85358dfe48a3f1b0b9 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Wed, 10 Dec 2025 09:26:26 -0600 Subject: [PATCH 473/671] Copilot Bot File (#3651) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Paul Romano Co-authored-by: Patrick Shriwise --- AGENTS.md | 280 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..32e30213a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,280 @@ +# OpenMC AI Coding Agent Instructions + +## Project Overview + +OpenMC is a Monte Carlo particle transport code for simulating nuclear reactors, +fusion devices, or other systems with neutron/photon radiation. It's a hybrid +C++17/Python codebase where: +- **C++ core** (`src/`, `include/openmc/`) handles the computationally intensive transport simulation +- **Python API** (`openmc/`) provides user-facing model building, post-processing, and depletion capabilities +- **C API bindings** (`openmc/lib/`) wrap the C++ library via ctypes for runtime control + +## Architecture & Key Components + +### C++ Component Structure +- **Global vectors of unique_ptrs**: Core objects like `model::cells`, `model::universes`, `nuclides` are stored as `vector>` in nested namespaces (`openmc::model`, `openmc::simulation`, `openmc::settings`, `openmc::data`) +- **Custom container types**: OpenMC provides its own `vector`, `array`, `unique_ptr`, and `make_unique` in the `openmc::` namespace (defined in `vector.h`, `array.h`, `memory.h`). These are currently typedefs to `std::` equivalents but may become custom implementations for accelerator support. Always use `openmc::vector`, not `std::vector`. +- **Geometry systems**: + - **CSG (default)**: Arbitrarily complex Constructive Solid Geometry using `Surface`, `Region`, `Cell`, `Universe`, `Lattice` + - **DAGMC**: CAD-based geometry via Direct Accelerated Geometry Monte Carlo (optional, requires `OPENMC_USE_DAGMC`) + - **Unstructured mesh**: libMesh-based geometry (optional, requires `OPENMC_USE_LIBMESH`) +- **Particle tracking**: `Particle` class with `GeometryState` manages particle transport through geometry +- **Tallies**: Score quantities during simulation via `Filter` and `Tally` objects +- **Random ray solver**: Alternative deterministic method in `src/random_ray/` +- **Optional features**: DAGMC (CAD geometry), libMesh (unstructured mesh), MPI, all controlled by `#ifdef OPENMC_MPI`, etc. + +### Python Component Structure +- **ID management**: All geometry objects (Cell, Surface, Material, etc.) inherit from `IDManagerMixin` which auto-assigns unique integer IDs and tracks them via class-level `used_ids` and `next_id` +- **Input validation**: Extensive use of `openmc.checkvalue` module functions (`check_type`, `check_value`, `check_length`) for all setters +- **XML I/O**: Most classes implement `to_xml_element()` and `from_xml_element()` for serialization to OpenMC's XML input format +- **HDF5 output**: Post-simulation data in statepoint files read via `openmc.StatePoint` +- **Depletion**: `openmc.deplete` implements burnup via operator-splitting with various integrators (Predictor, CECM, etc.) +- **Nuclear Data**: `openmc.data` provides programmatic access to nuclear data files (ENDF, ACE, HDF5) + +## Critical Build & Test Workflows + +### Build Dependencies +- **C++17 compiler**: GCC, Clang, or Intel +- **CMake** (3.16+): Required for configuring and building the C++ library +- **HDF5**: Required for cross section data and output file formats +- **libpng**: Used for generating visualization when OpenMC is run in plotting mode + +Without CMake and HDF5, OpenMC cannot be compiled. + +### Building the C++ Library +```bash +# Configure with CMake (from build/ directory) +cmake .. -DOPENMC_USE_MPI=ON -DOPENMC_USE_OPENMP=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo + +# Available CMake options (all default OFF except OPENMC_USE_OPENMP and OPENMC_BUILD_TESTS): +# -DOPENMC_USE_OPENMP=ON/OFF # OpenMP parallelism +# -DOPENMC_USE_MPI=ON/OFF # MPI support +# -DOPENMC_USE_DAGMC=ON/OFF # CAD geometry support +# -DOPENMC_USE_LIBMESH=ON/OFF # Unstructured mesh +# -DOPENMC_ENABLE_PROFILE=ON/OFF # Profiling flags +# -DOPENMC_ENABLE_COVERAGE=ON/OFF # Coverage analysis + +# Build +make -j + +# C++ unit tests (uses Catch2) +ctest +``` + +### Python Development +```bash +# Install in development mode (requires building C++ library first) +pip install -e . + +# Python tests (uses pytest) +pytest tests/unit_tests/ # Fast unit tests +pytest tests/regression_tests/ # Full regression suite (requires nuclear data) +``` + +### Nuclear Data Setup (CRITICAL for Running OpenMC) +Most tests require the NNDC HDF5 nuclear cross-section library. + +**Important**: Check if `OPENMC_CROSS_SECTIONS` is already set in the user's +environment before downloading, as many users already have nuclear data +installed. Though do note that if this variable is present that it may point to +different cross section data and that the NNDC data is required for tests to +pass. + +**If not already configured, download and setup:** +```bash +# Download NNDC HDF5 cross section library (~800 MB compressed) +wget -q -O - https://anl.box.com/shared/static/teaup95cqv8s9nn56hfn7ku8mmelr95p.xz | tar -C $HOME -xJ + +# Set environment variable (add to ~/.bashrc or ~/.zshrc for persistence) +export OPENMC_CROSS_SECTIONS=$HOME/nndc_hdf5/cross_sections.xml +``` + +**Alternative**: Use the provided download script (checks if data exists before downloading): +```bash +bash tools/ci/download-xs.sh # Downloads both NNDC HDF5 and ENDF/B-VII.1 data +``` + +Without this data, regression tests will fail with "No cross_sections.xml file +found" errors, or, in the case that alternative cross section data is configured +the tests will execute but will not pass. The `cross_sections.xml` file is an +index listing paths to individual HDF5 nuclear data files for each nuclide. + +## Testing Expectations + +### Environment Requirements + + - **Data**: As described above, OpenMC's test suite requires OpenMC to be configured with NNDC data. + - **OpenMP Settings**: OpenMC's tests may fail is more than two OpenMP threads are used. The environment variable `OMP_NUM_THREADS=2` should be set to avoid sporadic test failures. + - **Executable configuration**: The OpenMC executable should compiled with debug symbols enabled. + +### C++ Tests +Located in `tests/cpp_unit_tests/`, use Catch2 framework. Run via `ctest` after building with `-DOPENMC_BUILD_TESTS=ON`. + +### Python Unit Tests +Located in `tests/unit_tests/`, these are fast, standalone tests that verify Python API functionality without running full simulations. Use standard pytest patterns: + +**Categories**: +- **API validation**: Test object creation, property setters/getters, XML serialization (e.g., `test_material.py`, `test_cell.py`, `test_source.py`) +- **Data processing**: Test nuclear data handling, cross sections, depletion chains (e.g., `test_data_neutron.py`, `test_deplete_chain.py`) +- **Library bindings**: Test `openmc.lib` ctypes interface with `model.init_lib()`/`model.finalize_lib()` (e.g., `test_lib.py`) +- **Geometry operations**: Test bounding boxes, containment, lattice generation (e.g., `test_bounding_box.py`, `test_lattice.py`) + +**Common patterns**: +- Use fixtures from `tests/unit_tests/conftest.py` (e.g., `uo2`, `water`, `sphere_model`) +- Test invalid inputs with `pytest.raises(ValueError)` or `pytest.raises(TypeError)` +- Use `run_in_tmpdir` fixture for tests that create files +- Tests with `openmc.lib` require calling `model.init_lib()` in try/finally with `model.finalize_lib()` + +**Example**: +```python +def test_material_properties(): + m = openmc.Material() + m.add_nuclide('U235', 1.0) + assert 'U235' in m.nuclides + + with pytest.raises(TypeError): + m.add_nuclide('H1', '1.0') # Invalid type +``` + +Unit tests should be fast. For tests requiring simulation output, use regression tests instead. + +### Python Regression Tests +Regression tests compare OpenMC output against reference data. **Prefer using existing models from `openmc.examples` or those found in tests/unit_tests/conftest.py** (like `pwr_pin_cell()`, `pwr_assembly()`, `slab_mg()`) rather than building from scratch. + +**Test Harness Types** (in `tests/testing_harness.py`): +- **PyAPITestHarness**: Standard harness for Python API tests. Compares `inputs_true.dat` (XML hash) and `results_true.dat` (statepoint k-eff and tally values). Requires `model.xml` generation. +- **HashedPyAPITestHarness**: Like PyAPITestHarness but hashes the results for compact comparison +- **TolerantPyAPITestHarness**: For tests with floating-point non-associativity (e.g., random ray solver with single precision). Uses relative tolerance comparisons. +- **WeightWindowPyAPITestHarness**: Compares weight window bounds from `weight_windows.h5` +- **CollisionTrackTestHarness**: Compares collision track data from `collision_track.h5` against `collision_track_true.h5` +- **TestHarness**: Base harness for XML-based tests (no Python model building) +- **PlotTestHarness**: Compares plot output files (PNG or voxel HDF5) +- **CMFDTestHarness**: Specialized for CMFD acceleration tests +- **ParticleRestartTestHarness**: Tests particle restart functionality + +Almost all cases use either `PyAPITestHarness` or `HashedPyAPITestHarness` + +**Example Test**: +```python +from openmc.examples import pwr_pin_cell +from tests.testing_harness import PyAPITestHarness + +def test_my_feature(): + model = pwr_pin_cell() + model.settings.particles = 1000 # Modify to exercise feature + harness = PyAPITestHarness('statepoint.10.h5', model) + 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`) + +**Critical**: When modifying OpenMC code, regenerate affected test references with `pytest --update` and commit updated reference files. + +### Test Configuration + +`pytest.ini` sets: `python_files = test*.py`, `python_classes = NoThanks` (disables class-based test collection). + +### Testing Options + +For builds of OpenMC with MPI enabled, the `--mpi` flag should be passed to the test suite to ensure that appropriate tests are executed using two MPI processes. + +The entire test suite can be executed with OpenMC running in event-based mode (instead of the default history-based mode) by providing the `--event` flag to the `pytest` command. + +## Cross-Language Boundaries + +The C API (defined in `include/openmc/capi.h`) exposes C++ functionality to Python via ctypes bindings in `openmc/lib/`. Example: +```cpp +// C++ API in capi.h +extern "C" int openmc_run(); + +// Python binding in openmc/lib/core.py +_dll.openmc_run.restype = c_int +def run(): + _dll.openmc_run() +``` + +When modifying C++ public APIs, update corresponding ctypes signatures in `openmc/lib/*.py`. + +## Code Style & Conventions + +### C++ Style (enforced by .clang-format) + OpenMC generally tries to follow C++ core guidelines where possible + (https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines) and follow + modern C++ practices (e.g. RAII) whenever possible. + +- **Naming**: + - Classes: `CamelCase` (e.g., `HexLattice`) + - Functions/methods: `snake_case` (e.g., `get_indices`) + - Variables: `snake_case` with trailing underscore for class members (e.g., `n_particles_`, `energy_`) + - Constants: `UPPER_SNAKE_CASE` (e.g., `SQRT_PI`) +- **Namespaces**: All code in `openmc::` namespace, global state in sub-namespaces +- **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` + +### Python Style +- **PEP8** compliant +- **Docstrings**: numpydoc format for all public functions/methods +- **Type hints**: Use sparingly, primarily for complex signatures +- **Path handling**: Use `pathlib.Path` for filesystem operations, accept `str | os.PathLike` in function arguments +- **Dependencies**: Core dependencies only (numpy, scipy, h5py, pandas, matplotlib, lxml, ipython, uncertainties, setuptools, endf). Other packages must be optional +- **Python version**: Minimum 3.11 (as of Nov 2025) + +### ID Management Pattern (Python) +When creating geometry objects, IDs can be auto-assigned or explicit: +```python +# Auto-assigned ID +cell = openmc.Cell() # Gets next available ID + +# Explicit ID +cell = openmc.Cell(id=10) # Warning if ID already used + +# Reset all IDs (useful in test fixtures) +openmc.reset_auto_ids() +``` + +### Input Validation Pattern (Python) +All setters use checkvalue functions: +```python +import openmc.checkvalue as cv + +@property +def temperature(self): + return self._temperature + +@temperature.setter +def temperature(self, temp): + cv.check_type('temperature', temp, Real) + cv.check_greater_than('temperature', temp, 0.0) + self._temperature = temp +``` + +### Working with HDF5 Files +C++ uses custom HDF5 wrappers in `src/hdf5_interface.cpp`. Python uses h5py directly. Statepoint format version is `VERSION_STATEPOINT` in `include/openmc/constants.h`. + +### Conditional Compilation +Check for optional features: +```cpp +#ifdef OPENMC_MPI + // MPI-specific code +#endif + +#ifdef OPENMC_DAGMC + // DAGMC-specific code +#endif +``` + +## Documentation + +- **User docs**: Sphinx documentation in `docs/source/` hosted at https://docs.openmc.org +- **C++ docs**: Doxygen-style comments with `\brief`, `\param` tags +- **Python docs**: numpydoc format docstrings + +## Common Pitfalls + +1. **Forgetting nuclear data**: Tests fail without `OPENMC_CROSS_SECTIONS` environment variable +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 From a62e754bbb77646a81bcbe2dd6bcb58e9686286d Mon Sep 17 00:00:00 2001 From: pranav Date: Thu, 11 Dec 2025 09:24:28 +0530 Subject: [PATCH 474/671] Add user documentation for DAGMCUniverse synchronization (#3674) Co-authored-by: Patrick Shriwise --- docs/source/usersguide/geometry.rst | 85 +++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/docs/source/usersguide/geometry.rst b/docs/source/usersguide/geometry.rst index 8c68e4851..f51fbd73c 100644 --- a/docs/source/usersguide/geometry.rst +++ b/docs/source/usersguide/geometry.rst @@ -182,6 +182,8 @@ boundary condition. Periodic boundary conditions can be applied to pairs of planar surfaces. If there are only two periodic surfaces they will be matched automatically. + + Otherwise it is necessary to specify pairs explicitly using the :attr:`Surface.periodic_surface` attribute as in the following example:: @@ -530,6 +532,89 @@ UWUW and OpenMC material ID space will cause an error. To automatically resolve these ID overlaps, ``auto_ids`` can be set to ``True`` to append the UWUW material IDs to the OpenMC material ID space. + +Material overrides and differentiation +-------------------------------------- + +Programmatic access to DAGMC cell information for material overrides +and differentiation requires synchronization of the DAGMC universe +representation across Python and C-API:: + + model.init_lib() + model.sync_dagmc_universes() + model.finalize_lib() + +Upon completion of these steps, the :attr:`DAGMCUniverse.cells` attribute will +be populated with :class:`DAGMCCell` proxy objects that represent the cells +defined in the DAGMC model. The :class:`DAGMCCell` objects will have +:class:`openmc.Material`'s' applied according to the assignments upon +initialization of the model. These materials can be replaced in the same manner +as :class:`openmc.Cell` objects to override material assignments in the DAGMC +model. + +Depletion with DAGMC geometry +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The synchronization of :class:`openmc.DAGMCUniverse`'s is important for +depletion calculations using DAGMC geometry when materials need to be +differentiated to perform material burnup independently in each DAGMC cell. See +:meth:`openmc.model.Model.differentiate_mats`. + +Material overrides +~~~~~~~~~~~~~~~~~~ + +OpenMC supports overriding material assignments defined inside a DAGMC HDF5 +model so that CAD-assigned materials can be replaced by :class:`openmc.Material` +objects. This is useful when the CAD geometry provides the shape but OpenMC +materials (specific nuclide content, densities, or depletion behavior) are +required. + + +Replacing materials by name +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If a DAGMC file includes material name tags, you can replace all cells that +reference a particular name with an :class:`openmc.Material` using +:meth:`~openmc.DAGMCUniverse.replace_material_assignment`:: + + import openmc + + dag_univ = openmc.DAGMCUniverse('dagmc.h5m') + + fuel = openmc.Material(name='fuel') + fuel.add_nuclide('U235', 0.05) + fuel.add_nuclide('U238', 0.95) + fuel.set_density('g/cm3', 10.5) + + dag_univ.replace_material_assignment('Fuel', fuel) + +This lets you keep CAD geometry while adopting OpenMC material definitions. + +Per-cell material overrides +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To assign overrides without initializing :class:`openmc.Model`, the +:meth:`openmc.DAGMCUniverse.add_material_override` method can be used to assign +materials to particular DAGMC cells. The method accepts either an integer cell +ID:: + + dag_univ = openmc.DAGMCUniverse('dagmc.h5m') + + enriched = openmc.Material(name='fuel_enriched') + enriched.add_nuclide('U235', 0.10) + enriched.add_nuclide('U238', 0.90) + enriched.set_density('g/cm3', 10.5) + + dag_univ.add_material_override(1, enriched) + +In the case that the :class:`openmc.DAGMCUniverse` has already been synchronized, +a :class:`openmc.DAGMCCell` object can also be provide to assign the material. + +Overrides are written to the `` element of the +:ref:` ` XML element so the C++ core can apply +them on initialization. + + .. _Direct Accelerated Geometry Monte Carlo: https://svalinn.github.io/DAGMC/ .. _University of Wisconsin Unified Workflow: https://svalinn.github.io/DAGMC/usersguide/uw2.html From 5c4121efd2ad6f2ef2b3bc394157a4eb6d9a1a73 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Fri, 12 Dec 2025 06:01:10 +0200 Subject: [PATCH 475/671] Fix hdf5 source_bank struct size. (#3676) --- include/openmc/bank_io.h | 16 ++++++------ src/collision_track.cpp | 4 +-- src/state_point.cpp | 23 +++++++++++------- .../surface_source/surface_source_true.h5 | Bin 106144 -> 88096 bytes .../case-01/surface_source_true.h5 | Bin 33344 -> 29296 bytes .../case-02/surface_source_true.h5 | Bin 13896 -> 13588 bytes .../case-03/surface_source_true.h5 | Bin 33344 -> 29296 bytes .../case-04/surface_source_true.h5 | Bin 33344 -> 29296 bytes .../case-05/surface_source_true.h5 | Bin 33344 -> 29296 bytes .../case-06/surface_source_true.h5 | Bin 33344 -> 29296 bytes .../case-07/surface_source_true.h5 | Bin 33344 -> 29296 bytes .../case-08/surface_source_true.h5 | Bin 33344 -> 29296 bytes .../case-09/surface_source_true.h5 | Bin 33344 -> 29296 bytes .../case-10/surface_source_true.h5 | Bin 33344 -> 29296 bytes .../case-11/surface_source_true.h5 | Bin 5160 -> 6532 bytes .../case-12/surface_source_true.h5 | Bin 33344 -> 29296 bytes .../case-13/surface_source_true.h5 | Bin 33344 -> 29296 bytes .../case-14/surface_source_true.h5 | Bin 33344 -> 29296 bytes .../case-15/surface_source_true.h5 | Bin 2144 -> 2144 bytes .../case-16/surface_source_true.h5 | Bin 33344 -> 29296 bytes .../case-17/surface_source_true.h5 | Bin 2144 -> 2144 bytes .../case-18/surface_source_true.h5 | Bin 2144 -> 2144 bytes .../case-19/surface_source_true.h5 | Bin 2144 -> 2144 bytes .../case-20/surface_source_true.h5 | Bin 33344 -> 29296 bytes .../case-21/surface_source_true.h5 | Bin 2144 -> 2144 bytes .../case-a01/surface_source_true.h5 | Bin 6720 -> 7792 bytes 26 files changed, 25 insertions(+), 18 deletions(-) diff --git a/include/openmc/bank_io.h b/include/openmc/bank_io.h index 90ffd820f..418ec111f 100644 --- a/include/openmc/bank_io.h +++ b/include/openmc/bank_io.h @@ -16,8 +16,9 @@ namespace openmc { template -void write_bank_dataset(const char* dataset_name, hid_t group_id, - span bank, const vector& bank_index, hid_t banktype +void write_bank_dataset( + const char* dataset_name, hid_t group_id, span bank, + const vector& bank_index, hid_t membanktype, hid_t filebanktype #ifdef OPENMC_MPI , MPI_Datatype mpi_dtype @@ -30,8 +31,8 @@ void write_bank_dataset(const char* dataset_name, hid_t group_id, #ifdef PHDF5 hsize_t dims[] {static_cast(dims_size)}; hid_t dspace = H5Screate_simple(1, dims, nullptr); - hid_t dset = H5Dcreate(group_id, dataset_name, banktype, dspace, H5P_DEFAULT, - H5P_DEFAULT, H5P_DEFAULT); + hid_t dset = H5Dcreate(group_id, dataset_name, filebanktype, dspace, + H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); hsize_t count[] {static_cast(count_size)}; hid_t memspace = H5Screate_simple(1, count, nullptr); @@ -42,7 +43,7 @@ void write_bank_dataset(const char* dataset_name, hid_t group_id, hid_t plist = H5Pcreate(H5P_DATASET_XFER); H5Pset_dxpl_mpio(plist, H5FD_MPIO_COLLECTIVE); - H5Dwrite(dset, banktype, memspace, dspace, plist, bank.data()); + H5Dwrite(dset, membanktype, memspace, dspace, plist, bank.data()); H5Sclose(dspace); H5Sclose(memspace); @@ -52,7 +53,7 @@ void write_bank_dataset(const char* dataset_name, hid_t group_id, if (mpi::master) { hsize_t dims[] {static_cast(dims_size)}; hid_t dspace = H5Screate_simple(1, dims, nullptr); - hid_t dset = H5Dcreate(group_id, dataset_name, banktype, dspace, + hid_t dset = H5Dcreate(group_id, dataset_name, filebanktype, dspace, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT); #ifdef OPENMC_MPI @@ -75,7 +76,8 @@ void write_bank_dataset(const char* dataset_name, hid_t group_id, H5Sselect_hyperslab( dspace_rank, H5S_SELECT_SET, start, nullptr, count, nullptr); - H5Dwrite(dset, banktype, memspace, dspace_rank, H5P_DEFAULT, bank.data()); + H5Dwrite( + dset, membanktype, memspace, dspace_rank, H5P_DEFAULT, bank.data()); H5Sclose(memspace); H5Sclose(dspace_rank); diff --git a/src/collision_track.cpp b/src/collision_track.cpp index 75e56574a..03cbc32b7 100644 --- a/src/collision_track.cpp +++ b/src/collision_track.cpp @@ -78,10 +78,10 @@ void write_collision_track_bank(hid_t group_id, hid_t banktype = h5_collision_track_banktype(); #ifdef OPENMC_MPI write_bank_dataset("collision_track_bank", group_id, collision_track_bank, - bank_index, banktype, mpi::collision_track_site); + bank_index, banktype, banktype, mpi::collision_track_site); #else write_bank_dataset("collision_track_bank", group_id, collision_track_bank, - bank_index, banktype); + bank_index, banktype, banktype); #endif H5Tclose(banktype); diff --git a/src/state_point.cpp b/src/state_point.cpp index 47296da3a..8ccebeb05 100644 --- a/src/state_point.cpp +++ b/src/state_point.cpp @@ -554,7 +554,7 @@ extern "C" int openmc_statepoint_load(const char* filename) return 0; } -hid_t h5banktype() +hid_t h5banktype(bool memory) { // Create compound type for position hid_t postype = H5Tcreate(H5T_COMPOUND, sizeof(struct Position)); @@ -569,7 +569,10 @@ hid_t h5banktype() // - openmc/statepoint.py // - docs/source/io_formats/statepoint.rst // - docs/source/io_formats/source.rst - hid_t banktype = H5Tcreate(H5T_COMPOUND, sizeof(struct SourceSite)); + auto n = sizeof(SourceSite); + if (!memory) + n = 2 * sizeof(struct Position) + 3 * sizeof(double) + 3 * sizeof(int); + hid_t banktype = H5Tcreate(H5T_COMPOUND, n); H5Tinsert(banktype, "r", HOFFSET(SourceSite, r), postype); H5Tinsert(banktype, "u", HOFFSET(SourceSite, u), postype); H5Tinsert(banktype, "E", HOFFSET(SourceSite, E), H5T_NATIVE_DOUBLE); @@ -641,17 +644,19 @@ void write_h5_source_point(const char* filename, span source_bank, void write_source_bank(hid_t group_id, span source_bank, const vector& bank_index) { - hid_t banktype = h5banktype(); + hid_t membanktype = h5banktype(true); + hid_t filebanktype = h5banktype(false); #ifdef OPENMC_MPI - write_bank_dataset("source_bank", group_id, source_bank, bank_index, banktype, - mpi::source_site); + write_bank_dataset("source_bank", group_id, source_bank, bank_index, + membanktype, filebanktype, mpi::source_site); #else - write_bank_dataset( - "source_bank", group_id, source_bank, bank_index, banktype); + write_bank_dataset("source_bank", group_id, source_bank, bank_index, + membanktype, filebanktype); #endif - H5Tclose(banktype); + H5Tclose(membanktype); + H5Tclose(filebanktype); } // Determine member names of a compound HDF5 datatype @@ -672,7 +677,7 @@ std::string dtype_member_names(hid_t dtype_id) void read_source_bank( hid_t group_id, vector& sites, bool distribute) { - hid_t banktype = h5banktype(); + hid_t banktype = h5banktype(true); // Open the dataset hid_t dset = H5Dopen(group_id, "source_bank", H5P_DEFAULT); diff --git a/tests/regression_tests/surface_source/surface_source_true.h5 b/tests/regression_tests/surface_source/surface_source_true.h5 index 2c2a19038fc1b6d9c7785e8a7f591e9e5c7fd959..d343d12aed654f2da53310c5943c20d8bd85afb9 100644 GIT binary patch delta 4436 zcmY+Gd;C;$8pr!pGTo=5hHlfmjxIX%RR>*;QYIBsx{NMNQ6^ER`PT1D zg&mh=7TIZY8EnjA?O-uj{1(fwtifU|%PhIPAOEe_>+|k;o;mY5&-ZzbGr#PSR@wY9 zUZi>0IW%}niw1T0V2e>Lx;Hd$XvTkmW}#*hv}kA+l)PE9u%(Z-3bkx|r?K>K=vb@a zP_g=d(r!TMXls>jqjU_FQMRqpfp$taA+x>G!466%kkwJ?JW2+YwmT_ZK&j43*HE#G z(vjnot|Rw&rAsK&Ri(ROiHs1ICnz09=7~z@P@=oi6_oFxbf~A&Rb=;4I@}vu;>$^WhcnFgu5bdA-6&9BFrzv9K{yni?U1by^JRknK7QorT8N2 za(q!}8NSF~j_(S5k#hyU$h#6>REp!fQl(d6i;SzVMe)_xqTCv6*U~Do)?u08K}Yu0 zJm@I&EIG3OLXJ`;9(+`MjxjyYEs*;HxA-fqqu7hIj6 zz!y3H#20xV;)_Zj;rlOqQS4)UQMQWjC-jQUPwDkDd{N+A^6&7k0XS7j9i((@fEy|8$ zjHfVKWR7CAqZuu-PGw~%c^WH21%s81VZ6vWoe?8%EWW5T4&U)AJpo%}oPjNho7ke< znb=NbOURnUrY2*H?6a^%;j@*lA!iCtmxV2I&%qXDrmFNbIz`5GI-P;-4CW+9GS6i% zrxWA|@&tZpELmbEn$xb%2W@K(*&DSx1WL?kGgOXc$dQf2- zYrX+bv~^DETP9$bJmp$MHqZAMr)r5qweU34EVa=_0ns_!G7${uH(- z_cXT8&?>V2to-;^c@PBVHhB;P!5ctAP!U)ukQaougQTD;ux|tfL3jr+mp5>aobpqW zaXMX7WMFDYfn{G`-f zUhV0_e10PT3wac(RJHLS*HH0eQnKR`@&4EGhm>C?@XJy!P}jMKm7kTL1!Fe|$Ztb< zCH2Z9%vrLm)J^%hNJ%~PD3g%ijX)aIq*Qo}^Ghi%B};x!oK@6ok7FUFko>w7r4%_r z-;$Xc*nL6dD(dbNY(Po{DW&9x$dh{JN$$3XtR|(pl*&cC8>qHCxpo?$W(vQQ&go(C*L*)K~eNy*O+qF#MgHLfKI z3}(0EcM_}P@t~8=V?_kV;u-*fORtU(hImtDNp97sHb1# zro%`A2~d+OtEAd^iP1`_FdR7hKsZW0{xbLf^-?mwNL}a3G;+0EH3CHT3r12;yuy5? z?veSP)ZJ64=U=7O^<;G^m84XCjeez+86{U8;JF%2Is7`~m2!!SE(O?4Yle6W6>9?( z=M5TdOH=LmCROdd$4WPic1zEnDeoSM=_=w_e>pjdf6JLs?mN!SpNR=%{SRA| z{2p6W_yOCa^oE=t>FOtJk^3{YKdZewEsYJ*vcPB#vVwRE&;)d@J&!<3vfwdfl@6>^ zf>4kJwj!t!gpMT$cVce3qDJ|lZeK}M| z_PUrR`Z4(+9~es*-DR9nl30I^7~|>hM3NoA3768)K<>7TULxFRIc*H$9xJE~#{Wuc zL$Hoh8_H%^QX9r%S5X_z`((9BkKoO*h8vA!Cu_OcDcoZeds&D5XhxP`ey1_9^{m}s zU>mR+!@M?PHI~jdVKq*r`6DDV9-AcY6R_EgyNT5n?9asNI_xK6wH2Ff*iYi=;g68& zWNfw*pN$QFfE1_Tvx6te!sjLiF%_Sk9DN!-yEyf9d^}D)1CQODdL|xeVjGV=#OL9$ zmv(32(MY=rj|}ZzK&!Ly*{9MDHv7?q*c`y(R)%&PU*)fk@a=r%4lciw%kNU@ySeNh F^$W+aIS~K= delta 18335 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 t=VY&IRpL>aBOZm<#3OXmtQo0{@wrp delta 4990 zcmZXXOH7 zsql6)&+B=D*g$;3=?e5>l3nhSM-i8-<)T;6B!)yH6G~)4iA*RlWD*li;&vYTGilCu z$*Le*`Is@$B!*4mN&);#HU)_mx|(G{9u}fU*z`y;J?Lba&^a)nb6`T}kYo~*O=4*Y zCStN!g8s=y?LV5<%`&0>OsGE->Yri~Q%!%`5fe&eLWxW$G1VmQ6NyYHkqIR-p~QVA zalc4pLWxW$kqIU4H;HK?kqIR-p+qK>m}U~wP2x~B?vcq&LG}dcuW?=Lg6s-%qSkfY z5afv9icd04PI^0fWU9CBrFOh^b@4&tuzuIFmfTskTWgps9YIefKMGRW7KoZZR<2_&>uP*3#SxfF*;m)<)x*#`e$sG~yi0#$|xmio@ zJmJn$?#URgL>HgOaKfx5cfN4v+r4!W?0}oK{T(<*ChLMwheFYz(C(lM+8t}jU1YoU zZ8F*E#8XnF@=kSONM2s+I-3@MNEkfs!qiNtOR?QWKbXb#jJoLUMg!K8yF|E4Y_~3G zhpgQgz%-@C-O!8ulq&a!y|_PJOb(+rYspv6bs(b5MuYsp=L;S8G88T>MaSBNee&)`6sV2C-9d|X0g4`3N@T}`v5ae5*w3zOZ3C!N2W`B1Am5n>~B^^qjlAHV^kP<$gMcJ?P@( z8_*szQ8!-0d>uXsdrt)EdEM1q_sMg{wIfL9JY1}yxjOB+bU|}po z#hN8S=st0wev)QVqIvRYq3DP{!CDZ#MjOE*dDtoP=%BPj9?mjP z50zI$>u}Se_voch^vpfhhAZl=idN7^xJDj+hI~e%b@FJTm?aNugFM;@Hp#>G$fJYO z7I`>1evdB7+vMTykVg-vyP^$vd*snaxKAFwPu`d4fIM0#9+HQ3L>_GfdGfFehBFwtnsJ?~t@>(b`SIrlZWfB(LD`svxL zCU@@QQsSlQ8eLvA(`Lx+b{WN2g%K>h8K$dHvH!vNmb8AC2g|nOpGTL2hjOmhzCQ2Y z;%h%%idenA`~g{jxMf5!V~a`Y@k^_WG7s@k>{hu$@a6uSeAtmW;*g^bsif?kkniKA zWUCL#oI`dY4Qyos(rTp4LK2i6NQ%;NQd-v_Rn`oo8{)B2HIli@mM!-v)5r`^>`dMv zgARGNMi0UV zh+Z3iAiCr`jnb;&oc9o2=`To_nckSRbjb^l7&AX1_L2>&=d83^n3;#PQhq?%C^ybY zOMkfUAnnMQ)^|t;TU|}k(!=r=a)p^a$W=-y%7FTJ{k$ty#!z zW_BR`l#cV#(gQW)kgpC&HA_pkJPpx{{|iL7oJ>l~$EUx5R8zJgK}x(uS|Q4FhipOg zY{Xinr7{Jn=bTMQloDx^mR{JCkY;8!AbK{!DQW51n1JZs*CAQXscn~*Zh0Ki#mpMy kCM9q|S~r-d5Pw=)ni+xUWwVmbSC{_Y`QygCfBZk0r~m)} diff --git a/tests/regression_tests/surface_source_write/case-03/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-03/surface_source_true.h5 index 228f5a7d0a462f38a1cb0b4274dd40d4307b5a12..d14771f5a00887c16ca1399a629d0e2448430bda 100644 GIT binary patch delta 1272 zcmZ|LIc(EV9DwnpL14roi=!CI5hM;%E+dMt0HJW3a=G-Cn%k5DDQ8-)l%u4GA!FsY zFrrRbkf^$J%$N<29z8I4^yq~HK@<=f6jCYhg=G~U10@I^#jOmen zUp~(Cuud>Nw3Fl!!9K+qP^3ZOPM+EyedE{O4h~eBJ4}FL{61aEC!x$z%EWCT<;lsR79?Apq2v8`J zhdM$YA*_ewp*b`snHOE({{OYsIdcYn z=I_6Z-@bKRO>>k?F1RM0#?b9s&OB#~!(jyYUmF7=??9hG z(#+Y&TTOyUpskIQncLjp@?&$#PvxOwA^8nVp%Kn$0#16H~Lr zreR`gw%RmIvLbO6A*7kGk!2f6k6P~+$`*Sk3Q-!%| zigkxnYq8H-r0GKaJV>YQ=2NkYOCJB5id|fET)q#>XA+syB#X#fO|pr2(%{;!i6Le+ z$sx^Z8V0-N;F@#{&ZXm-bR6f(;}81y038>)Fd>h~v))&z-^{dg=qexkMrqv?M?*X-TOm$;rVYWpa_p99)Dd zR&sE`Wn_+Zqkp++{>BY+h4l_p%HnY@I#pSVYHLwrEo!Yroh%+5z%8tovwZR4;`dz4 zYOqQhW$`c%vzn|Y&DP?$wP=yWt^#;kttV}=xL<(FZl}w>eh6>I2_nY9^~BV4*fdPq zw5yXeH*I9iMw$yTtBYJWY{Z=IC5^cu6H{~2reRWj7_+)bGp|Vx5wlM(5pxz3Q{%H~ zn0SgXt50i!#jGZ$$hB%CMa6LSlg2+weUb*`oA;p@Z{7eM7aoOy$+RXMkzQ)zAXk0~ zTuvfWHu78(PCRZ8Rtb`;T=U=wQ^ji!_Tt2nbqo`jOxVb>CT6N}iu6*rIKzyWV&*0} z^UqSu+$@XSGR))*a;Xe%P8>^RaBr1{>~a`5eq1cav5E~V`gj}Nlh_IzZq)03 zF?t(^xMd?oJ!YzCajG6ORdKf-Gh^vm*0ffIjh+S^$J6nR2DDO%Be)T*RO0Argf@Y+ z*Bh}claGyXtHjaX1cS^M@@0tmCK#B!w~_K@Xp&*_zw>j=XsQCpr)D%wp<~Z+7*dJM zYO;^WswQcggo>gTxR}gn!kOh&3kIvqQqhXR8Ps&T6~`I!oqpGfXH^Cmk<*4}m9xuK z8^)QrBv3rp#I5@U3U@m+98SK|#LOds;#oUp=E<3dPGF^cT4~}0S`-jj)`SyEdIwsl zJaVN2EjW)n=|Br6DV;E=gmSraydMg^EjT^yFnu5{rBsqk{XD_mrH5i5#l z*D)^!6-)h7F9w<6#VMjrVzUx$aEcKdS=7WsO(VMDVsg40V~)u&kGe6Ylnh}#I4+gP z7kY49MlLo&YVNm&Hv=@d-ed-iN;+|;CzG;5kP5ozv6pJq)Aaibr5sW22gfK^(^ zP(J`eE0OD(v=RAeBh{b7#c63?6EiIt;=LxEgvtlu;v_UTh~>KETlZ-Y%W-(|d;tTK zRZTd&n1sVi(GZ&U$fiFGp(&@7S3_v(qw6a?4bn$sS`*GBFKr}$7_NSDO$}pkza0E# zxN)ohZ#51e?lU-5iRIcEtYwh-*%`EBl67`Hnb3q2%ko(?b;^+iBWTLeWPAkCU4GFb zR?E??xOV;T#Bm6LRq!dTfzfteis&jvKM( sVc>)@rU{3N2R0HlhNdcod^Luq{N=^s7@9JPxZn+!e;(7<8T#J-55UccTL1t6 diff --git a/tests/regression_tests/surface_source_write/case-04/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-04/surface_source_true.h5 index c276af40f4302f4e0feff883bc3c472bc4df7ca1..d2acc69910740026a283af0568d90370f9ff7065 100644 GIT binary patch delta 1271 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 JQSyjJ#UJ$40M-Bi delta 4948 zcmZXYTWC~Q6ozN!WTH_M6HUy`M8{kw8Z|Mu$#r6GlSwir=AM|CYc;mfD7D~&f}s`! zmBvjTQc&=r4+=`7C@5I;L8yX)Pz4KyT57?9q6JGWP21V~UuW%uGcUTnz5ajgv(F^I z+qb{k?|kX_I@{r$p3j*!?0c_WHVTYnhr@0QewNx2B3h#Ror=uN$B8S^fsdA}SNAL8fL~Bx5`)RP81#fOc}K*_ z#8ibdnOg>5}DO`i&i;~B; zkci`3$zz_cfoU|8y!emFi4gKE9;VUKv@{{0UXUuzBw#`e`#nj3-zp-8c$^6JYTA-g z$6AXxS>z|dca3!=UKT$kVMsg=aV5i2EeD@cStiQfr^zUitS-scVy!G5rJyZE7MWC} zT36Oti}lvRC5taoVX#3K4^pwgG}>Ueh*^gXrdx#>)*{ndWLb-BD%AJf&CeX+!olV+ z86dKW$vlx#Tm|Ssf&5NYGQ+Y^N5Rn9W8@joI>7HpbSi$-hVw-!y-qFEMaH{rlqs;4@4lT-+uy!aXqs=ow{VI*&2A zNj*B`A`79?DK<<*r`UBOI>jQ2@N?yqhrX`}hpq!`z6hpffEmJB459<9w-^=?(&APz z-smz}M3ul#$5v+vdRNHh8?>wwYq138QT&{39tc)S!Br2;RZN7J;;SiRLikFD)Q^~% z5H@k46jrO*>U}A!RAzLQ!AfPur84X@j(gvgp;raR&T=g4te7jua)R8|-{r7USrMo} zZxUP0R$wE^L>%H(1=K1v{FM--uwbSV%PKnlpyhSE?5V=C%8zfVFk9uvn<`kjxVO-Y z*_xd6Laj39cP|8H#@Iz(H3aEwae_z&lV?OSndEMVDvQZ+BH2Wu#gh=?u7OI&(a{=g zE=L~gLJb~mqb%}k@v>zsiO*~CvUMUotc8`CHete52a-n{2or~iuKTe~F9{40(F9V>76VPN*e)%; zZ^HZg_t4p-36Y1SJX6rjr-6tUC z9myuqzV_$iOBF$ACdPdR64y*5YhLnXGF{hvx(f#pz@N9 zO&lj;MwlU6RJ!NrB#uwT+`=S|Pu|1SM`Y}Wz|1`9N%)r3cF;?PKf=D0z^RE2zf3NmsptX{&7z4$b#RH!+ zX<7P_8OaQ%2hu4%T1roaUoz=bHXNr?#c}m}t-j{hcWY`%UuUNmYt01ZX`+6@92E6+ zQ2h0x3J9mI4apj8jKEee{5_KRY4oxR^nj}5kDmUHsVo-rAI%jZ5LHU z@V8cXh{_I&Dxm>;XSA;G5;cb=oZX_9(R(gFAJ_Jt?z*{`c=YbK_9e&Rx%_@nRfM%< zB**c5T$c}u${i9_M!4TS{O|qoxwu+KM16yHl>dPM;kY_N|HsHj1NL!I3(!x{KbmkX z`bX|0`DnpCC8`GFG}n(dyfa)s%u)Iu75^;Pk2R>}mF{}wI*r4-LF4GH z1((iY-K2A9x037e{?cuFhka-Dx^tK25&mx3i>iCCyRR}%KD_(n!g9uZc$k_8T4^dWm>6;k+dtxp%~)1@}GiFg_5EHoT9-!<;34 TR{T%IqYi71cxdy)Bbb*zT`c@f delta 4999 zcmZXYO-xi*6vyX-Z$uenR6a%+D&iPv+bSo!RL7{r=~i``$bf z4!{56{q{58mwSB2ZY}P;<@b&~dg!nA7x;W$PvVp2*&+V%_`UHo`5({EGtS>BJCc5i zd_VSRM{28gKD#&T-^548Ma1dIj=9JJBkve#3&fITpSi`L^B~ZP3uj}?sBGrL^hEnN zISBg5`I+;ROCon?E9#QSC6lYqg-a%vLQc<1A(u*SHi)`Za_T8r=S(A)PIbCxI=Kwy zGRS2zmq~6DbDPLzk-M3Puf&jbMoRPDnHff&F%m9tSKVObJ4Ql;{ z)BQ|_P;bae2LR9#ZC%WC*w$gDON5@^hohB{ez6bF@jXRSobqxv zN!*=$OLkG!bCoefKY>#F6s1(4^=@15X1dJQWlZm}^&X}pwvO0Z9)B;9wF=yjA$gS` z`zSNX$ciH6lnGX$$`IY>eabAWj3I$){AhlV$T;z8`~->%@$ouOs_`!E7k86igD-49 zY@%Se1~V&&JZ2>QfIBn9$l?KvsT5;!YB8pgnhw-LR|)-8=_=CRgP2fFq>qvNiquf% zA4a;P7*i|8EJbmOS}OQ6iZKU?bkw=YLLFK}MGM~{w1`qcLp^kz&<~WZBfW75{SGOT z>@**Cle>x>rpy~g^q6{kOg)X!`iQNMke)k&nMW1b?7U_qdK8&sDwFKYGV)T92FlbN zLzN-=D2?_}8dZVEsc1mKacXg!5#6Fmw5V&uFHrL03;W_#Bfg9#Y92X`=FQanCL_9e zi)dcigcdE-LhBRu@lTKrH{t;^sRP zNq3eR(Z}z!TXa$jt-Ea9MS8d$H-B2_)pp$cY0|p7+pg|r`i!m5Fx_M89;SPR9_hgB zUZ(r(>ORulojCbfp_e*QeU|h>7arX?A{~tAOh3yQqB8@Oxpx|!4cSoSJY|}@kul^h zBX1bdvtraopV<(d8DtqlbY_S$uX}JFL!!MP7br8!i0*TdGWtA*=uDht45>JaS;H!m z;@oCL_Zgu+<>ydkh|Y{s=DErkqBEB$qt9%}dOtdQDN{P&CNqloDD#Yw@Of1EDRYC7 z=@>FelnF7SGiK#mQyD|@2JyP5h}Zp-LA>r}4_q07mCw1&$O}cxl2tx|DnmYGe*KwXdxG{TT}RLNM8Ing??eG4t<1vhD<9`LYXy2@}^N`GLwv~ QFcSPYmMi`QKvinOe;dR{8vp~{on{b6IB#44C@-!(U zDjFoBG%2@C>4L(g3ksJmT~P4M?rMct>C$Y6~~NHb#Su%Q@JRnzLz`7 zZN*Ztz)vY?h5VshEU5XuP^k9G-?Oz@KfhZ2Q`#J-I9W^nA$>*28JKg09B-)16EZ}; zpP1h|Z@$l0b%BsWh~X?0@&`0kNMosx39GwE$Z6<{h5UjP-je$J{7x=wtTW5#FaLWM ztY}@aH1f#TCVQoj-=Ma&&YJJ?bz~KJxU0!SUqc=#JYC2djI~0JA%nk;@ln~pnCnHr zl0W9GjpU(hs_!FKdoy{c2Gb)(ephS%-}IK&S-Q3LoZlVWR^R*E$v2FfBG|#_&^(&h zNq$#rZ+DP~y1RZBvpReD^P%l!dL(cyriZ?d>5;3P>(V_VmQahLpx3$3EUIpp?8r-3hyL&7!G-4@K2G4*-gG%1gFU( zgms2Il(Xa!!9GVG>Ur{r;ke|XT_BGH?nUy@FOf$IPmqUknLIN1SIEQcA>Shck32$H zSII-UMjjFD>*S%{Am8BJBoFNtc_eUelZWn;M+)x_c^JLqk-@)99%di;J`vm_j}X>< z@=zX-M+Cb<9%?^%#Bd&xhxUj(61b1aLw`aZDZHoTVLT&`41Se7%mMNPA{Zo(5Y}_@ zP+pKn1p6g1qX63g delta 4948 zcmZXYTWC~Q6ozN!WTH_M6HUy`M8{kw8Z|Mu$#r6GlSwir=AM|CYc;mfD7D~&f}s`! zmBvjTQc&=r4+=`7C@5I;L8yX)Pz4KyT57?9q6JGWP21V~UuW%uGcUTnz5ajgv(F^I z+qb{k?|kX_I@{r$p3j*!?0c_WHVTYnhr@0QewNx2B3h#Ror=uN$B8S^fsdA}SNAL8fL~Bx5`)RP81#fOc}K*_ z#8ibdnOg>5}DO`i&i;~B; zkci`3$zz_cfoU|8y!emFi4gKE9;VUKv@{{0UXUuzBw#`e`#nj3-zp-8c$^6JYTA-g z$6AXxS>z|dca3!=UKT$kVMsg=aV5i2EeD@cStiQfr^zUitS-scVy!G5rJyZE7MWC} zT36Oti}lvRC5taoVX#3K4^pwgG}>Ueh*^gXrdx#>)*{ndWLb-BD%AJf&CeX+!olV+ z86dKW$vlx#Tm|Ssf&5NYGQ+Y^N5Rn9W8@joI>7HpbSi$-hVw-!y-qFEMaH{rlqs;4@4lT-+uy!aXqs=ow{VI*&2A zNj*B`A`79?DK<<*r`UBOI>jQ2@N?yqhrX`}hpq!`z6hpffEmJB459<9w-^=?(&APz z-smz}M3ul#$5v+vdRNHh8?>wwYq138QT&{39tc)S!Br2;RZN7J;;SiRLikFD)Q^~% z5H@k46jrO*>U}A!RAzLQ!AfPur84X@j(gvgp;raR&T=g4te7jua)R8|-{r7USrMo} zZxUP0R$wE^L>%H(1=K1v{FM--uwbSV%PKnlpyhSE?5V=C%8zfVFk9uvn<`kjxVO-Y z*_xd6Laj39cP|8H#@Iz(H3aEwae_z&lV?OSndEMVDvQZ+BH2Wu#gh=?u7OI&(a{=g zE=L~gLJb~mqb%}k@v>zsiO*~CvUMUotc8`CHete52a-n{2or~iuKTe~F9{40(F9V>76VPN*e)%; zZ^HZg_t4p-36Y1SJX6rjr-6tUC z9myuqzV_$iOBF$ACdPdR64y*5YhLnXGF{hvx(f#pz@N9 zO&lj;MwlU6RJ!NrB#uwT+`=S|Pu|1SM`Y}Wz|1?37F6fJc#Kg1h`Nyp&czGh#_O-w`4?2 zEJ#FIGTwNjC*<)429F*+Fz{LCbRbUp=@nb|>F@fdQu4DnW|XRf6PrGki(>Lyxud+S zSSl9yDFv;NZOX-hnw<-UYOnk?U7Pmfi`74=&2Wkn_4pssSA=Z9oGIk^AC*}`hRDvJ zXSeTL&$CsXBV-LtICF*k4owwu7%|*=LQX-SFXSi^cnk7p+4D@gFkc&s@^yT1`?<12 zNJ|qkTU#Rb_EI6gqILK8sx7CF814%C&{xt&0#6sR4r7&&V@TnzW`0!GGUpl*uw>he zwT|(jtk2I8tG$6dRDJ|YkOF?l5Lo{)#}lsr=SRq`+g$Pb8MkUTm^_pb I@`y&nZ^?BCC;$Ke delta 4980 zcmZXYTTE0}6o%)*peQKn3@{)lcZP}zGIGBu*MS-C$X!5C&|<~JCO+5%Q=8DlMBMR# zrZ(}x2Td@d(ZnVuzR<)bnA+5&K4>HtAb~eGdE{ z-~DEN@}=!cmaX#QLiRQ;Svso>{YojGj{$a6Ohc>FeSl*l$|Axnf9j*9f z{C?bqdh6Bbj;Q~FmnxSf!XMq|P-J$&CGJEA)}^a<^eJLPzo5V%b|%-ip(c#U8W9H* zLlw?sZaY*Pm>4y*yJ=QLNX@1YvN?oAGMRN^Ruq$!5K`n?UvoKxEQb(p%zD*K3=VXQ zJkZx}9OzaaoEr;4G?PoQ5JXGC@38^(I8;y^2lY0tosR31bXggF6%y%fOZaVF?Kxq9FtCT4|7oZ_|g=+f+tE zb$;bMq$U$vRS?-C=7>}hafr8>=vNgavY%>kh|(5uEekCUvtTs~H&!iY`aIaLngz2S z2x?fc;(?&ne0Oy+_%j>#U2hH=%z@7wG@64Z8IOPgg- zoP${{=1i+OXfp@xGWdEgG#%zlrwsD;;l8{0zNd0=-$z17H1VP?(+8Ni^Dt|WiJ>|gLWY>Q4nQ?bBwS1o8R448M2;~rI*&3L%SRvWXpe|! z%Zl=%MqAb}5p7uyym)sU^3Y=r;?T8Q4ID%Z!>#OMfrxf1M*&(y$QH*7@I*Js;QIpf z(;j6lL~W!@j}#)U?det_#%M0v(z`faRDrZ|ly54aPUiG-1;%OOtprKonwb#t zsuE3=8I@O|sbNM|aj6O|(zwM-BI!(W4xuK4$uyBnCNGF&G4ULR${^9=LI`+`~u(UPtF5M~i*EFeLYzmyI!P}!P?r9B{FzhQ#B-Vl$_7x_k zh~)8#%6PT=ZET7WHxqS3Lv=nM@GT*7mp_EtIt-HK0Wi3&t9d zR%Z7@171aCcJV$0C35y@AAWw6&)xOm1j@KeL?gPC%i5ETsFgk^UU_c<9GSX?$vAWB?-#-?NC;07lmG$eRJQQ|1@m zjFYM39sSJ^)U)6Qkp?F3iTIc_w4kYS$J;Gv+QjL#7BrO>sV-5|3KC#4N2Hm_TOut? zs@m|1wg!o~(uU_D19@4+Y8$ptChBX)7VXFe1+(p#*}-ImNGFq`4%BE*y-dXLRI6Af zqCM5y36=KLnNF4*uUl18) z;^{_>_RS03XnIUGecg?hX;cPzJ(yr+_4yvCwOcOrKy8bhRZmjA4Sd#Op+~C9VJ@q_sg_p1=hC4Y*9i z_y)8H?@81cCTbBgL<|$Ph*v}m6Sa%nQ`qZ(9DMZ@25bL(c?wOnW#)VeqAhcpNFhJR z7eus6dM2UL8GJ8;hOX7NVr%cB8^pDBKb3@w7Z@oqCX{{5|I`QRpJ?_ xG?^k&NhKDsNW^emyKqlIrOD(J4)l;b&}UOPP~&scB3x(tobvxxq|utM{{gElbWQ*O diff --git a/tests/regression_tests/surface_source_write/case-09/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-09/surface_source_true.h5 index ebce87b36aff8050b2b6b9df2bf4292296045da5..d62363f97fd4cdbacfd65b2dd99605721d60f03f 100644 GIT binary patch delta 1273 zcmb8sH*C~U9DwmF2+RTogk*sL(@hbWZc4y(6Pkesm~KLV3-uD((ZU5W+<5X^GDexQ zAQ5G`F=NIXJ>J0J(W3_jKFdU=WFgW||K}oq_g*Hyl;ZEjrctU6jBos0E{ch-<(=iF z#Zs}rpHk2Y>7raLsOfc~Q0_I;>I<`V%=8vz>*?pyb87Lw_bpAxnb=EazCZnLeHq_EI$O)>uMo0< z7@jWVB#f0pHj%(z#r{!Q!*5?L0!F$_S!;zHfwC^U4u8$D^+Kvf{+_deBlwHEDZkg) zoUea>FWEwRYi`&S!M6M|eKfK?zYcfg>-1S|7pD(rHz&U*Q)fnSEq0H_-b~E&_OW{y z``JAb_y_nV%r17{C4z(O3T;@2$V0J(tRjScSjaJ`N64cC=P3VtXva8oL~xIjhkk-Q zVt6OX!*IwWfq#lT%x?1CA~;PRZCGc>Lpe(xA?$PHp`Is?4jh*}v6RCh+gkhuKTNR|NOTqYdi;c_+9{M}-h~d2_ f590%QB=A3yhdD@oPz0aIqYbN09?B4TghS#d{mBeO delta 5311 zcmZYDKTMQ&9LMqJ{voI+ww{8DZvHGh$#4{N?Y1u!oXlsOdT9Z zioY;0IM9TFqzuG3IGEJIfx)CEb)bn!Ny^|rQwEZf)U@|J-_P&+A^dJ=pT7Kko;&V< zx9Q)1hJN`W{NuK8{loE!hmp{|XJe7Qk@ewl$jd%*y+Z$AUL@3>8(esQ^qPNj3Zpmb ze!KUqaLLKglY*xF|FVzZxPQcC3R)M*a6xM z<>}Q@y;`PM%l)h1mE_Ay&%UNWBeW6#2s0)P=e2A?Z9~4{J!pN zaMQI5|2U8$N7gyA-jNL|-5caBDt2U}BcC}^;z+`gQkBk)@?4uBImSNwW<6u2%#kgQ zY;|OtBjt`%sEjA%^=wyZgH)>6GdombrSj45%!+9$l}~$@{_$9;+_Xw1v}v(4Lw4(# zQOF*ZkC448jhp2@)hZ*9eJb;i{VJI?UyWBYg$usu3&gvN}=Ts^yWp!S~o@sRCf+H6lX>z1l zrEI4>*Cmz2E|JTQTydmDr3b64Die;_JGbf?E7u%pQz_alujjgoUEOe`T_wIpRvnNW zV^=rzjFnrC+*Y}>SMI}@zJP@FOn$YTiKuiyqK?E=@@iz2qtaF*zfqQC#r#|&zflnR zti<-oAWsKlkfn|+Qwi;t)pC_)2#Y%kVKF~KSj@(x+=qP}f!G>G%{;`fVel}S1G1tF zL)gVRN74sn#Zo?nu#~e9ma^fHtQf~3i2RKy`-;pAgt<)B%8I!hgxFk$%?Ai`S$kMk z%;f-txtxYDm&rO=mB_o3eLekk^0$_R|NHE(I{AIZc2?KRfbD$j$a_bsQu2M|_tku# zlJ6s?buuM4Wm+qbh%l{v5T^AVglR25Dl01m=7A$`AtF4w%l})r~S{xaJFxsJvtZcMB)9lD-M!rM+a+`lL@*QfU?U}~2atlU#1j1;~FOrDK zoRbwZJ`7>T=OE1Z)AO=o#;q{pvp8cj?wN*0Sux{75N3P^!i=Xb$jW9sWCk7i0Abi` zFUpExAAm6I(-4L|*(585y&q!39x_u9hP}F3Rt)=N2*ds!!i-m4k`*&^E+<)YnZU|fc8e&@>HbvKD#g=zL*z#8pwmjY@E4I87!j`{;u;r_+%Ze?( z1FJd0jnj8z#Tq|_ zu*S0x*0|vdS=sh_W(dO4&Nz~~E48WcYxC1xsSQK?;jY}2QhRT)3_uv`X^2g;XOf+= zVw(FQJM@j1g6veO{!&&p-=294vHAAQdx*_<#8iDHE1Pf6Jc2OalMu$b@@rW!)_oAB z`5lBcF7J{R}Qx_e|njS+U_g5H@@QVjJ$6;_qZd>4wzn j`}`VWs~#~$-Lj%|L0I)y5LP{Yzjd|$zrOs2FMRwDQPTh6 diff --git a/tests/regression_tests/surface_source_write/case-10/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-10/surface_source_true.h5 index fded9d987e392214edeb3fccd50c3ecc5f8a7728..acaf39cb485cef41a634a2e980fe160b4bcc1316 100644 GIT binary patch delta 1273 zcmZ9{H*C~U7{Kwn90CIZOt%z@sj@;d5WtjxDFG9jfd`mwLVyeP5?W{>K@1rqzlAYk z%7R3cCF6}5Jz7j27(9CPz`)GON$aoA|8^p3TBDiy`}w^D1V zx!6=J@TU~CLjF-I7S#M)C{#M-ubJA6A78ERac!1U9IGb(kiH`1B)-g^63h{@jQnWL z{c~M^o|>ccgjDAX*@qa;0wKRcQ-vHr0(W8UK7Y%li)ypFxHgBEG~Oqd3TZCm-(c2- z%js)Ej-Y<`_f(tdBZjkrKD3qek-%LgXMY=gOuq-)$sJ zP9ZCZV7Cf63UwFjBZjk^^`Y%yeI#%#^3eCPK2mu5gsj5Y&-%#VA0Q92jrH3^aF9Gg zSck|%vB@KXeV9DdBjgdoIZ7VdG4e>@wv&f`oIFx^C&ppoX z56B~eT_z8;n>=DT56MG&L>>v;$K;_uA&(T^Q}QsLkw*r57NW}{IP6V2vevTDxRXrehMn{3XTlQ|zvVh)=ao2ZGV(2HCsRoaUd z6qBKXr74YIsTUSfv5-Qg7Q9eUuvAc?r7877L1+aFg4*uP`~BbiuzRE5GxNUR%3&$K)s6eoQ#Vmz%?feI6JX0@uv$4gpuV3pjbEgcK1A`&T9h*T4Ch*#;u%Cjaw zRFinOvTJuHc302SFJ)r7&vbl4^Bb%?Y?Q^9Zfv*7S~OdW z7He_9TC~cdAsa{3CVPDt7bCuMV{Pr$sU5PY-Gg48){!o2(QPeyWU;gtnqKQjpDa8% z*mgf}`%DfFdmxA$Wa7@noIxh1hzv0?eTIX`Atnp^;4{MH9+6Qdrq5Uq8E0~5KYaW| z!h|aiWP<0+6PaXUIv-||kPnp#BT2<95gjPEi0D9Z7C@x~Wu^c>42L}8y9GF6oh8wQ z5SUqF7gI!ZmaG-RB3xSRD8l!*NfvWO@Y4x$qX=`i$l=&x4D0Y1DMq_U-sfsD1Y4zG zTM5k7oBv1&9z^-(7vGeOtDlhh(l>;y6higVpD2Y;z4X5?MO!B~c}sKEn}4(n!}`sC zr3_Z_at&b~SgAMvkOy;ju+@qOJ4hrFB3^l*RvFM=4uOjW%jFnWLGX@-ck!^V0>df| zzOH~enTKCgpq(Z)UIhJ;N#^Folu7T!vbbS8^LGMGFilF6j33aTt3QQ{mC zogV8|SW31$x7uo)+a6h5tj2dIQ$#$f#&@R!B(DbMW;le3kBQ__yHN3%NIowqrxq$R zCPKxTS}dSY_WZLJXHmogcO3-9OimHe!SSFD7AiRM>tRvGf`xhvt0;L=k1eVw$@QTq zm%Y#WuwNA;7qVi+BOhM*UUqUdz^PKsKiz;vW{lXy?FOv1ngn){&P?>>a6IY1nzG{v?qX6(OJaFM9FB>ug!?g~JA6a|GTGeX!8E@<|_hns<~C@f#6+N6G7l z$7Z>TrG8lG>&fGOSm=bw8Gt}tJvp@j(pBgi#-H>LC1d%se2no#&(v zVonT`1tL0d?h(-$lRN|!Zj}M4_<%?P``jU-)5bLnm6k38)N9{mTh9k(MDfSM#eF$BIhOz@8LZ-{21OV)kpkL>)g5K#{%@l#Wn$AUR(^( zN5sq~LtG_dW|JY_5;3#MF1jYsxIi}kZW4|4?dA0(+URU*I}D<;X@y8B@8%T|9Y@Vm zP8 zG+CU+(WumVIgO){7a#QzWit?%(PfBpM09km6KNokT@)RKsk-xAJc>tC-FcoyGOr{8S#DJ_!q*N4IJC iknE5#k6|;4>|%Bv9rU&577?9C&i997@34xHw4M6+?hyw&eChq`Z1)-419YFj6h$DnU JCLa(E0RVxPKIZ@c delta 545 zcmZoMUZF8TgGobVqgE3$W5(vS%qGmzOiYXnK)}Hu0iqa~8520b42BcjlmD zm9bfZ-GgzGKnU-DAOI@|DVgjm5DgaLg=heZEW{A`3KbE6tML?!W(AoeFma>khYr5^lyIj+F#Ntt(t@qXa~42uS)7R5}bU PttlBT0*V5-W1%ttGbq0H diff --git a/tests/regression_tests/surface_source_write/case-12/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-12/surface_source_true.h5 index 6c6925daba145c5db52a281023dd017f066e0732..4b830d057dee2d3175982058352b5aae3fbb2e45 100644 GIT binary patch delta 1337 zcmY+@OKc2r9KiA2Ru~ctA{nn3B#e5ED#mlid(>;_A4`={udPRIJ?q^f9=B%ym!70t z?14n};?})sdRiCRn@Kp#aTCHEM0_{X$+F2O|M}1C=KK3kvW3Sr0Uzqa$-kZvA5r6;R)zXCQ zgECFXEJ`Et(}h%L2-yiA&P*ZSq4KbuW%P}CdB*lVt&7*y6;&kWC(9rbk8(}>@`JhxsVwY zVbybdC@VNVO0XMv{ZLnuhYx2J$A`9><0F7;k%zv9bA% z5V?)yQG}Hu55*>r66{Unp>8G*AI=u?(6*9C0CyXC=-bJojPwrjFdXs-;q4?3vyptG z$n7GJBCOryq3j`#670R?q3$COAC5~N+J5o~;2t0ky@`C2NJW1I#cl~Fe&4$-9DLK& z;=lUwXYg~O>G|m9AZ5I$|J3>NYwA~i_3QUPhg19A;g9W4Hrr1hIhl>Oy5*ocRi*{w z!#K$J2;m)Ke3*wB|FFoV86QPhM;ITx z%1ECi52KYlLU^ah!#qtMnKO|ht93S_oMQ%*U}xwED1@67P$m<0JR6yV&YEq)n=9X=>bmg+m zEj+WWHe0s!;vcru*=9@2HrH~?&8^yUv#FI!w_db#*0Q?K`99}4zm5BkKA-3LKF>Ml zUeDL_`;WfQUQBp(aYAVS$%6fU-;UEK{44#l6B2w*{4>!>r#~FOFE&yC#ksy${S!!c zV^v@8IGsMB-gkIfXYzmXk1nSYb!=L%CJ7LDh+oKku5pDlq(GG@#PkUWxyv;ZgqRv$ zoM%?jxSC1h$mDV4F(J2SU_Hq~DpEnF2r<*Ajw92A1k<2O5psyhbS8;%Vl$*B-Z_y@ zHFfM{nyQ=?)j=8scV;4(>CMdY1{oCGVojzuGus=?@dk4#D9u3YdEQKxH<<4YvMD%_ z2~7?KS2Lsfn_s~Da8>av>?7AJ%<~2dy}=?5beF|)8W-oFOFpF+&c(z63ZCU)2?duq zSW3b9Y?Kvppj$4JmgnZ-Ygx`@vP#M7ok&Ms6elW{71M%ZPA93eSrC*+i{$xO)e4%h zYd*%U@{TK&+N5l#%a{aIACuKWPG+ND(7XI{3byA&_0rbFYt>u9RP{+tvj%Gkv@M9~ zpGO6iPFa9;RCENr*JpJ5p6Fw^{Eo90&ACSH1;ULBuPrrl2PeZEL)lS_%>t zB2(whtn&u--k`x7G@pFoG61Xo!U-Wjic2U{}vAP#sAT22xp7%(P{|w?cX1S zP~ZNif~cJ*wSNV%zbqjw{pQE)2^webr_ z6-J;Avf8iqMWEKEaWew-8d8^5BUnqpE7fRKAq%-u4WV|8k{YC|B)zu==@6-}aylaE z>{=|OTF9Q--X!wU8Y1dq?SQ^PwOqClF}~tg$&FB1Um0w~m<}5Ac_YSH zOG#>ipi9Q|H9@d}1m~L|u$Ge841uu}zv^y=pqm6AH{;vwi3ckth6Wx^{UciY@Y547 z^j&}Z&woF?`dP=pK=030*>9FydFI#;RlmJo_TH%nm#U&|m%i9_AQF06gpSm6k&aP1(26AvQqa?iTI)8)T2VVh>4Y|wv)cjH7~`5Lll%)Cs{968zeULz+rKqzl0`3SQ$N zg@QXAXiur!fU+5sIn05!lUo}wp6o<_RN+R9w?49OBgShVxw#R746b)o{w4@AnK zCe~4|FqtDYdEIELZRJ2W)~DYQS2@uBv8)Ff{f^k*gW1+P=JsN?wGKmaWzs0u80&CU z#%8Fjbr@o;;{w+h>u{7I);iMq(8pTGFq0)R{TvhPA1VFVx%Q8z`>`eCAC5Z9>E-lY zWe#BbMHK8BK&|zWGXq$Fd2jetQVe8;ETE5xb(E7aEI>a)rVpa2eTH-oqD2|EaMcMW z)>eF5P-ASxQC&>RW$(=-YY+J&NcU0t zl@aVpJHz)QsI|_Jw+qc<@}2EvGAQILCe|aecSAKKHG6hrrhbupyBlMyM~prNf%OPO pj7P-(Z^AX!BQi$ORC~m;qu5A*c6FJ92^8c!9Ze-?Fpm+_;D5%jpD6$U diff --git a/tests/regression_tests/surface_source_write/case-13/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-13/surface_source_true.h5 index 6c6925daba145c5db52a281023dd017f066e0732..f53e2c3a2b0b5d0a55456dd2c6ab1ea3d7ca01af 100644 GIT binary patch delta 1337 zcmY+@OKc2r9KiA2Ru~ct(qt$xNEr1RRXUzK-s3&=kEP0}*Vd!9p7m}Kk6W||?)lEM6&wpk&-`{_d%|EUxeNXlnRoN^3bq}kP zV&GkMLv?MkDw*J;B(y~NqB@yS%lkwk+baKz)y8~%ZM6+(s3XYEKlh5AoigB&0LBvi~?m$Uqgcyq4)xE7#?He%k20SHo`+fB*m0(u8~o zWx9}A6i4*U5K^rbvJ*a>nL@rt`FYnY`iQ@)&ld6-(t3q4N5~$;_lD+1*S+&9_u^;q znw?*H*0+F09epEOorTe=aj$&8%w5Ep5bsNi`Tdg0zGtbBA-rYLJ<}Aj*A%(sLS|5a zRnPIEtl;=4!fxR8LtRN8KAcq?A6g^FM*!C%4}CSqM+xaQynYyKIX*&o>v;Vz*OOl_ zavR8_04qfvicKCx*c-`1-9#QfoXzB+Z6S{U?pE^9w~FwlUIOGw++d&@YPVzfN zZWnnJVC^OkWe<52Vecgmbsu^7a9r}x_LD~d_W*h5P2`(ID*7uZc1t+<$KI{s;M=Yi z|MkzmgJ1GZ&&M_gDdR=`=gwE(Qos8e-+cHroZ9aWe` z;~?WBgm;MXVIF4u!y=bvd=y|EVSFe@86QR1&5RHA7~{i-)57@Bjx#<2xF;ANy2tn^ zA$^iOj8^gp;hiE6^E7#6&O{2V*4c=1ju}vdouLo4jXr!h=jlVcKpz3zi}ayiqK^{N z?et+>rjHO_mOji5`W+&7g*>dQk(aE>HI9!W?Ca#A-XISj&Q0>rI>{q|dy72u+vHI~ i`VM&*cgZ7!caJ>G`{eJ7+yn9>tS;U@ly34Uc8h-=b1uvP delta 4944 zcmZXXeN5F=7{`B?`{ODaDhdktf*`1%D4>ED1@67P$m<0JR6yV&YEq)n=9X=>bmg+m zEj+WWHe0s!;vcru*=9@2HrH~?&8^yUv#FI!w_db#*0Q?K`99}4zm5BkKA-3LKF>Ml zUeDL_`;WfQUQBp(aYAVS$%6fU-;UEK{44#l6B2w*{4>!>r#~FOFE&yC#ksy${S!!c zV^v@8IGsMB-gkIfXYzmXk1nSYb!=L%CJ7LDh+oKku5pDlq(GG@#PkUWxyv;ZgqRv$ zoM%?jxSC1h$mDV4F(J2SU_Hq~DpEnF2r<*Ajw92A1k<2O5psyhbS8;%Vl$*B-Z_y@ zHFfM{nyQ=?)j=8scV;4(>CMdY1{oCGVojzuGus=?@dk4#D9u3YdEQKxH<<4YvMD%_ z2~7?KS2Lsfn_s~Da8>av>?7AJ%<~2dy}=?5beF|)8W-oFOFpF+&c(z63ZCU)2?duq zSW3b9Y?Kvppj$4JmgnZ-Ygx`@vP#M7ok&Ms6elW{71M%ZPA93eSrC*+i{$xO)e4%h zYd*%U@{TK&+N5l#%a{aIACuKWPG+ND(7XI{3byA&_0rbFYt>u9RP{+tvj%Gkv@M9~ zpGO6iPFa9;RCENr*JpJ5p6Fw^{Eo90&ACSH1;ULBuPrrl2PeZEL)lS_%>t zB2(whtn&u--k`x7G@pFoG61Xo!U-Wjic2U{}vAP#sAT22xp7%(P{|w?cX1S zP~ZNif~cJ*wSNV%zbqjw{pQE)2^webr_ z6-J;Avf8iqMWEKEaWew-8d8^5BUnqpE7fRKAq%-u4WV|8k{YC|B)zu==@6-}aylaE z>{=|OTF9Q--X!wU8Y1dq?SQ^PwOqClF}~tg$&FB1Um0w~m<}5Ac_YSH zOG#>ipi9Q|H9@d}1m~L|u$Ge841uu}zv^y=pqm6AH{;vwi3ckth6Wx^{UciY@Y547 z^j&}Z&woF?`dP=pK=030*>9FydFI#;RlmJo_TH%nm#U&|m%i9_AQF06gpSm6k&aP1(26AvQqa?iTI)8)T2VVh>4Y|wv)cjH7~`5Lll%)Cs{968zeULz+rKqzl0`3SQ$N zg@QXAXiur!fU+5sIn05!lUo}wp6o<_RN+R9w?49OBgShVxw#R746b)o{w4@AnK zCe~4|FqtDYdEIELZRJ2W)~DYQS2@uBv8)Ff{f^k*gW1+P=JsN?wGKmaWzs0u80&CU z#%8Fjbr@o;;{w+h>u{7I);iMq(8pTGFq0)R{TvhPA1VFVx%Q8z`>`eCAC5Z9>E-lY zWe#BbMHK8BK&|zWGXq$Fd2jetQVe8;ETE5xb(E7aEI>a)rVpa2eTH-oqD2|EaMcMW z)>eF5P-ASxQC&>RW$(=-YY+J&NcU0t zl@aVpJHz)QsI|_Jw+qc<@}2EvGAQILCe|aecSAKKHG6hrrhbupyBlMyM~prNf%OPO pj7P-(Z^AX!BQi$ORC~m;qu5A*c6FJ92^8c!9Ze-?Fpm+_;D5%jpD6$U diff --git a/tests/regression_tests/surface_source_write/case-14/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-14/surface_source_true.h5 index 6c6925daba145c5db52a281023dd017f066e0732..ec049fec48a990387c41b36f590e7ee85eb667eb 100644 GIT binary patch delta 1337 zcmY+@Ic(EV9DwnpMPQJi283iTVL$>?E>kXZHKttVDuzC2pcKpB%ljhvw=E=1!c5U29J?I-~l1N$h88IetLd(qVNAKrSPPt{4LpQ)MT%|uYXjV z6oYSTSJlo;)+7^rl!TV3T+}8LYGt2DWKYW9W3@3O1J?OLZJd*QUkLsqJw?dRqx!~+ z+=OUb99`o_@oG;LGLLxgPZH9ptL{He7BWzUtgIFKlF*8$PnK0=$>f`*=LH}3L!Hn z!fN37P*!q$lwhyo^+R1v9zL8k93R?Rj*kGYMIQP(j*l|Z>v{b!HgJ4|@HX=LVQwP7 zN#q*IqX;WS9*RvKCD@zELv11tAI=u?(6*9C0CyXC=-bJojPwrjFdXs-;q4?3a~Jtt zBDb47im>*Ohq9MEO0b*BL)}LnJ{*@kwEg4}z&$`7dJFj$k&6BbirpU8eQ(|t4!!Aa z_XmFb8Twjic`>#vNEt61zI44BN&U*Nef{C*aB9Ch{Hfz5l#!2!B;hiE6^E7#6&P0l=*4c=1ju}vbouLo)Jbn0ZF3^W|kv;;rm*_*kOdn;W zJLto>LLVW#EPa@r^gBiFDtTDfBK@q&b&ih`>>K2v-Xsqn&MorLy2vAddz(D;JLFMD i`Yw4G_sAoJcb`1W2jm}!+(YuCtZv>ulpgXZ^@zVHcP`EV delta 4944 zcmZXXeN5F=7{`B?`{ODaDhdktf*`1%D4>ED1@67P$m<0JR6yV&YEq)n=9X=>bmg+m zEj+WWHe0s!;vcru*=9@2HrH~?&8^yUv#FI!w_db#*0Q?K`99}4zm5BkKA-3LKF>Ml zUeDL_`;WfQUQBp(aYAVS$%6fU-;UEK{44#l6B2w*{4>!>r#~FOFE&yC#ksy${S!!c zV^v@8IGsMB-gkIfXYzmXk1nSYb!=L%CJ7LDh+oKku5pDlq(GG@#PkUWxyv;ZgqRv$ zoM%?jxSC1h$mDV4F(J2SU_Hq~DpEnF2r<*Ajw92A1k<2O5psyhbS8;%Vl$*B-Z_y@ zHFfM{nyQ=?)j=8scV;4(>CMdY1{oCGVojzuGus=?@dk4#D9u3YdEQKxH<<4YvMD%_ z2~7?KS2Lsfn_s~Da8>av>?7AJ%<~2dy}=?5beF|)8W-oFOFpF+&c(z63ZCU)2?duq zSW3b9Y?Kvppj$4JmgnZ-Ygx`@vP#M7ok&Ms6elW{71M%ZPA93eSrC*+i{$xO)e4%h zYd*%U@{TK&+N5l#%a{aIACuKWPG+ND(7XI{3byA&_0rbFYt>u9RP{+tvj%Gkv@M9~ zpGO6iPFa9;RCENr*JpJ5p6Fw^{Eo90&ACSH1;ULBuPrrl2PeZEL)lS_%>t zB2(whtn&u--k`x7G@pFoG61Xo!U-Wjic2U{}vAP#sAT22xp7%(P{|w?cX1S zP~ZNif~cJ*wSNV%zbqjw{pQE)2^webr_ z6-J;Avf8iqMWEKEaWew-8d8^5BUnqpE7fRKAq%-u4WV|8k{YC|B)zu==@6-}aylaE z>{=|OTF9Q--X!wU8Y1dq?SQ^PwOqClF}~tg$&FB1Um0w~m<}5Ac_YSH zOG#>ipi9Q|H9@d}1m~L|u$Ge841uu}zv^y=pqm6AH{;vwi3ckth6Wx^{UciY@Y547 z^j&}Z&woF?`dP=pK=030*>9FydFI#;RlmJo_TH%nm#U&|m%i9_AQF06gpSm6k&aP1(26AvQqa?iTI)8)T2VVh>4Y|wv)cjH7~`5Lll%)Cs{968zeULz+rKqzl0`3SQ$N zg@QXAXiur!fU+5sIn05!lUo}wp6o<_RN+R9w?49OBgShVxw#R746b)o{w4@AnK zCe~4|FqtDYdEIELZRJ2W)~DYQS2@uBv8)Ff{f^k*gW1+P=JsN?wGKmaWzs0u80&CU z#%8Fjbr@o;;{w+h>u{7I);iMq(8pTGFq0)R{TvhPA1VFVx%Q8z`>`eCAC5Z9>E-lY zWe#BbMHK8BK&|zWGXq$Fd2jetQVe8;ETE5xb(E7aEI>a)rVpa2eTH-oqD2|EaMcMW z)>eF5P-ASxQC&>RW$(=-YY+J&NcU0t zl@aVpJHz)QsI|_Jw+qc<@}2EvGAQILCe|aecSAKKHG6hrrhbupyBlMyM~prNf%OPO pj7P-(Z^AX!BQi$ORC~m;qu5A*c6FJ92^8c!9Ze-?Fpm+_;D5%jpD6$U diff --git a/tests/regression_tests/surface_source_write/case-15/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-15/surface_source_true.h5 index 5bdb39b2a703fa7cdb17a153a688cb0ea7664f1c..fd5892096c58be336ca5c9fea4b7afc60dd66dbb 100644 GIT binary patch delta 30 mcmaDL@IYY08fM0j&1;!Wm>C%-KVmatdA7(RbF&1y2O|Ki$_gd` delta 30 mcmaDL@IYY08fM0f&1;!Wm>HQSKVmatxuw3WrHw1^ml1Um% z$I8YT85+x5Ql9dZ#~F*1DNkYHL+Lm3%zSV4TR*up{4+k4Ev+3nz4fQ%aX9nG^0wto z;&Xd6THD|Kdwz0W|68$oCb?i`e0shApYC9^(w&V$mv0E&FB?L8 zVdxGqrMM_`zY<*>x;17@E(zW5#FvKdD09loLiZ<09J=H5RF{YDFVZVQ_cH_am7!ZF zyDD_2SkPP@y0hdP zE>FH)etT%2kY`Hqq&(3?o*9#;&Z^@IqEl*GNjy!22&p`dIJlTGE7Buh4lOK>j5Zd?U znNl2-C;C918Iwcu#H;eFd%M3^_jKil@+2S0(^GvcPx^^G1NEo!WS_~ipjneAKP-PZ zv`6HbQhY8?^o2Y#CSS@EeCnqp4o};AC~9`mjD0& delta 4944 zcmZXXZ%EZ=6vw^SKc%Jl@2b~->CJ0i>)JH6tkkr$Zp~X(T56VBEp=H_C@?5cR!~sP zE(HYz1_lO=N%W$?n1O=^?VUk^vc?L0Va%XGV{P;KeJ{^*c8+iIe9rlPzvn#SCI9%{ z_T#tq@AB-M2gmXULv5W8AA}Z%rrGVbkcF?sAt~ygkWgFOV(XibU;6`pN2G+!cz*7D zn6k)k3q-{FqyJs_vgXxHpf0wBkzqkT3gV07s@sCh3sMu`5^kjrjX47M;`u6bwCzvu zo7uYwe0Da0FHoLH%TPUEW6f;<-_Bb}^r zKAo>JN!z&@yxo%lb(lKjmtZ?gzl9?cD;d%y$ap5P$Xt#DBRIC4TRavdI*ZS=XA#q9 zS==--n+sY6851Pj#Z^r%67$5xV{9wX4p*$RI{W_zx9hHOMENk{q4rU|t>_`r* zB9v8xo44!SSj`BA-9%u|q4{7ybu(~BoeJ~FF zkQ8_)x?V4KHuD35OnH$Xsh+5`00EKuP0tmO_DlipvSy3c@Y((~WcKwMGF$KkDF$ir zk{0=e+_JY27np@61#uSfnI1tVipXlNwb&*~oqT33GNbg#<7?q(NL(?<5UzNNXtsOO^T4QkYvs{5qJM*`{@5_Q^WTMk`^= zddx=aq=xlKGGtVc&w|vKan*<*pUSXBj0$evfZ!M%JiGxGhI|ynw-GbTwCc7X^BWPf zOvUWkgk!DZ-X>%k_1q>h>n}$;R-0chN1P$Ef|OVAnV|}p#;OHARB+R>O47bn$=h!# zxnId|wA&AalhytoU{w_C`gj@7LhxVnWQqi zcOug&p6*1ZnGOFE35GNY@<0$<9aPCWvq6wiK|Tvo@8>=vf_w@R{B^Mq7^vHY`%=_> z-|xbGR$<>wibh)8r^P?J@eIbYrXGtJa#N5wL8|s})pbE;_mE|G?B&5jd%4A1LCW^= znJfE<>HB@;=;{WZaH#=mr~X+@H$d%F?^n?XwN+eegxbu$YUFC~eoh7inc7bhN)PA8q_`L?($v`NCb*>QGqRiElCIi=&Ak1*8G9N*Mhg*~Z{dPT z+RiR28W;6Akc4khnuoXDW5ypwG)dc?O4F_i|HUmM+Y|_?I6Oa zBfMRIgeQ$0A%cjb5M=A?>W)HS$gm(E1@Rr@s@sCh3sUnHSKSN}m)>VikgDTQnVIXy zags~D|DVTkl4+NBlJ=cW-hNBlwOx2^%j#AapMBHCvrD>(`HgM}R_OJoyCE>7@M}&k z2=XdOaC%-KVmatdA-OYbF&1y2O|Ki_X;Zj delta 30 mcmaDL@IYY08fM0f&1;!Wm>HQSKVmatxvzODW3vRi2O|Kkf(lFk diff --git a/tests/regression_tests/surface_source_write/case-18/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-18/surface_source_true.h5 index e5a7619c552c4321cdcaa3710363e430fa57516b..436063aee23677c481b9cec5ff8e6c8791d021b7 100644 GIT binary patch delta 30 mcmaDL@IYY08fM0j&1;!Wm>C%-KVmatdA-OYbF&1y2O|Ki_X;Zj delta 30 mcmaDL@IYY08fM0f&1;!Wm>HQSKVmatd7ybJW3vRi2O|KkiwaEu diff --git a/tests/regression_tests/surface_source_write/case-19/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-19/surface_source_true.h5 index db8a49dceeb20ebdc3e0c1b8db765cafed5a4052..eda3e030bda7d7bc798329e59061173d516ec10c 100644 GIT binary patch delta 30 mcmaDL@IYY08fM0j&1;!Wm>C%-KVmatdArCWbF&1y2O|Kj2?{L$ delta 30 mcmaDL@IYY08fM0f&1;!Wm>HQSKVmatd8m0RW3vRi2O|KklnPD& diff --git a/tests/regression_tests/surface_source_write/case-20/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-20/surface_source_true.h5 index 01d2abf61cf8a41876d664d1aa505a967e7559cb..083ce90c8964c549b597eceef343401fe63fd70d 100644 GIT binary patch delta 1270 zcmY+ls^*pfT-a5zzsJ9$HJ0H8cWB@ z#u*tJ%U4pSOnD27+j+}dSol$TX1!sPfuhkYlly6`*C#|PW`dEeRb1x zWjZm@B$=F9R;QEr%srW`?d$(NJ2~s0--?5$l5?u*$@TgF^lcRS)8rdM|I3EZog4ZC z3>4>u{#T;&L%+sIc|quZC%!QBN0?Dv6#73&;?N&sPJMCc|02C4^gpwpxis|aWS52h z1WVe><;ge7Zw%cP@(dJ-Jkgc%jFeZ&6K|4dMs>A3$u;uKsjrnMy-uD5O)5{eS)S$Q z-TmGzyZX~xSlS8ZSu^iZ;>axRh|XSZSrKd z%d@1t!}sL5d>*H4=Jag(N8CXF>C{JXs~rlJ*&S z@}2TKL-(va1I2UlM9<4JQobNh{GvQFs#>1pC3)u5FUynel4n5^2@*6TK$SNcp-v@f-5YsNR$(c}t!-_1p5Ktvm~wz4B!HY1ib*56K@2-C=nK ziqGVUK9^^t{6e1iOL=BgU&)gkk!Mc*wLIxJ@+@e+l_&d7o+a(~^5jS5kB06Cc?ODO M@a{)f)KgDA_0&_(7WK3gQLv?jwzQ=Q0|Nu5 z3=AZt|1dBxFfcGM5W~PgQU(T^FfcGMkd&kh3?yY>ASr1N-|yZ_|34PYC-3|I|C59~ z{rfNH_g{R!p7h;#Gk)q#q0{nqtnhqcwa@1iH$WEP!`=v7JBUvVi?ekW>BugaG{obkvvP<7*5rQ9Q?r$Sf zDOHKibVJ@#Ss)2lNwr&Nx=7|Br8*NjAeBic$s9?r+FNx%KG4mxB!PoceW)|-ki9B1 zkP3+7Rt2OoX(O42_;ru}kW?nEBvT~ihrLxZWS?%Hgd9*QJtCD!6Qo*aCLjTolAu(F zR2m>hRK`h)j!I=SbtGeupzd*wNoA5H870|*9MwIwHL}NKq~^u$+%KDBIbX{ZkUEuRlGqKYY^E1duhkMsG%1zM z^gv!}wFr5o61gdrNjJ#?q*1HzEvcGRx=7|pLbs)QtyL$)QJI6-nHfw;Wzs=1OA@#v zRgrFPhuA4PR` zmfrt=<+x1{J4Gi*N*+jMGYur;Bt;LUvU9bLWDHWKpTfyVWs)TsCE0?MV~@|ReI$EK zMj(6iS(^|$X)~WnWikxOpR@(;21)uescdElVyEajNvc*Vn;9foBS}7%YQKJp0g_dc z#1n7TPqG4$f3)V-yYZ(|Rq3<(NR}aXP{*=Tne>t@L8^6g^qEv9JtT`Hk>^q!)IHrK z3lKZ!!=FoK(nT^4v6DYkCzVMjB&biHBME*Xm7U=oko+C)xU(dIdZ}!ton(fj>V>yz zgB-^jI_@-yzdXh+XEBB&DyUvRl0gVwd>@NlBwrc851W><%9% zDQc3+?(jNDRDWJ$kaH@|YpLwY%|h~5uH%kE?55o!sr^#+*vtsYCd97X%vVxf()SsL z*iE|uv70vCEY%gQhDg>ScEP5;mda!ha!vQFktDy7%4P;gR!I^q-l`vB*YXNUyj7~3 z`aXRm%OtUHrMjh6FUb-~^gD0W1G%kx79nC%-KVmat`LxI)bF&1y2O|KjHVQHT delta 30 mcmaDL@IYY08fM0f&1;!Wm>HQSKVmatd8BzNW3vRi2O|KkoeEC? diff --git a/tests/regression_tests/surface_source_write/case-a01/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-a01/surface_source_true.h5 index da36fc505eb67f3da0ce48511464b699a4fb16e3..807211e3ac16e7c7c452f6fd43079e2a4cfa71ea 100644 GIT binary patch delta 252 zcmY+-FAM=;7zXg~bH@39{+-WkCYmH%xFCq4@B~q$T@+0c&09p16dO~-i=rrs;srqv zJfe5|?RnnxgkIEJwPQi^)LB>>=Y!>04IR^pXDg$A#nQ_8{YnLS`6NvIwezQElN{(% z`1~Vd8*-&?3bILIorX;4Mt%ljXCZgmviEb4fPK$&@{k!lXi$J0$vBWXJ!x2kT&Puo nEGeQ<84^;v0$I_x3VAcH%&yCM7wa15=|;ZcEZKF=(!Gu^BJ)*K delta 850 zcmexhbHHSR29tx-My)1h#*EEtnN66bnV1+EfPjNR0z@$|GbV6=84M2uC;wrSU`evS zlCfEW-GgzGKnU-DAOI@|DVgjm5DgaLg=heZEW{A`iXq}D7|j7PM+juZ48a7j5m9g> zJ_ Date: Fri, 12 Dec 2025 08:21:46 +0200 Subject: [PATCH 476/671] IFP creates fatal error in all run modes except eigenvalue. (#3681) --- src/tallies/tally.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index 9daeb1d69..6eef1da9c 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -215,7 +215,7 @@ Tally::Tally(pugi::xml_node node) "number of inactive cycles."); } settings::ifp_on = true; - } else { + } else if (settings::run_mode == RunMode::FIXED_SOURCE) { fatal_error( "Iterated Fission Probability can only be used in an eigenvalue " "calculation."); From bbfa18d72c34d8710b5f4f3fa1fff54f5248fcc5 Mon Sep 17 00:00:00 2001 From: Kevin Sawatzky <66632997+nuclearkevin@users.noreply.github.com> Date: Fri, 12 Dec 2025 03:29:43 -0600 Subject: [PATCH 477/671] Add a command-line argument for output verbosity (#3680) Co-authored-by: Paul Romano --- docs/source/usersguide/scripts.rst | 1 + man/man1/openmc.1 | 3 +++ src/finalize.cpp | 2 +- src/initialize.cpp | 13 ++++++++++++- src/output.cpp | 1 + src/settings.cpp | 6 ++++-- 6 files changed, 22 insertions(+), 4 deletions(-) diff --git a/docs/source/usersguide/scripts.rst b/docs/source/usersguide/scripts.rst index 0879d63ef..eb0abeb0d 100644 --- a/docs/source/usersguide/scripts.rst +++ b/docs/source/usersguide/scripts.rst @@ -48,6 +48,7 @@ flags: restart file -s, --threads N Run with *N* OpenMP threads -t, --track Write tracks for all particles (up to max_tracks) +-q, --verbosity V Set the output verbosity to *V* -v, --version Show version information -h, --help Show help message diff --git a/man/man1/openmc.1 b/man/man1/openmc.1 index 7826a759a..30e8b2ce4 100644 --- a/man/man1/openmc.1 +++ b/man/man1/openmc.1 @@ -39,6 +39,9 @@ Use \fIN\fP OpenMP threads. .B "\-t\fR, \fP\-\-track" Write tracks for all particles (up to max_tracks). .TP +.BI \-q " V" "\fR,\fP \-\-verbosity" " V" +Set the output verbosity to \fIV\fP. +.TP .B "\-v\fR, \fP\-\-version" Show version information. .TP diff --git a/src/finalize.cpp b/src/finalize.cpp index 9ee943409..344eaa1a0 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -142,7 +142,7 @@ int openmc_finalize() settings::uniform_source_sampling = false; settings::ufs_on = false; settings::urr_ptables_on = true; - settings::verbosity = 7; + settings::verbosity = -1; settings::weight_cutoff = 0.25; settings::weight_survive = 1.0; settings::weight_windows_file.clear(); diff --git a/src/initialize.cpp b/src/initialize.cpp index e2a5b9743..a2269ed1e 100644 --- a/src/initialize.cpp +++ b/src/initialize.cpp @@ -226,6 +226,15 @@ int parse_command_line(int argc, char* argv[]) i += 1; settings::n_particles = std::stoll(argv[i]); + } else if (arg == "-q" || arg == "--verbosity") { + i += 1; + settings::verbosity = std::stoi(argv[i]); + if (settings::verbosity > 10 || settings::verbosity < 1) { + auto msg = fmt::format("Invalid verbosity: {}.", settings::verbosity); + strcpy(openmc_err_msg, msg.c_str()); + return OPENMC_E_INVALID_ARGUMENT; + } + } else if (arg == "-e" || arg == "--event") { settings::event_based = true; } else if (arg == "-r" || arg == "--restart") { @@ -376,8 +385,10 @@ bool read_model_xml() auto settings_root = root.child("settings"); // Verbosity - if (check_for_node(settings_root, "verbosity")) { + if (check_for_node(settings_root, "verbosity") && settings::verbosity == -1) { settings::verbosity = std::stoi(get_node_value(settings_root, "verbosity")); + } else if (settings::verbosity == -1) { + settings::verbosity = 7; } // To this point, we haven't displayed any output since we didn't know what diff --git a/src/output.cpp b/src/output.cpp index 0a14e8843..80e2b10ab 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -281,6 +281,7 @@ void print_usage() " -t, --track Write tracks for all particles (up to " "max_tracks)\n" " -e, --event Run using event-based parallelism\n" + " -q, --verbosity Output verbosity\n" " -v, --version Show version information\n" " -h, --help Show this message\n"); } diff --git a/src/settings.cpp b/src/settings.cpp index d47e9b5e6..5b472468f 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -145,7 +145,7 @@ int trace_gen; int64_t trace_particle; vector> track_identifiers; int trigger_batch_interval {1}; -int verbosity {7}; +int verbosity {-1}; double weight_cutoff {0.25}; double weight_survive {1.0}; @@ -396,8 +396,10 @@ void read_settings_xml() xml_node root = doc.document_element(); // Verbosity - if (check_for_node(root, "verbosity")) { + if (check_for_node(root, "verbosity") && verbosity == -1) { verbosity = std::stoi(get_node_value(root, "verbosity")); + } else if (verbosity == -1) { + verbosity = 7; } // To this point, we haven't displayed any output since we didn't know what From d1183566383e38e96c97db195613c407f9a5c091 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Wed, 17 Dec 2025 00:10:07 -0600 Subject: [PATCH 478/671] Use MeshBase method to check replicated. Add header for replicated mesh (#3689) --- src/mesh.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mesh.cpp b/src/mesh.cpp index 610d057cf..c3a2d4581 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -51,6 +51,7 @@ #include "libmesh/mesh_modification.h" #include "libmesh/mesh_tools.h" #include "libmesh/numeric_vector.h" +#include "libmesh/replicated_mesh.h" #endif #ifdef OPENMC_DAGMC_ENABLED @@ -3435,7 +3436,7 @@ LibMesh::LibMesh(hid_t group) : UnstructuredMesh(group) // create the mesh from a pointer to a libMesh Mesh LibMesh::LibMesh(libMesh::MeshBase& input_mesh, double length_multiplier) { - if (!dynamic_cast(&input_mesh)) { + if (!input_mesh.is_replicated()) { fatal_error("At present LibMesh tallies require a replicated mesh. Please " "ensure 'input_mesh' is a libMesh::ReplicatedMesh."); } From e0eb91b9551ecc97c7027b4dfe242fc63e4eedde Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Wed, 17 Dec 2025 21:44:53 +0100 Subject: [PATCH 479/671] using id_map in model.plot for more efficient plotting (#3678) --- openmc/model/model.py | 195 +++++++++++++++------------------ openmc/plots.py | 82 +++++++++++++- tests/unit_tests/test_model.py | 42 +++++++ 3 files changed, 211 insertions(+), 108 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index a9aaa481d..6e4c1c585 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -23,7 +23,7 @@ from openmc.dummy_comm import DummyCommunicator from openmc.executor import _process_CLI_arguments from openmc.checkvalue import check_type, check_value, PathLike from openmc.exceptions import InvalidIDError -from openmc.plots import add_plot_params, _BASIS_INDICES +from openmc.plots import add_plot_params, _BASIS_INDICES, id_map_to_rgb from openmc.utility_funcs import change_directory @@ -1114,13 +1114,12 @@ class Model: color_by: str = 'cell', colors: dict | None = None, seed: int | None = None, - openmc_exec: PathLike = 'openmc', axes=None, legend: bool = False, axis_units: str = 'cm', outline: bool | str = False, show_overlaps: bool = False, - overlap_color: Sequence[int] | str | None = None, + overlap_color: Sequence[int] | str = (255, 0, 0), n_samples: int | None = None, plane_tolerance: float = 1., legend_kwargs: dict | None = None, @@ -1132,7 +1131,6 @@ class Model: .. versionadded:: 0.15.1 """ - import matplotlib.image as mpimg import matplotlib.patches as mpatches import matplotlib.pyplot as plt @@ -1162,125 +1160,108 @@ class Model: y_min = (origin[y] - 0.5*width[1]) * axis_scaling_factor[axis_units] y_max = (origin[y] + 0.5*width[1]) * axis_scaling_factor[axis_units] - # Determine whether any materials contains macroscopic data and if so, - # set energy mode accordingly - _energy_mode = self.settings._energy_mode - for mat in self.geometry.get_all_materials().values(): - if mat._macroscopic is not None: - self.settings.energy_mode = 'multi-group' - break + # Get ID map from the C API + id_map = self.id_map( + origin=origin, + width=width, + pixels=pixels, + basis=basis, + color_overlaps=show_overlaps + ) - with TemporaryDirectory() as tmpdir: - _plot_seed = self.settings.plot_seed - if seed is not None: - self.settings.plot_seed = seed - - # Create plot object matching passed arguments + # Generate colors if not provided + if colors is None and seed is not None: + # Use the colorize method to generate random colors plot = openmc.SlicePlot() - plot.origin = origin - plot.width = width - plot.pixels = pixels - plot.basis = basis plot.color_by = color_by - plot.show_overlaps = show_overlaps - if overlap_color is not None: - plot.overlap_color = overlap_color - if colors is not None: - plot.colors = colors - self.plots.append(plot) + plot.colorize(self.geometry, seed=seed) + colors = plot.colors - # Run OpenMC in geometry plotting mode - self.plot_geometry(False, cwd=tmpdir, openmc_exec=openmc_exec) + # Convert ID map to RGB image + img = id_map_to_rgb( + id_map=id_map, + color_by=color_by, + colors=colors, + overlap_color=overlap_color + ) - # Undo changes to model - self.plots.pop() - self.settings._plot_seed = _plot_seed - self.settings._energy_mode = _energy_mode + # Create a figure sized such that the size of the axes within + # exactly matches the number of pixels specified + if axes is None: + px = 1/plt.rcParams['figure.dpi'] + fig, axes = plt.subplots() + axes.set_xlabel(xlabel) + axes.set_ylabel(ylabel) + params = fig.subplotpars + width_px = pixels[0]*px/(params.right - params.left) + height_px = pixels[1]*px/(params.top - params.bottom) + fig.set_size_inches(width_px, height_px) - # Read image from file - img_path = Path(tmpdir) / f'plot_{plot.id}.png' - if not img_path.is_file(): - img_path = img_path.with_suffix('.ppm') - img = mpimg.imread(str(img_path)) + if outline: + # Combine R, G, B values into a single int for contour detection + rgb = (img * 256).astype(int) + image_value = (rgb[..., 0] << 16) + \ + (rgb[..., 1] << 8) + (rgb[..., 2]) - # Create a figure sized such that the size of the axes within - # exactly matches the number of pixels specified - if axes is None: - px = 1/plt.rcParams['figure.dpi'] - fig, axes = plt.subplots() - axes.set_xlabel(xlabel) - axes.set_ylabel(ylabel) - params = fig.subplotpars - width = pixels[0]*px/(params.right - params.left) - height = pixels[1]*px/(params.top - params.bottom) - fig.set_size_inches(width, height) + # Set default arguments for contour() + if contour_kwargs is None: + contour_kwargs = {} + contour_kwargs.setdefault('colors', 'k') + contour_kwargs.setdefault('linestyles', 'solid') + contour_kwargs.setdefault('algorithm', 'serial') - if outline: - # Combine R, G, B values into a single int - rgb = (img * 256).astype(int) - image_value = (rgb[..., 0] << 16) + \ - (rgb[..., 1] << 8) + (rgb[..., 2]) + axes.contour( + image_value, + origin="upper", + levels=np.unique(image_value), + extent=(x_min, x_max, y_min, y_max), + **contour_kwargs + ) + + # If only showing outline, set the axis limits and aspect explicitly + if outline == 'only': + axes.set_xlim(x_min, x_max) + axes.set_ylim(y_min, y_max) + axes.set_aspect('equal') - # Set default arguments for contour() - if contour_kwargs is None: - contour_kwargs = {} - contour_kwargs.setdefault('colors', 'k') - contour_kwargs.setdefault('linestyles', 'solid') - contour_kwargs.setdefault('algorithm', 'serial') + # Add legend showing which colors represent which material or cell + if legend: + if colors is None or len(colors) == 0: + raise ValueError("Must pass 'colors' dictionary if you " + "are adding a legend via legend=True.") - axes.contour( - image_value, - origin="upper", - levels=np.unique(image_value), - extent=(x_min, x_max, y_min, y_max), - **contour_kwargs - ) + if color_by == "cell": + expected_key_type = openmc.Cell + else: + expected_key_type = openmc.Material - # add legend showing which colors represent which material - # or cell if that was requested - if legend: - if plot.colors == {}: - raise ValueError("Must pass 'colors' dictionary if you " - "are adding a legend via legend=True.") + patches = [] + for key, color in colors.items(): + if isinstance(key, int): + raise TypeError( + "Cannot use IDs in colors dict for auto legend.") + elif not isinstance(key, expected_key_type): + raise TypeError( + "Color dict key type does not match color_by") - if color_by == "cell": - expected_key_type = openmc.Cell + # this works whether we're doing cells or materials + label = key.name if key.name != '' else key.id + + # matplotlib takes RGB on 0-1 scale rather than 0-255 + if len(color) == 3 and not isinstance(color, str): + scaled_color = ( + color[0]/255, color[1]/255, color[2]/255) else: - expected_key_type = openmc.Material + scaled_color = color - patches = [] - for key, color in plot.colors.items(): + key_patch = mpatches.Patch(color=scaled_color, label=label) + patches.append(key_patch) - if isinstance(key, int): - raise TypeError( - "Cannot use IDs in colors dict for auto legend.") - elif not isinstance(key, expected_key_type): - raise TypeError( - "Color dict key type does not match color_by") - - # this works whether we're doing cells or materials - label = key.name if key.name != '' else key.id - - # matplotlib takes RGB on 0-1 scale rather than 0-255. at - # this point PlotBase has already checked that 3-tuple - # based colors are already valid, so if the length is three - # then we know it just needs to be converted to the 0-1 - # format. - if len(color) == 3 and not isinstance(color, str): - scaled_color = ( - color[0]/255, color[1]/255, color[2]/255) - else: - scaled_color = color - - key_patch = mpatches.Patch(color=scaled_color, label=label) - patches.append(key_patch) - - axes.legend(handles=patches, **legend_kwargs) - - # Plot image and return the axes - if outline != 'only': - axes.imshow(img, extent=(x_min, x_max, y_min, y_max), **kwargs) + axes.legend(handles=patches, **legend_kwargs) + # Plot image and return the axes + if outline != 'only': + axes.imshow(img, extent=(x_min, x_max, y_min, y_max), **kwargs) if n_samples: # Sample external source particles diff --git a/openmc/plots.py b/openmc/plots.py index cb722abc6..8b67d5cac 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -1,4 +1,4 @@ -from collections.abc import Iterable, Mapping +from collections.abc import Iterable, Mapping, Sequence from numbers import Integral, Real from pathlib import Path from textwrap import dedent @@ -355,6 +355,86 @@ def voxel_to_vtk(voxel_file: PathLike, output: PathLike = 'plot.vti'): return output +def id_map_to_rgb( + id_map: np.ndarray, + color_by: str = 'cell', + colors: dict | None = None, + overlap_color: Sequence[int] | str = (255, 0, 0) +) -> np.ndarray: + """Convert ID map array to RGB image array. + + Parameters + ---------- + id_map : numpy.ndarray + Array with shape (v_pixels, h_pixels, 3) containing cell IDs, + cell instances, and material IDs + color_by : {'cell', 'material'} + Whether to color by cell or material + colors : dict, optional + Dictionary mapping cells/materials to colors + overlap_color : sequence of int or str, optional + Color to use for overlaps. Defaults to red (255, 0, 0). + + Returns + ------- + numpy.ndarray + RGB image array with shape (v_pixels, h_pixels, 3) with values + in range [0, 1] for matplotlib + """ + # Initialize RGB array with white background (values between 0 and 1 for matplotlib) + img = np.ones(id_map.shape, dtype=float) + + # Get the appropriate index based on color_by + if color_by == 'cell': + id_index = 0 # Cell IDs are in the first channel + elif color_by == 'material': + id_index = 2 # Material IDs are in the third channel + else: + raise ValueError("color_by must be either 'cell' or 'material'") + + # Get all unique IDs in the plot + unique_ids = np.unique(id_map[:, :, id_index]) + + # Generate default colors if not provided + if colors is None: + colors = {} + + # Convert colors dict to use IDs as keys + color_map = {} + for key, color in colors.items(): + if isinstance(key, (openmc.Cell, openmc.Material)): + color_map[key.id] = color + else: + color_map[key] = color + + # Generate random colors for IDs not in color_map + rng = np.random.RandomState(1) + for uid in unique_ids: + if uid > 0 and uid not in color_map: + color_map[uid] = rng.randint(0, 256, (3,)) + + # Apply colors to each pixel + for uid in unique_ids: + if uid == -1: # Background/void + continue + elif uid == -3: # Overlap (only present if color_overlaps was True) + if isinstance(overlap_color, str): + rgb = _SVG_COLORS[overlap_color.lower()] + else: + rgb = overlap_color + mask = id_map[:, :, id_index] == uid + img[mask] = np.array(rgb) / 255.0 + elif uid in color_map: + color = color_map[uid] + if isinstance(color, str): + rgb = _SVG_COLORS[color.lower()] + else: + rgb = color + mask = id_map[:, :, id_index] == uid + img[mask] = np.array(rgb) / 255.0 + + return img + class PlotBase(IDManagerMixin): """ Parameters diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index d553af53c..3846ba4fb 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -7,6 +7,7 @@ import pytest import openmc import openmc.lib +from openmc.plots import id_map_to_rgb @pytest.fixture(scope='function') @@ -996,3 +997,44 @@ def test_keff_search(run_in_tmpdir): # Check that total_batches property works assert result.total_batches == sum(result.batches) assert result.total_batches > 0 + + +def test_id_map_to_rgb(): + """Test conversion of ID map to RGB image array.""" + # Create a simple model + mat = openmc.Material() + mat.set_density('g/cm3', 1.0) + mat.add_nuclide('Li7', 1.0) + + sphere = openmc.Sphere(r=5.0, boundary_type='vacuum') + cell = openmc.Cell(fill=mat, region=-sphere) + geometry = openmc.Geometry([cell]) + settings = openmc.Settings( + batches=10, particles=100, run_mode='fixed source' + ) + model = openmc.Model(geometry, settings=settings) + + id_data = np.zeros((10, 10, 3), dtype=np.int32) + id_data[:, :, 0] = cell.id # Cell IDs + id_data[:, :, 2] = mat.id # Material IDs + + # Test color_by with default colors + for color_by in ['cell', 'material']: + rgb = id_map_to_rgb(id_data, color_by=color_by) + assert rgb.shape == (10, 10, 3) + assert rgb.dtype == float + assert np.all((rgb >= 0) & (rgb <= 1)) # RGB values in [0, 1] + + # Test with custom colors + colors = {cell.id: (255, 0, 0)} # Red + rgb_custom = id_map_to_rgb(id_data, color_by='cell', colors=colors) + assert np.allclose(rgb_custom, [1.0, 0.0, 0.0]) # All pixels should be red + + # Test with overlaps + id_data_overlap = id_data.copy() + id_data_overlap[5:, 5:, 0] = -3 # Mark some pixels as overlaps + rgb_overlap = id_map_to_rgb( + id_data_overlap, overlap_color=(0, 255, 0) + ) + # Check that overlap region is green + assert np.allclose(rgb_overlap[5:, 5:], [0.0, 1.0, 0.0]) From a230b86128f58e093e4cba4f499efdfd5d90cdc9 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Thu, 18 Dec 2025 17:56:43 +0200 Subject: [PATCH 480/671] Fix mcpl dependency in test (#3691) --- tests/unit_tests/test_collision_track.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit_tests/test_collision_track.py b/tests/unit_tests/test_collision_track.py index 9bc6a8c15..25344a3bd 100644 --- a/tests/unit_tests/test_collision_track.py +++ b/tests/unit_tests/test_collision_track.py @@ -5,6 +5,7 @@ import openmc import pytest import h5py import numpy as np +import shutil from tests.testing_harness import CollisionTrackTestHarness as ctt @@ -109,6 +110,7 @@ def test_particle_location(run_in_tmpdir, model): assert False +@pytest.mark.skipif(shutil.which("mcpl-config") is None, reason="MCPL is not available.") def test_format_similarity(run_in_tmpdir, model): model.settings.collision_track = {"max_collisions": 200, "reactions": ['elastic'], "cell_ids": [1, 2], "mcpl": False} From a2fd6cc57e7f0bce55b90e57e8d1a28d84bca5d4 Mon Sep 17 00:00:00 2001 From: Zoe Prieto <101403129+zoeprieto@users.noreply.github.com> Date: Sat, 20 Dec 2025 01:56:13 -0300 Subject: [PATCH 481/671] Support rotation in MeshFilter (#3176) Co-authored-by: Jonathan Shimwell Co-authored-by: Patrick Shriwise --- include/openmc/tallies/filter_mesh.h | 8 + openmc/filter.py | 43 +++ openmc/lib/filter.py | 40 +++ src/cell.cpp | 2 +- src/tallies/filter_mesh.cpp | 92 +++++++ src/weight_windows.cpp | 3 +- .../filter_rotations/__init__.py | 0 .../filter_rotations/inputs_true.dat | 59 +++++ .../filter_rotations/results_true.dat | 244 ++++++++++++++++++ .../regression_tests/filter_rotations/test.py | 72 ++++++ tests/unit_tests/test_filter_mesh.py | 26 ++ tests/unit_tests/test_lib.py | 7 + 12 files changed, 594 insertions(+), 2 deletions(-) create mode 100644 tests/regression_tests/filter_rotations/__init__.py create mode 100644 tests/regression_tests/filter_rotations/inputs_true.dat create mode 100644 tests/regression_tests/filter_rotations/results_true.dat create mode 100644 tests/regression_tests/filter_rotations/test.py diff --git a/include/openmc/tallies/filter_mesh.h b/include/openmc/tallies/filter_mesh.h index 4c9460d2e..c35a477fe 100644 --- a/include/openmc/tallies/filter_mesh.h +++ b/include/openmc/tallies/filter_mesh.h @@ -51,6 +51,12 @@ public: virtual bool translated() const { return translated_; } + virtual void set_rotation(const vector& rotation); + + virtual const vector& rotation() const { return rotation_; } + + virtual bool rotated() const { return rotated_; } + protected: //---------------------------------------------------------------------------- // Data members @@ -58,6 +64,8 @@ protected: int32_t mesh_; //!< Index of the mesh bool translated_ {false}; //!< Whether or not the filter is translated Position translation_ {0.0, 0.0, 0.0}; //!< Filter translation + bool rotated_ {false}; //!< Whether or not the filter is rotated + vector rotation_; //!< Filter rotation }; } // namespace openmc diff --git a/openmc/filter.py b/openmc/filter.py index 550146f85..39844798d 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -833,6 +833,25 @@ class MeshFilter(Filter): translation : Iterable of float This array specifies a vector that is used to translate (shift) the mesh for this filter + rotation : Iterable of float + This array specifies the angles in degrees about the x, y, and z axes + that the mesh should be rotated. The rotation applied is an intrinsic + rotation with specified Tait-Bryan angles. That is to say, if the angles + are :math:`(\phi, \theta, \psi)`, then the rotation matrix applied is + :math:`R_z(\psi) R_y(\theta) R_x(\phi)` or + + .. math:: + + \left [ \begin{array}{ccc} \cos\theta \cos\psi & -\cos\phi \sin\psi + + \sin\phi \sin\theta \cos\psi & \sin\phi \sin\psi + \cos\phi + \sin\theta \cos\psi \\ \cos\theta \sin\psi & \cos\phi \cos\psi + + \sin\phi \sin\theta \sin\psi & -\sin\phi \cos\psi + \cos\phi + \sin\theta \sin\psi \\ -\sin\theta & \sin\phi \cos\theta & \cos\phi + \cos\theta \end{array} \right ] + + A rotation matrix can also be specified directly by setting this + attribute to a nested list (or 2D numpy array) that specifies each + element of the matrix. bins : list of tuple A list of mesh indices for each filter bin, e.g. [(1, 1, 1), (2, 1, 1), ...] @@ -845,6 +864,7 @@ class MeshFilter(Filter): self.mesh = mesh self.id = filter_id self._translation = None + self._rotation = None def __hash__(self): string = type(self).__name__ + '\n' @@ -856,6 +876,7 @@ class MeshFilter(Filter): string += '{: <16}=\t{}\n'.format('\tMesh ID', self.mesh.id) string += '{: <16}=\t{}\n'.format('\tID', self.id) string += '{: <16}=\t{}\n'.format('\tTranslation', self.translation) + string += '{: <16}=\t{}\n'.format('\tRotation', self.rotation) return string @classmethod @@ -879,6 +900,10 @@ class MeshFilter(Filter): if translation: out.translation = translation[()] + rotation = group.get('rotation') + if rotation: + out.rotation = rotation[()] + return out @property @@ -911,6 +936,15 @@ class MeshFilter(Filter): cv.check_length('mesh filter translation', t, 3) self._translation = np.asarray(t) + @property + def rotation(self): + return self._rotation + + @rotation.setter + def rotation(self, rotation): + cv.check_length('mesh filter rotation', rotation, 3) + self._rotation = np.asarray(rotation) + def can_merge(self, other): # Mesh filters cannot have more than one bin return False @@ -996,6 +1030,8 @@ class MeshFilter(Filter): subelement.text = str(self.mesh.id) if self.translation is not None: element.set('translation', ' '.join(map(str, self.translation))) + if self.rotation is not None: + element.set('rotation', ' '.join(map(str, self.rotation.ravel()))) return element @classmethod @@ -1008,6 +1044,13 @@ class MeshFilter(Filter): translation = get_elem_list(elem, "translation", float) or [] if translation: out.translation = translation + + rotation = get_elem_list(elem, 'rotation', float) or [] + if rotation: + if len(rotation) == 3: + out.rotation = rotation + elif len(rotation) == 9: + out.rotation = np.array(rotation).reshape(3, 3) return out diff --git a/openmc/lib/filter.py b/openmc/lib/filter.py index 55b681d89..5cf496107 100644 --- a/openmc/lib/filter.py +++ b/openmc/lib/filter.py @@ -97,6 +97,14 @@ _dll.openmc_mesh_filter_get_translation.errcheck = _error_handler _dll.openmc_mesh_filter_set_translation.argtypes = [c_int32, POINTER(c_double*3)] _dll.openmc_mesh_filter_set_translation.restype = c_int _dll.openmc_mesh_filter_set_translation.errcheck = _error_handler +_dll.openmc_mesh_filter_get_rotation.argtypes = [c_int32, POINTER(c_double), + POINTER(c_size_t)] +_dll.openmc_mesh_filter_get_rotation.restype = c_int +_dll.openmc_mesh_filter_get_rotation.errcheck = _error_handler +_dll.openmc_mesh_filter_set_rotation.argtypes = [ + c_int32, POINTER(c_double), c_size_t] +_dll.openmc_mesh_filter_set_rotation.restype = c_int +_dll.openmc_mesh_filter_set_rotation.errcheck = _error_handler _dll.openmc_meshborn_filter_get_mesh.argtypes = [c_int32, POINTER(c_int32)] _dll.openmc_meshborn_filter_get_mesh.restype = c_int _dll.openmc_meshborn_filter_get_mesh.errcheck = _error_handler @@ -393,6 +401,10 @@ class MeshFilter(Filter): Mesh used for the filter translation : Iterable of float 3-D coordinates of the translation vector + rotation : Iterable of float + The rotation matrix or angles of the filter mesh. This can either be + a fully specified 3 x 3 rotation matrix or an Iterable of length 3 + with the angles in degrees about the x, y, and z axes, respectively. """ filter_type = 'mesh' @@ -422,6 +434,34 @@ class MeshFilter(Filter): def translation(self, translation): _dll.openmc_mesh_filter_set_translation(self._index, (c_double*3)(*translation)) + @property + def rotation(self): + rotation_data = np.zeros(12) + rot_size = c_size_t() + + _dll.openmc_mesh_filter_get_rotation( + self._index, rotation_data.ctypes.data_as(POINTER(c_double)), + rot_size) + rot_size = rot_size.value + + if rot_size == 9: + return rotation_data[:rot_size].shape(3, 3) + elif rot_size in (0, 12): + # If size is 0, rotation_data[9:] will be zeros. This indicates no + # rotation and is the most straightforward way to always return + # an iterable of floats + return rotation_data[9:] + else: + raise ValueError( + f'Invalid size of rotation matrix: {rot_size}') + + @rotation.setter + def rotation(self, rotation_data): + flat_rotation = np.asarray(rotation_data, dtype=float).flatten() + + _dll.openmc_mesh_filter_set_rotation( + self._index, flat_rotation.ctypes.data_as(POINTER(c_double)), + c_size_t(len(flat_rotation))) class MeshBornFilter(Filter): """MeshBorn filter stored internally. diff --git a/src/cell.cpp b/src/cell.cpp index e2479994a..fd459cdcc 100644 --- a/src/cell.cpp +++ b/src/cell.cpp @@ -57,7 +57,7 @@ void Cell::set_rotation(const vector& rot) fatal_error(fmt::format("Non-3D rotation vector applied to cell {}", id_)); } - // Compute and store the rotation matrix. + // Compute and store the inverse rotation matrix for the angles given. rotation_.clear(); rotation_.reserve(rot.size() == 9 ? 9 : 12); if (rot.size() == 3) { diff --git a/src/tallies/filter_mesh.cpp b/src/tallies/filter_mesh.cpp index 4edfbec4b..a0698992d 100644 --- a/src/tallies/filter_mesh.cpp +++ b/src/tallies/filter_mesh.cpp @@ -6,6 +6,7 @@ #include "openmc/constants.h" #include "openmc/error.h" #include "openmc/mesh.h" +#include "openmc/position.h" #include "openmc/xml_interface.h" namespace openmc { @@ -30,6 +31,10 @@ void MeshFilter::from_xml(pugi::xml_node node) if (check_for_node(node, "translation")) { set_translation(get_node_array(node, "translation")); } + // Read the rotation transform. + if (check_for_node(node, "rotation")) { + set_rotation(get_node_array(node, "rotation")); + } } void MeshFilter::get_all_bins( @@ -45,6 +50,12 @@ void MeshFilter::get_all_bins( last_r -= translation(); r -= translation(); } + // apply rotation if present + if (!rotation_.empty()) { + last_r = last_r.rotate(rotation_); + r = r.rotate(rotation_); + u = u.rotate(rotation_); + } if (estimator != TallyEstimator::TRACKLENGTH) { auto bin = model::meshes[mesh_]->get_bin(r); @@ -65,6 +76,9 @@ void MeshFilter::to_statepoint(hid_t filter_group) const if (translated_) { write_dataset(filter_group, "translation", translation_); } + if (rotated_) { + write_dataset(filter_group, "rotation", rotation_); + } } std::string MeshFilter::text_label(int bin) const @@ -93,6 +107,40 @@ void MeshFilter::set_translation(const double translation[3]) this->set_translation({translation[0], translation[1], translation[2]}); } +void MeshFilter::set_rotation(const vector& rot) +{ + rotated_ = true; + + // Compute and store the inverse rotation matrix for the angles given. + rotation_.clear(); + rotation_.reserve(rot.size() == 9 ? 9 : 12); + if (rot.size() == 3) { + double phi = -rot[0] * PI / 180.0; + double theta = -rot[1] * PI / 180.0; + double psi = -rot[2] * PI / 180.0; + rotation_.push_back(std::cos(theta) * std::cos(psi)); + rotation_.push_back(-std::cos(phi) * std::sin(psi) + + std::sin(phi) * std::sin(theta) * std::cos(psi)); + rotation_.push_back(std::sin(phi) * std::sin(psi) + + std::cos(phi) * std::sin(theta) * std::cos(psi)); + rotation_.push_back(std::cos(theta) * std::sin(psi)); + rotation_.push_back(std::cos(phi) * std::cos(psi) + + std::sin(phi) * std::sin(theta) * std::sin(psi)); + rotation_.push_back(-std::sin(phi) * std::cos(psi) + + std::cos(phi) * std::sin(theta) * std::sin(psi)); + rotation_.push_back(-std::sin(theta)); + rotation_.push_back(std::sin(phi) * std::cos(theta)); + rotation_.push_back(std::cos(phi) * std::cos(theta)); + + // When user specifies angles, write them at end of vector + rotation_.push_back(rot[0]); + rotation_.push_back(rot[1]); + rotation_.push_back(rot[2]); + } else { + std::copy(rot.begin(), rot.end(), std::back_inserter(rotation_)); + } +} + //============================================================================== // C-API functions //============================================================================== @@ -201,4 +249,48 @@ extern "C" int openmc_mesh_filter_set_translation( return 0; } +//! Return the rotation matrix of a mesh filter +extern "C" int openmc_mesh_filter_get_rotation( + int32_t index, double rot[], size_t* n) +{ + // Make sure this is a valid index to an allocated filter + if (int err = verify_filter(index)) + return err; + + // Check the filter type + const auto& filter = model::tally_filters[index]; + if (filter->type() != FilterType::MESH) { + set_errmsg("Tried to get a rotation from a non-mesh filter."); + return OPENMC_E_INVALID_TYPE; + } + // Get rotation from the mesh filter and set value + auto mesh_filter = dynamic_cast(filter.get()); + *n = mesh_filter->rotation().size(); + std::memcpy(rot, mesh_filter->rotation().data(), + *n * sizeof(mesh_filter->rotation()[0])); + return 0; +} + +//! Set the flattened rotation matrix of a mesh filter +extern "C" int openmc_mesh_filter_set_rotation( + int32_t index, const double rot[], size_t rot_len) +{ + // Make sure this is a valid index to an allocated filter + if (int err = verify_filter(index)) + return err; + + const auto& filter = model::tally_filters[index]; + // Check the filter type + if (filter->type() != FilterType::MESH) { + set_errmsg("Tried to set a rotation from a non-mesh filter."); + return OPENMC_E_INVALID_TYPE; + } + + // Get a pointer to the filter and downcast + auto mesh_filter = dynamic_cast(filter.get()); + std::vector vec_rot(rot, rot + rot_len); + mesh_filter->set_rotation(vec_rot); + return 0; +} + } // namespace openmc diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index 9333800bc..4838e4591 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -933,7 +933,8 @@ void WeightWindowsGenerator::create_tally() for (const auto& f : model::tally_filters) { if (f->type() == FilterType::MESH) { const auto* mesh_filter = dynamic_cast(f.get()); - if (mesh_filter->mesh() == mesh_idx && !mesh_filter->translated()) { + if (mesh_filter->mesh() == mesh_idx && !mesh_filter->translated() && + !mesh_filter->rotated()) { ww_tally->add_filter(f.get()); found_mesh_filter = true; break; diff --git a/tests/regression_tests/filter_rotations/__init__.py b/tests/regression_tests/filter_rotations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/filter_rotations/inputs_true.dat b/tests/regression_tests/filter_rotations/inputs_true.dat new file mode 100644 index 000000000..1ad2b9e86 --- /dev/null +++ b/tests/regression_tests/filter_rotations/inputs_true.dat @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 1000 + 5 + 0 + + + + 3 4 5 + -9 -9 -9 + 9 9 9 + + + 3 4 5 + -9 -9 -9 + 9 9 9 + + + 1 + + + 2 + + + 1 + total + + + 2 + total + + + diff --git a/tests/regression_tests/filter_rotations/results_true.dat b/tests/regression_tests/filter_rotations/results_true.dat new file mode 100644 index 000000000..5c8b83b76 --- /dev/null +++ b/tests/regression_tests/filter_rotations/results_true.dat @@ -0,0 +1,244 @@ +k-combined: +7.729082E-01 3.775399E-02 +tally 1: +5.296804E-02 +5.661701E-04 +8.356446E-02 +1.412139E-03 +5.041335E-02 +5.143568E-04 +1.299348E-01 +3.467618E-03 +3.929702E-01 +3.147038E-02 +1.379707E-01 +3.888484E-03 +1.405034E-01 +4.473799E-03 +3.785796E-01 +2.940585E-02 +1.422010E-01 +4.113723E-03 +5.647073E-02 +6.735251E-04 +7.911154E-02 +1.329137E-03 +5.160755E-02 +5.361448E-04 +6.669424E-02 +9.090832E-04 +1.008621E-01 +2.134534E-03 +6.808932E-02 +9.355993E-04 +1.873006E-01 +7.135961E-03 +6.221575E-01 +7.819842E-02 +1.856653E-01 +6.954762E-03 +2.014929E-01 +8.327845E-03 +5.853251E-01 +6.945708E-02 +1.709645E-01 +5.917124E-03 +7.214913E-02 +1.058962E-03 +1.027720E-01 +2.138475E-03 +6.099853E-02 +7.493941E-04 +6.892071E-02 +9.630680E-04 +1.035459E-01 +2.173883E-03 +6.973870E-02 +9.904237E-04 +2.125703E-01 +9.112659E-03 +9.012205E-01 +2.163546E-01 +2.066426E-01 +8.617414E-03 +2.258950E-01 +1.039607E-02 +9.476792E-01 +2.350708E-01 +2.225585E-01 +1.017898E-02 +7.111503E-02 +1.036847E-03 +1.117012E-01 +2.530040E-03 +6.870474E-02 +9.551035E-04 +5.738897E-02 +6.699030E-04 +9.522335E-02 +1.835769E-03 +6.570917E-02 +8.656870E-04 +1.945592E-01 +7.593336E-03 +5.514753E-01 +6.122981E-02 +2.144202E-01 +9.421739E-03 +1.971631E-01 +7.944046E-03 +6.088996E-01 +7.442954E-02 +1.965447E-01 +7.765628E-03 +7.005494E-02 +1.012891E-03 +1.010633E-01 +2.084095E-03 +6.145926E-02 +7.694351E-04 +4.999479E-02 +5.129164E-04 +7.238243E-02 +1.062921E-03 +4.902309E-02 +4.852193E-04 +1.324655E-01 +3.642431E-03 +3.305312E-01 +2.265726E-02 +1.332993E-01 +3.728385E-03 +1.547469E-01 +4.894837E-03 +3.625944E-01 +2.747313E-02 +1.435761E-01 +4.334405E-03 +5.789603E-02 +7.065383E-04 +7.589559E-02 +1.205386E-03 +5.210018E-02 +5.790843E-04 +tally 2: +4.597932E-02 +4.246849E-04 +8.494738E-02 +1.467238E-03 +5.155072E-02 +5.373129E-04 +1.479395E-01 +4.468164E-03 +3.931843E-01 +3.141943E-02 +1.264081E-01 +3.243689E-03 +1.229742E-01 +3.276501E-03 +3.768427E-01 +2.927971E-02 +1.574908E-01 +5.079304E-03 +5.621044E-02 +6.877866E-04 +8.490616E-02 +1.510673E-03 +4.709600E-02 +4.576632E-04 +5.382674E-02 +5.890600E-04 +1.116560E-01 +2.599817E-03 +6.648634E-02 +8.862677E-04 +1.996861E-01 +8.137198E-03 +6.218616E-01 +7.805532E-02 +1.672379E-01 +5.667797E-03 +1.841275E-01 +6.936896E-03 +5.887874E-01 +7.031665E-02 +1.872574E-01 +7.086584E-03 +7.574698E-02 +1.160992E-03 +1.100173E-01 +2.453972E-03 +5.427866E-02 +5.955556E-04 +5.644318E-02 +6.503634E-04 +1.058628E-01 +2.254338E-03 +8.012794E-02 +1.297032E-03 +2.292757E-01 +1.064490E-02 +9.001874E-01 +2.153097E-01 +1.921874E-01 +7.537343E-03 +2.058642E-01 +8.537136E-03 +9.442653E-01 +2.359241E-01 +2.419900E-01 +1.202201E-02 +7.563876E-02 +1.169874E-03 +1.165428E-01 +2.750405E-03 +5.646354E-02 +6.408683E-04 +4.963879E-02 +4.975915E-04 +1.005478E-01 +2.046400E-03 +7.041191E-02 +1.002590E-03 +1.959123E-01 +7.727902E-03 +5.625596E-01 +6.366248E-02 +1.987313E-01 +8.108832E-03 +1.922194E-01 +7.602752E-03 +6.062527E-01 +7.384124E-02 +2.039506E-01 +8.379010E-03 +7.019905E-02 +1.034239E-03 +1.081383E-01 +2.344040E-03 +5.125142E-02 +5.430397E-04 +4.230495E-02 +3.666730E-04 +7.722275E-02 +1.208856E-03 +4.998937E-02 +5.049355E-04 +1.461632E-01 +4.456332E-03 +3.315459E-01 +2.288314E-02 +1.238273E-01 +3.205844E-03 +1.355317E-01 +3.754923E-03 +3.646520E-01 +2.766067E-02 +1.543013E-01 +5.044487E-03 +6.082173E-02 +7.636985E-04 +8.490569E-02 +1.506309E-03 +4.046126E-02 +3.495832E-04 diff --git a/tests/regression_tests/filter_rotations/test.py b/tests/regression_tests/filter_rotations/test.py new file mode 100644 index 000000000..f5d63d6b4 --- /dev/null +++ b/tests/regression_tests/filter_rotations/test.py @@ -0,0 +1,72 @@ +import numpy as np + +import openmc +import pytest + +from tests.testing_harness import PyAPITestHarness + + +@pytest.fixture +def model(): + + model = openmc.model.Model() + + fuel = openmc.Material() + fuel.set_density('g/cm3', 10.0) + fuel.add_nuclide('U235', 1.0) + zr = openmc.Material() + zr.set_density('g/cm3', 1.0) + zr.add_nuclide('Zr90', 1.0) + model.materials.extend([fuel, zr]) + + box1 = openmc.model.RectangularPrism(10.0, 10.0) + box2 = openmc.model.RectangularPrism(20.0, 20.0, boundary_type='reflective') + top = openmc.ZPlane(z0=10.0, boundary_type='vacuum') + bottom = openmc.ZPlane(z0=-10.0, boundary_type='vacuum') + cell1 = openmc.Cell(fill=fuel, region=-box1 & +bottom & -top) + cell2 = openmc.Cell(fill=zr, region=+box1 & -box2 & +bottom & -top) + model.geometry = openmc.Geometry([cell1, cell2]) + + model.settings.batches = 5 + model.settings.inactive = 0 + model.settings.particles = 1000 + + rotation = np.array((0, 0, 10)) + + llc = np.array([-9, -9, -9]) + urc = np.array([9, 9, 9]) + + mesh_dims = (3, 4, 5) + + filters = [] + + # un-rotated meshes + reg_mesh = openmc.RegularMesh() + reg_mesh.dimension = mesh_dims + reg_mesh.lower_left = llc + reg_mesh.upper_right = urc + + filters.append(openmc.MeshFilter(reg_mesh)) + + # rotated meshes + rotated_reg_mesh = openmc.RegularMesh() + rotated_reg_mesh.dimension = mesh_dims + rotated_reg_mesh.lower_left = llc + rotated_reg_mesh.upper_right = urc + + filters.append(openmc.MeshFilter(rotated_reg_mesh)) + filters[-1].rotation = rotation + + # Create tallies + for f in filters: + tally = openmc.Tally() + tally.filters = [f] + tally.scores = ['total'] + model.tallies.append(tally) + + return model + + +def test_filter_mesh_rotations(model): + harness = PyAPITestHarness('statepoint.5.h5', model) + harness.main() diff --git a/tests/unit_tests/test_filter_mesh.py b/tests/unit_tests/test_filter_mesh.py index a8bd4996d..faa43af47 100644 --- a/tests/unit_tests/test_filter_mesh.py +++ b/tests/unit_tests/test_filter_mesh.py @@ -259,3 +259,29 @@ def test_get_reshaped_data(run_in_tmpdir): assert data1.shape == (2, 19*3*2, 1, 1) assert data2.shape == (2, 19, 3, 2, 1, 1) + +def test_mesh_filter_rotation_roundtrip(run_in_tmpdir): + """Test that MeshFilter rotation works as expected""" + + + mesh = openmc.RegularMesh() + mesh.lower_left = [-10, -10, -10] + mesh.upper_right = [10, 10, 10] + mesh.dimension = [2, 3, 4] + + # check that rotatoin is round-tripped correctly for a set of angles + mesh_filter = openmc.MeshFilter(mesh) + mesh_filter.rotation = [0, 0, 90] # Rotate around z-axis by 90 degrees + + elem = mesh_filter.to_xml_element() + mesh_filter_xml = openmc.MeshFilter.from_xml_element(elem, meshes={mesh.id: mesh}) + assert all(mesh_filter_xml.rotation == mesh_filter.rotation) + + # check that rotation matrix is round-tripped correctly for a rotation matrix + mesh_filter.rotation = np.array([[0.7071, 0, 0.7071], + [0, 1, 0], + [-0.7071, 0, 0.7071]]) + + 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) diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index eb4dc3dce..e5a4d198e 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -608,6 +608,13 @@ def test_regular_mesh(lib_init): assert isinstance(mesh, openmc.lib.RegularMesh) assert mesh_id == mesh.id + rotation = (180.0, 0.0, 0.0) + + mf = openmc.lib.MeshFilter(mesh) + assert mf.mesh == mesh + mf.rotation = rotation + assert np.allclose(mf.rotation, rotation) + translation = (1.0, 2.0, 3.0) mf = openmc.lib.MeshFilter(mesh) From 3f06a42abb89a9b09cb3f2e86680a31bae90cc08 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Tue, 23 Dec 2025 22:18:59 +0200 Subject: [PATCH 482/671] Refactor get_energy_index to prevent repetition (#3686) --- include/openmc/math_functions.h | 11 +++++++++++ src/distribution_angle.cpp | 18 +++--------------- src/math_functions.cpp | 15 +++++++++++++++ src/secondary_correlated.cpp | 16 +++------------- src/secondary_kalbach.cpp | 16 +++------------- src/secondary_thermal.cpp | 15 +-------------- 6 files changed, 36 insertions(+), 55 deletions(-) diff --git a/include/openmc/math_functions.h b/include/openmc/math_functions.h index c30ef7558..0d960c33d 100644 --- a/include/openmc/math_functions.h +++ b/include/openmc/math_functions.h @@ -9,6 +9,7 @@ #include #include "openmc/position.h" +#include "openmc/search.h" namespace openmc { @@ -200,5 +201,15 @@ std::complex faddeeva(std::complex z); //! \return Derivative of Faddeeva function evaluated at z std::complex w_derivative(std::complex z, int order); +//! Helper function to get index and interpolation function on an incident +//! energy grid +//! +//! \param energies energy grid +//! \param E incident energy +//! \param i grid index +//! \param f interpolation factor +void get_energy_index( + const vector& energies, double E, int& i, double& f); + } // namespace openmc #endif // OPENMC_MATH_FUNCTIONS_H diff --git a/src/distribution_angle.cpp b/src/distribution_angle.cpp index 5e92b794a..a571b2a5f 100644 --- a/src/distribution_angle.cpp +++ b/src/distribution_angle.cpp @@ -7,6 +7,7 @@ #include "openmc/endf.h" #include "openmc/hdf5_interface.h" +#include "openmc/math_functions.h" #include "openmc/random_lcg.h" #include "openmc/search.h" #include "openmc/vector.h" // for vector @@ -64,23 +65,10 @@ AngleDistribution::AngleDistribution(hid_t group) double AngleDistribution::sample(double E, uint64_t* seed) const { - // Determine number of incoming energies - auto n = energy_.size(); - - // Find energy bin and calculate interpolation factor -- if the energy is - // outside the range of the tabulated energies, choose the first or last bins + // Find energy bin and calculate interpolation factor int i; double r; - if (E < energy_[0]) { - i = 0; - r = 0.0; - } else if (E > energy_[n - 1]) { - i = n - 2; - r = 1.0; - } else { - i = lower_bound_index(energy_.begin(), energy_.end(), E); - r = (E - energy_[i]) / (energy_[i + 1] - energy_[i]); - } + get_energy_index(energy_, E, i, r); // Sample between the ith and (i+1)th bin if (r > prn(seed)) diff --git a/src/math_functions.cpp b/src/math_functions.cpp index 5469b56c8..9473f928b 100644 --- a/src/math_functions.cpp +++ b/src/math_functions.cpp @@ -919,4 +919,19 @@ std::complex w_derivative(std::complex z, int order) } } +// Helper function to get index and interpolation function on an incident energy +// grid +void get_energy_index( + const vector& energies, double E, int& i, double& f) +{ + // Get index and interpolation factor for linear-linear energy grid + i = 0; + f = 0.0; + if (E >= energies.front()) { + i = lower_bound_index(energies.begin(), energies.end(), E); + if (i + 1 < energies.size()) + f = (E - energies[i]) / (energies[i + 1] - energies[i]); + } +} + } // namespace openmc diff --git a/src/secondary_correlated.cpp b/src/secondary_correlated.cpp index 0e4891dd3..3f26fdb01 100644 --- a/src/secondary_correlated.cpp +++ b/src/secondary_correlated.cpp @@ -10,6 +10,7 @@ #include "openmc/endf.h" #include "openmc/hdf5_interface.h" +#include "openmc/math_functions.h" #include "openmc/random_lcg.h" #include "openmc/search.h" @@ -156,21 +157,10 @@ CorrelatedAngleEnergy::CorrelatedAngleEnergy(hid_t group) void CorrelatedAngleEnergy::sample( double E_in, double& E_out, double& mu, uint64_t* seed) const { - // Find energy bin and calculate interpolation factor -- if the energy is - // outside the range of the tabulated energies, choose the first or last bins - auto n_energy_in = energy_.size(); + // Find energy bin and calculate interpolation factor int i; double r; - if (E_in < energy_[0]) { - i = 0; - r = 0.0; - } else if (E_in > energy_[n_energy_in - 1]) { - i = n_energy_in - 2; - r = 1.0; - } else { - i = lower_bound_index(energy_.begin(), energy_.end(), E_in); - r = (E_in - energy_[i]) / (energy_[i + 1] - energy_[i]); - } + get_energy_index(energy_, E_in, i, r); // Sample between the ith and [i+1]th bin int l = r > prn(seed) ? i + 1 : i; diff --git a/src/secondary_kalbach.cpp b/src/secondary_kalbach.cpp index 4a7878343..6ac91e665 100644 --- a/src/secondary_kalbach.cpp +++ b/src/secondary_kalbach.cpp @@ -9,6 +9,7 @@ #include "xtensor/xview.hpp" #include "openmc/hdf5_interface.h" +#include "openmc/math_functions.h" #include "openmc/random_dist.h" #include "openmc/random_lcg.h" #include "openmc/search.h" @@ -117,21 +118,10 @@ KalbachMann::KalbachMann(hid_t group) void KalbachMann::sample( double E_in, double& E_out, double& mu, uint64_t* seed) const { - // Find energy bin and calculate interpolation factor -- if the energy is - // outside the range of the tabulated energies, choose the first or last bins - auto n_energy_in = energy_.size(); + // Find energy bin and calculate interpolation factor int i; double r; - if (E_in < energy_[0]) { - i = 0; - r = 0.0; - } else if (E_in > energy_[n_energy_in - 1]) { - i = n_energy_in - 2; - r = 1.0; - } else { - i = lower_bound_index(energy_.begin(), energy_.end(), E_in); - r = (E_in - energy_[i]) / (energy_[i + 1] - energy_[i]); - } + get_energy_index(energy_, E_in, i, r); // Sample between the ith and [i+1]th bin int l = r > prn(seed) ? i + 1 : i; diff --git a/src/secondary_thermal.cpp b/src/secondary_thermal.cpp index 8b9e8737c..030d398aa 100644 --- a/src/secondary_thermal.cpp +++ b/src/secondary_thermal.cpp @@ -1,6 +1,7 @@ #include "openmc/secondary_thermal.h" #include "openmc/hdf5_interface.h" +#include "openmc/math_functions.h" #include "openmc/random_lcg.h" #include "openmc/search.h" @@ -11,20 +12,6 @@ namespace openmc { -// Helper function to get index on incident energy grid -void get_energy_index( - const vector& energies, double E, int& i, double& f) -{ - // Get index and interpolation factor for elastic grid - i = 0; - f = 0.0; - if (E >= energies.front()) { - i = lower_bound_index(energies.begin(), energies.end(), E); - if (i + 1 < energies.size()) - f = (E - energies[i]) / (energies[i + 1] - energies[i]); - } -} - //============================================================================== // CoherentElasticAE implementation //============================================================================== From 92d7fdb199032f89ed15533da87050ff90a5a55a Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 30 Dec 2025 22:31:53 -0500 Subject: [PATCH 483/671] Use min/max position members in C++ BoundingBox class (#3699) --- include/openmc/bounding_box.h | 46 +++++++++++++++++++---------------- include/openmc/mesh.h | 4 +-- src/cell.cpp | 12 ++++----- src/dagmc.cpp | 2 +- src/geometry.cpp | 12 ++++----- src/mesh.cpp | 27 ++++++++++---------- src/surface.cpp | 28 ++++++++++----------- src/universe.cpp | 2 +- 8 files changed, 68 insertions(+), 65 deletions(-) diff --git a/include/openmc/bounding_box.h b/include/openmc/bounding_box.h index d02d92cb4..4fabe1b70 100644 --- a/include/openmc/bounding_box.h +++ b/include/openmc/bounding_box.h @@ -13,12 +13,19 @@ namespace openmc { //============================================================================== struct BoundingBox { - double xmin = -INFTY; - double xmax = INFTY; - double ymin = -INFTY; - double ymax = INFTY; - double zmin = -INFTY; - double zmax = INFTY; + Position min = {-INFTY, -INFTY, -INFTY}; + Position max = {INFTY, INFTY, INFTY}; + + // Constructors + BoundingBox() = default; + BoundingBox(Position min_, Position max_) : min {min_}, max {max_} {} + + // Static factory methods + static BoundingBox infinite() { return {}; } + static BoundingBox inverted() + { + return {{INFTY, INFTY, INFTY}, {-INFTY, -INFTY, -INFTY}}; + } inline BoundingBox operator&(const BoundingBox& other) { @@ -35,29 +42,26 @@ struct BoundingBox { // intersect operator inline BoundingBox& operator&=(const BoundingBox& other) { - xmin = std::max(xmin, other.xmin); - xmax = std::min(xmax, other.xmax); - ymin = std::max(ymin, other.ymin); - ymax = std::min(ymax, other.ymax); - zmin = std::max(zmin, other.zmin); - zmax = std::min(zmax, other.zmax); + min.x = std::max(min.x, other.min.x); + min.y = std::max(min.y, other.min.y); + min.z = std::max(min.z, other.min.z); + max.x = std::min(max.x, other.max.x); + max.y = std::min(max.y, other.max.y); + max.z = std::min(max.z, other.max.z); return *this; } // union operator inline BoundingBox& operator|=(const BoundingBox& other) { - xmin = std::min(xmin, other.xmin); - xmax = std::max(xmax, other.xmax); - ymin = std::min(ymin, other.ymin); - ymax = std::max(ymax, other.ymax); - zmin = std::min(zmin, other.zmin); - zmax = std::max(zmax, other.zmax); + min.x = std::min(min.x, other.min.x); + min.y = std::min(min.y, other.min.y); + min.z = std::min(min.z, other.min.z); + max.x = std::max(max.x, other.max.x); + max.y = std::max(max.y, other.max.y); + max.z = std::max(max.z, other.max.z); return *this; } - - inline Position min() const { return {xmin, ymin, zmin}; } - inline Position max() const { return {xmax, ymax, zmax}; } }; } // namespace openmc diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index 5c9272e93..7ceb623da 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -244,9 +244,7 @@ public: //! \return Bounding box of mesh BoundingBox bounding_box() const { - auto ll = this->lower_left(); - auto ur = this->upper_right(); - return {ll.x, ur.x, ll.y, ur.y, ll.z, ur.z}; + return {this->lower_left(), this->upper_right()}; } virtual Position lower_left() const = 0; diff --git a/src/cell.cpp b/src/cell.cpp index fd459cdcc..aceed31ca 100644 --- a/src/cell.cpp +++ b/src/cell.cpp @@ -1334,14 +1334,14 @@ extern "C" int openmc_cell_bounding_box( bbox = c->bounding_box(); // set lower left corner values - llc[0] = bbox.xmin; - llc[1] = bbox.ymin; - llc[2] = bbox.zmin; + llc[0] = bbox.min.x; + llc[1] = bbox.min.y; + llc[2] = bbox.min.z; // set upper right corner values - urc[0] = bbox.xmax; - urc[1] = bbox.ymax; - urc[2] = bbox.zmax; + urc[0] = bbox.max.x; + urc[1] = bbox.max.y; + urc[2] = bbox.max.z; return 0; } diff --git a/src/dagmc.cpp b/src/dagmc.cpp index b2ebe89c0..571182fa6 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -753,7 +753,7 @@ BoundingBox DAGCell::bounding_box() const double min[3], max[3]; rval = dagmc_ptr_->getobb(vol, min, max); MB_CHK_ERR_CONT(rval); - return {min[0], max[0], min[1], max[1], min[2], max[2]}; + return {{min[0], min[1], min[2]}, {max[0], max[1], max[2]}}; } //============================================================================== diff --git a/src/geometry.cpp b/src/geometry.cpp index 5ff15097b..ddb61385f 100644 --- a/src/geometry.cpp +++ b/src/geometry.cpp @@ -480,14 +480,14 @@ extern "C" int openmc_global_bounding_box(double* llc, double* urc) auto bbox = model::universes.at(model::root_universe)->bounding_box(); // set lower left corner values - llc[0] = bbox.xmin; - llc[1] = bbox.ymin; - llc[2] = bbox.zmin; + llc[0] = bbox.min.x; + llc[1] = bbox.min.y; + llc[2] = bbox.min.z; // set upper right corner values - urc[0] = bbox.xmax; - urc[1] = bbox.ymax; - urc[2] = bbox.zmax; + urc[0] = bbox.max.x; + urc[1] = bbox.max.y; + urc[2] = bbox.max.z; return 0; } diff --git a/src/mesh.cpp b/src/mesh.cpp index c3a2d4581..9a7a8e751 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -359,9 +359,10 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, std::array n_rays = {nx, ny, nz}; // Determine effective width of rays - Position width((nx > 0) ? (bbox.xmax - bbox.xmin) / nx : 0.0, - (ny > 0) ? (bbox.ymax - bbox.ymin) / ny : 0.0, - (nz > 0) ? (bbox.zmax - bbox.zmin) / nz : 0.0); + Position width = bbox.max - bbox.min; + width.x = (nx > 0) ? width.x / nx : 0.0; + width.y = (ny > 0) ? width.y / ny : 0.0; + width.z = (nz > 0) ? width.z / nz : 0.0; // Set flag for mesh being contained within model bool out_of_model = false; @@ -380,15 +381,15 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, for (int axis = 0; axis < 3; ++axis) { // Set starting position and direction site.r = {0.0, 0.0, 0.0}; - site.r[axis] = bbox.min()[axis]; + site.r[axis] = bbox.min[axis]; site.u = {0.0, 0.0, 0.0}; site.u[axis] = 1.0; // Determine width of rays and number of rays in other directions int ax1 = (axis + 1) % 3; int ax2 = (axis + 2) % 3; - double min1 = bbox.min()[ax1]; - double min2 = bbox.min()[ax2]; + double min1 = bbox.min[ax1]; + double min2 = bbox.min[ax2]; double d1 = width[ax1]; double d2 = width[ax2]; int n1 = n_rays[ax1]; @@ -433,7 +434,7 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, while (true) { // Ray trace from r_start to r_end Position r0 = p.r(); - double max_distance = bbox.max()[axis] - r0[axis]; + double max_distance = bbox.max[axis] - r0[axis]; // Find the distance to the nearest boundary BoundaryInfo boundary = distance_to_boundary(p); @@ -2415,14 +2416,14 @@ extern "C" int openmc_mesh_bounding_box(int32_t index, double* ll, double* ur) BoundingBox bbox = model::meshes[index]->bounding_box(); // set lower left corner values - ll[0] = bbox.xmin; - ll[1] = bbox.ymin; - ll[2] = bbox.zmin; + ll[0] = bbox.min.x; + ll[1] = bbox.min.y; + ll[2] = bbox.min.z; // set upper right corner values - ur[0] = bbox.xmax; - ur[1] = bbox.ymax; - ur[2] = bbox.zmax; + ur[0] = bbox.max.x; + ur[1] = bbox.max.y; + ur[2] = bbox.max.z; return 0; } diff --git a/src/surface.cpp b/src/surface.cpp index 09fc0f24f..bc9c7f4a9 100644 --- a/src/surface.cpp +++ b/src/surface.cpp @@ -251,9 +251,9 @@ void SurfaceXPlane::to_hdf5_inner(hid_t group_id) const BoundingBox SurfaceXPlane::bounding_box(bool pos_side) const { if (pos_side) { - return {x0_, INFTY, -INFTY, INFTY, -INFTY, INFTY}; + return {{x0_, -INFTY, -INFTY}, {INFTY, INFTY, INFTY}}; } else { - return {-INFTY, x0_, -INFTY, INFTY, -INFTY, INFTY}; + return {{-INFTY, -INFTY, -INFTY}, {x0_, INFTY, INFTY}}; } } @@ -291,9 +291,9 @@ void SurfaceYPlane::to_hdf5_inner(hid_t group_id) const BoundingBox SurfaceYPlane::bounding_box(bool pos_side) const { if (pos_side) { - return {-INFTY, INFTY, y0_, INFTY, -INFTY, INFTY}; + return {{-INFTY, y0_, -INFTY}, {INFTY, INFTY, INFTY}}; } else { - return {-INFTY, INFTY, -INFTY, y0_, -INFTY, INFTY}; + return {{-INFTY, -INFTY, -INFTY}, {INFTY, y0_, INFTY}}; } } @@ -331,9 +331,9 @@ void SurfaceZPlane::to_hdf5_inner(hid_t group_id) const BoundingBox SurfaceZPlane::bounding_box(bool pos_side) const { if (pos_side) { - return {-INFTY, INFTY, -INFTY, INFTY, z0_, INFTY}; + return {{-INFTY, -INFTY, z0_}, {INFTY, INFTY, INFTY}}; } else { - return {-INFTY, INFTY, -INFTY, INFTY, -INFTY, z0_}; + return {{-INFTY, -INFTY, -INFTY}, {INFTY, INFTY, z0_}}; } } @@ -492,8 +492,8 @@ void SurfaceXCylinder::to_hdf5_inner(hid_t group_id) const BoundingBox SurfaceXCylinder::bounding_box(bool pos_side) const { if (!pos_side) { - return {-INFTY, INFTY, y0_ - radius_, y0_ + radius_, z0_ - radius_, - z0_ + radius_}; + return {{-INFTY, y0_ - radius_, z0_ - radius_}, + {INFTY, y0_ + radius_, z0_ + radius_}}; } else { return {}; } @@ -535,8 +535,8 @@ void SurfaceYCylinder::to_hdf5_inner(hid_t group_id) const BoundingBox SurfaceYCylinder::bounding_box(bool pos_side) const { if (!pos_side) { - return {x0_ - radius_, x0_ + radius_, -INFTY, INFTY, z0_ - radius_, - z0_ + radius_}; + return {{x0_ - radius_, -INFTY, z0_ - radius_}, + {x0_ + radius_, INFTY, z0_ + radius_}}; } else { return {}; } @@ -579,8 +579,8 @@ void SurfaceZCylinder::to_hdf5_inner(hid_t group_id) const BoundingBox SurfaceZCylinder::bounding_box(bool pos_side) const { if (!pos_side) { - return {x0_ - radius_, x0_ + radius_, y0_ - radius_, y0_ + radius_, -INFTY, - INFTY}; + return {{x0_ - radius_, y0_ - radius_, -INFTY}, + {x0_ + radius_, y0_ + radius_, INFTY}}; } else { return {}; } @@ -657,8 +657,8 @@ void SurfaceSphere::to_hdf5_inner(hid_t group_id) const BoundingBox SurfaceSphere::bounding_box(bool pos_side) const { if (!pos_side) { - return {x0_ - radius_, x0_ + radius_, y0_ - radius_, y0_ + radius_, - z0_ - radius_, z0_ + radius_}; + return {{x0_ - radius_, y0_ - radius_, z0_ - radius_}, + {x0_ + radius_, y0_ + radius_, z0_ + radius_}}; } else { return {}; } diff --git a/src/universe.cpp b/src/universe.cpp index 78ddd54d4..e1db80b99 100644 --- a/src/universe.cpp +++ b/src/universe.cpp @@ -61,7 +61,7 @@ bool Universe::find_cell(GeometryState& p) const BoundingBox Universe::bounding_box() const { - BoundingBox bbox = {INFTY, -INFTY, INFTY, -INFTY, INFTY, -INFTY}; + BoundingBox bbox = BoundingBox::inverted(); if (cells_.size() == 0) { return {}; } else { From f08326a9ac416d54973ce0d210078729cfc3b008 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Thu, 1 Jan 2026 13:30:09 +0200 Subject: [PATCH 484/671] Cache cross sections according to the hash of download-xs.sh script (#3701) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d75a64d66..8a9c7322e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -154,7 +154,7 @@ jobs: path: | ~/nndc_hdf5 ~/endf-b-vii.1 - key: ${{ runner.os }}-build-xs-cache + key: ${{ runner.os }}-build-xs-cache-${{ hashFiles(format('{0}/tools/ci/download-xs.sh', github.workspace)) }} - name: before shell: bash From 932f36f411ef11cbaa69635a2a35c0060edc58c2 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 2 Jan 2026 05:31:46 -0500 Subject: [PATCH 485/671] Add recognized thermal scattering names for JEFF 4.0 and JENDL 5 (#3693) --- openmc/data/neutron.py | 6 +- openmc/data/njoy.py | 98 +++++++++++++++++++++- openmc/data/thermal.py | 115 +++++++++++++++++++++++--- tests/unit_tests/test_data_thermal.py | 23 ++++++ 4 files changed, 230 insertions(+), 12 deletions(-) diff --git a/openmc/data/neutron.py b/openmc/data/neutron.py index 628801e5e..71927cbed 100644 --- a/openmc/data/neutron.py +++ b/openmc/data/neutron.py @@ -765,6 +765,11 @@ class IncidentNeutron(EqualityMixin): for table in lib.tables[1:]: data.add_temperature_from_ace(table) + # Use name based on ENDF evaluation. The name assigned by from_ace + # may be wrong for higher metastable states (e.g., Hf178_m2) + ev = evaluation if evaluation is not None else Evaluation(filename) + data.name = ev.gnds_name + # Add 0K elastic scattering cross section if '0K' not in data.energy: pendf = Evaluation(kwargs['pendf']) @@ -775,7 +780,6 @@ class IncidentNeutron(EqualityMixin): data[2].xs['0K'] = xs # Add fission energy release data - ev = evaluation if evaluation is not None else Evaluation(filename) if (1, 458) in ev.section: data.fission_energy = f = FissionEnergyRelease.from_endf(ev, data) else: diff --git a/openmc/data/njoy.py b/openmc/data/njoy.py index 4c538bd6e..ddc68cc37 100644 --- a/openmc/data/njoy.py +++ b/openmc/data/njoy.py @@ -15,19 +15,41 @@ import openmc.data # identifiers. ThermalTuple = namedtuple('ThermalTuple', ['name', 'zaids', 'nmix']) _THERMAL_DATA = { + 'c_Ag': ThermalTuple('ag', [47107, 47109], 1), 'c_Al27': ThermalTuple('al27', [13027], 1), 'c_Al_in_Al2O3': ThermalTuple('asap00', [13027], 1), + 'c_Al_in_Y3Al5O12': ThermalTuple('alyag', [13027], 1), + 'c_Au': ThermalTuple('au', [79197], 1), 'c_Be': ThermalTuple('be', [4009], 1), 'c_Be_distinct': ThermalTuple('besd', [4009], 1), 'c_Be_in_BeO': ThermalTuple('bebeo', [4009], 1), 'c_Be_in_Be2C': ThermalTuple('bebe2c', [4009], 1), 'c_Be_in_BeF2': ThermalTuple('bebef2', [4009], 1), 'c_Be_in_FLiBe': ThermalTuple('beflib', [4009], 1), + 'c_BeO': ThermalTuple('beo', [4009, 8016, 8017, 8018], 2), + 'c_Bi': ThermalTuple('bi', [83209], 1), + 'c_Bi_in_Ge3Bi4O12': ThermalTuple('bigbo', [83209], 1), 'c_C6H6': ThermalTuple('benz', [1001, 6000, 6012], 2), 'c_C_in_Be2C': ThermalTuple('cbe2c', [6000, 6012, 6013], 1), 'c_C_in_C5O2H8': ThermalTuple('clucit', [6000, 6012, 6013], 1), 'c_C_in_C8H8': ThermalTuple('cc8h8', [6000, 6012, 6013], 1), + 'c_C_in_C19H16_liquid': ThermalTuple('c19liq', [6000, 6012, 6013], 1), + 'c_C_in_C19H16_solid': ThermalTuple('c19sol', [6000, 6012, 6013], 1), + 'c_C_in_C2H6O_liquid': ThermalTuple('ethliq', [6000, 6012, 6013], 1), + 'c_C_in_C2H6O_solid': ThermalTuple('ethsol', [6000, 6012, 6013], 1), + 'c_C_in_C6H6_liquid': ThermalTuple('benzlq', [6000, 6012, 6013], 1), + 'c_C_in_C6H6_solid': ThermalTuple('benzsl', [6000, 6012, 6013], 1), + 'c_C_in_C7H8_liquid': ThermalTuple('tolliq', [6000, 6012, 6013], 1), + 'c_C_in_C7H8_solid': ThermalTuple('tolsol', [6000, 6012, 6013], 1), + 'c_C_in_C8H10_liquid': ThermalTuple('xylliq', [6000, 6012, 6013], 1), + 'c_C_in_C8H10_solid': ThermalTuple('xylsol', [6000, 6012, 6013], 1), + 'c_C_in_C9H12_liquid': ThermalTuple('mesliq', [6000, 6012, 6013], 1), + 'c_C_in_C9H12_solid': ThermalTuple('messol', [6000, 6012, 6013], 1), 'c_C_in_CF2': ThermalTuple('ccf2', [6000, 6012, 6013], 1), + 'c_C_in_CH2': ThermalTuple('cch2', [6000, 6012, 6013], 1), + 'c_C_in_CH4_liquid': ThermalTuple('cch4lq', [6000, 6012, 6013], 1), + 'c_C_in_CH4_solid': ThermalTuple('cch4sl', [6000, 6012, 6013], 1), + 'c_C_in_Diamond': ThermalTuple('cdiam', [6000, 6012, 6013], 1), 'c_C_in_SiC': ThermalTuple('csic', [6000, 6012, 6013], 1), 'c_C_in_UC_100p': ThermalTuple('cuc100', [6000, 6012, 6013], 1), 'c_C_in_UC_10p': ThermalTuple('cuc10', [6000, 6012, 6013], 1), @@ -36,16 +58,29 @@ _THERMAL_DATA = { 'c_C_in_UC_HALEU': ThermalTuple('cuchal', [6000, 6012, 6013], 1), 'c_C_in_UC_HEU': ThermalTuple('cucheu', [6000, 6012, 6013], 1), 'c_C_in_ZrC': ThermalTuple('czrc', [6000, 6012, 6013], 1), + 'c_Ca': ThermalTuple('ca', [20040, 20042, 20043, 20044, 20046, 20048], 1), 'c_Ca_in_CaH2': ThermalTuple('cacah2', [20040, 20042, 20043, 20044, 20046, 20048], 1), + 'c_Ca_in_CaO2H2': ThermalTuple('cacaoh', [20040, 20042, 20043, 20044, 20046, 20048], 1), + 'c_Cr': ThermalTuple('cr', [24050, 24052, 24053, 24054], 1), + 'c_Cu': ThermalTuple('cu', [29063, 29065], 1), 'c_D_in_7LiD': ThermalTuple('dlid', [1002], 1), 'c_D_in_D2O': ThermalTuple('dd2o', [1002], 1), 'c_D_in_D2O_solid': ThermalTuple('dice', [1002], 1), + 'c_D_in_MgD2': ThermalTuple('dmgd2', [1002], 1), 'c_F_in_Be2': ThermalTuple('fbef2', [9019], 1), 'c_F_in_CF2': ThermalTuple('fcf2', [9019], 1), 'c_F_in_FLiBe': ThermalTuple('fflibe', [9019], 1), 'c_F_in_HF': ThermalTuple('f_hf', [9019], 1), + 'c_F_in_LiF': ThermalTuple('flif', [9019], 1), 'c_F_in_MgF2': ThermalTuple('fmgf2', [9019], 1), 'c_Fe56': ThermalTuple('fe56', [26056], 1), + 'c_Fe_in_Fe_alpha': ThermalTuple('fealph', [26054, 26056, 26057, 26058], 1), + 'c_Fe_in_Fe_gamma': ThermalTuple('fegamm', [26054, 26056, 26057, 26058], 1), + 'c_Ga_in_GaN': ThermalTuple('gagan', [31069, 31071], 1), + 'c_Ga_in_GaSe': ThermalTuple('gagase', [31069, 31071], 1), + 'c_Ge': ThermalTuple('ge', [32070, 32072, 32073, 32074, 32076], 1), + 'c_Ge_in_Ge3Bi4O12': ThermalTuple('gegbo', [32070, 32072, 32073, 32074, 32076], 1), + 'c_Ge_in_GeTe': ThermalTuple('gegete', [32070, 32072, 32073, 32074, 32076], 1), 'c_Graphite': ThermalTuple('graph', [6000, 6012, 6013], 1), 'c_Graphite_10p': ThermalTuple('grph10', [6000, 6012, 6013], 1), 'c_Graphite_20p': ThermalTuple('grph20', [6000, 6012, 6013], 1), @@ -54,7 +89,20 @@ _THERMAL_DATA = { 'c_H_in_7LiH': ThermalTuple('hlih', [1001], 1), 'c_H_in_C5O2H8': ThermalTuple('lucite', [1001], 1), 'c_H_in_C8H8': ThermalTuple('hc8h8', [1001], 1), + 'c_H_in_C19H16_liquid': ThermalTuple('h19liq', [1001], 1), + 'c_H_in_C19H16_solid': ThermalTuple('h19sol', [1001], 1), + 'c_H_in_C2H6O_liquid': ThermalTuple('hetliq', [1001], 1), + 'c_H_in_C2H6O_solid': ThermalTuple('hetsol', [1001], 1), + 'c_H_in_C6H6_liquid': ThermalTuple('hbzliq', [1001], 1), + 'c_H_in_C6H6_solid': ThermalTuple('hbzsol', [1001], 1), + 'c_H_in_C7H8_liquid': ThermalTuple('htlliq', [1001], 1), + 'c_H_in_C7H8_solid': ThermalTuple('htlsol', [1001], 1), + 'c_H_in_C8H10_liquid': ThermalTuple('hxyliq', [1001], 1), + 'c_H_in_C8H10_solid': ThermalTuple('hxysol', [1001], 1), + 'c_H_in_C9H12_liquid': ThermalTuple('hmsliq', [1001], 1), + 'c_H_in_C9H12_solid': ThermalTuple('hmssol', [1001], 1), 'c_H_in_CaH2': ThermalTuple('hcah2', [1001], 1), + 'c_H_in_CaO2H2': ThermalTuple('hcaoh', [1001], 1), 'c_H1_in_CaH2': ThermalTuple('h1cah2', [1001], 1), 'c_H2_in_CaH2': ThermalTuple('h2cah2', [1001], 1), 'c_H_in_CH2': ThermalTuple('hch2', [1001], 1), @@ -64,32 +112,64 @@ _THERMAL_DATA = { 'c_H_in_H2O': ThermalTuple('hh2o', [1001], 1), 'c_H_in_H2O_solid': ThermalTuple('hice', [1001], 1), 'c_H_in_HF': ThermalTuple('hhf', [1001], 1), + 'c_H_in_KOH': ThermalTuple('hkoh', [1001], 1), + 'c_H_in_LiH': ThermalTuple('hlih2', [1001], 1), 'c_H_in_Mesitylene': ThermalTuple('mesi00', [1001], 1), 'c_H_in_ParaffinicOil': ThermalTuple('hparaf', [1001], 1), 'c_H_in_Toluene': ThermalTuple('tol00', [1001], 1), + 'c_H_in_MgH2': ThermalTuple('hmgh2', [1001], 1), + 'c_H_in_MgOH2': ThermalTuple('hmgoh', [1001], 1), + 'c_H_in_NaMgH3': ThermalTuple('hnamg', [1001], 1), + 'c_H_in_NaOH': ThermalTuple('hnaoh', [1001], 1), + 'c_H_in_SrH2': ThermalTuple('hsrh2', [1001], 1), 'c_H_in_UH3': ThermalTuple('huh3', [1001], 1), 'c_H_in_YH2': ThermalTuple('hyh2', [1001], 1), 'c_H_in_ZrH': ThermalTuple('hzrh', [1001], 1), 'c_H_in_ZrH2': ThermalTuple('hzrh2', [1001], 1), 'c_H_in_ZrHx': ThermalTuple('hzrhx', [1001], 1), + 'c_I_in_NaI': ThermalTuple('inai', [53127], 1), + 'c_K': ThermalTuple('k', [19039, 19040, 19041], 1), + 'c_K_in_KOH': ThermalTuple('kkoh', [19039, 19040, 19041], 1), 'c_Li_in_FLiBe': ThermalTuple('liflib', [3006, 3007], 1), 'c_Li_in_7LiD': ThermalTuple('lilid', [3007], 1), 'c_Li_in_7LiH': ThermalTuple('lilih', [3007], 1), + 'c_Li_in_LiF': ThermalTuple('lilif', [3006, 3007], 1), + 'c_Li_in_LiH': ThermalTuple('lilih2', [3006, 3007], 1), 'c_Mg24': ThermalTuple('mg24', [12024], 1), 'c_Mg_in_MgF2': ThermalTuple('mgmgf2', [12024, 12025, 12026], 1), 'c_Mg_in_MgO': ThermalTuple('mgmgo', [12024, 12025, 12026], 1), + 'c_Mg_in_MgD2': ThermalTuple('mgmgd2', [12024, 12025, 12026], 1), + 'c_Mg_in_MgH2': ThermalTuple('mgmgh2', [12024, 12025, 12026], 1), + 'c_Mg_in_MgOH2': ThermalTuple('mgoh2', [12024, 12025, 12026], 1), + 'c_Mg_in_NaMgH3': ThermalTuple('mgnamg', [12024, 12025, 12026], 1), + 'c_Mo': ThermalTuple('mo', [42092, 42094, 42095, 42096, 42097, 42098, 42100], 1), + 'c_N_in_GaN': ThermalTuple('ngan', [7014, 7015], 1), 'c_N_in_UN_100p': ThermalTuple('nun100', [7014, 7015], 1), 'c_N_in_UN_10p': ThermalTuple('nun10', [7014, 7015], 1), 'c_N_in_UN_5p': ThermalTuple('nun5', [7014, 7015], 1), 'c_N_in_UN': ThermalTuple('n-un', [7014, 7015], 1), 'c_N_in_UN_HALEU': ThermalTuple('nunhal', [7014, 7015], 1), 'c_N_in_UN_HEU': ThermalTuple('nunheu', [7014, 7015], 1), + 'c_Na': ThermalTuple('na', [11023], 1), + 'c_Na_in_NaI': ThermalTuple('nanai', [11023], 1), + 'c_Na_in_NaMgH3': ThermalTuple('nanamg', [11023], 1), + 'c_Na_in_NaOH': ThermalTuple('nanaoh', [11023], 1), + 'c_Nb': ThermalTuple('nb', [41093], 1), + 'c_Ni': ThermalTuple('ni', [28058, 28060, 28061, 28062, 28064], 1), 'c_O_in_Al2O3': ThermalTuple('osap00', [8016, 8017, 8018], 1), 'c_O_in_BeO': ThermalTuple('obeo', [8016, 8017, 8018], 1), 'c_O_in_C5O2H8': ThermalTuple('olucit', [8016, 8017, 8018], 1), + 'c_O_in_C2H6O_liquid': ThermalTuple('oetliq', [8016, 8017, 8018], 1), + 'c_O_in_C2H6O_solid': ThermalTuple('oetsol', [8016, 8017, 8018], 1), + 'c_O_in_CaO2H2': ThermalTuple('ocaoh', [8016, 8017, 8018], 1), 'c_O_in_D2O': ThermalTuple('od2o', [8016, 8017, 8018], 1), 'c_O_in_H2O_solid': ThermalTuple('oice', [8016, 8017, 8018], 1), 'c_O_in_MgO': ThermalTuple('omgo', [8016, 8017, 8018], 1), + 'c_O_in_Ge3Bi4O12': ThermalTuple('ogbo', [8016, 8017, 8018], 1), + 'c_O_in_H2O': ThermalTuple('oh2o', [8016, 8017, 8018], 1), + 'c_O_in_KOH': ThermalTuple('okoh', [8016, 8017, 8018], 1), + 'c_O_in_MgOH2': ThermalTuple('omgoh', [8016, 8017, 8018], 1), + 'c_O_in_NaOH': ThermalTuple('onaoh', [8016, 8017, 8018], 1), 'c_O_in_PuO2': ThermalTuple('opuo2', [8016, 8017, 8018], 1), 'c_O_in_SiO2_alpha': ThermalTuple('osio2a', [8016, 8017, 8018], 1), 'c_O_in_UO2_100p': ThermalTuple('ouo200', [8016, 8017, 8018], 1), @@ -98,16 +178,26 @@ _THERMAL_DATA = { 'c_O_in_UO2': ThermalTuple('ouo2', [8016, 8017, 8018], 1), 'c_O_in_UO2_HALEU': ThermalTuple('ouo2hl', [8016, 8017, 8018], 1), 'c_O_in_UO2_HEU': ThermalTuple('ouo2he', [8016, 8017, 8018], 1), + 'c_O_in_Y3Al5O12': ThermalTuple('oyag', [8016, 8017, 8018], 1), 'c_ortho_D': ThermalTuple('orthod', [1002], 1), 'c_ortho_H': ThermalTuple('orthoh', [1001], 1), 'c_para_D': ThermalTuple('parad', [1002], 1), 'c_para_H': ThermalTuple('parah', [1001], 1), + 'c_Pb': ThermalTuple('pb', [82204, 82206, 82207, 82208], 1), + 'c_Pd': ThermalTuple('pd', [46102, 46104, 46105, 46106, 46108, 46110], 1), + 'c_Pt': ThermalTuple('pt', [78190, 78192, 78194, 78195, 78196, 78198], 1), 'c_Pu_in_PuO2': ThermalTuple('puo2', [94239, 94240, 94241, 94242, 94243], 1), 'c_Si28': ThermalTuple('si00', [14028], 1), 'c_Si_in_SiC': ThermalTuple('sisic', [14028, 14029, 14030], 1), 'c_Si_in_SiO2_alpha': ThermalTuple('si_o2a', [14028, 14029, 14030], 1), 'c_SiO2_alpha': ThermalTuple('sio2-a', [8016, 8017, 8018, 14028, 14029, 14030], 3), 'c_SiO2_beta': ThermalTuple('sio2-b', [8016, 8017, 8018, 14028, 14029, 14030], 3), + 'c_S_in_ZnS': ThermalTuple('szns', [16032, 16033, 16034, 16036], 1), + 'c_Se_in_GaSe': ThermalTuple('segase', [34074, 34076, 34077, 34078, 34080, 34082], 1), + 'c_Sn': ThermalTuple('sn', [50112, 50114, 50115, 50116, 50117, 50118, 50119, 50120, 50122, 50124], 1), + 'c_Sr_in_SrH2': ThermalTuple('srsrh2', [38084, 38086, 38087, 38088], 1), + 'c_Te_in_GeTe': ThermalTuple('tegete', [52120, 52122, 52123, 52124, 52125, 52126, 52128, 52130], 1), + 'c_Ti': ThermalTuple('ti', [22046, 22047, 22048, 22049, 22050], 1), 'c_U_metal_100p': ThermalTuple('u-100p', [92233, 92234, 92235, 92236, 92238], 1), 'c_U_metal_10p': ThermalTuple('u-10p', [92233, 92234, 92235, 92236, 92238], 1), 'c_U_metal_5p': ThermalTuple('u-5p', [92233, 92234, 92235, 92236, 92238], 1), @@ -132,7 +222,13 @@ _THERMAL_DATA = { 'c_U_in_UO2': ThermalTuple('uuo2', [92233, 92234, 92235, 92236, 92238], 1), 'c_U_in_UO2_HALEU': ThermalTuple('uo2hal', [92233, 92234, 92235, 92236, 92238], 1), 'c_U_in_UO2_HEU': ThermalTuple('uo2heu', [92233, 92234, 92235, 92236, 92238], 1), + 'c_V': ThermalTuple('v', [23050, 23051], 1), + 'c_W': ThermalTuple('w', [74180, 74182, 74183, 74184, 74186], 1), + 'c_Y_in_Y3Al5O12': ThermalTuple('yyag', [39089], 1), 'c_Y_in_YH2': ThermalTuple('yyh2', [39089], 1), + 'c_Zn': ThermalTuple('zn', [30064, 30066, 30067, 30068, 30070], 1), + 'c_Zn_in_ZnS': ThermalTuple('znzns', [30064, 30066, 30067, 30068, 30070], 1), + 'c_Zr': ThermalTuple('zr', [40090, 40091, 40092, 40094, 40096], 1), 'c_Zr_in_ZrC': ThermalTuple('zrzrc', [40000, 40090, 40091, 40092, 40094, 40096], 1), 'c_Zr_in_ZrH': ThermalTuple('zrzrh', [40000, 40090, 40091, 40092, 40094, 40096], 1), 'c_Zr_in_ZrH2': ThermalTuple('zrzrh2', [40000, 40090, 40091, 40092, 40094, 40096], 1), @@ -565,7 +661,7 @@ def make_ace_thermal(filename, filename_thermal, temperatures=None, else: with warnings.catch_warnings(record=True) as w: proper_name = openmc.data.get_thermal_name(zsymam_thermal) - if w: + if w or proper_name not in _THERMAL_DATA: raise RuntimeError( f"Thermal scattering material {zsymam_thermal} not " "recognized. Please contact OpenMC developers at " diff --git a/openmc/data/thermal.py b/openmc/data/thermal.py index 4ecc7c040..54e3a7330 100644 --- a/openmc/data/thermal.py +++ b/openmc/data/thermal.py @@ -27,20 +27,41 @@ from .thermal_angle_energy import (CoherentElasticAE, IncoherentElasticAE, _THERMAL_NAMES = { + 'c_Ag': ('ag',), 'c_Al27': ('al', 'al27', 'al-27', '13-al- 27'), 'c_Al_in_Al2O3': ('asap00', 'asap', 'al(al2o3)'), - 'c_Be': ('be', 'be-metal', 'be-met', 'be00', 'be-metal', 'be metal', '4-be'), + 'c_Al_in_Y3Al5O12': ('al(y3al5o1', 'alyag'), + 'c_Au': ('au',), + 'c_Be': ('be', 'be-metal', 'be-met', 'be00', 'be-metal', 'be metal', '4-be', '4-be-'), 'c_BeO': ('beo',), 'c_Be_distinct': ('besd', 'be+sd'), 'c_Be_in_BeO': ('bebeo', 'be-beo', 'be-o', 'be/o', 'bbeo00', 'be(beo)', 'be_beo'), 'c_Be_in_Be2C': ('bebe2c', 'be(be2c)'), 'c_Be_in_BeF2': ('bebef2', 'be in bef2'), 'c_Be_in_FLiBe': ('beflib', 'be(flibe)'), + 'c_Bi': ('83-bi-', 'bi'), + 'c_Bi_in_Ge3Bi4O12': ('bi(ge3bi4o', 'bigbo'), 'c_C6H6': ('benz', 'c6h6', 'benzine'), 'c_C_in_Be2C': ('cbe2c', 'c(be2c)'), + 'c_C_in_C19H16_liquid': ('c(c19h16)l', 'c19liq'), + 'c_C_in_C19H16_solid': ('c(c19h16)s', 'c19sol'), + 'c_C_in_C2H6O_liquid': ('c(c2h6o)l', 'ethliq'), + 'c_C_in_C2H6O_solid': ('c(c2h6o)s', 'ethsol'), 'c_C_in_C5O2H8': ('clucit', 'c(lucite)'), + 'c_C_in_C6H6_liquid': ('c(c6h6)l', 'benzlq'), + 'c_C_in_C6H6_solid': ('c(c6h6)s', 'benzsl'), + 'c_C_in_C7H8_liquid': ('c(c7h8)l', 'tolliq'), + 'c_C_in_C7H8_solid': ('c(c7h8)s', 'tolsol'), 'c_C_in_C8H8': ('cc8h8', 'c(polystyr'), + 'c_C_in_C8H10_liquid': ('c(m-c8h10)l', 'xylliq'), + 'c_C_in_C8H10_solid': ('c(m-c8h10)s', 'xylsol'), + 'c_C_in_C9H12_liquid': ('c(c9h12)l', 'mesliq'), + 'c_C_in_C9H12_solid': ('c(c9h12)s', 'messol'), 'c_C_in_CF2': ('ccf2', 'c(teflon)'), + 'c_C_in_CH2': ('c(c2h4)n r', 'cch2'), + 'c_C_in_CH4_liquid': ('c(ch4)l', 'cch4lq'), + 'c_C_in_CH4_solid': ('c(ch4)s', 'cch4sl'), + 'c_C_in_Diamond': ('c(c-diamon', 'cdiam'), 'c_C_in_SiC': ('csic', 'c-sic', 'c(3c-sic)', 'c_sic'), 'c_C_in_UC_100p': ('cuc100', 'cinuc_100p'), 'c_C_in_UC_10p': ('cuc10', 'cinuc_10p'), @@ -49,16 +70,29 @@ _THERMAL_NAMES = { 'c_C_in_UC_HALEU': ('cuchal', 'cinuc_haleu'), 'c_C_in_UC_HEU': ('cucheu', 'cinuc_heu'), 'c_C_in_ZrC': ('czrc', 'c(zrc)'), + 'c_Ca': ('ca',), 'c_Ca_in_CaH2': ('cah', 'cah00', 'cacah2', 'ca(cah2)', 'ca_cah2'), + 'c_Ca_in_CaO2H2': ('ca(caoh2)', 'cacaoh'), + 'c_Cr': ('cr',), + 'c_Cu': ('cu',), 'c_D_in_7LiD': ('dlid', 'd(7lid)'), 'c_D_in_D2O': ('dd2o', 'd-d2o', 'hwtr', 'hw', 'dhw00', 'd(d2o)'), 'c_D_in_D2O_solid': ('dice',), + 'c_D_in_MgD2': ('d(mgd2)', 'dmgd2'), 'c_F_in_Be2': ('fbef2', 'f in bef2'), 'c_F_in_CF2': ('fcf2', 'f(teflon)'), 'c_F_in_FLiBe': ('fflibe', 'f(flibe)'), 'c_F_in_HF': ('f_hf',), + 'c_F_in_LiF': ('f(lif)', 'flif'), 'c_F_in_MgF2': ('fmgf2', 'f in mgf2'), 'c_Fe56': ('fe', 'fe56', 'fe-56', '26-fe- 56'), + 'c_Fe_in_Fe_alpha': ('fe(fe-alph', 'fealph'), + 'c_Fe_in_Fe_gamma': ('fe(fe-gamm', 'fegamm'), + 'c_Ga_in_GaN': ('ga(gan)', 'gagan'), + 'c_Ga_in_GaSe': ('ga(gase)', 'gagase'), + 'c_Ge': ('ge',), + 'c_Ge_in_Ge3Bi4O12': ('ge(ge3bi4o', 'gegbo'), + 'c_Ge_in_GeTe': ('ge(gete)', 'gegete'), 'c_Graphite': ('graph', 'grph', 'gr', 'gr00', 'graphite'), 'c_Graphite_10p': ('grph10', '10p graphit'), 'c_Graphite_20p': ('grph20', '20 graphite'), @@ -68,41 +102,86 @@ _THERMAL_NAMES = { 'c_H_in_C5O2H8': ('lucite', 'c5o2h8', 'h-luci', 'h(lucite)'), 'c_H_in_C8H8': ('hc8h8', 'h(polystyr'), 'c_H_in_CaH2': ('hcah2', 'hca00', 'h(cah2)'), + 'c_H_in_CaO2H2': ('h(caoh2)', 'hcaoh'), 'c_H1_in_CaH2': ('h1cah2', 'h1_cah2'), 'c_H2_in_CaH2': ('h2cah2', 'h2_cah2'), - 'c_H_in_CH2': ('hch2', 'poly', 'pol', 'h-poly', 'pol00', 'h(ch2)'), - 'c_H_in_CH4_liquid': ('lch4', 'lmeth', 'l-ch4'), - 'c_H_in_CH4_solid': ('sch4', 'smeth', 's-ch4'), + 'c_H_in_C19H16_liquid': ('h(c19h16)l', 'h19liq'), + 'c_H_in_C19H16_solid': ('h(c19h16)s', 'h19sol'), + 'c_H_in_C2H6O_liquid': ('h(c2h6o)l', 'hetliq'), + 'c_H_in_C2H6O_solid': ('h(c2h6o)s', 'hetsol'), + 'c_H_in_C6H6_liquid': ('h(c6h6)l', 'hbzliq'), + 'c_H_in_C6H6_solid': ('h(c6h6)s', 'hbzsol'), + 'c_H_in_C7H8_liquid': ('h(c7h8)l', 'htlliq'), + 'c_H_in_C7H8_solid': ('h(c7h8)s', 'htlsol'), + 'c_H_in_C8H10_liquid': ('h(m-c8h10)l', 'hxyliq'), + 'c_H_in_C8H10_solid': ('h(m-c8h10)s', 'hxysol'), + 'c_H_in_C9H12_liquid': ('h(c9h12)l', 'hmsliq'), + 'c_H_in_C9H12_solid': ('h(c9h12)s', 'hmssol'), + 'c_H_in_CH2': ('hch2', 'poly', 'pol', 'h-poly', 'pol00', 'h(ch2)', 'h(c2h4)n r'), + 'c_H_in_CH4_liquid': ('lch4', 'lmeth', 'l-ch4', 'h(ch4)l'), + 'c_H_in_CH4_solid': ('sch4', 'smeth', 's-ch4', 'h(ch4)s'), 'c_H_in_CH4_solid_phase_II': ('sch4p2',), 'c_H_in_H2O': ('hh2o', 'h-h2o', 'lwtr', 'lw', 'lw00', 'h(h2o)'), 'c_H_in_H2O_solid': ('hice', 'h-ice', 'ice00', 'h(ice-ih)', 'h(ice)'), 'c_H_in_HF': ('hhf', 'h(hf)', 'h_hf'), + 'c_H_in_KOH': ('h(koh)', 'hkoh'), + 'c_H_in_LiH': ('h(lih)', 'hlih2'), 'c_H_in_Mesitylene': ('mesi00', 'mesi', 'mesi-phii'), + 'c_H_in_MgH2': ('h(mgh2)', 'hmgh2'), + 'c_H_in_MgOH2': ('h(mgoh2)', 'hmgoh'), + 'c_H_in_NaMgH3': ('h(namgh3)', 'hnamg'), + 'c_H_in_NaOH': ('h(naoh)', 'hnaoh'), 'c_H_in_ParaffinicOil': ('hparaf', 'h(paraffin', 'h(paraffini'), + 'c_H_in_SrH2': ('h(srh2)', 'hsrh2'), 'c_H_in_Toluene': ('tol00', 'tol', 'tolue-phii'), 'c_H_in_UH3': ('huh3', 'h(uh3)'), 'c_H_in_YH2': ('hyh2', 'h-yh2', 'h(yh2)'), 'c_H_in_ZrH': ('hzrh', 'h-zrh', 'h-zr', 'h/zr', 'hzr', 'hzr00', 'h(zrh)'), - 'c_H_in_ZrH2': ('hzrh2', 'h(zrh2)'), - 'c_H_in_ZrHx': ('hzrhx', 'h(zrhx)'), + 'c_H_in_ZrH2': ('hzrh2', 'h(zrh2)', 'h(zrh2) in'), + 'c_H_in_ZrHx': ('hzrhx', 'h(zrhx)', 'h(zrh15) i'), + 'c_I_in_NaI': ('i(nai)', 'inai'), + 'c_K': ('k',), + 'c_K_in_KOH': ('k(koh)', 'kkoh'), 'c_Li_in_FLiBe': ('liflib', 'li(flibe)'), 'c_Li_in_7LiD': ('lilid', '7li(7lid)'), 'c_Li_in_7LiH': ('lilih', '7li(7lih)'), + 'c_Li_in_LiF': ('li(lif)', 'lilif'), + 'c_Li_in_LiH': ('li(lih)', 'lilih2'), 'c_Mg24': ('mg', 'mg24', 'mg00', '24-mg'), + 'c_Mg_in_MgD2': ('mg(mgd2)', 'mgmgd2'), 'c_Mg_in_MgF2': ('mgmgf2', 'mg in mgf2'), + 'c_Mg_in_MgH2': ('mg(mgh2)', 'mgmgh2'), 'c_Mg_in_MgO': ('mgmgo', 'mg in mgo'), + 'c_Mg_in_MgOH2': ('mg(mgoh2)', 'mgoh2'), + 'c_Mg_in_NaMgH3': ('mg(namgh3)', 'mgnamg'), + 'c_Mo': ('mo',), + 'c_N_in_GaN': ('n(gan)', 'ngan'), 'c_N_in_UN_100p': ('nun100', 'n-un-100p'), 'c_N_in_UN_10p': ('nun10', 'n-un-10p'), 'c_N_in_UN_5p': ('nun5', 'n-un-5p'), 'c_N_in_UN': ('n-un', 'n(un)', 'n(un) l', 'ninun'), 'c_N_in_UN_HALEU': ('nunhal', 'n-un-haleu'), 'c_N_in_UN_HEU': ('nunheu', 'n-un-heu'), + 'c_Na': ('na',), + 'c_Na_in_NaI': ('na(nai)', 'nanai'), + 'c_Na_in_NaMgH3': ('na(namgh3)', 'nanamg'), + 'c_Na_in_NaOH': ('na(naoh)', 'nanaoh'), + 'c_Nb': ('nb',), + 'c_Ni': ('ni',), 'c_O_in_Al2O3': ('osap00', 'osap', 'o(al2o3)'), 'c_O_in_BeO': ('obeo', 'o-beo', 'o-be', 'o/be', 'obeo00', 'o(beo)', 'o_beo'), + 'c_O_in_C2H6O_liquid': ('o(c2h6o)l', 'oetliq'), + 'c_O_in_C2H6O_solid': ('o(c2h6o)s', 'oetsol'), 'c_O_in_C5O2H8': ('olucit', 'o(lucite)'), + 'c_O_in_CaO2H2': ('o(caoh2)', 'ocaoh'), 'c_O_in_D2O': ('od2o', 'o-d2o', 'ohw00', 'o(d2o)'), + 'c_O_in_H2O': ('o(h2o)', 'oh2o'), 'c_O_in_H2O_solid': ('oice', 'o-ice', 'o(ice-ih)'), + 'c_O_in_Ge3Bi4O12': ('o(ge3bi4o1', 'ogbo'), + 'c_O_in_KOH': ('o(koh)', 'okoh'), 'c_O_in_MgO': ('omgo', 'o in mgo'), + 'c_O_in_MgOH2': ('o(mgoh2)', 'omgoh'), + 'c_O_in_NaOH': ('o(naoh)', 'onaoh'), 'c_O_in_PuO2': ('opuo2', 'o in puo2'), 'c_O_in_SiO2_alpha': ('osio2a', 'o_sio2a'), 'c_O_in_UO2_100p': ('ouo200', 'o-uo2-100p'), @@ -111,16 +190,26 @@ _THERMAL_NAMES = { 'c_O_in_UO2': ('ouo2', 'o-uo2', 'o2-u', 'o2/u', 'ouo200', 'o(uo2)'), 'c_O_in_UO2_HALEU': ('ouo2hl', 'ouo2-haleu'), 'c_O_in_UO2_HEU': ('ouo2he', 'o_uo2-heu'), + 'c_O_in_Y3Al5O12': ('o(y3al5o12', 'oyag'), 'c_ortho_D': ('orthod', 'orthoD', 'dortho', 'od200', 'ortod', 'ortho-d'), 'c_ortho_H': ('orthoh', 'orthoH', 'hortho', 'oh200', 'ortoh', 'ortho-h'), 'c_para_D': ('parad', 'paraD', 'dpara', 'pd200', 'para-d'), 'c_para_H': ('parah', 'paraH', 'hpara', 'ph200', 'para-h'), + 'c_Pb': ('pb',), + 'c_Pd': ('pd',), + 'c_Pt': ('pt',), 'c_Pu_in_PuO2': ('puo2', 'pu in puo2'), 'c_Si28': ('si00', 'sili', 'si'), 'c_Si_in_SiC': ('sisic', 'si-sic', 'si(3c-sic)', 'si_sic'), 'c_Si_in_SiO2_alpha': ('si_o2a', 'si_sio2a'), - 'c_SiO2_alpha': ('sio2', 'sio2a', 'sio2alpha'), - 'c_SiO2_beta': ('sio2b', 'sio2beta'), + 'c_SiO2_alpha': ('sio2', 'sio2a', 'sio2alpha', 'sio2-a'), + 'c_SiO2_beta': ('sio2b', 'sio2beta', 'sio2-b'), + 'c_S_in_ZnS': ('s(zns-spha', 'szns'), + 'c_Se_in_GaSe': ('se(gase)', 'segase'), + 'c_Sn': ('sn',), + 'c_Sr_in_SrH2': ('sr(srh2)', 'srsrh2'), + 'c_Te_in_GeTe': ('te(gete)', 'tegete'), + 'c_Ti': ('ti',), 'c_U_metal_100p': ('u-100p',), 'c_U_metal_10p': ('u-10p',), 'c_U_metal_5p': ('u-5p',), @@ -145,11 +234,17 @@ _THERMAL_NAMES = { 'c_U_in_UO2': ('uuo2', 'u-uo2', 'u-o2', 'u/o2', 'uuo200', 'u(uo2)'), 'c_U_in_UO2_HALEU': ('uo2hal', 'uuo2-haleu'), 'c_U_in_UO2_HEU': ('uo2heu', 'u_uo2-heu'), + 'c_V': ('v',), + 'c_W': ('w',), + 'c_Y_in_Y3Al5O12': ('y(y3al5o12', 'yyag'), 'c_Y_in_YH2': ('yyh2', 'y-yh2', 'y(yh2)'), + 'c_Zn': ('zn',), + 'c_Zn_in_ZnS': ('zn(zns-sph', 'znzns'), + 'c_Zr': ('zr',), 'c_Zr_in_ZrC': ('zrzrc', 'zr(zrc)'), 'c_Zr_in_ZrH': ('zrzrh', 'zr-zrh', 'zr-h', 'zr/h', 'zr(zrh)'), - 'c_Zr_in_ZrH2': ('zrzrh2', 'zr(zrh2)'), - 'c_Zr_in_ZrHx': ('zrzrhx', 'zr(zrhx)'), + 'c_Zr_in_ZrH2': ('zrzrh2', 'zr(zrh2)', 'zr(zrh2) i'), + 'c_Zr_in_ZrHx': ('zrzrhx', 'zr(zrhx)', 'zr(zrh15)'), } diff --git a/tests/unit_tests/test_data_thermal.py b/tests/unit_tests/test_data_thermal.py index c444d0c58..7cd63ca71 100644 --- a/tests/unit_tests/test_data_thermal.py +++ b/tests/unit_tests/test_data_thermal.py @@ -6,6 +6,8 @@ import random import numpy as np import pytest import openmc.data +from openmc.data.thermal import _THERMAL_NAMES +from openmc.data.njoy import _THERMAL_DATA from . import needs_njoy @@ -258,6 +260,27 @@ def test_get_thermal_name(): assert f('boogie_monster') == 'c_boogie_monster' +def test_thermal_names_data_consistency(): + # Check that keys in _THERMAL_NAMES are also in _THERMAL_DATA + names_only = set(_THERMAL_NAMES.keys()) - set(_THERMAL_DATA.keys()) + assert not names_only, f"Keys in _THERMAL_NAMES but not in _THERMAL_DATA: {names_only}" + + # Check that keys in _THERMAL_DATA are also in _THERMAL_NAMES + data_only = set(_THERMAL_DATA.keys()) - set(_THERMAL_NAMES.keys()) + assert not data_only, f"Keys in _THERMAL_DATA but not in _THERMAL_NAMES: {data_only}" + + # Check that the name from each ThermalTuple in _THERMAL_DATA appears as + # a recognized alias in _THERMAL_NAMES for the same key + missing_aliases = [] + for key, thermal_tuple in _THERMAL_DATA.items(): + name = thermal_tuple.name + if name not in _THERMAL_NAMES[key]: + missing_aliases.append((key, name, _THERMAL_NAMES[key])) + assert not missing_aliases, ( + f"ThermalTuple names not in _THERMAL_NAMES aliases: {missing_aliases}" + ) + + @pytest.fixture def fake_mixed_elastic(): fake_tsl = openmc.data.ThermalScattering("c_D_in_7LiD", 1.9968, 4.9, [0.0253]) From 9c91bddf0402ba7e9b1ff94da2b73a46cc750811 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 2 Jan 2026 06:43:25 -0500 Subject: [PATCH 486/671] Migrate to SciPy sparse arrays (#3613) --- openmc/_sparse_compat.py | 43 ++++++++++++++++++++++++++++++++++++++++ openmc/cmfd.py | 8 ++++---- openmc/deplete/abc.py | 6 +++--- openmc/deplete/chain.py | 20 +++++++++---------- openmc/deplete/cram.py | 8 ++++---- openmc/deplete/pool.py | 7 ++++--- openmc/tallies.py | 35 +++++++++++++------------------- 7 files changed, 82 insertions(+), 45 deletions(-) create mode 100644 openmc/_sparse_compat.py diff --git a/openmc/_sparse_compat.py b/openmc/_sparse_compat.py new file mode 100644 index 000000000..c00777e19 --- /dev/null +++ b/openmc/_sparse_compat.py @@ -0,0 +1,43 @@ +"""Compatibility module for scipy.sparse arrays + +This module provides a compatibility layer for working with scipy.sparse arrays +across different scipy versions. Sparse arrays were introduced gradually in +scipy, with full support arriving in scipy 1.15. This module provides a unified +API that uses sparse arrays when available and falls back to sparse matrices for +older scipy versions. + +For more information on the migration from sparse matrices to sparse arrays, +see: https://docs.scipy.org/doc/scipy/reference/sparse.migration_to_sparray.html +""" + +import scipy +from scipy import sparse as sp + +# Check scipy version for feature availability +_SCIPY_VERSION = tuple(map(int, scipy.__version__.split('.')[:2])) + +if _SCIPY_VERSION >= (1, 15): + # Use sparse arrays + csr_array = sp.csr_array + csc_array = sp.csc_array + dok_array = sp.dok_array + lil_array = sp.lil_array + eye_array = sp.eye_array + block_array = sp.block_array +else: + # Fall back to sparse matrices + csr_array = sp.csr_matrix + csc_array = sp.csc_matrix + dok_array = sp.dok_matrix + lil_array = sp.lil_matrix + eye_array = sp.eye + block_array = sp.bmat + +__all__ = [ + 'csr_array', + 'csc_array', + 'dok_array', + 'lil_array', + 'eye_array', + 'block_array', +] diff --git a/openmc/cmfd.py b/openmc/cmfd.py index eff6a151f..8595d1e02 100644 --- a/openmc/cmfd.py +++ b/openmc/cmfd.py @@ -25,6 +25,7 @@ import openmc.lib from .checkvalue import (check_type, check_length, check_value, check_greater_than, check_less_than) from .exceptions import OpenMCError +from ._sparse_compat import csr_array # See if mpi4py module can be imported, define have_mpi global variable try: @@ -980,8 +981,7 @@ class CMFDRun: loss_row = self._loss_row loss_col = self._loss_col temp_data = np.ones(len(loss_row)) - temp_loss = sparse.csr_matrix((temp_data, (loss_row, loss_col)), - shape=(n, n)) + temp_loss = csr_array((temp_data, (loss_row, loss_col)), shape=(n, n)) temp_loss.sort_indices() # Pass coremap as 1-d array of 32-bit integers @@ -1585,7 +1585,7 @@ class CMFDRun: # Create csr matrix loss_row = self._loss_row loss_col = self._loss_col - loss = sparse.csr_matrix((data, (loss_row, loss_col)), shape=(n, n)) + loss = csr_array((data, (loss_row, loss_col)), shape=(n, n)) loss.sort_indices() return loss @@ -1612,7 +1612,7 @@ class CMFDRun: # Create csr matrix prod_row = self._prod_row prod_col = self._prod_col - prod = sparse.csr_matrix((data, (prod_row, prod_col)), shape=(n, n)) + prod = csr_array((data, (prod_row, prod_col)), shape=(n, n)) prod.sort_indices() return prod diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 32f468306..ee742f22b 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -600,7 +600,7 @@ class Integrator(ABC): User-supplied functions are expected to have the following signature: ``solver(A, n0, t) -> n1`` where - * ``A`` is a :class:`scipy.sparse.csc_matrix` making up the + * ``A`` is a :class:`scipy.sparse.csc_array` making up the depletion matrix * ``n0`` is a 1-D :class:`numpy.ndarray` of initial compositions for a given material in atoms/cm3 @@ -1134,7 +1134,7 @@ class SIIntegrator(Integrator): User-supplied functions are expected to have the following signature: ``solver(A, n0, t) -> n1`` where - * ``A`` is a :class:`scipy.sparse.csc_matrix` making up the + * ``A`` is a :class:`scipy.sparse.csc_array` making up the depletion matrix * ``n0`` is a 1-D :class:`numpy.ndarray` of initial compositions for a given material in atoms/cm3 @@ -1297,7 +1297,7 @@ class DepSystemSolver(ABC): Parameters ---------- - A : scipy.sparse.csc_matrix + A : scipy.sparse.csc_array Sparse transmutation matrix ``A[j, i]`` describing rates at which isotope ``i`` transmutes to isotope ``j`` n0 : numpy.ndarray diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index 873d7ca89..a835face7 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -17,13 +17,13 @@ from warnings import warn from typing import List import lxml.etree as ET -import scipy.sparse as sp from openmc.checkvalue import check_type, check_greater_than, PathLike from openmc.data import gnds_name, zam from openmc.exceptions import DataError from .nuclide import FissionYieldDistribution, Nuclide from .._xml import get_text +from .._sparse_compat import csc_array, dok_array import openmc.data @@ -619,7 +619,7 @@ class Chain: Returns ------- - scipy.sparse.csc_matrix + scipy.sparse.csc_array Sparse matrix representing depletion. See Also @@ -713,7 +713,7 @@ class Chain: reactions.clear() # Return CSC representation instead of DOK - return sp.csc_matrix((vals, (rows, cols)), shape=(n, n)) + return csc_array((vals, (rows, cols)), shape=(n, n)) def add_redox_term(self, matrix, buffer, oxidation_states): r"""Adds a redox term to the depletion matrix from data contained in @@ -731,7 +731,7 @@ class Chain: Parameters ---------- - matrix : scipy.sparse.csc_matrix + matrix : scipy.sparse.csc_array Sparse matrix representing depletion buffer : dict Dictionary of buffer nuclides used to maintain anoins net balance. @@ -743,7 +743,7 @@ class Chain: states as integers (e.g., +1, 0). Returns ------- - matrix : scipy.sparse.csc_matrix + matrix : scipy.sparse.csc_array Sparse matrix with redox term added """ # Elements list with the same size as self.nuclides @@ -769,7 +769,7 @@ class Chain: for nuc, idx in buffer_idx.items(): array[idx] -= redox_change * buffer[nuc] / os[idx] - return sp.csc_matrix(array) + return csc_array(array) def form_rr_term(self, tr_rates, current_timestep, mats): """Function to form the transfer rate term matrices. @@ -800,13 +800,13 @@ class Chain: Returns ------- - scipy.sparse.csc_matrix + scipy.sparse.csc_array Sparse matrix representing transfer term. """ # Use DOK as intermediate representation n = len(self) - matrix = sp.dok_matrix((n, n)) + matrix = dok_array((n, n)) for i, nuc in enumerate(self.nuclides): elm = re.split(r'\d+', nuc.name)[0] @@ -857,7 +857,7 @@ class Chain: Returns ------- - scipy.sparse.csc_matrix + scipy.sparse.csc_array Sparse vector representing external source term. """ @@ -865,7 +865,7 @@ class Chain: return # Use DOK as intermediate representation n = len(self) - vector = sp.dok_matrix((n, 1)) + vector = dok_array((n, 1)) for i, nuc in enumerate(self.nuclides): # Build source term vector diff --git a/openmc/deplete/cram.py b/openmc/deplete/cram.py index 53de83bb6..cecc388f4 100644 --- a/openmc/deplete/cram.py +++ b/openmc/deplete/cram.py @@ -6,11 +6,11 @@ Implements two different forms of CRAM for use in openmc.deplete. import numbers import numpy as np -import scipy.sparse as sp import scipy.sparse.linalg as sla from openmc.checkvalue import check_type, check_length from .abc import DepSystemSolver +from .._sparse_compat import csc_array, eye_array __all__ = ["CRAM16", "CRAM48", "Cram16Solver", "Cram48Solver", "IPFCramSolver"] @@ -60,7 +60,7 @@ class IPFCramSolver(DepSystemSolver): Parameters ---------- - A : scipy.sparse.csr_matrix + A : scipy.sparse.csc_array Sparse transmutation matrix ``A[j, i]`` desribing rates at which isotope ``i`` transmutes to isotope ``j`` n0 : numpy.ndarray @@ -75,9 +75,9 @@ class IPFCramSolver(DepSystemSolver): Final compositions after ``dt`` """ - A = dt * sp.csc_matrix(A, dtype=np.float64) + A = dt * csc_array(A, dtype=np.float64) y = n0.copy() - ident = sp.eye(A.shape[0], format='csc') + ident = eye_array(A.shape[0], format='csc') for alpha, theta in zip(self.alpha, self.theta): y += 2*np.real(alpha*sla.spsolve(A - theta*ident, y)) return y * self.alpha0 diff --git a/openmc/deplete/pool.py b/openmc/deplete/pool.py index aa348c02a..58f90894b 100644 --- a/openmc/deplete/pool.py +++ b/openmc/deplete/pool.py @@ -5,10 +5,11 @@ Provided to avoid some circular imports from itertools import repeat, starmap from multiprocessing import Pool -from scipy.sparse import bmat, hstack, vstack, csc_matrix import numpy as np +from scipy.sparse import hstack from openmc.mpi import comm +from .._sparse_compat import block_array # Configurable switch that enables / disables the use of # multiprocessing routines during depletion @@ -159,7 +160,7 @@ def deplete(func, chain, n, rates, dt, current_timestep=None, matrix_func=None, cols.append(None) rows.append(cols) - matrix = bmat(rows) + matrix = block_array(rows) # Concatenate vectors of nuclides in one n_multi = np.concatenate(n) @@ -194,7 +195,7 @@ def deplete(func, chain, n, rates, dt, current_timestep=None, matrix_func=None, # of the nuclide vectors for i, matrix in enumerate(matrices): if not np.equal(*matrix.shape): - matrices[i] = vstack([matrix, csc_matrix([0]*matrix.shape[1])]) + matrix.resize(matrix.shape[1], matrix.shape[1]) n[i] = np.append(n[i], 1.0) inputs = zip(matrices, n, repeat(dt)) diff --git a/openmc/tallies.py b/openmc/tallies.py index 09365e525..7b1ef0219 100644 --- a/openmc/tallies.py +++ b/openmc/tallies.py @@ -12,11 +12,11 @@ import lxml.etree as ET import h5py import numpy as np import pandas as pd -import scipy.sparse as sps from scipy.stats import chi2, norm import openmc import openmc.checkvalue as cv +from ._sparse_compat import lil_array from ._xml import clean_indentation, get_elem_list, get_text from .mixin import IDManagerMixin from .mesh import MeshBase @@ -435,10 +435,10 @@ class Tally(IDManagerMixin): # Convert NumPy arrays to SciPy sparse LIL matrices if self.sparse: - self._sum = sps.lil_matrix(self._sum.flatten(), self._sum.shape) - self._sum_sq = sps.lil_matrix(self._sum_sq.flatten(), self._sum_sq.shape) - self._sum_third = sps.lil_matrix(self._sum_third.flatten(), self._sum_third.shape) - self._sum_fourth = sps.lil_matrix(self.sum_fourth.flatten(), self._sum_fourth.shape) + self._sum = lil_array(self._sum.flatten(), self._sum.shape) + self._sum_sq = lil_array(self._sum_sq.flatten(), self._sum_sq.shape) + self._sum_third = lil_array(self._sum_third.flatten(), self._sum_third.shape) + self._sum_fourth = lil_array(self._sum_fourth.flatten(), self._sum_fourth.shape) # Read simulation time (needed for figure of merit) self._simulation_time = f["runtime"]["simulation"][()] @@ -534,8 +534,7 @@ class Tally(IDManagerMixin): # Convert NumPy array to SciPy sparse LIL matrix if self.sparse: - self._mean = sps.lil_matrix(self._mean.flatten(), - self._mean.shape) + self._mean = lil_array(self._mean.flatten(), self._mean.shape) if self.sparse: return np.reshape(self._mean.toarray(), self.shape) @@ -556,8 +555,7 @@ class Tally(IDManagerMixin): # Convert NumPy array to SciPy sparse LIL matrix if self.sparse: - self._std_dev = sps.lil_matrix(self._std_dev.flatten(), - self._std_dev.shape) + self._std_dev = lil_array(self._std_dev.flatten(), self._std_dev.shape) self.with_batch_statistics = True @@ -588,7 +586,7 @@ class Tally(IDManagerMixin): self._vov[mask] = numerator[mask]/denominator[mask] - 1.0/n if self.sparse: - self._vov = sps.lil_matrix(self._vov.flatten(), self._vov.shape) + self._vov = lil_array(self._vov.flatten(), self._vov.shape) if self.sparse: return np.reshape(self._vov.toarray(), self.shape) @@ -963,22 +961,17 @@ class Tally(IDManagerMixin): # Convert NumPy arrays to SciPy sparse LIL matrices if sparse and not self.sparse: if self._sum is not None: - self._sum = sps.lil_matrix(self._sum.flatten(), self._sum.shape) + self._sum = lil_array(self._sum.flatten(), self._sum.shape) if self._sum_sq is not None: - self._sum_sq = sps.lil_matrix(self._sum_sq.flatten(), - self._sum_sq.shape) + self._sum_sq = lil_array(self._sum_sq.flatten(), self._sum_sq.shape) if self._sum_third is not None: - self._sum_third = sps.lil_matrix(self._sum_third.flatten(), - self._sum_third.shape) + self._sum_third = lil_array(self._sum_third.flatten(), self._sum_third.shape) if self._sum_fourth is not None: - self._sum_fourth = sps.lil_matrix(self._sum_fourth.flatten(), - self._sum_fourth.shape) + self._sum_fourth = lil_array(self._sum_fourth.flatten(), self._sum_fourth.shape) if self._mean is not None: - self._mean = sps.lil_matrix(self._mean.flatten(), - self._mean.shape) + self._mean = lil_array(self._mean.flatten(), self._mean.shape) if self._std_dev is not None: - self._std_dev = sps.lil_matrix(self._std_dev.flatten(), - self._std_dev.shape) + self._std_dev = lil_array(self._std_dev.flatten(), self._std_dev.shape) self._sparse = True From 60ddafa9b385490ac5935ca0420634c2bba398a7 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Mon, 5 Jan 2026 21:57:05 +0100 Subject: [PATCH 487/671] Open dagmc model file within a context manager (#3706) --- openmc/dagmc.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/openmc/dagmc.py b/openmc/dagmc.py index 3cb48ddf1..fd4258225 100644 --- a/openmc/dagmc.py +++ b/openmc/dagmc.py @@ -223,21 +223,19 @@ class DAGMCUniverse(openmc.UniverseBase): @property def material_names(self): - dagmc_file_contents = h5py.File(self.filename) - material_tags_hex = dagmc_file_contents['/tstt/tags/NAME'].get( - 'values') material_tags_ascii = [] - for tag in material_tags_hex: - candidate_tag = tag.tobytes().decode().replace('\x00', '') - # tags might be for temperature or reflective surfaces - if candidate_tag.startswith('mat:'): - # if name ends with _comp remove it, it is not parsed - if candidate_tag.endswith('_comp'): - candidate_tag = candidate_tag[:-5] - # removes first 4 characters as openmc.Material name should be - # set without the 'mat:' part of the tag - material_tags_ascii.append(candidate_tag[4:]) - + with h5py.File(self.filename) as dagmc_file_contents: + material_tags_hex = dagmc_file_contents['/tstt/tags/NAME'].get('values') + for tag in material_tags_hex: + candidate_tag = tag.tobytes().decode().replace('\x00', '') + # tags might be for temperature or reflective surfaces + if candidate_tag.startswith('mat:'): + # if name ends with _comp remove it, it is not parsed + if candidate_tag.endswith('_comp'): + candidate_tag = candidate_tag[:-5] + # removes first 4 characters as openmc.Material name should be + # set without the 'mat:' part of the tag + material_tags_ascii.append(candidate_tag[4:]) return sorted(set(material_tags_ascii)) def _n_geom_elements(self, geom_type): From 818fd11b18fa1d089a0c1ae648939e23b56aa185 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Tue, 6 Jan 2026 11:29:46 +0200 Subject: [PATCH 488/671] Simplify rectangular lattice crossing and correctly handle corner checks (#3703) Co-authored-by: Paul Romano --- src/lattice.cpp | 54 ++--- .../lattice_corner_crossing/__init__.py | 0 .../lattice_corner_crossing/inputs_true.dat | 58 +++++ .../lattice_corner_crossing/results_true.dat | 201 ++++++++++++++++++ .../lattice_corner_crossing/test.py | 83 ++++++++ 5 files changed, 358 insertions(+), 38 deletions(-) create mode 100644 tests/regression_tests/lattice_corner_crossing/__init__.py create mode 100644 tests/regression_tests/lattice_corner_crossing/inputs_true.dat create mode 100644 tests/regression_tests/lattice_corner_crossing/results_true.dat create mode 100644 tests/regression_tests/lattice_corner_crossing/test.py diff --git a/src/lattice.cpp b/src/lattice.cpp index efbfcb216..92d451f61 100644 --- a/src/lattice.cpp +++ b/src/lattice.cpp @@ -260,46 +260,24 @@ std::pair> RectLattice::distance( // Determine the oncoming edge. double x0 {copysign(0.5 * pitch_[0], u.x)}; double y0 {copysign(0.5 * pitch_[1], u.y)}; + double z0; - // Left and right sides - double d {INFTY}; - array lattice_trans; - if ((std::abs(x - x0) > FP_PRECISION) && u.x != 0) { - d = (x0 - x) / u.x; - if (u.x > 0) { - lattice_trans = {1, 0, 0}; - } else { - lattice_trans = {-1, 0, 0}; - } - } - - // Front and back sides - if ((std::abs(y - y0) > FP_PRECISION) && u.y != 0) { - double this_d = (y0 - y) / u.y; - if (this_d < d) { - d = this_d; - if (u.y > 0) { - lattice_trans = {0, 1, 0}; - } else { - lattice_trans = {0, -1, 0}; - } - } - } - - // Top and bottom sides + double d = std::min( + u.x != 0.0 ? (x0 - x) / u.x : INFTY, u.y != 0.0 ? (y0 - y) / u.y : INFTY); if (is_3d_) { - double z0 {copysign(0.5 * pitch_[2], u.z)}; - if ((std::abs(z - z0) > FP_PRECISION) && u.z != 0) { - double this_d = (z0 - z) / u.z; - if (this_d < d) { - d = this_d; - if (u.z > 0) { - lattice_trans = {0, 0, 1}; - } else { - lattice_trans = {0, 0, -1}; - } - } - } + z0 = copysign(0.5 * pitch_[2], u.z); + d = std::min(d, u.z != 0.0 ? (z0 - z) / u.z : INFTY); + } + + // Determine which lattice boundaries are being crossed + array lattice_trans = {0, 0, 0}; + if (u.x != 0.0 && std::abs(x + u.x * d - x0) < FP_PRECISION) + lattice_trans[0] = copysign(1, u.x); + if (u.y != 0.0 && std::abs(y + u.y * d - y0) < FP_PRECISION) + lattice_trans[1] = copysign(1, u.y); + if (is_3d_) { + if (u.z != 0.0 && std::abs(z + u.z * d - z0) < FP_PRECISION) + lattice_trans[2] = copysign(1, u.z); } return {d, lattice_trans}; diff --git a/tests/regression_tests/lattice_corner_crossing/__init__.py b/tests/regression_tests/lattice_corner_crossing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/lattice_corner_crossing/inputs_true.dat b/tests/regression_tests/lattice_corner_crossing/inputs_true.dat new file mode 100644 index 000000000..4b49b1403 --- /dev/null +++ b/tests/regression_tests/lattice_corner_crossing/inputs_true.dat @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + 10.0 10.0 + 2 2 + -10.0 -10.0 + +1 2 +2 1 + + + + + + + + + fixed source + 1000 + 10 + + + -0.7071067811865476 -0.7071067811865475 0.0 + + + + + + + 10 10 + -20.0 -20.0 + 20.0 20.0 + + + 1 + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/lattice_corner_crossing/results_true.dat b/tests/regression_tests/lattice_corner_crossing/results_true.dat new file mode 100644 index 000000000..bd81cf535 --- /dev/null +++ b/tests/regression_tests/lattice_corner_crossing/results_true.dat @@ -0,0 +1,201 @@ +tally 1: +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.648775E-03 +7.016009E-06 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.981847E-03 +8.891414E-06 +3.331677E-03 +1.110007E-05 +6.742237E-03 +4.545776E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.388206E-03 +1.925636E-05 +2.032104E-03 +4.129446E-06 +3.014946E-03 +9.089898E-06 +7.058968E-03 +4.982903E-05 +9.013188E-04 +8.123755E-07 +7.501461E-04 +5.627191E-07 +0.000000E+00 +0.000000E+00 +5.134898E-03 +2.636718E-05 +1.207302E-03 +1.457579E-06 +3.045925E-03 +9.277657E-06 +4.132399E-03 +1.707672E-05 +1.055271E-02 +5.829711E-05 +4.014909E-03 +1.611949E-05 +2.698215E-03 +7.280363E-06 +1.751215E-02 +1.073504E-04 +1.356908E-02 +9.589157E-05 +5.451466E-03 +2.971848E-05 +3.165676E-04 +1.002151E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.724938E-03 +1.387516E-05 +2.812863E-03 +7.278837E-06 +1.000700E+01 +1.001402E+01 +2.345904E-02 +2.127641E-04 +2.451653E-02 +1.750725E-04 +8.695904E-03 +5.553981E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.947565E-03 +2.447840E-05 +1.725721E-02 +8.878803E-05 +5.656444E+01 +3.199538E+02 +2.159961E-02 +1.034364E-04 +3.091063E-02 +5.543317E-04 +4.232209E-03 +1.791159E-05 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.716170E-03 +2.224226E-05 +1.321855E-02 +5.288265E-05 +5.309576E-02 +7.984865E-04 +5.656179E+01 +3.199238E+02 +2.399311E-02 +2.936670E-04 +4.680753E-02 +9.526509E-04 +1.074699E-02 +1.154978E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.526425E-03 +2.048853E-05 +3.983003E-03 +9.553596E-06 +1.726789E-02 +9.579848E-05 +3.355605E-02 +3.331438E-04 +3.578422E-02 +3.560242E-04 +5.649209E+01 +3.191358E+02 +2.591495E-02 +4.169753E-04 +9.484744E-03 +8.065715E-05 +0.000000E+00 +0.000000E+00 +4.232587E-03 +9.113263E-06 +5.787378E-03 +2.194940E-05 +6.193626E-03 +2.357423E-05 +4.552787E-03 +6.987360E-06 +9.641308E-03 +3.506339E-05 +1.303892E-02 +4.378408E-05 +1.831744E-02 +8.836683E-05 +3.024725E+01 +9.148969E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.488204E-03 +2.214752E-06 +5.067487E-03 +1.520987E-05 +7.550927E-03 +2.993991E-05 +3.215559E-03 +1.033982E-05 +4.258525E-03 +1.205064E-05 +2.115487E-03 +4.475286E-06 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 diff --git a/tests/regression_tests/lattice_corner_crossing/test.py b/tests/regression_tests/lattice_corner_crossing/test.py new file mode 100644 index 000000000..4684d144e --- /dev/null +++ b/tests/regression_tests/lattice_corner_crossing/test.py @@ -0,0 +1,83 @@ +""" +This test is designed to ensure that we account for potential corner crossings +in floating point precision. + +""" + +from math import pi, cos, sin + +import openmc +import pytest + +from tests.testing_harness import PyAPITestHarness + + +@pytest.fixture +def model(): + model = openmc.Model() + + # Length of lattice on each side + lat_size = 20.0 + + # Angle that we're crossing the corner at + phi = pi/4.0 + + air = openmc.Material() + air.set_density('g/cm3', 0.001) + air.add_nuclide('N14', 1.0) + metal = openmc.Material() + metal.set_density('g/cm3', 7.0) + metal.add_nuclide('Fe56', 1.0) + + metal_cell = openmc.Cell(fill=metal) + metal_uni = openmc.Universe(cells=[metal_cell]) + + air_cell = openmc.Cell(fill=air) + air_uni = openmc.Universe(cells=[air_cell]) + + # Define a checkerboard lattice + lattice = openmc.RectLattice() + lattice.lower_left = (-lat_size/2.0, -lat_size/2.0) + lattice.pitch = (lat_size/2, lat_size/2) + lattice.universes = [ + [metal_uni, air_uni], + [air_uni, metal_uni] + ] + + box = openmc.model.RectangularPrism(lat_size, lat_size) + cyl = openmc.ZCylinder(r=lat_size, boundary_type='vacuum') + outside_lattice = openmc.Cell(region=-cyl & +box, fill=air) + inside_lattice = openmc.Cell(region=-box, fill=lattice) + + model.geometry = openmc.Geometry([outside_lattice, inside_lattice]) + + # Set all runtime parameters + model.settings.run_mode = 'fixed source' + model.settings.batches = 10 + model.settings.particles = 1000 + + # Define a source located outside the lattice and pointing straight into its + # corner at 45 degrees + model.settings.source = openmc.IndependentSource( + space=openmc.stats.Point((-cos(phi), -sin(phi), 0.0)), + angle=openmc.stats.Monodirectional((cos(phi), sin(phi), 0.0)) + ) + + # Create a mesh tally + mesh = openmc.RegularMesh() + mesh.dimension = (10, 10) + mesh.lower_left = (-lat_size, -lat_size) + mesh.upper_right = (lat_size, lat_size) + mesh_filter = openmc.MeshFilter(mesh) + tally = openmc.Tally(tally_id=1) + tally.filters = [mesh_filter] + tally.scores = ['flux'] + tally.estimator = 'tracklength' + model.tallies = [tally] + + return model + + +def test_lattice_corner_crossing(model): + harness = PyAPITestHarness('statepoint.10.h5', model) + harness.main() From c7d7fa4613146e97e22a5b616b4b70ca7640629d Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Tue, 6 Jan 2026 15:29:40 +0200 Subject: [PATCH 489/671] Fix a bug in rotational periodic boundary conditions (#3692) Co-authored-by: Paul Romano --- include/openmc/boundary_condition.h | 2 + include/openmc/surface.h | 35 ++++++++- src/boundary_condition.cpp | 73 +++++++------------ src/geometry_aux.cpp | 7 +- src/particle.cpp | 4 +- src/surface.cpp | 39 ++++++++-- .../{ => False-False}/inputs_true.dat | 0 .../{ => False-False}/results_true.dat | 0 .../periodic_6fold/False-True/inputs_true.dat | 34 +++++++++ .../False-True/results_true.dat | 2 + .../periodic_6fold/True-False/inputs_true.dat | 34 +++++++++ .../True-False/results_true.dat | 2 + .../periodic_6fold/True-True/inputs_true.dat | 34 +++++++++ .../periodic_6fold/True-True/results_true.dat | 2 + tests/regression_tests/periodic_6fold/test.py | 55 ++++++++++---- tests/unit_tests/test_periodic_bc.py | 47 ++++++++++++ 16 files changed, 297 insertions(+), 73 deletions(-) rename tests/regression_tests/periodic_6fold/{ => False-False}/inputs_true.dat (100%) rename tests/regression_tests/periodic_6fold/{ => False-False}/results_true.dat (100%) create mode 100644 tests/regression_tests/periodic_6fold/False-True/inputs_true.dat create mode 100644 tests/regression_tests/periodic_6fold/False-True/results_true.dat create mode 100644 tests/regression_tests/periodic_6fold/True-False/inputs_true.dat create mode 100644 tests/regression_tests/periodic_6fold/True-False/results_true.dat create mode 100644 tests/regression_tests/periodic_6fold/True-True/inputs_true.dat create mode 100644 tests/regression_tests/periodic_6fold/True-True/results_true.dat create mode 100644 tests/unit_tests/test_periodic_bc.py diff --git a/include/openmc/boundary_condition.h b/include/openmc/boundary_condition.h index 5a14239e8..89bf7ca8b 100644 --- a/include/openmc/boundary_condition.h +++ b/include/openmc/boundary_condition.h @@ -152,6 +152,8 @@ public: protected: //! Angle about the axis by which particle coordinates will be rotated double angle_; + //! Do we need to flip surfaces senses when applying the transformation? + bool flip_sense_; //! Ensure that choice of axes is right handed. axis_1_idx_ corresponds to the //! independent axis and axis_2_idx_ corresponds to the dependent axis in the //! 2D plane perpendicular to the planes' axis of rotation diff --git a/include/openmc/surface.h b/include/openmc/surface.h index 549e8f6ab..2d8580345 100644 --- a/include/openmc/surface.h +++ b/include/openmc/surface.h @@ -2,6 +2,7 @@ #define OPENMC_SURFACE_H #include // For numeric_limits +#include #include #include @@ -378,7 +379,39 @@ public: // Non-member functions //============================================================================== -void read_surfaces(pugi::xml_node node); +//! Read surface definitions from XML and populate the global surfaces vector. +//! +//! This function parses surface elements from the XML input, creates the +//! appropriate surface objects, and identifies periodic surfaces along with +//! their albedo values and sense information. +//! +//! \param node XML node containing surface definitions +//! \param[out] periodic_pairs Set of surface ID pairs representing periodic +//! boundary conditions +//! \param[out] albedo_map Map of surface IDs to albedo values for periodic +//! surfaces +//! \param[out] periodic_sense_map Map of surface IDs to their sense values +//! (used to determine orientation for periodic BCs) +void read_surfaces(pugi::xml_node node, + std::set>& periodic_pairs, + std::unordered_map& albedo_map, + std::unordered_map& periodic_sense_map); + +//! Resolve periodic surface pairs and assign boundary conditions. +//! +//! This function completes the setup of periodic boundary conditions by +//! resolving unpaired periodic surfaces, determining whether each pair +//! represents translational or rotational periodicity based on surface +//! normals, and assigning the appropriate boundary condition objects. +//! +//! \param[inout] periodic_pairs Set of surface ID pairs representing periodic +//! boundary conditions; unpaired entries are resolved +//! \param albedo_map Map of surface IDs to albedo values for periodic surfaces +//! \param periodic_sense_map Map of surface IDs to their sense values (used to +//! determine orientation for periodic BCs) +void prepare_boundary_conditions(std::set>& periodic_pairs, + std::unordered_map& albedo_map, + std::unordered_map& periodic_sense_map); void free_memory_surfaces(); diff --git a/src/boundary_condition.cpp b/src/boundary_condition.cpp index 2840b3c7d..eb095d8c5 100644 --- a/src/boundary_condition.cpp +++ b/src/boundary_condition.cpp @@ -160,7 +160,7 @@ void TranslationalPeriodicBC::handle_particle( RotationalPeriodicBC::RotationalPeriodicBC( int i_surf, int j_surf, PeriodicAxis axis) - : PeriodicBC(i_surf, j_surf) + : PeriodicBC(std::abs(i_surf) - 1, std::abs(j_surf) - 1) { Surface& surf1 {*model::surfaces[i_surf_]}; Surface& surf2 {*model::surfaces[j_surf_]}; @@ -173,14 +173,9 @@ RotationalPeriodicBC::RotationalPeriodicBC( axis_2_idx_ = 2; // z component dependent break; case y: - // for a right handed coordinate system, z should be the independent axis - // but this would cause the y-rotation case to be different than the other - // two. using a left handed coordinate system and a negative rotation the - // compute angle and rotation matrix behavior mimics that of the x and z - // cases zero_axis_idx_ = 1; // y component of plane must be zero - axis_1_idx_ = 0; // x component independent - axis_2_idx_ = 2; // z component dependent + axis_1_idx_ = 2; // z component independent + axis_2_idx_ = 0; // x component dependent break; case z: zero_axis_idx_ = 2; // z component of plane must be zero @@ -192,10 +187,16 @@ RotationalPeriodicBC::RotationalPeriodicBC( fmt::format("You've specified an axis that is not x, y, or z.")); } + Direction ax = {0.0, 0.0, 0.0}; + ax[zero_axis_idx_] = 1.0; + + auto i_sign = std::copysign(1, i_surf); + auto j_sign = -std::copysign(1, j_surf); + // Compute the surface normal vectors and make sure they are perpendicular // to the correct axis - Direction norm1 = surf1.normal({0, 0, 0}); - Direction norm2 = surf2.normal({0, 0, 0}); + Direction norm1 = i_sign * surf1.normal({0, 0, 0}); + Direction norm2 = j_sign * surf2.normal({0, 0, 0}); // Make sure both surfaces intersect the origin if (std::abs(surf1.evaluate({0, 0, 0})) > FP_COINCIDENT) { throw std::invalid_argument(fmt::format( @@ -212,8 +213,15 @@ RotationalPeriodicBC::RotationalPeriodicBC( surf2.id_)); } - angle_ = compute_periodic_rotation(norm1[axis_2_idx_], norm1[axis_1_idx_], - norm2[axis_2_idx_], norm2[axis_1_idx_]); + // Compute the signed rotation angle about the periodic axis. Note that + // (n1×n2)·a = |n1||n2|sin(θ) and n1·n2 = |n1||n2|cos(θ), where a is the axis + // of rotation. + auto c = norm1.cross(norm2); + angle_ = std::atan2(c.dot(ax), norm1.dot(norm2)); + + // If the normals point in the same general direction, the surface sense + // should change when crossing the boundary + flip_sense_ = (i_sign * j_sign > 0.0); // Warn the user if the angle does not evenly divide a circle double rem = std::abs(std::remainder((2 * PI / angle_), 1.0)); @@ -225,47 +233,18 @@ RotationalPeriodicBC::RotationalPeriodicBC( } } -double RotationalPeriodicBC::compute_periodic_rotation( - double rise_1, double run_1, double rise_2, double run_2) const -{ - // Compute the BC rotation angle. Here it is assumed that both surface - // normal vectors point inwards---towards the valid geometry region. - // Consequently, the rotation angle is not the difference between the two - // normals, but is instead the difference between one normal and one - // anti-normal. (An incident ray on one surface must be an outgoing ray on - // the other surface after rotation hence the anti-normal.) - double theta1 = std::atan2(rise_1, run_1); - double theta2 = std::atan2(rise_2, run_2) + PI; - return theta2 - theta1; -} - void RotationalPeriodicBC::handle_particle( Particle& p, const Surface& surf) const { - int i_particle_surf = p.surface_index(); + int new_surface = p.surface() > 0 ? -(j_surf_ + 1) : j_surf_ + 1; + if (flip_sense_) + new_surface = -new_surface; - // Figure out which of the two BC surfaces were struck to figure out if a - // forward or backward rotation is required. Specify the other surface as - // the particle's new surface. - double theta; - int new_surface; - if (i_particle_surf == i_surf_) { - theta = angle_; - new_surface = p.surface() > 0 ? -(j_surf_ + 1) : j_surf_ + 1; - } else if (i_particle_surf == j_surf_) { - theta = -angle_; - new_surface = p.surface() > 0 ? -(i_surf_ + 1) : i_surf_ + 1; - } else { - throw std::runtime_error( - "Called BoundaryCondition::handle_particle after " - "hitting a surface, but that surface is not recognized by the BC."); - } - - // Rotate the particle's position and direction about the z-axis. + // Rotate the particle's position and direction. Position r = p.r(); Direction u = p.u(); - double cos_theta = std::cos(theta); - double sin_theta = std::sin(theta); + double cos_theta = std::cos(angle_); + double sin_theta = std::sin(angle_); Position new_r; new_r[zero_axis_idx_] = r[zero_axis_idx_]; diff --git a/src/geometry_aux.cpp b/src/geometry_aux.cpp index 8a145fb1f..a740740c1 100644 --- a/src/geometry_aux.cpp +++ b/src/geometry_aux.cpp @@ -55,8 +55,13 @@ void read_geometry_xml() void read_geometry_xml(pugi::xml_node root) { // Read surfaces, cells, lattice - read_surfaces(root); + std::set> periodic_pairs; + std::unordered_map albedo_map; + std::unordered_map periodic_sense_map; + + read_surfaces(root, periodic_pairs, albedo_map, periodic_sense_map); read_cells(root); + prepare_boundary_conditions(periodic_pairs, albedo_map, periodic_sense_map); read_lattices(root); // Check to make sure a boundary condition was applied to at least one diff --git a/src/particle.cpp b/src/particle.cpp index 2d70d715e..b9f9c8f86 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -744,9 +744,7 @@ void Particle::cross_periodic_bc( if (!neighbor_list_find_cell(*this)) { mark_as_lost("Couldn't find particle after hitting periodic " "boundary on surface " + - std::to_string(surf.id_) + - ". The normal vector " - "of one periodic surface may need to be reversed."); + std::to_string(surf.id_) + "."); return; } diff --git a/src/surface.cpp b/src/surface.cpp index bc9c7f4a9..9ac1c41b4 100644 --- a/src/surface.cpp +++ b/src/surface.cpp @@ -9,6 +9,7 @@ #include #include "openmc/array.h" +#include "openmc/cell.h" #include "openmc/container_util.h" #include "openmc/error.h" #include "openmc/external/quartic_solver.h" @@ -1169,7 +1170,10 @@ Direction SurfaceZTorus::normal(Position r) const //============================================================================== -void read_surfaces(pugi::xml_node node) +void read_surfaces(pugi::xml_node node, + std::set>& periodic_pairs, + std::unordered_map& albedo_map, + std::unordered_map& periodic_sense_map) { // Count the number of surfaces int n_surfaces = 0; @@ -1180,8 +1184,6 @@ void read_surfaces(pugi::xml_node node) // Loop over XML surface elements and populate the array. Keep track of // periodic surfaces and their albedos. model::surfaces.reserve(n_surfaces); - std::set> periodic_pairs; - std::unordered_map albedo_map; { pugi::xml_node surf_node; int i_surf; @@ -1244,6 +1246,7 @@ void read_surfaces(pugi::xml_node node) if (check_for_node(surf_node, "boundary")) { std::string surf_bc = get_node_value(surf_node, "boundary", true, true); if (surf_bc == "periodic") { + periodic_sense_map[model::surfaces.back()->id_] = 0; // Check for surface albedo. Skip sanity check as it is already done // in the Surface class's constructor. if (check_for_node(surf_node, "albedo")) { @@ -1275,6 +1278,28 @@ void read_surfaces(pugi::xml_node node) fmt::format("Two or more surfaces use the same unique ID: {}", id)); } } +} + +void prepare_boundary_conditions(std::set>& periodic_pairs, + std::unordered_map& albedo_map, + std::unordered_map& periodic_sense_map) +{ + // Fill the senses map for periodic surfaces + auto n_periodic = periodic_sense_map.size(); + for (const auto& cell : model::cells) { + if (n_periodic == 0) + break; // Early exit once all periodic surfaces found + + for (auto s : cell->surfaces()) { + auto surf_idx = std::abs(s) - 1; + auto id = model::surfaces[surf_idx]->id_; + + if (periodic_sense_map.count(id)) { + periodic_sense_map[id] = std::copysign(1, s); + --n_periodic; + } + } + } // Resolve unpaired periodic surfaces. A lambda function is used with // std::find_if to identify the unpaired surfaces. @@ -1370,8 +1395,12 @@ void read_surfaces(pugi::xml_node node) "indicates that the two planes are not periodic about the X, Y, or Z " "axis, which is not supported.")); } - surf1.bc_ = make_unique(i_surf, j_surf, axis); - surf2.bc_ = make_unique(i_surf, j_surf, axis); + auto i_sign = periodic_sense_map[periodic_pair.first]; + auto j_sign = periodic_sense_map[periodic_pair.second]; + surf1.bc_ = make_unique( + i_sign * (i_surf + 1), j_sign * (j_surf + 1), axis); + surf2.bc_ = make_unique( + j_sign * (j_surf + 1), i_sign * (i_surf + 1), axis); } // If albedo data is present in albedo map, set the boundary albedo. diff --git a/tests/regression_tests/periodic_6fold/inputs_true.dat b/tests/regression_tests/periodic_6fold/False-False/inputs_true.dat similarity index 100% rename from tests/regression_tests/periodic_6fold/inputs_true.dat rename to tests/regression_tests/periodic_6fold/False-False/inputs_true.dat diff --git a/tests/regression_tests/periodic_6fold/results_true.dat b/tests/regression_tests/periodic_6fold/False-False/results_true.dat similarity index 100% rename from tests/regression_tests/periodic_6fold/results_true.dat rename to tests/regression_tests/periodic_6fold/False-False/results_true.dat diff --git a/tests/regression_tests/periodic_6fold/False-True/inputs_true.dat b/tests/regression_tests/periodic_6fold/False-True/inputs_true.dat new file mode 100644 index 000000000..cbc1e9100 --- /dev/null +++ b/tests/regression_tests/periodic_6fold/False-True/inputs_true.dat @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 1000 + 4 + 0 + + + 0 0 0 5 5 0 + + + + diff --git a/tests/regression_tests/periodic_6fold/False-True/results_true.dat b/tests/regression_tests/periodic_6fold/False-True/results_true.dat new file mode 100644 index 000000000..04aa30878 --- /dev/null +++ b/tests/regression_tests/periodic_6fold/False-True/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +1.848492E+00 2.933785E-03 diff --git a/tests/regression_tests/periodic_6fold/True-False/inputs_true.dat b/tests/regression_tests/periodic_6fold/True-False/inputs_true.dat new file mode 100644 index 000000000..675a8b300 --- /dev/null +++ b/tests/regression_tests/periodic_6fold/True-False/inputs_true.dat @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 1000 + 4 + 0 + + + 0 0 0 5 5 0 + + + + diff --git a/tests/regression_tests/periodic_6fold/True-False/results_true.dat b/tests/regression_tests/periodic_6fold/True-False/results_true.dat new file mode 100644 index 000000000..04aa30878 --- /dev/null +++ b/tests/regression_tests/periodic_6fold/True-False/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +1.848492E+00 2.933785E-03 diff --git a/tests/regression_tests/periodic_6fold/True-True/inputs_true.dat b/tests/regression_tests/periodic_6fold/True-True/inputs_true.dat new file mode 100644 index 000000000..e5ac03b48 --- /dev/null +++ b/tests/regression_tests/periodic_6fold/True-True/inputs_true.dat @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 1000 + 4 + 0 + + + 0 0 0 5 5 0 + + + + diff --git a/tests/regression_tests/periodic_6fold/True-True/results_true.dat b/tests/regression_tests/periodic_6fold/True-True/results_true.dat new file mode 100644 index 000000000..04aa30878 --- /dev/null +++ b/tests/regression_tests/periodic_6fold/True-True/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +1.848492E+00 2.933785E-03 diff --git a/tests/regression_tests/periodic_6fold/test.py b/tests/regression_tests/periodic_6fold/test.py index 272c65df5..bded1c465 100644 --- a/tests/regression_tests/periodic_6fold/test.py +++ b/tests/regression_tests/periodic_6fold/test.py @@ -1,14 +1,16 @@ from math import sin, cos, pi import openmc +from openmc.utility_funcs import change_directory import pytest from tests.testing_harness import PyAPITestHarness -@pytest.fixture -def model(): - model = openmc.model.Model() +@pytest.mark.parametrize("flip1", [False, True]) +@pytest.mark.parametrize("flip2", [False, True]) +def test_periodic(flip1, flip2): + model = openmc.Model() # Define materials water = openmc.Material() @@ -27,31 +29,52 @@ def model(): # answers. theta1 = (-1/6 + 1/2) * pi theta2 = (1/6 - 1/2) * pi - plane1 = openmc.Plane(a=cos(theta1), b=sin(theta1), boundary_type='periodic') - plane2 = openmc.Plane(a=cos(theta2), b=sin(theta2), boundary_type='periodic') + if flip1: + plane1 = openmc.Plane(a=-cos(theta1), b=-sin(theta1), boundary_type='periodic') + else: + plane1 = openmc.Plane(a=cos(theta1), b=sin(theta1), boundary_type='periodic') + if flip2: + plane2 = openmc.Plane(a=-cos(theta2), b=-sin(theta2), boundary_type='periodic') + else: + plane2 = openmc.Plane(a=cos(theta2), b=sin(theta2), boundary_type='periodic') x_max = openmc.XPlane(5., boundary_type='reflective') z_cyl = openmc.ZCylinder(x0=3*cos(pi/6), y0=3*sin(pi/6), r=2.0) - outside_cyl = openmc.Cell(1, fill=water, region=( - +plane1 & +plane2 & -x_max & +z_cyl)) - inside_cyl = openmc.Cell(2, fill=fuel, region=( - +plane1 & +plane2 & -z_cyl)) + match (flip1, flip2): + case (False, False): + outside_cyl = openmc.Cell(1, fill=water, region=( + +plane1 & +plane2 & -x_max & +z_cyl)) + inside_cyl = openmc.Cell(2, fill=fuel, region=( + +plane1 & +plane2 & -z_cyl)) + case (False, True): + outside_cyl = openmc.Cell(1, fill=water, region=( + +plane1 & -plane2 & -x_max & +z_cyl)) + inside_cyl = openmc.Cell(2, fill=fuel, region=( + +plane1 & -plane2 & -z_cyl)) + case (True, False): + outside_cyl = openmc.Cell(1, fill=water, region=( + -plane1 & +plane2 & -x_max & +z_cyl)) + inside_cyl = openmc.Cell(2, fill=fuel, region=( + -plane1 & +plane2 & -z_cyl)) + case (True, True): + outside_cyl = openmc.Cell(1, fill=water, region=( + -plane1 & -plane2 & -x_max & +z_cyl)) + inside_cyl = openmc.Cell(2, fill=fuel, region=( + -plane1 & -plane2 & -z_cyl)) root_universe = openmc.Universe(0, cells=(outside_cyl, inside_cyl)) model.geometry = openmc.Geometry(root_universe) # Define settings - model.settings = openmc.Settings() model.settings.particles = 1000 model.settings.batches = 4 model.settings.inactive = 0 - model.settings.source = openmc.IndependentSource(space=openmc.stats.Box( - (0, 0, 0), (5, 5, 0)) + model.settings.source = openmc.IndependentSource( + space=openmc.stats.Box((0, 0, 0), (5, 5, 0)) ) - return model + with change_directory(f'{flip1}-{flip2}'): + harness = PyAPITestHarness('statepoint.4.h5', model) + harness.main() -def test_periodic(model): - harness = PyAPITestHarness('statepoint.4.h5', model) - harness.main() diff --git a/tests/unit_tests/test_periodic_bc.py b/tests/unit_tests/test_periodic_bc.py new file mode 100644 index 000000000..4cc126abc --- /dev/null +++ b/tests/unit_tests/test_periodic_bc.py @@ -0,0 +1,47 @@ +from math import cos, sin, radians +import random + +import openmc +import pytest + + +@pytest.mark.parametrize("angle", [30., 45., 60., 90., 120.]) +def test_rotational_periodic_bc(angle): + # Pick random starting angle + start = random.uniform(0., 360.) + degrees = angle + ang1 = radians(start) + ang2 = radians(start + degrees) + + # Define three points on each plane and then randomly shuffle them + p1_points = [(0., 0., 0.), (cos(ang1), sin(ang1), 0.), (0., 0., 1.)] + p2_points = [(0., 0., 0.), (cos(ang2), sin(ang2), 0.), (0., 0., 1.)] + random.shuffle(p1_points) + random.shuffle(p2_points) + + # Create periodic planes and a cylinder + p1 = openmc.Plane.from_points(*p1_points, boundary_type='periodic') + p2 = openmc.Plane.from_points(*p2_points, boundary_type='periodic') + p1.periodic_surface = p2 + zcyl = openmc.ZCylinder(r=5., boundary_type='vacuum') + + # Figure out which side of planes to use based on a point in the middle + ang_mid = radians(start + degrees/2.) + mid_point = (cos(ang_mid), sin(ang_mid), 0.) + r1 = -p1 if mid_point in -p1 else +p1 + r2 = -p2 if mid_point in -p2 else +p2 + + # Create one cell bounded by the two planes and the cylinder + mat = openmc.Material(density=1.0, density_units='g/cm3', components={'U235': 1.0}) + cell = openmc.Cell(fill=mat, region=r1 & r2 & -zcyl) + + # Make the model complete + model = openmc.Model() + model.geometry = openmc.Geometry([cell]) + model.settings.source = openmc.IndependentSource(space=openmc.stats.Point(mid_point)) + model.settings.particles = 1000 + model.settings.batches = 10 + model.settings.inactive = 5 + + # Run the model + model.run() From 10f2b7534c44104324b2baeabc89330fc39bd9fb Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Tue, 6 Jan 2026 21:18:23 +0100 Subject: [PATCH 490/671] Fixing group names in MGXS HDF5 file (#3708) --- openmc/mgxs/mgxs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmc/mgxs/mgxs.py b/openmc/mgxs/mgxs.py index f621db092..12b8f6a75 100644 --- a/openmc/mgxs/mgxs.py +++ b/openmc/mgxs/mgxs.py @@ -1923,7 +1923,7 @@ class MGXS: # Create an HDF5 group for the subdomain if self.domain_type == 'distribcell': - group_name = ''.zfill(num_digits) + group_name = str(subdomain).zfill(num_digits) subdomain_group = domain_group.require_group(group_name) else: subdomain_group = domain_group From 7dceb1d80a1412cdf9c7a893b48ef16af379ead5 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Tue, 6 Jan 2026 23:59:38 +0100 Subject: [PATCH 491/671] closing mgxs h5py file with context manager (#3707) Co-authored-by: GuySten <62616591+GuySten@users.noreply.github.com> --- openmc/mgxs_library.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/openmc/mgxs_library.py b/openmc/mgxs_library.py index 4bc2d4a5a..414bf7cbd 100644 --- a/openmc/mgxs_library.py +++ b/openmc/mgxs_library.py @@ -2554,20 +2554,20 @@ class MGXSLibrary: "must be set") check_type('filename', filename, (str, PathLike)) - file = h5py.File(filename, 'r') + with h5py.File(filename, 'r') as file: - # Check filetype and version - check_filetype_version(file, _FILETYPE_MGXS_LIBRARY, - _VERSION_MGXS_LIBRARY) + # Check filetype and version + check_filetype_version(file, _FILETYPE_MGXS_LIBRARY, + _VERSION_MGXS_LIBRARY) - group_structure = file.attrs['group structure'] - num_delayed_groups = file.attrs['delayed_groups'] - energy_groups = openmc.mgxs.EnergyGroups(group_structure) - data = cls(energy_groups, num_delayed_groups) + group_structure = file.attrs['group structure'] + num_delayed_groups = file.attrs['delayed_groups'] + energy_groups = openmc.mgxs.EnergyGroups(group_structure) + data = cls(energy_groups, num_delayed_groups) - for group_name, group in file.items(): - data.add_xsdata(openmc.XSdata.from_hdf5(group, group_name, - energy_groups, - num_delayed_groups)) + for group_name, group in file.items(): + data.add_xsdata(openmc.XSdata.from_hdf5(group, group_name, + energy_groups, + num_delayed_groups)) return data From 830f075b5c2d5af125f5a947ed98951a9530d0a4 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 6 Jan 2026 16:59:58 -0600 Subject: [PATCH 492/671] Update documentation describing HexLattice orientation (#3709) --- docs/source/io_formats/geometry.rst | 7 ++++--- docs/source/usersguide/geometry.rst | 10 +++++----- openmc/lattice.py | 7 +++++-- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/docs/source/io_formats/geometry.rst b/docs/source/io_formats/geometry.rst index dda1efa9a..cef3bb79c 100644 --- a/docs/source/io_formats/geometry.rst +++ b/docs/source/io_formats/geometry.rst @@ -316,9 +316,10 @@ the following attributes or sub-elements: *Default*: None :orientation: - The orientation of the hexagonal lattice. The string "x" indicates that two - sides of the lattice are parallel to the x-axis, whereas the string "y" - indicates that two sides are parallel to the y-axis. + The orientation of the hexagonal lattice. The string "x" indicates that each + lattice element has two faces that are perpendicular to the x-axis, whereas + the string "y" indicates that each lattice element has two faces that are + perpendicular to the y-axis. *Default*: "y" diff --git a/docs/source/usersguide/geometry.rst b/docs/source/usersguide/geometry.rst index f51fbd73c..3224d5fff 100644 --- a/docs/source/usersguide/geometry.rst +++ b/docs/source/usersguide/geometry.rst @@ -415,11 +415,11 @@ to help figure out how to place universes:: Note that by default, hexagonal lattices are positioned such that each lattice -element has two faces that are parallel to the :math:`y` axis. As one example, -to create a three-ring lattice centered at the origin with a pitch of 10 cm -where all the lattice elements centered along the :math:`y` axis are filled with -universe ``u`` and the remainder are filled with universe ``q``, the following -code would work:: +element has two faces that are perpendicular to the :math:`y` axis. As one +example, to create a three-ring lattice centered at the origin with a pitch of +10 cm where all the lattice elements centered along the :math:`y` axis are +filled with universe ``u`` and the remainder are filled with universe ``q``, the +following code would work:: hexlat = openmc.HexLattice() hexlat.center = (0, 0) diff --git a/openmc/lattice.py b/openmc/lattice.py index f0e8b40b0..21849ec40 100644 --- a/openmc/lattice.py +++ b/openmc/lattice.py @@ -1083,8 +1083,11 @@ class HexLattice(Lattice): from the outermost ring), and i is the index with a ring starting from the top and proceeding clockwise. orientation : {'x', 'y'} - str by default 'y' orientation of main lattice diagonal another option - - 'x' + The orientation of the lattice. The 'x' orientation means that each + lattice element has two faces that are perpendicular to the x-axis, + while the 'y' orientation means that each lattice element has two faces + that are perpendicular to the y-axis. By default, the orientation is + 'y'. num_rings : int Number of radial ring positions in the xy-plane num_axial : int From dfc80c70694c238ee64206106843879042e12d6a Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Wed, 7 Jan 2026 11:54:57 +0100 Subject: [PATCH 493/671] fixing temperatures setting for mgxs (#3712) --- openmc/mgxs_library.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openmc/mgxs_library.py b/openmc/mgxs_library.py index 414bf7cbd..c1a15998e 100644 --- a/openmc/mgxs_library.py +++ b/openmc/mgxs_library.py @@ -484,7 +484,8 @@ class XSdata: check_type('temperature', temperature, Real) - temp_store = self.temperatures.tolist().append(temperature) + temp_store = self.temperatures.tolist() + temp_store.append(temperature) self.temperatures = temp_store self._total.append(None) From 551bf0730bacdb5e00ffa5f55e4e2329c448ecf0 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Fri, 9 Jan 2026 15:56:40 +0200 Subject: [PATCH 494/671] Simplify translational periodic boundary conditions (#3697) --- src/boundary_condition.cpp | 19 ++----------------- src/surface.cpp | 2 +- 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/src/boundary_condition.cpp b/src/boundary_condition.cpp index eb095d8c5..5bbda4830 100644 --- a/src/boundary_condition.cpp +++ b/src/boundary_condition.cpp @@ -129,23 +129,8 @@ TranslationalPeriodicBC::TranslationalPeriodicBC(int i_surf, int j_surf) void TranslationalPeriodicBC::handle_particle( Particle& p, const Surface& surf) const { - int i_particle_surf = p.surface_index(); - - // Figure out which of the two BC surfaces were struck then find the - // particle's new location and surface. - Position new_r; - int new_surface; - if (i_particle_surf == i_surf_) { - new_r = p.r() + translation_; - new_surface = p.surface() > 0 ? j_surf_ + 1 : -(j_surf_ + 1); - } else if (i_particle_surf == j_surf_) { - new_r = p.r() - translation_; - new_surface = p.surface() > 0 ? i_surf_ + 1 : -(i_surf_ + 1); - } else { - throw std::runtime_error( - "Called BoundaryCondition::handle_particle after " - "hitting a surface, but that surface is not recognized by the BC."); - } + auto new_r = p.r() + translation_; + int new_surface = p.surface() > 0 ? j_surf_ + 1 : -(j_surf_ + 1); // Handle the effects of the surface albedo on the particle's weight. BoundaryCondition::handle_albedo(p, surf); diff --git a/src/surface.cpp b/src/surface.cpp index 9ac1c41b4..81b756dea 100644 --- a/src/surface.cpp +++ b/src/surface.cpp @@ -1357,7 +1357,7 @@ void prepare_boundary_conditions(std::set>& periodic_pairs, // condition. Otherwise, it is a rotational periodic BC. if (std::abs(1.0 - dot_prod) < FP_PRECISION) { surf1.bc_ = make_unique(i_surf, j_surf); - surf2.bc_ = make_unique(i_surf, j_surf); + surf2.bc_ = make_unique(j_surf, i_surf); } else { // check that both normals have at least one 0 component if (std::abs(norm1.x) > FP_PRECISION && From 8c8867ea1c1ae0ffe387f657726557898ff688f5 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 9 Jan 2026 14:05:51 -0600 Subject: [PATCH 495/671] Add --merge-mode-functions=separate to gcovr call in CI (#3716) --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a9c7322e..6fc130947 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -186,6 +186,7 @@ jobs: --gcov-ignore-errors source_not_found \ --gcov-ignore-errors output_error \ --gcov-ignore-parse-errors suspicious_hits.warn \ + --merge-mode-functions=separate \ --print-summary \ --lcov -o coverage-cpp.lcov || true From 37e2feb34b5d8665d28abdc64ec18e8a2b007734 Mon Sep 17 00:00:00 2001 From: Perry <100789850+yrrepy@users.noreply.github.com> Date: Sat, 10 Jan 2026 12:04:47 -0800 Subject: [PATCH 496/671] Flux Energy Group Conversion using lethargy-weighted redistribution (#3705) Co-authored-by: GuySten <62616591+GuySten@users.noreply.github.com> --- docs/source/pythonapi/mgxs.rst | 10 ++ openmc/mgxs/__init__.py | 2 +- openmc/mgxs/groups.py | 136 +++++++++++++++++++++ tests/fns_flux_709.npy | Bin 0 -> 5800 bytes tests/unit_tests/test_mgxs_convert_flux.py | 62 ++++++++++ 5 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 tests/fns_flux_709.npy create mode 100644 tests/unit_tests/test_mgxs_convert_flux.py diff --git a/docs/source/pythonapi/mgxs.rst b/docs/source/pythonapi/mgxs.rst index 392914b36..4141aa0a0 100644 --- a/docs/source/pythonapi/mgxs.rst +++ b/docs/source/pythonapi/mgxs.rst @@ -11,6 +11,16 @@ Module Variables .. autodata:: openmc.mgxs.GROUP_STRUCTURES :annotation: +Functions ++++++++++ + +.. autosummary:: + :toctree: generated + :nosignatures: + :template: myfunction.rst + + openmc.mgxs.convert_flux_groups + Classes +++++++ diff --git a/openmc/mgxs/__init__.py b/openmc/mgxs/__init__.py index 682b5d550..5adbac9c4 100644 --- a/openmc/mgxs/__init__.py +++ b/openmc/mgxs/__init__.py @@ -1,6 +1,6 @@ import numpy as np -from openmc.mgxs.groups import EnergyGroups +from openmc.mgxs.groups import EnergyGroups, convert_flux_groups from openmc.mgxs.library import Library from openmc.mgxs.mgxs import * from openmc.mgxs.mdgxs import * diff --git a/openmc/mgxs/groups.py b/openmc/mgxs/groups.py index 182402ed7..8910c7d42 100644 --- a/openmc/mgxs/groups.py +++ b/openmc/mgxs/groups.py @@ -300,3 +300,139 @@ class EnergyGroups: # Assign merged edges to merged groups merged_groups.group_edges = list(merged_edges) return merged_groups + + +def convert_flux_groups(flux, source_groups, target_groups): + """Convert flux spectrum between energy group structures. + + Uses flux-per-unit-lethargy conservation, which assumes constant flux per + unit lethargy within each source group and distributes flux to target + groups proportionally to their lethargy width. + + .. versionadded:: 0.15.4 + + Parameters + ---------- + flux : Iterable of float + Flux values for source groups. Length must equal + source_groups.num_groups. + source_groups : EnergyGroups or str + Energy group structure of the input flux with boundaries in [eV]. + Can be an EnergyGroups instance or the name of a group structure + (e.g., 'CCFE-709'). + target_groups : EnergyGroups or str + Target energy group structure with boundaries in [eV]. Can be an + EnergyGroups instance or the name of a group structure + (e.g., 'UKAEA-1102'). + + Returns + ------- + numpy.ndarray + Flux values for target groups. Total flux is conserved for + overlapping energy regions. + + Raises + ------ + TypeError + If source_groups or target_groups is not EnergyGroups or str + ValueError + If flux length doesn't match source_groups, or flux contains + negative, NaN, or infinite values + + See Also + -------- + EnergyGroups : Energy group structure class + + Notes + ----- + The assumption of constant flux per unit lethargy within each source + group is physically reasonable for most reactor spectra but is not + exact. For best accuracy, use source spectra with sufficiently fine + energy resolution. + + Examples + -------- + Convert FNS 709-group flux to UKAEA-1102 structure: + + >>> import numpy as np + >>> flux_709 = np.load('tests/fns_flux_709.npy') + >>> flux_1102 = openmc.mgxs.convert_flux_groups(flux_709, 'CCFE-709', 'UKAEA-1102') + + Convert using EnergyGroups instances: + + >>> source = openmc.mgxs.EnergyGroups([1.0, 10.0, 100.0]) + >>> target = openmc.mgxs.EnergyGroups([1.0, 5.0, 10.0, 50.0, 100.0]) + >>> flux_target = openmc.mgxs.convert_flux_groups([1e8, 2e8], source, target) + + References + ---------- + .. [1] J. J. Duderstadt and L. J. Hamilton, "Nuclear Reactor Analysis," + John Wiley & Sons, 1976. + .. [2] M. Fleming and J.-Ch. Sublet, "FISPACT-II User Manual," + UKAEA-R(18)001, UK Atomic Energy Authority, 2018. See GRPCONVERT keyword. + + """ + # Handle string group structure names + if isinstance(source_groups, str): + source_groups = EnergyGroups(source_groups) + if isinstance(target_groups, str): + target_groups = EnergyGroups(target_groups) + + # Type validation + cv.check_type('source_groups', source_groups, EnergyGroups) + cv.check_type('target_groups', target_groups, EnergyGroups) + + # Convert flux to numpy array + flux = np.asarray(flux, dtype=np.float64) + if flux.ndim != 1: + raise ValueError(f'flux must be 1-dimensional, got shape {flux.shape}') + + # Validate flux length matches source groups + if len(flux) != source_groups.num_groups: + raise ValueError( + f'Length of flux ({len(flux)}) must equal number of source ' + f'groups ({source_groups.num_groups})' + ) + + # Check for invalid flux values + if np.any(np.isnan(flux)): + raise ValueError('flux contains NaN values') + if np.any(np.isinf(flux)): + raise ValueError('flux contains infinite values') + if np.any(flux < 0): + raise ValueError('flux values must be non-negative') + + # Get energy edges + source_edges = source_groups.group_edges + target_edges = target_groups.group_edges + num_target = target_groups.num_groups + + # Initialize output array + flux_target = np.zeros(num_target) + + # Main conversion loop: distribute flux using lethargy weighting + for idx_src, flux_src in enumerate(flux): + if flux_src == 0: + continue + + e_low_src = source_edges[idx_src] + e_high_src = source_edges[idx_src + 1] + lethargy_src = np.log(e_high_src / e_low_src) + + for idx_tgt in range(num_target): + e_low_tgt = target_edges[idx_tgt] + e_high_tgt = target_edges[idx_tgt + 1] + + # Skip non-overlapping groups + if e_high_tgt <= e_low_src or e_low_tgt >= e_high_src: + continue + + # Calculate overlap region + e_low_overlap = max(e_low_src, e_low_tgt) + e_high_overlap = min(e_high_src, e_high_tgt) + lethargy_overlap = np.log(e_high_overlap / e_low_overlap) + + # Distribute flux proportionally to lethargy fraction + flux_target[idx_tgt] += flux_src * (lethargy_overlap / lethargy_src) + + return flux_target diff --git a/tests/fns_flux_709.npy b/tests/fns_flux_709.npy new file mode 100644 index 0000000000000000000000000000000000000000..62c6128422fd8d0014533727b0eee96d369c18a0 GIT binary patch literal 5800 zcmeIve@v8h90%|x1;f1L5-pa^&Ia8X7oMhw4C|4E0V*CKpyCfmzyk>g&@d4mjfe;u zi!$fT9boFRA3(FL%$PrNX&Qne3hB)>lqMhz#GmE8-_QNQP41ZgG`G8dUVDE&@B7|! z-={n*YOa3aBf@H7tvVqwYk8*HPo?%x@>Oe9>ZJ6{?9BMIW$Bp-iRO7wd}>zWy?K@) zJ|pp7)=bk)*G|x?)~O!&^O)fHIBvjk1CASbm~J5Cu!zT%m8`O0XKvX$?0=6BdLNSE zDBg!fA-R9HH@EB`8b6T#-}2x1zwLSF2zyz!6)NO&bB?obN1^g}_Udh0H|zv^YZ`33 zLEaahX3vhIZxeg7PT5rOW=~w&!k!)L3YytVk)LA##nx8z;)Y!sltR*<=!qUwC2PqR z$(2j;phpiw7TW~S?4+ey{<|H&mBPh4(^L;mFU^C ze%}*3w_p_PGf`!-8?s9k4Sr=b_Y$+NfZ1YEHNPjWrurLW(63wc6jWAFsinWBqs$qf z4bGgm-Em&o4r;K{GT)T{G1Zt)&H7o7X#39#ir@MpW zk(a1n!#;Z}6ju2x$9!5F-8(%2`3DodoS%ri{<$RBdxU;;cLlPys{x)1q@gv``4-)I zC>i^b%_%VTR~mVNmgcX-y!1AWDWYCJshG#^qQ!ft>xkDe_Ya`6^XM_xG|cZ5Gb?5> z2d<$Zp7hC1?sH?AeK#_1%VTceLcboFj`Q68Y4kg^bw910kYV0ShH~mP6$%sU`Te=U z>>VqFQfoAOXWZ%TaqM3^`38QcIVBUOML?;%19Qn^KljBqsqs_xHf+g4-z{Mkzn_5j zCvA4(dt6pZKbg<>7}vwB)H1&}lzM;8`M7n=g#py(qSyB|W6%fT9Rc6=N+@W0)_f9fba_5c6? literal 0 HcmV?d00001 diff --git a/tests/unit_tests/test_mgxs_convert_flux.py b/tests/unit_tests/test_mgxs_convert_flux.py new file mode 100644 index 000000000..e235f88e0 --- /dev/null +++ b/tests/unit_tests/test_mgxs_convert_flux.py @@ -0,0 +1,62 @@ +"""Tests for openmc.mgxs.convert_flux_groups function.""" + +import numpy as np +import pytest +from pytest import approx + +import openmc.mgxs + + +def test_coarse_to_fine(): + """Test coarse to fine conversion with flux conservation.""" + source = openmc.mgxs.EnergyGroups([1.0, 10.0, 100.0]) + target = openmc.mgxs.EnergyGroups([1.0, 5.0, 10.0, 50.0, 100.0]) + flux_source = np.array([1e8, 2e8]) + + flux_target = openmc.mgxs.convert_flux_groups(flux_source, source, target) + + # Check conservation + assert np.sum(flux_target) == approx(np.sum(flux_source)) + assert len(flux_target) == 4 + assert np.all(flux_target >= 0) + + +def test_fine_to_coarse(): + """Test fine to coarse conversion (reverse direction).""" + source = openmc.mgxs.EnergyGroups([1.0, 5.0, 10.0, 50.0, 100.0]) + target = openmc.mgxs.EnergyGroups([1.0, 10.0, 100.0]) + flux_source = np.array([1e7, 2e7, 3e7, 4e7]) + + flux_target = openmc.mgxs.convert_flux_groups(flux_source, source, target) + + assert np.sum(flux_target) == approx(np.sum(flux_source)) + assert len(flux_target) == 2 + + +def test_lethargy_distribution(): + """Test that flux is distributed by lethargy, not linear energy.""" + # Single group from 1 to 100 eV + source = openmc.mgxs.EnergyGroups([1.0, 100.0]) + # Split into two groups: 1-10 eV and 10-100 eV + target = openmc.mgxs.EnergyGroups([1.0, 10.0, 100.0]) + flux_source = np.array([1e8]) + + flux_target = openmc.mgxs.convert_flux_groups(flux_source, source, target) + + # Each target group spans one decade (ln(10) lethargy each) + # So flux should be split 50/50 by lethargy + assert flux_target[0] == approx(5e7) + assert flux_target[1] == approx(5e7) + + +def test_fns_ccfe709_to_ukaea1102(): + """Test CCFE-709 to UKAEA-1102 conversion with real FNS flux spectrum.""" + from pathlib import Path + flux_file = Path(__file__).parent.parent / 'fns_flux_709.npy' + fns_flux_709 = np.load(flux_file) + + flux_1102 = openmc.mgxs.convert_flux_groups(fns_flux_709, 'CCFE-709', 'UKAEA-1102') + + assert len(flux_1102) == 1102 + assert np.sum(flux_1102) == approx(np.sum(fns_flux_709), rel=1e-10) + assert np.all(flux_1102 >= 0) From 0486e433d2c362253e2be27ffdaa8486376b518a Mon Sep 17 00:00:00 2001 From: Jack Fletcher <115663563+j-fletcher@users.noreply.github.com> Date: Mon, 12 Jan 2026 10:51:12 -0500 Subject: [PATCH 497/671] Source biasing capabilities (#3460) Co-authored-by: Paul Romano --- docs/source/io_formats/settings.rst | 34 +- docs/source/methods/variance_reduction.rst | 82 ++- docs/source/usersguide/settings.rst | 6 + docs/source/usersguide/variance_reduction.rst | 155 ++++- include/openmc/distribution.h | 188 ++++-- include/openmc/distribution_multi.h | 43 +- include/openmc/distribution_spatial.h | 37 +- include/openmc/source.h | 4 +- openmc/filter.py | 2 +- openmc/settings.py | 9 +- openmc/stats/multivariate.py | 142 ++++- openmc/stats/univariate.py | 574 ++++++++++++++++-- src/chain.cpp | 4 +- src/distribution.cpp | 288 +++++++-- src/distribution_angle.cpp | 2 +- src/distribution_multi.cpp | 69 ++- src/distribution_spatial.cpp | 106 +++- src/secondary_correlated.cpp | 4 +- src/source.cpp | 24 +- tests/cpp_unit_tests/test_distribution.cpp | 4 +- .../regression_tests/source/results_true.dat | 2 +- tests/unit_tests/__init__.py | 9 + tests/unit_tests/test_source_biasing.py | 276 +++++++++ tests/unit_tests/test_stats.py | 191 +++++- 24 files changed, 2003 insertions(+), 252 deletions(-) create mode 100644 tests/unit_tests/test_source_biasing.py diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index b7874fcf2..b986a526a 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -844,13 +844,18 @@ attributes/sub-elements: relative source strength of each mesh element or each point in the cloud. :volume_normalized: - For "mesh" spatial distrubtions, this optional boolean element specifies + For "mesh" spatial distributions, this optional boolean element specifies whether the vector of relative strengths should be multiplied by the mesh element volume. This is most common if the strengths represent a source per unit volume. *Default*: false + :bias: + For "mesh" and "cloud" spatial distributions, this optional element + specifies floating point values corresponding to alternative probabilities + for each value/component to use for biased sampling. + :angle: An element specifying the angular distribution of source sites. This element has the following attributes: @@ -883,6 +888,10 @@ attributes/sub-elements: are those of a univariate probability distribution (see the description in :ref:`univariate`). + :bias: + For "isotropic" angular distributions, this optional element specifies a + "mu-phi" angular distribution used for biased sampling. + :energy: An element specifying the energy distribution of source sites. The necessary sub-elements/attributes are those of a univariate probability distribution @@ -906,6 +915,10 @@ attributes/sub-elements: mesh element and follows the format for :ref:`source_element`. The number of ```` sub-elements should correspond to the number of mesh elements. + .. note:: Biased sampling can be applied to the spatial and energy distributions + of a source by using the ```` sub-element (see + :ref:`univariate` for details on how to specify bias distributions). + :constraints: This sub-element indicates the presence of constraints on sampled source sites (see :ref:`usersguide_source_constraints` for details). It may have @@ -998,13 +1011,26 @@ variable and whose sub-elements/attributes are as follows: *Default*: histogram :pair: - For a "mixture" distribution, this element provides a distribution and its corresponding probability. + For a "mixture" distribution, this element provides a distribution and its + corresponding probability. :probability: - An attribute or ``pair`` that provides the probability of a univariate distribution within a "mixture" distribution. + An attribute or ``pair`` that provides the probability of a univariate + distribution within a "mixture" distribution. :dist: - This sub-element of a ``pair`` element provides information on the corresponding univariate distribution. + This sub-element of a ``pair`` element provides information on the + corresponding univariate distribution. + +:bias: + This optional element specifies a biased distribution for importance sampling. + For continuous distributions, the ``bias`` element should contain another + univariate distribution with the same support (interval) as the parent + distribution. For discrete distributions, the ``bias`` element should contain + floating point values corresponding to alternative probabilities for each + value/component to be used for biased sampling. + + *Default*: None --------------------------------------- ```` Element diff --git a/docs/source/methods/variance_reduction.rst b/docs/source/methods/variance_reduction.rst index 353ae5077..cdda5ea92 100644 --- a/docs/source/methods/variance_reduction.rst +++ b/docs/source/methods/variance_reduction.rst @@ -22,12 +22,14 @@ not experience a single scoring event, even after billions of analog histories. Variance reduction techniques aim to either flatten the global uncertainty distribution, such that all regions of phase space have a fairly similar uncertainty, or to reduce the uncertainty in specific locations (such as a -detector). There are two strategies available in OpenMC for variance reduction: -the Monte Carlo MAGIC method and the FW-CADIS method. Both strategies work by -developing a weight window mesh that can be utilized by subsequent Monte Carlo -solves to split particles heading towards areas of lower flux densities while -terminating particles in higher flux regions---all while maintaining a fair -game. +detector). There are three strategies available in OpenMC for variance +reduction: weight windows generated via the MAGIC method or the FW-CADIS method, +and source biasing. Both weight windowing strategies work by developing a mesh +that can be utilized by subsequent Monte Carlo solves to split particles heading +towards areas of lower flux densities while terminating particles in higher flux +regions. In contrast, source biasing modifies source site sampling behavior to +preferentially track particles more likely to reach phase space regions of +interest. ------------ MAGIC Method @@ -132,3 +134,71 @@ aware of this. :label: variance_fom \text{FOM} = \frac{1}{\text{Time} \times \sigma^2} + +.. _methods_source_biasing: + +-------------- +Source Biasing +-------------- + +In contrast to the previous two methods that introduce population controls +during transport, source biasing modifies the sampling of the external source +distribution. The basic premise of the technique is that for each spatial, +angular, energy, or time distribution of a source, an additional distribution +can be specified provided that the two share a common support (set of points +where the distribution is nonzero). Samples are then drawn from this "bias" +distribution, which can be chosen to preferentially direct particles towards +phase space regions of interest. In order to avoid biasing the tally results, +however, a weight adjustment is applied to each sampled site as described below. + +Assume that the unbiased probability density function of a random variable +:math:`X:x \rightarrow \mathbb{R}` is given by :math:`f(x)`, but that using the +biased distribution :math:`g(x)` will result in a greater number of particle +trajectories reaching some phase space region of interest. Then a sample +:math:`x_0` may be drawn from :math:`g(x)` while maintaining a fair game, +provided that its weight is adjusted as: + +.. math:: + :label: source_bias + + w = w_0 \times \frac{f(x_0)}{g(x_0)} + +where :math:`w_0` is the weight of an unbiased sample from :math:`f(x)`, +typically unity. + +Returning now to Equation :eq:`source_bias`, the requirement for common support +becomes evident. If :math:`\mathrm{supp} (g)` fully contains but is not +identical to :math:`\mathrm{supp} (f)`, then some samples from :math:`g(x)` will +correspond to points where :math:`f(x) = 0`. Thus these source sites would be +assigned a starting weight of 0, meaning the particles would be killed +immediately upon transport, effectively wasting computation time. Conversely, if +:math:`\mathrm{supp} (g)` is fully contained by but not identical to +:math:`\mathrm{supp} (f)`, the contributions of some regions outside +:math:`\mathrm{supp} (g)` will not be counted towards the integral, potentially +biasing the tally. The weight assigned to such points would be undefined since +:math:`g(x) = \mathbf{0}` at these points. + +When an independent source is sampled in OpenMC, the particle's coordinate in +each variable of phase space :math:`(\mathbf{r},\mathbf{\Omega},E,t)` is +successively drawn from an independent probability distribution. Multiple +variables can be biased, in which case the resultant weight :math:`w` applied to +the particle is the product of the weights assigned from all sampled +distributions: space, angle, energy, and time, as shown in Equation +:eq:`tot_wgt`. + +.. math:: + :label: tot_wgt + + w = w_r \times w_{\Omega} \times w_E \times w_t + +Finally, source biasing and weight windows serve different purposes. Source +biasing changes how particles are born, allowing the initial source sites to be +sampled preferentially from important regions of phase space (space, angle, +energy, and time) with an accompanying weight adjustment. Weight windows, by +contrast, apply population control during transport (splitting and Russian +roulette) to help particles reach and contribute in important regions as they +move through the system. Because particle transport proceeds as usual after a +biased source is sampled, particle attenuation in optically thick regions +outside the source volume will not be affected by source biasing; in such +scenarios, transport biasing techniques such as weight windows are often more +effective. diff --git a/docs/source/usersguide/settings.rst b/docs/source/usersguide/settings.rst index f973f1465..b9c693ba1 100644 --- a/docs/source/usersguide/settings.rst +++ b/docs/source/usersguide/settings.rst @@ -272,6 +272,12 @@ option:: settings.source = [src1, src2] settings.uniform_source_sampling = True +Additionally, sampling from an :class:`openmc.IndependentSource` may be biased +for local or global variance reduction by modifying the +:attr:`~openmc.IndependentSource.bias` attribute of each of its four main +distributions. Further discussion of source biasing can be found in +:ref:`source_biasing`. + Finally, the :attr:`IndependentSource.particle` attribute can be used to indicate the source should be composed of particles other than neutrons. For example, the following would generate a photon source:: diff --git a/docs/source/usersguide/variance_reduction.rst b/docs/source/usersguide/variance_reduction.rst index 93321e107..8d41807e1 100644 --- a/docs/source/usersguide/variance_reduction.rst +++ b/docs/source/usersguide/variance_reduction.rst @@ -5,10 +5,12 @@ Variance Reduction ================== Global variance reduction in OpenMC is accomplished by weight windowing -techniques. OpenMC is capable of generating weight windows using either the -MAGIC or FW-CADIS methods. Both techniques will produce a ``weight_windows.h5`` -file that can be loaded and used later on. In this section, we break down the -steps required to both generate and then apply weight windows. +or source biasing techniques, the latter of which additionally provides a +local variance reduction capability. OpenMC is capable of generating weight +windows using either the MAGIC or FW-CADIS methods. Both techniques will +produce a ``weight_windows.h5`` file that can be loaded and used later on. In +this section, we first break down the steps required to generate and apply +weight windows, then describe how source biasing may be applied. .. _ww_generator: @@ -172,3 +174,148 @@ Weight window mesh information is embedded into the weight window file, so the mesh does not need to be redefined. Monte Carlo solves that load a weight window file as above will utilize weight windows to reduce the variance of the simulation. + +.. _source_biasing: + +-------------- +Source Biasing +-------------- + +In fixed source problems, source biasing provides a means to reduce the variance +on global or localized responses, depending on the biasing scheme. In either +case, the premise of the method is to sample source sites from a biased +distribution that directs a larger fraction of the simulated histories towards +phase space regions of interest than would be found there under analog sampling. +In order to preserve an unbiased estimate of the tally mean, the weight of these +with analog sampling, divided by the probability assigned by the biased +distribution. While the assignment of statistical weights is outlined in the +:ref:`methods section `, this section demonstrates the +implementation of source biasing to problems in OpenMC. + +Source biasing in OpenMC is accomplished by applying a distribution to the +:attr:`bias` attribute of one or more of the univariate or independent +multivariate distributions which make up an :class:`~openmc.IndependentSource` +instance as follows:: + + # First create the biased distribution + biased_dist = openmc.stats.PowerLaw(a=0, b=3, n=3) + + # Construct a new distribution with the bias applied + dist = openmc.stats.PowerLaw(a=0, b=3, n=2, bias=biased_dist) + + # The bias attribute can also be set on an existing "analog" distribution: + sphere_dist = openmc.stats.spherical_uniform(r_outer=3) + sphere_dist.r.bias = biased_dist + +Univariate distributions may be sampled via the Python API, returning the +sample(s) along with the associated weight(s):: + + sample_vec, wgt_vec = dist.sample(n_samples=100) + +Here, if the distribution is unbiased, the weight of each sample will be unity. +Finally, :class:`~openmc.IndependentSource` instances can be constructed with +biased distributions:: + + # Create a source with a biased spatial distribution + source = openmc.IndependentSource(space=sphere_dist) + +During the simulation, source sites are then sampled using the biased +distributions where available and given starting statistical weights +corresponding to the cumulative product of the weights assigned by each +distribution in the source object. Hence multiple source variables (e.g., +direction and energy) may be biased and the resulting source sites will have +their weights adjusted accordingly. + +.. note:: + Combining source biasing with weight windows can be a powerful variance + reduction technique if each is constructed appropriately for the response + of interest. For example, if a source biasing scheme is devised for + variance reduction of a specific localized response, the user may be able + to specify their own weight window structure that results in more efficient + transport than if weight windows were generated by either of OpenMC's + automatic weight window generators, which are intended for global variance + reduction. + +Biased distributions that could result in degenerate weight mappings are not +recommended; this is most commonly seen when biasing the :math:`\phi`-coordinate +of spherical or cylindrical independent multivariate distributions. In such +cases degenerate behavior will be observed at the pole about which :math:`\phi` +is measured, with all values of :math:`\phi` (hence many possible statistical +weights) mapping to the same point for :math:`r=0` or :math:`\mu=0`, and large +weight gradients in the vicinity. In most cases requiring a spherical +independent source, it would be preferable to reorient the reference vector of +the distribution such that biasing could be applied to the +:math:`\mu`-coordinate instead. + +When biasing a distribution, care should also be taken to ensure that both the +unbiased and biased distribution share a common support---that is, every region +of phase space mapped to a nonzero probability density by the unbiased +distribution should likewise map to nonzero probability under the biased +distribution, and vice versa. In OpenMC, this places restrictions on the set of +compatible distributions that may be used to bias sampling of each distribution +type. The following table summarizes the method for each distribution in OpenMC +that permits biased sampling. + +.. list-table:: **Distributions that support biased sampling** + :header-rows: 1 + :widths: 35 65 + + * - Discrete Univariate PDFs + - Biasing Method + * - :class:`openmc.stats.Discrete` + - Apply a vector of alternative probabilities to the :attr:`bias` + attribute + +.. list-table:: + :header-rows: 1 + :widths: 35 65 + + * - Continuous Univariate PDFs + - Biasing Method + * - :class:`openmc.stats.Uniform`, + :class:`openmc.stats.PowerLaw`, + :class:`openmc.stats.Maxwell`, + :class:`openmc.stats.Watt`, + :class:`openmc.stats.Normal`, + :class:`openmc.stats.Tabular` + - Apply a second, unbiased continous univariate PDF to the :attr:`bias` + attribute, ensuring that the :attr:`support` attribute of each + distribution is the same + +.. list-table:: + :header-rows: 1 + :widths: 35 65 + + * - Mixed Univariate PDFs + - Biasing Method + * - :class:`openmc.stats.Mixture` + - May be constructed from multiple biased univariate distributions, or a + second, unbiased continous univariate PDF may be applied to the + :attr:`bias` attribute + +.. list-table:: + :header-rows: 1 + :widths: 35 65 + + * - Discrete Multivariate PDFs + - Biasing Method + * - :class:`openmc.stats.PointCloud`, + :class:`openmc.stats.MeshSpatial` + - Apply a vector of the new relative probabilities of each point or mesh + element under biased sampling to the :attr:`bias` attribute + +.. list-table:: + :header-rows: 1 + :widths: 35 65 + + * - Continuous Multivariate PDFs + - Biasing Method + * - :class:`openmc.stats.CartesianIndependent`, + :class:`openmc.stats.CylindricalIndependent`, + :class:`openmc.stats.SphericalIndependent`, + :class:`openmc.stats.PolarAzimuthal` + - Construct from biased univariate distributions for :attr:`x`, :attr:`y`, + :attr:`z`, etc. + * - :class:`openmc.stats.Isotropic` + - Apply an unbiased :class:`openmc.stats.PolarAzimuthal` to the + :attr:`bias` attribute diff --git a/include/openmc/distribution.h b/include/openmc/distribution.h index 854cf7d77..80fe70bae 100644 --- a/include/openmc/distribution.h +++ b/include/openmc/distribution.h @@ -15,6 +15,17 @@ namespace openmc { +//============================================================================== +// Helper function for computing importance weights from biased sampling +//============================================================================== + +//! Compute importance weights for biased sampling +//! \param p Unnormalized original probability vector +//! \param b Unnormalized bias probability vector +//! \return Vector of importance weights (p_norm[i] / b_norm[i]) +vector compute_importance_weights( + const vector& p, const vector& b); + //============================================================================== //! Abstract class representing a univariate probability distribution //============================================================================== @@ -22,11 +33,41 @@ namespace openmc { class Distribution { public: virtual ~Distribution() = default; - virtual double sample(uint64_t* seed) const = 0; + + //! Sample a value from the distribution, handling biasing automatically + //! \param seed Pseudorandom number seed pointer + //! \return (sampled value, importance weight) + virtual std::pair sample(uint64_t* seed) const; + + //! Evaluate probability density, f(x), at a point + //! \param x Point to evaluate f(x) + //! \return f(x) + virtual double evaluate(double x) const; //! Return integral of distribution //! \return Integral of distribution virtual double integral() const { return 1.0; }; + + //! Set bias distribution + virtual void set_bias(std::unique_ptr bias) + { + bias_ = std::move(bias); + } + + const Distribution* bias() const { return bias_.get(); } + +protected: + //! Sample a value (unbiased) from the distribution + //! \param seed Pseudorandom number seed pointer + //! \return Sampled value + virtual double sample_unbiased(uint64_t* seed) const = 0; + + //! Read bias distribution from XML + //! \param node XML node that may contain a bias child element + void read_bias_from_xml(pugi::xml_node node); + + // Biasing distribution + unique_ptr bias_; }; using UPtrDist = unique_ptr; @@ -50,7 +91,7 @@ public: //! Sample a value from the distribution //! \param seed Pseudorandom number seed pointer - //! \return Sampled value + //! \return Sampled index size_t sample(uint64_t* seed) const; // Properties @@ -67,7 +108,7 @@ private: //! Normalize distribution so that probabilities sum to unity void normalize(); - //! Initialize alias tables for distribution + //! Initialize alias table for sampling void init_alias(); }; @@ -82,20 +123,30 @@ public: //! Sample a value from the distribution //! \param seed Pseudorandom number seed pointer - //! \return Sampled value - double sample(uint64_t* seed) const override; + //! \return (sampled value, sample weight) + std::pair sample(uint64_t* seed) const override; double integral() const override { return di_.integral(); }; + //! Override set_bias as no-op (bias handled in constructor) + void set_bias(std::unique_ptr bias) override {} + // Properties const vector& x() const { return x_; } const vector& prob() const { return di_.prob(); } const vector& alias() const { return di_.alias(); } + const vector& weight() const { return weight_; } + +protected: + //! Sample a value (unbiased) from the distribution + //! \param seed Pseudorandom number seed pointer + //! \return Sampled value + double sample_unbiased(uint64_t* seed) const override; private: - vector x_; //!< Possible outcomes - DiscreteIndex di_; //!< discrete probability distribution of - //!< outcome indices + vector x_; //!< Possible outcomes + vector weight_; //!< Importance weights (empty if unbiased) + DiscreteIndex di_; //!< Discrete probability distribution of outcome indices }; //============================================================================== @@ -107,14 +158,20 @@ public: explicit Uniform(pugi::xml_node node); Uniform(double a, double b) : a_ {a}, b_ {b} {}; - //! Sample a value from the distribution - //! \param seed Pseudorandom number seed pointer - //! \return Sampled value - double sample(uint64_t* seed) const override; + //! Evaluate probability density, f(x), at a point + //! \param x Point to evaluate f(x) + //! \return f(x) + double evaluate(double x) const override; double a() const { return a_; } double b() const { return b_; } +protected: + //! Sample a value (unbiased) from the distribution + //! \param seed Pseudorandom number seed pointer + //! \return Sampled value + double sample_unbiased(uint64_t* seed) const override; + private: double a_; //!< Lower bound of distribution double b_; //!< Upper bound of distribution @@ -131,15 +188,21 @@ public: : offset_ {std::pow(a, n + 1)}, span_ {std::pow(b, n + 1) - offset_}, ninv_ {1 / (n + 1)} {}; - //! Sample a value from the distribution - //! \param seed Pseudorandom number seed pointer - //! \return Sampled value - double sample(uint64_t* seed) const override; + //! Evaluate probability density, f(x), at a point + //! \param x Point to evaluate f(x) + //! \return f(x) + double evaluate(double x) const override; double a() const { return std::pow(offset_, ninv_); } double b() const { return std::pow(offset_ + span_, ninv_); } double n() const { return 1 / ninv_ - 1; } +protected: + //! Sample a value (unbiased) from the distribution + //! \param seed Pseudorandom number seed pointer + //! \return Sampled value + double sample_unbiased(uint64_t* seed) const override; + private: //! Store processed values in object to allow for faster sampling double offset_; //!< a^(n+1) @@ -156,13 +219,19 @@ public: explicit Maxwell(pugi::xml_node node); Maxwell(double theta) : theta_ {theta} {}; - //! Sample a value from the distribution - //! \param seed Pseudorandom number seed pointer - //! \return Sampled value - double sample(uint64_t* seed) const override; + //! Evaluate probability density, f(x), at a point + //! \param x Point to evaluate f(x) + //! \return f(x) + double evaluate(double x) const override; double theta() const { return theta_; } +protected: + //! Sample a value (unbiased) from the distribution + //! \param seed Pseudorandom number seed pointer + //! \return Sampled value + double sample_unbiased(uint64_t* seed) const override; + private: double theta_; //!< Factor in exponential [eV] }; @@ -176,14 +245,20 @@ public: explicit Watt(pugi::xml_node node); Watt(double a, double b) : a_ {a}, b_ {b} {}; - //! Sample a value from the distribution - //! \param seed Pseudorandom number seed pointer - //! \return Sampled value - double sample(uint64_t* seed) const override; + //! Evaluate probability density, f(x), at a point + //! \param x Point to evaluate f(x) + //! \return f(x) + double evaluate(double x) const override; double a() const { return a_; } double b() const { return b_; } +protected: + //! Sample a value (unbiased) from the distribution + //! \param seed Pseudorandom number seed pointer + //! \return Sampled value + double sample_unbiased(uint64_t* seed) const override; + private: double a_; //!< Factor in exponential [eV] double b_; //!< Factor in square root [1/eV] @@ -200,14 +275,20 @@ public: Normal(double mean_value, double std_dev) : mean_value_ {mean_value}, std_dev_ {std_dev} {}; - //! Sample a value from the distribution - //! \param seed Pseudorandom number seed pointer - //! \return Sampled value - double sample(uint64_t* seed) const override; + //! Evaluate probability density, f(x), at a point + //! \param x Point to evaluate f(x) + //! \return f(x) + double evaluate(double x) const override; double mean_value() const { return mean_value_; } double std_dev() const { return std_dev_; } +protected: + //! Sample a value (unbiased) from the distribution + //! \param seed Pseudorandom number seed pointer + //! \return Sampled value + double sample_unbiased(uint64_t* seed) const override; + private: double mean_value_; //!< middle of distribution [eV] double std_dev_; //!< standard deviation [eV] @@ -223,10 +304,10 @@ public: Tabular(const double* x, const double* p, int n, Interpolation interp, const double* c = nullptr); - //! Sample a value from the distribution - //! \param seed Pseudorandom number seed pointer - //! \return Sampled value - double sample(uint64_t* seed) const override; + //! Evaluate probability density, f(x), at a point + //! \param x Point to evaluate f(x) + //! \return f(x) + double evaluate(double x) const override; // properties vector& x() { return x_; } @@ -235,6 +316,12 @@ public: Interpolation interp() const { return interp_; } double integral() const override { return integral_; }; +protected: + //! Sample a value (unbiased) from the distribution + //! \param seed Pseudorandom number seed pointer + //! \return Sampled value + double sample_unbiased(uint64_t* seed) const override; + private: vector x_; //!< tabulated independent variable vector p_; //!< tabulated probability density @@ -259,13 +346,19 @@ public: explicit Equiprobable(pugi::xml_node node); Equiprobable(const double* x, int n) : x_ {x, x + n} {}; - //! Sample a value from the distribution - //! \param seed Pseudorandom number seed pointer - //! \return Sampled value - double sample(uint64_t* seed) const override; + //! Evaluate probability density, f(x), at a point + //! \param x Point to evaluate f(x) + //! \return f(x) + double evaluate(double x) const override; const vector& x() const { return x_; } +protected: + //! Sample a value (unbiased) from the distribution + //! \param seed Pseudorandom number seed pointer + //! \return Sampled value + double sample_unbiased(uint64_t* seed) const override; + private: vector x_; //! Possible outcomes }; @@ -280,18 +373,25 @@ public: //! Sample a value from the distribution //! \param seed Pseudorandom number seed pointer - //! \return Sampled value - double sample(uint64_t* seed) const override; + //! \return (sampled value, sample weight) + std::pair sample(uint64_t* seed) const override; double integral() const override { return integral_; } -private: - // Storrage for probability + distribution - using DistPair = std::pair; + //! Override set_bias as no-op (bias handled in constructor) + void set_bias(std::unique_ptr bias) override {} - vector - distribution_; //!< sub-distributions + cummulative probabilities - double integral_; //!< integral of distribution +protected: + //! Sample a value (unbiased) from the distribution + //! \param seed Pseudorandom number seed pointer + //! \return Sampled value + double sample_unbiased(uint64_t* seed) const override; + +private: + vector distribution_; //!< Sub-distributions + vector weight_; //!< Importance weights for component selection + DiscreteIndex di_; //!< Discrete probability distribution of indices + double integral_; //!< Integral of distribution }; } // namespace openmc diff --git a/include/openmc/distribution_multi.h b/include/openmc/distribution_multi.h index 75126593f..7b9c2abf8 100644 --- a/include/openmc/distribution_multi.h +++ b/include/openmc/distribution_multi.h @@ -26,8 +26,8 @@ public: //! Sample a direction from the distribution //! \param seed Pseudorandom number seed pointer - //! \return Direction sampled - virtual Direction sample(uint64_t* seed) const = 0; + //! \return (sampled Direction, sample weight) + virtual std::pair sample(uint64_t* seed) const = 0; Direction u_ref_ {0.0, 0.0, 1.0}; //!< reference direction }; @@ -43,14 +43,28 @@ public: //! Sample a direction from the distribution //! \param seed Pseudorandom number seed pointer - //! \return Direction sampled - Direction sample(uint64_t* seed) const override; + //! \return (sampled Direction, sample weight) + std::pair sample(uint64_t* seed) const override; + + //! Sample a direction and return evaluation of the PDF for biased sampling. + //! Note that bias distributions are intended to return unit-weight samples. + //! \param seed Pseudorandom number seed points + //! \return (sampled Direction, value of the PDF at this Direction) + std::pair sample_as_bias(uint64_t* seed) const; // Observing pointers Distribution* mu() const { return mu_.get(); } Distribution* phi() const { return phi_.get(); } private: + //! Common sampling implementation + //! \param seed Pseudorandom number seed pointer + //! \param return_pdf If true, return PDF evaluation; if false, return + //! importance weight + //! \return (sampled Direction, weight or PDF value) + std::pair sample_impl( + uint64_t* seed, bool return_pdf) const; + Direction v_ref_ {1.0, 0.0, 0.0}; //!< reference direction Direction w_ref_; UPtrDist mu_; //!< Distribution of polar angle @@ -66,11 +80,24 @@ Direction isotropic_direction(uint64_t* seed); class Isotropic : public UnitSphereDistribution { public: Isotropic() {}; + explicit Isotropic(pugi::xml_node node); //! Sample a direction from the distribution //! \param seed Pseudorandom number seed pointer - //! \return Sampled direction - Direction sample(uint64_t* seed) const override; + //! \return (sampled direction, sample weight) + std::pair sample(uint64_t* seed) const override; + + // Set or get bias distribution + void set_bias(std::unique_ptr bias) + { + bias_ = std::move(bias); + } + + const PolarAzimuthal* bias() const { return bias_.get(); } + +protected: + // Biasing distribution + unique_ptr bias_; }; //============================================================================== @@ -85,8 +112,8 @@ public: //! Sample a direction from the distribution //! \param seed Pseudorandom number seed pointer - //! \return Sampled direction - Direction sample(uint64_t* seed) const override; + //! \return (sampled direction, sample weight) + std::pair sample(uint64_t* seed) const override; }; using UPtrAngle = unique_ptr; diff --git a/include/openmc/distribution_spatial.h b/include/openmc/distribution_spatial.h index 9c3bc743f..26f106fca 100644 --- a/include/openmc/distribution_spatial.h +++ b/include/openmc/distribution_spatial.h @@ -19,7 +19,9 @@ public: virtual ~SpatialDistribution() = default; //! Sample a position from the distribution - virtual Position sample(uint64_t* seed) const = 0; + //! \param seed Pseudorandom number seed pointer + //! \return Sampled (position, importance weight) + virtual std::pair sample(uint64_t* seed) const = 0; static unique_ptr create(pugi::xml_node node); }; @@ -34,8 +36,8 @@ public: //! Sample a position from the distribution //! \param seed Pseudorandom number seed pointer - //! \return Sampled position - Position sample(uint64_t* seed) const override; + //! \return Sampled (position, importance weight) + std::pair sample(uint64_t* seed) const override; // Observer pointers Distribution* x() const { return x_.get(); } @@ -58,8 +60,8 @@ public: //! Sample a position from the distribution //! \param seed Pseudorandom number seed pointer - //! \return Sampled position - Position sample(uint64_t* seed) const override; + //! \return Sampled (position, importance weight) + std::pair sample(uint64_t* seed) const override; Distribution* r() const { return r_.get(); } Distribution* phi() const { return phi_.get(); } @@ -83,8 +85,8 @@ public: //! Sample a position from the distribution //! \param seed Pseudorandom number seed pointer - //! \return Sampled position - Position sample(uint64_t* seed) const override; + //! \return Sampled (position, importance weight) + std::pair sample(uint64_t* seed) const override; Distribution* r() const { return r_.get(); } Distribution* cos_theta() const { return cos_theta_.get(); } @@ -109,8 +111,8 @@ public: //! Sample a position from the distribution //! \param seed Pseudorandom number seed pointer - //! \return Sampled position - Position sample(uint64_t* seed) const override; + //! \return Sampled (position, importance weight) + std::pair sample(uint64_t* seed) const override; //! Sample the mesh for an element and position within that element //! \param seed Pseudorandom number seed pointer @@ -133,8 +135,8 @@ public: private: int32_t mesh_idx_ {C_NONE}; - DiscreteIndex elem_idx_dist_; //!< Distribution of - //!< mesh element indices + DiscreteIndex elem_idx_dist_; //!< Distribution of mesh element indices + vector weight_; //!< Importance weights (empty if unbiased) }; //============================================================================== @@ -149,12 +151,13 @@ public: //! Sample a position from the distribution //! \param seed Pseudorandom number seed pointer - //! \return Sampled position - Position sample(uint64_t* seed) const override; + //! \return Sampled (position, importance weight) + std::pair sample(uint64_t* seed) const override; private: std::vector point_cloud_; DiscreteIndex point_idx_dist_; //!< Distribution of Position indices + vector weight_; //!< Importance weights (empty if unbiased) }; //============================================================================== @@ -167,8 +170,8 @@ public: //! Sample a position from the distribution //! \param seed Pseudorandom number seed pointer - //! \return Sampled position - Position sample(uint64_t* seed) const override; + //! \return Sampled (position, importance weight) + std::pair sample(uint64_t* seed) const override; // Properties bool only_fissionable() const { return only_fissionable_; } @@ -193,8 +196,8 @@ public: //! Sample a position from the distribution //! \param seed Pseudorandom number seed pointer - //! \return Sampled position - Position sample(uint64_t* seed) const override; + //! \return Sampled (position, importance weight) + std::pair sample(uint64_t* seed) const override; Position r() const { return r_; } diff --git a/include/openmc/source.h b/include/openmc/source.h index 1fbd31904..36c821fc0 100644 --- a/include/openmc/source.h +++ b/include/openmc/source.h @@ -217,8 +217,8 @@ public: //! Sample a position from the distribution //! \param seed Pseudorandom number seed pointer - //! \return Sampled position - Position sample(uint64_t* seed) const override; + //! \return (sampled position, importance weight) + std::pair sample(uint64_t* seed) const override; private: int32_t mesh_index_ {C_NONE}; //!< Index in global meshes array diff --git a/openmc/filter.py b/openmc/filter.py index 39844798d..e7c54f2f0 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -815,7 +815,7 @@ class ParentNuclideFilter(ParticleFilter): class MeshFilter(Filter): - """Bins tally event locations by mesh elements. + r"""Bins tally event locations by mesh elements. Parameters ---------- diff --git a/openmc/settings.py b/openmc/settings.py index 76e191c5e..a022b045f 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -10,7 +10,7 @@ import warnings import openmc import openmc.checkvalue as cv from openmc.checkvalue import PathLike -from openmc.stats.multivariate import MeshSpatial +from openmc.stats.multivariate import MeshSpatial, Box, PolarAzimuthal, Isotropic from ._xml import clean_indentation, get_elem_list, get_text from .mesh import _read_meshes, RegularMesh, MeshBase from .source import SourceBase, MeshSource, IndependentSource @@ -1877,7 +1877,11 @@ class Settings: for key, value in self._random_ray.items(): if key == 'ray_source' and isinstance(value, SourceBase): source_element = value.to_xml_element() + if source_element.find('bias') is not None: + raise RuntimeError( + "Ray source distributions should not be biased.") element.append(source_element) + elif key == 'source_region_meshes': subelement = ET.SubElement(element, 'source_region_meshes') for mesh, domains in value: @@ -2324,6 +2328,9 @@ class Settings: self.random_ray[child.tag] = float(child.text) elif child.tag == 'source': source = SourceBase.from_xml_element(child) + if child.find('bias') is not None: + raise RuntimeError( + "Ray source distributions should not be biased.") self.random_ray['ray_source'] = source elif child.tag == 'volume_estimator': self.random_ray['volume_estimator'] = child.text diff --git a/openmc/stats/multivariate.py b/openmc/stats/multivariate.py index 1ce998758..2057f4a49 100644 --- a/openmc/stats/multivariate.py +++ b/openmc/stats/multivariate.py @@ -81,7 +81,7 @@ class PolarAzimuthal(UnitSphere): z-direction. reference_vwu : Iterable of float Direction from which azimuthal angle is measured. Defaults to the positive - x-direction. + x-direction. Attributes ---------- @@ -104,7 +104,7 @@ class PolarAzimuthal(UnitSphere): self.phi = phi else: self.phi = Uniform(0., 2*pi) - + @property def reference_vwu(self): return self._reference_vwu @@ -114,8 +114,8 @@ class PolarAzimuthal(UnitSphere): cv.check_type('reference v direction', vwu, Iterable, Real) vwu = np.asarray(vwu) uvw = self.reference_uvw - cv.check_greater_than('reference v direction must not be parallel to reference u direction', np.linalg.norm(np.cross(vwu,uvw)), 1e-6*np.linalg.norm(vwu)) - vwu -= vwu.dot(uvw)*uvw + cv.check_greater_than('reference v direction must not be parallel to reference u direction', np.linalg.norm(np.cross(vwu,uvw)), 1e-6*np.linalg.norm(vwu)) + vwu -= vwu.dot(uvw)*uvw cv.check_less_than('reference v direction must be orthogonal to reference u direction', np.abs(vwu.dot(uvw)), 1e-6) self._reference_vwu = vwu/np.linalg.norm(vwu) @@ -137,21 +137,30 @@ class PolarAzimuthal(UnitSphere): cv.check_type('azimuthal angle', phi, Univariate) self._phi = phi - def to_xml_element(self): + def to_xml_element(self, element_name: str = None): """Return XML representation of the angular distribution + Parameters + ---------- + element_name : str, optional + XML element name + Returns ------- element : lxml.etree._Element XML element containing angular distribution data """ - element = ET.Element('angle') + if element_name is not None: + element = ET.Element(element_name) + else: + element = ET.Element('angle') + element.set("type", "mu-phi") if self.reference_uvw is not None: element.set("reference_uvw", ' '.join(map(str, self.reference_uvw))) if self.reference_vwu is not None: - element.set("reference_vwu", ' '.join(map(str, self.reference_vwu))) + element.set("reference_vwu", ' '.join(map(str, self.reference_vwu))) element.append(self.mu.to_xml_element('mu')) element.append(self.phi.to_xml_element('phi')) return element @@ -177,17 +186,48 @@ class PolarAzimuthal(UnitSphere): mu_phi.reference_uvw = uvw vwu = get_elem_list(elem, "reference_vwu", float) if vwu is not None: - mu_phi.reference_vwu = vwu + mu_phi.reference_vwu = vwu mu_phi.mu = Univariate.from_xml_element(elem.find('mu')) mu_phi.phi = Univariate.from_xml_element(elem.find('phi')) return mu_phi class Isotropic(UnitSphere): - """Isotropic angular distribution.""" + """Isotropic angular distribution. - def __init__(self): + Parameters + ---------- + bias : openmc.stats.PolarAzimuthal, optional + Distribution for biased sampling. + + Attributes + ---------- + bias : openmc.stats.PolarAzimuthal or None + Distribution for biased sampling + + """ + + def __init__(self, bias: PolarAzimuthal | None = None): super().__init__() + self.bias = bias + + @property + def bias(self): + return self._bias + + @bias.setter + def bias(self, bias): + cv.check_type('Biasing distribution', bias, PolarAzimuthal, none_ok=True) + if bias is not None: + if (bias.mu.bias is not None) or (bias.phi.bias is not None): + raise RuntimeError('Biasing distributions should not have their own bias.') + elif (bias.mu.support != (-1., 1.) + or not np.all(np.isclose(bias.phi.support, (0., 2*np.pi)))): + raise ValueError("Biasing distribution for an isotropic " + "distribution should be supported on " + "mu=(-1.0,1.0) and phi=(0.0,2*pi).") + + self._bias = bias def to_xml_element(self): """Return XML representation of the isotropic distribution @@ -200,6 +240,15 @@ class Isotropic(UnitSphere): """ element = ET.Element('angle') element.set("type", "isotropic") + + if self.bias is not None: + bias_dist = self.bias + if (bias_dist.mu.bias is not None) or (bias_dist.phi.bias is not None): + raise RuntimeError('Biasing distributions should not have their own bias!') + else: + bias_elem = self.bias.to_xml_element("bias") + element.append(bias_elem) + return element @classmethod @@ -217,7 +266,13 @@ class Isotropic(UnitSphere): Isotropic distribution generated from XML element """ - return cls() + bias_elem = elem.find('bias') + if bias_elem is not None: + bias_dist = PolarAzimuthal.from_xml_element(bias_elem) + return cls(bias=bias_dist) + else: + return cls() + class Monodirectional(UnitSphere): @@ -672,6 +727,9 @@ class MeshSpatial(Spatial): volume_normalized : bool, optional Whether or not the strengths will be multiplied by element volumes at runtime. Default is True. + bias : iterable of float, optional + An iterable of values giving the selection weights assigned to each + element during biased sampling. Attributes ---------- @@ -682,12 +740,16 @@ class MeshSpatial(Spatial): volume_normalized : bool Whether or not the strengths will be multiplied by element volumes at runtime. + bias : numpy.ndarray or None + Distribution for biased sampling """ - def __init__(self, mesh, strengths=None, volume_normalized=True): + def __init__(self, mesh, strengths=None, volume_normalized=True, + bias: Sequence[float] | None = None): self.mesh = mesh self.strengths = strengths self.volume_normalized = volume_normalized + self.bias = bias @property def mesh(self): @@ -720,6 +782,23 @@ class MeshSpatial(Spatial): else: self._strengths = None + @property + def bias(self): + return self._bias + + @bias.setter + def bias(self, given_bias): + if given_bias is not None: + cv.check_type('Biasing strengths array', given_bias, Iterable, Real) + bias_array = np.asarray(given_bias, dtype=float).flatten() + if bias_array.size != self.strengths.size: + raise ValueError( + 'Bias strengths array must have same size as strengths array.') + else: + self._bias = bias_array + else: + self._bias = None + @property def num_strength_bins(self): if self.strengths is None: @@ -745,6 +824,9 @@ class MeshSpatial(Spatial): subelement = ET.SubElement(element, 'strengths') subelement.text = ' '.join(str(e) for e in self.strengths) + if self.bias is not None: + Univariate._append_array_bias_to_xml(self, element) + return element @classmethod @@ -774,7 +856,8 @@ class MeshSpatial(Spatial): volume_normalized = get_text(elem, 'volume_normalized').lower() == 'true' strengths = get_elem_list(elem, 'strengths', float) - return cls(meshes[mesh_id], strengths, volume_normalized) + bias_strengths = Univariate._read_array_bias_from_xml(elem) + return cls(meshes[mesh_id], strengths, volume_normalized, bias=bias_strengths) class PointCloud(Spatial): @@ -792,6 +875,9 @@ class PointCloud(Spatial): strengths : iterable of float, optional An iterable of values that represents the relative probabilty of each point. + bias : iterable of float, optional + An iterable of values representing the relative probability of each + point under biased sampling. Attributes ---------- @@ -799,15 +885,19 @@ class PointCloud(Spatial): The points in space to be sampled with shape (N, 3) strengths : numpy.ndarray or None An array of relative probabilities for each mesh point + bias : numpy.ndarray or None + An array of relative probabilities for biased sampling of mesh points """ def __init__( self, positions: Sequence[Sequence[float]], - strengths: Sequence[float] | None = None + strengths: Sequence[float] | None = None, + bias: Sequence[float] | None = None ): self.positions = positions self.strengths = strengths + self.bias = bias @property def positions(self) -> np.ndarray: @@ -836,6 +926,23 @@ class PointCloud(Spatial): raise ValueError('strengths must have the same length as positions') self._strengths = strengths + @property + def bias(self): + return self._bias + + @bias.setter + def bias(self, given_bias): + if given_bias is not None: + cv.check_type('Biasing strengths array', given_bias, Iterable, Real) + bias_array = np.asarray(given_bias, dtype=float).flatten() + if bias_array.size != self.strengths.size: + raise ValueError( + 'Bias strengths array must have same size as strengths array.') + else: + self._bias = bias_array + else: + self._bias = None + @property def num_strength_bins(self) -> int: if self.strengths is None: @@ -861,6 +968,9 @@ class PointCloud(Spatial): subelement = ET.SubElement(element, 'strengths') subelement.text = ' '.join(str(e) for e in self.strengths) + if self.bias is not None: + Univariate._append_array_bias_to_xml(self, element) + return element @classmethod @@ -883,8 +993,8 @@ class PointCloud(Spatial): positions = np.array(coord_data).reshape((-1, 3)) strengths = get_elem_list(elem, 'strengths', float) - - return cls(positions, strengths) + bias_strengths = Univariate._read_array_bias_from_xml(elem) + return cls(positions, strengths, bias=bias_strengths) class Box(Spatial): diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index c48cc0075..272367f6f 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -1,15 +1,16 @@ from __future__ import annotations -import math from abc import ABC, abstractmethod from collections import defaultdict from collections.abc import Iterable, Sequence from copy import deepcopy +from math import sqrt, pi, exp from numbers import Real from warnings import warn import lxml.etree as ET import numpy as np from scipy.integrate import trapezoid +import scipy import openmc.checkvalue as cv from .._xml import get_elem_list, get_text @@ -30,7 +31,55 @@ class Univariate(EqualityMixin, ABC): The Univariate class is an abstract class that can be derived to implement a specific probability distribution. + Parameters + ---------- + bias : Iterable of float, optional + Distribution or discrete probabilities for biased sampling or discrete + probabilities for biased sampling. + """ + def __init__(self, bias: Univariate | Sequence[float] | None = None): + self.bias = bias + + @property + def bias(self): + return self._bias + + @bias.setter + def bias(self, bias): + check_bias_support(self, bias) + self._bias = bias + + def _append_bias_to_xml(self, element: ET.Element) -> None: + """Append bias distribution element to XML if present.""" + if self.bias is not None: + if self.bias.bias is not None: + raise RuntimeError('Biasing distributions should not have their own bias.') + bias_elem = self.bias.to_xml_element("bias") + element.append(bias_elem) + + @classmethod + def _read_bias_from_xml(cls, elem: ET.Element): + """Read bias distribution from XML element if present.""" + bias_elem = elem.find('bias') + if bias_elem is not None: + return Univariate.from_xml_element(bias_elem) + return None + + def _append_array_bias_to_xml(self, element: ET.Element) -> None: + """Append array-based bias probabilities to XML.""" + if self.bias is not None: + bias_elem = ET.SubElement(element, "bias") + bias_elem.text = ' '.join(map(str, self.bias)) + + @classmethod + def _read_array_bias_from_xml(cls, elem: ET.Element): + """Read array-based bias probabilities from XML.""" + bias_elem = elem.find('bias') + if bias_elem is not None: + return get_elem_list(elem, "bias", float) + return None + @abstractmethod def to_xml_element(self, element_name): return '' @@ -66,8 +115,8 @@ class Univariate(EqualityMixin, ABC): return Mixture.from_xml_element(elem) @abstractmethod - def sample(n_samples: int = 1, seed: int | None = None): - """Sample the univariate distribution + def _sample_unbiased(self, n_samples: int = 1, seed: int | None = None): + """Sample without bias handling. Parameters ---------- @@ -79,10 +128,35 @@ class Univariate(EqualityMixin, ABC): Returns ------- numpy.ndarray - A 1-D array of sampled values + The array of sampled values """ pass + def sample(self, n_samples: int = 1, seed: int | None = None): + """Sample the univariate distribution, handling biasing automatically. + + Parameters + ---------- + n_samples : int + Number of sampled values to generate + seed : int or None + Initial random number seed. + + Returns + ------- + tuple of numpy.ndarray + A tuple of (samples, weights) + """ + if self.bias is None: + x = self._sample_unbiased(n_samples, seed) + return x, np.ones_like(x) + else: + if self.bias.bias is not None: + raise RuntimeError('Biasing distributions should not have their own bias.') + x, _ = self.bias.sample(n_samples=n_samples, seed=seed) + weight = self.evaluate(x) / self.bias.evaluate(x) + return x, weight + def integral(self): """Return integral of distribution @@ -95,6 +169,36 @@ class Univariate(EqualityMixin, ABC): """ return 1.0 + @abstractmethod + def evaluate(self, x: float | Sequence[float]): + """Evaluate the probability density at the provided value. + + Parameters + ---------- + x : float or sequence of float + Location to evaluate p(x) + + Returns + ------- + float or numpy.ndarray + Value of p(x) + """ + pass + + @property + @abstractmethod + def support(self): + """Return the support of the probability distribution. + + Returns + ------- + set or tuple of float or dict + Returns the set of unique points assigned probability mass in a + discrete distribution, the sampling interval for a continuous + distribution, or a dictionary storing the discrete and continuous + parts of the support of a mixed random variable + """ + pass def _intensity_clip(intensity: Sequence[float], tolerance: float = 1e-6) -> np.ndarray: """Clip low-importance points from an array of intensities. @@ -148,6 +252,9 @@ class Discrete(Univariate): Values of the random variable p : Iterable of float Discrete probability for each value + bias : Iterable of float, optional + Alternative discrete probabilities for biased sampling. Defaults to + None for unbiased sampling. Attributes ---------- @@ -155,12 +262,18 @@ class Discrete(Univariate): Values of the random variable p : numpy.ndarray Discrete probability for each value + support : set + Values of the random variable over which the distribution is + nonzero-valued + bias : numpy.ndarray or None + Discrete probabilities for biased sampling """ - def __init__(self, x, p): + def __init__(self, x, p, bias=None): self.x = x self.p = p + super().__init__(bias) def __len__(self): return len(self.x) @@ -189,10 +302,42 @@ class Discrete(Univariate): cv.check_greater_than('discrete probability', pk, 0.0, True) self._p = np.array(p, dtype=float) + @property + def support(self): + return set(np.unique(self._x)) + + @Univariate.bias.setter + def bias(self, bias): + if bias is None: + self._bias = bias + else: + if isinstance(bias, Real): + bias = [bias] + cv.check_type('discrete bias probabilities', bias, Iterable, Real) + for bk in bias: + cv.check_greater_than('discrete probability', bk, 0.0, True) + if len(bias) != len(self.x): + raise RuntimeError("Discrete distribution has unequal number of " + "biased and unbiased probability entries.") + self._bias = np.array(bias, dtype=float) + def cdf(self): return np.insert(np.cumsum(self.p), 0, 0.0) def sample(self, n_samples=1, seed=None): + if self.bias is None: + samples = self._sample_unbiased(n_samples, seed) + return samples, np.ones_like(samples) + else: + rng = np.random.RandomState(seed) + p = self.p / self.p.sum() + b = self.bias / self.bias.sum() + indices = rng.choice(self.x.size, n_samples, p=b) + biased_sample = self.x[indices] + wgt = p[indices] / b[indices] + return biased_sample, wgt + + def _sample_unbiased(self, n_samples=1, seed=None): rng = np.random.RandomState(seed) p = self.p / self.p.sum() return rng.choice(self.x, n_samples, p=p) @@ -202,6 +347,9 @@ class Discrete(Univariate): norm = sum(self.p) self.p = [val / norm for val in self.p] + def evaluate(self, x): + raise NotImplementedError + def to_xml_element(self, element_name): """Return XML representation of the discrete distribution @@ -221,7 +369,7 @@ class Discrete(Univariate): params = ET.SubElement(element, "parameters") params.text = ' '.join(map(str, self.x)) + ' ' + ' '.join(map(str, self.p)) - + self._append_array_bias_to_xml(element) return element @classmethod @@ -242,7 +390,8 @@ class Discrete(Univariate): params = get_elem_list(elem, "parameters", float) x = params[:len(params)//2] p = params[len(params)//2:] - return cls(x, p) + bias_dist = cls._read_array_bias_from_xml(elem) + return cls(x, p, bias=bias_dist) @classmethod def merge( @@ -270,18 +419,52 @@ class Discrete(Univariate): if len(dists) != len(probs): raise ValueError("Number of distributions and probabilities must match.") + biasing = False + for d in dists: + if d.bias is not None: + # If we find that at least one distribution is biased, all + # distributions which are not biased will be assigned their + # default probability vector as a "bias" so that biased + # sampling can occur on the merged distribution. + biasing = True + break + # Combine distributions accounting for duplicate x values x_merged = set() p_merged = defaultdict(float) - for dist, p_dist in zip(dists, probs): - for x, p in zip(dist.x, dist.p): - x_merged.add(x) - p_merged[x] += p*p_dist + new_bias = None - # Create values and probabilities as arrays - x_arr = np.array(sorted(x_merged)) + if biasing: + b_merged = defaultdict(float) + + # Generate any missing bias distributions + dists = dists.copy() + for i, d in enumerate(dists): + if d.bias is None: + dists[i] = Discrete(d.x, d.p, bias=d.p) + + for dist, p_dist in zip(dists, probs): + for x, p, b in zip(dist.x, dist.p, dist.bias): + x_merged.add(x) + p_merged[x] += p*p_dist + b_merged[x] += b*p_dist + + # Create values and bias probabilities as arrays + x_arr = np.array(sorted(x_merged)) + new_bias = np.array([b_merged[x] for x in x_arr]) + + else: + for dist, p_dist in zip(dists, probs): + for x, p in zip(dist.x, dist.p): + x_merged.add(x) + p_merged[x] += p*p_dist + + # Create values as array + x_arr = np.array(sorted(x_merged)) + + # Create probabilities as array p_arr = np.array([p_merged[x] for x in x_arr]) - return cls(x_arr, p_arr) + return cls(x_arr, p_arr, new_bias) def integral(self): """Return integral of distribution @@ -318,6 +501,9 @@ class Discrete(Univariate): function will remove any low-importance points such that :math:`\sum_i x_i p_i` is preserved to within some threshold. + For biased distributions, clipping should be performed before the bias + probabilities are added. + .. versionadded:: 0.14.0 Parameters @@ -332,6 +518,10 @@ class Discrete(Univariate): Discrete distribution with low-importance points removed """ + if self.bias is not None: + raise RuntimeError("Biased discrete distributions should be clipped " + "before applying bias.") + cv.check_less_than("tolerance", tolerance, 1.0, equality=True) cv.check_greater_than("tolerance", tolerance, 0.0, equality=True) @@ -382,6 +572,8 @@ class Uniform(Univariate): Lower bound of the sampling interval. Defaults to zero. b : float, optional Upper bound of the sampling interval. Defaults to unity. + bias : openmc.stats.Univariate, optional + Distribution for biased sampling. Attributes ---------- @@ -389,12 +581,19 @@ class Uniform(Univariate): Lower bound of the sampling interval b : float Upper bound of the sampling interval + support : tuple of float + A 2-tuple (lower, upper) defining the interval over which the + distribution is nonzero-valued + bias : openmc.stats.Univariate or None + Distribution for biased sampling """ - def __init__(self, a: float = 0.0, b: float = 1.0): + def __init__(self, a: float = 0.0, b: float = 1.0, + bias: Univariate | None = None): self.a = a self.b = b + super().__init__(bias) def __len__(self): return 2 @@ -417,16 +616,25 @@ class Uniform(Univariate): cv.check_type('Uniform b', b, Real) self._b = b + @property + def support(self): + return (self._a, self._b) + def to_tabular(self): + if self.bias is not None: + raise RuntimeError("to_tabular() is not permitted for biased distributions.") prob = 1./(self.b - self.a) t = Tabular([self.a, self.b], [prob, prob], 'histogram') t.c = [0., 1.] return t - def sample(self, n_samples=1, seed=None): + def _sample_unbiased(self, n_samples=1, seed=None): rng = np.random.RandomState(seed) return rng.uniform(self.a, self.b, n_samples) + def evaluate(self, x): + return np.where((self.a <= x) & (x <= self.b), 1/(self.b - self.a), 0.0) + def mean(self) -> float: """Return mean of the uniform distribution @@ -456,6 +664,7 @@ class Uniform(Univariate): element = ET.Element(element_name) element.set("type", "uniform") element.set("parameters", f'{self.a} {self.b}') + self._append_bias_to_xml(element) return element @classmethod @@ -474,7 +683,8 @@ class Uniform(Univariate): """ params = get_elem_list(elem, "parameters", float) - return cls(*params) + bias_dist = cls._read_bias_from_xml(elem) + return cls(*params, bias=bias_dist) class PowerLaw(Univariate): @@ -493,6 +703,8 @@ class PowerLaw(Univariate): n : float, optional Power law exponent. Defaults to zero, which is equivalent to a uniform distribution. + bias : openmc.stats.Univariate, optional + Distribution for biased sampling. Attributes ---------- @@ -502,16 +714,23 @@ class PowerLaw(Univariate): Upper bound of the sampling interval n : float Power law exponent + support : tuple of float + A 2-tuple (lower, upper) defining the interval over which the + distribution is nonzero-valued + bias : openmc.stats.Univariate or None + Distribution for biased sampling """ - def __init__(self, a: float = 0.0, b: float = 1.0, n: float = 0.): + def __init__(self, a: float = 0.0, b: float = 1.0, n: float = 0., + bias: Univariate | None = None): if a >= b: raise ValueError( "Lower bound of sampling interval must be less than upper bound.") self.a = a self.b = b self.n = n + super().__init__(bias) def __len__(self): return 3 @@ -549,7 +768,11 @@ class PowerLaw(Univariate): cv.check_type('power law exponent', n, Real) self._n = n - def sample(self, n_samples=1, seed=None): + @property + def support(self): + return (self._a, self._b) + + def _sample_unbiased(self, n_samples=1, seed=None): rng = np.random.RandomState(seed) xi = rng.random(n_samples) pwr = self.n + 1 @@ -557,6 +780,10 @@ class PowerLaw(Univariate): span = self.b**pwr - offset return np.power(offset + xi * span, 1/pwr) + def evaluate(self, x): + c = (self.n + 1)/(self.b**(self.n + 1) - self.a**(self.n + 1)) + return np.where((self.a <= x) & (x <= self.b), c * np.abs(x)**self.n, 0.0) + def to_xml_element(self, element_name: str): """Return XML representation of the power law distribution @@ -574,6 +801,7 @@ class PowerLaw(Univariate): element = ET.Element(element_name) element.set("type", "powerlaw") element.set("parameters", f'{self.a} {self.b} {self.n}') + self._append_bias_to_xml(element) return element @classmethod @@ -592,7 +820,8 @@ class PowerLaw(Univariate): """ params = get_elem_list(elem, "parameters", float) - return cls(*params) + bias_dist = cls._read_bias_from_xml(elem) + return cls(*map(float, params), bias=bias_dist) class Maxwell(Univariate): @@ -606,16 +835,24 @@ class Maxwell(Univariate): ---------- theta : float Effective temperature for distribution in eV + bias : openmc.stats.Univariate, optional + Distribution for biased sampling. Attributes ---------- theta : float Effective temperature for distribution in eV + support : tuple of float + A 2-tuple (lower, upper) defining the interval over which the + distribution is nonzero-valued + bias : openmc.stats.Univariate or None + Distribution for biased sampling """ - def __init__(self, theta): + def __init__(self, theta, bias: Univariate | None = None): self.theta = theta + super().__init__(bias) def __len__(self): return 1 @@ -630,7 +867,11 @@ class Maxwell(Univariate): cv.check_greater_than('Maxwell temperature', theta, 0.0) self._theta = theta - def sample(self, n_samples=1, seed=None): + @property + def support(self): + return (0.0, np.inf) + + def _sample_unbiased(self, n_samples=1, seed=None): rng = np.random.RandomState(seed) return self.sample_maxwell(self.theta, n_samples, rng=rng) @@ -638,9 +879,10 @@ class Maxwell(Univariate): def sample_maxwell(t, n_samples: int, rng=None): if rng is None: rng = np.random.default_rng() - r1, r2, r3 = rng.random((3, n_samples)) - c = np.cos(0.5 * np.pi * r3) - return -t * (np.log(r1) + np.log(r2) * c * c) + return rng.gamma(1.5, t, n_samples) + + def evaluate(self, E): + return scipy.stats.gamma.pdf(E, 1.5, scale=self.theta) def to_xml_element(self, element_name: str): """Return XML representation of the Maxwellian distribution @@ -659,6 +901,7 @@ class Maxwell(Univariate): element = ET.Element(element_name) element.set("type", "maxwell") element.set("parameters", str(self.theta)) + self._append_bias_to_xml(element) return element @classmethod @@ -677,7 +920,8 @@ class Maxwell(Univariate): """ theta = float(get_text(elem, 'parameters')) - return cls(theta) + bias_dist = cls._read_bias_from_xml(elem) + return cls(theta, bias=bias_dist) class Watt(Univariate): @@ -693,6 +937,8 @@ class Watt(Univariate): First parameter of distribution in units of eV b : float Second parameter of distribution in units of 1/eV + bias : openmc.stats.Univariate, optional + Distribution for biased sampling. Attributes ---------- @@ -700,12 +946,18 @@ class Watt(Univariate): First parameter of distribution in units of eV b : float Second parameter of distribution in units of 1/eV + support : tuple of float + A 2-tuple (lower, upper) defining the interval over which the + distribution is nonzero-valued + bias : openmc.stats.Univariate or None + Distribution for biased sampling """ - def __init__(self, a=0.988e6, b=2.249e-6): + def __init__(self, a=0.988e6, b=2.249e-6, bias: Univariate | None = None): self.a = a self.b = b + super().__init__(bias) def __len__(self): return 2 @@ -730,13 +982,21 @@ class Watt(Univariate): cv.check_greater_than('Watt b', b, 0.0) self._b = b - def sample(self, n_samples=1, seed=None): + @property + def support(self): + return (0.0, np.inf) + + def _sample_unbiased(self, n_samples=1, seed=None): rng = np.random.RandomState(seed) w = Maxwell.sample_maxwell(self.a, n_samples, rng=rng) u = rng.uniform(-1., 1., n_samples) aab = self.a * self.a * self.b return w + 0.25*aab + u*np.sqrt(aab*w) + def evaluate(self, E): + c = 2.0/(sqrt(pi * self.b) * (self.a**1.5) * exp(self.a*self.b/4)) + return c*np.exp(-E/self.a)*np.sinh(np.sqrt(self.b*E)) + def to_xml_element(self, element_name: str): """Return XML representation of the Watt distribution @@ -754,6 +1014,7 @@ class Watt(Univariate): element = ET.Element(element_name) element.set("type", "watt") element.set("parameters", f'{self.a} {self.b}') + self._append_bias_to_xml(element) return element @classmethod @@ -772,7 +1033,8 @@ class Watt(Univariate): """ params = get_elem_list(elem, "parameters", float) - return cls(*params) + bias_dist = cls._read_bias_from_xml(elem) + return cls(*map(float, params), bias=bias_dist) class Normal(Univariate): @@ -788,6 +1050,8 @@ class Normal(Univariate): Mean value of the distribution std_dev : float Standard deviation of the Normal distribution + bias : openmc.stats.Univariate, optional + Distribution for biased sampling. Attributes ---------- @@ -795,11 +1059,17 @@ class Normal(Univariate): Mean of the Normal distribution std_dev : float Standard deviation of the Normal distribution + support : tuple of float + A 2-tuple (lower, upper) defining the interval over which the + distribution is nonzero-valued + bias : openmc.stats.Univariate or None + Distribution for biased sampling """ - def __init__(self, mean_value, std_dev): + def __init__(self, mean_value, std_dev, bias: Univariate | None = None): self.mean_value = mean_value self.std_dev = std_dev + super().__init__(bias) def __len__(self): return 2 @@ -823,10 +1093,17 @@ class Normal(Univariate): cv.check_greater_than('Normal std_dev', std_dev, 0.0) self._std_dev = std_dev - def sample(self, n_samples=1, seed=None): + @property + def support(self): + return (-np.inf, np.inf) + + def _sample_unbiased(self, n_samples=1, seed=None): rng = np.random.RandomState(seed) return rng.normal(self.mean_value, self.std_dev, n_samples) + def evaluate(self, x): + return scipy.stats.norm.pdf(x, self.mean_value, self.std_dev) + def to_xml_element(self, element_name: str): """Return XML representation of the Normal distribution @@ -844,6 +1121,7 @@ class Normal(Univariate): element = ET.Element(element_name) element.set("type", "normal") element.set("parameters", f'{self.mean_value} {self.std_dev}') + self._append_bias_to_xml(element) return element @classmethod @@ -862,10 +1140,11 @@ class Normal(Univariate): """ params = get_elem_list(elem, "parameters", float) - return cls(*params) + bias_dist = cls._read_bias_from_xml(elem) + return cls(*map(float, params), bias=bias_dist) -def muir(e0: float, m_rat: float, kt: float): +def muir(e0: float, m_rat: float, kt: float, bias: Univariate | None = None): """Generate a Muir energy spectrum The Muir energy spectrum is a normal distribution, but for convenience @@ -883,6 +1162,8 @@ def muir(e0: float, m_rat: float, kt: float): Ratio of the sum of the masses of the reaction inputs to 1 amu kt : float Ion temperature for the Muir distribution in [eV] + bias : openmc.stats.Univariate, optional + Distribution for biased sampling. Returns ------- @@ -891,8 +1172,8 @@ def muir(e0: float, m_rat: float, kt: float): """ # https://permalink.lanl.gov/object/tr?what=info:lanl-repo/lareport/LA-05411-MS - std_dev = math.sqrt(2 * e0 * kt / m_rat) - return Normal(e0, std_dev) + std_dev = sqrt(2 * e0 * kt / m_rat) + return Normal(e0, std_dev, bias) # Retain deprecated name for the time being @@ -926,6 +1207,8 @@ class Tabular(Univariate): points. Defaults to 'linear-linear'. ignore_negative : bool Ignore negative probabilities + bias : openmc.stats.Univariate, optional + Distribution for biased sampling. Attributes ---------- @@ -936,6 +1219,11 @@ class Tabular(Univariate): interpolation : {'histogram', 'linear-linear', 'linear-log', 'log-linear', 'log-log'} Indicates how the density function is interpolated between tabulated points. Defaults to 'linear-linear'. + support : tuple of float + A 2-tuple (lower, upper) defining the interval over which the + distribution is nonzero-valued + bias : openmc.stats.Univariate or None + Distribution for biased sampling Notes ----- @@ -953,7 +1241,8 @@ class Tabular(Univariate): x: Sequence[float], p: Sequence[float], interpolation: str = 'linear-linear', - ignore_negative: bool = False + ignore_negative: bool = False, + bias: Univariate | None = None ): self.interpolation = interpolation @@ -975,6 +1264,7 @@ class Tabular(Univariate): self._x = x self._p = p + super().__init__(bias) def __len__(self): return self.p.size @@ -996,6 +1286,10 @@ class Tabular(Univariate): cv.check_value('interpolation', interpolation, _INTERPOLATION_SCHEMES) self._interpolation = interpolation + @property + def support(self): + return (self._x[0], self._x[-1]) + def cdf(self): c = np.zeros_like(self.x) x = self.x @@ -1049,7 +1343,7 @@ class Tabular(Univariate): """Normalize the probabilities stored on the distribution""" self._p /= self.cdf().max() - def sample(self, n_samples: int = 1, seed: int | None = None): + def _sample_unbiased(self, n_samples: int = 1, seed: int | None = None): rng = np.random.RandomState(seed) xi = rng.random(n_samples) @@ -1110,6 +1404,39 @@ class Tabular(Univariate): assert all(samples_out < self.x[-1]) return samples_out + def sample(self, n_samples: int = 1, seed: int | None = None): + if self.bias is None: + samples = self._sample_unbiased(n_samples, seed) + return samples, np.ones_like(samples) + else: + if self.bias.bias is not None: + raise RuntimeError('Biasing distributions should not have their own bias.') + biased_sample, _ = self.bias.sample(n_samples=n_samples, seed=seed) + self.normalize() # must have normalized probabilities to apply correct weights + wgt = np.array([self.evaluate(s) / self.bias.evaluate(s) for s in biased_sample]) + return biased_sample, wgt + + def evaluate(self, x): + if self.interpolation == 'linear-linear': + i = np.searchsorted(self.x, x, side='left') - 1 + if i < 0 or i >= len(self.p) - 1: + return 0.0 + x0, x1 = self.x[i], self.x[i + 1] + p0, p1 = self.p[i], self.p[i + 1] + t = (x - x0) / (x1 - x0) + return (1 - t) * p0 + t * p1 + + elif self.interpolation == 'histogram': + i = np.searchsorted(self.x, x, side='right') - 1 + if i < 0 or i >= len(self.p): + return 0.0 + return self.p[i] + + else: + raise NotImplementedError('Can only evaluate tabular ' + 'distributions using histogram ' + 'or linear-linear interpolation.') + def to_xml_element(self, element_name: str): """Return XML representation of the tabular distribution @@ -1130,7 +1457,7 @@ class Tabular(Univariate): params = ET.SubElement(element, "parameters") params.text = ' '.join(map(str, self.x)) + ' ' + ' '.join(map(str, self.p)) - + self._append_bias_to_xml(element) return element @classmethod @@ -1153,7 +1480,8 @@ class Tabular(Univariate): m = (len(params) + 1)//2 # +1 for when len(params) is odd x = params[:m] p = params[m:] - return cls(x, p, interpolation) + bias_dist = cls._read_bias_from_xml(elem) + return cls(x, p, interpolation, bias=bias_dist) def integral(self): """Return integral of distribution @@ -1183,16 +1511,24 @@ class Legendre(Univariate): coefficients : Iterable of Real Expansion coefficients :math:`a_\ell`. Note that the :math:`(2\ell + 1)/2` factor should not be included. + bias : openmc.stats.Univariate or None, optional + Distribution for biased sampling. Attributes ---------- coefficients : Iterable of Real Expansion coefficients :math:`a_\ell`. Note that the :math:`(2\ell + 1)/2` factor should not be included. + support : tuple of float + A 2-tuple (lower, upper) defining the interval over which the + distribution is nonzero-valued + bias : openmc.stats.Univariate or None + Distribution for biased sampling """ - def __init__(self, coefficients: Sequence[float]): + def __init__(self, coefficients: Sequence[float], bias: Univariate | None = None): + super().__init__(bias) self.coefficients = coefficients self._legendre_poly = None @@ -1216,9 +1552,19 @@ class Legendre(Univariate): def coefficients(self, coefficients): self._coefficients = np.asarray(coefficients) + @property + def support(self): + raise NotImplementedError + + def _sample_unbiased(self, n_samples=1, seed=None): + raise NotImplementedError + def sample(self, n_samples=1, seed=None): raise NotImplementedError + def evaluate(self, x): + raise NotImplementedError + def to_xml_element(self, element_name): raise NotImplementedError @@ -1236,6 +1582,9 @@ class Mixture(Univariate): Probability of selecting a particular distribution distribution : Iterable of Univariate List of distributions with corresponding probabilities + bias : Iterable of Real, optional + Probability of selecting a particular distribution under biased + sampling Attributes ---------- @@ -1243,14 +1592,20 @@ class Mixture(Univariate): Probability of selecting a particular distribution distribution : Iterable of Univariate List of distributions with corresponding probabilities + support : dict + Dictionary containing discrete and continuous parts of the support + bias : numpy.ndarray or None + Probability of selecting each distribution under biased sampling """ def __init__( self, probability: Sequence[float], - distribution: Sequence[Univariate] + distribution: Sequence[Univariate], + bias: Sequence[float] | None = None ): + super().__init__(bias) self.probability = probability self.distribution = distribution @@ -1280,10 +1635,52 @@ class Mixture(Univariate): Iterable, Univariate) self._distribution = distribution + @Univariate.bias.setter + def bias(self, bias): + if bias is None: + self._bias = bias + else: + cv.check_type('biased mixture distribution probabilities', bias, + Iterable, Real) + for b in bias: + cv.check_greater_than('biased mixture distribution probabilities', + b, 0.0, True) + self._bias = np.array(bias, dtype=float) + + @property + def support(self): + discrete_points = set() + intervals = [] + + for dist in self.distribution: + if isinstance(dist, Discrete): + discrete_points |= dist.support + else: + intervals.append(tuple(dist.support)) + + if intervals: + # simplify union by combining intervals when able + sorted_intervals = sorted(intervals, key=lambda x: x[0]) + merged = [sorted_intervals[0]] + + for current in sorted_intervals[1:]: + prev_start, prev_end = merged[-1] + curr_start, curr_end = current + + if curr_start <= prev_end: + merged[-1] = (prev_start, max(prev_end, curr_end)) + else: + merged.append(current) + + intervals = merged + + return {"discrete": discrete_points, "continuous": intervals} + def cdf(self): return np.insert(np.cumsum(self.probability), 0, 0.0) - def sample(self, n_samples=1, seed=None): + def _sample_unbiased(self, n_samples=1, seed=None): + # Mixture uses internal bias mechanism, not base class bias rng = np.random.RandomState(seed) # Get probability of each distribution accounting for its intensity @@ -1296,11 +1693,49 @@ class Mixture(Univariate): # Draw samples from the distributions sampled above out = np.empty_like(idx, dtype=float) + out_wgt = np.empty_like(idx, dtype=float) for i in np.unique(idx): n_dist_samples = np.count_nonzero(idx == i) - samples = self.distribution[i].sample(n_dist_samples) + samples, weights = self.distribution[i].sample(n_dist_samples) out[idx == i] = samples - return out + out_wgt[idx == i] = weights + return out, out_wgt + + def sample(self, n_samples=1, seed=None): + # Mixture uses internal bias mechanism, not base class bias + if self.bias is None: + return self._sample_unbiased(n_samples, seed) + + rng = np.random.RandomState(seed) + + # Get probability of each distribution accounting for its intensity + p = np.array([prob*dist.integral() for prob, dist in + zip(self.probability, self.distribution)]) + p /= p.sum() + + b = np.array([prob*dist.integral() for prob, dist in + zip(self.bias, self.distribution)]) + b /= b.sum() + + # Sample from the distributions using biased probabilities + idx = rng.choice(range(len(self.distribution)), n_samples, p=b) + idx_wgt = np.ones(n_samples) + for i in np.unique(idx): + idx_wgt[idx == i] = p[i]/b[i] + + # Draw samples from the distributions sampled above + out = np.empty_like(idx, dtype=float) + out_wgt = np.empty_like(idx, dtype=float) + for i in np.unique(idx): + n_dist_samples = np.count_nonzero(idx == i) + samples, weights = self.distribution[i].sample(n_dist_samples) + out[idx == i] = samples + out_wgt[idx == i] = weights * idx_wgt[idx == i] + return out, out_wgt + + def evaluate(self, x): + raise NotImplementedError( + "evaluate() is undefined for Mixture distributions") def normalize(self): """Normalize the probabilities stored on the distribution""" @@ -1331,6 +1766,7 @@ class Mixture(Univariate): data.set("probability", str(p)) data.append(d.to_xml_element("dist")) + self._append_array_bias_to_xml(element) return element @classmethod @@ -1356,7 +1792,8 @@ class Mixture(Univariate): probability.append(float(get_text(pair, 'probability'))) distribution.append(Univariate.from_xml_element(pair.find("dist"))) - return cls(probability, distribution) + bias_dist = cls._read_array_bias_from_xml(elem) + return cls(probability, distribution, bias=bias_dist) def integral(self): """Return integral of the distribution @@ -1510,3 +1947,50 @@ def combine_distributions( dist_list[:] = [Mixture(probs, dist_list.copy())] return dist_list[0] + + +def check_bias_support(parent: Univariate, bias: Univariate | None): + """Ensure that bias distributions share the support of the univariate + distribution they are biasing. + + Parameters + ---------- + parent : openmc.stats.Univariate + Distributions to be biased + bias : openmc.stats.Univariate or None + Proposed bias distribution + + """ + if bias is None: + return + + def mismatch_error(err_type, msg): + raise err_type(f"Support of parent {type(parent).__name__} and bias " + f"{type(bias).__name__} distributions do not match. " + f"{msg}") + + p_sup, b_sup = parent.support, bias.support + + if isinstance(p_sup, set) or isinstance(b_sup, set): + raise RuntimeError("Discrete distributions cannot be used as biasing " + "distributions or be biased by another Univariate " + "distribution. Instead, assign a vector of " + "alternate probabilities to the bias attribute.") + + elif isinstance(p_sup, dict) or isinstance (b_sup, dict): + raise RuntimeError("Mixture distributions cannot be used as biasing " + "distributions or be biased by another Univariate " + "distribution. Instead, instantiate the Mixture " + "object using biased member distributions, or " + "assign a vector of alternative probabilities to " + "the bias attribute.") + + elif isinstance(p_sup, tuple): + if isinstance(b_sup, tuple): + if p_sup != b_sup: + mismatch_error(ValueError, "") + else: + mismatch_error(TypeError, "Incompatible support types.") + + else: + raise TypeError("Unrecognized type for parent distribution support") diff --git a/src/chain.cpp b/src/chain.cpp index e279d1f59..a60ef12cd 100644 --- a/src/chain.cpp +++ b/src/chain.cpp @@ -70,8 +70,8 @@ ChainNuclide::~ChainNuclide() void DecayPhotonAngleEnergy::sample( double E_in, double& E_out, double& mu, uint64_t* seed) const { - E_out = photon_energy_->sample(seed); - mu = Uniform(-1., 1.).sample(seed); + E_out = photon_energy_->sample(seed).first; + mu = Uniform(-1., 1.).sample(seed).first; } //============================================================================== diff --git a/src/distribution.cpp b/src/distribution.cpp index ca00cbde4..537a56171 100644 --- a/src/distribution.cpp +++ b/src/distribution.cpp @@ -8,6 +8,7 @@ #include // for runtime_error #include // for string, stod +#include "openmc/constants.h" #include "openmc/error.h" #include "openmc/math_functions.h" #include "openmc/random_dist.h" @@ -16,6 +17,74 @@ namespace openmc { +//============================================================================== +// Helper function for computing importance weights from biased sampling +//============================================================================== + +vector compute_importance_weights( + const vector& p, const vector& b) +{ + std::size_t n = p.size(); + + // Normalize original probabilities + double sum_p = std::accumulate(p.begin(), p.end(), 0.0); + vector p_norm(n); + for (std::size_t i = 0; i < n; ++i) { + p_norm[i] = p[i] / sum_p; + } + + // Normalize bias probabilities + double sum_b = std::accumulate(b.begin(), b.end(), 0.0); + vector b_norm(n); + for (std::size_t i = 0; i < n; ++i) { + b_norm[i] = b[i] / sum_b; + } + + // Compute importance weights + vector weights(n); + for (std::size_t i = 0; i < n; ++i) { + weights[i] = (b_norm[i] == 0.0) ? INFTY : p_norm[i] / b_norm[i]; + } + return weights; +} + +std::pair Distribution::sample(uint64_t* seed) const +{ + if (bias_) { + // Sample from the bias distribution and compute importance weight + double val = bias_->sample_unbiased(seed); + double wgt = this->evaluate(val) / bias_->evaluate(val); + return {val, wgt}; + } else { + // Unbiased sampling: return sampled value with weight 1.0 + double val = sample_unbiased(seed); + return {val, 1.0}; + } +} + +// PDF evaluation not supported for all distribution types +double Distribution::evaluate(double x) const +{ + throw std::runtime_error( + "PDF evaluation not implemented for this distribution type."); +} + +void Distribution::read_bias_from_xml(pugi::xml_node node) +{ + if (check_for_node(node, "bias")) { + pugi::xml_node bias_node = node.child("bias"); + + if (check_for_node(bias_node, "bias")) { + openmc::fatal_error( + "Distribution has a bias distribution with its own bias distribution. " + "Please ensure bias distributions do not have their own bias."); + } + + UPtrDist bias = distribution_from_xml(bias_node); + this->set_bias(std::move(bias)); + } +} + //============================================================================== // DiscreteIndex implementation //============================================================================== @@ -36,7 +105,6 @@ DiscreteIndex::DiscreteIndex(span p) void DiscreteIndex::assign(span p) { prob_.assign(p.begin(), p.end()); - this->init_alias(); } @@ -115,24 +183,55 @@ void DiscreteIndex::normalize() // Discrete implementation //============================================================================== -Discrete::Discrete(pugi::xml_node node) : di_(node) +Discrete::Discrete(pugi::xml_node node) { auto params = get_node_array(node, "parameters"); - std::size_t n = params.size() / 2; + // First half is x values, second half is probabilities x_.assign(params.begin(), params.begin() + n); + const double* p = params.data() + n; + + // Check for bias + if (check_for_node(node, "bias")) { + // Get bias probabilities + auto bias_params = get_node_array(node, "bias"); + if (bias_params.size() != n) { + openmc::fatal_error( + "Size mismatch: Attempted to bias Discrete distribution with " + + std::to_string(n) + " probability entries using a bias with " + + std::to_string(bias_params.size()) + + " entries. Please ensure distributions have the same size."); + } + + // Compute importance weights + vector p_vec(p, p + n); + weight_ = compute_importance_weights(p_vec, bias_params); + + // Initialize DiscreteIndex with bias probabilities for sampling + di_.assign(bias_params); + } else { + // Unbiased case: weight_ stays empty + di_.assign({p, n}); + } } Discrete::Discrete(const double* x, const double* p, size_t n) : di_({p, n}) { - x_.assign(x, x + n); } -double Discrete::sample(uint64_t* seed) const +std::pair Discrete::sample(uint64_t* seed) const { - return x_[di_.sample(seed)]; + size_t idx = di_.sample(seed); + double wgt = weight_.empty() ? 1.0 : weight_[idx]; + return {x_[idx], wgt}; +} + +double Discrete::sample_unbiased(uint64_t* seed) const +{ + size_t idx = di_.sample(seed); + return x_[idx]; } //============================================================================== @@ -149,13 +248,26 @@ Uniform::Uniform(pugi::xml_node node) a_ = params.at(0); b_ = params.at(1); + + read_bias_from_xml(node); } -double Uniform::sample(uint64_t* seed) const +double Uniform::sample_unbiased(uint64_t* seed) const { return a_ + prn(seed) * (b_ - a_); } +double Uniform::evaluate(double x) const +{ + if (x <= a()) { + return 0.0; + } else if (x >= b()) { + return 0.0; + } else { + return 1 / (b() - a()); + } +} + //============================================================================== // PowerLaw implementation //============================================================================== @@ -175,9 +287,24 @@ PowerLaw::PowerLaw(pugi::xml_node node) offset_ = std::pow(a, n + 1); span_ = std::pow(b, n + 1) - offset_; ninv_ = 1 / (n + 1); + + read_bias_from_xml(node); } -double PowerLaw::sample(uint64_t* seed) const +double PowerLaw::evaluate(double x) const +{ + if (x <= a()) { + return 0.0; + } else if (x >= b()) { + return 0.0; + } else { + int pwr = n() + 1; + double norm = pwr / span_; + return norm * std::pow(std::fabs(x), n()); + } +} + +double PowerLaw::sample_unbiased(uint64_t* seed) const { return std::pow(offset_ + prn(seed) * span_, ninv_); } @@ -189,13 +316,21 @@ double PowerLaw::sample(uint64_t* seed) const Maxwell::Maxwell(pugi::xml_node node) { theta_ = std::stod(get_node_value(node, "parameters")); + + read_bias_from_xml(node); } -double Maxwell::sample(uint64_t* seed) const +double Maxwell::sample_unbiased(uint64_t* seed) const { return maxwell_spectrum(theta_, seed); } +double Maxwell::evaluate(double x) const +{ + double c = (2.0 / SQRT_PI) * std::pow(theta_, -1.5); + return c * std::sqrt(x) * std::exp(-x / theta_); +} + //============================================================================== // Watt implementation //============================================================================== @@ -209,13 +344,22 @@ Watt::Watt(pugi::xml_node node) a_ = params.at(0); b_ = params.at(1); + + read_bias_from_xml(node); } -double Watt::sample(uint64_t* seed) const +double Watt::sample_unbiased(uint64_t* seed) const { return watt_spectrum(a_, b_, seed); } +double Watt::evaluate(double x) const +{ + double c = + 2.0 / (std::sqrt(PI * b_) * std::pow(a_, 1.5) * std::exp(a_ * b_ / 4.0)); + return c * std::exp(-x / a_) * std::sinh(std::sqrt(b_ * x)); +} + //============================================================================== // Normal implementation //============================================================================== @@ -229,13 +373,22 @@ Normal::Normal(pugi::xml_node node) mean_value_ = params.at(0); std_dev_ = params.at(1); + + read_bias_from_xml(node); } -double Normal::sample(uint64_t* seed) const +double Normal::sample_unbiased(uint64_t* seed) const { return normal_variate(mean_value_, std_dev_, seed); } +double Normal::evaluate(double x) const +{ + return (1.0 / (std::sqrt(2.0 / PI) * std_dev_)) * + std::exp(-(std::pow((x - mean_value_), 2.0)) / + (2.0 * std::pow(std_dev_, 2.0))); +} + //============================================================================== // Tabular implementation //============================================================================== @@ -266,6 +419,8 @@ Tabular::Tabular(pugi::xml_node node) const double* x = params.data(); const double* p = x + n; init(x, p, n); + + read_bias_from_xml(node); } Tabular::Tabular(const double* x, const double* p, int n, Interpolation interp, @@ -314,7 +469,7 @@ void Tabular::init( } } -double Tabular::sample(uint64_t* seed) const +double Tabular::sample_unbiased(uint64_t* seed) const { // Sample value of CDF double c = prn(seed); @@ -356,11 +511,39 @@ double Tabular::sample(uint64_t* seed) const } } +double Tabular::evaluate(double x) const +{ + int i; + + if (interp_ == Interpolation::histogram) { + i = std::upper_bound(x_.begin(), x_.end(), x) - x_.begin() - 1; + if (i < 0 || i >= static_cast(p_.size())) { + return 0.0; + } else { + return p_[i]; + } + } else { + i = std::lower_bound(x_.begin(), x_.end(), x) - x_.begin() - 1; + + if (i < 0 || i >= static_cast(p_.size()) - 1) { + return 0.0; + } else { + double x0 = x_[i]; + double x1 = x_[i + 1]; + double p0 = p_[i]; + double p1 = p_[i + 1]; + + double t = (x - x0) / (x1 - x0); + return (1 - t) * p0 + t * p1; + } + } +} + //============================================================================== // Equiprobable implementation //============================================================================== -double Equiprobable::sample(uint64_t* seed) const +double Equiprobable::sample_unbiased(uint64_t* seed) const { std::size_t n = x_.size(); @@ -372,13 +555,27 @@ double Equiprobable::sample(uint64_t* seed) const return xl + ((n - 1) * r - i) * (xr - xl); } +double Equiprobable::evaluate(double x) const +{ + double x_min = *std::min_element(x_.begin(), x_.end()); + double x_max = *std::max_element(x_.begin(), x_.end()); + + if (x < x_min || x > x_max) { + return 0.0; + } else { + return 1.0 / (x_max - x_min); + } +} + //============================================================================== // Mixture implementation //============================================================================== Mixture::Mixture(pugi::xml_node node) { - double cumsum = 0.0; + vector probabilities; + + // First pass: collect distributions and their probabilities for (pugi::xml_node pair : node.children("pair")) { // Check that required data exists if (!pair.attribute("probability")) @@ -386,39 +583,60 @@ Mixture::Mixture(pugi::xml_node node) if (!pair.child("dist")) fatal_error("Mixture pair element does not have a distribution."); - // cummulative sum of probabilities + // Get probability and distribution double p = std::stod(pair.attribute("probability").value()); - - // Save cummulative probability and distribution auto dist = distribution_from_xml(pair.child("dist")); - cumsum += p * dist->integral(); - distribution_.push_back(std::make_pair(cumsum, std::move(dist))); + // Weight probability by the distribution's integral + double weighted_prob = p * dist->integral(); + probabilities.push_back(weighted_prob); + distribution_.push_back(std::move(dist)); } - // Save integral of distribution - integral_ = cumsum; + // Save sum of weighted probabilities + integral_ = std::accumulate(probabilities.begin(), probabilities.end(), 0.0); - // Normalize cummulative probabilities to 1 - for (auto& pair : distribution_) { - pair.first /= cumsum; + std::size_t n = probabilities.size(); + + // Check for bias + if (check_for_node(node, "bias")) { + // Get bias probabilities + auto bias_params = get_node_array(node, "bias"); + if (bias_params.size() != n) { + openmc::fatal_error( + "Size mismatch: Attempted to bias Mixture distribution with " + + std::to_string(n) + " components using a bias with " + + std::to_string(bias_params.size()) + + " entries. Please ensure distributions have the same size."); + } + + // Compute importance weights + weight_ = compute_importance_weights(probabilities, bias_params); + + // Initialize DiscreteIndex with bias probabilities for sampling + di_.assign(bias_params); + } else { + // Unbiased case: weight_ stays empty + di_.assign(probabilities); } } -double Mixture::sample(uint64_t* seed) const +std::pair Mixture::sample(uint64_t* seed) const { - // Sample value of CDF - const double p = prn(seed); - - // find matching distribution - const auto it = std::lower_bound(distribution_.cbegin(), distribution_.cend(), - p, [](const DistPair& pair, double p) { return pair.first < p; }); - - // This should not happen. Catch it - assert(it != distribution_.cend()); + size_t idx = di_.sample(seed); // Sample the chosen distribution - return it->second->sample(seed); + auto [val, sub_wgt] = distribution_[idx]->sample(seed); + + // Multiply by component selection weight + double mix_wgt = weight_.empty() ? 1.0 : weight_[idx]; + return {val, mix_wgt * sub_wgt}; +} + +double Mixture::sample_unbiased(uint64_t* seed) const +{ + size_t idx = di_.sample(seed); + return distribution_[idx]->sample(seed).first; } //============================================================================== diff --git a/src/distribution_angle.cpp b/src/distribution_angle.cpp index a571b2a5f..50f1aca11 100644 --- a/src/distribution_angle.cpp +++ b/src/distribution_angle.cpp @@ -75,7 +75,7 @@ double AngleDistribution::sample(double E, uint64_t* seed) const ++i; // Sample i-th distribution - double mu = distribution_[i]->sample(seed); + double mu = distribution_[i]->sample(seed).first; // Make sure mu is in range [-1,1] and return if (std::abs(mu) > 1.0) diff --git a/src/distribution_multi.cpp b/src/distribution_multi.cpp index cdb33adc2..857e1c30b 100644 --- a/src/distribution_multi.cpp +++ b/src/distribution_multi.cpp @@ -20,7 +20,7 @@ unique_ptr UnitSphereDistribution::create( if (check_for_node(node, "type")) type = get_node_value(node, "type", true, true); if (type == "isotropic") { - return UPtrAngle {new Isotropic()}; + return UPtrAngle {new Isotropic(node)}; } else if (type == "monodirectional") { return UPtrAngle {new Monodirectional(node)}; } else if (type == "mu-phi") { @@ -82,27 +82,63 @@ PolarAzimuthal::PolarAzimuthal(pugi::xml_node node) } } -Direction PolarAzimuthal::sample(uint64_t* seed) const +std::pair PolarAzimuthal::sample(uint64_t* seed) const +{ + return sample_impl(seed, false); +} + +std::pair PolarAzimuthal::sample_as_bias( + uint64_t* seed) const +{ + return sample_impl(seed, true); +} + +std::pair PolarAzimuthal::sample_impl( + uint64_t* seed, bool return_pdf) const { // Sample cosine of polar angle - double mu = mu_->sample(seed); - if (mu == 1.0) - return u_ref_; - if (mu == -1.0) - return -u_ref_; + auto [mu, mu_wgt] = mu_->sample(seed); // Sample azimuthal angle - double phi = phi_->sample(seed); + auto [phi, phi_wgt] = phi_->sample(seed); + + // Compute either the PDF value or the importance weight + double weight = + return_pdf ? (mu_->evaluate(mu) * phi_->evaluate(phi)) : (mu_wgt * phi_wgt); + + if (mu == 1.0) + return {u_ref_, weight}; + if (mu == -1.0) + return {-u_ref_, weight}; double f = std::sqrt(1 - mu * mu); - - return mu * u_ref_ + f * std::cos(phi) * v_ref_ + f * std::sin(phi) * w_ref_; + return {mu * u_ref_ + f * std::cos(phi) * v_ref_ + f * std::sin(phi) * w_ref_, + weight}; } //============================================================================== // Isotropic implementation //============================================================================== +Isotropic::Isotropic(pugi::xml_node node) : UnitSphereDistribution {node} +{ + if (check_for_node(node, "bias")) { + pugi::xml_node bias_node = node.child("bias"); + std::string bias_type = get_node_value(bias_node, "type", true, true); + if (bias_type != "mu-phi") { + openmc::fatal_error( + "Isotropic distributions may only be biased by a PolarAzimuthal."); + } + auto bias = std::make_unique(bias_node); + if (bias->mu()->bias() || bias->phi()->bias()) { + openmc::fatal_error( + "Attempted to bias Isotropic distribution with a biased PolarAzimuthal " + "distribution. Please ensure bias distributions are unbiased."); + } + this->set_bias(std::move(bias)); + } +} + Direction isotropic_direction(uint64_t* seed) { double phi = uniform_distribution(0., 2.0 * PI, seed); @@ -111,18 +147,23 @@ Direction isotropic_direction(uint64_t* seed) std::sqrt(1.0 - mu * mu) * std::sin(phi)}; } -Direction Isotropic::sample(uint64_t* seed) const +std::pair Isotropic::sample(uint64_t* seed) const { - return isotropic_direction(seed); + if (bias()) { + auto [val, eval] = bias()->sample_as_bias(seed); + return {val, 1.0 / (4.0 * PI * eval)}; + } else { + return {isotropic_direction(seed), 1.0}; + } } //============================================================================== // Monodirectional implementation //============================================================================== -Direction Monodirectional::sample(uint64_t* seed) const +std::pair Monodirectional::sample(uint64_t* seed) const { - return u_ref_; + return {u_ref_, 1.0}; } } // namespace openmc diff --git a/src/distribution_spatial.cpp b/src/distribution_spatial.cpp index d2b0f413b..80f8f7de1 100644 --- a/src/distribution_spatial.cpp +++ b/src/distribution_spatial.cpp @@ -80,9 +80,13 @@ CartesianIndependent::CartesianIndependent(pugi::xml_node node) } } -Position CartesianIndependent::sample(uint64_t* seed) const +std::pair CartesianIndependent::sample(uint64_t* seed) const { - return {x_->sample(seed), y_->sample(seed), z_->sample(seed)}; + auto [x_val, x_wgt] = x_->sample(seed); + auto [y_val, y_wgt] = y_->sample(seed); + auto [z_val, z_wgt] = z_->sample(seed); + Position xi {x_val, y_val, z_val}; + return {xi, x_wgt * y_wgt * z_wgt}; } //============================================================================== @@ -139,14 +143,16 @@ CylindricalIndependent::CylindricalIndependent(pugi::xml_node node) } } -Position CylindricalIndependent::sample(uint64_t* seed) const +std::pair CylindricalIndependent::sample(uint64_t* seed) const { - double r = r_->sample(seed); - double phi = phi_->sample(seed); + 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; - double z = z_->sample(seed) + origin_.z; - return {x, y, z}; + z += origin_.z; + Position xi {x, y, z}; + return {xi, r_wgt * phi_wgt * z_wgt}; } //============================================================================== @@ -203,16 +209,17 @@ SphericalIndependent::SphericalIndependent(pugi::xml_node node) } } -Position SphericalIndependent::sample(uint64_t* seed) const +std::pair SphericalIndependent::sample(uint64_t* seed) const { - double r = r_->sample(seed); - double cos_theta = cos_theta_->sample(seed); - double phi = phi_->sample(seed); + auto [r, r_wgt] = r_->sample(seed); + auto [cos_theta, cos_theta_wgt] = cos_theta_->sample(seed); + auto [phi, phi_wgt] = phi_->sample(seed); // sin(theta) by sin**2 + cos**2 = 1 double x = r * std::sqrt(1 - cos_theta * cos_theta) * cos(phi) + origin_.x; double y = r * std::sqrt(1 - cos_theta * cos_theta) * sin(phi) + origin_.y; double z = r * cos_theta + origin_.z; - return {x, y, z}; + Position xi {x, y, z}; + return {xi, r_wgt * cos_theta_wgt * phi_wgt}; } //============================================================================== @@ -260,6 +267,37 @@ MeshSpatial::MeshSpatial(pugi::xml_node node) } elem_idx_dist_.assign(strengths); + + if (check_for_node(node, "bias")) { + pugi::xml_node bias_node = node.child("bias"); + + if (check_for_node(bias_node, "strengths")) { + std::vector bias_strengths(n_bins, 1.0); + bias_strengths = get_node_array(node, "strengths"); + + if (bias_strengths.size() != n_bins) { + fatal_error( + fmt::format("Number of entries in the bias strengths array {} does " + "not match the number of entities in mesh {} ({}).", + bias_strengths.size(), mesh_id, n_bins)); + } + + if (get_node_value_bool(node, "volume_normalized")) { + for (int i = 0; i < n_bins; i++) { + bias_strengths[i] *= this->mesh()->volume(i); + } + } + + // Compute importance weights + weight_ = compute_importance_weights(strengths, bias_strengths); + + // Re-initialize DiscreteIndex with bias strengths for sampling + elem_idx_dist_.assign(bias_strengths); + } else { + fatal_error(fmt::format( + "Bias node for mesh {} found without strengths array.", mesh_id)); + } + } } MeshSpatial::MeshSpatial(int32_t mesh_idx, span strengths) @@ -295,9 +333,11 @@ std::pair MeshSpatial::sample_mesh(uint64_t* seed) const return {elem_idx, mesh()->sample_element(elem_idx, seed)}; } -Position MeshSpatial::sample(uint64_t* seed) const +std::pair MeshSpatial::sample(uint64_t* seed) const { - return this->sample_mesh(seed).second; + auto [elem_idx, u] = this->sample_mesh(seed); + double wgt = weight_.empty() ? 1.0 : weight_[elem_idx]; + return {u, wgt}; } //============================================================================== @@ -328,6 +368,31 @@ PointCloud::PointCloud(pugi::xml_node node) } point_idx_dist_.assign(strengths); + + if (check_for_node(node, "bias")) { + pugi::xml_node bias_node = node.child("bias"); + + if (check_for_node(bias_node, "strengths")) { + std::vector bias_strengths(point_cloud_.size(), 1.0); + bias_strengths = get_node_array(node, "strengths"); + + if (bias_strengths.size() != point_cloud_.size()) { + fatal_error( + fmt::format("Number of entries in the bias strengths array {} does " + "not match the number of spatial points provided {}.", + bias_strengths.size(), point_cloud_.size())); + } + + // Compute importance weights + weight_ = compute_importance_weights(strengths, bias_strengths); + + // Re-initialize DiscreteIndex with bias strengths for sampling + point_idx_dist_.assign(bias_strengths); + } else { + fatal_error( + fmt::format("Bias node for PointCloud found without strengths array.")); + } + } } PointCloud::PointCloud( @@ -337,10 +402,11 @@ PointCloud::PointCloud( point_idx_dist_.assign(strengths); } -Position PointCloud::sample(uint64_t* seed) const +std::pair PointCloud::sample(uint64_t* seed) const { int32_t index = point_idx_dist_.sample(seed); - return point_cloud_[index]; + double wgt = weight_.empty() ? 1.0 : weight_[index]; + return {point_cloud_[index], wgt}; } //============================================================================== @@ -360,10 +426,10 @@ SpatialBox::SpatialBox(pugi::xml_node node, bool fission) upper_right_ = Position {params[3], params[4], params[5]}; } -Position SpatialBox::sample(uint64_t* seed) const +std::pair SpatialBox::sample(uint64_t* seed) const { Position xi {prn(seed), prn(seed), prn(seed)}; - return lower_left_ + xi * (upper_right_ - lower_left_); + return {lower_left_ + xi * (upper_right_ - lower_left_), 1.0}; } //============================================================================== @@ -382,9 +448,9 @@ SpatialPoint::SpatialPoint(pugi::xml_node node) r_ = Position {params.data()}; } -Position SpatialPoint::sample(uint64_t* seed) const +std::pair SpatialPoint::sample(uint64_t* seed) const { - return r_; + return {r_, 1.0}; } } // namespace openmc diff --git a/src/secondary_correlated.cpp b/src/secondary_correlated.cpp index 3f26fdb01..e820419b4 100644 --- a/src/secondary_correlated.cpp +++ b/src/secondary_correlated.cpp @@ -247,9 +247,9 @@ void CorrelatedAngleEnergy::sample( // Find correlated angular distribution for closest outgoing energy bin if (r1 - c_k < c_k1 - r1 || distribution_[l].interpolation == Interpolation::histogram) { - mu = distribution_[l].angle[k]->sample(seed); + mu = distribution_[l].angle[k]->sample(seed).first; } else { - mu = distribution_[l].angle[k + 1]->sample(seed); + mu = distribution_[l].angle[k + 1]->sample(seed).first; } } diff --git a/src/source.cpp b/src/source.cpp index f6aa665eb..b30b964d3 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -356,6 +356,8 @@ SourceSite IndependentSource::sample(uint64_t* seed) const { 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; @@ -365,7 +367,9 @@ SourceSite IndependentSource::sample(uint64_t* seed) const while (!accepted) { // Sample spatial distribution - site.r = space_->sample(seed); + auto [r, r_wgt_temp] = space_->sample(seed); + site.r = r; + r_wgt = r_wgt_temp; // Check if sampled position satisfies spatial constraints accepted = satisfies_spatial_constraints(site.r); @@ -378,7 +382,10 @@ SourceSite IndependentSource::sample(uint64_t* seed) const } // Sample angle - site.u = angle_->sample(seed); + auto [u, u_wgt] = angle_->sample(seed); + site.u = u; + + site.wgt = r_wgt * u_wgt; // Sample energy and time for neutron and photon sources if (settings::solver_type != SolverType::RANDOM_RAY) { @@ -395,7 +402,9 @@ SourceSite IndependentSource::sample(uint64_t* seed) const while (true) { // Sample energy spectrum - site.E = energy_->sample(seed); + auto [E, E_wgt_temp] = energy_->sample(seed); + site.E = E; + E_wgt = E_wgt_temp; // Resample if energy falls above maximum particle energy if (site.E < data::energy_max[p] && @@ -407,7 +416,10 @@ SourceSite IndependentSource::sample(uint64_t* seed) const } // Sample particle creation time - site.time = time_->sample(seed); + auto [time, time_wgt] = time_->sample(seed); + site.time = time; + + site.wgt *= (E_wgt * time_wgt); } // Increment number of accepted samples @@ -537,9 +549,9 @@ CompiledSourceWrapper::~CompiledSourceWrapper() // MeshElementSpatial implementation //============================================================================== -Position MeshElementSpatial::sample(uint64_t* seed) const +std::pair MeshElementSpatial::sample(uint64_t* seed) const { - return model::meshes[mesh_index_]->sample_element(elem_index_, seed); + return {model::meshes[mesh_index_]->sample_element(elem_index_, seed), 1.0}; } //============================================================================== diff --git a/tests/cpp_unit_tests/test_distribution.cpp b/tests/cpp_unit_tests/test_distribution.cpp index e1a212db5..2024b812e 100644 --- a/tests/cpp_unit_tests/test_distribution.cpp +++ b/tests/cpp_unit_tests/test_distribution.cpp @@ -28,7 +28,7 @@ TEST_CASE("Test alias method sampling of a discrete distribution") int counter = 0; for (size_t i = 0; i < n_samples; i++) { - auto sample = dist.sample(&seed); + auto sample = dist.sample(&seed).first; std += sample * sample / n_samples; dist_mean += sample; @@ -61,7 +61,7 @@ TEST_CASE("Test alias sampling method for pugixml constructor") // Initialize discrete distribution and seed openmc::Discrete dist(energy); uint64_t seed = openmc::init_seed(0, 0); - auto sample = dist.sample(&seed); + auto sample = dist.sample(&seed).first; // Assertions REQUIRE(dist.x().size() == 3); diff --git a/tests/regression_tests/source/results_true.dat b/tests/regression_tests/source/results_true.dat index 7d03c696d..c671463be 100644 --- a/tests/regression_tests/source/results_true.dat +++ b/tests/regression_tests/source/results_true.dat @@ -1,2 +1,2 @@ k-combined: -3.080655E-01 4.837707E-03 +2.942254E-01 2.571435E-03 diff --git a/tests/unit_tests/__init__.py b/tests/unit_tests/__init__.py index e97ab13e2..44b5a5018 100644 --- a/tests/unit_tests/__init__.py +++ b/tests/unit_tests/__init__.py @@ -14,3 +14,12 @@ def assert_unbounded(obj): ll, ur = obj.bounding_box assert ll == pytest.approx((-np.inf, -np.inf, -np.inf)) assert ur == pytest.approx((np.inf, np.inf, np.inf)) + + +def assert_sample_mean(samples, expected_mean): + # Calculate sample standard deviation + std_dev = samples.std() / np.sqrt(samples.size - 1) + + # Means should agree within 4 sigma 99.993% of the time. Note that this is + # expected to fail about 1 out of 16,000 times + assert np.abs(expected_mean - samples.mean()) < 4*std_dev diff --git a/tests/unit_tests/test_source_biasing.py b/tests/unit_tests/test_source_biasing.py new file mode 100644 index 000000000..12588594c --- /dev/null +++ b/tests/unit_tests/test_source_biasing.py @@ -0,0 +1,276 @@ +"""Tests for source biasing using C++ sampling routines via openmc.lib + +This test module validates that the C++ distribution sampling implementations +correctly handle both unbiased and biased sampling when used in source +definitions. Each test: + +1. Creates a minimal model with a source using a specific energy distribution +2. Uses model.sample_external_source() to generate samples via openmc.lib +3. Extracts energies from the returned particle list +4. Validates that: + - Unbiased sampling produces the expected mean + - Biased sampling with importance weighting produces the expected mean + - Weights are correctly applied (non-unity for biased case) + +These tests complement the Python-level tests in test_stats.py by exercising +the full C++ sampling codepath that is used during actual simulations. +""" + +import numpy as np +import pytest +import openmc + +from tests.unit_tests import assert_sample_mean + + +@pytest.fixture +def model(): + """Create a minimal model for source sampling tests.""" + sphere = openmc.Sphere(r=100.0, boundary_type='vacuum') + cell = openmc.Cell(region=-sphere) + geometry = openmc.Geometry([cell]) + settings = openmc.Settings(particles=100, batches=1) + space = openmc.stats.Point() + angle = openmc.stats.Monodirectional((1.0, 0.0, 0.0)) + settings.source = openmc.IndependentSource(space=space, angle=angle) + return openmc.Model(geometry=geometry, settings=settings) + + +@pytest.mark.flaky(reruns=1) +def test_discrete(run_in_tmpdir, model): + """Test Discrete distribution sampling via C++ routines.""" + vals = np.array([1.0, 2.0, 3.0]) + probs = np.array([0.1, 0.7, 0.2]) + exp_mean = (vals * probs).sum() + + # Create source with discrete energy distribution + model.settings.source[0].energy = energy_dist = openmc.stats.Discrete(vals, probs) + + # Sample using C++ routines and extract energies + n_samples = 10_000 + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + + # Check unbiased mean + assert_sample_mean(energies, exp_mean) + + # Sample from biased distribution + energy_dist.bias = np.array([0.2, 0.1, 0.7]) + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + weights = np.array([p.wgt for p in particles]) + + # Check biased weighted mean + weighted_energies = energies * weights + assert_sample_mean(weighted_energies, exp_mean) + assert np.any(weights != 1.0) + + +@pytest.mark.flaky(reruns=1) +def test_uniform(run_in_tmpdir, model): + """Test Uniform distribution sampling via C++ routines.""" + a, b = 5.0, 10.0 + exp_mean = 0.5 * (a + b) + + # Create source with uniform energy distribution + model.settings.source[0].energy = energy_dist = openmc.stats.Uniform(a, b) + + # Sample using C++ routines and extract energies + n_samples = 10_000 + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + + # Check unbiased mean + assert_sample_mean(energies, exp_mean) + + # Sample from biased distribution + energy_dist.bias = openmc.stats.PowerLaw(a, b, 2) + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + weights = np.array([p.wgt for p in particles]) + + # Check biased weighted mean + weighted_energies = energies * weights + assert_sample_mean(weighted_energies, exp_mean) + assert np.any(weights != 1.0) + + +@pytest.mark.flaky(reruns=1) +def test_powerlaw(run_in_tmpdir, model): + """Test PowerLaw distribution sampling via C++ routines.""" + a, b, n = 1.0, 20.0, 2.0 + + # Determine mean of distribution + exp_mean = (n+1)*(b**(n+2) - a**(n+2))/((n+2)*(b**(n+1) - a**(n+1))) + + # Create source with powerlaw energy distribution + model.settings.source[0].energy = energy_dist = openmc.stats.PowerLaw(a, b, n) + + # Sample using C++ routines and extract energies + n_samples = 10_000 + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + + # Check unbiased mean + assert_sample_mean(energies, exp_mean) + + # Sample from biased distribution + energy_dist.bias = openmc.stats.Uniform(a, b) + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + weights = np.array([p.wgt for p in particles]) + + # Check biased weighted mean + weighted_energies = energies * weights + assert_sample_mean(weighted_energies, exp_mean) + assert np.any(weights != 1.0) + + +@pytest.mark.flaky(reruns=1) +def test_maxwell(run_in_tmpdir, model): + """Test Maxwell distribution sampling via C++ routines.""" + theta = 1.2895e6 + exp_mean = 3/2 * theta + + # Create source with Maxwell energy distribution + model.settings.source[0].energy = energy_dist = openmc.stats.Maxwell(theta) + + # Sample using C++ routines and extract energies + n_samples = 10_000 + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + + # Check unbiased mean + assert_sample_mean(energies, exp_mean) + + # Sample from biased distribution + energy_dist.bias = openmc.stats.Maxwell(theta * 1.1) + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + weights = np.array([p.wgt for p in particles]) + + # Check biased weighted mean + weighted_energies = energies * weights + assert_sample_mean(weighted_energies, exp_mean) + assert np.any(weights != 1.0) + + +@pytest.mark.flaky(reruns=1) +def test_watt(run_in_tmpdir, model): + """Test Watt distribution sampling via C++ routines.""" + a, b = 0.965e6, 2.29e-6 + exp_mean = 3/2 * a + a**2 * b / 4 + + # Create source with Watt energy distribution + model.settings.source[0].energy = energy_dist = openmc.stats.Watt(a, b) + + # Sample using C++ routines and extract energies + n_samples = 10_000 + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + + # Check unbiased mean + assert_sample_mean(energies, exp_mean) + + # Sample from biased distribution + energy_dist.bias = openmc.stats.Watt(a*1.05, b) + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + weights = np.array([p.wgt for p in particles]) + + # Check biased weighted mean + weighted_energies = energies * weights + assert_sample_mean(weighted_energies, exp_mean) + assert np.any(weights != 1.0) + + +@pytest.mark.flaky(reruns=1) +def test_tabular(run_in_tmpdir, model): + """Test Tabular distribution sampling via C++ routines.""" + # Test linear-linear sampling + x = np.array([0.0, 5.0, 7.0, 10.0]) + p = np.array([10.0, 20.0, 5.0, 6.0]) + + # Create tabular distribution and normalize to get expected mean + model.settings.source[0].energy = energy_dist = openmc.stats.Tabular(x, p, 'linear-linear') + energy_dist.normalize() + exp_mean = energy_dist.mean() + + # Sample using C++ routines and extract energies + n_samples = 10_000 + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + + # Check unbiased mean + assert_sample_mean(energies, exp_mean) + + # Sample from biased distribution + energy_dist.bias = openmc.stats.Uniform(x[0], x[-1]) + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + weights = np.array([p.wgt for p in particles]) + + # Check biased weighted mean + weighted_energies = energies * weights + assert_sample_mean(weighted_energies, exp_mean) + assert np.any(weights != 1.0) + + +@pytest.mark.flaky(reruns=1) +def test_mixture(run_in_tmpdir, model): + """Test Mixture distribution sampling via C++ routines.""" + d1 = openmc.stats.Uniform(0, 5) + d2 = openmc.stats.Uniform(3, 7) + p = [0.5, 0.5] + + # Create mixture energy distribution + model.settings.source[0].energy = energy_dist = openmc.stats.Mixture(p, [d1, d2]) + exp_mean = (2.5 + 5.0) / 2 + + # Sample using C++ routines and extract energies + n_samples = 10_000 + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + + # Check unbiased mean + assert_sample_mean(energies, exp_mean) + + # Sample using biased sub-distribution + energy_dist.distribution[0].bias = openmc.stats.PowerLaw(0, 5, 2) + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + weights = np.array([p.wgt for p in particles]) + + # Check biased weighted mean + weighted_energies = energies * weights + assert_sample_mean(weighted_energies, exp_mean) + assert np.any(weights != 1.0) + + +@pytest.mark.flaky(reruns=1) +def test_normal(run_in_tmpdir, model): + """Test Normal distribution sampling via C++ routines.""" + mean_val = 25.0 + std_dev = 2.0 + + # Create source with normal energy distribution + model.settings.source[0].energy = energy_dist = openmc.stats.Normal(mean_val, std_dev) + + # Sample using C++ routines and extract energies + n_samples = 10_000 + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + + # Check unbiased mean + assert_sample_mean(energies, mean_val) + + # Sample from biased distribution + energy_dist.bias = openmc.stats.Normal(mean_val * 1.1, std_dev) + particles = model.sample_external_source(n_samples) + energies = np.array([p.E for p in particles]) + weights = np.array([p.wgt for p in particles]) + + # Check biased weighted mean + weighted_energies = energies * weights + assert_sample_mean(weighted_energies, mean_val) + assert np.any(weights != 1.0) diff --git a/tests/unit_tests/test_stats.py b/tests/unit_tests/test_stats.py index 998d4b984..e92e9e135 100644 --- a/tests/unit_tests/test_stats.py +++ b/tests/unit_tests/test_stats.py @@ -6,14 +6,7 @@ import openmc import openmc.stats from scipy.integrate import trapezoid - -def assert_sample_mean(samples, expected_mean): - # Calculate sample standard deviation - std_dev = samples.std() / np.sqrt(samples.size - 1) - - # Means should agree within 4 sigma 99.993% of the time. Note that this is - # expected to fail about 1 out of 16,000 times - assert np.abs(expected_mean - samples.mean()) < 4*std_dev +from tests.unit_tests import assert_sample_mean @pytest.mark.flaky(reruns=1) @@ -47,8 +40,19 @@ def test_discrete(): # sample discrete distribution and check that the mean of the samples is # within 4 std. dev. of the expected mean n_samples = 1_000_000 - samples = d3.sample(n_samples) + samples, weights = d3.sample(n_samples) assert_sample_mean(samples, exp_mean) + assert np.all(weights == 1.0) + + # Test biased distribution + d3.bias = np.array([0.2, 0.1, 0.7]) + bias_elem = d3.to_xml_element('distribution') + d4 = openmc.stats.Univariate.from_xml_element(bias_elem) + np.testing.assert_array_equal(d4.bias, [0.2, 0.1, 0.7]) + samples, weights = d4.sample(n_samples) + weighted_sample = samples * weights + assert_sample_mean(weighted_sample, exp_mean) + assert np.all(weights != 1.0) def test_delta_function(): @@ -83,6 +87,28 @@ def test_merge_discrete(): assert triple.integral() == pytest.approx(6.0) +def test_merge_discrete_with_bias(): + # Two discrete distributions with different biases + d1 = openmc.stats.Discrete([1.0, 2.0], [0.5, 0.5]) + d2 = openmc.stats.Discrete([2.0, 3.0], [0.3, 0.7], bias=[0.1, 0.9]) + + merged = openmc.stats.Discrete.merge([d1, d2], [0.6, 0.4]) + exp_mean = 0.6 * d1.mean() + 0.4 * d2.mean() + + # Verify merged distribution has correct x values + assert set(merged.x) == {1.0, 2.0, 3.0} + + # Bias should not be changed in original distributions + assert d1.bias is None + assert np.all(d2.bias == [0.1, 0.9]) + + # Sample and verify bias is applied correctly + samples, weights = merged.sample(10_000) + + # Verify weighted mean matches expected unbiased mean + assert_sample_mean(samples*weights, exp_mean) + + def test_clip_discrete(): # Create discrete distribution with two points that are not important, one # because the x value is very small, and one because the p value is very @@ -125,8 +151,19 @@ def test_uniform(): # std. dev. of the expected mean exp_mean = 0.5 * (a + b) n_samples = 1_000_000 - samples = d.sample(n_samples) + samples, weights = d.sample(n_samples) assert_sample_mean(samples, exp_mean) + assert np.all(weights == 1.0) + + # Test biased distribution + d.bias = openmc.stats.PowerLaw(a, b, 2) + bias_elem = d.to_xml_element('distribution') + d2 = openmc.stats.Univariate.from_xml_element(bias_elem) + assert isinstance (d2.bias, openmc.stats.PowerLaw) + samples, weights = d2.sample(n_samples) + weighted_sample = samples * weights + assert_sample_mean(weighted_sample, exp_mean) + assert np.all(weights != 1.0) @pytest.mark.flaky(reruns=1) @@ -147,8 +184,19 @@ def test_powerlaw(): # sample power law distribution and check that the mean of the samples is # within 4 std. dev. of the expected mean n_samples = 1_000_000 - samples = d.sample(n_samples) + samples, weights = d.sample(n_samples) assert_sample_mean(samples, exp_mean) + assert np.all(weights == 1.0) + + # Test biased distribution + d.bias = openmc.stats.Uniform(a, b) + bias_elem = d.to_xml_element('distribution') + d2 = openmc.stats.Univariate.from_xml_element(bias_elem) + assert isinstance (d2.bias, openmc.stats.Uniform) + samples, weights = d2.sample(n_samples) + weighted_sample = samples * weights + assert_sample_mean(weighted_sample, exp_mean) + assert np.all(weights != 1.0) @pytest.mark.flaky(reruns=1) @@ -166,13 +214,25 @@ def test_maxwell(): # sample maxwell distribution and check that the mean of the samples is # within 4 std. dev. of the expected mean n_samples = 1_000_000 - samples = d.sample(n_samples) + samples, weights = d.sample(n_samples) assert_sample_mean(samples, exp_mean) + assert np.all(weights == 1.0) # A second sample starting from a different seed - samples_2 = d.sample(n_samples) + samples_2, weights_2 = d.sample(n_samples) assert_sample_mean(samples_2, exp_mean) assert samples_2.mean() != samples.mean() + assert np.all(weights_2 == 1) + + # Test biased distribution + d.bias = openmc.stats.Maxwell((theta * 1.1)) + bias_elem = d.to_xml_element('distribution') + d2 = openmc.stats.Univariate.from_xml_element(bias_elem) + assert isinstance (d2.bias, openmc.stats.Maxwell) + samples, weights = d2.sample(n_samples) + weighted_sample = samples * weights + assert_sample_mean(weighted_sample, exp_mean) + assert np.all(weights != 1.0) @pytest.mark.flaky(reruns=1) @@ -195,8 +255,19 @@ def test_watt(): # sample Watt distribution and check that the mean of the samples is within # 4 std. dev. of the expected mean n_samples = 1_000_000 - samples = d.sample(n_samples) + samples, weights = d.sample(n_samples) assert_sample_mean(samples, exp_mean) + assert np.all(weights == 1.0) + + # Test biased distribution with 5 percent higher T_e + d.bias = openmc.stats.Watt(a*1.05, b) + bias_elem = d.to_xml_element('distribution') + d2 = openmc.stats.Univariate.from_xml_element(bias_elem) + assert isinstance (d2.bias, openmc.stats.Watt) + samples, weights = d2.sample(n_samples) + weighted_sample = samples * weights + assert_sample_mean(weighted_sample, exp_mean) + assert np.all(weights != 1.0) @pytest.mark.flaky(reruns=1) @@ -206,8 +277,9 @@ def test_tabular(): p = np.array([10.0, 20.0, 5.0, 6.0]) d = openmc.stats.Tabular(x, p, 'linear-linear') n_samples = 100_000 - samples = d.sample(n_samples) + samples, weights = d.sample(n_samples) assert_sample_mean(samples, d.mean()) + assert np.all(weights == 1.0) # test linear-linear normalization d.normalize() @@ -215,8 +287,9 @@ def test_tabular(): # test histogram sampling d = openmc.stats.Tabular(x, p, interpolation='histogram') - samples = d.sample(n_samples) + samples, weights = d.sample(n_samples) assert_sample_mean(samples, d.mean()) + assert np.all(weights == 1.0) d.normalize() assert d.integral() == pytest.approx(1.0) @@ -226,7 +299,7 @@ def test_tabular(): d = openmc.stats.Tabular(x, p[:-1], interpolation='histogram') d.cdf() d.mean() - assert_sample_mean(d.sample(n_samples), d.mean()) + assert_sample_mean(d.sample(n_samples)[0], d.mean()) # passing a shorter probability set should raise an error for linear-linear with pytest.raises(ValueError): @@ -238,6 +311,16 @@ def test_tabular(): d = openmc.stats.Tabular(x, p, interpolation='linear-linear') d.cdf() + # Test biased distribution + d.bias = openmc.stats.Uniform(x[0], x[-1]) + bias_elem = d.to_xml_element('distribution') + d2 = openmc.stats.Univariate.from_xml_element(bias_elem) + assert isinstance (d2.bias, openmc.stats.Uniform) + samples, weights = d2.sample(n_samples) + weighted_sample = samples * weights + assert_sample_mean(weighted_sample, d2.mean()) + assert np.all(weights != 1.0) + def test_tabular_from_xml(): x = np.array([0.0, 5.0, 7.0, 10.0]) @@ -288,8 +371,9 @@ def test_mixture(): # Sample and make sure sample mean is close to expected mean n_samples = 1_000_000 - samples = mix.sample(n_samples) + samples, weights = mix.sample(n_samples) assert_sample_mean(samples, (2.5 + 5.0)/2) + assert np.all(weights == 1.0) elem = mix.to_xml_element('distribution') @@ -298,6 +382,26 @@ def test_mixture(): assert d.distribution == [d1, d2] assert len(d) == 4 + # Test biased sub-distribution + d.distribution[0].bias = openmc.stats.PowerLaw(0, 5, 2) + bias_elem = d.to_xml_element('distribution') + d3 = openmc.stats.Univariate.from_xml_element(bias_elem) + assert isinstance (d3.distribution[0].bias, openmc.stats.PowerLaw) + samples, weights = d3.sample(n_samples) + weighted_sample = samples * weights + assert_sample_mean(weighted_sample, (2.5 + 5.0)/2) + + # Test biased meta-probability + d.distribution[0].bias = None + d.bias = [0.25, 0.75] + bias_elem_2 = d.to_xml_element('distribution') + d4 = openmc.stats.Univariate.from_xml_element(bias_elem_2) + assert isinstance (d4.bias, np.ndarray) + samples, weights = d4.sample(n_samples) + weighted_sample = samples * weights + assert_sample_mean(weighted_sample, (2.5 + 5.0)/2) + assert np.all(weights != 1.0) + def test_mixture_clip(): # Create mixture distribution containing a discrete distribution with two @@ -330,6 +434,13 @@ def test_mixture_clip(): with pytest.warns(UserWarning): mix_clip = mix.clip(1e-6) + # Make sure warning is raised if a biased Discrete is clipped + d3 = openmc.stats.Discrete([1.0, 1.001], [1.0, 0.7e-8]) + d3.bias = [0.9, 0.1] + mix = openmc.stats.Mixture([1.0, 1.0], [d3, d2]) + with pytest.raises(RuntimeError): + mix_clip = mix.clip(1e-6) + def test_polar_azimuthal(): # default polar-azimuthal should be uniform in mu and phi @@ -365,12 +476,17 @@ def test_polar_azimuthal(): def test_isotropic(): d = openmc.stats.Isotropic() + mu = openmc.stats.Uniform(-1.0, 1.0) + phi = openmc.stats.PowerLaw(0., 2*np.pi, 2) + d2 = openmc.stats.PolarAzimuthal(mu, phi) + d.bias = d2 elem = d.to_xml_element() assert elem.tag == 'angle' assert elem.attrib['type'] == 'isotropic' d = openmc.stats.Isotropic.from_xml_element(elem) assert isinstance(d, openmc.stats.Isotropic) + assert isinstance(d.bias, openmc.stats.PolarAzimuthal) def test_monodirectional(): @@ -387,6 +503,7 @@ def test_cartesian(): x = openmc.stats.Uniform(-10., 10.) y = openmc.stats.Uniform(-10., 10.) z = openmc.stats.Uniform(0., 20.) + z.bias = openmc.stats.PowerLaw(0., 20., 3) d = openmc.stats.CartesianIndependent(x, y, z) elem = d.to_xml_element() @@ -402,6 +519,7 @@ def test_cartesian(): d = openmc.stats.Spatial.from_xml_element(elem) assert isinstance(d, openmc.stats.CartesianIndependent) + assert isinstance (d.z.bias, openmc.stats.PowerLaw) def test_box(): @@ -448,8 +566,19 @@ def test_normal(): # sample normal distribution n_samples = 100_000 - samples = d.sample(n_samples) + samples, weights = d.sample(n_samples) assert_sample_mean(samples, mean) + assert np.all(weights == 1.0) + + # Test biased distribution + d.bias = openmc.stats.Normal(10.0, 4.0) + bias_elem = d.to_xml_element('distribution') + d2 = openmc.stats.Univariate.from_xml_element(bias_elem) + assert isinstance (d2.bias, openmc.stats.Normal) + samples, weights = d2.sample(n_samples) + weighted_sample = samples * weights + assert_sample_mean(weighted_sample, mean) + assert np.all(weights != 1.0) @pytest.mark.flaky(reruns=1) @@ -468,8 +597,9 @@ def test_muir(): # sample muir distribution n_samples = 100_000 - samples = d.sample(n_samples) + samples, weights = d.sample(n_samples) assert_sample_mean(samples, mean) + assert np.all(weights == 1.0) @pytest.mark.flaky(reruns=1) @@ -514,8 +644,27 @@ def test_combine_distributions(): # Sample the combined distribution and make sure the sample mean is within # uncertainty of the expected value - samples = combined.sample(10_000) + samples, weights = combined.sample(10_000) assert_sample_mean(samples, 0.25) + assert np.all(weights == 1.0) + + # If biased/unbiased Discrete distributions are combined, unbiased probability + # should be conserved and points from both original distributions should be + # assigned bias probabilities. + x1 = [0.0, 1.0, 10.0] + p1 = [0.3, 0.2, 0.5] + b1 = [0.2, 0.5, 0.3] + d1 = openmc.stats.Discrete(x1, p1, b1) + x2 = [0.5, 1.0, 5.0] + p2 = [0.4, 0.5, 0.1] + d2 = openmc.stats.Discrete(x2, p2) + combined = openmc.stats.combine_distributions([d1, d2, t1], [0.25, 0.25, 0.5]) + + p3 = [0.075, 0.1, 0.175, 0.025, 0.125] + b3 = [0.05, 0.1, 0.25, 0.025, 0.075] + assert all(combined.distribution[-1].p == p3) + assert all(combined.distribution[-1].bias == b3) + def test_reference_vwu_projection(): """When a non-orthogonal vector is provided, the setter should project out From 84a413b130d97a6bc6f3770fc7374617f701cede Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Tue, 13 Jan 2026 21:34:07 +0100 Subject: [PATCH 498/671] Speed up reaction rate lookup for FluxCollapseHelper (#3724) --- openmc/deplete/helpers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openmc/deplete/helpers.py b/openmc/deplete/helpers.py index f24b89868..14780d61a 100644 --- a/openmc/deplete/helpers.py +++ b/openmc/deplete/helpers.py @@ -379,17 +379,17 @@ class FluxCollapseHelper(ReactionRateHelper): nuclides_direct = self._rate_tally.nuclides shape = (len(nuclides_direct), len(self._reactions_direct)) rx_rates = self.rate_tally_means[mat_index].reshape(shape) + direct_rx_index = {score: i for i, score in enumerate(self._reactions_direct)} + direct_nuc_index = {nuc: i for i, nuc in enumerate(nuclides_direct)} mat = self._materials[mat_index] for name, i_nuc in zip(self.nuclides, nuc_index): for mt, score, i_rx in zip(self._mts, self._scores, react_index): if score in self._reactions_direct and name in nuclides_direct: - # Determine index in rx_rates - i_rx_direct = self._reactions_direct.index(score) - i_nuc_direct = nuclides_direct.index(name) - # Get reaction rate from tally + i_rx_direct = direct_rx_index[score] + i_nuc_direct = direct_nuc_index[name] self._results_cache[i_nuc, i_rx] = rx_rates[i_nuc_direct, i_rx_direct] else: # Use flux to collapse reaction rate (per N) From 65e19c1d538bdf33471a50cdfe928b70cc2d1c3e Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Tue, 13 Jan 2026 23:31:01 +0200 Subject: [PATCH 499/671] Fix settings io_format documentation. (#3718) Co-authored-by: Paul Romano --- docs/source/io_formats/settings.rst | 66 ++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index b986a526a..8fc26ae07 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -277,6 +277,15 @@ ignored for all run modes other than "eigenvalue". *Default*: 1 +------------------------------ +```` Element +------------------------------ + +The ```` element indicates the number of generations to +consider for the Iterated Fission Probability method. + + *Default*: 10 + ---------------------- ```` Element ---------------------- @@ -402,7 +411,25 @@ then, OpenMC will only use up to the :math:`P_1` data. ```` Element -------------------------------- -The ```` element indicates the number of times a particle can split during a history. +The ```` element indicates the number of times a particle +can split during a history. + + *Default*: 1000 + +----------------------------- +```` Element +----------------------------- + +The ```` element indicates the maximum secondary bank size. + + *Default*: 10000 + +------------------------ +```` Element +------------------------ + +The ```` element indicates the maximum number of tracks written to a +track file (per MPI process). *Default*: 1000 @@ -1378,6 +1405,15 @@ has the following attributes/sub-elements: for fixed source and small criticality calculations, but is very optimistic for highly coupled full-core reactor problems. +------------------------------------- +```` Element +------------------------------------- + +The ```` element indicates whether to sample among +multiple sources uniformly, applying their strengths as weights to sampled +particles. + + *Default*: False ------------------------ ```` Element @@ -1390,6 +1426,16 @@ Agency Monte Carlo Performance Benchmark Problem," Proceedings of *Physor 2012*, Knoxville, TN (2012). The mesh should cover all possible fissionable materials in the problem and is specified using a :ref:`mesh_element`. +------------------------------- +```` Element +------------------------------- + +The ```` element indicates whether to produce decay photons +from neutron reactions instead of prompt photons. This is used in conjunction +with the direct 1-step method for shutdown dose rate calculations. + + *Default*: False + .. _verbosity: ----------------------- @@ -1620,3 +1666,21 @@ following sub-elements/attributes: The ``weight_windows_file`` element has no attributes and contains the path to a weight windows HDF5 file to load during simulation initialization. + +------------------------------- +```` Element +------------------------------- + + The ``weight_windows_on`` element indicates whether weight windows are + enabled. + + *Default*: False + +---------------------------------- +```` Element +---------------------------------- + + The ``write_initial_source`` element indicates whether to write the initial + source distribution to file. + + *Default*: False From 7106958e001963724110610aa263866220369d52 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Wed, 14 Jan 2026 01:25:33 +0200 Subject: [PATCH 500/671] Simplify IFP message passing (#3719) Co-authored-by: Paul Romano --- include/openmc/ifp.h | 67 +++++++++++++++++++------- include/openmc/message_passing.h | 13 +++++ src/eigenvalue.cpp | 25 +++++++--- src/ifp.cpp | 81 -------------------------------- src/message_passing.cpp | 8 ++++ 5 files changed, 90 insertions(+), 104 deletions(-) diff --git a/include/openmc/ifp.h b/include/openmc/ifp.h index 01904d13c..2b751d66f 100644 --- a/include/openmc/ifp.h +++ b/include/openmc/ifp.h @@ -6,6 +6,8 @@ #include "openmc/particle_data.h" #include "openmc/settings.h" +#include // for copy + namespace openmc { //! Check the value of the IFP parameter for beta effective or both. @@ -113,14 +115,25 @@ void broadcast_ifp_n_generation(int& n_generation, //! \param[in] n_generation Number of generations //! \param[in] neighbor Index of the neighboring processor //! \param[in] requests MPI requests -//! \param[in] delayed_groups List of delayed group numbers lists -//! \param[out] send_delayed_groups Delayed group numbers buffer -//! \param[in] lifetimes List of lifetimes lists -//! \param[out] send_lifetimes Lifetimes buffer +//! \param[in] data List of data lists +//! \param[out] send_data data buffer +template void send_ifp_info(int64_t idx, int64_t n, int n_generation, int neighbor, - vector& requests, const vector>& delayed_groups, - vector& send_delayed_groups, const vector>& lifetimes, - vector& send_lifetimes); + vector& requests, const vector>& data, + vector& send_data) +{ + // Copy data in buffer + for (int i = idx; i < idx + n; i++) { + std::copy( + data[i].begin(), data[i].end(), send_data.begin() + i * n_generation); + } + + // Send data + requests.emplace_back(); + MPI_Datatype datatype = mpi::MPITypeMap::mpi_type; + MPI_Isend(&send_data[n_generation * idx], n_generation * static_cast(n), + datatype, neighbor, mpi::rank, mpi::intracomm, &requests.back()); +} //! Receive IFP data using MPI. //! @@ -129,12 +142,22 @@ void send_ifp_info(int64_t idx, int64_t n, int n_generation, int neighbor, //! \param[in] n_generation Number of generations //! \param[in] neighbor Index of the neighboring processor //! \param[in] requests MPI requests -//! \param[in] delayed_groups List of delayed group numbers -//! \param[in] lifetimes List of lifetimes +//! \param[in] data data buffer //! \param[out] deserialization Information to deserialize the received data +template void receive_ifp_data(int64_t idx, int64_t n, int n_generation, int neighbor, - vector& requests, vector& delayed_groups, - vector& lifetimes, vector& deserialization); + vector& requests, vector& data, + vector& deserialization) +{ + requests.emplace_back(); + MPI_Datatype datatype = mpi::MPITypeMap::mpi_type; + MPI_Irecv(&data[n_generation * idx], n_generation * static_cast(n), + datatype, neighbor, neighbor, mpi::intracomm, &requests.back()); + + // Deserialization info to reconstruct data later + DeserializationInfo info = {idx, n}; + deserialization.push_back(info); +} //! Copy partial IFP data from local lists to source banks. //! @@ -151,12 +174,24 @@ void copy_partial_ifp_data_to_source_banks(int64_t idx, int n, int64_t i_bank, //! the IFP source banks. //! //! \param[in] n_generation Number of generations +//! \param[in] data data to deserialize +//! \param[in] bank bank to store data //! \param[out] deserialization Information to deserialize the received data -//! \param[in] delayed_groups List of delayed group numbers -//! \param[in] lifetimes List of lifetimes -void deserialize_ifp_info(int n_generation, - const vector& deserialization, - const vector& delayed_groups, const vector& lifetimes); +template +void deserialize_ifp_info(int n_generation, const vector& data, + vector>& bank, const vector& deserialization) +{ + for (auto info : deserialization) { + int64_t index_local = info.index_local; + int64_t n = info.n; + + for (int i = index_local; i < index_local + n; i++) { + vector data_received( + data.begin() + n_generation * i, data.begin() + n_generation * (i + 1)); + bank[i] = data_received; + } + } +} #endif diff --git a/include/openmc/message_passing.h b/include/openmc/message_passing.h index ce993776b..03e236338 100644 --- a/include/openmc/message_passing.h +++ b/include/openmc/message_passing.h @@ -22,6 +22,19 @@ extern MPI_Datatype collision_track_site; extern MPI_Comm intracomm; #endif +//============================================================================== +// Template struct used to map types to MPI datatypes +// By having a single static data member, the template can +// be specialized for each type we know of. The specializations appear in the +// .cpp file since they are definitions. +//============================================================================== +#ifdef OPENMC_MPI +template +struct MPITypeMap { + static const MPI_Datatype mpi_type; +}; +#endif + // Calculates global indices of the bank particles // across all ranks using a parallel scan. This is used to write // the surface source file in parallel runs. It will probably diff --git a/src/eigenvalue.cpp b/src/eigenvalue.cpp index 8412cbd3b..2fb9dabf1 100644 --- a/src/eigenvalue.cpp +++ b/src/eigenvalue.cpp @@ -248,9 +248,12 @@ void synchronize_bank() if (settings::ifp_on) { // Send IFP data - send_ifp_info(index_local, n, ifp_n_generation, neighbor, requests, - temp_delayed_groups, send_delayed_groups, temp_lifetimes, - send_lifetimes); + if (is_beta_effective_or_both()) + send_ifp_info(index_local, n, ifp_n_generation, neighbor, requests, + temp_delayed_groups, send_delayed_groups); + if (is_generation_time_or_both()) + send_ifp_info(index_local, n, ifp_n_generation, neighbor, requests, + temp_lifetimes, send_lifetimes); } } @@ -316,8 +319,12 @@ void synchronize_bank() if (settings::ifp_on) { // Receive IFP data - receive_ifp_data(index_local, n, ifp_n_generation, neighbor, requests, - recv_delayed_groups, recv_lifetimes, deserialization_info); + if (is_beta_effective_or_both()) + receive_ifp_data(index_local, n, ifp_n_generation, neighbor, requests, + recv_delayed_groups, deserialization_info); + if (is_generation_time_or_both()) + receive_ifp_data(index_local, n, ifp_n_generation, neighbor, requests, + recv_lifetimes, deserialization_info); } } else { @@ -348,8 +355,12 @@ void synchronize_bank() MPI_Waitall(n_request, requests.data(), MPI_STATUSES_IGNORE); if (settings::ifp_on) { - deserialize_ifp_info(ifp_n_generation, deserialization_info, - recv_delayed_groups, recv_lifetimes); + if (is_beta_effective_or_both()) + deserialize_ifp_info(ifp_n_generation, recv_delayed_groups, + simulation::ifp_source_delayed_group_bank, deserialization_info); + if (is_generation_time_or_both()) + deserialize_ifp_info(ifp_n_generation, recv_lifetimes, + simulation::ifp_source_lifetime_bank, deserialization_info); } #else diff --git a/src/ifp.cpp b/src/ifp.cpp index cc4a76538..a5157440d 100644 --- a/src/ifp.cpp +++ b/src/ifp.cpp @@ -63,7 +63,6 @@ void copy_ifp_data_from_fission_banks( } #ifdef OPENMC_MPI - void broadcast_ifp_n_generation(int& n_generation, const vector>& delayed_groups, const vector>& lifetimes) @@ -78,61 +77,6 @@ void broadcast_ifp_n_generation(int& n_generation, MPI_Bcast(&n_generation, 1, MPI_INT, 0, mpi::intracomm); } -void send_ifp_info(int64_t idx, int64_t n, int n_generation, int neighbor, - vector& requests, const vector>& delayed_groups, - vector& send_delayed_groups, const vector>& lifetimes, - vector& send_lifetimes) -{ - // Copy data in send buffers - for (int i = idx; i < idx + n; i++) { - if (is_beta_effective_or_both()) { - std::copy(delayed_groups[i].begin(), delayed_groups[i].end(), - send_delayed_groups.begin() + i * n_generation); - } - if (is_generation_time_or_both()) { - std::copy(lifetimes[i].begin(), lifetimes[i].end(), - send_lifetimes.begin() + i * n_generation); - } - } - // Send delayed groups - if (is_beta_effective_or_both()) { - requests.emplace_back(); - MPI_Isend(&send_delayed_groups[n_generation * idx], - n_generation * static_cast(n), MPI_INT, neighbor, mpi::rank, - mpi::intracomm, &requests.back()); - } - // Send lifetimes - if (is_generation_time_or_both()) { - requests.emplace_back(); - MPI_Isend(&send_lifetimes[n_generation * idx], - n_generation * static_cast(n), MPI_DOUBLE, neighbor, mpi::rank, - mpi::intracomm, &requests.back()); - } -} - -void receive_ifp_data(int64_t idx, int64_t n, int n_generation, int neighbor, - vector& requests, vector& delayed_groups, - vector& lifetimes, vector& deserialization) -{ - // Receive delayed groups - if (is_beta_effective_or_both()) { - requests.emplace_back(); - MPI_Irecv(&delayed_groups[n_generation * idx], - n_generation * static_cast(n), MPI_INT, neighbor, neighbor, - mpi::intracomm, &requests.back()); - } - // Receive lifetimes - if (is_generation_time_or_both()) { - requests.emplace_back(); - MPI_Irecv(&lifetimes[n_generation * idx], - n_generation * static_cast(n), MPI_DOUBLE, neighbor, neighbor, - mpi::intracomm, &requests.back()); - } - // Deserialization info to reconstruct data later - DeserializationInfo info = {idx, n}; - deserialization.push_back(info); -} - void copy_partial_ifp_data_to_source_banks(int64_t idx, int n, int64_t i_bank, const vector>& delayed_groups, const vector>& lifetimes) @@ -146,31 +90,6 @@ void copy_partial_ifp_data_to_source_banks(int64_t idx, int n, int64_t i_bank, &simulation::ifp_source_lifetime_bank[i_bank]); } } - -void deserialize_ifp_info(int n_generation, - const vector& deserialization, - const vector& delayed_groups, const vector& lifetimes) -{ - for (auto info : deserialization) { - int64_t index_local = info.index_local; - int64_t n = info.n; - - for (int i = index_local; i < index_local + n; i++) { - if (is_beta_effective_or_both()) { - vector delayed_groups_received( - delayed_groups.begin() + n_generation * i, - delayed_groups.begin() + n_generation * (i + 1)); - simulation::ifp_source_delayed_group_bank[i] = delayed_groups_received; - } - if (is_generation_time_or_both()) { - vector lifetimes_received(lifetimes.begin() + n_generation * i, - lifetimes.begin() + n_generation * (i + 1)); - simulation::ifp_source_lifetime_bank[i] = lifetimes_received; - } - } - } -} - #endif void copy_complete_ifp_data_to_source_banks( diff --git a/src/message_passing.cpp b/src/message_passing.cpp index a160f6d73..cf6113ed6 100644 --- a/src/message_passing.cpp +++ b/src/message_passing.cpp @@ -39,6 +39,14 @@ vector calculate_parallel_index_vector(int64_t size) return result; } +#ifdef OPENMC_MPI +// Specializations of the MPITypeMap template struct +template<> +const MPI_Datatype MPITypeMap::mpi_type = MPI_INT; +template<> +const MPI_Datatype MPITypeMap::mpi_type = MPI_DOUBLE; +#endif + } // namespace mpi } // namespace openmc From 5bce5adabc42c9ebcfd3d5b9ef474b5eb9fc29aa Mon Sep 17 00:00:00 2001 From: Kevin Sawatzky <66632997+nuclearkevin@users.noreply.github.com> Date: Wed, 14 Jan 2026 12:02:17 -0600 Subject: [PATCH 501/671] Support cell densities in the random ray solver (#3720) --- include/openmc/material.h | 8 +- include/openmc/random_ray/source_region.h | 12 +- src/random_ray/flat_source_domain.cpp | 53 ++-- src/random_ray/linear_source_domain.cpp | 11 +- src/random_ray/random_ray.cpp | 6 +- src/random_ray/source_region.cpp | 12 +- .../random_ray_cell_density/__init__.py | 0 .../eigen/inputs_true.dat | 109 ++++++++ .../eigen/results_true.dat | 171 ++++++++++++ .../fs/inputs_true.dat | 244 ++++++++++++++++++ .../fs/results_true.dat | 9 + .../random_ray_cell_density/test.py | 43 +++ 12 files changed, 648 insertions(+), 30 deletions(-) create mode 100644 tests/regression_tests/random_ray_cell_density/__init__.py create mode 100644 tests/regression_tests/random_ray_cell_density/eigen/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_cell_density/eigen/results_true.dat create mode 100644 tests/regression_tests/random_ray_cell_density/fs/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_cell_density/fs/results_true.dat create mode 100644 tests/regression_tests/random_ray_cell_density/test.py diff --git a/include/openmc/material.h b/include/openmc/material.h index e36946c71..c10f25551 100644 --- a/include/openmc/material.h +++ b/include/openmc/material.h @@ -14,6 +14,7 @@ #include "openmc/memory.h" // for unique_ptr #include "openmc/ncrystal_interface.h" #include "openmc/particle.h" +#include "openmc/settings.h" #include "openmc/vector.h" namespace openmc { @@ -110,9 +111,12 @@ public: //! \return Density in [atom/b-cm] double density() const { return density_; } - //! Get density in [g/cm^3] + //! Get density in [g/cm^3]. //! \return Density in [g/cm^3] - double density_gpcc() const { return density_gpcc_; } + double density_gpcc() const + { + return settings::run_CE ? density_gpcc_ : density(); + } //! Get charge density in [e/b-cm] //! \return Charge density in [e/b-cm] diff --git a/include/openmc/random_ray/source_region.h b/include/openmc/random_ray/source_region.h index 0f5a747ff..c20d46abe 100644 --- a/include/openmc/random_ray/source_region.h +++ b/include/openmc/random_ray/source_region.h @@ -146,6 +146,7 @@ public: // Scalar fields int* material_; + double* density_mult_; int* is_small_; int* n_hits_; int* birthday_; @@ -195,6 +196,9 @@ public: int& material() { return *material_; } const int material() const { return *material_; } + double& density_mult() { return *density_mult_; } + const double density_mult() const { return *density_mult_; } + int& is_small() { return *is_small_; } const int is_small() const { return *is_small_; } @@ -316,7 +320,9 @@ public: //--------------------------------------- // Scalar fields - int material_ {0}; //!< Index in openmc::model::materials array + int material_ {0}; //!< Index in openmc::model::materials array + double density_mult_ {1.0}; //!< A density multiplier queried from the cell + //!< corresponding to the source region. OpenMPMutex lock_; double volume_ { 0.0}; //!< Volume (computed from the sum of ray crossing lengths) @@ -394,6 +400,9 @@ public: int& material(int64_t sr) { return material_[sr]; } const int material(int64_t sr) const { return material_[sr]; } + double& density_mult(int64_t sr) { return density_mult_[sr]; } + const double density_mult(int64_t sr) const { return density_mult_[sr]; } + int& is_small(int64_t sr) { return is_small_[sr]; } const int is_small(int64_t sr) const { return is_small_[sr]; } @@ -625,6 +634,7 @@ private: // SoA storage for scalar fields (one item per source region) vector material_; + vector density_mult_; vector is_small_; vector n_hits_; vector mesh_; diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index ec14795dd..2f5007fc9 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -109,18 +109,21 @@ void FlatSourceDomain::update_single_neutron_source(SourceRegionHandle& srh) // Add scattering + fission source int material = srh.material(); + double density_mult = srh.density_mult(); if (material != MATERIAL_VOID) { double inverse_k_eff = 1.0 / k_eff_; for (int g_out = 0; g_out < negroups_; g_out++) { - double sigma_t = sigma_t_[material * negroups_ + g_out]; + double sigma_t = sigma_t_[material * negroups_ + g_out] * density_mult; double scatter_source = 0.0; double fission_source = 0.0; for (int g_in = 0; g_in < negroups_; g_in++) { double scalar_flux = srh.scalar_flux_old(g_in); - double sigma_s = - sigma_s_[material * negroups_ * negroups_ + g_out * negroups_ + g_in]; - double nu_sigma_f = nu_sigma_f_[material * negroups_ + g_in]; + double sigma_s = sigma_s_[material * negroups_ * negroups_ + + g_out * negroups_ + g_in] * + density_mult; + double nu_sigma_f = + nu_sigma_f_[material * negroups_ + g_in] * density_mult; double chi = chi_[material * negroups_ + g_out]; scatter_source += sigma_s * scalar_flux; @@ -198,7 +201,8 @@ void FlatSourceDomain::set_flux_to_flux_plus_source( source_regions_.volume_sq(sr); } } else { - double sigma_t = sigma_t_[source_regions_.material(sr) * negroups_ + g]; + double sigma_t = sigma_t_[source_regions_.material(sr) * negroups_ + g] * + source_regions_.density_mult(sr); source_regions_.scalar_flux_new(sr, g) /= (sigma_t * volume); source_regions_.scalar_flux_new(sr, g) += source_regions_.source(sr, g); } @@ -332,7 +336,8 @@ void FlatSourceDomain::compute_k_eff() double sr_fission_source_new = 0; for (int g = 0; g < negroups_; g++) { - double nu_sigma_f = nu_sigma_f_[material * negroups_ + g]; + double nu_sigma_f = nu_sigma_f_[material * negroups_ + g] * + source_regions_.density_mult(sr); sr_fission_source_old += nu_sigma_f * source_regions_.scalar_flux_old(sr, g); sr_fission_source_new += @@ -562,7 +567,8 @@ double FlatSourceDomain::compute_fixed_source_normalization_factor() const // to get the total source strength in the expected units. double sigma_t = 1.0; if (material != MATERIAL_VOID) { - sigma_t = sigma_t_[material * negroups_ + g]; + sigma_t = + sigma_t_[material * negroups_ + g] * source_regions_.density_mult(sr); } simulation_external_source_strength += source_regions_.external_source(sr, g) * sigma_t * volume; @@ -618,7 +624,9 @@ void FlatSourceDomain::random_ray_tally() // source strength. double volume = source_regions_.volume(sr) * simulation_volume_; - double material = source_regions_.material(sr); + int material = source_regions_.material(sr); + double density_mult = source_regions_.density_mult(sr); + for (int g = 0; g < negroups_; g++) { double flux = source_regions_.scalar_flux_new(sr, g) * source_normalization_factor; @@ -634,19 +642,22 @@ void FlatSourceDomain::random_ray_tally() case SCORE_TOTAL: if (material != MATERIAL_VOID) { - score = flux * volume * sigma_t_[material * negroups_ + g]; + score = + flux * volume * sigma_t_[material * negroups_ + g] * density_mult; } break; case SCORE_FISSION: if (material != MATERIAL_VOID) { - score = flux * volume * sigma_f_[material * negroups_ + g]; + score = + flux * volume * sigma_f_[material * negroups_ + g] * density_mult; } break; case SCORE_NU_FISSION: if (material != MATERIAL_VOID) { - score = flux * volume * nu_sigma_f_[material * negroups_ + g]; + score = flux * volume * nu_sigma_f_[material * negroups_ + g] * + density_mult; } break; @@ -913,7 +924,8 @@ void FlatSourceDomain::output_to_vtk() const for (int g = 0; g < negroups_; g++) { int64_t source_element = fsr * negroups_ + g; float flux = evaluate_flux_at_point(voxel_positions[i], fsr, g); - double sigma_f = sigma_f_[mat * negroups_ + g]; + double sigma_f = sigma_f_[mat * negroups_ + g] * + source_regions_.density_mult(fsr); total_fission += sigma_f * flux; } } @@ -934,7 +946,8 @@ void FlatSourceDomain::output_to_vtk() const // multiply it back to get the true external source. double sigma_t = 1.0; if (mat != MATERIAL_VOID) { - sigma_t = sigma_t_[mat * negroups_ + g]; + sigma_t = sigma_t_[mat * negroups_ + g] * + source_regions_.density_mult(fsr); } total_external += source_regions_.external_source(fsr, g) * sigma_t; } @@ -1244,7 +1257,8 @@ void FlatSourceDomain::set_adjoint_sources() continue; } for (int g = 0; g < negroups_; g++) { - double sigma_t = sigma_t_[material * negroups_ + g]; + double sigma_t = + sigma_t_[material * negroups_ + g] * source_regions_.density_mult(sr); source_regions_.external_source(sr, g) /= sigma_t; } } @@ -1495,6 +1509,8 @@ SourceRegionHandle FlatSourceDomain::get_subdivided_source_region_handle( handle.material() = material; + handle.density_mult() = cell.density_mult(gs.cell_instance()); + // Store the mesh index (if any) assigned to this source region handle.mesh() = mesh_idx; @@ -1523,7 +1539,8 @@ SourceRegionHandle FlatSourceDomain::get_subdivided_source_region_handle( // Divide external source term by sigma_t if (material != C_NONE) { for (int g = 0; g < negroups_; g++) { - double sigma_t = sigma_t_[material * negroups_ + g]; + double sigma_t = + sigma_t_[material * negroups_ + g] * handle.density_mult(); handle.external_source(g) /= sigma_t; } } @@ -1598,6 +1615,7 @@ void FlatSourceDomain::apply_transport_stabilization() #pragma omp parallel for for (int64_t sr = 0; sr < n_source_regions(); sr++) { int material = source_regions_.material(sr); + double density_mult = source_regions_.density_mult(sr); if (material == MATERIAL_VOID) { continue; } @@ -1605,9 +1623,10 @@ void FlatSourceDomain::apply_transport_stabilization() // Only apply stabilization if the diagonal (in-group) scattering XS is // negative double sigma_s = - sigma_s_[material * negroups_ * negroups_ + g * negroups_ + g]; + sigma_s_[material * negroups_ * negroups_ + g * negroups_ + g] * + density_mult; if (sigma_s < 0.0) { - double sigma_t = sigma_t_[material * negroups_ + g]; + double sigma_t = sigma_t_[material * negroups_ + g] * density_mult; double phi_new = source_regions_.scalar_flux_new(sr, g); double phi_old = source_regions_.scalar_flux_old(sr, g); diff --git a/src/random_ray/linear_source_domain.cpp b/src/random_ray/linear_source_domain.cpp index 47ffbb727..02f4c9e23 100644 --- a/src/random_ray/linear_source_domain.cpp +++ b/src/random_ray/linear_source_domain.cpp @@ -43,12 +43,13 @@ void LinearSourceDomain::update_single_neutron_source(SourceRegionHandle& srh) // Add scattering + fission source int material = srh.material(); + double density_mult = srh.density_mult(); if (material != MATERIAL_VOID) { double inverse_k_eff = 1.0 / k_eff_; MomentMatrix invM = srh.mom_matrix().inverse(); for (int g_out = 0; g_out < negroups_; g_out++) { - double sigma_t = sigma_t_[material * negroups_ + g_out]; + double sigma_t = sigma_t_[material * negroups_ + g_out] * density_mult; double scatter_flat = 0.0f; double fission_flat = 0.0f; @@ -61,9 +62,11 @@ void LinearSourceDomain::update_single_neutron_source(SourceRegionHandle& srh) MomentArray flux_linear = srh.flux_moments_old(g_in); // Handles for cross sections - double sigma_s = - sigma_s_[material * negroups_ * negroups_ + g_out * negroups_ + g_in]; - double nu_sigma_f = nu_sigma_f_[material * negroups_ + g_in]; + double sigma_s = sigma_s_[material * negroups_ * negroups_ + + g_out * negroups_ + g_in] * + density_mult; + double nu_sigma_f = + nu_sigma_f_[material * negroups_ + g_in] * density_mult; double chi = chi_[material * negroups_ + g_out]; // Compute source terms for flat and linear components of the flux diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp index c19d136a4..1b61d8c20 100644 --- a/src/random_ray/random_ray.cpp +++ b/src/random_ray/random_ray.cpp @@ -435,7 +435,8 @@ void RandomRay::attenuate_flux_flat_source( // MOC incoming flux attenuation + source contribution/attenuation equation for (int g = 0; g < negroups_; g++) { - float sigma_t = domain_->sigma_t_[material * negroups_ + g]; + float sigma_t = + domain_->sigma_t_[material * negroups_ + g] * srh.density_mult(); float tau = sigma_t * distance; float exponential = cjosey_exponential(tau); // exponential = 1 - exp(-tau) float new_delta_psi = (angular_flux_[g] - srh.source(g)) * exponential; @@ -558,7 +559,8 @@ void RandomRay::attenuate_flux_linear_source( for (int g = 0; g < negroups_; g++) { // Compute tau, the optical thickness of the ray segment - float sigma_t = domain_->sigma_t_[material * negroups_ + g]; + float sigma_t = + domain_->sigma_t_[material * negroups_ + g] * srh.density_mult(); float tau = sigma_t * distance; // If tau is very small, set it to zero to avoid numerical issues. diff --git a/src/random_ray/source_region.cpp b/src/random_ray/source_region.cpp index 3b06f0ed0..15c65221a 100644 --- a/src/random_ray/source_region.cpp +++ b/src/random_ray/source_region.cpp @@ -11,10 +11,11 @@ namespace openmc { //============================================================================== SourceRegionHandle::SourceRegionHandle(SourceRegion& sr) : negroups_(sr.scalar_flux_old_.size()), material_(&sr.material_), - is_small_(&sr.is_small_), n_hits_(&sr.n_hits_), - is_linear_(sr.source_gradients_.size() > 0), lock_(&sr.lock_), - volume_(&sr.volume_), volume_t_(&sr.volume_t_), volume_sq_(&sr.volume_sq_), - volume_sq_t_(&sr.volume_sq_t_), volume_naive_(&sr.volume_naive_), + density_mult_(&sr.density_mult_), is_small_(&sr.is_small_), + n_hits_(&sr.n_hits_), is_linear_(sr.source_gradients_.size() > 0), + lock_(&sr.lock_), volume_(&sr.volume_), volume_t_(&sr.volume_t_), + volume_sq_(&sr.volume_sq_), volume_sq_t_(&sr.volume_sq_t_), + volume_naive_(&sr.volume_naive_), position_recorded_(&sr.position_recorded_), external_source_present_(&sr.external_source_present_), position_(&sr.position_), centroid_(&sr.centroid_), @@ -70,6 +71,7 @@ void SourceRegionContainer::push_back(const SourceRegion& sr) // Scalar fields material_.push_back(sr.material_); + density_mult_.push_back(sr.density_mult_); is_small_.push_back(sr.is_small_); n_hits_.push_back(sr.n_hits_); lock_.push_back(sr.lock_); @@ -123,6 +125,7 @@ void SourceRegionContainer::assign( // Clear existing data n_source_regions_ = 0; material_.clear(); + density_mult_.clear(); is_small_.clear(); n_hits_.clear(); lock_.clear(); @@ -180,6 +183,7 @@ SourceRegionHandle SourceRegionContainer::get_source_region_handle(int64_t sr) SourceRegionHandle handle; handle.negroups_ = negroups(); handle.material_ = &material(sr); + handle.density_mult_ = &density_mult(sr); handle.is_small_ = &is_small(sr); handle.n_hits_ = &n_hits(sr); handle.is_linear_ = is_linear(); diff --git a/tests/regression_tests/random_ray_cell_density/__init__.py b/tests/regression_tests/random_ray_cell_density/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_cell_density/eigen/inputs_true.dat b/tests/regression_tests/random_ray_cell_density/eigen/inputs_true.dat new file mode 100644 index 000000000..0dd354cf0 --- /dev/null +++ b/tests/regression_tests/random_ray_cell_density/eigen/inputs_true.dat @@ -0,0 +1,109 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.126 0.126 + 10 10 + -0.63 -0.63 + +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 + + + 1.26 1.26 + 2 2 + -1.26 -1.26 + +2 2 +2 5 + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + multi-group + + 100.0 + 20.0 + + + -1.26 -1.26 -1 1.26 1.26 1 + + + true + + + + + 2 2 + -1.26 -1.26 + 1.26 1.26 + + + 1 + + + 1e-05 0.0635 10.0 100.0 1000.0 500000.0 1000000.0 20000000.0 + + + 1 2 + flux fission nu-fission + analog + + + diff --git a/tests/regression_tests/random_ray_cell_density/eigen/results_true.dat b/tests/regression_tests/random_ray_cell_density/eigen/results_true.dat new file mode 100644 index 000000000..3e6ce4039 --- /dev/null +++ b/tests/regression_tests/random_ray_cell_density/eigen/results_true.dat @@ -0,0 +1,171 @@ +k-combined: +7.606488E-01 9.025978E-03 +tally 1: +6.209503E-01 +7.732732E-02 +4.335729E-01 +3.769026E-02 +1.055230E+00 +2.232538E-01 +4.077492E-01 +3.335434E-02 +1.188353E-01 +2.833644E-03 +2.892214E-01 +1.678476E-02 +2.715245E-01 +1.488194E-02 +1.736084E-02 +6.079207E-05 +4.225281E-02 +3.600946E-04 +3.700491E-01 +2.796457E-02 +2.369715E-02 +1.145314E-04 +5.767413E-02 +6.784132E-04 +1.223083E+00 +3.058118E-01 +2.790791E-02 +1.593458E-04 +6.792309E-02 +9.438893E-04 +4.095779E+00 +3.377358E+00 +1.251201E-02 +3.153331E-05 +3.096010E-02 +1.930723E-04 +2.608638E+00 +1.361111E+00 +7.059742E-02 +9.968342E-04 +1.963632E-01 +7.711968E-03 +1.164050E+00 +2.710289E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.325486E-01 +5.675375E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.058117E-01 +1.905099E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.645650E-01 +4.428410E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.350036E+00 +3.718611E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.538916E+00 +2.521670E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.201812E+00 +9.698678E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +6.776093E-01 +9.211106E-02 +2.527022E-01 +1.280619E-02 +6.150266E-01 +7.585593E-02 +4.194143E-01 +3.528585E-02 +6.303447E-02 +7.972233E-04 +1.534133E-01 +4.722259E-03 +2.743408E-01 +1.520611E-02 +8.961769E-03 +1.621446E-05 +2.181115E-02 +9.604444E-05 +3.871710E-01 +3.063162E-02 +1.288801E-02 +3.392550E-05 +3.136683E-02 +2.009537E-04 +1.259769E+00 +3.241545E-01 +1.478360E-02 +4.466102E-05 +3.598077E-02 +2.645508E-04 +4.032169E+00 +3.273866E+00 +6.215187E-03 +7.776862E-06 +1.537904E-02 +4.761620E-05 +2.583647E+00 +1.335275E+00 +3.533526E-02 +2.498005E-04 +9.828323E-02 +1.932572E-03 +7.851145E-01 +1.234783E-01 +2.966638E-01 +1.763751E-02 +7.220204E-01 +1.044737E-01 +4.505673E-01 +4.067697E-02 +6.823342E-02 +9.332472E-04 +1.660665E-01 +5.527980E-03 +2.832534E-01 +1.626181E-02 +9.297014E-03 +1.749663E-05 +2.262707E-02 +1.036392E-04 +4.056699E-01 +3.370803E-02 +1.358439E-02 +3.774180E-05 +3.306168E-02 +2.235591E-04 +1.279744E+00 +3.345476E-01 +1.512005E-02 +4.668112E-05 +3.679962E-02 +2.765169E-04 +3.917888E+00 +3.090981E+00 +6.082371E-03 +7.449625E-06 +1.505040E-02 +4.561259E-05 +2.520906E+00 +1.271106E+00 +3.477796E-02 +2.419730E-04 +9.673314E-02 +1.872015E-03 diff --git a/tests/regression_tests/random_ray_cell_density/fs/inputs_true.dat b/tests/regression_tests/random_ray_cell_density/fs/inputs_true.dat new file mode 100644 index 000000000..e90f25973 --- /dev/null +++ b/tests/regression_tests/random_ray_cell_density/fs/inputs_true.dat @@ -0,0 +1,244 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_cell_density/fs/results_true.dat b/tests/regression_tests/random_ray_cell_density/fs/results_true.dat new file mode 100644 index 000000000..e12f476e0 --- /dev/null +++ b/tests/regression_tests/random_ray_cell_density/fs/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +6.203077E-01 +7.706659E-02 +tally 2: +3.203448E-02 +2.058679E-04 +tally 3: +2.091970E-03 +8.765168E-07 diff --git a/tests/regression_tests/random_ray_cell_density/test.py b/tests/regression_tests/random_ray_cell_density/test.py new file mode 100644 index 000000000..cb6062cdc --- /dev/null +++ b/tests/regression_tests/random_ray_cell_density/test.py @@ -0,0 +1,43 @@ +import os + +import openmc +from openmc.examples import random_ray_lattice, random_ray_three_region_cube +from openmc.utility_funcs import change_directory +import pytest + +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) + + +@pytest.mark.parametrize("run_mode", ["eigen", "fs"]) +def test_random_ray_basic(run_mode): + with change_directory(run_mode): + if run_mode == "eigen": + openmc.reset_auto_ids() + model = random_ray_lattice() + # Double the densities of the lower-left fuel pin -> cell instances [0, 9). + for id, cell in model.geometry.get_all_cells().items(): + if cell.fill.name == "UO2 fuel": + cell.density = [((i < 8) + 1.0) for i in range(24)] + + # Gold file was generated with manually scaled fuel cross sections. + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() + else: + openmc.reset_auto_ids() + model = random_ray_three_region_cube() + # Increase the density in the source region. + for id, cell in model.geometry.get_all_cells().items(): + if cell.fill.name == "source": + cell.density = 1e3 + + # Gold file was generated with manually scaled source cross sections. + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() From 7861adf53b4728c6be7ce5583035f7e3c915af82 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 14 Jan 2026 14:29:24 -0500 Subject: [PATCH 502/671] Add git branching information to AGENTS.md (#3726) --- AGENTS.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 32e30213a..44962d1ad 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,6 +31,24 @@ C++17/Python codebase where: - **Depletion**: `openmc.deplete` implements burnup via operator-splitting with various integrators (Predictor, CECM, etc.) - **Nuclear Data**: `openmc.data` provides programmatic access to nuclear data files (ENDF, ACE, HDF5) +## Git Branching Workflow + +OpenMC uses a git flow branching model with two primary branches: + +- **`develop` branch**: The main development branch where all ongoing development takes place. This is the **primary branch against which pull requests are submitted and merged**. This branch is not guaranteed to be stable and may contain work-in-progress features. +- **`master` branch**: The stable release branch containing the latest stable release of OpenMC. This branch only receives merges from `develop` when the development team decides a release should occur. + +### 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`. + +### Workflow for contributors + +1. Create a feature/bugfix branch off `develop` +2. Make changes and commit to the feature branch +3. Open a pull request to merge the feature branch into `develop` +4. A committer reviews and merges the PR into `develop` + ## Critical Build & Test Workflows ### Build Dependencies From 179048b80139c0e846e2d020c8379dcea850fb02 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Thu, 15 Jan 2026 07:24:07 +0200 Subject: [PATCH 503/671] Skip tests on documentation-only changes (#3727) Co-authored-by: Paul Romano --- .github/workflows/ci.yml | 45 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6fc130947..580d409c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: CI +name: Tests and Coverage on: # allows us to run workflows manually @@ -21,7 +21,25 @@ env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} jobs: + filter-changes: + runs-on: ubuntu-latest + outputs: + source_changed: ${{ steps.filter.outputs.source_changed }} + steps: + - name: Check out the repository + uses: actions/checkout@v4 + - name: Examine changed files + id: filter + uses: dorny/paths-filter@668c092af3649c4b664c54e4b704aa46782f6f7c # latest master commit, not released yet + with: + filters: | + source_changed: + - '!docs/**' + - '!**/*.md' + predicate-quantifier: 'every' main: + needs: filter-changes + if: ${{ needs.filter-changes.outputs.source_changed == 'true' }} runs-on: ubuntu-22.04 strategy: matrix: @@ -205,12 +223,33 @@ jobs: flag-name: C++ and Python path-to-lcov: coverage.lcov - finish: - needs: main + coverage: + needs: [filter-changes, main] + if: ${{ always() }} runs-on: ubuntu-latest steps: - name: Coveralls Finished + if: ${{ needs.filter-changes.outputs.source_changed == 'true' }} uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.GITHUB_TOKEN }} parallel-finished: true + + ci-pass: + needs: [filter-changes, main, coverage] + name: Check CI status + if: ${{ always() }} + runs-on: ubuntu-latest + steps: + - name: Check CI status + run: | + if [[ "${{ needs.filter-changes.outputs.source_changed }}" == "false" ]]; then + echo "Documentation-only change - CI skipped successfully" + exit 0 + fi + if [[ "${{ needs.main.result }}" == "success" && "${{ needs.coverage.result }}" == "success" ]]; then + echo "CI passed" + exit 0 + fi + echo "CI failed" + exit 1 From 51ea89ccc8cff49b6e4166ee620728b880150796 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 15 Jan 2026 22:23:35 -0600 Subject: [PATCH 504/671] Implement depth-awareness when enforcing precedence between union/intersection operators (#3730) Co-authored-by: GuySten --- include/openmc/cell.h | 6 +- src/cell.cpp | 102 +++++++++++++++------------ tests/cpp_unit_tests/CMakeLists.txt | 1 + tests/cpp_unit_tests/test_region.cpp | 101 ++++++++++++++++++++++++++ 4 files changed, 161 insertions(+), 49 deletions(-) create mode 100644 tests/cpp_unit_tests/test_region.cpp diff --git a/include/openmc/cell.h b/include/openmc/cell.h index 9dfe2339a..26c34ebd4 100644 --- a/include/openmc/cell.h +++ b/include/openmc/cell.h @@ -123,11 +123,11 @@ private: //! BoundingBox if the particle is in a complex cell. BoundingBox bounding_box_complex(vector postfix) const; - //! Enfource precedence: Parenthases, Complement, Intersection, Union - void add_precedence(); + //! Enforce precedence between intersections and unions + void enforce_precedence(); //! Add parenthesis to enforce precedence - int64_t add_parentheses(int64_t start); + void add_parentheses(int64_t start); //! Remove complement operators from the expression void remove_complement_ops(); diff --git a/src/cell.cpp b/src/cell.cpp index aceed31ca..ebe28c3d2 100644 --- a/src/cell.cpp +++ b/src/cell.cpp @@ -660,7 +660,7 @@ Region::Region(std::string region_spec, int32_t cell_id) if (token == OP_UNION) { simple_ = false; // Ensure intersections have precedence over unions - add_precedence(); + enforce_precedence(); break; } } @@ -703,7 +703,7 @@ void Region::apply_demorgan( //! precedence than unions using parentheses. //============================================================================== -int64_t Region::add_parentheses(int64_t start) +void Region::add_parentheses(int64_t start) { int32_t start_token = expression_[start]; // Add left parenthesis and set new position to be after parenthesis @@ -712,14 +712,6 @@ int64_t Region::add_parentheses(int64_t start) } expression_.insert(expression_.begin() + start - 1, OP_LEFT_PAREN); - // Keep track of return iterator distance. If we don't encounter a left - // parenthesis, we return an iterator corresponding to wherever the right - // parenthesis is inserted. If a left parenthesis is encountered, an iterator - // corresponding to the left parenthesis is returned. Also note that we keep - // track of a *distance* instead of an iterator because the underlying memory - // allocation may change. - std::size_t return_it_dist = 0; - // Add right parenthesis // While the start iterator is within the bounds of infix while (start + 1 < expression_.size()) { @@ -733,7 +725,6 @@ int64_t Region::add_parentheses(int64_t start) // in the region, when the operator is an intersection then include the // operator and next surface if (expression_[start] == OP_LEFT_PAREN) { - return_it_dist = start; int depth = 1; do { start++; @@ -750,54 +741,73 @@ int64_t Region::add_parentheses(int64_t start) --start; } expression_.insert(expression_.begin() + start, OP_RIGHT_PAREN); - if (return_it_dist > 0) { - return return_it_dist; - } else { - return start - 1; - } + return; } } } - // If we get here a right parenthesis hasn't been placed, - // return iterator + // If we get here a right parenthesis hasn't been placed expression_.push_back(OP_RIGHT_PAREN); - if (return_it_dist > 0) { - return return_it_dist; - } else { - return start - 1; - } } +//============================================================================== +//! Add parentheses to enforce operator precedence in region expressions +//! +//! This function ensures that intersection operators have higher precedence +//! than union operators by adding parentheses where needed. For example: +//! "1 2 | 3" becomes "(1 2) | 3" +//! "1 | 2 3" becomes "1 | (2 3)" +//! +//! The algorithm uses stacks to track the current operator type and its +//! position at each parenthesis depth level. When it encounters a different +//! operator at the same depth, it adds parentheses to group the +//! higher-precedence operations. //============================================================================== -void Region::add_precedence() +void Region::enforce_precedence() { - int32_t current_op = 0; - std::size_t current_dist = 0; + // Stack tracking the operator type at each depth (0 = no operator seen yet) + vector op_stack = {0}; - for (int64_t i = 0; i < expression_.size(); i++) { + // Stack tracking where the operator sequence started at each depth + vector pos_stack = {0}; + + for (int64_t i = 0; i < expression_.size(); ++i) { int32_t token = expression_[i]; - if (token == OP_UNION || token == OP_INTERSECTION) { - if (current_op == 0) { - // Set the current operator if is hasn't been set - current_op = token; - current_dist = i; - } else if (token != current_op) { - // If the current operator doesn't match the token, add parenthesis to - // assert precedence - if (current_op == OP_INTERSECTION) { - i = add_parentheses(current_dist); - } else { - i = add_parentheses(i); - } - current_op = 0; - current_dist = 0; + if (token == OP_LEFT_PAREN) { + // Entering a new parenthesis level - push new tracking state + op_stack.push_back(0); + pos_stack.push_back(0); + continue; + } else if (token == OP_RIGHT_PAREN) { + // Exiting a parenthesis level - pop tracking state (keep at least one) + if (op_stack.size() > 1) { + op_stack.pop_back(); + pos_stack.pop_back(); + } + continue; + } + + if (token == OP_UNION || token == OP_INTERSECTION) { + if (op_stack.back() == 0) { + // First operator at this depth - record it and its position + op_stack.back() = token; + pos_stack.back() = i; + } else if (token != op_stack.back()) { + // Encountered a different operator at the same depth - need to add + // parentheses to enforce precedence. Intersection has higher + // precedence, so we parenthesize the intersection terms. + if (op_stack.back() == OP_INTERSECTION) { + add_parentheses(pos_stack.back()); + } else { + add_parentheses(i); + } + + // Restart the scan since we modified the expression + i = -1; // Will be incremented to 0 by the for loop + op_stack = {0}; + pos_stack = {0}; } - } else if (token > OP_COMPLEMENT) { - // If the token is a parenthesis reset the current operator - current_op = 0; - current_dist = 0; } } } diff --git a/tests/cpp_unit_tests/CMakeLists.txt b/tests/cpp_unit_tests/CMakeLists.txt index 5f87db9ea..20341b420 100644 --- a/tests/cpp_unit_tests/CMakeLists.txt +++ b/tests/cpp_unit_tests/CMakeLists.txt @@ -6,6 +6,7 @@ set(TEST_NAMES test_math test_mcpl_stat_sum test_mesh + test_region # Add additional unit test files here ) diff --git a/tests/cpp_unit_tests/test_region.cpp b/tests/cpp_unit_tests/test_region.cpp new file mode 100644 index 000000000..b3d9a1427 --- /dev/null +++ b/tests/cpp_unit_tests/test_region.cpp @@ -0,0 +1,101 @@ +#include + +#include "openmc/cell.h" +#include "openmc/surface.h" + +#include + +namespace { + +// Helper class to set up and tear down test surfaces +class SurfaceFixture { +public: + SurfaceFixture() + { + pugi::xml_document doc; + pugi::xml_node surf_node = doc.append_child("surface"); + surf_node.set_name("surface"); + surf_node.append_attribute("id") = "0"; + surf_node.append_attribute("type") = "x-plane"; + surf_node.append_attribute("coeffs") = "1"; + + for (int i = 1; i < 10; ++i) { + surf_node.attribute("id") = i; + openmc::model::surfaces.push_back( + std::make_unique(surf_node)); + openmc::model::surface_map[i] = i - 1; + } + } + + ~SurfaceFixture() + { + openmc::model::surfaces.clear(); + openmc::model::surface_map.clear(); + } +}; + +} // anonymous namespace + +TEST_CASE("Test region simplification") +{ + SurfaceFixture fixture; + + SECTION("Original bug case from issue #3685") + { + // Input: "-1 2 (-3 4) | (-5 6)" was being incorrectly interpreted + auto region = openmc::Region("(-1 2 (-3 4) | (-5 6))", 0); + REQUIRE(region.str() == " ( ( -1 2 ( -3 4 ) ) | ( -5 6 ) )"); + } + + SECTION("Simple union - no extra parentheses needed") + { + auto region = openmc::Region("1 | 2", 0); + REQUIRE(region.str() == " 1 | 2"); + } + + SECTION("Intersection then union") + { + // Intersection should have higher precedence, so (1 2) grouped + auto region = openmc::Region("1 2 | 3", 0); + REQUIRE(region.str() == " ( 1 2 ) | 3"); + } + + SECTION("Union then intersection") + { + // The (2 3) intersection should be grouped + auto region = openmc::Region("1 | 2 3", 0); + REQUIRE(region.str() == " 1 | ( 2 3 )"); + } + + SECTION("Nested parentheses preserved") + { + // These parentheses are meaningful and should be preserved + auto region = openmc::Region("(1 | 2) (3 | 4)", 0); + REQUIRE(region.str() == " ( 1 | 2 ) ( 3 | 4 )"); + } + + SECTION("Deep nesting") + { + auto region = openmc::Region("((1 2) | (3 4)) 5", 0); + REQUIRE(region.str() == " ( ( 1 2 ) | ( 3 4 ) ) 5"); + } + + SECTION("Multiple unions") + { + auto region = openmc::Region("1 | 2 | 3", 0); + REQUIRE(region.str() == " 1 | 2 | 3"); + } + + SECTION("Multiple intersections") + { + auto region = openmc::Region("1 2 3", 0); + // Simple cell - no operators in output + REQUIRE(region.str() == " 1 2 3"); + } + + SECTION("Complex mixed expression") + { + auto region = openmc::Region("1 2 | 3 4 | 5 6", 0); + REQUIRE(region.str() == " ( 1 2 ) | ( 3 4 ) | ( 5 6 )"); + } +} From 5847b0de23eaf152cb5078801ee13cad2d2cc015 Mon Sep 17 00:00:00 2001 From: Kevin Sawatzky <66632997+nuclearkevin@users.noreply.github.com> Date: Fri, 16 Jan 2026 22:01:37 -0600 Subject: [PATCH 505/671] Block restriction of libMesh unstructured mesh tallies (#3694) --- include/openmc/mesh.h | 23 ++++++++++----- src/mesh.cpp | 67 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 70 insertions(+), 20 deletions(-) diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index 7ceb623da..2936d3ab1 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -4,6 +4,7 @@ #ifndef OPENMC_MESH_H #define OPENMC_MESH_H +#include #include #include "hdf5.h" @@ -969,7 +970,7 @@ public: Position sample_element(int32_t bin, uint64_t* seed) const override; - int get_bin(Position r) const override; + virtual int get_bin(Position r) const override; int n_bins() const override; @@ -1007,16 +1008,21 @@ public: protected: // Methods - //! Translate a bin value to an element reference virtual const libMesh::Elem& get_element_from_bin(int bin) const; //! Translate an element pointer to a bin index virtual int get_bin_from_element(const libMesh::Elem* elem) const; + // Data members libMesh::MeshBase* m_; //!< pointer to libMesh MeshBase instance, always set //!< during intialization + vector> + pl_; //!< per-thread point locators + libMesh::BoundingBox bbox_; //!< bounding box of the mesh + private: + // Methods void initialize() override; void set_mesh_pointer_from_filename(const std::string& filename); void build_eqn_sys(); @@ -1025,8 +1031,6 @@ private: unique_ptr unique_m_ = nullptr; //!< pointer to the libMesh MeshBase instance, only used if mesh is //!< created inside OpenMC - vector> - pl_; //!< per-thread point locators unique_ptr equation_systems_; //!< pointer to the libMesh EquationSystems //!< instance @@ -1035,7 +1039,6 @@ private: std::unordered_map variable_map_; //!< mapping of variable names (tally scores) to libMesh //!< variable numbers - libMesh::BoundingBox bbox_; //!< bounding box of the mesh libMesh::dof_id_type first_element_id_; //!< id of the first element in the mesh }; @@ -1043,8 +1046,9 @@ private: class AdaptiveLibMesh : public LibMesh { public: // Constructor - AdaptiveLibMesh( - libMesh::MeshBase& input_mesh, double length_multiplier = 1.0); + AdaptiveLibMesh(libMesh::MeshBase& input_mesh, double length_multiplier = 1.0, + const std::set& block_ids = + std::set()); // Overridden methods int n_bins() const override; @@ -1056,6 +1060,8 @@ public: void write(const std::string& filename) const override; + int get_bin(Position r) const override; + protected: // Overridden methods int get_bin_from_element(const libMesh::Elem* elem) const override; @@ -1064,6 +1070,9 @@ protected: private: // Data members + const std::set + block_ids_; //!< subdomains of the mesh to tally on + const bool block_restrict_; //!< whether a subset of the mesh is being used const libMesh::dof_id_type num_active_; //!< cached number of active elements std::vector diff --git a/src/mesh.cpp b/src/mesh.cpp index 9a7a8e751..6ee15b9c1 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -3485,9 +3485,6 @@ void LibMesh::initialize() // assuming that unstructured meshes used in OpenMC are 3D n_dimension_ = 3; - if (length_multiplier_ > 0.0) { - libMesh::MeshTools::Modification::scale(*m_, length_multiplier_); - } // if OpenMC is managing the libMesh::MeshBase instance, prepare the mesh. // Otherwise assume that it is prepared by its owning application if (unique_m_) { @@ -3537,7 +3534,11 @@ Position LibMesh::centroid(int bin) const { const auto& elem = this->get_element_from_bin(bin); auto centroid = elem.vertex_average(); - return {centroid(0), centroid(1), centroid(2)}; + if (length_multiplier_ > 0.0) { + return length_multiplier_ * Position(centroid(0), centroid(1), centroid(2)); + } else { + return {centroid(0), centroid(1), centroid(2)}; + } } int LibMesh::n_vertices() const @@ -3548,7 +3549,11 @@ int LibMesh::n_vertices() const Position LibMesh::vertex(int vertex_id) const { const auto node_ref = m_->node_ref(vertex_id); - return {node_ref(0), node_ref(1), node_ref(2)}; + if (length_multiplier_ > 0.0) { + return length_multiplier_ * Position(node_ref(0), node_ref(1), node_ref(2)); + } else { + return {node_ref(0), node_ref(1), node_ref(2)}; + } } std::vector LibMesh::connectivity(int elem_id) const @@ -3689,6 +3694,11 @@ int LibMesh::get_bin(Position r) const // look-up a tet using the point locator libMesh::Point p(r.x, r.y, r.z); + if (length_multiplier_ > 0.0) { + // Scale the point down + p /= length_multiplier_; + } + // quick rejection check if (!bbox_.contains_point(p)) { return -1; @@ -3722,22 +3732,32 @@ const libMesh::Elem& LibMesh::get_element_from_bin(int bin) const double LibMesh::volume(int bin) const { - return this->get_element_from_bin(bin).volume(); + return this->get_element_from_bin(bin).volume() * length_multiplier_ * + length_multiplier_ * length_multiplier_; } -AdaptiveLibMesh::AdaptiveLibMesh( - libMesh::MeshBase& input_mesh, double length_multiplier) - : LibMesh(input_mesh, length_multiplier), num_active_(m_->n_active_elem()) +AdaptiveLibMesh::AdaptiveLibMesh(libMesh::MeshBase& input_mesh, + double length_multiplier, + const std::set& block_ids) + : LibMesh(input_mesh, length_multiplier), block_ids_(block_ids), + block_restrict_(!block_ids_.empty()), + num_active_( + block_restrict_ + ? std::distance(m_->active_subdomain_set_elements_begin(block_ids_), + m_->active_subdomain_set_elements_end(block_ids_)) + : m_->n_active_elem()) { // if the mesh is adaptive elements aren't guaranteed by libMesh to be // contiguous in ID space, so we need to map from bin indices (defined over // active elements) to global dof ids bin_to_elem_map_.reserve(num_active_); elem_to_bin_map_.resize(m_->n_elem(), -1); - for (auto it = m_->active_elements_begin(); it != m_->active_elements_end(); - it++) { - auto elem = *it; - + auto begin = block_restrict_ + ? m_->active_subdomain_set_elements_begin(block_ids_) + : m_->active_elements_begin(); + auto end = block_restrict_ ? m_->active_subdomain_set_elements_end(block_ids_) + : m_->active_elements_end(); + for (const auto& elem : libMesh::as_range(begin, end)) { bin_to_elem_map_.push_back(elem->id()); elem_to_bin_map_[elem->id()] = bin_to_elem_map_.size() - 1; } @@ -3770,6 +3790,27 @@ void AdaptiveLibMesh::write(const std::string& filename) const this->id_)); } +int AdaptiveLibMesh::get_bin(Position r) const +{ + // look-up a tet using the point locator + libMesh::Point p(r.x, r.y, r.z); + + if (length_multiplier_ > 0.0) { + // Scale the point down + p /= length_multiplier_; + } + + // quick rejection check + if (!bbox_.contains_point(p)) { + return -1; + } + + const auto& point_locator = pl_.at(thread_num()); + + const auto elem_ptr = (*point_locator)(p, &block_ids_); + return elem_ptr ? get_bin_from_element(elem_ptr) : -1; +} + int AdaptiveLibMesh::get_bin_from_element(const libMesh::Elem* elem) const { int bin = elem_to_bin_map_[elem->id()]; From 2691ff8a0f843b6afb52b0b165f05436f08bc03e Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Wed, 21 Jan 2026 16:33:30 +0200 Subject: [PATCH 506/671] Fix type hinting and simplify implementation of combine_distributions (#3445) Co-authored-by: Paul Romano --- openmc/stats/univariate.py | 49 +++++++++++++++++----------------- tests/unit_tests/test_stats.py | 17 +++++++++++- 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index 272367f6f..94258c833 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -397,7 +397,7 @@ class Discrete(Univariate): def merge( cls, dists: Sequence[Discrete], - probs: Sequence[int] + probs: Sequence[float] ): """Merge multiple discrete distributions into a single distribution @@ -1897,7 +1897,7 @@ class Mixture(Univariate): def combine_distributions( - dists: Sequence[Univariate], + dists: Sequence[Discrete | Tabular], probs: Sequence[float] ): """Combine distributions with specified probabilities @@ -1912,41 +1912,40 @@ def combine_distributions( Parameters ---------- - dists : iterable of openmc.stats.Univariate + dists : sequence of openmc.stats.Discrete or openmc.stats.Tabular Distributions to combine - probs : iterable of float + probs : sequence of float Probability (or intensity) of each distribution """ - # Get copy of distribution list so as not to modify the argument - dist_list = deepcopy(dists) + for i, dist in enumerate(dists): + cv.check_type(f'dists[{i}]', dist, (Discrete, Tabular)) + cv.check_type(f'probs[{i}]', probs[i], Real) + cv.check_greater_than(f'probs[{i}]', probs[i], 0.0) # Get list of discrete/continuous distribution indices - discrete_index = [i for i, d in enumerate(dist_list) if isinstance(d, Discrete)] - cont_index = [i for i, d in enumerate(dist_list) if isinstance(d, Tabular)] + discrete_index = [i for i, d in enumerate(dists) if isinstance(d, Discrete)] + cont_index = [i for i, d in enumerate(dists) if isinstance(d, Tabular)] - # Apply probabilites to continuous distributions - for i in cont_index: - dist = dist_list[i] - dist._p *= probs[i] + cont_dists = [dists[i] for i in cont_index] + cont_probs = [probs[i] for i in cont_index] if discrete_index: # Create combined discrete distribution - dist_discrete = [dist_list[i] for i in discrete_index] + dist_discrete = [dists[i] for i in discrete_index] discrete_probs = [probs[i] for i in discrete_index] combined_dist = Discrete.merge(dist_discrete, discrete_probs) - - # Replace multiple discrete distributions with merged - for idx in reversed(discrete_index): - dist_list.pop(idx) - dist_list.append(combined_dist) - - # Combine discrete and continuous if present - if len(dist_list) > 1: - probs = [1.0]*len(dist_list) - dist_list[:] = [Mixture(probs, dist_list.copy())] - - return dist_list[0] + if cont_index: + return Mixture(cont_probs + [1.0], cont_dists + [combined_dist]) + else: + return combined_dist + else: + if len(cont_dists) == 1: + dist = cont_dists[0] + return Tabular(dist.x, dist.p * cont_probs[0], + dist.interpolation, bias=dist.bias) + else: + return Mixture(cont_probs, cont_dists) def check_bias_support(parent: Univariate, bias: Univariate | None): diff --git a/tests/unit_tests/test_stats.py b/tests/unit_tests/test_stats.py index e92e9e135..507e85743 100644 --- a/tests/unit_tests/test_stats.py +++ b/tests/unit_tests/test_stats.py @@ -633,12 +633,27 @@ def test_combine_distributions(): assert len(mixed.distribution) == 2 assert len(mixed.probability) == 2 + # Single tabular returns a tabular distribution with scaled probabilities + t_single = openmc.stats.Tabular([0.0, 1.0], [2.0, 0.0]) + scaled = openmc.stats.combine_distributions([t_single], [0.25]) + assert isinstance(scaled, openmc.stats.Tabular) + assert scaled.p == pytest.approx([0.5, 0.0]) + + # Mixture with biased tabular should preserve unbiased mean via weights + bias = openmc.stats.Tabular([0.0, 1.0], [2.0, 0.0]) + t_biased = openmc.stats.Tabular([0.0, 1.0], [1.0, 1.0], bias=bias) + d1 = openmc.stats.delta_function(0.0) + mixed = openmc.stats.combine_distributions([t_biased, d1], [0.5, 0.5]) + assert isinstance(mixed, openmc.stats.Mixture) + samples, weights = mixed.sample(10_000) + assert_sample_mean(samples*weights, 0.25) + # Combine 1 discrete and 2 tabular -- the tabular distributions should # combine to produce a uniform distribution with mean 0.5. The combined # distribution should have a mean of 0.25. t1 = openmc.stats.Tabular([0., 1.], [2.0, 0.0]) t2 = openmc.stats.Tabular([0., 1.], [0.0, 2.0]) - d1 = openmc.stats.Discrete([0.0], [1.0]) + d1 = openmc.stats.delta_function(0.0) combined = openmc.stats.combine_distributions([t1, t2, d1], [0.25, 0.25, 0.5]) assert combined.integral() == pytest.approx(1.0) From c5df2bf621ffdd3eee9f07334563c78293fa9519 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Thu, 22 Jan 2026 05:57:40 +0200 Subject: [PATCH 507/671] Fix for pandas version 3 (#3743) --- openmc/data/photon.py | 4 +++- openmc/tallies.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/openmc/data/photon.py b/openmc/data/photon.py index 25ded24cb..bc21b2e56 100644 --- a/openmc/data/photon.py +++ b/openmc/data/photon.py @@ -203,7 +203,9 @@ class AtomicRelaxation(EqualityMixin): for subshell, df in transitions.items(): cv.check_value('subshell', subshell, _SUBSHELLS) cv.check_type('transitions', df, pd.DataFrame) - self._transitions = transitions + self._transitions = { + subshell: df.convert_dtypes() for subshell, df in transitions.items() + } @classmethod def from_ace(cls, ace): diff --git a/openmc/tallies.py b/openmc/tallies.py index 7b1ef0219..74d4722af 100644 --- a/openmc/tallies.py +++ b/openmc/tallies.py @@ -1982,7 +1982,7 @@ class Tally(IDManagerMixin): # Expand the columns into Pandas MultiIndices for readability if pd.__version__ >= '0.16': - columns = copy.deepcopy(df.columns.values) + columns = copy.deepcopy(list(df.columns.values)) # Convert all elements in columns list to tuples for i, column in enumerate(columns): From 6b43a298f4dd6d252e61db81a95dca33f5a83428 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Thu, 22 Jan 2026 17:04:30 +0200 Subject: [PATCH 508/671] Add n_elements to the MeshBase protocol and deprecate num_mesh_cells (#3745) --- openmc/mesh.py | 36 +++++++++++++++++-- openmc/source.py | 4 +-- tests/unit_tests/mesh_to_vtk/test_vtk_dims.py | 4 +-- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/openmc/mesh.py b/openmc/mesh.py index ce5218b5e..12e879810 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -153,11 +153,17 @@ class MeshBase(IDManagerMixin, ABC): Unique identifier for the mesh name : str Name of the mesh + lower_left : Iterable of float + The lower-left coordinates + upper_right : Iterable of float + The upper-right coordinates bounding_box : openmc.BoundingBox Axis-aligned bounding box of the mesh as defined by the upper-right and lower-left coordinates. indices : Iterable of tuple An iterable of mesh indices for each mesh element, e.g. [(1, 1, 1), (2, 1, 1), ...] + n_elements : int + Number of elements in the mesh """ next_id = 1 @@ -179,6 +185,16 @@ class MeshBase(IDManagerMixin, ABC): self._name = name else: self._name = '' + + @property + @abstractmethod + def lower_left(self): + pass + + @property + @abstractmethod + def upper_right(self): + pass @property def bounding_box(self) -> openmc.BoundingBox: @@ -188,6 +204,11 @@ class MeshBase(IDManagerMixin, ABC): @abstractmethod def indices(self): pass + + @property + @abstractmethod + def n_elements(self): + pass def __repr__(self): string = type(self).__name__ + '\n' @@ -557,10 +578,19 @@ 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) @property def num_mesh_cells(self): - return np.prod(self.dimension) + warnings.warn( + "The 'num_mesh_cells' attribute is deprecated and will be removed in a future version. " + "Use 'n_elements' instead.", + FutureWarning, stacklevel=2 + ) + return self.n_elements def write_data_to_vtk(self, filename: PathLike, @@ -822,10 +852,10 @@ class StructuredMesh(MeshBase): """ cv.check_type('data label', label, str) - if dataset.size != self.num_mesh_cells: + if dataset.size != self.n_elements: raise ValueError( f"The size of the dataset '{label}' ({dataset.size}) should be" - f" equal to the number of mesh cells ({self.num_mesh_cells})" + f" equal to the number of mesh cells ({self.n_elements})" ) # accept a flat array as-is, assuming it is in the correct order diff --git a/openmc/source.py b/openmc/source.py index 84d8a9619..5acf55c21 100644 --- a/openmc/source.py +++ b/openmc/source.py @@ -572,10 +572,10 @@ class MeshSource(SourceBase): s = np.asarray(s) if isinstance(self.mesh, StructuredMesh): - if s.size != self.mesh.num_mesh_cells: + if s.size != self.mesh.n_elements: raise ValueError( f'The length of the source array ({s.size}) does not match ' - f'the number of mesh elements ({self.mesh.num_mesh_cells}).') + f'the number of mesh elements ({self.mesh.n_elements}).') # If user gave a multidimensional array, flatten in the order # of the mesh indices diff --git a/tests/unit_tests/mesh_to_vtk/test_vtk_dims.py b/tests/unit_tests/mesh_to_vtk/test_vtk_dims.py index 8166ba68c..366ce18ef 100644 --- a/tests/unit_tests/mesh_to_vtk/test_vtk_dims.py +++ b/tests/unit_tests/mesh_to_vtk/test_vtk_dims.py @@ -131,7 +131,7 @@ def test_write_data_to_vtk_size_mismatch(mesh): mesh : openmc.StructuredMesh The mesh to test """ - right_size = mesh.num_mesh_cells + right_size = mesh.n_elements data = np.random.random(right_size + 1) # Error message has \ in to escape characters that are otherwise recognized @@ -139,7 +139,7 @@ def test_write_data_to_vtk_size_mismatch(mesh): # string when using the match argument as that uses regular expression expected_error_msg = ( fr"The size of the dataset 'label' \({len(data)}\) should be equal to " - fr"the number of mesh cells \({mesh.num_mesh_cells}\)" + fr"the number of mesh cells \({mesh.n_elements}\)" ) with pytest.raises(ValueError, match=expected_error_msg): mesh.write_data_to_vtk(filename="out.vtk", datasets={"label": data}) From 049a852e52510c11cc5e1a7a8ddb3c58e393c910 Mon Sep 17 00:00:00 2001 From: Olek <45364492+yardasol@users.noreply.github.com> Date: Thu, 22 Jan 2026 15:21:47 -0600 Subject: [PATCH 509/671] Random Ray main function and autosetup refactoring (#3733) --- .../openmc/random_ray/random_ray_simulation.h | 14 +- openmc/model/model.py | 227 +++++++----------- src/random_ray/random_ray_simulation.cpp | 167 +++++++------ 3 files changed, 193 insertions(+), 215 deletions(-) diff --git a/include/openmc/random_ray/random_ray_simulation.h b/include/openmc/random_ray/random_ray_simulation.h index 3dec48bf2..3db651069 100644 --- a/include/openmc/random_ray/random_ray_simulation.h +++ b/include/openmc/random_ray/random_ray_simulation.h @@ -19,9 +19,9 @@ public: //---------------------------------------------------------------------------- // Methods - void compute_segment_correction_factors(); void apply_fixed_sources_and_mesh_domains(); void prepare_fixed_sources_adjoint(); + void prepare_adjoint_simulation(); void simulate(); void output_simulation_results() const; void instability_check( @@ -34,9 +34,15 @@ public: // Accessors FlatSourceDomain* domain() const { return domain_.get(); } + //---------------------------------------------------------------------------- + // Public data members + + // Flag for adjoint simulation; + bool adjoint_needed_; + private: //---------------------------------------------------------------------------- - // Data members + // Private data members // Contains all flat source region data unique_ptr domain_; @@ -51,6 +57,9 @@ private: // Number of energy groups int negroups_; + // Toggle for first simulation + bool is_first_simulation_; + }; // class RandomRaySimulation //============================================================================ @@ -60,6 +69,7 @@ private: void openmc_run_random_ray(); void validate_random_ray_inputs(); void openmc_reset_random_ray(); +void print_adjoint_header(); } // namespace openmc diff --git a/openmc/model/model.py b/openmc/model/model.py index 6e4c1c585..ca78f282f 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1179,8 +1179,8 @@ class Model: # Convert ID map to RGB image img = id_map_to_rgb( - id_map=id_map, - color_by=color_by, + id_map=id_map, + color_by=color_by, colors=colors, overlap_color=overlap_color ) @@ -1217,7 +1217,7 @@ class Model: extent=(x_min, x_max, y_min, y_max), **contour_kwargs ) - + # If only showing outline, set the axis limits and aspect explicitly if outline == 'only': axes.set_xlim(x_min, x_max) @@ -1685,6 +1685,85 @@ class Model: self.geometry.get_all_materials().values() ) + def _auto_generate_mgxs_lib( + self, + model: openmc.model.model, + groups: openmc.mgxs.EnergyGroups, + correction: str | none, + directory: pathlike, + ) -> openmc.mgxs.Library: + """ + Automatically generate a multi-group cross section libray from a model + with the specified group structure. + + Parameters + ---------- + groups : openmc.mgxs.EnergyGroups + Energy group structure for the MGXS. + nparticles : int + Number of particles to simulate per batch when generating MGXS. + mgxs_path : str + Filename for the MGXS HDF5 file. + correction : str + Transport correction to apply to the MGXS. Options are None and + "P0". + directory : str + Directory to run the simulation in, so as to contain XML files. + + Returns + ------- + mgxs_lib : openmc.mgxs.Library + OpenMC MGXS Library object + """ + + # Initialize MGXS library with a finished OpenMC geometry object + mgxs_lib = openmc.mgxs.Library(model.geometry) + + # Pick energy group structure + mgxs_lib.energy_groups = groups + + # Disable transport correction + mgxs_lib.correction = correction + + # Specify needed cross sections for random ray + if correction == 'P0': + mgxs_lib.mgxs_types = [ + 'nu-transport', 'absorption', 'nu-fission', 'fission', + 'consistent nu-scatter matrix', 'multiplicity matrix', 'chi' + ] + elif correction is None: + mgxs_lib.mgxs_types = [ + 'total', 'absorption', 'nu-fission', 'fission', + 'consistent nu-scatter matrix', 'multiplicity matrix', 'chi' + ] + + # Specify a "material" domain type for the cross section tally filters + mgxs_lib.domain_type = "material" + + # Specify the domains over which to compute multi-group cross sections + mgxs_lib.domains = model.geometry.get_all_materials().values() + + # Do not compute cross sections on a nuclide-by-nuclide basis + mgxs_lib.by_nuclide = False + + # Check the library - if no errors are raised, then the library is satisfactory. + mgxs_lib.check_library_for_openmc_mgxs() + + # Construct all tallies needed for the multi-group cross section library + mgxs_lib.build_library() + + # Create a "tallies.xml" file for the MGXS Library + mgxs_lib.add_to_tallies(model.tallies, merge=True) + + # Run + statepoint_filename = model.run(cwd=directory) + + # Load MGXS + with openmc.StatePoint(statepoint_filename) as sp: + mgxs_lib.load_from_statepoint(sp) + + return mgxs_lib + def _create_mgxs_sources( self, groups: openmc.mgxs.EnergyGroups, @@ -1848,52 +1927,8 @@ class Model: model.geometry.root_universe = infinite_universe # Add MGXS Tallies - - # Initialize MGXS library with a finished OpenMC geometry object - mgxs_lib = openmc.mgxs.Library(model.geometry) - - # Pick energy group structure - mgxs_lib.energy_groups = groups - - # Disable transport correction - mgxs_lib.correction = correction - - # Specify needed cross sections for random ray - if correction == 'P0': - mgxs_lib.mgxs_types = [ - 'nu-transport', 'absorption', 'nu-fission', 'fission', - 'consistent nu-scatter matrix', 'multiplicity matrix', 'chi' - ] - elif correction is None: - mgxs_lib.mgxs_types = [ - 'total', 'absorption', 'nu-fission', 'fission', - 'consistent nu-scatter matrix', 'multiplicity matrix', 'chi' - ] - - # Specify a "cell" domain type for the cross section tally filters - mgxs_lib.domain_type = "material" - - # Specify the cell domains over which to compute multi-group cross sections - mgxs_lib.domains = model.geometry.get_all_materials().values() - - # Do not compute cross sections on a nuclide-by-nuclide basis - mgxs_lib.by_nuclide = False - - # Check the library - if no errors are raised, then the library is satisfactory. - mgxs_lib.check_library_for_openmc_mgxs() - - # Construct all tallies needed for the multi-group cross section library - mgxs_lib.build_library() - - # Create a "tallies.xml" file for the MGXS Library - mgxs_lib.add_to_tallies(model.tallies, merge=True) - - # Run - statepoint_filename = model.run(cwd=directory) - - # Load MGXS - with openmc.StatePoint(statepoint_filename) as sp: - mgxs_lib.load_from_statepoint(sp) + mgxs_lib = self._auto_generate_mgxs_lib( + model, groups, correction, directory) # Create a MGXS File which can then be written to disk mgxs_set = mgxs_lib.get_xsdata(domain=material, xsdata_name=name) @@ -2055,48 +2090,8 @@ class Model: model.settings.output = {'summary': True, 'tallies': False} # Add MGXS Tallies - - # Initialize MGXS library with a finished OpenMC geometry object - mgxs_lib = openmc.mgxs.Library(model.geometry) - - # Pick energy group structure - mgxs_lib.energy_groups = groups - - # Disable transport correction - mgxs_lib.correction = correction - - # Specify needed cross sections for random ray - if correction == 'P0': - mgxs_lib.mgxs_types = ['nu-transport', 'absorption', 'nu-fission', 'fission', - 'consistent nu-scatter matrix', 'multiplicity matrix', 'chi'] - elif correction is None: - mgxs_lib.mgxs_types = ['total', 'absorption', 'nu-fission', 'fission', - 'consistent nu-scatter matrix', 'multiplicity matrix', 'chi'] - - # Specify a "cell" domain type for the cross section tally filters - mgxs_lib.domain_type = "material" - - # Specify the cell domains over which to compute multi-group cross sections - mgxs_lib.domains = model.geometry.get_all_materials().values() - - # Do not compute cross sections on a nuclide-by-nuclide basis - mgxs_lib.by_nuclide = False - - # Check the library - if no errors are raised, then the library is satisfactory. - mgxs_lib.check_library_for_openmc_mgxs() - - # Construct all tallies needed for the multi-group cross section library - mgxs_lib.build_library() - - # Create a "tallies.xml" file for the MGXS Library - mgxs_lib.add_to_tallies(model.tallies, merge=True) - - # Run - statepoint_filename = model.run(cwd=directory) - - # Load MGXS - with openmc.StatePoint(statepoint_filename) as sp: - mgxs_lib.load_from_statepoint(sp) + mgxs_lib = self._auto_generate_mgxs_lib( + model, groups, correction, directory) names = [mat.name for mat in mgxs_lib.domains] @@ -2146,52 +2141,8 @@ class Model: model.settings.output = {'summary': True, 'tallies': False} # Add MGXS Tallies - - # Initialize MGXS library with a finished OpenMC geometry object - mgxs_lib = openmc.mgxs.Library(model.geometry) - - # Pick energy group structure - mgxs_lib.energy_groups = groups - - # Disable transport correction - mgxs_lib.correction = correction - - # Specify needed cross sections for random ray - if correction == 'P0': - mgxs_lib.mgxs_types = [ - 'nu-transport', 'absorption', 'nu-fission', 'fission', - 'consistent nu-scatter matrix', 'multiplicity matrix', 'chi' - ] - elif correction is None: - mgxs_lib.mgxs_types = [ - 'total', 'absorption', 'nu-fission', 'fission', - 'consistent nu-scatter matrix', 'multiplicity matrix', 'chi' - ] - - # Specify a "cell" domain type for the cross section tally filters - mgxs_lib.domain_type = "material" - - # Specify the cell domains over which to compute multi-group cross sections - mgxs_lib.domains = model.geometry.get_all_materials().values() - - # Do not compute cross sections on a nuclide-by-nuclide basis - mgxs_lib.by_nuclide = False - - # Check the library - if no errors are raised, then the library is satisfactory. - mgxs_lib.check_library_for_openmc_mgxs() - - # Construct all tallies needed for the multi-group cross section library - mgxs_lib.build_library() - - # Create a "tallies.xml" file for the MGXS Library - mgxs_lib.add_to_tallies(model.tallies, merge=True) - - # Run - statepoint_filename = model.run(cwd=directory) - - # Load MGXS - with openmc.StatePoint(statepoint_filename) as sp: - mgxs_lib.load_from_statepoint(sp) + mgxs_lib = self._auto_generate_mgxs_lib( + model, groups, correction, directory) names = [mat.name for mat in mgxs_lib.domains] @@ -2617,5 +2568,3 @@ class SearchResult: def total_batches(self) -> int: """Total number of active batches used across all evaluations.""" return sum(self.batches) - - diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index d475b2593..2b54ae2eb 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -28,17 +28,13 @@ void openmc_run_random_ray() // Run forward simulation ////////////////////////////////////////////////////////// - // Check if adjoint calculation is needed. If it is, we will run the forward - // calculation first and then the adjoint calculation later. - bool adjoint_needed = FlatSourceDomain::adjoint_; - - // Configure the domain for forward simulation - FlatSourceDomain::adjoint_ = false; - - // If we're going to do an adjoint simulation afterwards, report that this is - // the initial forward flux solve. - if (adjoint_needed && mpi::master) - header("FORWARD FLUX SOLVE", 3); + if (mpi::master) { + if (FlatSourceDomain::adjoint_) { + FlatSourceDomain::adjoint_ = false; + openmc::print_adjoint_header(); + FlatSourceDomain::adjoint_ = true; + } + } // Initialize OpenMC general data structures openmc_simulation_init(); @@ -53,76 +49,20 @@ void openmc_run_random_ray() // Initialize fixed sources, if present sim.apply_fixed_sources_and_mesh_domains(); - // Begin main simulation timer - simulation::time_total.start(); - - // Execute random ray simulation + // Run initial random ray simulation sim.simulate(); - // End main simulation timer - simulation::time_total.stop(); - - // Normalize and save the final forward flux - double source_normalization_factor = - sim.domain()->compute_fixed_source_normalization_factor() / - (settings::n_batches - settings::n_inactive); - -#pragma omp parallel for - for (uint64_t se = 0; se < sim.domain()->n_source_elements(); se++) { - sim.domain()->source_regions_.scalar_flux_final(se) *= - source_normalization_factor; - } - - // Finalize OpenMC - openmc_simulation_finalize(); - - // Output all simulation results - sim.output_simulation_results(); - ////////////////////////////////////////////////////////// // Run adjoint simulation (if enabled) ////////////////////////////////////////////////////////// - if (!adjoint_needed) { - return; + if (sim.adjoint_needed_) { + // Setup for adjoint simulation + sim.prepare_adjoint_simulation(); + + // Run adjoint simulation + sim.simulate(); } - - reset_timers(); - - // Configure the domain for adjoint simulation - FlatSourceDomain::adjoint_ = true; - - if (mpi::master) - header("ADJOINT FLUX SOLVE", 3); - - // Initialize OpenMC general data structures - openmc_simulation_init(); - - sim.domain()->k_eff_ = 1.0; - - // Initialize adjoint fixed sources, if present - sim.prepare_fixed_sources_adjoint(); - - // Transpose scattering matrix - sim.domain()->transpose_scattering_matrix(); - - // Swap nu_sigma_f and chi - sim.domain()->nu_sigma_f_.swap(sim.domain()->chi_); - - // Begin main simulation timer - simulation::time_total.start(); - - // Execute random ray simulation - sim.simulate(); - - // End main simulation timer - simulation::time_total.stop(); - - // Finalize OpenMC - openmc_simulation_finalize(); - - // Output all simulation results - sim.output_simulation_results(); } // Enforces restrictions on inputs in random ray mode. While there are @@ -353,6 +293,17 @@ void openmc_reset_random_ray() RandomRay::sample_method_ = RandomRaySampleMethod::PRNG; } +void print_adjoint_header() +{ + if (!FlatSourceDomain::adjoint_) + // If we're going to do an adjoint simulation afterwards, report that this + // is the initial forward flux solve. + header("FORWARD FLUX SOLVE", 3); + else + // Otherwise report that we are doing the adjoint simulation + header("ADJOINT FLUX SOLVE", 3); +} + //============================================================================== // RandomRaySimulation implementation //============================================================================== @@ -383,6 +334,16 @@ RandomRaySimulation::RandomRaySimulation() // Convert OpenMC native MGXS into a more efficient format // internal to the random ray solver domain_->flatten_xs(); + + // Check if adjoint calculation is needed. If it is, we will run the forward + // calculation first and then the adjoint calculation later. + adjoint_needed_ = FlatSourceDomain::adjoint_; + + // Adjoint is always false for the forward calculation + FlatSourceDomain::adjoint_ = false; + + // The first simulation is run after initialization + is_first_simulation_ = true; } void RandomRaySimulation::apply_fixed_sources_and_mesh_domains() @@ -403,8 +364,41 @@ void RandomRaySimulation::prepare_fixed_sources_adjoint() } } +void RandomRaySimulation::prepare_adjoint_simulation() +{ + // Configure the domain for adjoint simulation + FlatSourceDomain::adjoint_ = true; + + // Reset k-eff + domain_->k_eff_ = 1.0; + + // Initialize adjoint fixed sources, if present + prepare_fixed_sources_adjoint(); + + // Transpose scattering matrix + domain_->transpose_scattering_matrix(); + + // Swap nu_sigma_f and chi + domain_->nu_sigma_f_.swap(domain_->chi_); +} + void RandomRaySimulation::simulate() { + if (!is_first_simulation_) { + if (mpi::master && adjoint_needed_) + openmc::print_adjoint_header(); + + // Reset the timers and reinitialize the general OpenMC datastructures if + // this is after the first simulation + reset_timers(); + + // Initialize OpenMC general data structures + openmc_simulation_init(); + } + + // Begin main simulation timer + simulation::time_total.start(); + // Random ray power iteration loop while (simulation::current_batch < settings::n_batches) { // Initialize the current batch @@ -493,6 +487,31 @@ void RandomRaySimulation::simulate() } // End random ray power iteration loop domain_->count_external_source_regions(); + + // End main simulation timer + simulation::time_total.stop(); + + // Normalize and save the final flux + double source_normalization_factor = + domain_->compute_fixed_source_normalization_factor() / + (settings::n_batches - settings::n_inactive); + +#pragma omp parallel for + for (uint64_t se = 0; se < domain_->n_source_elements(); se++) { + domain_->source_regions_.scalar_flux_final(se) *= + source_normalization_factor; + } + + // Finalize OpenMC + openmc_simulation_finalize(); + + // Output all simulation results + output_simulation_results(); + + // Toggle that the simulation object has been initialized after the first + // simulation + if (is_first_simulation_) + is_first_simulation_ = false; } void RandomRaySimulation::output_simulation_results() const From 73a98b2cc6ab3c1fbe889e155a269b8193d15f62 Mon Sep 17 00:00:00 2001 From: Olek <45364492+yardasol@users.noreply.github.com> Date: Fri, 23 Jan 2026 10:20:25 -0600 Subject: [PATCH 510/671] Add `random_ray_pincell()` example model (#3735) Co-authored-by: John Tramm --- openmc/examples.py | 185 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 151 insertions(+), 34 deletions(-) diff --git a/openmc/examples.py b/openmc/examples.py index 01dd9d01f..350a4d24d 100644 --- a/openmc/examples.py +++ b/openmc/examples.py @@ -4,7 +4,7 @@ import numpy as np import openmc - +PINCELL_PITCH = 1.26 # cm def pwr_pin_cell() -> openmc.Model: """Create a PWR pin-cell model. @@ -51,7 +51,7 @@ def pwr_pin_cell() -> openmc.Model: model.materials = (fuel, clad, hot_water) # Instantiate ZCylinder surfaces - pitch = 1.26 + pitch = PINCELL_PITCH fuel_or = openmc.ZCylinder(x0=0, y0=0, r=0.39218, name='Fuel OR') clad_or = openmc.ZCylinder(x0=0, y0=0, r=0.45720, name='Clad OR') left = openmc.XPlane(x0=-pitch/2, name='left', boundary_type='reflective') @@ -319,14 +319,14 @@ def pwr_core() -> openmc.Model: l100 = openmc.RectLattice( name='Fuel assembly (lower half)', lattice_id=100) l100.lower_left = (-10.71, -10.71) - l100.pitch = (1.26, 1.26) + l100.pitch = (PINCELL_PITCH, PINCELL_PITCH) l100.universes = np.tile(fuel_cold, (17, 17)) l100.universes[tube_x, tube_y] = tube_cold l101 = openmc.RectLattice( name='Fuel assembly (upper half)', lattice_id=101) l101.lower_left = (-10.71, -10.71) - l101.pitch = (1.26, 1.26) + l101.pitch = (PINCELL_PITCH, PINCELL_PITCH) l101.universes = np.tile(fuel_hot, (17, 17)) l101.universes[tube_x, tube_y] = tube_hot @@ -350,7 +350,7 @@ def pwr_core() -> openmc.Model: # Define core lattices l200 = openmc.RectLattice(name='Core lattice (lower half)', lattice_id=200) l200.lower_left = (-224.91, -224.91) - l200.pitch = (21.42, 21.42) + l200.pitch = (17 * PINCELL_PITCH, 17 * PINCELL_PITCH) l200.universes = [ [fa_cw]*21, [fa_cw]*21, @@ -376,7 +376,7 @@ def pwr_core() -> openmc.Model: l201 = openmc.RectLattice(name='Core lattice (lower half)', lattice_id=201) l201.lower_left = (-224.91, -224.91) - l201.pitch = (21.42, 21.42) + l201.pitch = (17 * PINCELL_PITCH, 17 * PINCELL_PITCH) l201.universes = [ [fa_hw]*21, [fa_hw]*21, @@ -488,7 +488,7 @@ def pwr_assembly() -> openmc.Model: clad_or = openmc.ZCylinder(x0=0, y0=0, r=0.45720, name='Clad OR') # Create boundary planes to surround the geometry - pitch = 21.42 + pitch = 17 * PINCELL_PITCH min_x = openmc.XPlane(x0=-pitch/2, boundary_type='reflective') max_x = openmc.XPlane(x0=+pitch/2, boundary_type='reflective') min_y = openmc.YPlane(y0=-pitch/2, boundary_type='reflective') @@ -514,7 +514,7 @@ def pwr_assembly() -> openmc.Model: # Create fuel assembly Lattice assembly = openmc.RectLattice(name='Fuel Assembly') - assembly.pitch = (pitch/17, pitch/17) + assembly.pitch = (PINCELL_PITCH, PINCELL_PITCH) assembly.lower_left = (-pitch/2, -pitch/2) # Create array indices for guide tube locations in lattice @@ -656,24 +656,21 @@ def slab_mg(num_regions=1, mat_names=None, mgxslib_name='2g.h5') -> openmc.Model return model -def random_ray_lattice() -> openmc.Model: - """Create a 2x2 PWR pincell asymmetrical lattic eexample. - - This model is a 2x2 reflective lattice of fuel pins with one of the lattice - locations having just moderator instead of a fuel pin. It uses 7 group - cross section data. +def _generate_c5g7_materials() -> openmc.Materials: + """Generate materials utilizing multi-group cross sections based on the + the C5G7 Benchmark. Returns ------- - model : openmc.Model - A PWR 2x2 lattice model + materials : openmc.Materials + Materials object containing UO2 and water materials. + Data Sources + ------------ + All cross section data are from: + Lewis et al., "Benchmark specification for determinisitc 2D/3D MOX fuel + assembly transport calculations without spatial homogenization" """ - model = openmc.Model() - - ########################################################################### - # Create MGXS data for the problem - # Instantiate the energy group data group_edges = [1e-5, 0.0635, 10.0, 1.0e2, 1.0e3, 0.5e6, 1.0e6, 20.0e6] groups = openmc.mgxs.EnergyGroups(group_edges) @@ -704,9 +701,10 @@ def random_ray_lattice() -> openmc.Model: uo2_xsdata.set_fission([7.21206e-03, 8.19301e-04, 6.45320e-03, 1.85648e-02, 1.78084e-02, 8.30348e-02, 2.16004e-01]) - uo2_xsdata.set_nu_fission([2.005998e-02, 2.027303e-03, 1.570599e-02, - 4.518301e-02, 4.334208e-02, 2.020901e-01, - 5.257105e-01]) + nu_fission = np.array([2.005998e-02, 2.027303e-03, 1.570599e-02, + 4.518301e-02, 4.334208e-02, 2.020901e-01, + 5.257105e-01]) + uo2_xsdata.set_nu_fission(nu_fission) uo2_xsdata.set_chi([5.8791e-01, 4.1176e-01, 3.3906e-04, 1.1761e-07, 0.0000e+00, 0.0000e+00, 0.0000e+00]) @@ -752,14 +750,28 @@ def random_ray_lattice() -> openmc.Model: # Instantiate a Materials collection and export to XML materials = openmc.Materials([uo2, water]) materials.cross_sections = "mgxs.h5" + return materials - ########################################################################### - # Define problem geometry +def _generate_subdivided_pin_cell(uo2, water) -> openmc.Universe: + """Create a radially and azimuthally subdivided pin cell universe. Helper + function for random_ray_pin_cell() and random_ray_lattice() + + Parameters + ---------- + uo2 : openmc.Material + UO2 material + water : openmc.Material + Water material + + Returns + ------- + pincell : openmc.Universe + Universe containing an unbounded pin cell + + """ ######################################## - # Define an unbounded pincell universe - - pitch = 1.26 + # Define an unbounded pin cell universe # Create a surface for the fuel outer radius fuel_or = openmc.ZCylinder(r=0.54, name='Fuel OR') @@ -781,7 +793,7 @@ def random_ray_lattice() -> openmc.Model: moderator_c = openmc.Cell( fill=water, region=+outer_ring_b, name='moderator outer c') - # Create pincell universe + # Create pin cell universe pincell_base = openmc.Universe() # Register Cells with Universe @@ -801,19 +813,125 @@ def random_ray_lattice() -> openmc.Model: for i in range(8): azimuthal_cell = openmc.Cell(name=f'azimuthal_cell_{i}') azimuthal_cell.fill = pincell_base - azimuthal_cell.region = +azimuthal_planes[i] & -azimuthal_planes[(i+1) % 8] + azimuthal_cell.region = + \ + azimuthal_planes[i] & -azimuthal_planes[(i+1) % 8] azimuthal_cells.append(azimuthal_cell) # Create a geometry with the azimuthal universes pincell = openmc.Universe(cells=azimuthal_cells, name='pincell') + return pincell + + +def random_ray_pin_cell() -> openmc.Model: + """Create a PWR pin cell example using C5G7 cross section data. + cross section data. + + Returns + ------- + model : openmc.Model + A PWR pin cell model + + """ + model = openmc.Model() + + ########################################################################### + # Create Materials for the problem + materials = _generate_c5g7_materials() + uo2 = materials[0] + water = materials[1] + + ########################################################################### + # Define problem geometry + pincell = _generate_subdivided_pin_cell(uo2, water) + + ######################################## + # Define cell containing lattice and other stuff + pitch = PINCELL_PITCH + box = openmc.model.RectangularPrism(pitch, pitch, boundary_type='reflective') + + pincell = openmc.Cell(fill=pincell, region=-box, name='pincell') + + # Create a geometry with the top-level cell + geometry = openmc.Geometry([pincell]) + + ########################################################################### + # Define problem settings + + # Instantiate a Settings object, set all runtime parameters, and export to XML + settings = openmc.Settings() + settings.energy_mode = "multi-group" + settings.batches = 400 + settings.inactive = 200 + settings.particles = 100 + + # Create an initial uniform spatial source distribution over fissionable zones + lower_left = (-pitch / 2, -pitch / 2, -1) + upper_right = (pitch / 2, pitch / 2, 1) + uniform_dist = openmc.stats.Box(lower_left, upper_right) + rr_source = openmc.IndependentSource(space=uniform_dist) + + settings.random_ray['distance_active'] = 100.0 + settings.random_ray['distance_inactive'] = 20.0 + settings.random_ray['ray_source'] = rr_source + settings.random_ray['volume_normalized_flux_tallies'] = True + + ########################################################################### + # Define tallies + # Now use the mesh filter in a tally and indicate what scores are desired + tally = openmc.Tally(name="Pin tally") + tally.scores = ['flux', 'fission', 'nu-fission'] + tally.estimator = 'analog' + + # Instantiate a Tallies collection and export to XML + tallies = openmc.Tallies([tally]) + + ########################################################################### + # Exporting to OpenMC model + ########################################################################### + + model.geometry = geometry + model.materials = materials + model.settings = settings + model.tallies = tallies + return model + + +def random_ray_lattice() -> openmc.Model: + """Create a 2x2 PWR pin cell asymmetrical lattice example. + + This model is a 2x2 reflective lattice of fuel pins with one of the lattice + locations having just moderator instead of a fuel pin. It uses C5G7 + cross section data. + + Returns + ------- + model : openmc.Model + A PWR 2x2 lattice model + + """ + model = openmc.Model() + + ########################################################################### + # Create Materials for the problem + materials = _generate_c5g7_materials() + uo2 = materials[0] + water = materials[1] + + ########################################################################### + # Define problem geometry + pincell = _generate_subdivided_pin_cell(uo2, water) + ######################################## # Define a moderator lattice universe - moderator_infinite = openmc.Cell(fill=water, name='moderator infinite') + moderator_infinite = openmc.Cell(name='moderator infinite') + moderator_infinite.fill = water + mu = openmc.Universe() mu.add_cells([moderator_infinite]) + pitch = PINCELL_PITCH lattice = openmc.RectLattice() lattice.lower_left = [-pitch/2.0, -pitch/2.0] lattice.pitch = [pitch/10.0, pitch/10.0] @@ -837,8 +955,7 @@ def random_ray_lattice() -> openmc.Model: ######################################## # Define cell containing lattice and other stuff - box = openmc.model.RectangularPrism( - pitch*2, pitch*2, boundary_type='reflective') + box = openmc.model.RectangularPrism(pitch*2, pitch*2, boundary_type='reflective') assembly = openmc.Cell(fill=lattice2x2, region=-box, name='assembly') From 3e2f1f521ade6df388544a3e0981a75503eb460c Mon Sep 17 00:00:00 2001 From: Kevin Sawatzky <66632997+nuclearkevin@users.noreply.github.com> Date: Fri, 23 Jan 2026 10:23:05 -0600 Subject: [PATCH 511/671] Enable fission heating tallies in the random ray solver (#3714) --- docs/source/usersguide/random_ray.rst | 1 + .../openmc/random_ray/flat_source_domain.h | 1 + openmc/model/model.py | 6 +- src/random_ray/flat_source_domain.cpp | 14 +++- src/random_ray/random_ray_simulation.cpp | 5 +- .../__init__.py | 0 .../infinite_medium/inputs_true.dat | 73 +++++++++++++++++++ .../infinite_medium/results_true.dat | 5 ++ .../material_wise/inputs_true.dat | 73 +++++++++++++++++++ .../material_wise/results_true.dat | 5 ++ .../stochastic_slab/inputs_true.dat | 73 +++++++++++++++++++ .../stochastic_slab/results_true.dat | 5 ++ .../test.py | 60 +++++++++++++++ 13 files changed, 315 insertions(+), 6 deletions(-) create mode 100644 tests/regression_tests/random_ray_auto_convert_kappa_fission/__init__.py create mode 100644 tests/regression_tests/random_ray_auto_convert_kappa_fission/infinite_medium/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert_kappa_fission/infinite_medium/results_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert_kappa_fission/material_wise/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert_kappa_fission/material_wise/results_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert_kappa_fission/stochastic_slab/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert_kappa_fission/stochastic_slab/results_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert_kappa_fission/test.py diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index 382381a9e..64ef7de6d 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -513,6 +513,7 @@ Supported scores: - total - fission - nu-fission + - kappa-fission - events Supported Estimators: diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index 4df4e5d8d..0d4086966 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -107,6 +107,7 @@ public: vector nu_sigma_f_; vector sigma_f_; vector chi_; + vector kappa_fission_; // 3D arrays stored in 1D representing values for all materials x energy // groups x energy groups diff --git a/openmc/model/model.py b/openmc/model/model.py index ca78f282f..90b43f9f7 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1729,12 +1729,14 @@ class Model: if correction == 'P0': mgxs_lib.mgxs_types = [ 'nu-transport', 'absorption', 'nu-fission', 'fission', - 'consistent nu-scatter matrix', 'multiplicity matrix', 'chi' + 'consistent nu-scatter matrix', 'multiplicity matrix', 'chi', + 'kappa-fission' ] elif correction is None: mgxs_lib.mgxs_types = [ 'total', 'absorption', 'nu-fission', 'fission', - 'consistent nu-scatter matrix', 'multiplicity matrix', 'chi' + 'consistent nu-scatter matrix', 'multiplicity matrix', 'chi', + 'kappa-fission' ] # Specify a "material" domain type for the cross section tally filters diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 2f5007fc9..c2effaa5d 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -665,10 +665,15 @@ void FlatSourceDomain::random_ray_tally() score = 1.0; break; + case SCORE_KAPPA_FISSION: + score = flux * volume * kappa_fission_[material * negroups_ + g] * + density_mult; + break; + default: fatal_error("Invalid score specified in tallies.xml. Only flux, " - "total, fission, nu-fission, and events are supported in " - "random ray mode."); + "total, fission, nu-fission, kappa-fission, and events " + "are supported in random ray mode."); break; } // Apply score to the appropriate tally bin @@ -1165,6 +1170,10 @@ void FlatSourceDomain::flatten_xs() } chi_.push_back(chi); + double kappa_fission = + m.get_xs(MgxsType::KAPPA_FISSION, g_out, NULL, NULL, NULL, t, a); + kappa_fission_.push_back(kappa_fission); + for (int g_in = 0; g_in < negroups_; g_in++) { double sigma_s = m.get_xs(MgxsType::NU_SCATTER, g_in, &g_out, NULL, NULL, t, a); @@ -1180,6 +1189,7 @@ void FlatSourceDomain::flatten_xs() nu_sigma_f_.push_back(0); sigma_f_.push_back(0); chi_.push_back(0); + kappa_fission_.push_back(0); for (int g_in = 0; g_in < negroups_; g_in++) { sigma_s_.push_back(0); } diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index 2b54ae2eb..7bab3a9b1 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -83,11 +83,12 @@ void validate_random_ray_inputs() case SCORE_FISSION: case SCORE_NU_FISSION: case SCORE_EVENTS: + case SCORE_KAPPA_FISSION: break; default: fatal_error( - "Invalid score specified. Only flux, total, fission, nu-fission, and " - "event scores are supported in random ray mode."); + "Invalid score specified. Only flux, total, fission, nu-fission, " + "kappa-fission, and event scores are supported in random ray mode."); } } diff --git a/tests/regression_tests/random_ray_auto_convert_kappa_fission/__init__.py b/tests/regression_tests/random_ray_auto_convert_kappa_fission/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_auto_convert_kappa_fission/infinite_medium/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_kappa_fission/infinite_medium/inputs_true.dat new file mode 100644 index 000000000..9f3a827f6 --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_kappa_fission/infinite_medium/inputs_true.dat @@ -0,0 +1,73 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + + + -0.63 -0.63 -1 0.63 0.63 1 + + + true + + + multi-group + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 + 150.0 + + + + + + linear + + + 2 2 + -0.63 -0.63 + 0.63 0.63 + + + + + 1 + + + 61 + kappa-fission + + + diff --git a/tests/regression_tests/random_ray_auto_convert_kappa_fission/infinite_medium/results_true.dat b/tests/regression_tests/random_ray_auto_convert_kappa_fission/infinite_medium/results_true.dat new file mode 100644 index 000000000..27b32d787 --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_kappa_fission/infinite_medium/results_true.dat @@ -0,0 +1,5 @@ +k-combined: +7.479770E-01 1.624548E-02 +tally 1: +2.965503E+08 +1.762157E+16 diff --git a/tests/regression_tests/random_ray_auto_convert_kappa_fission/material_wise/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_kappa_fission/material_wise/inputs_true.dat new file mode 100644 index 000000000..edf68f7e2 --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_kappa_fission/material_wise/inputs_true.dat @@ -0,0 +1,73 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + + + -0.63 -0.63 -1 0.63 0.63 1 + + + true + + + multi-group + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 + 150.0 + + + + + + linear + + + 2 2 + -0.63 -0.63 + 0.63 0.63 + + + + + 1 + + + 97 + kappa-fission + + + diff --git a/tests/regression_tests/random_ray_auto_convert_kappa_fission/material_wise/results_true.dat b/tests/regression_tests/random_ray_auto_convert_kappa_fission/material_wise/results_true.dat new file mode 100644 index 000000000..5a379db7e --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_kappa_fission/material_wise/results_true.dat @@ -0,0 +1,5 @@ +k-combined: +7.372542E-01 6.967831E-03 +tally 1: +2.909255E+08 +1.693395E+16 diff --git a/tests/regression_tests/random_ray_auto_convert_kappa_fission/stochastic_slab/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_kappa_fission/stochastic_slab/inputs_true.dat new file mode 100644 index 000000000..edf68f7e2 --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_kappa_fission/stochastic_slab/inputs_true.dat @@ -0,0 +1,73 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + + + -0.63 -0.63 -1 0.63 0.63 1 + + + true + + + multi-group + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 + 150.0 + + + + + + linear + + + 2 2 + -0.63 -0.63 + 0.63 0.63 + + + + + 1 + + + 97 + kappa-fission + + + diff --git a/tests/regression_tests/random_ray_auto_convert_kappa_fission/stochastic_slab/results_true.dat b/tests/regression_tests/random_ray_auto_convert_kappa_fission/stochastic_slab/results_true.dat new file mode 100644 index 000000000..a60e5aa3c --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_kappa_fission/stochastic_slab/results_true.dat @@ -0,0 +1,5 @@ +k-combined: +6.413334E-01 2.083132E-02 +tally 1: +2.541463E+08 +1.297259E+16 diff --git a/tests/regression_tests/random_ray_auto_convert_kappa_fission/test.py b/tests/regression_tests/random_ray_auto_convert_kappa_fission/test.py new file mode 100644 index 000000000..6decf165a --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_kappa_fission/test.py @@ -0,0 +1,60 @@ +import os + +import openmc +from openmc.examples import pwr_pin_cell +from openmc import RegularMesh +from openmc.utility_funcs import change_directory +import pytest + +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) + + +@pytest.mark.parametrize("method", ["material_wise", "stochastic_slab", "infinite_medium"]) +def test_random_ray_auto_convert(method): + with change_directory(method): + openmc.reset_auto_ids() + + # Start with a normal continuous energy model + model = pwr_pin_cell() + + # Convert to a multi-group model + model.convert_to_multigroup( + method=method, groups='CASMO-2', nparticles=100, + overwrite_mgxs_library=False, mgxs_path="mgxs.h5" + ) + + # Convert to a random ray model + model.convert_to_random_ray() + + # Set the number of particles + model.settings.particles = 100 + + # Overlay a basic 2x2 mesh + n = 2 + mesh = RegularMesh() + mesh.dimension = (n, n) + bbox = model.geometry.bounding_box + mesh.lower_left = (bbox.lower_left[0], bbox.lower_left[1]) + mesh.upper_right = (bbox.upper_right[0], bbox.upper_right[1]) + model.settings.random_ray['source_region_meshes'] = [ + (mesh, [model.geometry.root_universe])] + + # Set the source shape to linear + model.settings.random_ray['source_shape'] = 'linear' + + # Set a material tally + t = openmc.Tally(name = 'KF Tally') + t.filters = [openmc.MaterialFilter(bins=1)] + t.scores = ['kappa-fission'] + model.tallies.append(t) + + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() From 5c502ddb3e3a4e2112b93d6afdfc7f0afbc85194 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 26 Jan 2026 11:09:58 -0600 Subject: [PATCH 512/671] Do not add radioactive Ta180 when calling add_element('Ta') (#3750) --- openmc/element.py | 2 -- tests/unit_tests/test_element.py | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/openmc/element.py b/openmc/element.py index f1d8f5f24..5deede971 100644 --- a/openmc/element.py +++ b/openmc/element.py @@ -184,8 +184,6 @@ class Element(str): for nuclide in absent_nuclides: if nuclide in ['O17', 'O18'] and 'O16' in mutual_nuclides: abundances['O16'] += NATURAL_ABUNDANCE[nuclide] - elif nuclide == 'Ta180_m1' and 'Ta180' in library_nuclides: - abundances['Ta180'] = NATURAL_ABUNDANCE[nuclide] elif nuclide == 'Ta180_m1' and 'Ta181' in mutual_nuclides: abundances['Ta181'] += NATURAL_ABUNDANCE[nuclide] elif nuclide == 'W180' and 'W182' in mutual_nuclides: diff --git a/tests/unit_tests/test_element.py b/tests/unit_tests/test_element.py index d91cfaf6e..82e526ec3 100644 --- a/tests/unit_tests/test_element.py +++ b/tests/unit_tests/test_element.py @@ -45,11 +45,11 @@ def test_expand_no_isotopes(): def test_expand_ta(): - ref = {'Ta180': 0.01201, 'Ta181': 99.98799} + ref = {'Ta181': 100.0} element = openmc.Element('Ta') for isotope in element.expand(100.0, 'ao'): assert isotope[1] == approx(ref[isotope[0]]) - + def test_expand_exceptions(): """ Test that correct exceptions are raised for invalid input """ From db426b66ca6d40a7988f74bc0296e7a4a78a7d69 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Tue, 27 Jan 2026 07:06:36 +0100 Subject: [PATCH 513/671] Materials name persist when running depletion simulation (#3738) Co-authored-by: GuySten Co-authored-by: GuySten <62616591+GuySten@users.noreply.github.com> Co-authored-by: Paul Romano --- docs/source/io_formats/depletion_results.rst | 1 + openmc/deplete/abc.py | 2 ++ openmc/deplete/openmc_operator.py | 11 ++++++++--- openmc/deplete/stepresult.py | 19 +++++++++++++++---- tests/dummy_operator.py | 2 +- tests/unit_tests/test_deplete_activation.py | 5 +++++ tests/unit_tests/test_deplete_decay.py | 5 +++++ tests/unit_tests/test_deplete_integrator.py | 6 ++++-- 8 files changed, 41 insertions(+), 10 deletions(-) diff --git a/docs/source/io_formats/depletion_results.rst b/docs/source/io_formats/depletion_results.rst index b2a726dad..7fb088268 100644 --- a/docs/source/io_formats/depletion_results.rst +++ b/docs/source/io_formats/depletion_results.rst @@ -34,6 +34,7 @@ The current version of the depletion results file format is 1.2. :Attributes: - **index** (*int*) -- Index used in results for this material - **volume** (*double*) -- Volume of this material in [cm^3] + - **name** (*char[]*) -- Name of this material **/nuclides//** diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index ee742f22b..056f7c273 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -209,6 +209,8 @@ class TransportOperator(ABC): simulation. full_burn_list : list of int All burnable materials in the geometry. + name_list : list of str + Material names corresponding to materials in burn_list """ def finalize(self): diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py index 2d7694093..7928e89ee 100644 --- a/openmc/deplete/openmc_operator.py +++ b/openmc/deplete/openmc_operator.py @@ -211,7 +211,7 @@ class OpenMCOperator(TransportOperator): "section data.") warn(msg) if mat.depletable: - burnable_mats.add(str(mat.id)) + burnable_mats.add((str(mat.id), mat.name)) if mat.volume is None: if mat.name is None: msg = ("Volume not specified for depletable material " @@ -229,9 +229,12 @@ class OpenMCOperator(TransportOperator): "No depletable materials were found in the model.") # Sort the sets - burnable_mats = sorted(burnable_mats, key=int) + burnable_mats = sorted(burnable_mats, key=lambda x: int(x[0])) model_nuclides = sorted(model_nuclides) + # Store material names for later use + burnable_mats, self.name_list = zip(*burnable_mats) + # Construct a global nuclide dictionary, burned first nuclides = list(self.chain.nuclide_dict) for nuc in model_nuclides: @@ -541,6 +544,8 @@ class OpenMCOperator(TransportOperator): A list of all material IDs to be burned. Used for sorting the simulation. full_burn_list : list List of all burnable material IDs + name_list : list of str + Material names corresponding to materials in burn_list """ nuc_list = self.number.burnable_nuclides @@ -554,4 +559,4 @@ class OpenMCOperator(TransportOperator): volume_list = comm.allgather(volume) volume = {k: v for d in volume_list for k, v in d.items()} - return volume, nuc_list, burn_list, self.burnable_mats + return volume, nuc_list, burn_list, self.burnable_mats, self.name_list diff --git a/openmc/deplete/stepresult.py b/openmc/deplete/stepresult.py index ff39e9acb..27420246f 100644 --- a/openmc/deplete/stepresult.py +++ b/openmc/deplete/stepresult.py @@ -70,6 +70,7 @@ class StepResult: self.index_mat = None self.index_nuc = None self.mat_to_hdf5_ind = None + self.name_list = None self.data = None @@ -138,7 +139,7 @@ class StepResult: def n_hdf5_mats(self): return len(self.mat_to_hdf5_ind) - def allocate(self, volume, nuc_list, burn_list, full_burn_list): + def allocate(self, volume, nuc_list, burn_list, full_burn_list, name_list=None): """Allocate memory for depletion step data Parameters @@ -151,12 +152,15 @@ class StepResult: A list of all mat IDs to be burned. Used for sorting the simulation. full_burn_list : list of str List of all burnable material IDs + name_list : list of str, optional + Material names corresponding to materials in burn_list """ self.volume = copy.deepcopy(volume) self.index_nuc = {nuc: i for i, nuc in enumerate(nuc_list)} self.index_mat = {mat: i for i, mat in enumerate(burn_list)} self.mat_to_hdf5_ind = {mat: i for i, mat in enumerate(full_burn_list)} + self.mat_to_name = dict(zip(burn_list, name_list)) if name_list is not None else {} # Create storage array self.data = np.zeros((self.n_mat, self.n_nuc)) @@ -184,7 +188,7 @@ class StepResult: # Direct transfer direct_attrs = ("time", "k", "source_rate", "index_nuc", - "mat_to_hdf5_ind", "proc_time") + "mat_to_hdf5_ind", "mat_to_name", "proc_time") for attr in direct_attrs: setattr(new, attr, getattr(self, attr)) # Get applicable slice of data @@ -223,6 +227,8 @@ class StepResult: f'mat_id {mat_id} not found in StepResult. Available mat_id ' f'values are {list(self.volume.keys())}' ) from e + if mat_id in self.mat_to_name: + material.name = self.mat_to_name[mat_id] for nuc, _ in sorted(self.index_nuc.items(), key=lambda x: x[1]): atoms = self[mat_id, nuc] if atoms <= 0.0: @@ -313,6 +319,8 @@ class StepResult: mat_single_group = mat_group.create_group(mat) mat_single_group.attrs["index"] = self.mat_to_hdf5_ind[mat] mat_single_group.attrs["volume"] = self.volume[mat] + if mat in self.mat_to_name: + mat_single_group.attrs["name"] = self.mat_to_name[mat] nuc_group = handle.create_group("nuclides") @@ -495,6 +503,7 @@ class StepResult: results.volume = {} results.index_mat = {} results.index_nuc = {} + results.mat_to_name = {} rxn_nuc_to_ind = {} rxn_to_ind = {} @@ -504,6 +513,8 @@ class StepResult: results.volume[mat] = vol results.index_mat[mat] = ind + if "name" in mat_handle.attrs: + results.mat_to_name[mat] = mat_handle.attrs["name"] for nuc, nuc_handle in handle["/nuclides"].items(): ind_atom = nuc_handle.attrs["atom number index"] @@ -569,11 +580,11 @@ class StepResult: .. versionadded:: 0.14.0 """ # Get indexing terms - vol_dict, nuc_list, burn_list, full_burn_list = op.get_results_info() + vol_dict, nuc_list, burn_list, full_burn_list, name_list = op.get_results_info() # Create results results = StepResult() - results.allocate(vol_dict, nuc_list, burn_list, full_burn_list) + results.allocate(vol_dict, nuc_list, burn_list, full_burn_list, name_list) n_mat = len(burn_list) diff --git a/tests/dummy_operator.py b/tests/dummy_operator.py index 1bb00129d..9595765d7 100644 --- a/tests/dummy_operator.py +++ b/tests/dummy_operator.py @@ -243,4 +243,4 @@ class DummyOperator(TransportOperator): Maps cell name to index in global geometry. """ - return self.volume, self.nuc_list, self.local_mats, self.burnable_mats + return self.volume, self.nuc_list, self.local_mats, self.burnable_mats, {"1": ""} diff --git a/tests/unit_tests/test_deplete_activation.py b/tests/unit_tests/test_deplete_activation.py index eace1976e..2f1713880 100644 --- a/tests/unit_tests/test_deplete_activation.py +++ b/tests/unit_tests/test_deplete_activation.py @@ -113,6 +113,11 @@ def test_activation(run_in_tmpdir, model, reaction_rate_mode, reaction_rate_opts assert atoms[0] == pytest.approx(n0) assert atoms[1] / atoms[0] == pytest.approx(0.5, rel=tolerance) + # Check that material name is preserved in depletion results + step_result = results[0] + mat_from_results = step_result.get_material(f"{w.id}") + assert mat_from_results.name == 'tungsten' + def test_decay(run_in_tmpdir): """Test decay-only timesteps where no transport solve is performed""" diff --git a/tests/unit_tests/test_deplete_decay.py b/tests/unit_tests/test_deplete_decay.py index 6e7b0b101..db96fcbe2 100644 --- a/tests/unit_tests/test_deplete_decay.py +++ b/tests/unit_tests/test_deplete_decay.py @@ -82,3 +82,8 @@ def test_deplete_decay_step_fissionable(run_in_tmpdir): _, u238 = results.get_atoms(f"{mat.id}", "U238") assert u238[1] == pytest.approx(original_atoms) + + # Check that material name is preserved in depletion results + step_result = results[0] + mat_from_results = step_result.get_material(f"{mat.id}") + assert mat_from_results.name == 'I do not decay.' diff --git a/tests/unit_tests/test_deplete_integrator.py b/tests/unit_tests/test_deplete_integrator.py index 6463eaa20..558cb434b 100644 --- a/tests/unit_tests/test_deplete_integrator.py +++ b/tests/unit_tests/test_deplete_integrator.py @@ -59,8 +59,9 @@ def test_results_save(run_in_tmpdir): burn_list = full_burn_list[2*comm.rank: 2*comm.rank + 2] nuc_list = ["na", "nb"] + name_list = {mat: "" for mat in full_burn_list} op.get_results_info.return_value = ( - vol_dict, nuc_list, burn_list, full_burn_list) + vol_dict, nuc_list, burn_list, full_burn_list, name_list) # Construct end-of-step concentrations x1 = [rng.random(2), rng.random(2)] @@ -130,7 +131,8 @@ def test_results_save_without_rates(run_in_tmpdir): vol_dict = {"0": 1.0} nuc_list = ["na"] burn_list = ["0"] - op.get_results_info.return_value = (vol_dict, nuc_list, burn_list, burn_list) + name_list = {mat: "" for mat in burn_list} + op.get_results_info.return_value = (vol_dict, nuc_list, burn_list, burn_list, name_list) x = [np.array([1.0])] rates = ReactionRates(burn_list, nuc_list, ["ra"]) From 008d58460779c896342d59265390c76bee762d49 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Wed, 28 Jan 2026 10:02:02 +0200 Subject: [PATCH 514/671] Warn users when setting undefined attributes in ```openmc.Settings``` (#3746) Co-authored-by: Paul Romano --- openmc/settings.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/openmc/settings.py b/openmc/settings.py index a022b045f..3cf662e31 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -4,13 +4,14 @@ import itertools from math import ceil from numbers import Integral, Real from pathlib import Path +import traceback import lxml.etree as ET import warnings import openmc import openmc.checkvalue as cv from openmc.checkvalue import PathLike -from openmc.stats.multivariate import MeshSpatial, Box, PolarAzimuthal, Isotropic +from openmc.stats.multivariate import MeshSpatial from ._xml import clean_indentation, get_elem_list, get_text from .mesh import _read_meshes, RegularMesh, MeshBase from .source import SourceBase, MeshSource, IndependentSource @@ -467,6 +468,16 @@ class Settings: for key, value in kwargs.items(): setattr(self, key, value) + def __setattr__(self, name: str, value): + if not name.startswith('_'): + try: + getattr(self, name) + except AttributeError as e: + msg, = traceback.format_exception_only(e) + msg = msg.strip().split(maxsplit=1)[-1] + warnings.warn(msg, stacklevel=2) + super().__setattr__(name, value) + @property def run_mode(self) -> str: return self._run_mode.value From f7a734189a79c329363495b0ef6aa730424443d7 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 29 Jan 2026 01:20:48 -0600 Subject: [PATCH 515/671] Optionally compute bounding boxes in Mesh.material_volumes (#3731) --- include/openmc/capi.h | 2 +- include/openmc/mesh.h | 38 ++++++- openmc/deplete/r2s.py | 45 ++++----- openmc/lib/mesh.py | 28 ++++- openmc/mesh.py | 82 ++++++++++++--- src/mesh.cpp | 185 +++++++++++++++++++++++++++++++--- tests/unit_tests/test_mesh.py | 92 +++++++++++++++++ 7 files changed, 413 insertions(+), 59 deletions(-) diff --git a/include/openmc/capi.h b/include/openmc/capi.h index d8041ef41..749dc98a0 100644 --- a/include/openmc/capi.h +++ b/include/openmc/capi.h @@ -116,7 +116,7 @@ int openmc_mesh_set_id(int32_t index, int32_t id); int openmc_mesh_get_n_elements(int32_t index, size_t* n); int openmc_mesh_get_volumes(int32_t index, double* volumes); int openmc_mesh_material_volumes(int32_t index, int nx, int ny, int nz, - int max_mats, int32_t* materials, double* volumes); + int max_mats, int32_t* materials, double* volumes, double* bboxes); int openmc_meshsurface_filter_get_mesh(int32_t index, int32_t* index_mesh); int openmc_meshsurface_filter_set_mesh(int32_t index, int32_t index_mesh); int openmc_new_filter(const char* type, int32_t* index); diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index 2936d3ab1..5dc327fe3 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -88,8 +88,12 @@ namespace detail { class MaterialVolumes { public: + MaterialVolumes(int32_t* mats, double* vols, double* bboxes, int table_size) + : materials_(mats), volumes_(vols), bboxes_(bboxes), table_size_(table_size) + {} + MaterialVolumes(int32_t* mats, double* vols, int table_size) - : materials_(mats), volumes_(vols), table_size_(table_size) + : MaterialVolumes(mats, vols, nullptr, table_size) {} //! Add volume for a given material in a mesh element @@ -97,8 +101,11 @@ public: //! \param[in] index_elem Index of the mesh element //! \param[in] index_material Index of the material within the model //! \param[in] volume Volume to add - void add_volume(int index_elem, int index_material, double volume); - void add_volume_unsafe(int index_elem, int index_material, double volume); + //! \param[in] bbox Bounding box to union into the result (optional) + void add_volume(int index_elem, int index_material, double volume, + const BoundingBox* bbox = nullptr); + void add_volume_unsafe(int index_elem, int index_material, double volume, + const BoundingBox* bbox = nullptr); // Accessors int32_t& materials(int i, int j) { return materials_[i * table_size_ + j]; } @@ -113,11 +120,23 @@ public: return volumes_[i * table_size_ + j]; } + double& bboxes(int i, int j, int k) + { + return bboxes_[(i * table_size_ + j) * 6 + k]; + } + const double& bboxes(int i, int j, int k) const + { + return bboxes_[(i * table_size_ + j) * 6 + k]; + } + + bool has_bboxes() const { return bboxes_ != nullptr; } + bool table_full() const { return table_full_; } private: int32_t* materials_; //!< material index (bins, table_size) double* volumes_; //!< volume in [cm^3] (bins, table_size) + double* bboxes_; //!< bounding boxes (bins, table_size, 6) int table_size_; //!< Size of hash table for each mesh element bool table_full_ {false}; //!< Whether the hash table is full }; @@ -240,6 +259,19 @@ public: void material_volumes(int nx, int ny, int nz, int max_materials, int32_t* materials, double* volumes) const; + //! Determine volume and bounding boxes of materials within each mesh element + // + //! \param[in] nx Number of samples in x direction + //! \param[in] ny Number of samples in y direction + //! \param[in] nz Number of samples in z direction + //! \param[in] max_materials Maximum number of materials in a single mesh + //! element + //! \param[inout] materials Array storing material indices + //! \param[inout] volumes Array storing volumes + //! \param[inout] bboxes Array storing bounding boxes (n_elems, table_size, 6) + void material_volumes(int nx, int ny, int nz, int max_materials, + int32_t* materials, double* volumes, double* bboxes) const; + //! Determine bounding box of mesh // //! \return Bounding box of mesh diff --git a/openmc/deplete/r2s.py b/openmc/deplete/r2s.py index 97277cbda..57bbe437f 100644 --- a/openmc/deplete/r2s.py +++ b/openmc/deplete/r2s.py @@ -269,6 +269,7 @@ class R2SManager: # Compute material volume fractions on the mesh if mat_vol_kwargs is None: mat_vol_kwargs = {} + mat_vol_kwargs.setdefault('bounding_boxes', True) self.results['mesh_material_volumes'] = mmv = comm.bcast( self.domains.material_volumes(self.neutron_model, **mat_vol_kwargs)) @@ -539,14 +540,15 @@ class R2SManager: def get_decay_photon_source_mesh( self, time_index: int = -1 - ) -> list[openmc.MeshSource]: + ) -> list[openmc.IndependentSource]: """Create decay photon source for a mesh-based calculation. - This function creates N :class:`MeshSource` objects where N is the - maximum number of unique materials that appears in a single mesh - element. For each mesh element-material combination, and - IndependentSource instance is created with a spatial constraint limited - the sampled decay photons to the correct region. + For each mesh element-material combination, an + :class:`~openmc.IndependentSource` is created with a + :class:`~openmc.stats.Box` spatial distribution based on the bounding + box of the material within the mesh element. A material constraint is + also applied so that sampled source sites are limited to the correct + region. When the photon transport model is different from the neutron model, the photon MeshMaterialVolumes is used to determine whether an (element, @@ -559,19 +561,15 @@ class R2SManager: Returns ------- - list of openmc.MeshSource - A list of MeshSource objects, each containing IndependentSource - instances for the decay photons in the corresponding mesh element. + list of openmc.IndependentSource + A list of IndependentSource objects for the decay photons, one for + each mesh element-material combination with non-zero source strength. """ mat_dict = self.neutron_model._get_all_materials() - # Some MeshSource objects will have empty positions; create a "null source" - # that is used for this case - null_source = openmc.IndependentSource(particle='photon', strength=0.0) - - # List to hold sources for each MeshSource (length = N) - source_lists = [] + # List to hold all sources + sources = [] # Index in the overall list of activated materials index_mat = 0 @@ -594,7 +592,7 @@ class R2SManager: if mat_id is not None } - for j, (mat_id, _) in enumerate(mat_vols.by_element(index_elem)): + for mat_id, _, bbox in mat_vols.by_element(index_elem, include_bboxes=True): # Skip void volume if mat_id is None: continue @@ -604,30 +602,27 @@ class R2SManager: index_mat += 1 continue - # Check whether a new MeshSource object is needed - if j >= len(source_lists): - source_lists.append([null_source]*n_elements) - # Get activated material composition original_mat = materials[index_mat] activated_mat = results[time_index].get_material(str(original_mat.id)) - # Create decay photon source source + # Create decay photon source energy = activated_mat.get_decay_photon_energy() if energy is not None: strength = energy.integral() - source_lists[j][index_elem] = openmc.IndependentSource( + space = openmc.stats.Box(*bbox) + sources.append(openmc.IndependentSource( + space=space, energy=energy, particle='photon', strength=strength, constraints={'domains': [mat_dict[mat_id]]} - ) + )) # Increment index of activated material index_mat += 1 - # Return list of mesh sources - return [openmc.MeshSource(self.domains, sources) for sources in source_lists] + return sources def load_results(self, path: PathLike): """Load results from a previous R2S calculation. diff --git a/openmc/lib/mesh.py b/openmc/lib/mesh.py index 3a720f98a..19e6f74d7 100644 --- a/openmc/lib/mesh.py +++ b/openmc/lib/mesh.py @@ -1,5 +1,5 @@ from collections.abc import Mapping, Sequence -from ctypes import (c_int, c_int32, c_char_p, c_double, POINTER, +from ctypes import (c_int, c_int32, c_char_p, c_double, POINTER, c_void_p, create_string_buffer, c_size_t) from math import sqrt import sys @@ -47,7 +47,8 @@ _dll.openmc_mesh_bounding_box.argtypes = [ _dll.openmc_mesh_bounding_box.restype = c_int _dll.openmc_mesh_bounding_box.errcheck = _error_handler _dll.openmc_mesh_material_volumes.argtypes = [ - c_int32, c_int, c_int, c_int, c_int, arr_2d_int32, arr_2d_double] + c_int32, c_int, c_int, c_int, c_int, arr_2d_int32, arr_2d_double, + c_void_p] _dll.openmc_mesh_material_volumes.restype = c_int _dll.openmc_mesh_material_volumes.errcheck = _error_handler _dll.openmc_mesh_get_plot_bins.argtypes = [ @@ -188,6 +189,7 @@ class Mesh(_FortranObjectWithID): n_samples: int | tuple[int, int, int] = 10_000, max_materials: int = 4, output: bool = True, + bounding_boxes: bool = False, ) -> MeshMaterialVolumes: """Determine volume of materials in each mesh element. @@ -213,6 +215,11 @@ class Mesh(_FortranObjectWithID): Estimated maximum number of materials in any given mesh element. output : bool, optional Whether or not to show output. + bounding_boxes : bool, optional + Whether or not to compute an axis-aligned bounding box for each + (mesh element, material) combination. When enabled, the bounding + box encloses the ray-estimator prisms used for the volume + estimation. Returns ------- @@ -243,23 +250,36 @@ class Mesh(_FortranObjectWithID): table_size = slot_factor*max_materials materials = np.full((n, table_size), EMPTY_SLOT, dtype=np.int32) volumes = np.zeros((n, table_size), dtype=np.float64) + bboxes = None + if bounding_boxes: + bboxes = np.empty((n, table_size, 6), dtype=np.float64) + bboxes[..., 0:3] = np.inf + bboxes[..., 3:6] = -np.inf # Run material volume calculation while True: try: + bboxes_ptr = None + if bboxes is not None: + bboxes_ptr = bboxes.ctypes.data_as(POINTER(c_double)) with quiet_dll(output): _dll.openmc_mesh_material_volumes( - self._index, nx, ny, nz, table_size, materials, volumes) + self._index, nx, ny, nz, table_size, materials, + volumes, bboxes_ptr) except AllocationError: # Increase size of result array and try again table_size *= 2 materials = np.full((n, table_size), EMPTY_SLOT, dtype=np.int32) volumes = np.zeros((n, table_size), dtype=np.float64) + if bounding_boxes: + bboxes = np.empty((n, table_size, 6), dtype=np.float64) + bboxes[..., 0:3] = np.inf + bboxes[..., 3:6] = -np.inf else: # If no error, break out of loop break - return MeshMaterialVolumes(materials, volumes) + return MeshMaterialVolumes(materials, volumes, bboxes) def get_plot_bins( self, diff --git a/openmc/mesh.py b/openmc/mesh.py index 12e879810..efe1c20a1 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -17,6 +17,7 @@ import openmc import openmc.checkvalue as cv from openmc.checkvalue import PathLike from openmc.utility_funcs import change_directory +from .bounding_box import BoundingBox from ._xml import get_elem_list, get_text from .mixin import IDManagerMixin from .surface import _BOUNDARY_TYPES @@ -40,6 +41,11 @@ class MeshMaterialVolumes(Mapping): Array of shape (elements, max_materials) storing material IDs volumes : numpy.ndarray Array of shape (elements, max_materials) storing material volumes + bboxes : numpy.ndarray, optional + Array of shape (elements, max_materials, 6) storing axis-aligned + bounding boxes for each (element, material) combination with ordering + (xmin, ymin, zmin, xmax, ymax, zmax). Bounding boxes enclose the + ray-estimator prisms used to compute volumes. See Also -------- @@ -64,9 +70,30 @@ class MeshMaterialVolumes(Mapping): [(2, 31.87963824195591), (1, 6.129949130817542)] """ - def __init__(self, materials: np.ndarray, volumes: np.ndarray): + def __init__( + self, + materials: np.ndarray, + volumes: np.ndarray, + bboxes: np.ndarray | None = None + ): self._materials = materials self._volumes = volumes + self._bboxes = bboxes + + if self._bboxes is not None: + if self._bboxes.shape[:2] != self._materials.shape: + raise ValueError( + 'bboxes must have shape (elements, max_materials, 6) ' + 'matching materials/volumes.' + ) + if self._bboxes.shape[2] != 6: + raise ValueError( + 'bboxes must have shape (elements, max_materials, 6).' + ) + + @property + def has_bounding_boxes(self) -> bool: + return self._bboxes is not None @property def num_elements(self) -> int: @@ -92,7 +119,11 @@ class MeshMaterialVolumes(Mapping): volumes[indices] = self._volumes[indices, i] return volumes - def by_element(self, index_elem: int) -> list[tuple[int | None, float]]: + def by_element( + self, + index_elem: int, + include_bboxes: bool = False + ) -> list[tuple[int | None, float] | tuple[int | None, float, BoundingBox | None]]: """Get a list of volumes for each material within a specific element. Parameters @@ -102,15 +133,32 @@ class MeshMaterialVolumes(Mapping): Returns ------- - list of tuple of (material ID, volume) + list of tuple + If ``include_bboxes`` is False (default), returns tuples of + (material ID, volume). If ``include_bboxes`` is True, returns + tuples of (material ID, volume, bounding box). """ table_size = self._volumes.shape[1] - return [ - (m if m > -1 else None, self._volumes[index_elem, i]) - for i in range(table_size) - if (m := self._materials[index_elem, i]) != -2 - ] + if include_bboxes and self._bboxes is None: + raise ValueError('Bounding boxes were not computed for this object.') + + results = [] + for i in range(table_size): + m = self._materials[index_elem, i] + if m == -2: + continue + mat_id = m if m > -1 else None + vol = self._volumes[index_elem, i] + + if include_bboxes: + vals = self._bboxes[index_elem, i] + bbox = BoundingBox(vals[0:3], vals[3:6]) + results.append((mat_id, vol, bbox)) + else: + results.append((mat_id, vol)) + + return results def save(self, filename: PathLike): """Save material volumes to a .npz file. @@ -120,8 +168,10 @@ class MeshMaterialVolumes(Mapping): filename : path-like Filename where data will be saved """ - np.savez_compressed( - filename, materials=self._materials, volumes=self._volumes) + kwargs = {'materials': self._materials, 'volumes': self._volumes} + if self._bboxes is not None: + kwargs['bboxes'] = self._bboxes + np.savez_compressed(filename, **kwargs) @classmethod def from_npz(cls, filename: PathLike) -> MeshMaterialVolumes: @@ -134,7 +184,8 @@ class MeshMaterialVolumes(Mapping): """ filedata = np.load(filename) - return cls(filedata['materials'], filedata['volumes']) + bboxes = filedata['bboxes'] if 'bboxes' in filedata.files else None + return cls(filedata['materials'], filedata['volumes'], bboxes) class MeshBase(IDManagerMixin, ABC): @@ -387,6 +438,7 @@ class MeshBase(IDManagerMixin, ABC): model: openmc.Model, n_samples: int | tuple[int, int, int] = 10_000, max_materials: int = 4, + bounding_boxes: bool = False, **kwargs ) -> MeshMaterialVolumes: """Determine volume of materials in each mesh element. @@ -409,6 +461,11 @@ class MeshBase(IDManagerMixin, ABC): the x, y, and z dimensions. max_materials : int, optional Estimated maximum number of materials in any given mesh element. + bounding_boxes : bool, optional + Whether to compute an axis-aligned bounding box for each + (mesh element, material) combination. When enabled, the bounding + box encloses the ray-estimator prisms used for the volume + estimation. **kwargs : dict Keyword arguments passed to :func:`openmc.lib.init` @@ -440,7 +497,8 @@ class MeshBase(IDManagerMixin, ABC): # Compute material volumes volumes = mesh.material_volumes( - n_samples, max_materials, output=kwargs['output']) + n_samples, max_materials, output=kwargs['output'], + bounding_boxes=bounding_boxes) # Restore original tallies model.tallies = original_tallies diff --git a/src/mesh.cpp b/src/mesh.cpp index 6ee15b9c1..0b45774ff 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -1,6 +1,8 @@ #include "openmc/mesh.h" #include // for copy, equal, min, min_element #include +#include // for uint64_t +#include // for memcpy #define _USE_MATH_DEFINES // to make M_PI declared in Intel and MSVC compilers #include // for ceil #include // for size_t @@ -140,6 +142,63 @@ inline bool atomic_cas_int32(int32_t* ptr, int32_t& expected, int32_t desired) #endif } +// Helper function equivalent to std::bit_cast in C++20 +template +inline To bit_cast_value(const From& value) +{ + To out; + std::memcpy(&out, &value, sizeof(To)); + return out; +} + +inline void atomic_update_double(double* ptr, double value, bool is_min) +{ +#if defined(__GNUC__) || defined(__clang__) + using may_alias_uint64_t [[gnu::may_alias]] = uint64_t; + auto* bits_ptr = reinterpret_cast(ptr); + uint64_t current_bits = __atomic_load_n(bits_ptr, __ATOMIC_SEQ_CST); + double current = bit_cast_value(current_bits); + while (is_min ? (value < current) : (value > current)) { + uint64_t desired_bits = bit_cast_value(value); + uint64_t expected_bits = current_bits; + if (__atomic_compare_exchange_n(bits_ptr, &expected_bits, desired_bits, + false, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST)) { + return; + } + current_bits = expected_bits; + current = bit_cast_value(current_bits); + } + +#elif defined(_MSC_VER) + auto* bits_ptr = reinterpret_cast(ptr); + long long current_bits = *bits_ptr; + double current = bit_cast_value(current_bits); + while (is_min ? (value < current) : (value > current)) { + long long desired_bits = bit_cast_value(value); + long long old_bits = + _InterlockedCompareExchange64(bits_ptr, desired_bits, current_bits); + if (old_bits == current_bits) { + return; + } + current_bits = old_bits; + current = bit_cast_value(current_bits); + } + +#else +#error "No compare-and-swap implementation available for this compiler." +#endif +} + +inline void atomic_max_double(double* ptr, double value) +{ + atomic_update_double(ptr, value, false); +} + +inline void atomic_min_double(double* ptr, double value) +{ + atomic_update_double(ptr, value, true); +} + namespace detail { //============================================================================== @@ -147,7 +206,7 @@ namespace detail { //============================================================================== void MaterialVolumes::add_volume( - int index_elem, int index_material, double volume) + int index_elem, int index_material, double volume, const BoundingBox* bbox) { // This method handles adding elements to the materials hash table, // implementing open addressing with linear probing. Consistency across @@ -166,10 +225,18 @@ void MaterialVolumes::add_volume( // Non-atomic read of current material int32_t current_val = *slot_ptr; - // Found the desired material; accumulate volume + // Found the desired material; accumulate volume and bbox if (current_val == index_material) { #pragma omp atomic this->volumes(index_elem, slot) += volume; + if (bbox) { + atomic_min_double(&this->bboxes(index_elem, slot, 0), bbox->min.x); + atomic_min_double(&this->bboxes(index_elem, slot, 1), bbox->min.y); + atomic_min_double(&this->bboxes(index_elem, slot, 2), bbox->min.z); + atomic_max_double(&this->bboxes(index_elem, slot, 3), bbox->max.x); + atomic_max_double(&this->bboxes(index_elem, slot, 4), bbox->max.y); + atomic_max_double(&this->bboxes(index_elem, slot, 5), bbox->max.z); + } return; } @@ -185,6 +252,14 @@ void MaterialVolumes::add_volume( if (claimed_slot || (expected_val == index_material)) { #pragma omp atomic this->volumes(index_elem, slot) += volume; + if (bbox) { + atomic_min_double(&this->bboxes(index_elem, slot, 0), bbox->min.x); + atomic_min_double(&this->bboxes(index_elem, slot, 1), bbox->min.y); + atomic_min_double(&this->bboxes(index_elem, slot, 2), bbox->min.z); + atomic_max_double(&this->bboxes(index_elem, slot, 3), bbox->max.x); + atomic_max_double(&this->bboxes(index_elem, slot, 4), bbox->max.y); + atomic_max_double(&this->bboxes(index_elem, slot, 5), bbox->max.z); + } return; } } @@ -195,7 +270,7 @@ void MaterialVolumes::add_volume( } void MaterialVolumes::add_volume_unsafe( - int index_elem, int index_material, double volume) + int index_elem, int index_material, double volume, const BoundingBox* bbox) { // Linear probe for (int attempt = 0; attempt < table_size_; ++attempt) { @@ -207,9 +282,23 @@ void MaterialVolumes::add_volume_unsafe( // Read current material int32_t current_val = this->materials(index_elem, slot); - // Found the desired material; accumulate volume + // Found the desired material; accumulate volume and bbox if (current_val == index_material) { this->volumes(index_elem, slot) += volume; + if (bbox) { + this->bboxes(index_elem, slot, 0) = + std::min(this->bboxes(index_elem, slot, 0), bbox->min.x); + this->bboxes(index_elem, slot, 1) = + std::min(this->bboxes(index_elem, slot, 1), bbox->min.y); + this->bboxes(index_elem, slot, 2) = + std::min(this->bboxes(index_elem, slot, 2), bbox->min.z); + this->bboxes(index_elem, slot, 3) = + std::max(this->bboxes(index_elem, slot, 3), bbox->max.x); + this->bboxes(index_elem, slot, 4) = + std::max(this->bboxes(index_elem, slot, 4), bbox->max.y); + this->bboxes(index_elem, slot, 5) = + std::max(this->bboxes(index_elem, slot, 5), bbox->max.z); + } return; } @@ -217,6 +306,20 @@ void MaterialVolumes::add_volume_unsafe( if (current_val == EMPTY) { this->materials(index_elem, slot) = index_material; this->volumes(index_elem, slot) += volume; + if (bbox) { + this->bboxes(index_elem, slot, 0) = + std::min(this->bboxes(index_elem, slot, 0), bbox->min.x); + this->bboxes(index_elem, slot, 1) = + std::min(this->bboxes(index_elem, slot, 1), bbox->min.y); + this->bboxes(index_elem, slot, 2) = + std::min(this->bboxes(index_elem, slot, 2), bbox->min.z); + this->bboxes(index_elem, slot, 3) = + std::max(this->bboxes(index_elem, slot, 3), bbox->max.x); + this->bboxes(index_elem, slot, 4) = + std::max(this->bboxes(index_elem, slot, 4), bbox->max.y); + this->bboxes(index_elem, slot, 5) = + std::max(this->bboxes(index_elem, slot, 5), bbox->max.z); + } return; } } @@ -333,6 +436,12 @@ vector Mesh::volumes() const void Mesh::material_volumes(int nx, int ny, int nz, int table_size, int32_t* materials, double* volumes) const +{ + this->material_volumes(nx, ny, nz, table_size, materials, volumes, nullptr); +} + +void Mesh::material_volumes(int nx, int ny, int nz, int table_size, + int32_t* materials, double* volumes, double* bboxes) const { if (mpi::master) { header("MESH MATERIAL VOLUMES CALCULATION", 7); @@ -351,7 +460,8 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, timer.start(); // Create object for keeping track of materials/volumes - detail::MaterialVolumes result(materials, volumes, table_size); + detail::MaterialVolumes result(materials, volumes, bboxes, table_size); + bool compute_bboxes = bboxes != nullptr; // Determine bounding box auto bbox = this->bounding_box(); @@ -370,8 +480,8 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, #pragma omp parallel { // Preallocate vector for mesh indices and length fractions and particle - std::vector bins; - std::vector length_fractions; + vector bins; + vector length_fractions; Particle p; SourceSite site; @@ -453,12 +563,36 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, if (i_material != C_NONE) { i_material = model::materials[i_material]->id(); } + double cumulative_frac = 0.0; for (int i_bin = 0; i_bin < bins.size(); i_bin++) { int mesh_index = bins[i_bin]; double length = distance * length_fractions[i_bin]; + double volume = length * d1 * d2; - // Add volume to result - result.add_volume(mesh_index, i_material, length * d1 * d2); + if (compute_bboxes) { + double axis_start = r0[axis] + distance * cumulative_frac; + double axis_end = axis_start + length; + cumulative_frac += length_fractions[i_bin]; + + Position contrib_min = site.r; + Position contrib_max = site.r; + + contrib_min[ax1] = site.r[ax1] - 0.5 * d1; + contrib_max[ax1] = site.r[ax1] + 0.5 * d1; + contrib_min[ax2] = site.r[ax2] - 0.5 * d2; + contrib_max[ax2] = site.r[ax2] + 0.5 * d2; + contrib_min[axis] = std::min(axis_start, axis_end); + contrib_max[axis] = std::max(axis_start, axis_end); + + BoundingBox contrib_bbox {contrib_min, contrib_max}; + contrib_bbox &= bbox; + + result.add_volume( + mesh_index, i_material, volume, &contrib_bbox); + } else { + // Add volume to result + result.add_volume(mesh_index, i_material, volume); + } } if (distance == max_distance) @@ -505,10 +639,15 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, // Combine results from multiple MPI processes if (mpi::n_procs > 1) { int total = this->n_bins() * table_size; + int total_bbox = total * 6; if (mpi::master) { // Allocate temporary buffer for receiving data - std::vector mats(total); - std::vector vols(total); + vector mats(total); + vector vols(total); + vector recv_bboxes; + if (compute_bboxes) { + recv_bboxes.resize(total_bbox); + } for (int i = 1; i < mpi::n_procs; ++i) { // Receive material indices and volumes from process i @@ -516,6 +655,10 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, MPI_STATUS_IGNORE); MPI_Recv(vols.data(), total, MPI_DOUBLE, i, i, mpi::intracomm, MPI_STATUS_IGNORE); + if (compute_bboxes) { + MPI_Recv(recv_bboxes.data(), total_bbox, MPI_DOUBLE, i, i, + mpi::intracomm, MPI_STATUS_IGNORE); + } // Combine with existing results; we can call thread unsafe version of // add_volume because each thread is operating on a different element @@ -524,7 +667,18 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, for (int k = 0; k < table_size; ++k) { int index = index_elem * table_size + k; if (mats[index] != EMPTY) { - result.add_volume_unsafe(index_elem, mats[index], vols[index]); + if (compute_bboxes) { + int bbox_index = index * 6; + BoundingBox slot_bbox { + {recv_bboxes[bbox_index + 0], recv_bboxes[bbox_index + 1], + recv_bboxes[bbox_index + 2]}, + {recv_bboxes[bbox_index + 3], recv_bboxes[bbox_index + 4], + recv_bboxes[bbox_index + 5]}}; + result.add_volume_unsafe( + index_elem, mats[index], vols[index], &slot_bbox); + } else { + result.add_volume_unsafe(index_elem, mats[index], vols[index]); + } } } } @@ -533,6 +687,9 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, // Send material indices and volumes to process 0 MPI_Send(materials, total, MPI_INT32_T, 0, mpi::rank, mpi::intracomm); MPI_Send(volumes, total, MPI_DOUBLE, 0, mpi::rank, mpi::intracomm); + if (compute_bboxes) { + MPI_Send(bboxes, total_bbox, MPI_DOUBLE, 0, mpi::rank, mpi::intracomm); + } } } @@ -2428,14 +2585,14 @@ extern "C" int openmc_mesh_bounding_box(int32_t index, double* ll, double* ur) } extern "C" int openmc_mesh_material_volumes(int32_t index, int nx, int ny, - int nz, int table_size, int32_t* materials, double* volumes) + int nz, int table_size, int32_t* materials, double* volumes, double* bboxes) { if (int err = check_mesh(index)) return err; try { model::meshes[index]->material_volumes( - nx, ny, nz, table_size, materials, volumes); + nx, ny, nz, table_size, materials, volumes, bboxes); } catch (const std::exception& e) { set_errmsg(e.what()); if (starts_with(e.what(), "Mesh")) { diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index 9aca8b596..c5855a7b0 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -1,6 +1,8 @@ from math import pi from tempfile import TemporaryDirectory from pathlib import Path +import itertools +import random import h5py import numpy as np @@ -691,6 +693,49 @@ def test_mesh_material_volumes_serialize(): assert new_volumes.by_element(3) == [(2, 1.0)] +def test_mesh_material_volumes_serialize_with_bboxes(): + materials = np.array([ + [1, -1, -2], + [-1, -2, -2], + [2, 1, -2], + [2, -2, -2] + ]) + volumes = np.array([ + [0.5, 0.5, 0.0], + [1.0, 0.0, 0.0], + [0.5, 0.5, 0.0], + [1.0, 0.0, 0.0] + ]) + + # (xmin, ymin, zmin, xmax, ymax, zmax) + bboxes = np.empty((4, 3, 6)) + bboxes[..., 0:3] = np.inf + bboxes[..., 3:6] = -np.inf + bboxes[0, 0] = [-1.0, -2.0, -3.0, 1.0, 2.0, 3.0] # material 1 + bboxes[0, 1] = [-5.0, -6.0, -7.0, 5.0, 6.0, 7.0] # void + bboxes[1, 0] = [0.0, 0.0, 0.0, 10.0, 1.0, 2.0] # void + bboxes[2, 0] = [-1.0, -1.0, -1.0, 0.0, 0.0, 0.0] # material 2 + bboxes[2, 1] = [0.0, 0.0, 0.0, 1.0, 1.0, 1.0] # material 1 + bboxes[3, 0] = [-2.0, -2.0, -2.0, 2.0, 2.0, 2.0] # material 2 + + mmv = openmc.MeshMaterialVolumes(materials, volumes, bboxes) + with TemporaryDirectory() as tmpdir: + path = f'{tmpdir}/volumes_bboxes.npz' + mmv.save(path) + loaded = openmc.MeshMaterialVolumes.from_npz(path) + + assert loaded.has_bounding_boxes + first = loaded.by_element(0, include_bboxes=True)[0][2] + assert isinstance(first, openmc.BoundingBox) + np.testing.assert_array_equal(first.lower_left, (-1.0, -2.0, -3.0)) + np.testing.assert_array_equal(first.upper_right, (1.0, 2.0, 3.0)) + + second = loaded.by_element(0, include_bboxes=True)[1][2] + assert isinstance(second, openmc.BoundingBox) + np.testing.assert_array_equal(second.lower_left, (-5.0, -6.0, -7.0)) + np.testing.assert_array_equal(second.upper_right, (5.0, 6.0, 7.0)) + + def test_mesh_material_volumes_boundary_conditions(sphere_model): """Test the material volumes method using a regular mesh that overlaps with a vacuum boundary condition.""" @@ -718,6 +763,53 @@ def test_mesh_material_volumes_boundary_conditions(sphere_model): assert evaluated[1] == pytest.approx(expected[1], rel=1e-2) +def test_mesh_material_volumes_bounding_boxes(): + # Create a model with 8 spherical cells at known locations with random radii + box = openmc.model.RectangularParallelepiped( + -10, 10, -10, 10, -10, 10, boundary_type='vacuum') + + mat = openmc.Material() + mat.add_nuclide('H1', 1.0) + + sph_cells = [] + for x, y, z in itertools.product((-5., 5.), repeat=3): + mat_i = mat.clone() + sph = openmc.Sphere(x, y, z, r=random.uniform(0.5, 1.5)) + sph_cells.append(openmc.Cell(region=-sph, fill=mat_i)) + background = openmc.Cell(region=-box & openmc.Intersection([~c.region for c in sph_cells])) + + model = openmc.Model() + model.geometry = openmc.Geometry(sph_cells + [background]) + model.settings.particles = 1000 + model.settings.batches = 10 + + # Create a one-element mesh that encompasses the entire geometry + mesh = openmc.RegularMesh() + mesh.lower_left = (-10., -10., -10.) + mesh.upper_right = (10., 10., 10.) + mesh.dimension = (1, 1, 1) + + # Run material volume calculation with bounding boxes + n_samples = (400, 400, 400) + mmv = mesh.material_volumes(model, n_samples, max_materials=10, bounding_boxes=True) + assert mmv.has_bounding_boxes + + # Create a mapping of material ID to bounding box + bbox_by_mat = { + mat_id: bbox + for mat_id, vol, bbox in mmv.by_element(0, include_bboxes=True) + if mat_id is not None and vol > 0.0 + } + + # Match the mesh ray spacing used for the bounding box estimator. + tol = 0.5 * mesh.bounding_box.width[0] / n_samples[0] + for cell in sph_cells: + bbox = bbox_by_mat[cell.fill.id] + cell_bbox = cell.bounding_box + np.testing.assert_allclose(bbox.lower_left, cell_bbox.lower_left, atol=tol) + np.testing.assert_allclose(bbox.upper_right, cell_bbox.upper_right, atol=tol) + + def test_raytrace_mesh_infinite_loop(run_in_tmpdir): # Create a model with one large spherical cell sphere = openmc.Sphere(r=100, boundary_type='vacuum') From 7b4617affb5d75974aef737b749b4ad046be026b Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Fri, 30 Jan 2026 08:10:16 +0200 Subject: [PATCH 516/671] Fix for plotting model with multi-group cross sections (#3748) Co-authored-by: Paul Romano --- openmc/model/model.py | 10 ++++++++ tests/unit_tests/test_universe.py | 39 +++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/openmc/model/model.py b/openmc/model/model.py index 90b43f9f7..67da2f73a 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1160,6 +1160,16 @@ class Model: y_min = (origin[y] - 0.5*width[1]) * axis_scaling_factor[axis_units] y_max = (origin[y] + 0.5*width[1]) * axis_scaling_factor[axis_units] + # Determine whether any materials contains macroscopic data and if so, + # set energy mode accordingly and check that mg cross sections path is accessible + for mat in self.geometry.get_all_materials().values(): + if mat._macroscopic is not None: + self.settings.energy_mode = 'multi-group' + if 'mg_cross_sections' not in openmc.config: + raise RuntimeError("'mg_cross_sections' path must be set in " + "openmc.config before plotting.") + break + # Get ID map from the C API id_map = self.id_map( origin=origin, diff --git a/tests/unit_tests/test_universe.py b/tests/unit_tests/test_universe.py index efe8552a6..6fbf1cc38 100644 --- a/tests/unit_tests/test_universe.py +++ b/tests/unit_tests/test_universe.py @@ -1,3 +1,5 @@ +from pathlib import Path + import lxml.etree as ET import numpy as np import openmc @@ -104,6 +106,43 @@ def test_plot(run_in_tmpdir, sphere_model): plt.close('all') +def test_mg_plot(run_in_tmpdir): + # Create a simple universe with macroscopic data + h2o_data = openmc.Macroscopic('LWTR') + water = openmc.Material(name='Water') + water.set_density('macro', 1.0) + water.add_macroscopic(h2o_data) + sph = openmc.Sphere(r=10, boundary_type="vacuum") + cell = openmc.Cell(region=-sph, fill=water) + univ = openmc.Universe(cells=[cell]) + + # Create MGXS library and export to HDF5 + groups = openmc.mgxs.EnergyGroups([1e-5, 20.0e6]) + h2o_xsdata = openmc.XSdata('LWTR', groups) + h2o_xsdata.order = 0 + h2o_xsdata.set_total([1.0]) + h2o_xsdata.set_absorption([0.5]) + scatter_matrix = np.array([[[0.5]]]) + scatter_matrix = np.rollaxis(scatter_matrix, 0, 3) + h2o_xsdata.set_scatter_matrix(scatter_matrix) + mg_library = openmc.MGXSLibrary(groups) + mg_library.add_xsdatas([h2o_xsdata]) + mgxs_path = Path.cwd() / 'mgxs.h5' + mg_library.export_to_hdf5(mgxs_path) + + # Set MG cross sections in config and plot + with openmc.config.patch('mg_cross_sections', mgxs_path): + univ.plot(width=(200, 200), basis='yz', color_by='cell') + univ.plot(width=(200, 200), basis='yz', color_by='material') + + with pytest.raises(RuntimeError): + univ.plot(width=(200, 200), basis='yz', color_by='cell') + + # Close plots to avoid warning + import matplotlib.pyplot as plt + plt.close('all') + + def test_get_nuclides(uo2): c = openmc.Cell(fill=uo2) univ = openmc.Universe(cells=[c]) From 6041ee6ae3a721a566382a5923d1576d5d8fdba9 Mon Sep 17 00:00:00 2001 From: David Andrs Date: Sat, 31 Jan 2026 16:02:39 -0700 Subject: [PATCH 517/671] `SpatialBox` can be constructed via ctor with parameters (#3760) --- include/openmc/distribution_spatial.h | 1 + src/distribution_spatial.cpp | 5 +++++ tests/cpp_unit_tests/test_distribution.cpp | 13 +++++++++++++ 3 files changed, 19 insertions(+) diff --git a/include/openmc/distribution_spatial.h b/include/openmc/distribution_spatial.h index 26f106fca..3e31b17a9 100644 --- a/include/openmc/distribution_spatial.h +++ b/include/openmc/distribution_spatial.h @@ -167,6 +167,7 @@ private: class SpatialBox : public SpatialDistribution { public: explicit SpatialBox(pugi::xml_node node, bool fission = false); + SpatialBox(Position lower_left, Position upper_right, bool fission = false); //! Sample a position from the distribution //! \param seed Pseudorandom number seed pointer diff --git a/src/distribution_spatial.cpp b/src/distribution_spatial.cpp index 80f8f7de1..e25e08d74 100644 --- a/src/distribution_spatial.cpp +++ b/src/distribution_spatial.cpp @@ -426,6 +426,11 @@ SpatialBox::SpatialBox(pugi::xml_node node, bool fission) upper_right_ = Position {params[3], params[4], params[5]}; } +SpatialBox::SpatialBox(Position lower_left, Position upper_right, bool fission) + : lower_left_(lower_left), upper_right_(upper_right), + only_fissionable_(fission) +{} + std::pair SpatialBox::sample(uint64_t* seed) const { Position xi {prn(seed), prn(seed), prn(seed)}; diff --git a/tests/cpp_unit_tests/test_distribution.cpp b/tests/cpp_unit_tests/test_distribution.cpp index 2024b812e..f7c480c8d 100644 --- a/tests/cpp_unit_tests/test_distribution.cpp +++ b/tests/cpp_unit_tests/test_distribution.cpp @@ -1,4 +1,6 @@ #include "openmc/distribution.h" +#include "openmc/distribution_spatial.h" +#include "openmc/position.h" #include "openmc/random_lcg.h" #include #include @@ -79,3 +81,14 @@ TEST_CASE("Test alias sampling method for pugixml constructor") REQUIRE(dist.alias()[i] == correct_alias[i]); } } + +TEST_CASE("Test construction of SpatialBox with parameters") +{ + openmc::Position ll {-1, -2, -3}; + openmc::Position ur {30, 15, 5}; + openmc::SpatialBox box(ll, ur); + + REQUIRE(box.lower_left() == openmc::Position {-1, -2, -3}); + REQUIRE(box.upper_right() == openmc::Position {30, 15, 5}); + REQUIRE_FALSE(box.only_fissionable()); +} From fc0d9eec651dcbce5a1c6f094f859d872086ac31 Mon Sep 17 00:00:00 2001 From: Ethan Peterson Date: Mon, 2 Feb 2026 10:55:45 -0500 Subject: [PATCH 518/671] Ignore all source build directories that match build* pattern (#3762) --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3b2a24a4d..780059f30 100644 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,7 @@ docs/source/_images/*.aux docs/source/pythonapi/generated/ # Source build -build +build*/ # build from src/utils/setup.py src/utils/build From b41e22f68b86bb869d66f3e7340e52834ecfa0e8 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 3 Feb 2026 01:23:24 -0600 Subject: [PATCH 519/671] Refactor ParticleType to use PDG Monte Carlo numbering scheme (#3756) Co-authored-by: GuySten <62616591+GuySten@users.noreply.github.com> Co-authored-by: Amanda Lund --- CMakeLists.txt | 1 + docs/source/io_formats/collision_track.rst | 8 +- docs/source/io_formats/particle_restart.rst | 5 +- docs/source/io_formats/settings.rst | 11 +- docs/source/io_formats/source.rst | 6 +- docs/source/io_formats/statepoint.rst | 6 +- docs/source/io_formats/tallies.rst | 4 +- docs/source/io_formats/track.rst | 7 +- docs/source/usersguide/settings.rst | 4 +- examples/custom_source/source_ring.cpp | 18 +- .../parameterized_source_ring.cpp | 92 +++--- include/openmc/capi.h | 6 +- include/openmc/constants.h | 8 +- include/openmc/nuclide.h | 2 +- include/openmc/particle.h | 4 - include/openmc/particle_data.h | 6 +- include/openmc/particle_type.h | 172 ++++++++++ include/openmc/reaction_product.h | 2 +- include/openmc/source.h | 12 +- include/openmc/tallies/filter_particle.h | 2 +- include/openmc/weight_windows.h | 9 +- openmc/filter.py | 19 +- openmc/lib/core.py | 2 +- openmc/lib/filter.py | 13 +- openmc/lib/weight_windows.py | 29 +- openmc/particle_restart.py | 7 +- openmc/particle_type.py | 228 ++++++++++++++ openmc/source.py | 125 ++------ openmc/tracks.py | 13 +- openmc/weight_windows.py | 41 +-- src/bremsstrahlung.cpp | 6 +- src/material.cpp | 6 +- src/mcpl_interface.cpp | 76 +---- src/mesh.cpp | 2 +- src/mgxs_interface.cpp | 2 +- src/nuclide.cpp | 6 +- src/output.cpp | 12 +- src/particle.cpp | 69 ++-- src/particle_restart.cpp | 13 +- src/particle_type.cpp | 246 +++++++++++++++ src/photon.cpp | 9 +- src/physics.cpp | 49 +-- src/physics_mg.cpp | 4 +- src/reaction.cpp | 5 +- src/reaction_product.cpp | 6 +- src/simulation.cpp | 14 +- src/source.cpp | 45 ++- src/state_point.cpp | 18 ++ src/tallies/filter_particle.cpp | 10 +- src/tallies/tally.cpp | 4 +- src/tallies/tally_scoring.cpp | 77 ++--- src/track_output.cpp | 2 +- src/weight_windows.cpp | 43 +-- tests/cpp_unit_tests/test_mcpl_stat_sum.cpp | 4 +- .../source_dlopen/source_sampling.cpp | 2 +- .../parameterized_source_sampling.cpp | 51 +-- .../surface_source/surface_source_true.h5 | Bin 88096 -> 86216 bytes .../surface_source_write/test.py | 2 +- tests/unit_tests/test_particle_type.py | 298 ++++++++++++++++++ tests/unit_tests/test_source_file.py | 4 +- tests/unit_tests/test_tracks.py | 6 +- .../weightwindows/test_wwinp_reader.py | 6 +- 62 files changed, 1401 insertions(+), 558 deletions(-) create mode 100644 include/openmc/particle_type.h create mode 100644 openmc/particle_type.py create mode 100644 src/particle_type.cpp create mode 100644 tests/unit_tests/test_particle_type.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 87b8789d1..d3119fb87 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -372,6 +372,7 @@ list(APPEND libopenmc_SOURCES src/particle.cpp src/particle_data.cpp src/particle_restart.cpp + src/particle_type.cpp src/photon.cpp src/physics.cpp src/physics_common.cpp diff --git a/docs/source/io_formats/collision_track.rst b/docs/source/io_formats/collision_track.rst index a36459754..8e1e00ffb 100644 --- a/docs/source/io_formats/collision_track.rst +++ b/docs/source/io_formats/collision_track.rst @@ -10,7 +10,7 @@ may also be written after each batch when multiple files are requested (``collision_track.N.h5``) or when the run is performed in parallel. The file contains the information needed to reconstruct each recorded collision. -The current revision of the collision track file format is 1.0. +The current revision of the collision track file format is 1.1. **/** @@ -37,9 +37,9 @@ The current revision of the collision track file format is 1.0. - ``material_id`` (*int*) -- ID of the material containing the collision site. - ``universe_id`` (*int*) -- ID of the universe containing the collision site. - ``n_collision`` (*int*) -- Collision counter for the particle history. - - ``particle`` (*int*) -- Particle type (0=neutron, 1=photon, 2=electron, 3=positron). - - ``parent_id`` (*int64*) -- Unique ID of the parent particle. - - ``progeny_id`` (*int64*) -- Progeny ID of the particle. + - ``particle`` (*int32_t*) -- Particle type (PDG number). + - ``parent_id`` (*int64_t*) -- Unique ID of the parent particle. + - ``progeny_id`` (*int64_t*) -- Progeny ID of the particle. In an MPI run, OpenMC writes the combined dataset by gathering collision-track entries from all ranks before flushing them to disk, so the final file appears diff --git a/docs/source/io_formats/particle_restart.rst b/docs/source/io_formats/particle_restart.rst index 2734f0470..e0fe76b9d 100644 --- a/docs/source/io_formats/particle_restart.rst +++ b/docs/source/io_formats/particle_restart.rst @@ -4,7 +4,7 @@ Particle Restart File Format ============================ -The current version of the particle restart file format is 2.0. +The current version of the particle restart file format is 2.1. **/** @@ -26,8 +26,7 @@ The current version of the particle restart file format is 2.0. - **run_mode** (*char[]*) -- Run mode used, either 'fixed source', 'eigenvalue', or 'particle restart'. - **id** (*int8_t*) -- Unique identifier of the particle. - - **type** (*int*) -- Particle type (0=neutron, 1=photon, 2=electron, - 3=positron) + - **type** (*int32_t*) -- Particle type (PDG number) - **weight** (*double*) -- Weight of the particle. - **energy** (*double*) -- Energy of the particle in eV for continuous-energy mode, or the energy group of the particle for diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 8fc26ae07..ed7c6273a 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -721,7 +721,10 @@ attributes/sub-elements: is present. :particle: - The source particle type, either ``neutron`` or ``photon``. + The source particle type, specified as a PDG number or a string alias (e.g., + ``neutron``/``n``, ``photon``/``gamma``, ``electron``, ``positron``, + ``proton``/``p``, ``deuteron``/``d``, ``triton``/``t``, ``alpha``, or GNDS + nuclide names like ``Fe57``). *Default*: neutron @@ -1537,7 +1540,8 @@ sub-elements/attributes: *Default*: None :particle_type: - The particle that the weight windows will apply to (e.g., 'neutron') + The particle that the weight windows will apply to, specified as a PDG + code or string (e.g., ``neutron``). *Default*: 'neutron' @@ -1597,7 +1601,8 @@ mesh-based weight windows. *Default*: None :particle_type: - The particle that the weight windows will apply to (e.g., 'neutron') + The particle that the weight windows will apply to, specified as a PDG + code or string (e.g., ``neutron``). *Default*: neutron diff --git a/docs/source/io_formats/source.rst b/docs/source/io_formats/source.rst index 4ce2229ed..2e81e0a3f 100644 --- a/docs/source/io_formats/source.rst +++ b/docs/source/io_formats/source.rst @@ -15,6 +15,8 @@ following the same format. **/** :Attributes: - **filetype** (*char[]*) -- String indicating the type of file. + - **version** (*int[2]*) -- Major and minor version of the source + file format. :Datasets: @@ -22,5 +24,5 @@ following the same format. particle. The compound type has fields ``r``, ``u``, ``E``, ``time``, ``wgt``, ``delayed_group``, ``surf_id`` and ``particle``, which represent the position, direction, energy, time, weight, - delayed group, surface ID, and particle type (0=neutron, 1=photon, - 2=electron, 3=positron), respectively. + delayed group, surface ID, and particle type (PDG number), + respectively. diff --git a/docs/source/io_formats/statepoint.rst b/docs/source/io_formats/statepoint.rst index 2309643dc..7d7765b84 100644 --- a/docs/source/io_formats/statepoint.rst +++ b/docs/source/io_formats/statepoint.rst @@ -4,7 +4,7 @@ State Point File Format ======================= -The current version of the statepoint file format is 18.1. +The current version of the statepoint file format is 18.2. **/** @@ -56,8 +56,8 @@ The current version of the statepoint file format is 18.1. ``time``, ``wgt``, ``delayed_group``, ``surf_id``, and ``particle``, which represent the position, direction, energy, time, weight, delayed group, surface ID, and particle type - (0=neutron, 1=photon, 2=electron, 3=positron), respectively. Only - present when `run_mode` is 'eigenvalue'. + (PDG number), respectively. Only present when `run_mode` is + 'eigenvalue'. **/tallies/** diff --git a/docs/source/io_formats/tallies.rst b/docs/source/io_formats/tallies.rst index 9f29a949a..0ba0e061f 100644 --- a/docs/source/io_formats/tallies.rst +++ b/docs/source/io_formats/tallies.rst @@ -318,8 +318,8 @@ should be set to: they use ``energy`` and ``y``. :particle: - A list of integers indicating the type of particles to tally ('neutron' = 1, - 'photon' = 2, 'electron' = 3, 'positron' = 4). + A list of particle identifiers to tally, specified as strings (e.g., + ``neutron``, ``photon``, ``He4``) or as integer PDG numbers. ------------------ ```` Element diff --git a/docs/source/io_formats/track.rst b/docs/source/io_formats/track.rst index a97d75e58..9c05ac4c3 100644 --- a/docs/source/io_formats/track.rst +++ b/docs/source/io_formats/track.rst @@ -4,7 +4,7 @@ Track File Format ================= -The current revision of the particle track file format is 3.0. +The current revision of the particle track file format is 3.1. **/** @@ -32,6 +32,5 @@ The current revision of the particle track file format is 3.0. the array for each primary/secondary particle. The last offset should match the total size of the array. - - **particles** (*int[]*) -- Particle type for each - primary/secondary particle (0=neutron, 1=photon, - 2=electron, 3=positron). + - **particles** (*int32_t[]*) -- Particle type for + each primary/secondary particle (PDG number). diff --git a/docs/source/usersguide/settings.rst b/docs/source/usersguide/settings.rst index b9c693ba1..5a04fedd7 100644 --- a/docs/source/usersguide/settings.rst +++ b/docs/source/usersguide/settings.rst @@ -400,7 +400,7 @@ below. { openmc::SourceSite particle; // weight - particle.particle = openmc::ParticleType::neutron; + particle.particle = openmc::ParticleType::neutron(); particle.wgt = 1.0; // position double angle = 2.0 * M_PI * openmc::prn(seed); @@ -477,7 +477,7 @@ parameters to the source class when it is created: { openmc::SourceSite particle; // weight - particle.particle = openmc::ParticleType::neutron; + particle.particle = openmc::ParticleType::neutron(); particle.wgt = 1.0; // position particle.r.x = 0.0; diff --git a/examples/custom_source/source_ring.cpp b/examples/custom_source/source_ring.cpp index b87afbd17..5ab531a4a 100644 --- a/examples/custom_source/source_ring.cpp +++ b/examples/custom_source/source_ring.cpp @@ -1,17 +1,16 @@ -#include // for M_PI +#include // for M_PI #include // for unique_ptr +#include "openmc/particle.h" #include "openmc/random_lcg.h" #include "openmc/source.h" -#include "openmc/particle.h" -class RingSource : public openmc::Source -{ +class RingSource : public openmc::Source { openmc::SourceSite sample(uint64_t* seed) const { openmc::SourceSite particle; // particle type - particle.particle = openmc::ParticleType::neutron; + particle.particle = openmc::ParticleType::neutron(); // position double angle = 2.0 * M_PI * openmc::prn(seed); double radius = 3.0; @@ -25,10 +24,11 @@ class RingSource : public openmc::Source } }; -// A function to create a unique pointer to an instance of this class when generated -// via a plugin call using dlopen/dlsym. -// You must have external C linkage here otherwise dlopen will not find the file -extern "C" std::unique_ptr openmc_create_source(std::string parameters) +// A function to create a unique pointer to an instance of this class when +// generated via a plugin call using dlopen/dlsym. You must have external C +// linkage here otherwise dlopen will not find the file +extern "C" std::unique_ptr openmc_create_source( + std::string parameters) { return std::make_unique(); } diff --git a/examples/parameterized_custom_source/parameterized_source_ring.cpp b/examples/parameterized_custom_source/parameterized_source_ring.cpp index e70dc8bb1..a983414f1 100644 --- a/examples/parameterized_custom_source/parameterized_source_ring.cpp +++ b/examples/parameterized_custom_source/parameterized_source_ring.cpp @@ -1,63 +1,65 @@ -#include // for M_PI +#include // for M_PI #include // for unique_ptr #include +#include "openmc/particle.h" #include "openmc/random_lcg.h" #include "openmc/source.h" -#include "openmc/particle.h" class RingSource : public openmc::Source { - public: - RingSource(double radius, double energy) : radius_(radius), energy_(energy) { } +public: + RingSource(double radius, double energy) : radius_(radius), energy_(energy) {} - // Defines a function that can create a unique pointer to a new instance of this class - // by extracting the parameters from the provided string. - static std::unique_ptr from_string(std::string parameters) - { - std::unordered_map parameter_mapping; + // Defines a function that can create a unique pointer to a new instance of + // this class by extracting the parameters from the provided string. + static std::unique_ptr from_string(std::string parameters) + { + std::unordered_map parameter_mapping; - std::stringstream ss(parameters); - std::string parameter; - while (std::getline(ss, parameter, ',')) { - parameter.erase(0, parameter.find_first_not_of(' ')); - std::string key = parameter.substr(0, parameter.find_first_of('=')); - std::string value = parameter.substr(parameter.find_first_of('=') + 1, parameter.length()); - parameter_mapping[key] = value; - } - - double radius = std::stod(parameter_mapping["radius"]); - double energy = std::stod(parameter_mapping["energy"]); - return std::make_unique(radius, energy); + std::stringstream ss(parameters); + std::string parameter; + while (std::getline(ss, parameter, ',')) { + parameter.erase(0, parameter.find_first_not_of(' ')); + std::string key = parameter.substr(0, parameter.find_first_of('=')); + std::string value = + parameter.substr(parameter.find_first_of('=') + 1, parameter.length()); + parameter_mapping[key] = value; } - // Samples from an instance of this class. - openmc::SourceSite sample(uint64_t* seed) const - { - openmc::SourceSite particle; - // particle type - particle.particle = openmc::ParticleType::neutron; - // position - double angle = 2.0 * M_PI * openmc::prn(seed); - double radius = this->radius_; - particle.r.x = radius * std::cos(angle); - particle.r.y = radius * std::sin(angle); - particle.r.z = 0.0; - // angle - particle.u = {1.0, 0.0, 0.0}; - particle.E = this->energy_; + double radius = std::stod(parameter_mapping["radius"]); + double energy = std::stod(parameter_mapping["energy"]); + return std::make_unique(radius, energy); + } - return particle; - } + // Samples from an instance of this class. + openmc::SourceSite sample(uint64_t* seed) const + { + openmc::SourceSite particle; + // particle type + particle.particle = openmc::ParticleType::neutron(); + // position + double angle = 2.0 * M_PI * openmc::prn(seed); + double radius = this->radius_; + particle.r.x = radius * std::cos(angle); + particle.r.y = radius * std::sin(angle); + particle.r.z = 0.0; + // angle + particle.u = {1.0, 0.0, 0.0}; + particle.E = this->energy_; - private: - double radius_; - double energy_; + return particle; + } + +private: + double radius_; + double energy_; }; -// A function to create a unique pointer to an instance of this class when generated -// via a plugin call using dlopen/dlsym. -// You must have external C linkage here otherwise dlopen will not find the file -extern "C" std::unique_ptr openmc_create_source(std::string parameters) +// A function to create a unique pointer to an instance of this class when +// generated via a plugin call using dlopen/dlsym. You must have external C +// linkage here otherwise dlopen will not find the file +extern "C" std::unique_ptr openmc_create_source( + std::string parameters) { return RingSource::from_string(parameters); } diff --git a/include/openmc/capi.h b/include/openmc/capi.h index 749dc98a0..019a418a1 100644 --- a/include/openmc/capi.h +++ b/include/openmc/capi.h @@ -201,8 +201,8 @@ int openmc_weight_windows_set_energy_bounds( int32_t index, double* e_bounds, size_t e_bounds_size); int openmc_weight_windows_get_energy_bounds( int32_t index, const double** e_bounds, size_t* e_bounds_size); -int openmc_weight_windows_set_particle(int32_t index, int particle); -int openmc_weight_windows_get_particle(int32_t index, int* particle); +int openmc_weight_windows_set_particle(int32_t index, int32_t particle); +int openmc_weight_windows_get_particle(int32_t index, int32_t* particle); int openmc_weight_windows_get_bounds(int32_t index, const double** lower_bounds, const double** upper_bounds, size_t* size); int openmc_weight_windows_set_bounds(int32_t index, const double* lower_bounds, @@ -227,7 +227,7 @@ int openmc_zernike_filter_set_order(int32_t index, int order); int openmc_zernike_filter_set_params( int32_t index, const double* x, const double* y, const double* r); -int openmc_particle_filter_get_bins(int32_t idx, int bins[]); +int openmc_particle_filter_get_bins(int32_t idx, int32_t bins[]); //! Sets the mesh and energy grid for CMFD reweight //! \param[in] meshtyally_id id of CMFD Mesh Tally diff --git a/include/openmc/constants.h b/include/openmc/constants.h index bba260c3a..980cff447 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -25,16 +25,16 @@ using double_4dvec = vector>>>; constexpr int HDF5_VERSION[] {3, 0}; // Version numbers for binary files -constexpr array VERSION_STATEPOINT {18, 1}; -constexpr array VERSION_PARTICLE_RESTART {2, 0}; -constexpr array VERSION_TRACK {3, 0}; +constexpr array VERSION_STATEPOINT {18, 2}; +constexpr array VERSION_PARTICLE_RESTART {2, 1}; +constexpr array VERSION_TRACK {3, 1}; constexpr array VERSION_SUMMARY {6, 1}; constexpr array VERSION_VOLUME {1, 0}; constexpr array VERSION_VOXEL {2, 0}; constexpr array VERSION_MGXS_LIBRARY {1, 0}; constexpr array VERSION_PROPERTIES {1, 1}; constexpr array VERSION_WEIGHT_WINDOWS {1, 0}; -constexpr array VERSION_COLLISION_TRACK {1, 0}; +constexpr array VERSION_COLLISION_TRACK {1, 1}; // ============================================================================ // ADJUSTABLE PARAMETERS diff --git a/include/openmc/nuclide.h b/include/openmc/nuclide.h index 60b88a153..58d833939 100644 --- a/include/openmc/nuclide.h +++ b/include/openmc/nuclide.h @@ -163,7 +163,7 @@ bool multipole_in_range(const Nuclide& nuc, double E); namespace data { // Minimum/maximum transport energy for each particle type. Order corresponds to -// that of the ParticleType enum +// transport_index() for supported transport particles. extern array energy_min; extern array energy_max; diff --git a/include/openmc/particle.h b/include/openmc/particle.h index 0f37719b9..2f6e6196b 100644 --- a/include/openmc/particle.h +++ b/include/openmc/particle.h @@ -126,10 +126,6 @@ public: //! Functions //============================================================================ -std::string particle_type_to_str(ParticleType type); - -ParticleType str_to_particle_type(std::string str); - void add_surf_source_to_bank(Particle& p, const Surface& surf); } // namespace openmc diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index fdacfa765..75166b36a 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -3,6 +3,7 @@ #include "openmc/array.h" #include "openmc/constants.h" +#include "openmc/particle_type.h" #include "openmc/position.h" #include "openmc/random_lcg.h" #include "openmc/tallies/filter_match.h" @@ -30,9 +31,6 @@ constexpr double CACHE_INVALID {-1.0}; //========================================================================== // Aliases and type definitions -//! Particle types -enum class ParticleType { neutron, photon, electron, positron }; - //! Saved ("banked") state of a particle //! NOTE: This structure's MPI type is built in initialize_mpi() of //! initialize.cpp. Any changes made to the struct here must also be @@ -496,7 +494,7 @@ private: MacroXS macro_xs_; CacheDataMG mg_xs_cache_; - ParticleType type_ {ParticleType::neutron}; + ParticleType type_; double E_; double E_last_; diff --git a/include/openmc/particle_type.h b/include/openmc/particle_type.h new file mode 100644 index 000000000..fed3c9284 --- /dev/null +++ b/include/openmc/particle_type.h @@ -0,0 +1,172 @@ +//============================================================================== +// ParticleType class definition +//============================================================================== + +#ifndef OPENMC_PARTICLE_TYPE_H +#define OPENMC_PARTICLE_TYPE_H + +#include +#include +#include +#include +#include + +#include "openmc/constants.h" + +namespace openmc { + +//------------------------------------------------------------------------------ +// PDG constants (canonical particle identity as simple integers) +//------------------------------------------------------------------------------ + +inline constexpr int32_t PDG_NEUTRON = 2112; +inline constexpr int32_t PDG_PHOTON = 22; +inline constexpr int32_t PDG_ELECTRON = 11; +inline constexpr int32_t PDG_POSITRON = -11; +inline constexpr int32_t PDG_PROTON = 2212; +inline constexpr int32_t PDG_DEUTERON = 1000010020; +inline constexpr int32_t PDG_TRITON = 1000010030; +inline constexpr int32_t PDG_ALPHA = 1000020040; + +//------------------------------------------------------------------------------ +// ParticleType class (standard-layout, trivially copyable) +//------------------------------------------------------------------------------ + +class ParticleType { +public: + //---------------------------------------------------------------------------- + // Constructors + + // Default constructor: defaults to neutron + constexpr ParticleType() : pdg_number_(PDG_NEUTRON) {} + + // Constructor from PDG number + constexpr explicit ParticleType(int32_t pdg_number) : pdg_number_(pdg_number) + {} + + // Constructor from particle name string (e.g., "neutron", "photon", "Fe56") + explicit ParticleType(std::string_view str); + + // Constructor from Z, A, and metastable state for nuclear particles + constexpr ParticleType(int Z, int A, int m = 0) + : pdg_number_(1000000000 + Z * 10000 + A * 10 + m) + {} + + //---------------------------------------------------------------------------- + // Accessors + + // Accessor for the underlying PDG number + constexpr int32_t pdg_number() const { return pdg_number_; } + + //---------------------------------------------------------------------------- + // Methods + + // Convert to string representation + std::string str() const; + + // Check if this represents a nucleus (vs elementary particle) + constexpr bool is_nucleus() const + { + // PDG nuclear codes are >= 1000000000 (100ZZZAAAI format) + return pdg_number_ >= 1000000000; + } + + // Get transport index (0-3 for transportable particles, C_NONE otherwise) + constexpr int transport_index() const; + + // Check if this is a neutron + constexpr bool is_neutron() const { return pdg_number_ == PDG_NEUTRON; } + + // Check if this is a photon + constexpr bool is_photon() const { return pdg_number_ == PDG_PHOTON; } + + constexpr bool is_transportable() const + { + return this->transport_index() != C_NONE; + } + + //---------------------------------------------------------------------------- + // Static factory methods + + static constexpr ParticleType neutron() { return ParticleType {PDG_NEUTRON}; } + static constexpr ParticleType photon() { return ParticleType {PDG_PHOTON}; } + static constexpr ParticleType electron() + { + return ParticleType {PDG_ELECTRON}; + } + static constexpr ParticleType positron() + { + return ParticleType {PDG_POSITRON}; + } + static constexpr ParticleType proton() { return ParticleType {PDG_PROTON}; } + static constexpr ParticleType deuteron() + { + return ParticleType {PDG_DEUTERON}; + } + static constexpr ParticleType triton() { return ParticleType {PDG_TRITON}; } + static constexpr ParticleType alpha() { return ParticleType {PDG_ALPHA}; } + +private: + int32_t pdg_number_; +}; + +//------------------------------------------------------------------------------ +// Static assertions to ensure standard-layout and trivially copyable +//------------------------------------------------------------------------------ + +static_assert(std::is_standard_layout_v, + "ParticleType must be standard-layout"); +static_assert(std::is_trivially_copyable_v, + "ParticleType must be trivially copyable"); +static_assert(sizeof(ParticleType) == sizeof(int32_t), + "ParticleType must be same size as int32_t"); + +//------------------------------------------------------------------------------ +// Comparison operators (free functions for symmetry) +//------------------------------------------------------------------------------ + +constexpr bool operator==(ParticleType lhs, ParticleType rhs) +{ + return lhs.pdg_number() == rhs.pdg_number(); +} + +constexpr bool operator!=(ParticleType lhs, ParticleType rhs) +{ + return lhs.pdg_number() != rhs.pdg_number(); +} + +constexpr bool operator<(ParticleType lhs, ParticleType rhs) +{ + return lhs.pdg_number() < rhs.pdg_number(); +} + +//------------------------------------------------------------------------------ +// ParticleType member function implementations (inline) +//------------------------------------------------------------------------------ + +constexpr int ParticleType::transport_index() const +{ + switch (pdg_number_) { + case PDG_NEUTRON: + return 0; + case PDG_PHOTON: + return 1; + case PDG_ELECTRON: + return 2; + case PDG_POSITRON: + return 3; + default: + return C_NONE; + } +} + +//------------------------------------------------------------------------------ +// Legacy conversion helpers +//------------------------------------------------------------------------------ + +// Legacy enum code (0..3) to ParticleType conversion +ParticleType legacy_particle_index_to_type(int code); + +} // namespace openmc + +#endif // OPENMC_PARTICLE_TYPE_H diff --git a/include/openmc/reaction_product.h b/include/openmc/reaction_product.h index 4fbbc1b62..9a8eab7d9 100644 --- a/include/openmc/reaction_product.h +++ b/include/openmc/reaction_product.h @@ -10,7 +10,7 @@ #include "openmc/chain.h" #include "openmc/endf.h" #include "openmc/memory.h" // for unique_ptr -#include "openmc/particle.h" +#include "openmc/particle_type.h" #include "openmc/vector.h" // for vector namespace openmc { diff --git a/include/openmc/source.h b/include/openmc/source.h index 36c821fc0..2f32aa2a0 100644 --- a/include/openmc/source.h +++ b/include/openmc/source.h @@ -12,7 +12,7 @@ #include "openmc/distribution_multi.h" #include "openmc/distribution_spatial.h" #include "openmc/memory.h" -#include "openmc/particle.h" +#include "openmc/particle_type.h" #include "openmc/vector.h" namespace openmc { @@ -148,11 +148,11 @@ protected: private: // Data members - ParticleType particle_ {ParticleType::neutron}; //!< Type of particle emitted - UPtrSpace space_; //!< Spatial distribution - UPtrAngle angle_; //!< Angular distribution - UPtrDist energy_; //!< Energy distribution - UPtrDist time_; //!< Time distribution + ParticleType particle_; //!< Type of particle emitted + UPtrSpace space_; //!< Spatial distribution + UPtrAngle angle_; //!< Angular distribution + UPtrDist energy_; //!< Energy distribution + UPtrDist time_; //!< Time distribution }; //============================================================================== diff --git a/include/openmc/tallies/filter_particle.h b/include/openmc/tallies/filter_particle.h index 863a6d282..9932a6037 100644 --- a/include/openmc/tallies/filter_particle.h +++ b/include/openmc/tallies/filter_particle.h @@ -1,7 +1,7 @@ #ifndef OPENMC_TALLIES_FILTER_PARTICLE_H #define OPENMC_TALLIES_FILTER_PARTICLE_H -#include "openmc/particle.h" +#include "openmc/particle_type.h" #include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/vector.h" diff --git a/include/openmc/weight_windows.h b/include/openmc/weight_windows.h index 763815522..0d7435ca2 100644 --- a/include/openmc/weight_windows.h +++ b/include/openmc/weight_windows.h @@ -10,7 +10,7 @@ #include "openmc/constants.h" #include "openmc/memory.h" #include "openmc/mesh.h" -#include "openmc/particle.h" +#include "openmc/particle_type.h" #include "openmc/span.h" #include "openmc/tallies/tally.h" #include "openmc/vector.h" @@ -193,10 +193,9 @@ public: private: //---------------------------------------------------------------------------- // Data members - int32_t id_; //!< Unique ID - int64_t index_; //!< Index into weight windows vector - ParticleType particle_type_ { - ParticleType::neutron}; //!< Particle type to apply weight windows to + int32_t id_; //!< Unique ID + 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: //!< energy_bins, mesh_bins (k, j, i)) diff --git a/openmc/filter.py b/openmc/filter.py index e7c54f2f0..53ec93e21 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -35,7 +35,6 @@ _CURRENT_NAMES = ( 'z-min out', 'z-min in', 'z-max out', 'z-max in' ) -_PARTICLES = {'neutron', 'photon', 'electron', 'positron'} class FilterMeta(ABCMeta): @@ -735,9 +734,8 @@ class ParticleFilter(Filter): Parameters ---------- - bins : str, or sequence of str - The particles to tally represented as strings ('neutron', 'photon', - 'electron', 'positron'). + bins : str, int, openmc.ParticleType, or sequence + The particle types to tally represented as names, PDG numbers, or types. filter_id : int Unique identifier for the filter @@ -763,11 +761,16 @@ class ParticleFilter(Filter): @Filter.bins.setter def bins(self, bins): - cv.check_type('bins', bins, Sequence, str) + if isinstance(bins, (str, Integral, openmc.ParticleType)): + bins = [bins] + else: + cv.check_type('bins', bins, Sequence, + (str, Integral, openmc.ParticleType)) bins = np.atleast_1d(bins) - for edge in bins: - cv.check_value('filter bin', edge, _PARTICLES) - self._bins = bins + normalized = [] + for entry in bins: + normalized.append(str(openmc.ParticleType(entry))) + self._bins = np.array(normalized, dtype=str) @classmethod def from_hdf5(cls, group, **kwargs): diff --git a/openmc/lib/core.py b/openmc/lib/core.py index 02c7784d1..79310bb37 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -28,7 +28,7 @@ class _SourceSite(Structure): ('wgt', c_double), ('delayed_group', c_int), ('surf_id', c_int), - ('particle', c_int), + ('particle', c_int32), ('parent_nuclide', c_int), ('parent_id', c_int64), ('progeny_id', c_int64)] diff --git a/openmc/lib/filter.py b/openmc/lib/filter.py index 5cf496107..dc81c3487 100644 --- a/openmc/lib/filter.py +++ b/openmc/lib/filter.py @@ -132,6 +132,9 @@ _dll.openmc_meshsurface_filter_set_translation.errcheck = _error_handler _dll.openmc_new_filter.argtypes = [c_char_p, POINTER(c_int32)] _dll.openmc_new_filter.restype = c_int _dll.openmc_new_filter.errcheck = _error_handler +_dll.openmc_particle_filter_get_bins.argtypes = [c_int32, POINTER(c_int32)] +_dll.openmc_particle_filter_get_bins.restype = c_int +_dll.openmc_particle_filter_get_bins.errcheck = _error_handler _dll.openmc_spatial_legendre_filter_get_order.argtypes = [c_int32, POINTER(c_int)] _dll.openmc_spatial_legendre_filter_get_order.restype = c_int _dll.openmc_spatial_legendre_filter_get_order.errcheck = _error_handler @@ -402,8 +405,8 @@ class MeshFilter(Filter): translation : Iterable of float 3-D coordinates of the translation vector rotation : Iterable of float - The rotation matrix or angles of the filter mesh. This can either be - a fully specified 3 x 3 rotation matrix or an Iterable of length 3 + The rotation matrix or angles of the filter mesh. This can either be + a fully specified 3 x 3 rotation matrix or an Iterable of length 3 with the angles in degrees about the x, y, and z axes, respectively. """ @@ -454,7 +457,7 @@ class MeshFilter(Filter): else: raise ValueError( f'Invalid size of rotation matrix: {rot_size}') - + @rotation.setter def rotation(self, rotation_data): flat_rotation = np.asarray(rotation_data, dtype=float).flatten() @@ -598,9 +601,9 @@ class ParticleFilter(Filter): @property def bins(self): - particle_i = np.zeros((self.n_bins,), dtype=c_int) + particle_i = np.zeros((self.n_bins,), dtype=c_int32) _dll.openmc_particle_filter_get_bins( - self._index, particle_i.ctypes.data_as(POINTER(c_int))) + self._index, particle_i.ctypes.data_as(POINTER(c_int32))) return [ParticleType(i) for i in particle_i] diff --git a/openmc/lib/weight_windows.py b/openmc/lib/weight_windows.py index ed442d33f..2b26d3b55 100644 --- a/openmc/lib/weight_windows.py +++ b/openmc/lib/weight_windows.py @@ -53,11 +53,11 @@ _dll.openmc_weight_windows_get_energy_bounds.argtypes = [c_int32, POINTER(POINTE _dll.openmc_weight_windows_get_energy_bounds.restype = c_int _dll.openmc_weight_windows_get_energy_bounds.errcheck = _error_handler -_dll.openmc_weight_windows_set_particle.argtypes = [c_int32, c_int] +_dll.openmc_weight_windows_set_particle.argtypes = [c_int32, c_int32] _dll.openmc_weight_windows_set_particle.restype = c_int _dll.openmc_weight_windows_set_particle.errcheck = _error_handler -_dll.openmc_weight_windows_get_particle.argtypes = [c_int32, POINTER(c_int)] +_dll.openmc_weight_windows_get_particle.argtypes = [c_int32, POINTER(c_int32)] _dll.openmc_weight_windows_get_particle.restype = c_int _dll.openmc_weight_windows_get_particle.errcheck = _error_handler @@ -201,16 +201,13 @@ class WeightWindows(_FortranObjectWithID): @property def particle(self): - val = c_int() + val = c_int32() _dll.openmc_weight_windows_get_particle(self._index, val) return ParticleType(val.value) @particle.setter def particle(self, p): - if isinstance(p, str): - p = ParticleType.from_string(p) - else: - p = ParticleType(p) + p = ParticleType(p) _dll.openmc_weight_windows_set_particle(self._index, int(p)) @property @@ -304,10 +301,10 @@ class WeightWindows(_FortranObjectWithID): ---------- tally : openmc.lib.Tally The tally used to create the WeightWindows instance. - particle : openmc.ParticleType or str, optional + particle : openmc.ParticleType or str or int, optional The particle type to use for the WeightWindows instance. Should be - specified as an instance of ParticleType or as a string with a value of - 'neutron' or 'photon'. + specified as an instance of ParticleType, a PDG number, or as a + name. Returns ------- @@ -317,7 +314,8 @@ class WeightWindows(_FortranObjectWithID): Raises ------ ValueError - If the particle parameter is not an instance of ParticleType or a string. + If the particle parameter is not an instance of ParticleType, a string, + or an integer PDG number. ValueError If the particle parameter is not a valid particle type (i.e., not 'neutron' or 'photon'). @@ -328,12 +326,13 @@ class WeightWindows(_FortranObjectWithID): If the tally does not have a MeshFilter. """ # do some checks on particle value - if not isinstance(particle, (ParticleType, str)): - raise ValueError(f"Parameter 'particle' must be {ParticleType} or one of ('neutron', 'photon').") + if not isinstance(particle, (ParticleType, str, int)): + raise ValueError( + f"Parameter 'particle' must be {ParticleType} or one of ('neutron', 'photon')." + ) # convert particle type if needed - if isinstance(particle, str): - particle = ParticleType.from_string(particle) + particle = ParticleType(particle) if particle not in (ParticleType.NEUTRON, ParticleType.PHOTON): raise ValueError('Weight windows can only be applied for neutrons or photons') diff --git a/openmc/particle_restart.py b/openmc/particle_restart.py index 317d8761b..44864107d 100644 --- a/openmc/particle_restart.py +++ b/openmc/particle_restart.py @@ -1,6 +1,7 @@ import h5py import openmc.checkvalue as cv +from .particle_type import ParticleType _VERSION_PARTICLE_RESTART = 2 @@ -28,8 +29,8 @@ class Particle: Type of simulation (criticality or fixed source) id : long Identifier of the particle - type : int - Particle type (1 = neutron, 2 = photon, 3 = electron, 4 = positron) + type : openmc.ParticleType + Particle type weight : float Weight of the particle energy : float @@ -52,7 +53,7 @@ class Particle: self.energy = f['energy'][()] self.generations_per_batch = f['generations_per_batch'][()] self.id = f['id'][()] - self.type = f['type'][()] + self.type = ParticleType(f['type'][()]) self.n_particles = f['n_particles'][()] self.run_mode = f['run_mode'][()].decode() self.uvw = f['uvw'][()] diff --git a/openmc/particle_type.py b/openmc/particle_type.py new file mode 100644 index 000000000..9d7713443 --- /dev/null +++ b/openmc/particle_type.py @@ -0,0 +1,228 @@ +from numbers import Integral + +from openmc.data import gnds_name, zam, ATOMIC_SYMBOL + + +_PDG_NAME = { + 2112: 'neutron', + 22: 'photon', + 11: 'electron', + -11: 'positron', + 2212: 'H1', +} + +_ALIAS_PDG = { + 'neutron': 2112, + 'n': 2112, + 'photon': 22, + 'gamma': 22, + 'electron': 11, + 'positron': -11, + 'proton': 2212, + 'p': 2212, + 'h1': 2212, + 'deuteron': 1000010020, + 'd': 1000010020, + 'h2': 1000010020, + 'triton': 1000010030, + 't': 1000010030, + 'h3': 1000010030, + 'alpha': 1000020040, + 'he4': 1000020040, +} + +_LEGACY_PARTICLE_INDEX = { + 0: 2112, + 1: 22, + 2: 11, + 3: -11, +} + + +class ParticleType: + """Particle type defined by a PDG number. + + ParticleType uses the Particle Data Group (PDG) Monte Carlo numbering scheme + to uniquely identify particle types. This includes elementary particles + (neutrons, photons, etc.) and nuclear codes for isotopes. + + Parameters + ---------- + value : str, int, or ParticleType + The particle identifier. Can be: + + - A string name (e.g., 'neutron', 'photon', 'He4', 'U235') + - An integer PDG number (e.g., 2112 for neutron) + - A string with PDG prefix (e.g., 'pdg:2112') + - An existing ParticleType instance + + Attributes + ---------- + pdg_number : int + The PDG number for this particle type + zam : tuple of int or None + For nuclear particles, the (Z, A, m) tuple where Z is atomic number, + A is mass number, and m is metastable state. None for elementary particles. + is_nucleus : bool + Whether this particle is a nucleus (ion) + + Examples + -------- + >>> neutron = ParticleType('neutron') + >>> neutron.pdg_number + 2112 + >>> he4 = ParticleType('He4') + >>> he4.zam + (2, 4, 0) + >>> ParticleType(2112) == ParticleType('neutron') + True + + """ + + __slots__ = ('_pdg_number',) + + def __init__(self, value: 'str | int | ParticleType'): + if isinstance(value, ParticleType): + pdg = value._pdg_number + elif isinstance(value, str): + pdg = self._pdg_number_from_string(value) + elif isinstance(value, Integral): + pdg = int(value) + # Handle legacy particle indices (0, 1, 2, 3) + if pdg in _LEGACY_PARTICLE_INDEX: + pdg = _LEGACY_PARTICLE_INDEX[pdg] + else: + raise TypeError(f"Cannot create ParticleType from {type(value).__name__}") + + self._pdg_number = pdg + + def __eq__(self, other): + if isinstance(other, ParticleType): + return self._pdg_number == other._pdg_number + if isinstance(other, Integral): + return self._pdg_number == int(other) + if isinstance(other, str): + try: + return self._pdg_number == ParticleType(other)._pdg_number + except (ValueError, TypeError): + return False + return NotImplemented + + def __hash__(self) -> int: + return hash(self._pdg_number) + + def __int__(self) -> int: + return self._pdg_number + + @property + def pdg_number(self) -> int: + return self._pdg_number + + @staticmethod + def _pdg_number_from_string(value: str) -> int: + """Parse a string to get a PDG number. + + Parameters + ---------- + value : str + Particle identifier string + + Returns + ------- + int + PDG number + + Raises + ------ + ValueError + If string cannot be parsed as a valid particle identifier + + """ + s = value.strip() + if not s: + raise ValueError('Particle identifier cannot be empty.') + + lower = s.lower() + if lower.startswith('pdg:'): + code_str = lower[4:] + try: + return int(code_str) + except ValueError: + raise ValueError(f'Invalid PDG number: {code_str}') + + if lower in _ALIAS_PDG: + return _ALIAS_PDG[lower] + + # Assume it is a GNDS nuclide name + Z, A, m = zam(s) + if Z <= 0 or Z > 999 or A <= 0 or A > 999 or m < 0 or m > 9: + raise ValueError('Invalid Z/A/m for nuclear PDG number.') + return 1000000000 + Z * 10000 + A * 10 + m + + def __repr__(self) -> str: + return f'' + + def __str__(self) -> str: + """Return a canonical string representation of the particle type. + + Returns + ------- + str + Canonical name (e.g., 'neutron', 'He4', 'pdg:12345') + + """ + if self._pdg_number in _PDG_NAME: + return _PDG_NAME[self._pdg_number] + + if (zam_tuple := self.zam) is not None: + Z, A, m = zam_tuple + if Z <= 0 or Z > max(ATOMIC_SYMBOL) or A <= 0 or A > 999: + raise ValueError(f"Invalid nuclear PDG number: {self._pdg_number}") + return gnds_name(Z, A, m) + + return f'pdg:{self._pdg_number}' + + @property + def zam(self) -> 'tuple[int, int, int] | None': + """Return the (Z, A, m) tuple for nuclear particles. + + Returns + ------- + tuple of int or None + For nuclear particles, returns (Z, A, m) where Z is atomic number, + A is mass number, and m is metastable state. Returns None for + elementary particles. + + """ + if self._pdg_number < 1000000000: + return None + Z = (self._pdg_number // 10000) % 1000 + A = (self._pdg_number // 10) % 1000 + m = self._pdg_number % 10 + if Z <= 0 or A <= 0: + return None + else: + return (Z, A, m) + + @property + def is_nucleus(self) -> bool: + """Return whether this particle is a nucleus. + + Returns + ------- + bool + True if the particle is a nucleus (ion), False otherwise + + """ + return self.zam is not None + + +# Define common particle constants +ParticleType.NEUTRON = ParticleType(2112) +ParticleType.PHOTON = ParticleType(22) +ParticleType.ELECTRON = ParticleType(11) +ParticleType.POSITRON = ParticleType(-11) +ParticleType.PROTON = ParticleType(2212) +ParticleType.DEUTERON = ParticleType(1000010020) +ParticleType.TRITON = ParticleType(1000010030) +ParticleType.ALPHA = ParticleType(1000020040) diff --git a/openmc/source.py b/openmc/source.py index 5acf55c21..a11cfd6c8 100644 --- a/openmc/source.py +++ b/openmc/source.py @@ -1,7 +1,6 @@ from __future__ import annotations from abc import ABC, abstractmethod from collections.abc import Iterable, Sequence -from enum import IntEnum from numbers import Real from pathlib import Path import warnings @@ -19,6 +18,8 @@ from openmc.stats.multivariate import UnitSphere, Spatial from openmc.stats.univariate import Univariate from ._xml import get_elem_list, get_text from .mesh import MeshBase, StructuredMesh, UnstructuredMesh +from .particle_type import ParticleType +from .statepoint import _VERSION_STATEPOINT from .utility_funcs import input_path @@ -265,8 +266,8 @@ class IndependentSource(SourceBase): time distribution of source sites strength : float Strength of the source - particle : {'neutron', 'photon', 'electron', 'positron'} - Source particle type + particle : str or int or openmc.ParticleType + Source particle type (name, PDG number, or type) domains : iterable of openmc.Cell, openmc.Material, or openmc.Universe Domains to reject based on, i.e., if a sampled spatial location is not within one of these domains, it will be rejected. @@ -302,10 +303,9 @@ class IndependentSource(SourceBase): type : str Indicator of source type: 'independent' - .. versionadded:: 0.14.0 - - particle : {'neutron', 'photon', 'electron', 'positron'} - Source particle type + .. versionadded:: 0.14.0 + particle : str or int or openmc.ParticleType + Source particle type (alias, PDG number, or GNDS nuclide name) constraints : dict Constraints on sampled source particles. Valid keys include 'domain_type', 'domain_ids', 'time_bounds', 'energy_bounds', @@ -320,7 +320,7 @@ class IndependentSource(SourceBase): energy: openmc.stats.Univariate | None = None, time: openmc.stats.Univariate | None = None, strength: float = 1.0, - particle: str = 'neutron', + particle: str | int | ParticleType = 'neutron', domains: Sequence[openmc.Cell | openmc.Material | openmc.Universe] | None = None, constraints: dict[str, Any] | None = None @@ -405,14 +405,12 @@ class IndependentSource(SourceBase): self._time = time @property - def particle(self): + def particle(self) -> ParticleType: return self._particle @particle.setter def particle(self, particle): - cv.check_value('source particle', particle, - ['neutron', 'photon', 'electron', 'positron']) - self._particle = particle + self._particle = ParticleType(particle) def populate_xml_element(self, element): """Add necessary source information to an XML element @@ -423,7 +421,7 @@ class IndependentSource(SourceBase): XML element containing source data """ - element.set("particle", self.particle) + element.set("particle", str(self.particle)) if self.space is not None: element.append(self.space.to_xml_element()) if self.angle is not None: @@ -898,76 +896,6 @@ class FileSource(SourceBase): return cls(**kwargs) -class ParticleType(IntEnum): - """ - IntEnum class representing a particle type. Type - values mirror those found in the C++ class. - """ - NEUTRON = 0 - PHOTON = 1 - ELECTRON = 2 - POSITRON = 3 - - @classmethod - def from_string(cls, value: str): - """ - Constructs a ParticleType instance from a string. - - Parameters - ---------- - value : str - The string representation of the particle type. - - Returns - ------- - The corresponding ParticleType instance. - """ - try: - return cls[value.upper()] - except KeyError: - raise ValueError( - f"Invalid string for creation of {cls.__name__}: {value}") - - @classmethod - def from_pdg_number(cls, pdg_number: int) -> ParticleType: - """Constructs a ParticleType instance from a PDG number. - - The Particle Data Group at LBNL publishes a Monte Carlo particle - numbering scheme as part of the `Review of Particle Physics - <10.1103/PhysRevD.110.030001>`_. This method maps PDG numbers to the - corresponding :class:`ParticleType`. - - Parameters - ---------- - pdg_number : int - The PDG number of the particle type. - - Returns - ------- - The corresponding ParticleType instance. - """ - try: - return { - 2112: ParticleType.NEUTRON, - 22: ParticleType.PHOTON, - 11: ParticleType.ELECTRON, - -11: ParticleType.POSITRON, - }[pdg_number] - except KeyError: - raise ValueError(f"Unrecognized PDG number: {pdg_number}") - - def __repr__(self) -> str: - """ - Returns a string representation of the ParticleType instance. - - Returns: - str: The lowercase name of the ParticleType instance. - """ - return self.name.lower() - - # needed for < Python 3.11 - def __str__(self) -> str: - return self.__repr__() class SourceParticle: @@ -992,8 +920,8 @@ class SourceParticle: Delayed group particle was created in (neutrons only) surf_id : int Surface ID where particle is at, if any. - particle : ParticleType - Type of the particle + particle : ParticleType or str or int + Type of the particle (type, name, or PDG number) """ @@ -1006,7 +934,7 @@ class SourceParticle: wgt: float = 1.0, delayed_group: int = 0, surf_id: int = 0, - particle: ParticleType = ParticleType.NEUTRON + particle: ParticleType | str | int = ParticleType.NEUTRON ): self.r = tuple(r) @@ -1018,9 +946,16 @@ class SourceParticle: self.surf_id = surf_id self.particle = particle + @property + def particle(self) -> ParticleType: + return self._particle + + @particle.setter + def particle(self, particle): + self._particle = ParticleType(particle) + def __repr__(self): - name = self.particle.name.lower() - return f'' + return f'' def to_tuple(self) -> tuple: """Return source particle attributes as a tuple @@ -1032,7 +967,7 @@ class SourceParticle: """ return (self.r, self.u, self.E, self.time, self.wgt, - self.delayed_group, self.surf_id, self.particle.value) + self.delayed_group, self.surf_id, self.particle.pdg_number) def write_source_file( @@ -1116,12 +1051,7 @@ class ParticleList(list): particles = [] with mcpl.MCPLFile(filename) as f: for particle in f.particles: - # Determine particle type based on the PDG number - try: - particle_type = ParticleType.from_pdg_number( - particle.pdgcode) - except ValueError: - particle_type = "UNKNOWN" + particle_type = ParticleType(particle.pdgcode) # Create a source particle instance. Note that MCPL stores # energy in MeV and time in ms. @@ -1179,7 +1109,7 @@ class ParticleList(list): # Extract the attributes of the source particles into a list of tuples data = [(sp.r[0], sp.r[1], sp.r[2], sp.u[0], sp.u[1], sp.u[2], sp.E, sp.time, sp.wgt, sp.delayed_group, sp.surf_id, - sp.particle.name.lower()) for sp in self] + str(sp.particle)) for sp in self] # Define the column names for the DataFrame columns = ['x', 'y', 'z', 'u_x', 'u_y', 'u_z', 'E', 'time', 'wgt', @@ -1226,6 +1156,7 @@ class ParticleList(list): kwargs.setdefault('mode', 'w') with h5py.File(filename, **kwargs) as fh: fh.attrs['filetype'] = np.bytes_("source") + fh.attrs['version'] = np.array([_VERSION_STATEPOINT, 2]) fh.create_dataset('source_bank', data=arr, dtype=source_dtype) @@ -1337,7 +1268,7 @@ def read_collision_track_mcpl(file_path): data['material_id'].append(int(values_dict.get('material_id', 0))) data['universe_id'].append(int(values_dict.get('universe_id', 0))) data['n_collision'].append(int(values_dict.get('n_collision', 0))) - data['particle'].append(ParticleType.from_pdg_number(p.pdgcode)) + data['particle'].append(ParticleType(p.pdgcode)) data['parent_id'].append(int(values_dict.get('parent_id', 0))) data['progeny_id'].append(int(values_dict.get('progeny_id', 0))) diff --git a/openmc/tracks.py b/openmc/tracks.py index 81646e7d2..44e3c0eba 100644 --- a/openmc/tracks.py +++ b/openmc/tracks.py @@ -4,7 +4,8 @@ from collections.abc import Sequence import h5py from .checkvalue import check_filetype_version -from .source import SourceParticle, ParticleType +from .particle_type import ParticleType +from .source import SourceParticle from pathlib import Path @@ -25,7 +26,7 @@ states : numpy.ndarray """ def _particle_track_repr(self): - return f"" + return f"" ParticleTrack.__repr__ = _particle_track_repr @@ -92,8 +93,8 @@ class Track(Sequence): Parameters ---------- - particle : {'neutron', 'photon', 'electron', 'positron'} - Matching particle type + particle : str or int or openmc.ParticleType + Matching particle type (name, PDG number, or type) state_filter : function Function that takes a state (structured datatype) and returns a bool depending on some criteria. @@ -126,7 +127,7 @@ class Track(Sequence): for t in self: # Check for matching particle if particle is not None: - if t.particle.name.lower() != particle: + if t.particle != ParticleType(particle): continue # Apply arbitrary state filter @@ -184,7 +185,7 @@ class Track(Sequence): def sources(self): sources = [] for particle_track in self: - particle_type = ParticleType(particle_track.particle) + particle_type = particle_track.particle state = particle_track.states[0] sources.append( SourceParticle( diff --git a/openmc/weight_windows.py b/openmc/weight_windows.py index 5d52a579a..7797986df 100644 --- a/openmc/weight_windows.py +++ b/openmc/weight_windows.py @@ -10,13 +10,12 @@ import numpy as np import h5py import openmc -from openmc.filter import _PARTICLES from openmc.mesh import MeshBase, RectilinearMesh, CylindricalMesh, SphericalMesh, UnstructuredMesh import openmc.checkvalue as cv from openmc.checkvalue import PathLike from ._xml import get_elem_list, get_text, clean_indentation from .mixin import IDManagerMixin -from .utility_funcs import change_directory +from .particle_type import ParticleType class WeightWindows(IDManagerMixin): @@ -51,7 +50,7 @@ class WeightWindows(IDManagerMixin): A list of values for which each successive pair constitutes a range of energies in [eV] for a single bin. If no energy bins are provided, the maximum and minimum energy for the data available at runtime. - particle_type : {'neutron', 'photon'} + particle_type : str or int or openmc.ParticleType Particle type the weight windows apply to survival_ratio : float Ratio of the survival weight to the lower weight window bound for @@ -116,7 +115,7 @@ class WeightWindows(IDManagerMixin): upper_ww_bounds: Iterable[float] | None = None, upper_bound_ratio: float | None = None, energy_bounds: Iterable[Real] | None = None, - particle_type: str = 'neutron', + particle_type: str | int | openmc.ParticleType = 'neutron', survival_ratio: float = 3.0, max_lower_bound_ratio: float | None = None, max_split: int = 10, @@ -213,13 +212,15 @@ class WeightWindows(IDManagerMixin): self._mesh = mesh @property - def particle_type(self) -> str: + def particle_type(self) -> ParticleType: return self._particle_type @particle_type.setter - def particle_type(self, pt: str): - cv.check_value('Particle type', pt, _PARTICLES) - self._particle_type = pt + def particle_type(self, pt): + ptype = ParticleType(pt) + if ptype not in {ParticleType.NEUTRON, ParticleType.PHOTON}: + raise ValueError("Weight windows can only be applied for neutrons or photons") + self._particle_type = ptype @property def energy_bounds(self) -> Iterable[Real]: @@ -329,7 +330,7 @@ class WeightWindows(IDManagerMixin): subelement.text = str(self.mesh.id) subelement = ET.SubElement(element, 'particle_type') - subelement.text = self.particle_type + subelement.text = str(self.particle_type) if self.energy_bounds is not None: subelement = ET.SubElement(element, 'energy_bounds') @@ -494,7 +495,7 @@ class WeightWindowGenerator: A list of values for which each successive pair constitutes a range of energies in [eV] for a single bin. If no energy bins are provided, the maximum and minimum energy for the data available at runtime. - particle_type : {'neutron', 'photon'} + particle_type : str or int or openmc.ParticleType Particle type the weight windows apply to method : {'magic', 'fw_cadis'} The weight window generation methodology applied during an update. @@ -513,7 +514,7 @@ class WeightWindowGenerator: energy_bounds : Iterable of Real A list of values for which each successive pair constitutes a range of energies in [eV] for a single bin - particle_type : {'neutron', 'photon'} + particle_type : openmc.ParticleType Particle type the weight windows apply to method : {'magic', 'fw_cadis'} The weight window generation methodology applied during an update. @@ -534,7 +535,7 @@ class WeightWindowGenerator: self, mesh: openmc.MeshBase, energy_bounds: Sequence[float] | None = None, - particle_type: str = 'neutron', + particle_type: str | int | openmc.ParticleType = 'neutron', method: str = 'magic', max_realizations: int = 1, update_interval: int = 1, @@ -555,7 +556,7 @@ class WeightWindowGenerator: def __repr__(self): string = type(self).__name__ + '\n' string += f'\t{"Mesh":<20}=\t{self.mesh.id}\n' - string += f'\t{"Particle:":<20}=\t{self.particle_type}\n' + string += f'\t{"Particle:":<20}=\t{str(self.particle_type)}\n' string += f'\t{"Energy Bounds:":<20}=\t{self.energy_bounds}\n' string += f'\t{"Method":<20}=\t{self.method}\n' string += f'\t{"Max Realizations:":<20}=\t{self.max_realizations}\n' @@ -586,13 +587,15 @@ class WeightWindowGenerator: self._energy_bounds = eb @property - def particle_type(self) -> str: + def particle_type(self) -> ParticleType: return self._particle_type @particle_type.setter - def particle_type(self, pt: str): - cv.check_value('particle type', pt, ('neutron', 'photon')) - self._particle_type = pt + def particle_type(self, pt): + ptype = ParticleType(pt) + if ptype not in {ParticleType.NEUTRON, ParticleType.PHOTON}: + raise ValueError("Weight windows can only be applied for neutrons or photons") + self._particle_type = ptype @property def method(self) -> str: @@ -695,7 +698,7 @@ class WeightWindowGenerator: subelement = ET.SubElement(element, 'energy_bounds') subelement.text = ' '.join(str(e) for e in self.energy_bounds) particle_elem = ET.SubElement(element, 'particle_type') - particle_elem.text = self.particle_type + particle_elem.text = str(self.particle_type) realizations_elem = ET.SubElement(element, 'max_realizations') realizations_elem.text = str(self.max_realizations) update_interval_elem = ET.SubElement(element, 'update_interval') @@ -730,7 +733,7 @@ class WeightWindowGenerator: mesh_id = int(get_text(elem, 'mesh')) mesh = meshes[mesh_id] - + energy_bounds = get_elem_list(elem, "energy_bounds, float") particle_type = get_text(elem, 'particle_type') diff --git a/src/bremsstrahlung.cpp b/src/bremsstrahlung.cpp index a2320e0b4..d77066fb0 100644 --- a/src/bremsstrahlung.cpp +++ b/src/bremsstrahlung.cpp @@ -31,13 +31,13 @@ void thick_target_bremsstrahlung(Particle& p, double* E_lost) if (p.material() == MATERIAL_VOID) return; - int photon = static_cast(ParticleType::photon); + int photon = ParticleType::photon().transport_index(); if (p.E() < settings::energy_cutoff[photon]) return; // Get bremsstrahlung data for this material and particle type BremsstrahlungData* mat; - if (p.type() == ParticleType::positron) { + if (p.type() == ParticleType::positron()) { mat = &model::materials[p.material()]->ttb_->positron; } else { mat = &model::materials[p.material()]->ttb_->electron; @@ -119,7 +119,7 @@ void thick_target_bremsstrahlung(Particle& p, double* E_lost) } // Create secondary photon - p.create_secondary(p.wgt(), p.u(), w, ParticleType::photon); + p.create_secondary(p.wgt(), p.u(), w, ParticleType::photon()); *E_lost += w; } } diff --git a/src/material.cpp b/src/material.cpp index 54caa3840..072e6deca 100644 --- a/src/material.cpp +++ b/src/material.cpp @@ -819,9 +819,9 @@ void Material::calculate_xs(Particle& p) const p.macro_xs().fission = 0.0; p.macro_xs().nu_fission = 0.0; - if (p.type() == ParticleType::neutron) { + if (p.type().is_neutron()) { this->calculate_neutron_xs(p); - } else if (p.type() == ParticleType::photon) { + } else if (p.type().is_photon()) { this->calculate_photon_xs(p); } } @@ -829,7 +829,7 @@ void Material::calculate_xs(Particle& p) const void Material::calculate_neutron_xs(Particle& p) const { // Find energy index on energy grid - int neutron = static_cast(ParticleType::neutron); + int neutron = ParticleType::neutron().transport_index(); int i_grid = std::log(p.E() / data::energy_min[neutron]) / simulation::log_spacing; diff --git a/src/mcpl_interface.cpp b/src/mcpl_interface.cpp index 256f3343f..30c41ec5a 100644 --- a/src/mcpl_interface.cpp +++ b/src/mcpl_interface.cpp @@ -321,25 +321,7 @@ inline void ensure_mcpl_ready_or_fatal() SourceSite mcpl_particle_to_site(const mcpl_particle_repr_t* particle_repr) { SourceSite site; - switch (particle_repr->pdgcode) { - case 2112: - site.particle = ParticleType::neutron; - break; - case 22: - site.particle = ParticleType::photon; - break; - case 11: - site.particle = ParticleType::electron; - break; - case -11: - site.particle = ParticleType::positron; - break; - default: - fatal_error(fmt::format( - "MCPL: Encountered unexpected PDG code {} when converting to SourceSite.", - particle_repr->pdgcode)); - break; - } + site.particle = ParticleType {particle_repr->pdgcode}; // Copy position and direction site.r.x = particle_repr->position[0]; @@ -368,7 +350,6 @@ vector mcpl_source_sites(std::string path) } size_t n_particles_in_file = g_mcpl_api->hdr_nparticles(mcpl_file); - size_t n_skipped = 0; if (n_particles_in_file > 0) { sites.reserve(n_particles_in_file); } @@ -381,31 +362,16 @@ vector mcpl_source_sites(std::string path) path, sites.size(), n_particles_in_file)); break; } - if (p_repr->pdgcode == 2112 || p_repr->pdgcode == 22 || - p_repr->pdgcode == 11 || p_repr->pdgcode == -11) { - sites.push_back(mcpl_particle_to_site(p_repr)); - } else { - n_skipped++; - } + sites.push_back(mcpl_particle_to_site(p_repr)); } g_mcpl_api->close_file(mcpl_file); - if (n_skipped > 0 && n_particles_in_file > 0) { - double percent_skipped = - 100.0 * static_cast(n_skipped) / n_particles_in_file; - warning(fmt::format( - "MCPL: Skipped {} of {} total particles ({:.1f}%) in file '{}' because " - "their type is not supported by OpenMC.", - n_skipped, n_particles_in_file, percent_skipped, path)); - } - if (sites.empty()) { if (n_particles_in_file > 0) { fatal_error(fmt::format( - "MCPL file '{}' contained {} particles, but none were of the supported " - "types (neutron, photon, electron, positron). OpenMC cannot proceed " - "without source particles.", + "MCPL file '{}' contained {} particles, but no particles could be " + "read.", path, n_particles_in_file)); } else { fatal_error(fmt::format( @@ -461,22 +427,7 @@ void write_mcpl_source_bank_internal(mcpl_outfile_t* file_id, p_repr.ekin = site.E * 1e-6; p_repr.time = site.time * 1e3; p_repr.weight = site.wgt; - switch (site.particle) { - case ParticleType::neutron: - p_repr.pdgcode = 2112; - break; - case ParticleType::photon: - p_repr.pdgcode = 22; - break; - case ParticleType::electron: - p_repr.pdgcode = 11; - break; - case ParticleType::positron: - p_repr.pdgcode = -11; - break; - default: - continue; - } + p_repr.pdgcode = site.particle.pdg_number(); g_mcpl_api->add_particle(file_id, &p_repr); } } @@ -633,22 +584,7 @@ void write_mcpl_collision_track_internal(mcpl_outfile_t* file_id, p_repr.ekin = site.E * 1e-6; p_repr.time = site.time * 1e3; p_repr.weight = site.wgt; - switch (site.particle) { - case ParticleType::neutron: - p_repr.pdgcode = 2112; - break; - case ParticleType::photon: - p_repr.pdgcode = 22; - break; - case ParticleType::electron: - p_repr.pdgcode = 11; - break; - case ParticleType::positron: - p_repr.pdgcode = -11; - break; - default: - continue; - } + p_repr.pdgcode = site.particle.pdg_number(); g_mcpl_api->add_particle(file_id, &p_repr); } } else { diff --git a/src/mesh.cpp b/src/mesh.cpp index 0b45774ff..a72b1441f 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -486,7 +486,7 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, SourceSite site; site.E = 1.0; - site.particle = ParticleType::neutron; + site.particle = ParticleType::neutron(); for (int axis = 0; axis < 3; ++axis) { // Set starting position and direction diff --git a/src/mgxs_interface.cpp b/src/mgxs_interface.cpp index ed734d401..34f87d179 100644 --- a/src/mgxs_interface.cpp +++ b/src/mgxs_interface.cpp @@ -244,7 +244,7 @@ void MgxsInterface::read_header(const std::string& path_cross_sections) void put_mgxs_header_data_to_globals() { // Get the minimum and maximum energies - int neutron = static_cast(ParticleType::neutron); + int neutron = ParticleType::neutron().transport_index(); data::energy_min[neutron] = data::mg.energy_bins_.back(); data::energy_max[neutron] = data::mg.energy_bins_.front(); diff --git a/src/nuclide.cpp b/src/nuclide.cpp index 5ae6e30ee..69e603a7c 100644 --- a/src/nuclide.cpp +++ b/src/nuclide.cpp @@ -379,7 +379,7 @@ void Nuclide::create_derived( auto pprod = xt::view(xs_[t], xt::range(j, j + n), XS_PHOTON_PROD); for (const auto& p : rx->products_) { - if (p.particle_ == ParticleType::photon) { + if (p.particle_.is_photon()) { for (int k = 0; k < n; ++k) { double E = grid_[t].energy[k + j]; @@ -501,7 +501,7 @@ void Nuclide::create_derived( void Nuclide::init_grid() { - int neutron = static_cast(ParticleType::neutron); + int neutron = ParticleType::neutron().transport_index(); double E_min = data::energy_min[neutron]; double E_max = data::energy_max[neutron]; int M = settings::n_log_bins; @@ -552,7 +552,7 @@ double Nuclide::nu(double E, EmissionMode mode, int group) const for (int i = 1; i < rx->products_.size(); ++i) { // Skip any non-neutron products const auto& product = rx->products_[i]; - if (product.particle_ != ParticleType::neutron) + if (!product.particle_.is_neutron()) continue; // Evaluate yield diff --git a/src/output.cpp b/src/output.cpp index 80e2b10ab..ae2daaffc 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -155,21 +155,21 @@ std::string time_stamp() void print_particle(Particle& p) { // Display particle type and ID. - switch (p.type()) { - case ParticleType::neutron: + switch (p.type().pdg_number()) { + case PDG_NEUTRON: fmt::print("Neutron "); break; - case ParticleType::photon: + case PDG_PHOTON: fmt::print("Photon "); break; - case ParticleType::electron: + case PDG_ELECTRON: fmt::print("Electron "); break; - case ParticleType::positron: + case PDG_POSITRON: fmt::print("Positron "); break; default: - fmt::print("Unknown Particle "); + fmt::print("Particle {} ", p.type().str()); } fmt::print("{}\n", p.id()); diff --git a/src/particle.cpp b/src/particle.cpp index b9f9c8f86..a1176abc7 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -48,17 +48,19 @@ double Particle::speed() const if (settings::run_CE) { // Determine mass in eV/c^2 double mass; - switch (this->type()) { - case ParticleType::neutron: + switch (this->type().pdg_number()) { + case PDG_NEUTRON: mass = MASS_NEUTRON_EV; break; - case ParticleType::photon: + case PDG_PHOTON: mass = 0.0; break; - case ParticleType::electron: - case ParticleType::positron: + case PDG_ELECTRON: + case PDG_POSITRON: mass = MASS_ELECTRON_EV; break; + default: + fatal_error("Unsupported particle for speed calculation."); } // Equivalent to C * sqrt(1-(m/(m+E))^2) without problem at E<E() * (this->E() + 2 * mass)) / @@ -77,7 +79,11 @@ bool Particle::create_secondary( { // If energy is below cutoff for this particle, don't create secondary // particle - if (E < settings::energy_cutoff[static_cast(type)]) { + int idx = type.transport_index(); + if (idx == C_NONE) { + return false; + } + if (E < settings::energy_cutoff[idx]) { return false; } @@ -235,7 +241,8 @@ void Particle::event_advance() boundary() = distance_to_boundary(*this); // Sample a distance to collision - if (type() == ParticleType::electron || type() == ParticleType::positron) { + if (type() == ParticleType::electron() || + type() == ParticleType::positron()) { collision_distance() = material() == MATERIAL_VOID ? INFINITY : 0.0; } else if (macro_xs().total == 0.0) { collision_distance() = INFINITY; @@ -244,7 +251,7 @@ void Particle::event_advance() } double speed = this->speed(); - double time_cutoff = settings::time_cutoff[static_cast(type())]; + double time_cutoff = settings::time_cutoff[type().transport_index()]; double distance_cutoff = (time_cutoff < INFTY) ? (time_cutoff - time()) * speed : INFTY; @@ -269,8 +276,7 @@ void Particle::event_advance() } // Score track-length estimate of k-eff - if (settings::run_mode == RunMode::EIGENVALUE && - type() == ParticleType::neutron) { + if (settings::run_mode == RunMode::EIGENVALUE && type().is_neutron()) { keff_tally_tracklength() += wgt() * distance * macro_xs().nu_fission; } @@ -331,8 +337,7 @@ void Particle::event_cross_surface() void Particle::event_collide() { // Score collision estimate of keff - if (settings::run_mode == RunMode::EIGENVALUE && - type() == ParticleType::neutron) { + if (settings::run_mode == RunMode::EIGENVALUE && type().is_neutron()) { keff_tally_collision() += wgt() * macro_xs().nu_fission / macro_xs().total; } @@ -370,8 +375,7 @@ void Particle::event_collide() } } - if (!model::active_pulse_height_tallies.empty() && - type() == ParticleType::photon) { + if (!model::active_pulse_height_tallies.empty() && type().is_photon()) { pht_collision_energy(); } @@ -442,7 +446,7 @@ void Particle::event_revive_from_secondary() // Subtract secondary particle energy from interim pulse-height results if (!model::active_pulse_height_tallies.empty() && - this->type() == ParticleType::photon) { + this->type().is_photon()) { // Since the birth cell of the particle has not been set we // have to determine it before the energy of the secondary particle can be // removed from the pulse-height of this cell. @@ -525,7 +529,7 @@ void Particle::pht_collision_energy() // If the energy of the particle is below the cutoff, it will not be sampled // so its energy is added to the pulse-height in the cell - int photon = static_cast(ParticleType::photon); + int photon = ParticleType::photon().transport_index(); if (E() < settings::energy_cutoff[photon]) { pht_storage()[index] += E(); } @@ -824,7 +828,7 @@ void Particle::write_restart() const break; } write_dataset(file_id, "id", id()); - write_dataset(file_id, "type", static_cast(type())); + write_dataset(file_id, "type", type().pdg_number()); int64_t i = current_work(); if (settings::run_mode == RunMode::EIGENVALUE) { @@ -878,37 +882,6 @@ void Particle::update_neutron_xs( //============================================================================== // Non-method functions //============================================================================== - -std::string particle_type_to_str(ParticleType type) -{ - switch (type) { - case ParticleType::neutron: - return "neutron"; - case ParticleType::photon: - return "photon"; - case ParticleType::electron: - return "electron"; - case ParticleType::positron: - return "positron"; - } - UNREACHABLE(); -} - -ParticleType str_to_particle_type(std::string str) -{ - if (str == "neutron") { - return ParticleType::neutron; - } else if (str == "photon") { - return ParticleType::photon; - } else if (str == "electron") { - return ParticleType::electron; - } else if (str == "positron") { - return ParticleType::positron; - } else { - throw std::invalid_argument {fmt::format("Invalid particle name: {}", str)}; - } -} - void add_surf_source_to_bank(Particle& p, const Surface& surf) { if (simulation::current_batch <= settings::n_inactive || diff --git a/src/particle_restart.cpp b/src/particle_restart.cpp index 6b7778211..f02fcb94b 100644 --- a/src/particle_restart.cpp +++ b/src/particle_restart.cpp @@ -31,6 +31,16 @@ void read_particle_restart(Particle& p, RunMode& previous_run_mode) hid_t file_id = file_open(settings::path_particle_restart, 'r'); // Read data from file + bool legacy_particle_codes = true; + if (attribute_exists(file_id, "version")) { + array version; + read_attribute(file_id, "version", version); + if (version[0] > VERSION_PARTICLE_RESTART[0] || + (version[0] == VERSION_PARTICLE_RESTART[0] && version[1] >= 1)) { + legacy_particle_codes = false; + } + } + read_dataset(file_id, "current_batch", simulation::current_batch); read_dataset(file_id, "generations_per_batch", settings::gen_per_batch); read_dataset(file_id, "current_generation", simulation::current_gen); @@ -45,7 +55,8 @@ void read_particle_restart(Particle& p, RunMode& previous_run_mode) read_dataset(file_id, "id", p.id()); int type; read_dataset(file_id, "type", type); - p.type() = static_cast(type); + p.type() = legacy_particle_codes ? legacy_particle_index_to_type(type) + : ParticleType {type}; read_dataset(file_id, "weight", p.wgt()); read_dataset(file_id, "energy", p.E()); read_dataset(file_id, "xyz", p.r()); diff --git a/src/particle_type.cpp b/src/particle_type.cpp new file mode 100644 index 000000000..fe8f9fe2a --- /dev/null +++ b/src/particle_type.cpp @@ -0,0 +1,246 @@ +#include "openmc/particle_type.h" + +#include +#include +#include + +#include "openmc/string_utils.h" + +namespace openmc { +namespace { + +constexpr const char* ATOMIC_SYMBOL[] = {"", "H", "He", "Li", "Be", "B", "C", + "N", "O", "F", "Ne", "Na", "Mg", "Al", "Si", "P", "S", "Cl", "Ar", "K", "Ca", + "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni", "Cu", "Zn", "Ga", "Ge", "As", + "Se", "Br", "Kr", "Rb", "Sr", "Y", "Zr", "Nb", "Mo", "Tc", "Ru", "Rh", "Pd", + "Ag", "Cd", "In", "Sn", "Sb", "Te", "I", "Xe", "Cs", "Ba", "La", "Ce", "Pr", + "Nd", "Pm", "Sm", "Eu", "Gd", "Tb", "Dy", "Ho", "Er", "Tm", "Yb", "Lu", "Hf", + "Ta", "W", "Re", "Os", "Ir", "Pt", "Au", "Hg", "Tl", "Pb", "Bi", "Po", "At", + "Rn", "Fr", "Ra", "Ac", "Th", "Pa", "U", "Np", "Pu", "Am", "Cm", "Bk", "Cf", + "Es", "Fm", "Md", "No", "Lr", "Rf", "Db", "Sg", "Bh", "Hs", "Mt", "Ds", "Rg", + "Cn", "Nh", "Fl", "Mc", "Lv", "Ts", "Og"}; + +constexpr int MAX_Z = + static_cast(sizeof(ATOMIC_SYMBOL) / sizeof(ATOMIC_SYMBOL[0])) - 1; + +bool is_integer_string(const std::string& s) +{ + if (s.empty()) + return false; + size_t i = 0; + if (s[0] == '-' || s[0] == '+') { + if (s.size() == 1) + return false; + i = 1; + } + for (; i < s.size(); ++i) { + if (!std::isdigit(static_cast(s[i]))) + return false; + } + return true; +} + +int atomic_number_from_symbol(std::string_view symbol) +{ + for (int z = 1; z <= MAX_Z; ++z) { + if (symbol == ATOMIC_SYMBOL[z]) { + return z; + } + } + return 0; +} + +bool parse_gnds_nuclide(std::string_view name, int& Z, int& A, int& m) +{ + if (name.empty()) + return false; + + size_t pos = 0; + if (!std::isupper(static_cast(name[pos]))) + return false; + + std::string symbol; + symbol += name[pos++]; + if (pos < name.size() && + std::islower(static_cast(name[pos]))) { + symbol += name[pos++]; + } + + if (pos >= name.size() || + !std::isdigit(static_cast(name[pos]))) { + return false; + } + + size_t a_start = pos; + while ( + pos < name.size() && std::isdigit(static_cast(name[pos]))) { + ++pos; + } + A = std::stoi(std::string {name.substr(a_start, pos - a_start)}); + if (A <= 0 || A > 999) + return false; + + m = 0; + if (pos < name.size()) { + if (name[pos] != '_' || pos + 2 >= name.size() || name[pos + 1] != 'm') { + return false; + } + pos += 2; + size_t m_start = pos; + while (pos < name.size() && + std::isdigit(static_cast(name[pos]))) { + ++pos; + } + if (m_start == pos) + return false; + m = std::stoi(std::string {name.substr(m_start, pos - m_start)}); + if (m < 0 || m > 9) + return false; + } + + if (pos != name.size()) + return false; + + Z = atomic_number_from_symbol(symbol); + return Z != 0; +} + +// Helper to convert nuclear PDG number to nuclide name +std::string nuclide_name_from_pdg(int32_t pdg) +{ + int32_t code = pdg; + int m = code % 10; + int A = (code / 10) % 1000; + int Z = (code / 10000) % 1000; + + if (Z <= 0 || Z > MAX_Z || A <= 0 || A > 999) { + throw std::invalid_argument { + "Invalid nuclear PDG number: " + std::to_string(pdg)}; + } + + std::string name = ATOMIC_SYMBOL[Z] + std::to_string(A); + if (m > 0) { + name += "_m" + std::to_string(m); + } + return name; +} + +} // namespace + +//============================================================================== +// ParticleType member function implementations +//============================================================================== + +ParticleType::ParticleType(std::string_view str) +{ + std::string s {str}; + strtrim(s); + if (s.empty()) { + throw std::invalid_argument {"Particle string is empty."}; + } + + std::string lower = s; + to_lower(lower); + + // Check for pdg: prefix + if (starts_with(lower, "pdg:")) { + std::string value_str = lower.substr(4); + if (!is_integer_string(value_str)) { + throw std::invalid_argument {"Invalid PDG number: " + value_str}; + } + pdg_number_ = std::stoi(value_str); + return; + } + + // Check for known particle names + if (lower == "neutron" || lower == "n") { + pdg_number_ = PDG_NEUTRON; + return; + } + if (lower == "photon" || lower == "gamma") { + pdg_number_ = PDG_PHOTON; + return; + } + if (lower == "electron") { + pdg_number_ = PDG_ELECTRON; + return; + } + if (lower == "positron") { + pdg_number_ = PDG_POSITRON; + return; + } + if (lower == "proton" || lower == "p" || lower == "h1") { + pdg_number_ = PDG_PROTON; + return; + } + if (lower == "deuteron" || lower == "d" || lower == "h2") { + pdg_number_ = PDG_DEUTERON; + return; + } + if (lower == "triton" || lower == "t" || lower == "h3") { + pdg_number_ = PDG_TRITON; + return; + } + if (lower == "alpha" || lower == "he4") { + pdg_number_ = PDG_ALPHA; + return; + } + + // Check for integer string + if (is_integer_string(s)) { + pdg_number_ = std::stoi(s); + return; + } + + // Try to parse as GNDS nuclide name + int Z = 0; + int A = 0; + int m = 0; + if (!parse_gnds_nuclide(s, Z, A, m)) { + throw std::invalid_argument {"Invalid nuclide name: " + s}; + } + pdg_number_ = 1000000000 + Z * 10000 + A * 10 + m; +} + +std::string ParticleType::str() const +{ + if (pdg_number_ == PDG_NEUTRON) + return "neutron"; + if (pdg_number_ == PDG_PHOTON) + return "photon"; + if (pdg_number_ == PDG_ELECTRON) + return "electron"; + if (pdg_number_ == PDG_POSITRON) + return "positron"; + if (pdg_number_ == PDG_PROTON) + return "proton"; + + if (is_nucleus()) { + return nuclide_name_from_pdg(pdg_number_); + } + + return "pdg:" + std::to_string(pdg_number_); +} + +//============================================================================== +// Free function implementations +//============================================================================== + +ParticleType legacy_particle_index_to_type(int index) +{ + switch (index) { + case 0: + return ParticleType {PDG_NEUTRON}; + case 1: + return ParticleType {PDG_PHOTON}; + case 2: + return ParticleType {PDG_ELECTRON}; + case 3: + return ParticleType {PDG_POSITRON}; + default: + throw std::invalid_argument { + "Invalid legacy particle index: " + std::to_string(index)}; + } +} + +} // namespace openmc diff --git a/src/photon.cpp b/src/photon.cpp index 4926e3eae..951acb9fb 100644 --- a/src/photon.cpp +++ b/src/photon.cpp @@ -293,7 +293,7 @@ PhotonInteraction::PhotonInteraction(hid_t group) close_group(rgroup); // Truncate the bremsstrahlung data at the cutoff energy - int photon = static_cast(ParticleType::photon); + int photon = ParticleType::photon().transport_index(); const auto& E {electron_energy}; double cutoff = settings::energy_cutoff[photon]; if (cutoff > E(0)) { @@ -805,7 +805,7 @@ void PhotonInteraction::atomic_relaxation(int i_shell, Particle& p) const if (shell.transitions.empty()) { Direction u = isotropic_direction(p.current_seed()); double E = shell.binding_energy; - p.create_secondary(p.wgt(), u, E, ParticleType::photon); + p.create_secondary(p.wgt(), u, E, ParticleType::photon()); continue; } @@ -833,12 +833,13 @@ void PhotonInteraction::atomic_relaxation(int i_shell, Particle& p) const holes[n_holes++] = transition.secondary_subshell; // Create auger electron - p.create_secondary(p.wgt(), u, transition.energy, ParticleType::electron); + p.create_secondary( + p.wgt(), u, transition.energy, ParticleType::electron()); } else { // Radiative transition -- get X-ray energy // Create fluorescent photon - p.create_secondary(p.wgt(), u, transition.energy, ParticleType::photon); + p.create_secondary(p.wgt(), u, transition.energy, ParticleType::photon()); } } } diff --git a/src/physics.cpp b/src/physics.cpp index 41509af97..05a9e59d9 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -46,27 +46,29 @@ void collision(Particle& p) ++(p.n_collision()); // Sample reaction for the material the particle is in - switch (p.type()) { - case ParticleType::neutron: + switch (p.type().pdg_number()) { + case PDG_NEUTRON: sample_neutron_reaction(p); break; - case ParticleType::photon: + case PDG_PHOTON: sample_photon_reaction(p); break; - case ParticleType::electron: + case PDG_ELECTRON: sample_electron_reaction(p); break; - case ParticleType::positron: + case PDG_POSITRON: sample_positron_reaction(p); break; + default: + fatal_error("Unsupported particle PDG for collision sampling."); } if (settings::weight_window_checkpoint_collision) apply_weight_windows(p); // Kill particle if energy falls below cutoff - int type = static_cast(p.type()); - if (p.E() < settings::energy_cutoff[type]) { + int type = p.type().transport_index(); + if (type != C_NONE && p.E() < settings::energy_cutoff[type]) { p.wgt() = 0.0; } @@ -75,11 +77,11 @@ void collision(Particle& p) std::string msg; if (p.event() == TallyEvent::KILL) { msg = fmt::format(" Killed. Energy = {} eV.", p.E()); - } else if (p.type() == ParticleType::neutron) { + } else if (p.type().is_neutron()) { msg = fmt::format(" {} with {}. Energy = {} eV.", reaction_name(p.event_mt()), data::nuclides[p.event_nuclide()]->name_, p.E()); - } else if (p.type() == ParticleType::photon) { + } else if (p.type().is_photon()) { msg = fmt::format(" {} with {}. Energy = {} eV.", reaction_name(p.event_mt()), to_element(data::nuclides[p.event_nuclide()]->name_), p.E()); @@ -208,7 +210,7 @@ void create_fission_sites(Particle& p, int i_nuclide, const Reaction& rx) // Initialize fission site object with particle data SourceSite site; site.r = p.r(); - site.particle = ParticleType::neutron; + site.particle = ParticleType::neutron(); site.time = p.time(); site.wgt = 1. / weight; site.surf_id = 0; @@ -218,7 +220,7 @@ void create_fission_sites(Particle& p, int i_nuclide, const Reaction& rx) // Reject site if it exceeds time cutoff if (site.delayed_group > 0) { - double t_cutoff = settings::time_cutoff[static_cast(site.particle)]; + double t_cutoff = settings::time_cutoff[site.particle.transport_index()]; if (site.time > t_cutoff) { continue; } @@ -289,7 +291,7 @@ void sample_photon_reaction(Particle& p) // Kill photon if below energy cutoff -- an extra check is made here because // photons with energy below the cutoff may have been produced by neutrons // reactions or atomic relaxation - int photon = static_cast(ParticleType::photon); + int photon = ParticleType::photon().transport_index(); if (p.E() < settings::energy_cutoff[photon]) { p.E() = 0.0; p.wgt() = 0.0; @@ -339,13 +341,13 @@ void sample_photon_reaction(Particle& p) // Create Compton electron double phi = uniform_distribution(0., 2.0 * PI, p.current_seed()); double E_electron = (alpha - alpha_out) * MASS_ELECTRON_EV - e_b; - int electron = static_cast(ParticleType::electron); + int electron = ParticleType::electron().transport_index(); if (E_electron >= settings::energy_cutoff[electron]) { double mu_electron = (alpha - alpha_out * p.mu()) / std::sqrt(alpha * alpha + alpha_out * alpha_out - 2.0 * alpha * alpha_out * p.mu()); Direction u = rotate_angle(p.u(), mu_electron, &phi, p.current_seed()); - p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron); + p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron()); } // Allow electrons to fill orbital and produce Auger electrons and @@ -418,7 +420,7 @@ void sample_photon_reaction(Particle& p) u.z = std::sqrt(1.0 - mu * mu) * std::sin(phi); // Create secondary electron - p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron); + p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron()); // Allow electrons to fill orbital and produce auger electrons // and fluorescent photons @@ -443,12 +445,11 @@ void sample_photon_reaction(Particle& p) // Create secondary electron Direction u = rotate_angle(p.u(), mu_electron, nullptr, p.current_seed()); - p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron); + p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron()); // Create secondary positron u = rotate_angle(p.u(), mu_positron, nullptr, p.current_seed()); - p.create_secondary(p.wgt(), u, E_positron, ParticleType::positron); - + p.create_secondary(p.wgt(), u, E_positron, ParticleType::positron()); p.event() = TallyEvent::ABSORB; p.event_mt() = PAIR_PROD; p.wgt() = 0.0; @@ -483,8 +484,8 @@ void sample_positron_reaction(Particle& p) Direction u = isotropic_direction(p.current_seed()); // Create annihilation photon pair traveling in opposite directions - p.create_secondary(p.wgt(), u, MASS_ELECTRON_EV, ParticleType::photon); - p.create_secondary(p.wgt(), -u, MASS_ELECTRON_EV, ParticleType::photon); + p.create_secondary(p.wgt(), u, MASS_ELECTRON_EV, ParticleType::photon()); + p.create_secondary(p.wgt(), -u, MASS_ELECTRON_EV, ParticleType::photon()); p.E() = 0.0; p.wgt() = 0.0; @@ -609,7 +610,7 @@ void sample_photon_product( continue; for (int j = 0; j < rx->products_.size(); ++j) { - if (rx->products_[j].particle_ == ParticleType::photon) { + if (rx->products_[j].particle_.is_photon()) { // For fission, artificially increase the photon yield to account // for delayed photons double f = 1.0; @@ -1098,7 +1099,7 @@ void sample_fission_neutron( rx.products_[site->delayed_group].sample(E_in, site->E, mu, seed); // resample if energy is greater than maximum neutron energy - constexpr int neutron = static_cast(ParticleType::neutron); + int neutron = ParticleType::neutron().transport_index(); if (site->E < data::energy_max[neutron]) break; @@ -1158,7 +1159,7 @@ void inelastic_scatter(const Nuclide& nuc, const Reaction& rx, Particle& p) if (std::floor(yield) == yield && yield > 0) { // If yield is integral, create exactly that many secondary particles for (int i = 0; i < static_cast(std::round(yield)) - 1; ++i) { - p.create_secondary(p.wgt(), p.u(), p.E(), ParticleType::neutron); + p.create_secondary(p.wgt(), p.u(), p.E(), ParticleType::neutron()); } } else { // Otherwise, change weight of particle based on yield @@ -1214,7 +1215,7 @@ void sample_secondary_photons(Particle& p, int i_nuclide) } // Create the secondary photon - bool created_photon = p.create_secondary(wgt, u, E, ParticleType::photon); + bool created_photon = p.create_secondary(wgt, u, E, ParticleType::photon()); // Tag secondary particle with parent nuclide if (created_photon && settings::use_decay_photons) { diff --git a/src/physics_mg.cpp b/src/physics_mg.cpp index 4c28cb179..866d4d728 100644 --- a/src/physics_mg.cpp +++ b/src/physics_mg.cpp @@ -136,7 +136,7 @@ void create_fission_sites(Particle& p) // Initialize fission site object with particle data SourceSite site; site.r = p.r(); - site.particle = ParticleType::neutron; + site.particle = ParticleType::neutron(); site.time = p.time(); site.wgt = 1. / weight; @@ -171,7 +171,7 @@ void create_fission_sites(Particle& p) site.time -= std::log(prn(p.current_seed())) / decay_rate; // Reject site if it exceeds time cutoff - double t_cutoff = settings::time_cutoff[static_cast(site.particle)]; + double t_cutoff = settings::time_cutoff[site.particle.transport_index()]; if (site.time > t_cutoff) { continue; } diff --git a/src/reaction.cpp b/src/reaction.cpp index d96790c6d..9ac5a1f52 100644 --- a/src/reaction.cpp +++ b/src/reaction.cpp @@ -70,9 +70,8 @@ Reaction::Reaction( if (settings::use_decay_photons) { // Remove photon products for D1S method - products_.erase( - std::remove_if(products_.begin(), products_.end(), - [](const auto& p) { return p.particle_ == ParticleType::photon; }), + products_.erase(std::remove_if(products_.begin(), products_.end(), + [](const auto& p) { return p.particle_.is_photon(); }), products_.end()); // Determine product for D1S method diff --git a/src/reaction_product.cpp b/src/reaction_product.cpp index 3ba2c0cfb..ee560d607 100644 --- a/src/reaction_product.cpp +++ b/src/reaction_product.cpp @@ -26,7 +26,7 @@ ReactionProduct::ReactionProduct(hid_t group) // Read particle type std::string temp; read_attribute(group, "particle", temp); - particle_ = str_to_particle_type(temp); + particle_ = ParticleType {temp}; // Read emission mode and decay rate read_attribute(group, "emission_mode", temp); @@ -42,7 +42,7 @@ ReactionProduct::ReactionProduct(hid_t group) if (emission_mode_ == EmissionMode::delayed) { if (attribute_exists(group, "decay_rate")) { read_attribute(group, "decay_rate", decay_rate_); - } else if (particle_ == ParticleType::neutron) { + } else if (particle_.is_neutron()) { warning(fmt::format("Decay rate doesn't exist for delayed neutron " "emission ({}).", object_name(group))); @@ -85,7 +85,7 @@ ReactionProduct::ReactionProduct(hid_t group) ReactionProduct::ReactionProduct(const ChainNuclide::Product& product) { - particle_ = ParticleType::photon; + particle_ = ParticleType::photon(); emission_mode_ = EmissionMode::delayed; // Get chain nuclide object for radionuclide diff --git a/src/simulation.cpp b/src/simulation.cpp index b536ae588..18e40a8bc 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -687,7 +687,7 @@ void initialize_data() for (const auto& nuc : data::nuclides) { if (nuc->grid_.size() >= 1) { - int neutron = static_cast(ParticleType::neutron); + int neutron = ParticleType::neutron().transport_index(); data::energy_min[neutron] = std::max(data::energy_min[neutron], nuc->grid_[0].energy.front()); data::energy_max[neutron] = @@ -698,7 +698,7 @@ void initialize_data() if (settings::photon_transport) { for (const auto& elem : data::elements) { if (elem->energy_.size() >= 1) { - int photon = static_cast(ParticleType::photon); + int photon = ParticleType::photon().transport_index(); int n = elem->energy_.size(); data::energy_min[photon] = std::max(data::energy_min[photon], std::exp(elem->energy_(1))); @@ -711,9 +711,9 @@ void initialize_data() // Determine if minimum/maximum energy for bremsstrahlung is greater/less // than the current minimum/maximum if (data::ttb_e_grid.size() >= 1) { - int photon = static_cast(ParticleType::photon); - int electron = static_cast(ParticleType::electron); - int positron = static_cast(ParticleType::positron); + int photon = ParticleType::photon().transport_index(); + int electron = ParticleType::electron().transport_index(); + int positron = ParticleType::positron().transport_index(); int n_e = data::ttb_e_grid.size(); const std::vector charged = {electron, positron}; @@ -737,7 +737,7 @@ void initialize_data() // grid has not been allocated if (nuc->grid_.size() > 0) { double max_E = nuc->grid_[0].energy.back(); - int neutron = static_cast(ParticleType::neutron); + int neutron = ParticleType::neutron().transport_index(); if (max_E == data::energy_max[neutron]) { write_message(7, "Maximum neutron transport energy: {} eV for {}", data::energy_max[neutron], nuc->name_); @@ -754,7 +754,7 @@ void initialize_data() for (auto& nuc : data::nuclides) { nuc->init_grid(); } - int neutron = static_cast(ParticleType::neutron); + int neutron = ParticleType::neutron().transport_index(); simulation::log_spacing = std::log(data::energy_max[neutron] / data::energy_min[neutron]) / settings::n_log_bins; diff --git a/src/source.cpp b/src/source.cpp index b30b964d3..8bd9c7893 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -37,6 +37,20 @@ namespace openmc { +namespace { + +void validate_particle_type(ParticleType type, const std::string& context) +{ + if (type.is_transportable()) + return; + + fatal_error( + fmt::format("Unsupported source particle type '{}' (PDG {}) in {}.", + type.str(), type.pdg_number(), context)); +} + +} // namespace + //============================================================================== // Global variables //============================================================================== @@ -284,22 +298,15 @@ IndependentSource::IndependentSource(pugi::xml_node node) : Source(node) { // Check for particle type if (check_for_node(node, "particle")) { - auto temp_str = get_node_value(node, "particle", true, true); - if (temp_str == "neutron") { - particle_ = ParticleType::neutron; - } else if (temp_str == "photon") { - particle_ = ParticleType::photon; + auto temp_str = get_node_value(node, "particle", false, true); + particle_ = ParticleType(temp_str); + if (particle_ == ParticleType::photon() || + particle_ == ParticleType::electron() || + particle_ == ParticleType::positron()) { settings::photon_transport = true; - } else if (temp_str == "electron") { - particle_ = ParticleType::electron; - settings::photon_transport = true; - } else if (temp_str == "positron") { - particle_ = ParticleType::positron; - settings::photon_transport = true; - } else { - fatal_error(std::string("Unknown source particle type: ") + temp_str); } } + validate_particle_type(particle_, "IndependentSource"); // Check for external source file if (check_for_node(node, "file")) { @@ -390,7 +397,7 @@ SourceSite IndependentSource::sample(uint64_t* seed) const // Sample energy and time for neutron and photon sources if (settings::solver_type != SolverType::RANDOM_RAY) { // Check for monoenergetic source above maximum particle energy - auto p = static_cast(particle_); + auto p = particle_.transport_index(); auto energy_ptr = dynamic_cast(energy_.get()); if (energy_ptr) { auto energies = xt::adapt(energy_ptr->x()); @@ -472,6 +479,11 @@ void FileSource::load_sites_from_file(const std::string& path) // Close file file_close(file_id); } + + // Make sure particles in source file have valid types + for (const auto& site : this->sites_) { + validate_particle_type(site.particle, "FileSource"); + } } SourceSite FileSource::sample(uint64_t* seed) const @@ -585,6 +597,11 @@ MeshSource::MeshSource(pugi::xml_node node) : Source(node) std::make_unique(mesh_idx, elem_index)); } + // Make sure sources use valid particle types + for (const auto& src : sources_) { + validate_particle_type(src->particle_type(), "MeshSource"); + } + // the number of source distributions should either be one or equal to the // number of mesh elements if (sources_.size() > 1 && sources_.size() != mesh->n_bins()) { diff --git a/src/state_point.cpp b/src/state_point.cpp index 8ccebeb05..455299529 100644 --- a/src/state_point.cpp +++ b/src/state_point.cpp @@ -22,6 +22,7 @@ #include "openmc/mgxs_interface.h" #include "openmc/nuclide.h" #include "openmc/output.h" +#include "openmc/particle_type.h" #include "openmc/settings.h" #include "openmc/simulation.h" #include "openmc/tallies/derivative.h" @@ -632,6 +633,7 @@ void write_h5_source_point(const char* filename, span source_bank, if (mpi::master || parallel) { file_id = file_open(filename_.c_str(), 'w', true); write_attribute(file_id, "filetype", "source"); + write_attribute(file_id, "version", VERSION_STATEPOINT); } // Get pointer to source bank and write to file @@ -677,6 +679,16 @@ std::string dtype_member_names(hid_t dtype_id) void read_source_bank( hid_t group_id, vector& sites, bool distribute) { + bool legacy_particle_codes = true; + if (attribute_exists(group_id, "version")) { + array version; + read_attribute(group_id, "version", version); + if (version[0] > VERSION_STATEPOINT[0] || + (version[0] == VERSION_STATEPOINT[0] && version[1] >= 2)) { + legacy_particle_codes = false; + } + } + hid_t banktype = h5banktype(true); // Open the dataset @@ -738,6 +750,12 @@ void read_source_bank( H5Sclose(memspace); H5Dclose(dset); H5Tclose(banktype); + + if (legacy_particle_codes) { + for (auto& site : sites) { + site.particle = legacy_particle_index_to_type(site.particle.pdg_number()); + } + } } void write_unstructured_mesh_results() diff --git a/src/tallies/filter_particle.cpp b/src/tallies/filter_particle.cpp index eef1d1e63..031068f3a 100644 --- a/src/tallies/filter_particle.cpp +++ b/src/tallies/filter_particle.cpp @@ -13,7 +13,7 @@ void ParticleFilter::from_xml(pugi::xml_node node) // Convert to vector of ParticleType vector types; for (auto& p : particles) { - types.push_back(str_to_particle_type(p)); + types.emplace_back(p); } this->set_particles(types); } @@ -47,7 +47,7 @@ void ParticleFilter::to_statepoint(hid_t filter_group) const Filter::to_statepoint(filter_group); vector particles; for (auto p : particles_) { - particles.push_back(particle_type_to_str(p)); + particles.push_back(p.str()); } write_dataset(filter_group, "bins", particles); } @@ -55,10 +55,10 @@ void ParticleFilter::to_statepoint(hid_t filter_group) const std::string ParticleFilter::text_label(int bin) const { const auto& p = particles_.at(bin); - return fmt::format("Particle: {}", particle_type_to_str(p)); + return fmt::format("Particle: {}", p.str()); } -extern "C" int openmc_particle_filter_get_bins(int32_t idx, int bins[]) +extern "C" int openmc_particle_filter_get_bins(int32_t idx, int32_t bins[]) { if (int err = verify_filter(idx)) return err; @@ -68,7 +68,7 @@ extern "C" int openmc_particle_filter_get_bins(int32_t idx, int bins[]) if (pf) { const auto& particles = pf->particles(); for (int i = 0; i < particles.size(); i++) { - bins[i] = static_cast(particles[i]); + bins[i] = particles[i].pdg_number(); } } else { set_errmsg("The filter at the specified index is not a ParticleFilter"); diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index 6eef1da9c..2432f5c2f 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -289,12 +289,12 @@ Tally::Tally(pugi::xml_node node) const auto& f = model::tally_filters[particle_filter_index].get(); auto pf = dynamic_cast(f); for (auto p : pf->particles()) { - if (p != ParticleType::neutron) { + if (!p.is_neutron()) { warning(fmt::format( "Particle filter other than NEUTRON used with " "photon transport turned off. All tallies for particle type {}" " will have no scores", - static_cast(p))); + p.str())); } } } diff --git a/src/tallies/tally_scoring.cpp b/src/tallies/tally_scoring.cpp index 67e851644..51b5d9ffc 100644 --- a/src/tallies/tally_scoring.cpp +++ b/src/tallies/tally_scoring.cpp @@ -328,10 +328,10 @@ double score_neutron_heating(const Particle& p, const Tally& tally, double flux, //! Helper function to obtain reaction Q value for photons and charged particles double get_reaction_q_value(const Particle& p) { - if (p.type() == ParticleType::photon && p.event_mt() == PAIR_PROD) { + if (p.type().is_photon() && p.event_mt() == PAIR_PROD) { // pair production return -2 * MASS_ELECTRON_EV; - } else if (p.type() == ParticleType::positron) { + } else if (p.type() == ParticleType::positron()) { // positron annihilation return 2 * MASS_ELECTRON_EV; } else { @@ -344,7 +344,7 @@ double get_reaction_q_value(const Particle& p) double score_particle_heating(const Particle& p, const Tally& tally, double flux, int rxn_bin, int i_nuclide, double atom_density) { - if (p.type() == ParticleType::neutron) + if (p.type().is_neutron()) return score_neutron_heating( p, tally, flux, rxn_bin, i_nuclide, atom_density); if (i_nuclide == -1 || i_nuclide == p.event_nuclide() || @@ -584,8 +584,6 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, // Get the pre-collision energy of the particle. auto E = p.E_last(); - using Type = ParticleType; - for (auto i = 0; i < tally.scores_.size(); ++i) { auto score_bin = tally.scores_[i]; auto score_index = start_index + i; @@ -598,9 +596,9 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, case SCORE_TOTAL: if (i_nuclide >= 0) { - if (p.type() == Type::neutron) { + if (p.type().is_neutron()) { score = p.neutron_xs(i_nuclide).total * atom_density * flux; - } else if (p.type() == Type::photon) { + } else if (p.type().is_photon()) { score = p.photon_xs(i_nuclide).total * atom_density * flux; } } else { @@ -609,7 +607,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, break; case SCORE_INVERSE_VELOCITY: - if (p.type() != Type::neutron) + if (!p.type().is_neutron()) continue; // Score inverse velocity in units of s/cm. @@ -617,11 +615,11 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, break; case SCORE_SCATTER: - if (p.type() != Type::neutron && p.type() != Type::photon) + if (!p.type().is_neutron() && !p.type().is_photon()) continue; if (i_nuclide >= 0) { - if (p.type() == Type::neutron) { + if (p.type().is_neutron()) { const auto& micro = p.neutron_xs(i_nuclide); score = (micro.total - micro.absorption) * atom_density * flux; } else { @@ -629,7 +627,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, score = (micro.coherent + micro.incoherent) * atom_density * flux; } } else { - if (p.type() == Type::neutron) { + if (p.type().is_neutron()) { score = (p.macro_xs().total - p.macro_xs().absorption) * flux; } else { score = (p.macro_xs().coherent + p.macro_xs().incoherent) * flux; @@ -638,11 +636,11 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, break; case SCORE_ABSORPTION: - if (p.type() != Type::neutron && p.type() != Type::photon) + if (!p.type().is_neutron() && !p.type().is_photon()) continue; if (i_nuclide >= 0) { - if (p.type() == Type::neutron) { + if (p.type().is_neutron()) { score = p.neutron_xs(i_nuclide).absorption * atom_density * flux; } else { const auto& xs = p.photon_xs(i_nuclide); @@ -650,7 +648,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, (xs.total - xs.coherent - xs.incoherent) * atom_density * flux; } } else { - if (p.type() == Type::neutron) { + if (p.type().is_neutron()) { score = p.macro_xs().absorption * flux; } else { score = @@ -806,7 +804,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, // this loop. for (auto d = 1; d < rxn.products_.size(); ++d) { const auto& product = rxn.products_[d]; - if (product.particle_ != Type::neutron) + if (!product.particle_.is_neutron()) continue; auto yield = nuc.nu(E, ReactionProduct::EmissionMode::delayed, d); @@ -860,7 +858,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, // this loop. for (auto d = 1; d < rxn.products_.size(); ++d) { const auto& product = rxn.products_[d]; - if (product.particle_ != Type::neutron) + if (!product.particle_.is_neutron()) continue; auto yield = @@ -911,7 +909,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, continue; case ELASTIC: - if (p.type() != Type::neutron) + if (!p.type().is_neutron()) continue; if (i_nuclide >= 0) { @@ -943,7 +941,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, case SCORE_IFP_TIME_NUM: if (settings::ifp_on) { - if ((p.type() == Type::neutron) && (p.fission())) { + if (p.type().is_neutron() && p.fission()) { if (is_generation_time_or_both()) { const auto& lifetimes = simulation::ifp_source_lifetime_bank[p.current_work() - 1]; @@ -957,7 +955,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, case SCORE_IFP_BETA_NUM: if (settings::ifp_on) { - if ((p.type() == Type::neutron) && (p.fission())) { + if (p.type().is_neutron() && p.fission()) { if (is_beta_effective_or_both()) { const auto& delayed_groups = simulation::ifp_source_delayed_group_bank[p.current_work() - 1]; @@ -982,7 +980,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, case SCORE_IFP_DENOM: if (settings::ifp_on) { - if ((p.type() == Type::neutron) && (p.fission())) { + if (p.type().is_neutron() && p.fission()) { int ifp_data_size; if (is_beta_effective_or_both()) { ifp_data_size = static_cast( @@ -1012,7 +1010,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, if (!simulation::need_depletion_rx) goto default_case; - if (p.type() != Type::neutron) + if (!p.type().is_neutron()) continue; int m; @@ -1045,7 +1043,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, case INCOHERENT: case PHOTOELECTRIC: case PAIR_PROD: - if (p.type() != Type::photon) + if (!p.type().is_photon()) continue; if (i_nuclide >= 0) { @@ -1075,7 +1073,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, // The default block is really only meant for redundant neutron reactions // (e.g. 444, 901) - if (p.type() != Type::neutron) + if (!p.type().is_neutron()) continue; // Any other cross section has to be calculated on-the-fly @@ -1129,8 +1127,6 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index, p.neutron_xs(p.event_nuclide()).total : 0.0; - using Type = ParticleType; - for (auto i = 0; i < tally.scores_.size(); ++i) { auto score_bin = tally.scores_[i]; auto score_index = start_index + i; @@ -1141,7 +1137,7 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index, // All events score to a flux bin. We actually use a collision estimator // in place of an analog one since there is no way to count 'events' // exactly for the flux - if (p.type() == Type::neutron || p.type() == Type::photon) { + if (p.type().is_neutron() || p.type().is_photon()) { score = flux * p.wgt_last() / p.macro_xs().total; } else { score = 0.; @@ -1155,7 +1151,7 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index, break; case SCORE_INVERSE_VELOCITY: - if (p.type() != Type::neutron) + if (!p.type().is_neutron()) continue; // All events score to an inverse velocity bin. We actually use a @@ -1165,7 +1161,7 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index, break; case SCORE_SCATTER: - if (p.type() != Type::neutron && p.type() != Type::photon) + if (!p.type().is_neutron() && !p.type().is_photon()) continue; // Skip any event where the particle didn't scatter @@ -1177,7 +1173,7 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index, break; case SCORE_NU_SCATTER: - if (p.type() != Type::neutron) + if (!p.type().is_neutron()) continue; // Only analog estimators are available. @@ -1202,7 +1198,7 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index, break; case SCORE_ABSORPTION: - if (p.type() != Type::neutron && p.type() != Type::photon) + if (!p.type().is_neutron() && !p.type().is_photon()) continue; if (settings::survival_biasing) { @@ -1431,7 +1427,7 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index, // this loop. for (auto d = 1; d < rxn.products_.size(); ++d) { const auto& product = rxn.products_[d]; - if (product.particle_ != Type::neutron) + if (!product.particle_.is_neutron()) continue; auto yield = nuc.nu(E, ReactionProduct::EmissionMode::delayed, d); @@ -1523,7 +1519,7 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index, continue; case ELASTIC: - if (p.type() != Type::neutron) + if (!p.type().is_neutron()) continue; // Check if event MT matches @@ -1552,7 +1548,7 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index, if (!simulation::need_depletion_rx) goto default_case; - if (p.type() != Type::neutron) + if (!p.type().is_neutron()) continue; // Check if the event MT matches @@ -1565,7 +1561,7 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index, case INCOHERENT: case PHOTOELECTRIC: case PAIR_PROD: - if (p.type() != Type::photon) + if (!p.type().is_photon()) continue; if (score_bin == PHOTOELECTRIC) { @@ -1592,7 +1588,7 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index, // The default block is really only meant for redundant neutron reactions // (e.g. 444, 901) - if (p.type() != Type::neutron) + if (!p.type().is_neutron()) continue; // Any other score is assumed to be a MT number. Thus, we just need @@ -2316,10 +2312,7 @@ void score_analog_tally_ce(Particle& p) // Since electrons/positrons are not transported, we assign a flux of zero. // Note that the heating score does NOT use the flux and will be non-zero for // electrons/positrons. - double flux = - (p.type() == ParticleType::neutron || p.type() == ParticleType::photon) - ? 1.0 - : 0.0; + double flux = (p.type().is_neutron() || p.type().is_photon()) ? 1.0 : 0.0; for (auto i_tally : model::active_analog_tallies) { const Tally& tally {*model::tallies[i_tally]}; @@ -2447,7 +2440,7 @@ void score_tracklength_tally_general( if (j == C_NONE) { // Determine log union grid index if (i_log_union == C_NONE) { - int neutron = static_cast(ParticleType::neutron); + int neutron = ParticleType::neutron().transport_index(); i_log_union = std::log(p.E() / data::energy_min[neutron]) / simulation::log_spacing; } @@ -2544,7 +2537,7 @@ void score_collision_tally(Particle& p) { // Determine the collision estimate of the flux double flux = 0.0; - if (p.type() == ParticleType::neutron || p.type() == ParticleType::photon) { + if (p.type().is_neutron() || p.type().is_photon()) { flux = p.wgt_last() / p.macro_xs().total; } @@ -2578,7 +2571,7 @@ void score_collision_tally(Particle& p) if (j == C_NONE) { // Determine log union grid index if (i_log_union == C_NONE) { - int neutron = static_cast(ParticleType::neutron); + int neutron = ParticleType::neutron().transport_index(); i_log_union = std::log(p.E() / data::energy_min[neutron]) / simulation::log_spacing; } diff --git a/src/track_output.cpp b/src/track_output.cpp index f4344d50f..e86f774f0 100644 --- a/src/track_output.cpp +++ b/src/track_output.cpp @@ -122,7 +122,7 @@ void finalize_particle_track(Particle& p) int offset = 0; for (auto& track_i : p.tracks()) { offsets.push_back(offset); - particles.push_back(static_cast(track_i.particle)); + particles.push_back(track_i.particle.pdg_number()); offset += track_i.states.size(); tracks.insert(tracks.end(), track_i.states.begin(), track_i.states.end()); } diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index 4838e4591..5ca4addbb 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -59,7 +59,7 @@ void apply_weight_windows(Particle& p) return; // WW on photon and neutron only - if (p.type() != ParticleType::neutron && p.type() != ParticleType::photon) + if (!p.type().is_neutron() && !p.type().is_photon()) return; // skip dead or no energy @@ -179,7 +179,7 @@ WeightWindows::WeightWindows(pugi::xml_node node) // get the particle type auto particle_type_str = std::string(get_node_value(node, "particle_type")); - particle_type_ = openmc::str_to_particle_type(particle_type_str); + particle_type_ = ParticleType {particle_type_str}; // Determine associated mesh int32_t mesh_id = std::stoi(get_node_value(node, "mesh")); @@ -252,7 +252,7 @@ WeightWindows* WeightWindows::from_hdf5( std::string particle_type; read_dataset(ww_group, "particle_type", particle_type); - wws->particle_type_ = openmc::str_to_particle_type(particle_type); + wws->particle_type_ = ParticleType {particle_type}; read_dataset(ww_group, "energy_bounds", wws->energy_bounds_); @@ -284,7 +284,10 @@ void WeightWindows::set_defaults() { // set energy bounds to the min/max energy supported by the data if (energy_bounds_.size() == 0) { - int p_type = static_cast(particle_type_); + int p_type = particle_type_.transport_index(); + if (p_type == C_NONE) { + fatal_error("Weight windows particle is not supported for transport."); + } energy_bounds_.push_back(data::energy_min[p_type]); energy_bounds_.push_back(data::energy_max[p_type]); } @@ -345,10 +348,9 @@ void WeightWindows::set_energy_bounds(span bounds) void WeightWindows::set_particle_type(ParticleType p_type) { - if (p_type != ParticleType::neutron && p_type != ParticleType::photon) - fatal_error( - fmt::format("Particle type '{}' cannot be applied to weight windows.", - particle_type_to_str(p_type))); + if (!p_type.is_neutron() && !p_type.is_photon()) + fatal_error(fmt::format( + "Particle type '{}' cannot be applied to weight windows.", p_type.str())); particle_type_ = p_type; } @@ -608,8 +610,7 @@ void WeightWindows::update_weights(const Tally* tally, const std::string& value, if (p_it == particles.end()) { auto msg = fmt::format("Particle type '{}' not present on Filter {} for " "Tally {} used to update WeightWindows {}", - particle_type_to_str(this->particle_type_), pf->id(), tally->id(), - this->id()); + this->particle_type_.str(), pf->id(), tally->id(), this->id()); fatal_error(msg); } @@ -818,8 +819,7 @@ void WeightWindows::to_hdf5(hid_t group) const hid_t ww_group = create_group(group, fmt::format("weight_windows_{}", id())); write_dataset(ww_group, "mesh", this->mesh()->id()); - write_dataset( - ww_group, "particle_type", openmc::particle_type_to_str(particle_type_)); + write_dataset(ww_group, "particle_type", particle_type_.str()); write_dataset(ww_group, "energy_bounds", energy_bounds_); write_dataset(ww_group, "lower_ww_bounds", lower_ww_); write_dataset(ww_group, "upper_ww_bounds", upper_ww_); @@ -846,8 +846,8 @@ WeightWindowsGenerator::WeightWindowsGenerator(pugi::xml_node node) max_realizations_, active_batches); warning(msg); } - auto tmp_str = get_node_value(node, "particle_type", true, true); - auto particle_type = str_to_particle_type(tmp_str); + auto tmp_str = get_node_value(node, "particle_type", false, true); + auto particle_type = ParticleType {tmp_str}; update_interval_ = std::stoi(get_node_value(node, "update_interval")); on_the_fly_ = get_node_value_bool(node, "on_the_fly"); @@ -856,7 +856,10 @@ WeightWindowsGenerator::WeightWindowsGenerator(pugi::xml_node node) if (check_for_node(node, "energy_bounds")) { e_bounds = get_node_array(node, "energy_bounds"); } else { - int p_type = static_cast(particle_type); + int p_type = particle_type.transport_index(); + if (p_type == C_NONE) { + fatal_error("Weight windows particle is not supported for transport."); + } e_bounds.push_back(data::energy_min[p_type]); e_bounds.push_back(data::energy_max[p_type]); } @@ -1110,23 +1113,25 @@ extern "C" int openmc_weight_windows_get_energy_bounds( return 0; } -extern "C" int openmc_weight_windows_set_particle(int32_t index, int particle) +extern "C" int openmc_weight_windows_set_particle( + int32_t index, int32_t particle) { if (int err = verify_ww_index(index)) return err; const auto& wws = variance_reduction::weight_windows.at(index); - wws->set_particle_type(static_cast(particle)); + wws->set_particle_type(ParticleType {particle}); return 0; } -extern "C" int openmc_weight_windows_get_particle(int32_t index, int* particle) +extern "C" int openmc_weight_windows_get_particle( + int32_t index, int32_t* particle) { if (int err = verify_ww_index(index)) return err; const auto& wws = variance_reduction::weight_windows.at(index); - *particle = static_cast(wws->particle_type()); + *particle = wws->particle_type().pdg_number(); return 0; } diff --git a/tests/cpp_unit_tests/test_mcpl_stat_sum.cpp b/tests/cpp_unit_tests/test_mcpl_stat_sum.cpp index 909830e03..d0c26f26c 100644 --- a/tests/cpp_unit_tests/test_mcpl_stat_sum.cpp +++ b/tests/cpp_unit_tests/test_mcpl_stat_sum.cpp @@ -25,7 +25,7 @@ TEST_CASE("MCPL stat:sum field") // Initialize test particles for (int i = 0; i < 100; ++i) { - source_bank[i].particle = openmc::ParticleType::neutron; + source_bank[i].particle = openmc::ParticleType::neutron(); source_bank[i].r = {i * 0.1, i * 0.2, i * 0.3}; source_bank[i].u = {0.0, 0.0, 1.0}; source_bank[i].E = 2.0e6; // 2 MeV @@ -63,7 +63,7 @@ TEST_CASE("MCPL stat:sum field") // Initialize particles for (int i = 0; i < count; ++i) { - source_bank[i].particle = openmc::ParticleType::neutron; + source_bank[i].particle = openmc::ParticleType::neutron(); source_bank[i].r = {0.0, 0.0, 0.0}; source_bank[i].u = {0.0, 0.0, 1.0}; source_bank[i].E = 1.0e6; diff --git a/tests/regression_tests/source_dlopen/source_sampling.cpp b/tests/regression_tests/source_dlopen/source_sampling.cpp index fc61ef1fd..678eea4be 100644 --- a/tests/regression_tests/source_dlopen/source_sampling.cpp +++ b/tests/regression_tests/source_dlopen/source_sampling.cpp @@ -10,7 +10,7 @@ class CustomSource : public openmc::Source { { openmc::SourceSite particle; // wgt - particle.particle = openmc::ParticleType::neutron; + particle.particle = openmc::ParticleType::neutron(); particle.wgt = 1.0; // position diff --git a/tests/regression_tests/source_parameterized_dlopen/parameterized_source_sampling.cpp b/tests/regression_tests/source_parameterized_dlopen/parameterized_source_sampling.cpp index bf49af4c3..81f3770c6 100644 --- a/tests/regression_tests/source_parameterized_dlopen/parameterized_source_sampling.cpp +++ b/tests/regression_tests/source_parameterized_dlopen/parameterized_source_sampling.cpp @@ -2,36 +2,37 @@ #include "openmc/source.h" class CustomSource : public openmc::Source { - public: - CustomSource(double energy) : energy_(energy) { } +public: + CustomSource(double energy) : energy_(energy) {} - // Samples from an instance of this class. - openmc::SourceSite sample(uint64_t* seed) const - { - openmc::SourceSite particle; - // wgt - particle.particle = openmc::ParticleType::neutron; - particle.wgt = 1.0; - // position - particle.r.x = 0.0; - particle.r.y = 0.0; - particle.r.z = 0.0; - // angle - particle.u = {1.0, 0.0, 0.0}; - particle.E = this->energy_; - particle.delayed_group = 0; + // Samples from an instance of this class. + openmc::SourceSite sample(uint64_t* seed) const + { + openmc::SourceSite particle; + // wgt + particle.particle = openmc::ParticleType::neutron(); + particle.wgt = 1.0; + // position + particle.r.x = 0.0; + particle.r.y = 0.0; + particle.r.z = 0.0; + // angle + particle.u = {1.0, 0.0, 0.0}; + particle.E = this->energy_; + particle.delayed_group = 0; - return particle; - } + return particle; + } - private: - double energy_; +private: + double energy_; }; -// A function to create a unique pointer to an instance of this class when generated -// via a plugin call using dlopen/dlsym. -// You must have external C linkage here otherwise dlopen will not find the file -extern "C" std::unique_ptr openmc_create_source(std::string parameter) +// A function to create a unique pointer to an instance of this class when +// generated via a plugin call using dlopen/dlsym. You must have external C +// linkage here otherwise dlopen will not find the file +extern "C" std::unique_ptr openmc_create_source( + std::string parameter) { double energy = std::stod(parameter); return std::make_unique(energy); diff --git a/tests/regression_tests/surface_source/surface_source_true.h5 b/tests/regression_tests/surface_source/surface_source_true.h5 index d343d12aed654f2da53310c5943c20d8bd85afb9..02e94c90b990376b5db3595a486feefd236fd37b 100644 GIT binary patch delta 10557 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 xET@i*R+@-7?L|bB?L;)TSww{G5z#a^og$*K zJtCU2m569BBAWIhqG>N8n)V{1|8t-Bxqd!>`*7aQdA{eo@4fGGFIQjNx??!9V_^1J z_pvu+k}#+pyE(ghx8R`+q;y{X2Wdju~SW?Q-}I zHfW_Jy6}@@#|jb^OB(O8lE&v4zz4@So6arbh325BSWi{?5APy(K8-eQG1q|-) zvIch-kiUn^%HM-P&mICi6J3^PB4b?>IimDQlIkY8thz~*QYTZYm@FxKip$ELLaA&D zrH(0*YFsX>#zm>krC8NemsK@Yqlm(ZP9>0-Dxf>nWp$?taPR4|+Q1=Bg?rspA-ANG z8JN)-lm=!{%G=9G!I`e$ObWR(Db&wYj(2a&-GT) zV^XasnCL;O6SFlKJH<*om`gKtU#DF6!PvzIvs}gYp!*Q)=0lj!dWc5XhtoR*qb|}` zVso#Qv4Nq%&t;jQvj*xaia8(zg7RdlGNl+`$=S5qojO{r(@#iS?jV!)@qR6B5d&@0{uk-3fJMVUdLGP zI(C=kl@#~lWcE_ZI83p@!*DVmrh55@<77UZK;_{AJV)SUK0-hMPT&Xv=|>8vI}+#f zkpg0HVn-6FI7&d@Q7)_RD8|Z;=7>6umQ-^LPU&MPwH>2a)v;I&$EscwPV`s;iN^`( zJ`VGHoB($*=CznWYq5Z|<1vxPGuC`OI~+Y;QfJ9j>-mz!_PQA^QLOz09LXoBUP>t@ zvXr?^r2>*p#KC-`03RIRi3EmD6j0*B@$6%)sL`p~h^mcF)!R|^c2o6sr)m?bHaS({ z_?k@BCa3BhsCoxu19$K;^1_l@!dN6>O1WDp)o+#Ly%THVPD-_RDpq(G*2G=vDhwxl z7lGcp1T@}_2l(9r;&9@36R5sNz~DVtCigH_c`rNcxmS|sJ}i*?D0SUODgAy)vHP(a z?x&RcfMR_QU^P6TuCkl)0BJpnw`UH4hR(HLoR(pT-^xHo|u} z?Gj4;OC*JJv08H}_2w$pcp#?oKy?*|6F-nZhDX4l2UF=0kiQgDxs*W9QUNtsAT|F9 z@Z@27^9Z#4$KJd0>}GSaUePvJCAjcIB7K=Du{nxZ)VvxdCOGeMb<402mf2va)Cq>V z6Y$;!!~E2IlOJ-AMHGf`@mRi!S{3IyW8iG+}?|8T{tXwz zGK}cB?ZS}Gz~Zr|vf5chp}}OxFrxD(z=a`QIl<69Xw67GDUe}A2W%IHbXSU@(|2>| zy4@hdh>qDV4C&O}4PAlO)b86IGK}c#J>bHSF2gU_7`HXxW0QB1$qBA^1=rhvmAl?V ztdaHDwCha`%X=~&J|`3CKUqNGDR}Cf!dUMqtd@1Eq{dURgHNTDaT=xS(-^c25-SwzuE6Vjg}QQ|g9G>+0(#xKAR$L}XF>K9PH z0q+VM7#rTe4vWr}lyV+U-t#CGoJXnYJW0Ouaps;+so{J|L+4A1T!7Q|0!jlHDAsZz zPTLF3D1sYZ!HtybH%cDah{@ZiL3%I3{%jij1Qn7mDt+BPXxbvYiGm#bbBPV{mDiB|~dz5-Ktg#h=JQ>__S@>i_ZD+Q!o zg(NzGTW!_liGbzY5!=hc*kuU4%68azC&QN5IFF@@I>D7aQY(sg)vUMIi@$9Elp zq3Z;c)Z*b;%h*6IJIuS@NWnT+u#Q4*9fkTj<#=zvtldDV_6EfY>oIHf>L?5+Tu&hD zMgfgCV(xAf5Qh`LkwA4oz+eE6&H!VTH?hN>nMDCPo@1K{bZi!oxdrb$TLjd=so6pxIV7Megoz9>mUs(0EWJgN`&MkcTPYRa zN~!f$N&W_Gxduwj4T_cDhAnrSy6S|}d7BM{Dm>fIM2~$YdbXd5OYJjpsm{cu<^=6P zYfjM2JjgJnVyMP;VOX;*&(O)s9J*>5WEjy=+l3*Wm~ZG(w5E1$Ztb1 zgjzq+ZH!_#y2gB}1o!M{y1pD(7W+Z2r+dX`c zb8k}muwVO_Fnk+tMw5O64OL~J8Voaw4mN!iZ2utgDK@o!KMl1X3=~G=kJ_0qEZguY z%QiHz8TwRXO*sS%467&3amEw51Bdw?y4FZPoPH7ohnSIaH}i0mHp&> zW(%PvfLcj&&_nke=Pr(H#hJg=R1U^m!I%b{v=SH$i{!?X?9N0KhS7fK)67T=HJn7B zod~1BdOyd3^f?DxoM{G`bvMV_|2aq2u+m&YtJDnVb4}qFIJUlE=8$2m-Y+zcwEfWu zj5^7`7f+abHS&^G&|o;mtS^<}w=-ar5x@tZz{gz|OU2SgkaF%m1 zQGA4owy1*`$e1DP4QW+;$I%zfbrQNC!j^qV34Pz;DF05Q9YN>5=d))$W0irPG7JSd!HL8zVp_FWhFe+Cwfad_eyM$^B%&i=baBKzH#%&v*+Ygn? z+^YHkkBJ{xyx>u4O+bwng%4+xAh!gdCXGP(zG zQwHUfJceiXV*-749*i{ik9a)&$hqiU$ZYpGpWEiV?NJDHaL{~6`Mu%+3s?~S5P|19VFd9eaPdF>}St=B_6JeOx`;^K>>`(gp5P-T%; z&(0cB`!0A2?7|`S7%h~$z%Z#DEYp<8W@x<02zl0_D;RcF_8f0bJyOi+8n9Iu4YH;a z3%64jFoJ=EVAxyNIw!@u5MdalR6UP{`#hO+uUQGT3(bg1Ur;Y~UZlW~>X+L3A_v<1 zlAyvxfM8U&^JUEX%Nk19)?lQyzk<8)3aj-la%M8=HO$0o3}!7h0~mTuwHp^BAI4U# zye>uKb`*xu+&6e@GIE@H-flbzx>>J!d$90L9P)2!RD(GQQ$s)IBrG=Dw6O=ra*wq9 zC8!O<+SPBdcI6T?tjZ|e9=Ikm@HP(1x0UJ1MJ9}{HTDk9xp$-mYz;FskSN2s{2G)=PC>Dy>Frn?`OME2E9+ z)omJg{xf)jJj2SB&rs@l#-C5l);HZ1Sxwsc`aW4`meo;wjFX5zm zNq`@Y|0M#WT9j5fR$=+ej19lc4vSt195}W>R5n-&&u-0ZU z*%eG?f!;Tr0?8;~Vp%;VbAqHeR&Kn<;3)BM!W>b4Io1WR?!}uN+Hs8a#_ERi=DwdD=5SBVevBA@@U`}J* zvP@IL`4CpKKkke9Yl5ybgh#WcT5o6Z14gRzxvP6FcJEw`J#8L#>O48wXJNw5l2bk( zH*UV1One8+G~dCB4p8l?vvC*A2e76E97<^gn}&sKiqBy)u!v2wzHkL|T)`Yw?c9K8 z(FQr*C3r3@k<)%Io(9B*i?Bc7`_;H{$e??W!PHFx-R zRqQDFAU5gMY)bi$I;_;o=oKHLL1)+CX4f#Sd8J~Rn^0<#>UOTe-C0E-`Emj6hvL2* z%2=uM#VV-)ccg%^V&@}O$r{{|H7ex43VXzSvC1n{q4KLSyXI3>?phA4=o&V)MIyrM za9`H3Y}2)hHG1*P@-o(N9r5bJBt`I*DpIQ+Tdv3Izh2Hr9rlm;Ox1q_lX~mfWH{fI c@^8Y4cav~W5C>XNP944-)tPTc75aAcf0{`&E&u=k diff --git a/tests/regression_tests/surface_source_write/test.py b/tests/regression_tests/surface_source_write/test.py index f144eb82a..094df1f8b 100644 --- a/tests/regression_tests/surface_source_write/test.py +++ b/tests/regression_tests/surface_source_write/test.py @@ -634,7 +634,7 @@ def return_surface_source_data(filepath): wgt = point.wgt delayed_group = point.delayed_group surf_id = point.surf_id - particle = point.particle + particle = point.particle.pdg_number key = ( f"{r[0]:.10e} {r[1]:.10e} {r[2]:.10e} {u[0]:.10e} {u[1]:.10e} {u[2]:.10e}" f"{e:.10e} {time:.10e} {wgt:.10e} {delayed_group} {surf_id} {particle}" diff --git a/tests/unit_tests/test_particle_type.py b/tests/unit_tests/test_particle_type.py new file mode 100644 index 000000000..67e385ea6 --- /dev/null +++ b/tests/unit_tests/test_particle_type.py @@ -0,0 +1,298 @@ +"""Unit tests for ParticleType class.""" + +import pytest +from openmc import ParticleType + + +# Tests for creating ParticleType instances + +def test_create_from_int(): + """Test creation from PDG number.""" + p = ParticleType(2112) + assert p.pdg_number == 2112 + assert int(p) == 2112 + + +def test_create_from_string_name(): + """Test creation from particle name.""" + p = ParticleType('neutron') + assert p.pdg_number == 2112 + + p = ParticleType('photon') + assert p.pdg_number == 22 + + +def test_create_from_string_aliases(): + """Test creation from particle aliases.""" + assert ParticleType('n').pdg_number == 2112 + assert ParticleType('gamma').pdg_number == 22 + assert ParticleType('p').pdg_number == 2212 + assert ParticleType('proton').pdg_number == 2212 + assert ParticleType('d').pdg_number == 1000010020 + assert ParticleType('t').pdg_number == 1000010030 + + +def test_create_from_string_nuclide(): + """Test creation from GNDS nuclide name.""" + p = ParticleType('He4') + assert p.pdg_number == 1000020040 + + p = ParticleType('U235') + assert p.pdg_number == 1000922350 + + p = ParticleType('Am242_m1') + assert p.pdg_number == 1000952421 + + +def test_create_from_string_pdg_prefix(): + """Test creation with pdg: prefix.""" + p = ParticleType('pdg:2112') + assert p.pdg_number == 2112 + + p = ParticleType('PDG:22') + assert p.pdg_number == 22 + + +def test_create_from_particle_type(): + """Test creation from existing ParticleType.""" + p1 = ParticleType(2112) + p2 = ParticleType(p1) + assert p1 == p2 + assert p1 is not p2 # Different instances + + +def test_legacy_particle_indices(): + """Test backward compatibility with legacy indices 0-3.""" + assert ParticleType(0) == ParticleType.NEUTRON + assert ParticleType(1) == ParticleType.PHOTON + assert ParticleType(2) == ParticleType.ELECTRON + assert ParticleType(3) == ParticleType.POSITRON + + +def test_create_invalid_type(): + """Test creation with invalid type raises TypeError.""" + with pytest.raises(TypeError): + ParticleType([2112]) + + with pytest.raises(TypeError): + ParticleType({'pdg': 2112}) + + +def test_create_invalid_string(): + """Test creation with invalid string raises ValueError.""" + with pytest.raises(ValueError): + ParticleType('') + + with pytest.raises(ValueError): + ParticleType('pdg:invalid') + + +def test_create_case_insensitive(): + """Test that string parsing is case insensitive for aliases.""" + assert ParticleType('NEUTRON').pdg_number == 2112 + assert ParticleType('Neutron').pdg_number == 2112 + assert ParticleType('PHOTON').pdg_number == 22 + + +# Tests for equality and comparison + +def test_equality_same_pdg(): + """Test that instances with same PDG are equal.""" + p1 = ParticleType(2112) + p2 = ParticleType(2112) + assert p1 == p2 + + +def test_equality_int(): + """Test equality comparison with int.""" + p = ParticleType(2112) + assert p == 2112 + assert 2112 == p + + +def test_equality_string(): + """Test equality comparison with string.""" + p = ParticleType(2112) + assert p == 'neutron' + assert p == 'pdg:2112' + assert p == 'n' + + +def test_inequality(): + """Test inequality comparisons.""" + p1 = ParticleType(2112) + p2 = ParticleType(22) + assert p1 != p2 + assert p1 != 22 + assert p1 != 'photon' + + +def test_equality_with_constants(): + """Test equality with class constants.""" + p = ParticleType(2112) + assert p == ParticleType.NEUTRON + assert ParticleType.NEUTRON == p + + +def test_equality_invalid_string(): + """Test equality with invalid string returns False.""" + p = ParticleType(2112) + assert not (p == 'invalid_particle') + assert p != 'invalid_particle' + + +# Tests for hashing behavior + +def test_hash_consistency(): + """Test that equal instances hash to the same value.""" + p1 = ParticleType(2112) + p2 = ParticleType(2112) + assert hash(p1) == hash(p2) + + +def test_set_deduplication(): + """Test that equal instances deduplicate in sets.""" + p1 = ParticleType(2112) + p2 = ParticleType(2112) + s = {p1, p2} + assert len(s) == 1 + + +def test_dict_key(): + """Test use as dictionary key.""" + p1 = ParticleType(2112) + p2 = ParticleType(2112) + d = {p1: 'neutron'} + assert d[p2] == 'neutron' + + +def test_hash_different_particles(): + """Test that different particles have different hashes (usually).""" + p1 = ParticleType(2112) + p2 = ParticleType(22) + # Different PDG numbers should (almost always) have different hashes + assert hash(p1) != hash(p2) + + +# Tests for properties and computed attributes + +def test_pdg_number_property(): + """Test pdg_number property.""" + p = ParticleType(2112) + assert p.pdg_number == 2112 + assert isinstance(p.pdg_number, int) + + +def test_int_conversion(): + """Test __int__ conversion.""" + p = ParticleType(1000020040) + assert int(p) == 1000020040 + + +def test_zam_elementary(): + """Test zam property for elementary particles.""" + assert ParticleType.NEUTRON.zam is None + assert ParticleType.PHOTON.zam is None + assert ParticleType.ELECTRON.zam is None + assert ParticleType.POSITRON.zam is None + + +def test_zam_nucleus(): + """Test zam property for nuclear particles.""" + he4 = ParticleType('He4') + assert he4.zam == (2, 4, 0) + + u235 = ParticleType('U235') + Z, A, m = u235.zam + assert Z == 92 + assert A == 235 + assert m == 0 + + +def test_zam_metastable(): + """Test zam property for metastable nuclei.""" + # Am242m has m=1 + am242m = ParticleType('Am242_m1') + Z, A, m = am242m.zam + assert Z == 95 + assert A == 242 + assert m == 1 + + +def test_is_nucleus_false(): + """Test is_nucleus for elementary particles.""" + assert not ParticleType.NEUTRON.is_nucleus + assert not ParticleType.PHOTON.is_nucleus + assert not ParticleType.ELECTRON.is_nucleus + assert not ParticleType.POSITRON.is_nucleus + + +def test_is_nucleus_true(): + """Test is_nucleus for nuclear particles.""" + assert ParticleType.ALPHA.is_nucleus + assert ParticleType('He4').is_nucleus + assert ParticleType('U235').is_nucleus + assert ParticleType.DEUTERON.is_nucleus + assert ParticleType.TRITON.is_nucleus + + +# Tests for __str__ and __repr__ + +def test_str_elementary(): + """Test string representation of elementary particles.""" + assert str(ParticleType.NEUTRON) == 'neutron' + assert str(ParticleType.PHOTON) == 'photon' + assert str(ParticleType.ELECTRON) == 'electron' + assert str(ParticleType.POSITRON) == 'positron' + assert str(ParticleType.PROTON) == 'H1' + + +def test_str_nucleus(): + """Test string representation of nuclei.""" + assert str(ParticleType.ALPHA) == 'He4' + assert str(ParticleType('U235')) == 'U235' + assert str(ParticleType.DEUTERON) == 'H2' + assert str(ParticleType.TRITON) == 'H3' + + +def test_str_arbitrary_pdg(): + """Test string representation of arbitrary PDG number.""" + # PDG number that doesn't match any known particle + p = ParticleType(12345) + assert str(p) == 'pdg:12345' + + +def test_repr(): + """Test repr includes PDG number.""" + p = ParticleType.NEUTRON + repr_str = repr(p) + assert 'ParticleType' in repr_str + assert 'PDG=2112' in repr_str + assert 'neutron' in repr_str + + +def test_repr_nucleus(): + """Test repr for nuclear particles.""" + p = ParticleType.ALPHA + repr_str = repr(p) + assert 'ParticleType' in repr_str + assert 'PDG=1000020040' in repr_str + assert 'He4' in repr_str + + +def test_create_from_numpy_int(): + """Test creation from numpy integer types.""" + import numpy as np + p = ParticleType(np.int32(2112)) + assert p.pdg_number == 2112 + + p = ParticleType(np.int64(22)) + assert p.pdg_number == 22 + + +def test_equality_numpy_int(): + """Test equality comparison with numpy integer types.""" + import numpy as np + p = ParticleType(2112) + assert p == np.int32(2112) + assert p == np.int64(2112) diff --git a/tests/unit_tests/test_source_file.py b/tests/unit_tests/test_source_file.py index 41906c80f..3e84fbe51 100644 --- a/tests/unit_tests/test_source_file.py +++ b/tests/unit_tests/test_source_file.py @@ -42,7 +42,7 @@ def test_source_file(run_in_tmpdir): assert np.all(arr['E'] == n - np.arange(n)) assert np.all(arr['wgt'] == 1.0) assert np.all(arr['delayed_group'] == 0) - assert np.all(arr['particle'] == 0) + assert np.all(arr['particle'] == 2112) # PDG number for neutron # Ensure sites read in are consistent sites = openmc.ParticleList.from_hdf5('test_source.h5') @@ -64,7 +64,7 @@ def test_source_file(run_in_tmpdir): dgs = np.array([s.delayed_group for s in sites]) assert np.all(dgs == 0) p_types = np.array([s.particle for s in sites]) - assert np.all(p_types == 0) + assert np.all(p_types == 2112) # PDG number for neutron # Ensure a ParticleList item is a SourceParticle site = sites[0] diff --git a/tests/unit_tests/test_tracks.py b/tests/unit_tests/test_tracks.py index fa8664159..74da83552 100644 --- a/tests/unit_tests/test_tracks.py +++ b/tests/unit_tests/test_tracks.py @@ -70,7 +70,7 @@ def test_tracks(sphere_model, particle, run_in_tmpdir): particle_track = track.particle_tracks[0] assert isinstance(particle_track, openmc.ParticleTrack) - assert particle_track.particle.name.lower() == particle + assert str(particle_track.particle).lower() == particle assert isinstance(particle_track.states, np.ndarray) # Sanity checks on actual data @@ -140,8 +140,10 @@ def test_filter(sphere_model, run_in_tmpdir): assert matches == tracks matches = tracks.filter(state_filter=lambda s: s['E'] > 0.0) assert matches == tracks - matches = tracks.filter(particle='bunnytron') + matches = tracks.filter(particle='proton') assert matches == [] + with pytest.raises(ValueError): + tracks.filter(particle='bunnytron') def test_write_to_vtk(sphere_model): diff --git a/tests/unit_tests/weightwindows/test_wwinp_reader.py b/tests/unit_tests/weightwindows/test_wwinp_reader.py index 637463bb2..601517d47 100644 --- a/tests/unit_tests/weightwindows/test_wwinp_reader.py +++ b/tests/unit_tests/weightwindows/test_wwinp_reader.py @@ -52,7 +52,7 @@ n_mesh.z_grid = np.array([-100.0, n_e_bounds = (np.array([0.0, 100000.0, 146780.0]),) -n_particles = ('neutron',) +n_particles = [openmc.ParticleType.NEUTRON] # expected results - neutron and photon data np_mesh = openmc.RectilinearMesh() @@ -63,7 +63,7 @@ np_mesh.z_grid = n_mesh.z_grid np_e_bounds = (np.array([0.0, 100000.0, 146780.0, 215440.0]), np.array([0.0, 1.0E8])) -np_particles = ('neutron', 'photon') +np_particles = [openmc.ParticleType.NEUTRON, openmc.ParticleType.PHOTON] # expected results - photon data only p_mesh = openmc.RectilinearMesh() @@ -74,7 +74,7 @@ p_mesh.y_grid = np_mesh.y_grid p_mesh.z_grid = np.array([-50.0, 50.0]) p_e_bounds = (np.array([0.0, 100000.0, 146780.0, 215440.0, 316230.0]),) -p_particles = ('photon',) +p_particles = [openmc.ParticleType.PHOTON] expected_results = [('wwinp_n', n_mesh, n_particles, n_e_bounds), ('wwinp_np', np_mesh, np_particles, np_e_bounds), From a22238e069fb684d0be110dafeb343b381eda454 Mon Sep 17 00:00:00 2001 From: David Andrs Date: Wed, 4 Feb 2026 13:50:38 -0700 Subject: [PATCH 520/671] Fixing compiler warning about VLA (Clang extension) (#3769) --- src/eigenvalue.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/eigenvalue.cpp b/src/eigenvalue.cpp index 2fb9dabf1..dc4fbb420 100644 --- a/src/eigenvalue.cpp +++ b/src/eigenvalue.cpp @@ -192,9 +192,9 @@ void synchronize_bank() // TODO: protect for MPI_Exscan at rank 0 // Allocate space for bank_position if this hasn't been done yet - int64_t bank_position[mpi::n_procs]; - MPI_Allgather( - &start, 1, MPI_INT64_T, bank_position, 1, MPI_INT64_T, mpi::intracomm); + std::vector bank_position(mpi::n_procs); + MPI_Allgather(&start, 1, MPI_INT64_T, bank_position.data(), 1, MPI_INT64_T, + mpi::intracomm); #else start = 0; finish = index_temp; @@ -289,7 +289,7 @@ void synchronize_bank() neighbor = mpi::n_procs - 1; } else { neighbor = - upper_bound_index(bank_position, bank_position + mpi::n_procs, start); + upper_bound_index(bank_position.begin(), bank_position.end(), start); } // Resize IFP receive buffers From 3b619d69042b26d0ca25e94832794ba517123c4f Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 4 Feb 2026 22:29:30 -0600 Subject: [PATCH 521/671] Fix use of length multiplier in several LibMesh methods (#3773) --- src/mesh.cpp | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/mesh.cpp b/src/mesh.cpp index a72b1441f..acd90033f 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -3669,8 +3669,15 @@ void LibMesh::initialize() bbox_ = libMesh::MeshTools::create_bounding_box(*m_); libMesh::Point ll = bbox_.min(); libMesh::Point ur = bbox_.max(); - lower_left_ = {ll(0), ll(1), ll(2)}; - upper_right_ = {ur(0), ur(1), ur(2)}; + if (length_multiplier_ > 0.0) { + lower_left_ = {length_multiplier_ * ll(0), length_multiplier_ * ll(1), + length_multiplier_ * ll(2)}; + upper_right_ = {length_multiplier_ * ur(0), length_multiplier_ * ur(1), + length_multiplier_ * ur(2)}; + } else { + lower_left_ = {ll(0), ll(1), ll(2)}; + upper_right_ = {ur(0), ur(1), ur(2)}; + } } // Sample position within a tet for LibMesh type tets @@ -3684,7 +3691,12 @@ Position LibMesh::sample_element(int32_t bin, uint64_t* seed) const tet_verts[i] = {node_ref(0), node_ref(1), node_ref(2)}; } // Samples position within tet using Barycentric coordinates - return this->sample_tet(tet_verts, seed); + Position sampled_position = this->sample_tet(tet_verts, seed); + if (length_multiplier_ > 0.0) { + return length_multiplier_ * sampled_position; + } else { + return sampled_position; + } } Position LibMesh::centroid(int bin) const From 1039b5d9ffa0d0371f5f3fe100147cc3b8ea774f Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Thu, 5 Feb 2026 17:43:16 +0100 Subject: [PATCH 522/671] Parallelize transpose of scattering matrix for random ray adjoints (#3768) --- src/random_ray/flat_source_domain.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index c2effaa5d..d0c342bf5 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -1277,6 +1277,7 @@ void FlatSourceDomain::set_adjoint_sources() void FlatSourceDomain::transpose_scattering_matrix() { // Transpose the inner two dimensions for each material +#pragma omp parallel for for (int m = 0; m < n_materials_; ++m) { int material_offset = m * negroups_ * negroups_; for (int i = 0; i < negroups_; ++i) { From f2c936cf5b5187c8bd575fee8689e5fcee1a8166 Mon Sep 17 00:00:00 2001 From: Gavin Ridley Date: Thu, 5 Feb 2026 18:00:03 -0600 Subject: [PATCH 523/671] Implement filter for secondary particle production binned by energy (#3453) Co-authored-by: Paul Romano --- docs/source/pythonapi/base.rst | 1 + include/openmc/constants.h | 2 +- include/openmc/particle_data.h | 18 +++ include/openmc/tallies/filter.h | 1 + include/openmc/tallies/filter_energy.h | 32 +++++ openmc/filter.py | 115 +++++++++++++++--- src/particle.cpp | 9 ++ src/physics.cpp | 2 + src/physics_mg.cpp | 2 + src/tallies/filter.cpp | 2 + src/tallies/filter_delayedgroup.cpp | 6 +- src/tallies/filter_energy.cpp | 44 +++++++ src/tallies/tally.cpp | 2 + .../photon_production_inputs_true.dat | 15 +++ .../photon_production/inputs_true.dat | 15 +++ .../photon_production/results_true.dat | 11 ++ .../photon_production/test.py | 30 ++++- tests/unit_tests/test_filters.py | 47 ++++++- 18 files changed, 328 insertions(+), 26 deletions(-) diff --git a/docs/source/pythonapi/base.rst b/docs/source/pythonapi/base.rst index dea8c4427..7bd03f379 100644 --- a/docs/source/pythonapi/base.rst +++ b/docs/source/pythonapi/base.rst @@ -132,6 +132,7 @@ Constructing Tallies openmc.MeshSurfaceFilter openmc.EnergyFilter openmc.EnergyoutFilter + openmc.ParticleProductionFilter openmc.MuFilter openmc.MuSurfaceFilter openmc.PolarFilter diff --git a/include/openmc/constants.h b/include/openmc/constants.h index 980cff447..8185214fc 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -301,7 +301,7 @@ enum class TallyEstimator { ANALOG, TRACKLENGTH, COLLISION }; enum class TallyEvent { SURFACE, LATTICE, KILL, SCATTER, ABSORB }; // Tally score type -- if you change these, make sure you also update the -// _SCORES dictionary in openmc/capi/tally.py +// _SCORES dictionary in openmc/lib/tally.py // // These are kept as a normal enum and made negative, since variables which // store one of these enum values usually also may be responsible for storing diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index 75166b36a..5b632bbce 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -535,6 +535,11 @@ private: vector secondary_bank_; + // Keep track of how many secondary particles were created in the collision + // and what the starting index is in the secondary bank for this particle + int n_secondaries_ {0}; + int secondary_bank_index_ {0}; + int64_t current_work_; vector flux_derivs_; @@ -689,7 +694,20 @@ public: // secondary particle bank SourceSite& secondary_bank(int i) { return secondary_bank_[i]; } + const SourceSite& secondary_bank(int i) const { return secondary_bank_[i]; } decltype(secondary_bank_)& secondary_bank() { return secondary_bank_; } + decltype(secondary_bank_) const& secondary_bank() const + { + return secondary_bank_; + } + + // Number of secondaries created in a collision + int& n_secondaries() { return n_secondaries_; } + const int& n_secondaries() const { return n_secondaries_; } + + // Starting index in secondary bank for this collision + int& secondary_bank_index() { return secondary_bank_index_; } + const int& secondary_bank_index() const { return secondary_bank_index_; } // Current simulation work index int64_t& current_work() { return current_work_; } diff --git a/include/openmc/tallies/filter.h b/include/openmc/tallies/filter.h index 65098597a..0fef97e50 100644 --- a/include/openmc/tallies/filter.h +++ b/include/openmc/tallies/filter.h @@ -39,6 +39,7 @@ enum class FilterType { MUSURFACE, PARENT_NUCLIDE, PARTICLE, + PARTICLE_PRODUCTION, POLAR, SPHERICAL_HARMONICS, SPATIAL_LEGENDRE, diff --git a/include/openmc/tallies/filter_energy.h b/include/openmc/tallies/filter_energy.h index cf8a8aa0f..6bc9ff6b1 100644 --- a/include/openmc/tallies/filter_energy.h +++ b/include/openmc/tallies/filter_energy.h @@ -1,6 +1,7 @@ #ifndef OPENMC_TALLIES_FILTER_ENERGY_H #define OPENMC_TALLIES_FILTER_ENERGY_H +#include "openmc/particle.h" #include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/vector.h" @@ -72,5 +73,36 @@ public: std::string text_label(int bin) const override; }; +//============================================================================== +//! Bins the outgoing energy of secondary particles +//! +//! This filter can be used to get the photon production matrix for multigroup +//! photon transport, the energy distribution of secondary neutrons, etc. Unlike +//! other energy filters, the weight that is applied is equal to the weight of +//! the secondary particle. Thus, to get secondary production it should be used +//! in conjunction with the "events" score. +//============================================================================== + +class ParticleProductionFilter : public EnergyFilter { +public: + //---------------------------------------------------------------------------- + // Methods + + std::string type_str() const override { return "particleproduction"; } + FilterType type() const override { return FilterType::PARTICLE_PRODUCTION; } + + void get_all_bins(const Particle& p, TallyEstimator estimator, + FilterMatch& match) const override; + + std::string text_label(int bin) const override; + + void from_xml(pugi::xml_node node) override; + + void to_statepoint(hid_t filter_group) const override; + +protected: + ParticleType secondary_type_; //!< Type of secondary particle to filter +}; + } // namespace openmc #endif // OPENMC_TALLIES_FILTER_ENERGY_H diff --git a/openmc/filter.py b/openmc/filter.py index 53ec93e21..7561f6be6 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -126,9 +126,9 @@ class Filter(IDManagerMixin, metaclass=FilterMeta): def __gt__(self, other): if type(self) is not type(other): if self.short_name in _FILTER_TYPES and \ - other.short_name in _FILTER_TYPES: + other.short_name in _FILTER_TYPES: delta = _FILTER_TYPES.index(self.short_name) - \ - _FILTER_TYPES.index(other.short_name) + _FILTER_TYPES.index(other.short_name) return delta > 0 else: return False @@ -275,7 +275,6 @@ class Filter(IDManagerMixin, metaclass=FilterMeta): if filter_type == subclass.short_name.lower(): return subclass.from_xml_element(elem, **kwargs) - def can_merge(self, other): """Determine if filter can be merged with another. @@ -426,6 +425,7 @@ class Filter(IDManagerMixin, metaclass=FilterMeta): class WithIDFilter(Filter): """Abstract parent for filters of types with IDs (Cell, Material, etc.).""" + def __init__(self, bins, filter_id=None): bins = np.atleast_1d(bins) @@ -629,6 +629,7 @@ class CellInstanceFilter(Filter): DistribcellFilter """ + def __init__(self, bins, filter_id=None): self.bins = bins self.id = filter_id @@ -749,6 +750,7 @@ class ParticleFilter(Filter): The number of filter bins """ + def __eq__(self, other): if type(self) is not type(other): return False @@ -1103,6 +1105,7 @@ class MeshMaterialFilter(MeshFilter): Unique identifier for the filter """ + def __init__(self, mesh: openmc.MeshBase, bins, filter_id=None): self.mesh = mesh self.bins = bins @@ -1437,6 +1440,7 @@ class RealFilter(Filter): The number of filter bins """ + def __init__(self, values, filter_id=None): self.values = np.asarray(values) self.bins = np.vstack((self.values[:-1], self.values[1:])).T @@ -1712,7 +1716,8 @@ class EnergyFilter(RealFilter): """ - cv.check_value('group_structure', group_structure, openmc.mgxs.GROUP_STRUCTURES.keys()) + cv.check_value('group_structure', group_structure, + openmc.mgxs.GROUP_STRUCTURES.keys()) return cls(openmc.mgxs.GROUP_STRUCTURES[group_structure.upper()]) @@ -1742,6 +1747,82 @@ class EnergyoutFilter(EnergyFilter): """ + +class ParticleProductionFilter(EnergyFilter): + """Bins tally events based on energy of secondary particles. + + This filter bins the energies of secondary particles (e.g., photons, + electrons, or recoils) produced in a reaction. This is useful for + constructing production matrices or analyzing secondary particle spectra. + Note that unlike other energy filters, the weight that is applied is equal + to the weight of the secondary particle. Thus, to obtain secondary particle + production, it should be used in conjunction with the "events" score. + + The incident particle type can be filtered using :class:`ParticleFilter`. + + .. versionadded:: 0.15.4 + + Parameters + ---------- + particle : str, int, openmc.ParticleType + Type of secondary particle to tally ('photon', 'neutron', etc.) + values : Iterable of Real + A list of energy boundaries in [eV]; each successive pair defines a bin. + filter_id : int, optional + Unique identifier for the filter + + Attributes + ---------- + values : numpy.ndarray + Energy boundaries in [eV] + bins : numpy.ndarray + Array of (low, high) energy bin pairs + num_bins : int + Number of filter bins + particle : str + The secondary particle type this filter applies to + """ + + def __init__(self, particle, values, filter_id=None): + super().__init__(values, filter_id) + self.particle = particle + + def __repr__(self): + string = type(self).__name__ + '\n' + string += '{: <16}=\t{}\n'.format('\tParticle', self.particle) + string += '{: <16}=\t{}\n'.format('\tValues', self.values) + string += '{: <16}=\t{}\n'.format('\tID', self.id) + return string + + @property + def particle(self) -> openmc.ParticleType: + return self._particle + + @particle.setter + def particle(self, particle): + self._particle = openmc.ParticleType(particle) + + def to_xml_element(self): + element = super().to_xml_element() + subelement = ET.SubElement(element, 'particle') + subelement.text = str(self.particle) + return element + + @classmethod + def from_xml_element(cls, elem, **kwargs): + filter_id = int(elem.get('id')) + values = [float(x) for x in get_text(elem, 'bins').split()] + particle = get_text(elem, 'particle') + return cls(particle, values, filter_id=filter_id) + + @classmethod + def from_hdf5(cls, group, **kwargs): + filter_id = int(group.name.split('/')[-1].lstrip('filter ')) + bins = group['bins'][()] + particle = group['particle'][()].decode() + return cls(particle, bins, filter_id=filter_id) + + class TimeFilter(RealFilter): """Bins tally events based on the particle's time. @@ -1909,7 +1990,7 @@ class DistribcellFilter(Filter): # Make sure there is only 1 bin. if not len(bins) == 1: msg = (f'Unable to add bins "{bins}" to a DistribcellFilter since ' - 'only a single distribcell can be used per tally') + 'only a single distribcell can be used per tally') raise ValueError(msg) # Check the type and extract the id, if necessary. @@ -2062,7 +2143,7 @@ class DistribcellFilter(Filter): # requests Summary geometric information filter_bins = _repeat_and_tile( np.arange(self.num_bins), stride, data_size) - df = pd.DataFrame({self.short_name.lower() : filter_bins}) + df = pd.DataFrame({self.short_name.lower(): filter_bins}) # Concatenate with DataFrame of distribcell instance IDs if level_df is not None: @@ -2101,6 +2182,7 @@ class MuFilter(RealFilter): The number of filter bins """ + def __init__(self, values, filter_id=None): if isinstance(values, Integral): values = np.linspace(-1., 1., values + 1) @@ -2266,6 +2348,7 @@ class DelayedGroupFilter(Filter): The number of filter bins """ + def check_bins(self, bins): # Check the bin values. for g in bins: @@ -2334,9 +2417,9 @@ class EnergyFunctionFilter(Filter): def __gt__(self, other): if type(self) is not type(other): if self.short_name in _FILTER_TYPES and \ - other.short_name in _FILTER_TYPES: + other.short_name in _FILTER_TYPES: delta = _FILTER_TYPES.index(self.short_name) - \ - _FILTER_TYPES.index(other.short_name) + _FILTER_TYPES.index(other.short_name) return delta > 0 else: return False @@ -2346,9 +2429,9 @@ class EnergyFunctionFilter(Filter): def __lt__(self, other): if type(self) is not type(other): if self.short_name in _FILTER_TYPES and \ - other.short_name in _FILTER_TYPES: + other.short_name in _FILTER_TYPES: delta = _FILTER_TYPES.index(self.short_name) - \ - _FILTER_TYPES.index(other.short_name) + _FILTER_TYPES.index(other.short_name) return delta < 0 else: return False @@ -2359,14 +2442,16 @@ class EnergyFunctionFilter(Filter): string = type(self).__name__ + '\n' string += '{: <16}=\t{}\n'.format('\tEnergy', self.energy) string += '{: <16}=\t{}\n'.format('\tInterpolant', self.y) - string += '{: <16}=\t{}\n'.format('\tInterpolation', self.interpolation) + string += '{: <16}=\t{}\n'.format('\tInterpolation', + self.interpolation) return hash(string) def __repr__(self): string = type(self).__name__ + '\n' string += '{: <16}=\t{}\n'.format('\tEnergy', self.energy) string += '{: <16}=\t{}\n'.format('\tInterpolant', self.y) - string += '{: <16}=\t{}\n'.format('\tInterpolation', self.interpolation) + string += '{: <16}=\t{}\n'.format('\tInterpolation', + self.interpolation) string += '{: <16}=\t{}\n'.format('\tID', self.id) return string @@ -2452,10 +2537,12 @@ class EnergyFunctionFilter(Filter): @interpolation.setter def interpolation(self, val): cv.check_type('interpolation', val, str) - cv.check_value('interpolation', val, self.INTERPOLATION_SCHEMES.values()) + cv.check_value('interpolation', val, + self.INTERPOLATION_SCHEMES.values()) if val == 'quadratic' and len(self.energy) < 3: - raise ValueError('Quadratic interpolation requires 3 or more values.') + raise ValueError( + 'Quadratic interpolation requires 3 or more values.') if val == 'cubic' and len(self.energy) < 4: raise ValueError('Cubic interpolation requires 3 or more values.') diff --git a/src/particle.cpp b/src/particle.cpp index a1176abc7..a035e76b9 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -87,6 +87,9 @@ bool Particle::create_secondary( return false; } + // Increment number of secondaries created (for ParticleProductionFilter) + n_secondaries()++; + auto& bank = secondary_bank().emplace_back(); bank.particle = type; bank.wgt = wgt; @@ -336,6 +339,7 @@ void Particle::event_cross_surface() void Particle::event_collide() { + // Score collision estimate of keff if (settings::run_mode == RunMode::EIGENVALUE && type().is_neutron()) { keff_tally_collision() += wgt() * macro_xs().nu_fission / macro_xs().total; @@ -383,6 +387,11 @@ void Particle::event_collide() n_bank() = 0; bank_second_E() = 0.0; wgt_bank() = 0.0; + + // Clear number of secondaries in this collision. This is + // distinct from the number of created neutrons n_bank() above! + n_secondaries() = 0; + zero_delayed_bank(); // Reset fission logical diff --git a/src/physics.cpp b/src/physics.cpp index 05a9e59d9..1f72e4fac 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -44,6 +44,7 @@ void collision(Particle& p) { // Add to collision counter for particle ++(p.n_collision()); + p.secondary_bank_index() = p.secondary_bank().size(); // Sample reaction for the material the particle is in switch (p.type().pdg_number()) { @@ -253,6 +254,7 @@ void create_fission_sites(Particle& p, int i_nuclide, const Reaction& rx) } } else { p.secondary_bank().push_back(site); + p.n_secondaries()++; } // Increment the number of neutrons born delayed diff --git a/src/physics_mg.cpp b/src/physics_mg.cpp index 866d4d728..58a21a678 100644 --- a/src/physics_mg.cpp +++ b/src/physics_mg.cpp @@ -27,6 +27,7 @@ void collision_mg(Particle& p) { // Add to the collision counter for the particle p.n_collision()++; + p.secondary_bank_index() = p.secondary_bank().size(); // Sample the reaction type sample_reaction(p); @@ -200,6 +201,7 @@ void create_fission_sites(Particle& p) } } else { p.secondary_bank().push_back(site); + p.n_secondaries()++; } // Set the delayed group on the particle as well diff --git a/src/tallies/filter.cpp b/src/tallies/filter.cpp index 79817981d..16e4c0550 100644 --- a/src/tallies/filter.cpp +++ b/src/tallies/filter.cpp @@ -146,6 +146,8 @@ Filter* Filter::create(const std::string& type, int32_t id) return Filter::create(id); } else if (type == "particle") { return Filter::create(id); + } else if (type == "particleproduction") { + return Filter::create(id); } else if (type == "polar") { return Filter::create(id); } else if (type == "surface") { diff --git a/src/tallies/filter_delayedgroup.cpp b/src/tallies/filter_delayedgroup.cpp index 01e39e554..46adf529b 100644 --- a/src/tallies/filter_delayedgroup.cpp +++ b/src/tallies/filter_delayedgroup.cpp @@ -27,7 +27,7 @@ void DelayedGroupFilter::set_groups(span groups) } else if (group > MAX_DELAYED_GROUPS) { throw std::invalid_argument { "Encountered delayedgroup bin with index " + std::to_string(group) + - " which is greater than MAX_DELATED_GROUPS (" + + " which is greater than MAX_DELAYED_GROUPS (" + std::to_string(MAX_DELAYED_GROUPS) + ")"}; } groups_.push_back(group); @@ -39,6 +39,10 @@ void DelayedGroupFilter::set_groups(span groups) void DelayedGroupFilter::get_all_bins( const Particle& p, TallyEstimator estimator, FilterMatch& match) const { + // Note that the bin is set to zero here, but bins outside zero are + // tallied to regardless. This is because that logic has to be handled + // in the scoring code instead where looping over the delayed + // group takes place (tally_scoring.cpp). match.bins_.push_back(0); match.weights_.push_back(1.0); } diff --git a/src/tallies/filter_energy.cpp b/src/tallies/filter_energy.cpp index 0b954cce3..0b039440a 100644 --- a/src/tallies/filter_energy.cpp +++ b/src/tallies/filter_energy.cpp @@ -117,6 +117,50 @@ std::string EnergyoutFilter::text_label(int bin) const "Outgoing Energy [{}, {})", bins_.at(bin), bins_.at(bin + 1)); } +//============================================================================== +// ParticleProductionFilter implementation +//============================================================================== + +void ParticleProductionFilter::get_all_bins( + const Particle& p, TallyEstimator estimator, FilterMatch& match) const +{ + int start_idx = p.secondary_bank_index(); + int end_idx = start_idx + p.n_secondaries(); + + // Loop over secondary bank entries + for (int bank_idx = start_idx; bank_idx < end_idx; bank_idx++) { + // Check if this is the correct type of secondary, then match its energy if + // it's the right type + const auto& site = p.secondary_bank(bank_idx); + if (site.particle == secondary_type_) { + if (site.E >= bins_.front() && site.E <= bins_.back()) { + auto bin = lower_bound_index(bins_.begin(), bins_.end(), site.E); + match.bins_.push_back(bin); + match.weights_.push_back(site.wgt); + } + } + } +} + +std::string ParticleProductionFilter::text_label(int bin) const +{ + return fmt::format("Secondary {}, Energy [{}, {})", secondary_type_.str(), + bins_.at(bin), bins_.at(bin + 1)); +} + +void ParticleProductionFilter::from_xml(pugi::xml_node node) +{ + EnergyFilter::from_xml(node); + std::string p = get_node_value(node, "particle"); + secondary_type_ = ParticleType {p}; +} + +void ParticleProductionFilter::to_statepoint(hid_t filter_group) const +{ + EnergyFilter::to_statepoint(filter_group); + write_dataset(filter_group, "particle", secondary_type_.str()); +} + //============================================================================== // C-API functions //============================================================================== diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index 2432f5c2f..8cdf3a905 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -171,6 +171,8 @@ Tally::Tally(pugi::xml_node node) filt_type == FilterType::ZERNIKE || filt_type == FilterType::ZERNIKE_RADIAL) { estimator_ = TallyEstimator::COLLISION; + } else if (filt_type == FilterType::PARTICLE_PRODUCTION) { + estimator_ = TallyEstimator::ANALOG; } } diff --git a/tests/regression_tests/model_xml/photon_production_inputs_true.dat b/tests/regression_tests/model_xml/photon_production_inputs_true.dat index 07eebaa3e..cc618eaff 100644 --- a/tests/regression_tests/model_xml/photon_production_inputs_true.dat +++ b/tests/regression_tests/model_xml/photon_production_inputs_true.dat @@ -41,6 +41,16 @@ neutron photon electron positron + + neutron + + + 0.0 20000000.0 + + + 0.0 100000.0 300000.0 500000.0 2000000.0 20000000.0 + photon + 1 2 current @@ -63,5 +73,10 @@ total heating (n,gamma) analog + + 5 3 4 + events + analog + diff --git a/tests/regression_tests/photon_production/inputs_true.dat b/tests/regression_tests/photon_production/inputs_true.dat index 07eebaa3e..cc618eaff 100644 --- a/tests/regression_tests/photon_production/inputs_true.dat +++ b/tests/regression_tests/photon_production/inputs_true.dat @@ -41,6 +41,16 @@ neutron photon electron positron + + neutron + + + 0.0 20000000.0 + + + 0.0 100000.0 300000.0 500000.0 2000000.0 20000000.0 + photon + 1 2 current @@ -63,5 +73,10 @@ total heating (n,gamma) analog + + 5 3 4 + events + analog + diff --git a/tests/regression_tests/photon_production/results_true.dat b/tests/regression_tests/photon_production/results_true.dat index 413f6f0ca..ec319aa0e 100644 --- a/tests/regression_tests/photon_production/results_true.dat +++ b/tests/regression_tests/photon_production/results_true.dat @@ -138,3 +138,14 @@ tally 4: 2.173936E+08 0.000000E+00 0.000000E+00 +tally 5: +2.000000E-02 +4.000000E-04 +8.200000E-03 +6.724000E-05 +5.280000E-02 +2.787840E-03 +3.546000E-01 +1.257412E-01 +5.063000E-01 +2.563397E-01 diff --git a/tests/regression_tests/photon_production/test.py b/tests/regression_tests/photon_production/test.py index 150448a12..65d0688e9 100644 --- a/tests/regression_tests/photon_production/test.py +++ b/tests/regression_tests/photon_production/test.py @@ -25,7 +25,8 @@ def model(): inner_cyl_right.region = -cyl & +x_plane_center & -x_plane_right outer_cyl.region = ~(-cyl & +x_plane_left & -x_plane_right) inner_cyl_right.fill = mat - model.geometry = openmc.Geometry([inner_cyl_left, inner_cyl_right, outer_cyl]) + model.geometry = openmc.Geometry( + [inner_cyl_left, inner_cyl_right, outer_cyl]) source = openmc.IndependentSource() source.space = openmc.stats.Point((0, 0, 0)) @@ -38,17 +39,19 @@ def model(): model.settings.batches = 1 model.settings.photon_transport = True model.settings.electron_treatment = 'ttb' - model.settings.cutoff = {'energy_photon' : 1000.0} + model.settings.cutoff = {'energy_photon': 1000.0} model.settings.source = source surface_filter = openmc.SurfaceFilter(cyl) - particle_filter = openmc.ParticleFilter(['neutron', 'photon', 'electron', 'positron']) + particle_filter = openmc.ParticleFilter( + ['neutron', 'photon', 'electron', 'positron']) current_tally = openmc.Tally() current_tally.filters = [surface_filter, particle_filter] current_tally.scores = ['current'] tally_tracklength = openmc.Tally() tally_tracklength.filters = [particle_filter] - tally_tracklength.scores = ['total', '(n,gamma)'] # heating doesn't work with tracklength + # heating doesn't work with tracklength + tally_tracklength.scores = ['total', '(n,gamma)'] tally_tracklength.nuclides = ['Al27', 'total'] tally_tracklength.estimator = 'tracklength' tally_collision = openmc.Tally() @@ -61,8 +64,25 @@ def model(): tally_analog.scores = ['total', 'heating', '(n,gamma)'] tally_analog.nuclides = ['Al27', 'total'] tally_analog.estimator = 'analog' + + # This is an analog tally tracking the energy distribution of photons + # generated by neutrons. The sum of the tally should give the total + # number of photons generated per source neutron. + ene_filter = openmc.EnergyFilter([0.0, 20e6]) # incident neutron energy + + # Track source energies of secondary gammas + ene2_filter = openmc.ParticleProductionFilter( + 'photon', [0.0, 100e3, 300e3, 500e3, 2e6, 20e6]) + + neutron_only = openmc.ParticleFilter(['neutron']) + tally_gam_ene = openmc.Tally() + tally_gam_ene.filters = [neutron_only, ene_filter, ene2_filter] + tally_gam_ene.scores = ['events'] + tally_gam_ene.estimator = 'analog' + model.tallies.extend([current_tally, tally_tracklength, - tally_collision, tally_analog]) + tally_collision, tally_analog, + tally_gam_ene]) return model diff --git a/tests/unit_tests/test_filters.py b/tests/unit_tests/test_filters.py index 8c56a310e..ba3226e3a 100644 --- a/tests/unit_tests/test_filters.py +++ b/tests/unit_tests/test_filters.py @@ -17,14 +17,16 @@ def box_model(): model.settings.particles = 100 model.settings.batches = 10 model.settings.inactive = 0 - model.settings.source = openmc.IndependentSource(space=openmc.stats.Point()) + model.settings.source = openmc.IndependentSource( + space=openmc.stats.Point()) return model def test_cell_instance(): c1 = openmc.Cell() c2 = openmc.Cell() - f = openmc.CellInstanceFilter([(c1, 0), (c1, 1), (c1, 2), (c2, 0), (c2, 1)]) + f = openmc.CellInstanceFilter( + [(c1, 0), (c1, 1), (c1, 2), (c2, 0), (c2, 1)]) # Make sure __repr__ works repr(f) @@ -234,7 +236,8 @@ def test_first_moment(run_in_tmpdir, box_model): flux, scatter = sp.tallies[plain_tally.id].mean.ravel() # Check that first moment matches - first_score = lambda t: sp.tallies[t.id].mean.ravel()[0] + def first_score(t): + return sp.tallies[t.id].mean.ravel()[0] assert first_score(leg_tally) == scatter assert first_score(leg_sptl_tally) == scatter assert first_score(sph_scat_tally) == scatter @@ -258,7 +261,8 @@ def test_lethargy_bin_width(): assert len(f.lethargy_bin_width) == 175 energy_bins = openmc.mgxs.GROUP_STRUCTURES['VITAMIN-J-175'] assert f.lethargy_bin_width[0] == np.log10(energy_bins[1]/energy_bins[0]) - assert f.lethargy_bin_width[-1] == np.log10(energy_bins[-1]/energy_bins[-2]) + assert f.lethargy_bin_width[-1] == np.log10( + energy_bins[-1]/energy_bins[-2]) def test_energyfunc(): @@ -292,7 +296,8 @@ def test_tabular_from_energyfilter(): # 'histogram' is the default assert tab.interpolation == 'histogram' - tab = efilter.get_tabular(values=np.array([10, 10, 5]), interpolation='linear-linear') + tab = efilter.get_tabular(values=np.array( + [10, 10, 5]), interpolation='linear-linear') assert tab.interpolation == 'linear-linear' @@ -314,6 +319,38 @@ def test_energy_filter(): openmc.EnergyFilter([-1.2, 0.25, 0.5]) +def test_particle_production_filter(): + energy_bins = [1e3, 1e4, 1e5, 1e6] + f = openmc.ParticleProductionFilter('photon', energy_bins) + + assert f.particle == openmc.ParticleType.PHOTON + assert f.num_bins == 3 + assert f.bins.shape == (3, 2) + assert np.allclose(f.values, energy_bins) + + # __repr__ check + repr(f) + + # to_xml_element() + elem = f.to_xml_element() + assert elem.tag == 'filter' + assert elem.attrib['type'] == 'particleproduction' + assert elem.find('particle').text == 'photon' + assert elem.find('bins').text.split()[0] == str(energy_bins[0]) + + # from_xml_element() + new_f = openmc.Filter.from_xml_element(elem) + assert new_f.id == f.id + assert new_f.particle == f.particle + assert np.allclose(new_f.bins, f.bins) + + # pandas output + df = f.get_pandas_dataframe(data_size=3, stride=1) + assert df.shape[0] == 3 + assert "particleproduction low [eV]" in df.columns + assert "particleproduction high [eV]" in df.columns + + def test_weight(): f = openmc.WeightFilter([0.01, 0.1, 1.0, 10.0]) expected_bins = [[0.01, 0.1], [0.1, 1.0], [1.0, 10.0]] From 6f625f3dea6c7d6b838d7b0a774a540b748b6996 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Fri, 6 Feb 2026 16:25:14 +0100 Subject: [PATCH 524/671] Precompute offsets in random ray source update (#3775) --- src/random_ray/flat_source_domain.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index d0c342bf5..32b334933 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -112,19 +112,19 @@ void FlatSourceDomain::update_single_neutron_source(SourceRegionHandle& srh) double density_mult = srh.density_mult(); if (material != MATERIAL_VOID) { double inverse_k_eff = 1.0 / k_eff_; + int material_offset = material * negroups_; + int scatter_offset = material * negroups_ * negroups_; for (int g_out = 0; g_out < negroups_; g_out++) { - double sigma_t = sigma_t_[material * negroups_ + g_out] * density_mult; + double sigma_t = sigma_t_[material_offset + g_out] * density_mult; double scatter_source = 0.0; double fission_source = 0.0; for (int g_in = 0; g_in < negroups_; g_in++) { double scalar_flux = srh.scalar_flux_old(g_in); - double sigma_s = sigma_s_[material * negroups_ * negroups_ + - g_out * negroups_ + g_in] * - density_mult; - double nu_sigma_f = - nu_sigma_f_[material * negroups_ + g_in] * density_mult; - double chi = chi_[material * negroups_ + g_out]; + double sigma_s = + sigma_s_[scatter_offset + g_out * negroups_ + g_in] * density_mult; + double nu_sigma_f = nu_sigma_f_[material_offset + g_in] * density_mult; + double chi = chi_[material_offset + g_out]; scatter_source += sigma_s * scalar_flux; if (settings::create_fission_neutrons) { From 8a62a9711525cf1e0193e3ec448d653b090de979 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Fri, 6 Feb 2026 17:23:11 +0100 Subject: [PATCH 525/671] openmc.Tally creation from constructor (#3777) --- openmc/tallies.py | 29 ++++++++++++++++++++++++++++- tests/unit_tests/test_tally.py | 19 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 tests/unit_tests/test_tally.py diff --git a/openmc/tallies.py b/openmc/tallies.py index 74d4722af..151add2be 100644 --- a/openmc/tallies.py +++ b/openmc/tallies.py @@ -50,6 +50,18 @@ class Tally(IDManagerMixin): will automatically be assigned name : str, optional Name of the tally. If not specified, the name is the empty string. + scores : list of str, optional + List of scores, e.g. ['flux', 'fission'] + filters : list of openmc.Filter, optional + List of filters for the tally + nuclides : list of str, optional + List of nuclides to score results for + estimator : {'analog', 'tracklength', 'collision'}, optional + Type of estimator for the tally + triggers : list of openmc.Trigger, optional + List of tally triggers + derivative : openmc.TallyDerivative, optional + A material perturbation derivative to apply to all scores in the tally Attributes ---------- @@ -124,7 +136,9 @@ class Tally(IDManagerMixin): next_id = 1 used_ids = set() - def __init__(self, tally_id=None, name=''): + def __init__(self, tally_id=None, name='', scores=None, filters=None, + nuclides=None, estimator=None, triggers=None, + derivative=None): # Initialize Tally class attributes self.id = tally_id self.name = name @@ -155,6 +169,19 @@ class Tally(IDManagerMixin): self._sp_filename = None self._results_read = False + if filters is not None: + self.filters = filters + if nuclides is not None: + self.nuclides = nuclides + if scores is not None: + self.scores = scores + if estimator is not None: + self.estimator = estimator + if triggers is not None: + self.triggers = triggers + if derivative is not None: + self.derivative = derivative + def __eq__(self, other): if other.id != self.id: return False diff --git a/tests/unit_tests/test_tally.py b/tests/unit_tests/test_tally.py new file mode 100644 index 000000000..52647a36c --- /dev/null +++ b/tests/unit_tests/test_tally.py @@ -0,0 +1,19 @@ +import openmc + + +def test_tally_init_args(): + """Test that Tally constructor kwargs are applied correctly.""" + filter = openmc.EnergyFilter([0.0, 1.0, 20.0e6]) + tally = openmc.Tally( + name='my tally', + scores=['flux', 'fission'], + filters=[filter], + nuclides=['U235'], + estimator='tracklength', + ) + + assert tally.name == 'my tally' + assert tally.scores == ['flux', 'fission'] + assert tally.filters == [filter] + assert tally.nuclides == ['U235'] + assert tally.estimator == 'tracklength' From 14acc762bc0c3ce85b5c4045062304703afab59b Mon Sep 17 00:00:00 2001 From: Kevin Sawatzky <66632997+nuclearkevin@users.noreply.github.com> Date: Fri, 6 Feb 2026 10:42:32 -0600 Subject: [PATCH 526/671] C-API bindings for key random ray functions (#3749) --- docs/source/capi/index.rst | 4 + docs/source/pythonapi/capi.rst | 1 + include/openmc/capi.h | 1 + .../openmc/random_ray/random_ray_simulation.h | 3 +- openmc/lib/core.py | 13 ++ src/finalize.cpp | 3 +- src/random_ray/random_ray_simulation.cpp | 113 +++++++++--------- tests/unit_tests/test_lib.py | 26 ++++ 8 files changed, 107 insertions(+), 57 deletions(-) diff --git a/docs/source/capi/index.rst b/docs/source/capi/index.rst index 2583d51df..0a1962f77 100644 --- a/docs/source/capi/index.rst +++ b/docs/source/capi/index.rst @@ -601,6 +601,10 @@ Functions :return: Return status (negative if an error occurs) :rtype: int +.. c:function:: void openmc_run_random_ray() + + Run a random ray simulation + .. c:function:: int openmc_set_n_batches(int32_t n_batches, bool set_max_batches, bool add_statepoint_batch) Set number of batches and number of max batches diff --git a/docs/source/pythonapi/capi.rst b/docs/source/pythonapi/capi.rst index 67eca0094..6f18db56f 100644 --- a/docs/source/pythonapi/capi.rst +++ b/docs/source/pythonapi/capi.rst @@ -40,6 +40,7 @@ Functions reset_timers run run_in_memory + run_random_ray sample_external_source simulation_finalize simulation_init diff --git a/include/openmc/capi.h b/include/openmc/capi.h index 019a418a1..533ebb65a 100644 --- a/include/openmc/capi.h +++ b/include/openmc/capi.h @@ -140,6 +140,7 @@ int openmc_remove_tally(int32_t index); int openmc_reset(); int openmc_reset_timers(); int openmc_run(); +void openmc_run_random_ray(); int openmc_sample_external_source(size_t n, uint64_t* seed, void* sites); void openmc_set_seed(int64_t new_seed); void openmc_set_stride(uint64_t new_stride); diff --git a/include/openmc/random_ray/random_ray_simulation.h b/include/openmc/random_ray/random_ray_simulation.h index 3db651069..68c7779ed 100644 --- a/include/openmc/random_ray/random_ray_simulation.h +++ b/include/openmc/random_ray/random_ray_simulation.h @@ -66,10 +66,9 @@ private: //! Non-member functions //============================================================================ -void openmc_run_random_ray(); void validate_random_ray_inputs(); -void openmc_reset_random_ray(); void print_adjoint_header(); +void openmc_finalize_random_ray(); } // namespace openmc diff --git a/openmc/lib/core.py b/openmc/lib/core.py index 79310bb37..2764ddff1 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -82,6 +82,7 @@ _dll.openmc_properties_import.restype = c_int _dll.openmc_properties_import.errcheck = _error_handler _dll.openmc_run.restype = c_int _dll.openmc_run.errcheck = _error_handler +_dll.openmc_run_random_ray.restype = None _dll.openmc_reset.restype = c_int _dll.openmc_reset.errcheck = _error_handler _dll.openmc_reset_timers.restype = c_int @@ -479,6 +480,18 @@ def run(output=True): _dll.openmc_run() +def run_random_ray(output=True): + """Run a random ray simulation + + Parameters + ---------- + output : bool, optional + Whether or not to show output. Defaults to showing output + """ + + with quiet_dll(output): + _dll.openmc_run_random_ray() + def sample_external_source( n_samples: int = 1000, prn_seed: int | None = None diff --git a/src/finalize.cpp b/src/finalize.cpp index 344eaa1a0..ff5d79cb0 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -122,6 +122,7 @@ int openmc_finalize() settings::restart_run = false; settings::run_CE = true; settings::run_mode = RunMode::UNSET; + settings::solver_type = SolverType::MONTE_CARLO; settings::source_latest = false; settings::source_rejection_fraction = 0.05; settings::source_separate = false; @@ -184,7 +185,7 @@ int openmc_finalize() } #endif - openmc_reset_random_ray(); + openmc_finalize_random_ray(); return 0; } diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index 7bab3a9b1..af3916e3f 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -1,5 +1,6 @@ #include "openmc/random_ray/random_ray_simulation.h" +#include "openmc/capi.h" #include "openmc/eigenvalue.h" #include "openmc/geometry.h" #include "openmc/message_passing.h" @@ -22,49 +23,6 @@ namespace openmc { // Non-member functions //============================================================================== -void openmc_run_random_ray() -{ - ////////////////////////////////////////////////////////// - // Run forward simulation - ////////////////////////////////////////////////////////// - - if (mpi::master) { - if (FlatSourceDomain::adjoint_) { - FlatSourceDomain::adjoint_ = false; - openmc::print_adjoint_header(); - FlatSourceDomain::adjoint_ = true; - } - } - - // Initialize OpenMC general data structures - openmc_simulation_init(); - - // Validate that inputs meet requirements for random ray mode - if (mpi::master) - validate_random_ray_inputs(); - - // Initialize Random Ray Simulation Object - RandomRaySimulation sim; - - // Initialize fixed sources, if present - sim.apply_fixed_sources_and_mesh_domains(); - - // Run initial random ray simulation - sim.simulate(); - - ////////////////////////////////////////////////////////// - // Run adjoint simulation (if enabled) - ////////////////////////////////////////////////////////// - - if (sim.adjoint_needed_) { - // Setup for adjoint simulation - sim.prepare_adjoint_simulation(); - - // Run adjoint simulation - sim.simulate(); - } -} - // Enforces restrictions on inputs in random ray mode. While there are // many features that don't make sense in random ray mode, and are therefore // unsupported, we limit our testing/enforcement operations only to inputs @@ -283,17 +241,6 @@ void validate_random_ray_inputs() } } -void openmc_reset_random_ray() -{ - FlatSourceDomain::volume_estimator_ = RandomRayVolumeEstimator::HYBRID; - FlatSourceDomain::volume_normalized_flux_tallies_ = false; - FlatSourceDomain::adjoint_ = false; - FlatSourceDomain::mesh_domain_map_.clear(); - RandomRay::ray_source_.reset(); - RandomRay::source_shape_ = RandomRaySourceShape::FLAT; - RandomRay::sample_method_ = RandomRaySampleMethod::PRNG; -} - void print_adjoint_header() { if (!FlatSourceDomain::adjoint_) @@ -670,4 +617,62 @@ void RandomRaySimulation::print_results_random_ray( } } +void openmc_finalize_random_ray() +{ + FlatSourceDomain::volume_estimator_ = RandomRayVolumeEstimator::HYBRID; + FlatSourceDomain::volume_normalized_flux_tallies_ = false; + FlatSourceDomain::adjoint_ = false; + FlatSourceDomain::mesh_domain_map_.clear(); + RandomRay::ray_source_.reset(); + RandomRay::source_shape_ = RandomRaySourceShape::FLAT; + RandomRay::sample_method_ = RandomRaySampleMethod::PRNG; +} + } // namespace openmc + +//============================================================================== +// C API functions +//============================================================================== + +void openmc_run_random_ray() +{ + ////////////////////////////////////////////////////////// + // Run forward simulation + ////////////////////////////////////////////////////////// + + if (openmc::mpi::master) { + if (openmc::FlatSourceDomain::adjoint_) { + openmc::FlatSourceDomain::adjoint_ = false; + openmc::print_adjoint_header(); + openmc::FlatSourceDomain::adjoint_ = true; + } + } + + // Initialize OpenMC general data structures + openmc_simulation_init(); + + // Validate that inputs meet requirements for random ray mode + if (openmc::mpi::master) + openmc::validate_random_ray_inputs(); + + // Initialize Random Ray Simulation Object + openmc::RandomRaySimulation sim; + + // Initialize fixed sources, if present + sim.apply_fixed_sources_and_mesh_domains(); + + // Run initial random ray simulation + sim.simulate(); + + ////////////////////////////////////////////////////////// + // Run adjoint simulation (if enabled) + ////////////////////////////////////////////////////////// + + if (sim.adjoint_needed_) { + // Setup for adjoint simulation + sim.prepare_adjoint_simulation(); + + // Run adjoint simulation + sim.simulate(); + } +} diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index e5a4d198e..3ec2a9ae4 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -5,6 +5,7 @@ import os import numpy as np import pytest import openmc +from openmc.examples import random_ray_pin_cell import openmc.exceptions as exc import openmc.lib @@ -83,6 +84,19 @@ def uo2_trigger_model(): yield +@pytest.fixture(scope='module') +def random_ray_pincell_model(): + """Set up a random ray model to test with and delete files when done""" + openmc.reset_auto_ids() + # Write XML and MGXS files in tmpdir + with cdtemp(): + model = random_ray_pin_cell() + model.settings.batches = 200 + model.settings.inactive = 50 + model.settings.particles = 50 + model.export_to_xml() + yield + @pytest.fixture(scope='module') def lib_init(pincell_model, mpi_intracomm): openmc.lib.init(intracomm=mpi_intracomm) @@ -1052,3 +1066,15 @@ def test_sample_external_source(run_in_tmpdir, mpi_intracomm): openmc.lib.init(["-c"]) openmc.lib.sample_external_source(100) openmc.lib.finalize() + + +def test_random_ray(random_ray_pincell_model, mpi_intracomm): + openmc.lib.finalize() + openmc.lib.init(intracomm=mpi_intracomm) + openmc.lib.simulation_init() + openmc.lib.run_random_ray() + keff = openmc.lib.keff() + + assert keff[0]==pytest.approx(1.3236826574065745) + + openmc.lib.finalize() From a6324292676feba591ca58e52d2809a6f864607a Mon Sep 17 00:00:00 2001 From: Lewis Gross <43077972+lewisgross1296@users.noreply.github.com> Date: Fri, 6 Feb 2026 11:32:32 -0600 Subject: [PATCH 527/671] Move distrib (mat, temp, dens) from XML attribute to subelement for better compatibility with very large instance lists (#3774) --- openmc/cell.py | 14 +++++++++----- tests/regression_tests/distribmat/inputs_true.dat | 4 +++- .../lattice_distribmat/True/inputs_true.dat | 8 ++++++-- .../lattice_distribrho/inputs_true.dat | 4 +++- tests/regression_tests/multipole/inputs_true.dat | 4 +++- .../random_ray_cell_density/eigen/inputs_true.dat | 12 +++++++++--- 6 files changed, 33 insertions(+), 13 deletions(-) diff --git a/openmc/cell.py b/openmc/cell.py index 82a034b1c..945dff0db 100644 --- a/openmc/cell.py +++ b/openmc/cell.py @@ -636,8 +636,11 @@ class Cell(IDManagerMixin): element.set("material", str(self.fill.id)) elif self.fill_type == 'distribmat': - element.set("material", ' '.join(['void' if m is None else str(m.id) - for m in self.fill])) + material_subelement= ET.SubElement(element, "material") + matlist_str = " ".join( + ["void" if m is None else str(m.id) for m in self.fill] + ) + material_subelement.text = matlist_str elif self.fill_type in ('universe', 'lattice'): element.set("fill", str(self.fill.id)) @@ -677,14 +680,15 @@ class Cell(IDManagerMixin): if self.temperature is not None: if isinstance(self.temperature, Iterable): - element.set("temperature", ' '.join( - str(t) for t in self.temperature)) + temperature_subelement= ET.SubElement(element, "temperature") + temperature_subelement.text = ' '.join(str(t) for t in self.temperature) else: element.set("temperature", str(self.temperature)) if self.density is not None: if isinstance(self.density, Iterable): - element.set("density", ' '.join(str(t) for t in self.density)) + density_subelement= ET.SubElement(element, "density") + density_subelement.text = ' '.join(str(d) for d in self.density) else: element.set("density", str(self.density)) diff --git a/tests/regression_tests/distribmat/inputs_true.dat b/tests/regression_tests/distribmat/inputs_true.dat index 35b3b5b9f..8087d7564 100644 --- a/tests/regression_tests/distribmat/inputs_true.dat +++ b/tests/regression_tests/distribmat/inputs_true.dat @@ -17,7 +17,9 @@ - + + 2 void 3 2 + diff --git a/tests/regression_tests/lattice_distribmat/True/inputs_true.dat b/tests/regression_tests/lattice_distribmat/True/inputs_true.dat index aec3a5400..ab827dfde 100644 --- a/tests/regression_tests/lattice_distribmat/True/inputs_true.dat +++ b/tests/regression_tests/lattice_distribmat/True/inputs_true.dat @@ -47,8 +47,12 @@ - - + + 1 2 3 4 + + + 5 6 7 8 + diff --git a/tests/regression_tests/lattice_distribrho/inputs_true.dat b/tests/regression_tests/lattice_distribrho/inputs_true.dat index 5031bea6e..99994af3d 100644 --- a/tests/regression_tests/lattice_distribrho/inputs_true.dat +++ b/tests/regression_tests/lattice_distribrho/inputs_true.dat @@ -14,7 +14,9 @@ - + + 10.0 20.0 10.0 20.0 + diff --git a/tests/regression_tests/multipole/inputs_true.dat b/tests/regression_tests/multipole/inputs_true.dat index 22a351240..3b78db77b 100644 --- a/tests/regression_tests/multipole/inputs_true.dat +++ b/tests/regression_tests/multipole/inputs_true.dat @@ -14,7 +14,9 @@ - + + 500 700 0 800 + diff --git a/tests/regression_tests/random_ray_cell_density/eigen/inputs_true.dat b/tests/regression_tests/random_ray_cell_density/eigen/inputs_true.dat index 0dd354cf0..bcf4d0d90 100644 --- a/tests/regression_tests/random_ray_cell_density/eigen/inputs_true.dat +++ b/tests/regression_tests/random_ray_cell_density/eigen/inputs_true.dat @@ -12,9 +12,15 @@ - - - + + 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 1.0 1.0 1.0 1.0 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.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 1.0 1.0 1.0 1.0 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.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 + From 04bee9c49fc95836cd0324992d69deed72aa1e09 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Fri, 6 Feb 2026 20:44:16 +0200 Subject: [PATCH 528/671] Check that Surface IDs are at least 1 (#3772) Co-authored-by: Paul Romano --- openmc/mixin.py | 4 +++- openmc/surface.py | 1 + tests/unit_tests/test_cell.py | 6 ++++++ tests/unit_tests/test_material.py | 7 +++++++ tests/unit_tests/test_surface.py | 6 ++++++ tests/unit_tests/test_universe.py | 6 ++++++ 6 files changed, 29 insertions(+), 1 deletion(-) diff --git a/openmc/mixin.py b/openmc/mixin.py index 0bc4128b0..28c97bfdd 100644 --- a/openmc/mixin.py +++ b/openmc/mixin.py @@ -39,6 +39,8 @@ class IDManagerMixin: """ + min_id = 0 + @property def id(self): return self._id @@ -64,7 +66,7 @@ class IDManagerMixin: else: name = cls.__name__ cv.check_type(f'{name} ID', uid, Integral) - cv.check_greater_than(f'{name} ID', uid, 0, equality=True) + cv.check_greater_than(f'{name} ID', uid, cls.min_id, equality=True) if uid in cls.used_ids: msg = f'Another {name} instance already exists with id={uid}.' warn(msg, IDWarning) diff --git a/openmc/surface.py b/openmc/surface.py index 1fe5fabdf..97aca43b3 100644 --- a/openmc/surface.py +++ b/openmc/surface.py @@ -151,6 +151,7 @@ class Surface(IDManagerMixin, ABC): """ + min_id = 1 next_id = 1 used_ids = set() _atol = 1.e-12 diff --git a/tests/unit_tests/test_cell.py b/tests/unit_tests/test_cell.py index 60b205818..2baa59f1b 100644 --- a/tests/unit_tests/test_cell.py +++ b/tests/unit_tests/test_cell.py @@ -20,6 +20,12 @@ def test_contains(): assert (10.0, -4., 2.0) in c +def test_id(): + openmc.Cell(cell_id=0) + with pytest.raises(ValueError): + openmc.Cell(cell_id=-1) + + def test_repr(cell_with_lattice): cells, mats, univ, lattice = cell_with_lattice repr(cells[0]) # cell with distributed materials diff --git a/tests/unit_tests/test_material.py b/tests/unit_tests/test_material.py index 764c98d41..0b1b5fce2 100644 --- a/tests/unit_tests/test_material.py +++ b/tests/unit_tests/test_material.py @@ -77,6 +77,13 @@ def test_add_components(): with pytest.raises(ValueError): m.add_components({'H1': 1.0}, percent_type = 'oa') + +def test_id(): + openmc.Material(material_id=0) + with pytest.raises(ValueError): + openmc.Material(material_id=-1) + + def test_nuclides_to_ignore(run_in_tmpdir): """Test nuclides_to_ignore when exporting a material to XML""" m = openmc.Material() diff --git a/tests/unit_tests/test_surface.py b/tests/unit_tests/test_surface.py index e9560223d..aaf591e05 100644 --- a/tests/unit_tests/test_surface.py +++ b/tests/unit_tests/test_surface.py @@ -7,6 +7,12 @@ import openmc import pytest +def test_id(): + for i in range(-10, 1): + with pytest.raises(ValueError): + openmc.Plane(a=1, b=2, c=-1, d=3, surface_id=i) + + def assert_infinite_bb(s): ll, ur = (-s).bounding_box assert np.all(np.isinf(ll)) diff --git a/tests/unit_tests/test_universe.py b/tests/unit_tests/test_universe.py index 6fbf1cc38..0955347f7 100644 --- a/tests/unit_tests/test_universe.py +++ b/tests/unit_tests/test_universe.py @@ -49,6 +49,12 @@ def test_bounding_box(): assert_unbounded(u) +def test_id(): + openmc.Universe(universe_id=0) + with pytest.raises(ValueError): + openmc.Universe(universe_id=-1) + + def test_plot(run_in_tmpdir, sphere_model): # model with -inf and inf in the bounding box From 6efc9db7b3a6b6fc99da52c0f8791fa5bd843f0b Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Sun, 8 Feb 2026 12:11:19 -0600 Subject: [PATCH 529/671] Modifications to C++ plots for interactive raytrace plots (#3776) --- include/openmc/plot.h | 81 +++++++++++++----- src/plot.cpp | 193 +++++++++++++++++++++++------------------- 2 files changed, 170 insertions(+), 104 deletions(-) diff --git a/include/openmc/plot.h b/include/openmc/plot.h index 7e27679ea..02ff2848d 100644 --- a/include/openmc/plot.h +++ b/include/openmc/plot.h @@ -88,14 +88,21 @@ const RGBColor BLACK {0, 0, 0}; * is designed to be implemented by classes that produce plot-relevant data * which can be visualized. */ + +typedef xt::xtensor ImageData; class PlottableInterface { +public: + PlottableInterface() = default; + + void set_default_colors(); + private: void set_id(pugi::xml_node plot_node); int id_; // unique plot ID void set_bg_color(pugi::xml_node plot_node); void set_universe(pugi::xml_node plot_node); - void set_default_colors(pugi::xml_node plot_node); + void set_color_by(pugi::xml_node plot_node); void set_user_colors(pugi::xml_node plot_node); void set_overlap_color(pugi::xml_node plot_node); void set_mask(pugi::xml_node plot_node); @@ -107,9 +114,15 @@ protected: public: enum class PlotColorBy { cells = 0, mats = 1 }; + // Generates image data based on plot parameters and returns it + virtual ImageData create_image() const = 0; + // Creates the output image named path_plot_ virtual void create_output() const = 0; + // Write populated image data to file + void write_image(const ImageData& data) const; + // Print useful info to the terminal virtual void print_info() const = 0; @@ -117,20 +130,19 @@ public: std::string& path_plot() { return path_plot_; } int id() const { return id_; } int level() const { return level_; } + PlotColorBy color_by() const { return color_by_; } // Public color-related data PlottableInterface(pugi::xml_node plot_node); virtual ~PlottableInterface() = default; - int level_; // Universe level to plot - bool color_overlaps_; // Show overlapping cells? + int level_ {-1}; // Universe level to plot + bool color_overlaps_ {false}; // Show overlapping cells? PlotColorBy color_by_; // Plot coloring (cell/material) RGBColor not_found_ {WHITE}; // Plot background color RGBColor overlap_color_ {RED}; // Plot overlap color vector colors_; // Plot colors }; -typedef xt::xtensor ImageData; - struct IdData { // Constructor IdData(size_t h_res, size_t v_res); @@ -166,6 +178,11 @@ public: enum class PlotBasis { xy = 1, xz = 2, yz = 3 }; + // Accessors + + const std::array& pixels() const { return pixels_; } + std::array& pixels() { return pixels_; } + // Members public: Position origin_; //!< Plot origin in geometry @@ -270,11 +287,11 @@ private: public: // Add mesh lines to ImageData void draw_mesh_lines(ImageData& data) const; - void create_image() const; + ImageData create_image() const override; void create_voxel() const; - virtual void create_output() const; - virtual void print_info() const; + void create_output() const override; + void print_info() const override; PlotType type_; //!< Plot type (Slice/Voxel) int meshlines_width_; //!< Width of lines added to the plot @@ -294,17 +311,32 @@ public: */ class RayTracePlot : public PlottableInterface { public: + RayTracePlot() = default; RayTracePlot(pugi::xml_node plot); // Standard getters. No setting since it's done from XML. const Position& camera_position() const { return camera_position_; } + Position& camera_position() { return camera_position_; } const Position& look_at() const { return look_at_; } + Position& look_at() { return look_at_; } + const double& horizontal_field_of_view() const { return horizontal_field_of_view_; } + double& horizontal_field_of_view() { return horizontal_field_of_view_; } - virtual void print_info() const; + void print_info() const override; + + const std::array& pixels() const { return pixels_; } + std::array& pixels() { return pixels_; } + + const Direction& up() const { return up_; } + Direction& up() { return up_; } + + //! brief Updates the cached camera-to-model matrix after changes to + //! camera parameters. + void update_view(); protected: Direction camera_x_axis() const @@ -330,8 +362,6 @@ protected: */ std::pair get_pixel_ray(int horiz, int vert) const; - std::array pixels_; // pixel dimension of resulting image - private: void set_look_at(pugi::xml_node node); void set_camera_position(pugi::xml_node node); @@ -341,8 +371,8 @@ private: double horizontal_field_of_view_ {70.0}; // horiz. f.o.v. in degrees Position camera_position_; // where camera is - Position look_at_; // point camera is centered looking at - + Position look_at_; // point camera is centered looking at + std::array pixels_; // pixel dimension of resulting image Direction up_ {0.0, 0.0, 1.0}; // which way is up /* The horizontal thickness, if using an orthographic projection. @@ -377,8 +407,9 @@ class WireframeRayTracePlot : public RayTracePlot { public: WireframeRayTracePlot(pugi::xml_node plot); - virtual void create_output() const; - virtual void print_info() const; + ImageData create_image() const override; + void create_output() const override; + void print_info() const override; private: void set_opacities(pugi::xml_node node); @@ -434,10 +465,22 @@ class SolidRayTracePlot : public RayTracePlot { friend class PhongRay; public: + SolidRayTracePlot() = default; + SolidRayTracePlot(pugi::xml_node plot); - virtual void create_output() const; - virtual void print_info() const; + ImageData create_image() const override; + void create_output() const override; + void print_info() const override; + + const std::unordered_set& opaque_ids() const { return opaque_ids_; } + std::unordered_set& opaque_ids() { return opaque_ids_; } + + const Position& light_location() const { return light_location_; } + Position& light_location() { return light_location_; } + + const double& diffuse_fraction() const { return diffuse_fraction_; } + double& diffuse_fraction() { return diffuse_fraction_; } private: void set_opaque_ids(pugi::xml_node node); @@ -496,7 +539,7 @@ public: : Ray(r, u), plot_(plot), line_segments_(line_segments) {} - virtual void on_intersection() override; + void on_intersection() override; private: /* Store a reference to the plot object which is running this ray, in order @@ -519,7 +562,7 @@ public: result_color_ = plot_.not_found_; } - virtual void on_intersection() override; + void on_intersection() override; const RGBColor& result_color() { return result_color_; } diff --git a/src/plot.cpp b/src/plot.cpp index 2cadc48ce..daeb0bde7 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -15,6 +15,7 @@ #include #endif +#include "openmc/cell.h" #include "openmc/constants.h" #include "openmc/container_util.h" #include "openmc/dagmc.h" @@ -123,11 +124,21 @@ extern "C" int openmc_plot_geometry() return 0; } +void PlottableInterface::write_image(const ImageData& data) const +{ +#ifdef USE_LIBPNG + output_png(path_plot(), data); +#else + output_ppm(path_plot(), data); +#endif +} + void Plot::create_output() const { if (PlotType::slice == type_) { // create 2D image - create_image(); + ImageData image = create_image(); + write_image(image); } else if (PlotType::voxel == type_) { // create voxel file for 3D viewing create_voxel(); @@ -170,9 +181,9 @@ void Plot::print_info() const fmt::print("Basis: YZ\n"); break; } - fmt::print("Pixels: {} {}\n", pixels_[0], pixels_[1]); + fmt::print("Pixels: {} {}\n", pixels()[0], pixels()[1]); } else if (PlotType::voxel == type_) { - fmt::print("Voxels: {} {} {}\n", pixels_[0], pixels_[1], pixels_[2]); + fmt::print("Voxels: {} {} {}\n", pixels()[0], pixels()[1], pixels()[2]); } } @@ -234,11 +245,10 @@ void free_memory_plot() // creates an image based on user input from a plots.xml // specification in the PNG/PPM format -void Plot::create_image() const +ImageData Plot::create_image() const { - - size_t width = pixels_[0]; - size_t height = pixels_[1]; + size_t width = pixels()[0]; + size_t height = pixels()[1]; ImageData data({width, height}, not_found_); @@ -275,12 +285,7 @@ void Plot::create_image() const draw_mesh_lines(data); } -// create image file -#ifdef USE_LIBPNG - output_png(path_plot(), data); -#else - output_ppm(path_plot(), data); -#endif + return data; } void PlottableInterface::set_id(pugi::xml_node plot_node) @@ -348,17 +353,17 @@ void Plot::set_output_path(pugi::xml_node plot_node) vector pxls = get_node_array(plot_node, "pixels"); if (PlotType::slice == type_) { if (pxls.size() == 2) { - pixels_[0] = pxls[0]; - pixels_[1] = pxls[1]; + pixels()[0] = pxls[0]; + pixels()[1] = pxls[1]; } else { fatal_error( fmt::format(" must be length 2 in slice plot {}", id())); } } else if (PlotType::voxel == type_) { if (pxls.size() == 3) { - pixels_[0] = pxls[0]; - pixels_[1] = pxls[1]; - pixels_[2] = pxls[2]; + pixels()[0] = pxls[0]; + pixels()[1] = pxls[1]; + pixels()[2] = pxls[2]; } else { fatal_error( fmt::format(" must be length 3 in voxel plot {}", id())); @@ -447,23 +452,31 @@ void PlottableInterface::set_universe(pugi::xml_node plot_node) } } -void PlottableInterface::set_default_colors(pugi::xml_node plot_node) +void PlottableInterface::set_color_by(pugi::xml_node plot_node) { - // Copy plot color type and initialize all colors randomly + // Copy plot color type std::string pl_color_by = "cell"; if (check_for_node(plot_node, "color_by")) { pl_color_by = get_node_value(plot_node, "color_by", true); } if ("cell" == pl_color_by) { color_by_ = PlotColorBy::cells; - colors_.resize(model::cells.size()); } else if ("material" == pl_color_by) { color_by_ = PlotColorBy::mats; - colors_.resize(model::materials.size()); } else { fatal_error(fmt::format( "Unsupported plot color type '{}' in plot {}", pl_color_by, id())); } +} + +void PlottableInterface::set_default_colors() +{ + // Copy plot color type and initialize all colors randomly + if (PlotColorBy::cells == color_by_) { + colors_.resize(model::cells.size()); + } else if (PlotColorBy::mats == color_by_) { + colors_.resize(model::materials.size()); + } for (auto& c : colors_) { c = random_color(); @@ -710,7 +723,8 @@ PlottableInterface::PlottableInterface(pugi::xml_node plot_node) set_id(plot_node); set_bg_color(plot_node); set_universe(plot_node); - set_default_colors(plot_node); + set_color_by(plot_node); + set_default_colors(); set_user_colors(plot_node); set_mask(plot_node); set_overlap_color(plot_node); @@ -857,27 +871,27 @@ void Plot::draw_mesh_lines(ImageData& data) const int ax2_min, ax2_max; if (axis_lines.second.size() > 0) { double frac = (axis_lines.second.back() - ll_plot[ax2]) / width[ax2]; - ax2_min = (1.0 - frac) * pixels_[1]; + ax2_min = (1.0 - frac) * pixels()[1]; if (ax2_min < 0) ax2_min = 0; frac = (axis_lines.second.front() - ll_plot[ax2]) / width[ax2]; - ax2_max = (1.0 - frac) * pixels_[1]; - if (ax2_max > pixels_[1]) - ax2_max = pixels_[1]; + ax2_max = (1.0 - frac) * pixels()[1]; + if (ax2_max > pixels()[1]) + ax2_max = pixels()[1]; } else { ax2_min = 0; - ax2_max = pixels_[1]; + ax2_max = pixels()[1]; } // Iterate across the first axis and draw lines. for (auto ax1_val : axis_lines.first) { double frac = (ax1_val - ll_plot[ax1]) / width[ax1]; - int ax1_ind = frac * pixels_[0]; + int ax1_ind = frac * pixels()[0]; for (int ax2_ind = ax2_min; ax2_ind < ax2_max; ++ax2_ind) { for (int plus = 0; plus <= meshlines_width_; plus++) { - if (ax1_ind + plus >= 0 && ax1_ind + plus < pixels_[0]) + if (ax1_ind + plus >= 0 && ax1_ind + plus < pixels()[0]) data(ax1_ind + plus, ax2_ind) = rgb; - if (ax1_ind - plus >= 0 && ax1_ind - plus < pixels_[0]) + if (ax1_ind - plus >= 0 && ax1_ind - plus < pixels()[0]) data(ax1_ind - plus, ax2_ind) = rgb; } } @@ -887,27 +901,27 @@ void Plot::draw_mesh_lines(ImageData& data) const int ax1_min, ax1_max; if (axis_lines.first.size() > 0) { double frac = (axis_lines.first.front() - ll_plot[ax1]) / width[ax1]; - ax1_min = frac * pixels_[0]; + ax1_min = frac * pixels()[0]; if (ax1_min < 0) ax1_min = 0; frac = (axis_lines.first.back() - ll_plot[ax1]) / width[ax1]; - ax1_max = frac * pixels_[0]; - if (ax1_max > pixels_[0]) - ax1_max = pixels_[0]; + ax1_max = frac * pixels()[0]; + if (ax1_max > pixels()[0]) + ax1_max = pixels()[0]; } else { ax1_min = 0; - ax1_max = pixels_[0]; + ax1_max = pixels()[0]; } // Iterate across the second axis and draw lines. for (auto ax2_val : axis_lines.second) { double frac = (ax2_val - ll_plot[ax2]) / width[ax2]; - int ax2_ind = (1.0 - frac) * pixels_[1]; + int ax2_ind = (1.0 - frac) * pixels()[1]; for (int ax1_ind = ax1_min; ax1_ind < ax1_max; ++ax1_ind) { for (int plus = 0; plus <= meshlines_width_; plus++) { - if (ax2_ind + plus >= 0 && ax2_ind + plus < pixels_[1]) + if (ax2_ind + plus >= 0 && ax2_ind + plus < pixels()[1]) data(ax1_ind, ax2_ind + plus) = rgb; - if (ax2_ind - plus >= 0 && ax2_ind - plus < pixels_[1]) + if (ax2_ind - plus >= 0 && ax2_ind - plus < pixels()[1]) data(ax1_ind, ax2_ind - plus) = rgb; } } @@ -928,9 +942,9 @@ void Plot::create_voxel() const { // compute voxel widths in each direction array vox; - vox[0] = width_[0] / static_cast(pixels_[0]); - vox[1] = width_[1] / static_cast(pixels_[1]); - vox[2] = width_[2] / static_cast(pixels_[2]); + vox[0] = width_[0] / static_cast(pixels()[0]); + vox[1] = width_[1] / static_cast(pixels()[1]); + vox[2] = width_[2] / static_cast(pixels()[2]); // initial particle position Position ll = origin_ - width_ / 2.; @@ -952,18 +966,18 @@ void Plot::create_voxel() const // Write current date and time write_attribute(file_id, "date_and_time", time_stamp().c_str()); - array pixels; - std::copy(pixels_.begin(), pixels_.end(), pixels.begin()); - write_attribute(file_id, "num_voxels", pixels); + array h5_pixels; + std::copy(pixels().begin(), pixels().end(), h5_pixels.begin()); + write_attribute(file_id, "num_voxels", h5_pixels); write_attribute(file_id, "voxel_width", vox); write_attribute(file_id, "lower_left", ll); // Create dataset for voxel data -- note that the dimensions are reversed // since we want the order in the file to be z, y, x hsize_t dims[3]; - dims[0] = pixels_[2]; - dims[1] = pixels_[1]; - dims[2] = pixels_[0]; + dims[0] = pixels()[2]; + dims[1] = pixels()[1]; + dims[2] = pixels()[0]; hid_t dspace, dset, memspace; voxel_init(file_id, &(dims[0]), &dspace, &dset, &memspace); @@ -971,11 +985,11 @@ void Plot::create_voxel() const pltbase.width_ = width_; pltbase.origin_ = origin_; pltbase.basis_ = PlotBasis::xy; - pltbase.pixels_ = pixels_; + pltbase.pixels() = pixels(); pltbase.slice_color_overlaps_ = color_overlaps_; ProgressBar pb; - for (int z = 0; z < pixels_[2]; z++) { + for (int z = 0; z < pixels()[2]; z++) { // update z coordinate pltbase.origin_.z = ll.z + z * vox[2]; @@ -993,7 +1007,7 @@ void Plot::create_voxel() const // update progress bar pb.set_value( - 100. * static_cast(z + 1) / static_cast((pixels_[2]))); + 100. * static_cast(z + 1) / static_cast((pixels()[2]))); } voxel_finalize(dspace, dset, memspace); @@ -1052,7 +1066,10 @@ RayTracePlot::RayTracePlot(pugi::xml_node node) : PlottableInterface(node) check_for_node(node, "field_of_view")) fatal_error("orthographic_width and field_of_view are mutually exclusive " "parameters."); +} +void RayTracePlot::update_view() +{ // Get centerline vector for camera-to-model. We create vectors around this // that form a pixel array, and then trace rays along that. auto up = up_ / up_.norm(); @@ -1079,6 +1096,7 @@ WireframeRayTracePlot::WireframeRayTracePlot(pugi::xml_node node) set_wireframe_thickness(node); set_wireframe_ids(node); set_wireframe_color(node); + update_view(); } void WireframeRayTracePlot::set_wireframe_color(pugi::xml_node plot_node) @@ -1182,8 +1200,8 @@ std::pair RayTracePlot::get_pixel_ray( // Compute field of view in radians constexpr double DEGREE_TO_RADIAN = M_PI / 180.0; double horiz_fov_radians = horizontal_field_of_view_ * DEGREE_TO_RADIAN; - double p0 = static_cast(pixels_[0]); - double p1 = static_cast(pixels_[1]); + double p0 = static_cast(pixels()[0]); + double p1 = static_cast(pixels()[1]); double vert_fov_radians = horiz_fov_radians * p1 / p0; // focal_plane_dist can be changed to alter the perspective distortion @@ -1219,10 +1237,10 @@ std::pair RayTracePlot::get_pixel_ray( return result; } -void WireframeRayTracePlot::create_output() const +ImageData WireframeRayTracePlot::create_image() const { - size_t width = pixels_[0]; - size_t height = pixels_[1]; + size_t width = pixels()[0]; + size_t height = pixels()[1]; ImageData data({width, height}, not_found_); // This array marks where the initial wireframe was drawn. We convolve it with @@ -1245,11 +1263,11 @@ void WireframeRayTracePlot::create_output() const std::vector>> this_line_segments( n_threads); for (int t = 0; t < n_threads; ++t) { - this_line_segments[t].resize(pixels_[0]); + this_line_segments[t].resize(pixels()[0]); } // The last thread writes to this, and the first thread reads from it. - std::vector> old_segments(pixels_[0]); + std::vector> old_segments(pixels()[0]); #pragma omp parallel { @@ -1257,7 +1275,7 @@ void WireframeRayTracePlot::create_output() const const int tid = thread_num(); int vert = tid; - for (int iter = 0; iter <= pixels_[1] / n_threads; iter++) { + for (int iter = 0; iter <= pixels()[1] / n_threads; iter++) { // Save bottom line of current work chunk to compare against later. This // used to be inside the below if block, but it causes a spurious line to @@ -1266,9 +1284,9 @@ void WireframeRayTracePlot::create_output() const if (tid == n_threads - 1) old_segments = this_line_segments[n_threads - 1]; - if (vert < pixels_[1]) { + if (vert < pixels()[1]) { - for (int horiz = 0; horiz < pixels_[0]; ++horiz) { + for (int horiz = 0; horiz < pixels()[0]; ++horiz) { // RayTracePlot implements camera ray generation std::pair ru = get_pixel_ray(horiz, vert); @@ -1330,7 +1348,7 @@ void WireframeRayTracePlot::create_output() const // Now that the horizontal line has finished rendering, we can fill in // wireframe entries that require comparison among all the threads. Hence // the omp barrier being used. It has to be OUTSIDE any if blocks! - if (vert < pixels_[1]) { + if (vert < pixels()[1]) { // Loop over horizontal pixels, checking intersection stack of upper // neighbor @@ -1340,7 +1358,7 @@ void WireframeRayTracePlot::create_output() const else top_cmp = &this_line_segments[tid - 1]; - for (int horiz = 0; horiz < pixels_[0]; ++horiz) { + for (int horiz = 0; horiz < pixels()[0]; ++horiz) { if (!trackstack_equivalent( this_line_segments[tid][horiz], (*top_cmp)[horiz])) { wireframe_initial(horiz, vert) = 1; @@ -1357,8 +1375,8 @@ void WireframeRayTracePlot::create_output() const } // end omp parallel // Now thicken the wireframe lines and apply them to our image - for (int vert = 0; vert < pixels_[1]; ++vert) { - for (int horiz = 0; horiz < pixels_[0]; ++horiz) { + for (int vert = 0; vert < pixels()[1]; ++vert) { + for (int horiz = 0; horiz < pixels()[0]; ++horiz) { if (wireframe_initial(horiz, vert)) { if (wireframe_thickness_ == 1) data(horiz, vert) = wireframe_color_; @@ -1369,19 +1387,21 @@ void WireframeRayTracePlot::create_output() const if (i * i + j * j < wireframe_thickness_ * wireframe_thickness_) { // Check if wireframe pixel is out of bounds - int w_i = std::max(std::min(horiz + i, pixels_[0] - 1), 0); - int w_j = std::max(std::min(vert + j, pixels_[1] - 1), 0); + int w_i = std::max(std::min(horiz + i, pixels()[0] - 1), 0); + int w_j = std::max(std::min(vert + j, pixels()[1] - 1), 0); data(w_i, w_j) = wireframe_color_; } } } } -#ifdef USE_LIBPNG - output_png(path_plot(), data); -#else - output_ppm(path_plot(), data); -#endif + return data; +} + +void WireframeRayTracePlot::create_output() const +{ + ImageData data = create_image(); + write_image(data); } void RayTracePlot::print_info() const @@ -1391,7 +1411,7 @@ void RayTracePlot::print_info() const fmt::print("Look at: {} {} {}\n", look_at_.x, look_at_.y, look_at_.z); fmt::print( "Horizontal field of view: {} degrees\n", horizontal_field_of_view_); - fmt::print("Pixels: {} {}\n", pixels_[0], pixels_[1]); + fmt::print("Pixels: {} {}\n", pixels()[0], pixels()[1]); } void WireframeRayTracePlot::print_info() const @@ -1473,8 +1493,8 @@ void RayTracePlot::set_pixels(pugi::xml_node node) if (pxls.size() != 2) fatal_error( fmt::format(" must be length 2 in projection plot {}", id())); - pixels_[0] = pxls[0]; - pixels_[1] = pxls[1]; + pixels()[0] = pxls[0]; + pixels()[1] = pxls[1]; } void RayTracePlot::set_camera_position(pugi::xml_node node) @@ -1521,6 +1541,7 @@ SolidRayTracePlot::SolidRayTracePlot(pugi::xml_node node) : RayTracePlot(node) set_opaque_ids(node); set_diffuse_fraction(node); set_light_position(node); + update_view(); } void SolidRayTracePlot::print_info() const @@ -1529,15 +1550,15 @@ void SolidRayTracePlot::print_info() const RayTracePlot::print_info(); } -void SolidRayTracePlot::create_output() const +ImageData SolidRayTracePlot::create_image() const { - size_t width = pixels_[0]; - size_t height = pixels_[1]; + size_t width = pixels()[0]; + size_t height = pixels()[1]; ImageData data({width, height}, not_found_); #pragma omp parallel for schedule(dynamic) collapse(2) - for (int horiz = 0; horiz < pixels_[0]; ++horiz) { - for (int vert = 0; vert < pixels_[1]; ++vert) { + for (int horiz = 0; horiz < pixels()[0]; ++horiz) { + for (int vert = 0; vert < pixels()[1]; ++vert) { // RayTracePlot implements camera ray generation std::pair ru = get_pixel_ray(horiz, vert); PhongRay ray(ru.first, ru.second, *this); @@ -1546,11 +1567,13 @@ void SolidRayTracePlot::create_output() const } } -#ifdef USE_LIBPNG - output_png(path_plot(), data); -#else - output_ppm(path_plot(), data); -#endif + return data; +} + +void SolidRayTracePlot::create_output() const +{ + ImageData data = create_image(); + write_image(data); } void SolidRayTracePlot::set_opaque_ids(pugi::xml_node node) From d0346e94acf403d58377cfe8b205419cd969a11d Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Sun, 8 Feb 2026 16:22:39 -0600 Subject: [PATCH 530/671] Install parallel h5py with no build isolation (#3782) --- src/finalize.cpp | 2 ++ tools/ci/gha-install.sh | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/finalize.cpp b/src/finalize.cpp index ff5d79cb0..5ca9d5746 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -14,6 +14,7 @@ #include "openmc/material.h" #include "openmc/mesh.h" #include "openmc/message_passing.h" +#include "openmc/mgxs_interface.h" #include "openmc/nuclide.h" #include "openmc/photon.h" #include "openmc/plot.h" @@ -163,6 +164,7 @@ int openmc_finalize() data::energy_min = {0.0, 0.0, 0.0, 0.0}; data::temperature_min = 0.0; data::temperature_max = INFTY; + data::mg = {}; model::root_universe = -1; model::plotter_seed = 1; openmc::openmc_set_seed(DEFAULT_SEED); diff --git a/tools/ci/gha-install.sh b/tools/ci/gha-install.sh index 74c3947f1..cccac866c 100755 --- a/tools/ci/gha-install.sh +++ b/tools/ci/gha-install.sh @@ -39,7 +39,9 @@ if [[ $MPI == 'y' ]]; then export CC=mpicc export HDF5_MPI=ON export HDF5_DIR=/usr/lib/x86_64-linux-gnu/hdf5/mpich - pip install --no-binary=h5py h5py + # Install h5py without build isolation to pick up already installed mpi4py + pip install Cython pkgconfig + pip install --no-build-isolation --no-binary=h5py h5py fi # Build and install OpenMC executable From 4f4930b63395a6ea6b83a52bdd807e3939bbd59e Mon Sep 17 00:00:00 2001 From: Kevin Sawatzky <66632997+nuclearkevin@users.noreply.github.com> Date: Mon, 9 Feb 2026 16:54:59 -0600 Subject: [PATCH 531/671] Temperature feedback support in the random ray solver. (#3737) --- docs/source/usersguide/geometry.rst | 22 + docs/source/usersguide/random_ray.rst | 18 +- include/openmc/mgxs.h | 5 + .../openmc/random_ray/flat_source_domain.h | 9 +- include/openmc/random_ray/random_ray.h | 1 + include/openmc/random_ray/source_region.h | 13 +- openmc/examples.py | 117 +++- openmc/mgxs/library.py | 99 +++- openmc/mgxs_library.py | 69 +++ openmc/model/model.py | 529 +++++++++++++++--- src/mgxs_interface.cpp | 12 +- src/random_ray/flat_source_domain.cpp | 215 ++++--- src/random_ray/linear_source_domain.cpp | 19 +- src/random_ray/random_ray.cpp | 9 +- src/random_ray/random_ray_simulation.cpp | 4 - src/random_ray/source_region.cpp | 13 +- .../__init__.py | 0 .../infinite_medium/inputs_true.dat | 70 +++ .../infinite_medium/results_true.dat | 2 + .../material_wise/inputs_true.dat | 70 +++ .../material_wise/results_true.dat | 2 + .../stochastic_slab/inputs_true.dat | 70 +++ .../stochastic_slab/results_true.dat | 2 + .../test.py | 73 +++ .../random_ray_cell_density/test.py | 2 +- .../random_ray_cell_temperature/__init__.py | 0 .../inputs_true.dat | 118 ++++ .../results_true.dat | 171 ++++++ .../random_ray_cell_temperature/test.py | 36 ++ 29 files changed, 1515 insertions(+), 255 deletions(-) create mode 100644 tests/regression_tests/random_ray_auto_convert_temperature/__init__.py create mode 100644 tests/regression_tests/random_ray_auto_convert_temperature/infinite_medium/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert_temperature/infinite_medium/results_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert_temperature/material_wise/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert_temperature/material_wise/results_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert_temperature/stochastic_slab/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert_temperature/stochastic_slab/results_true.dat create mode 100644 tests/regression_tests/random_ray_auto_convert_temperature/test.py create mode 100644 tests/regression_tests/random_ray_cell_temperature/__init__.py create mode 100644 tests/regression_tests/random_ray_cell_temperature/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_cell_temperature/results_true.dat create mode 100644 tests/regression_tests/random_ray_cell_temperature/test.py diff --git a/docs/source/usersguide/geometry.rst b/docs/source/usersguide/geometry.rst index 3224d5fff..8f83b9b08 100644 --- a/docs/source/usersguide/geometry.rst +++ b/docs/source/usersguide/geometry.rst @@ -248,6 +248,28 @@ The classes :class:`Halfspace`, :class:`Intersection`, :class:`Union`, and :class:`Complement` and all instances of :class:`openmc.Region` and can be assigned to the :attr:`Cell.region` attribute. +Cells also contain :attr:`Cell.temperature` and :attr:`Cell.density` +attributes which override the temperature and density of the fill. These can +be quite useful when temperatures and densities are spatially varying, as the +alternative would be to add a unique :class:`Material` for each permutation of +temperature, density, and composition. You can set the temperature (K) and +density (g/cc) of a cell like so:: + + fuel.temperature = 800.0 + fuel.density = 10.0 + +The real utility of cell temperatures and densities occurs when a cell is +replicated across the geometry, such as when a cell is the root geometric element +in a replicated :ref:`universe` or :ref:`lattice +`. In those cases, you can provide a list of temperatures +and densities to apply a temperature/density field to all of the distributed cells:: + + fuel.temperature = [800.0, 900.0, 800.0, 900.0] + fuel.density = [10.0, 9.0, 10.0, 9.0] + +In this example, the fuel cell is distributed four times in the geometry. Each +distributed instance then receives its own temperature and density. + .. _usersguide_universes: --------- diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index 64ef7de6d..0c9a04028 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -646,7 +646,9 @@ model to use these multigroup cross sections. An example is given below:: overwrite_mgxs_library=False, mgxs_path="mgxs.h5", correction=None, - source_energy=None + source_energy=None, + temperatures=None, + temperature_settings=None ) The most important parameter to set is the ``method`` parameter, which can be @@ -733,6 +735,20 @@ distribution for MGXS generation as:: source_energy = openmc.stats.delta_function(2.45e6) +The ``temperatures`` parameter can be provided if temperature-dependent +multi-group cross sections are desired for multi-physics simulations. An +individual cross section generation calculation is run for each temperature +provided, where the materials in the model are set to the temperature. The +temperature settings used during cross section generation can be specified with the +``temperature_settings`` parameter. If no ``temperature_settings`` are provided, +the settings contained in the model will be used. The valid keys and values in the +``temperature_settings`` dictionary are identical to +:attr:`openmc.Settings.temperature_settings`; more information can be found in +:class:`openmc.Settings` . This approach yields isothermal cross section interpolation +tables, which can be inaccurate for systems with large differences between temperatures +in each material (often the case in fission reactors). If a more sophisticated +temperature-dependence is required, we recommend generating cross sections manually. + Ultimately, the methods described above are all just approximations. Approximations in the generated MGXS data will fundamentally limit the potential accuracy of the random ray solver. However, the methods described above are all diff --git a/include/openmc/mgxs.h b/include/openmc/mgxs.h index 9b1602f29..43505f432 100644 --- a/include/openmc/mgxs.h +++ b/include/openmc/mgxs.h @@ -113,6 +113,11 @@ public: const vector& micros, const vector& atom_densities, int num_group, int num_delay); + //! \brief Get the number of temperature data points. + //! + //! @return The number of temperature data points for this MGXS + inline int n_temperature_points() { return kTs.size(); } + //! \brief Provides a cross section value given certain parameters //! //! @param xstype Type of cross section requested, according to the diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index 0d4086966..693093683 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -100,17 +100,18 @@ public: // in model::cells vector source_region_offsets_; - // 2D arrays stored in 1D representing values for all materials x energy - // groups + // 3D arrays stored in 1D representing values for all materials x temperature + // points x energy groups int n_materials_; + int ntemperature_; vector sigma_t_; vector nu_sigma_f_; vector sigma_f_; vector chi_; vector kappa_fission_; - // 3D arrays stored in 1D representing values for all materials x energy - // groups x energy groups + // 4D arrays stored in 1D representing values for all materials x temperature + // points x energy groups x energy groups vector sigma_s_; // The abstract container holding all source region-specific data diff --git a/include/openmc/random_ray/random_ray.h b/include/openmc/random_ray/random_ray.h index 40c67ef95..7fd68b83e 100644 --- a/include/openmc/random_ray/random_ray.h +++ b/include/openmc/random_ray/random_ray.h @@ -65,6 +65,7 @@ private: vector mesh_fractional_lengths_; int negroups_; + int ntemperature_; FlatSourceDomain* domain_ {nullptr}; // pointer to domain that has flat source // data needed for ray transport double distance_travelled_ {0}; diff --git a/include/openmc/random_ray/source_region.h b/include/openmc/random_ray/source_region.h index c20d46abe..65f2e6bc4 100644 --- a/include/openmc/random_ray/source_region.h +++ b/include/openmc/random_ray/source_region.h @@ -146,6 +146,7 @@ public: // Scalar fields int* material_; + int* temperature_idx_; double* density_mult_; int* is_small_; int* n_hits_; @@ -199,6 +200,9 @@ public: double& density_mult() { return *density_mult_; } const double density_mult() const { return *density_mult_; } + int& temperature_idx() { return *temperature_idx_; } + const int temperature_idx() const { return *temperature_idx_; } + int& is_small() { return *is_small_; } const int is_small() const { return *is_small_; } @@ -319,8 +323,9 @@ public: //--------------------------------------- // Scalar fields - - int material_ {0}; //!< Index in openmc::model::materials array + int material_ {0}; //!< Index in openmc::model::materials array + int temperature_idx_ { + 0}; //!< Index into the MGXS array representing temperature double density_mult_ {1.0}; //!< A density multiplier queried from the cell //!< corresponding to the source region. OpenMPMutex lock_; @@ -400,6 +405,9 @@ public: int& material(int64_t sr) { return material_[sr]; } const int material(int64_t sr) const { return material_[sr]; } + int& temperature_idx(int64_t sr) { return temperature_idx_[sr]; } + const int temperature_idx(int64_t sr) const { return temperature_idx_[sr]; } + double& density_mult(int64_t sr) { return density_mult_[sr]; } const double density_mult(int64_t sr) const { return density_mult_[sr]; } @@ -634,6 +642,7 @@ private: // SoA storage for scalar fields (one item per source region) vector material_; + vector temperature_idx_; vector density_mult_; vector is_small_; vector n_hits_; diff --git a/openmc/examples.py b/openmc/examples.py index 350a4d24d..f7f2bd48d 100644 --- a/openmc/examples.py +++ b/openmc/examples.py @@ -656,10 +656,19 @@ def slab_mg(num_regions=1, mat_names=None, mgxslib_name='2g.h5') -> openmc.Model return model -def _generate_c5g7_materials() -> openmc.Materials: +def _generate_c5g7_materials(second_temp = False) -> openmc.Materials: """Generate materials utilizing multi-group cross sections based on the the C5G7 Benchmark. + Parameters + ---------- + second_temp : bool, optional + Whether or not the cross sections should contain two temperature datapoints. + The first data point is the C5G7 cross sections, which corresponds to a temperature + of 294 K. The second data point is the C5G7 cross sections multiplied by 1/2, + which corresponds to a temperature of 394 K. This temperature dependence is + fictitious; it is used for testing temperature feedback in the random ray solver. + Returns ------- materials : openmc.Materials @@ -672,9 +681,45 @@ def _generate_c5g7_materials() -> openmc.Materials: assembly transport calculations without spatial homogenization" """ # Instantiate the energy group data + # MGXS for the UO2 pins. group_edges = [1e-5, 0.0635, 10.0, 1.0e2, 1.0e3, 0.5e6, 1.0e6, 20.0e6] groups = openmc.mgxs.EnergyGroups(group_edges) + uo2_total = np.array([0.1779492, 0.3298048, 0.4803882, 0.5543674, 0.3118013, 0.3951678, + 0.5644058]) + uo2_abs = np.array([8.0248e-03, 3.7174e-03, 2.6769e-02, 9.6236e-02, 3.0020e-02, + 1.1126e-01, 2.8278e-01]) + uo2_scatter_matrix = np.array( + [[[0.1275370, 0.0423780, 0.0000094, 0.0000000, 0.0000000, 0.0000000, 0.0000000], + [0.0000000, 0.3244560, 0.0016314, 0.0000000, 0.0000000, 0.0000000, 0.0000000], + [0.0000000, 0.0000000, 0.4509400, 0.0026792, 0.0000000, 0.0000000, 0.0000000], + [0.0000000, 0.0000000, 0.0000000, 0.4525650, 0.0055664, 0.0000000, 0.0000000], + [0.0000000, 0.0000000, 0.0000000, 0.0001253, 0.2714010, 0.0102550, 0.0000000], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0012968, 0.2658020, 0.0168090], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0085458, 0.2730800]]]) + uo2_scatter_matrix = np.rollaxis(uo2_scatter_matrix, 0, 3) + uo2_fission = np.array([7.21206e-03, 8.19301e-04, 6.45320e-03, 1.85648e-02, 1.78084e-02, + 8.30348e-02, 2.16004e-01]) + uo2_nu_fission = np.array([2.005998e-02, 2.027303e-03, 1.570599e-02, 4.518301e-02, + 4.334208e-02, 2.020901e-01, 5.257105e-01]) + uo2_chi = np.array([5.8791e-01, 4.1176e-01, 3.3906e-04, 1.1761e-07, 0.0000e+00, + 0.0000e+00, 0.0000e+00]) + + # MGXS for the H2O moderator. + h2o_total = np.array([0.15920605, 0.412969593, 0.59030986, 0.58435, 0.718, 1.2544497, + 2.650379]) + h2o_abs = np.array([6.0105e-04, 1.5793e-05, 3.3716e-04, 1.9406e-03, 5.7416e-03, + 1.5001e-02, 3.7239e-02]) + h2o_scatter_matrix = np.array( + [[[0.0444777, 0.1134000, 0.0007235, 0.0000037, 0.0000001, 0.0000000, 0.0000000], + [0.0000000, 0.2823340, 0.1299400, 0.0006234, 0.0000480, 0.0000074, 0.0000010], + [0.0000000, 0.0000000, 0.3452560, 0.2245700, 0.0169990, 0.0026443, 0.0005034], + [0.0000000, 0.0000000, 0.0000000, 0.0910284, 0.4155100, 0.0637320, 0.0121390], + [0.0000000, 0.0000000, 0.0000000, 0.0000714, 0.1391380, 0.5118200, 0.0612290], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0022157, 0.6999130, 0.5373200], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.1324400, 2.4807000]]]) + h2o_scatter_matrix = np.rollaxis(h2o_scatter_matrix, 0, 3) + # Instantiate the 7-group (C5G7) cross section data uo2_xsdata = openmc.XSdata('UO2', groups) uo2_xsdata.order = 0 @@ -707,29 +752,33 @@ def _generate_c5g7_materials() -> openmc.Materials: uo2_xsdata.set_nu_fission(nu_fission) uo2_xsdata.set_chi([5.8791e-01, 4.1176e-01, 3.3906e-04, 1.1761e-07, 0.0000e+00, 0.0000e+00, 0.0000e+00]) + uo2_xsdata.set_total(uo2_total, temperature=294.0) + uo2_xsdata.set_absorption(uo2_abs, temperature=294.0) + uo2_xsdata.set_scatter_matrix(uo2_scatter_matrix, temperature=294.0) + uo2_xsdata.set_fission(uo2_fission, temperature=294.0) + uo2_xsdata.set_nu_fission(uo2_nu_fission, temperature=294.0) + uo2_xsdata.set_chi(uo2_chi, temperature=294.0) h2o_xsdata = openmc.XSdata('LWTR', groups) h2o_xsdata.order = 0 - h2o_xsdata.set_total([0.15920605, 0.412969593, 0.59030986, 0.58435, - 0.718, 1.2544497, 2.650379]) - h2o_xsdata.set_absorption([6.0105e-04, 1.5793e-05, 3.3716e-04, - 1.9406e-03, 5.7416e-03, 1.5001e-02, - 3.7239e-02]) - scatter_matrix = np.array( - [[[0.0444777, 0.1134000, 0.0007235, 0.0000037, 0.0000001, 0.0000000, 0.0000000], - [0.0000000, 0.2823340, 0.1299400, 0.0006234, - 0.0000480, 0.0000074, 0.0000010], - [0.0000000, 0.0000000, 0.3452560, 0.2245700, - 0.0169990, 0.0026443, 0.0005034], - [0.0000000, 0.0000000, 0.0000000, 0.0910284, - 0.4155100, 0.0637320, 0.0121390], - [0.0000000, 0.0000000, 0.0000000, 0.0000714, - 0.1391380, 0.5118200, 0.0612290], - [0.0000000, 0.0000000, 0.0000000, 0.0000000, - 0.0022157, 0.6999130, 0.5373200], - [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.1324400, 2.4807000]]]) - scatter_matrix = np.rollaxis(scatter_matrix, 0, 3) - h2o_xsdata.set_scatter_matrix(scatter_matrix) + h2o_xsdata.set_total(h2o_total, temperature=294.0) + h2o_xsdata.set_absorption(h2o_abs, temperature=294.0) + h2o_xsdata.set_scatter_matrix(h2o_scatter_matrix, temperature=294.0) + + # Add the second temperature data point if requested. + if second_temp: + uo2_xsdata.add_temperature(394.0) + uo2_xsdata.set_total(0.5 * uo2_total, temperature=394.0) + uo2_xsdata.set_absorption(0.5 * uo2_abs, temperature=394.0) + uo2_xsdata.set_scatter_matrix(0.5 * uo2_scatter_matrix, temperature=394.0) + uo2_xsdata.set_fission(0.5 * uo2_fission, temperature=394.0) + uo2_xsdata.set_nu_fission(0.5 * uo2_nu_fission, temperature=394.0) + uo2_xsdata.set_chi(uo2_chi, temperature=394.0) + + h2o_xsdata.add_temperature(394.0) + h2o_xsdata.set_total(0.5 * h2o_total, temperature=394.0) + h2o_xsdata.set_absorption(0.5 * h2o_abs, temperature=394.0) + h2o_xsdata.set_scatter_matrix(0.5 * h2o_scatter_matrix, temperature=394.0) mg_cross_sections = openmc.MGXSLibrary(groups) mg_cross_sections.add_xsdatas([uo2_xsdata, h2o_xsdata]) @@ -823,10 +872,19 @@ def _generate_subdivided_pin_cell(uo2, water) -> openmc.Universe: return pincell -def random_ray_pin_cell() -> openmc.Model: +def random_ray_pin_cell(second_temp = False) -> openmc.Model: """Create a PWR pin cell example using C5G7 cross section data. cross section data. + Parameters + ---------- + second_temp : bool, optional + Whether or not the cross sections should contain two temperature datapoints. + The first data point is the C5G7 cross sections, which corresponds to a temperature + of 294 K. The second data point is the C5G7 cross sections multiplied by 1/2, + which corresponds to a temperature of 3934 K. This temperature dependence is + fictitious; it is used for testing temperature feedback in the random ray solver. + Returns ------- model : openmc.Model @@ -837,7 +895,7 @@ def random_ray_pin_cell() -> openmc.Model: ########################################################################### # Create Materials for the problem - materials = _generate_c5g7_materials() + materials = _generate_c5g7_materials(second_temp) uo2 = materials[0] water = materials[1] @@ -897,13 +955,22 @@ def random_ray_pin_cell() -> openmc.Model: return model -def random_ray_lattice() -> openmc.Model: +def random_ray_lattice(second_temp = False) -> openmc.Model: """Create a 2x2 PWR pin cell asymmetrical lattice example. This model is a 2x2 reflective lattice of fuel pins with one of the lattice locations having just moderator instead of a fuel pin. It uses C5G7 cross section data. + Parameters + ---------- + second_temp : bool, optional + Whether or not the cross sections should contain two temperature datapoints. + The first data point is the C5G7 cross sections, which corresponds to a temperature + of 294 K. The second data point is the C5G7 cross sections multiplied by 1/2, + which corresponds to a temperature of 3934 K. This temperature dependence is + fictitious; it is used for testing temperature feedback in the random ray solver. + Returns ------- model : openmc.Model @@ -914,7 +981,7 @@ def random_ray_lattice() -> openmc.Model: ########################################################################### # Create Materials for the problem - materials = _generate_c5g7_materials() + materials = _generate_c5g7_materials(second_temp) uo2 = materials[0] water = materials[1] diff --git a/openmc/mgxs/library.py b/openmc/mgxs/library.py index b476de902..78ab3ca19 100644 --- a/openmc/mgxs/library.py +++ b/openmc/mgxs/library.py @@ -13,6 +13,7 @@ import openmc.checkvalue as cv from openmc.checkvalue import PathLike from ..tallies import ESTIMATOR_TYPES +ROOM_TEMPERATURE_KELVIN = 294.0 class Library: """A multi-energy-group and multi-delayed-group cross section library for @@ -954,7 +955,7 @@ class Library: return pickle.load(f) def get_xsdata(self, domain, xsdata_name, nuclide='total', xs_type='macro', - subdomain=None, apply_domain_chi=False): + subdomain=None, apply_domain_chi=False, temperature=ROOM_TEMPERATURE_KELVIN): """Generates an openmc.XSdata object describing a multi-group cross section dataset for writing to an openmc.MGXSLibrary object. @@ -990,6 +991,9 @@ class Library: downstream multigroup solvers that precompute a material-specific chi before the transport solve provides group-wise fluxes. Defaults to False. + temperature : float, optional + The temperature to set in the XSdata object. Defaults to 294 K + (room temperature). Returns ------- @@ -1036,6 +1040,7 @@ class Library: else: representation = 'isotropic' xsdata = openmc.XSdata(name, self.energy_groups, + temperatures=[temperature], representation=representation) xsdata.num_delayed_groups = self.num_delayed_groups if self.num_polar > 1 or self.num_azimuthal > 1: @@ -1053,45 +1058,61 @@ class Library: # Now get xs data itself if 'nu-transport' in self.mgxs_types and self.correction == 'P0': mymgxs = self.get_mgxs(domain, 'nu-transport') - xsdata.set_total_mgxs(mymgxs, xs_type=xs_type, nuclide=[nuclide], + xsdata.set_total_mgxs(mymgxs, temperature=temperature, + xs_type=xs_type, + nuclide=[nuclide], subdomain=subdomain) elif 'transport' in self.mgxs_types and self.correction == 'P0': mymgxs = self.get_mgxs(domain, 'transport') - xsdata.set_total_mgxs(mymgxs, xs_type=xs_type, nuclide=[nuclide], + xsdata.set_total_mgxs(mymgxs, temperature=temperature, + xs_type=xs_type, + nuclide=[nuclide], subdomain=subdomain) elif 'total' in self.mgxs_types: mymgxs = self.get_mgxs(domain, 'total') - xsdata.set_total_mgxs(mymgxs, xs_type=xs_type, nuclide=[nuclide], + xsdata.set_total_mgxs(mymgxs, temperature=temperature, + xs_type=xs_type, + nuclide=[nuclide], subdomain=subdomain) if 'absorption' in self.mgxs_types: mymgxs = self.get_mgxs(domain, 'absorption') - xsdata.set_absorption_mgxs(mymgxs, xs_type=xs_type, + xsdata.set_absorption_mgxs(mymgxs, + temperature=temperature, + xs_type=xs_type, nuclide=[nuclide], subdomain=subdomain) if 'fission' in self.mgxs_types: mymgxs = self.get_mgxs(domain, 'fission') - xsdata.set_fission_mgxs(mymgxs, xs_type=xs_type, - nuclide=[nuclide], subdomain=subdomain) + xsdata.set_fission_mgxs(mymgxs, temperature=temperature, + xs_type=xs_type, + nuclide=[nuclide], + subdomain=subdomain) if 'kappa-fission' in self.mgxs_types: mymgxs = self.get_mgxs(domain, 'kappa-fission') - xsdata.set_kappa_fission_mgxs(mymgxs, xs_type=xs_type, + xsdata.set_kappa_fission_mgxs(mymgxs, + temperature=temperature, + xs_type=xs_type, nuclide=[nuclide], subdomain=subdomain) if 'inverse-velocity' in self.mgxs_types: mymgxs = self.get_mgxs(domain, 'inverse-velocity') - xsdata.set_inverse_velocity_mgxs(mymgxs, xs_type=xs_type, + xsdata.set_inverse_velocity_mgxs(mymgxs, + temperature=temperature, + xs_type=xs_type, nuclide=[nuclide], subdomain=subdomain) if 'nu-fission matrix' in self.mgxs_types: mymgxs = self.get_mgxs(domain, 'nu-fission matrix') - xsdata.set_nu_fission_mgxs(mymgxs, xs_type=xs_type, + xsdata.set_nu_fission_mgxs(mymgxs, + temperature=temperature, + xs_type=xs_type, nuclide=[nuclide], subdomain=subdomain) @@ -1101,7 +1122,9 @@ class Library: nuc = "sum" else: nuc = nuclide - xsdata.set_chi_mgxs(mymgxs, xs_type=xs_type, nuclide=[nuc], + xsdata.set_chi_mgxs(mymgxs, temperature=temperature, + xs_type=xs_type, + nuclide=[nuc], subdomain=subdomain) if 'chi-prompt' in self.mgxs_types: @@ -1110,8 +1133,10 @@ class Library: nuc = "sum" else: nuc = nuclide - xsdata.set_chi_prompt_mgxs(mymgxs, xs_type=xs_type, - nuclide=[nuc], subdomain=subdomain) + xsdata.set_chi_prompt_mgxs(mymgxs, temperature=temperature, + xs_type=xs_type, + nuclide=[nuc], + subdomain=subdomain) if 'chi-delayed' in self.mgxs_types: mymgxs = self.get_mgxs(domain, 'chi-delayed') @@ -1119,53 +1144,61 @@ class Library: nuc = "sum" else: nuc = nuclide - xsdata.set_chi_delayed_mgxs(mymgxs, xs_type=xs_type, - nuclide=[nuc], subdomain=subdomain) + xsdata.set_chi_delayed_mgxs(mymgxs, temperature=temperature, + xs_type=xs_type, + nuclide=[nuc], + subdomain=subdomain) if 'nu-fission' in self.mgxs_types: mymgxs = self.get_mgxs(domain, 'nu-fission') - xsdata.set_nu_fission_mgxs(mymgxs, xs_type=xs_type, + xsdata.set_nu_fission_mgxs(mymgxs, temperature=temperature, + xs_type=xs_type, nuclide=[nuclide], subdomain=subdomain) if 'prompt-nu-fission' in self.mgxs_types: mymgxs = self.get_mgxs(domain, 'prompt-nu-fission') - xsdata.set_prompt_nu_fission_mgxs(mymgxs, xs_type=xs_type, + xsdata.set_prompt_nu_fission_mgxs(mymgxs, temperature=temperature, + xs_type=xs_type, nuclide=[nuclide], subdomain=subdomain) if 'prompt-nu-fission matrix' in self.mgxs_types: mymgxs = self.get_mgxs(domain, 'prompt-nu-fission matrix') - xsdata.set_prompt_nu_fission_mgxs(mymgxs, xs_type=xs_type, + xsdata.set_prompt_nu_fission_mgxs(mymgxs, temperature=temperature, + xs_type=xs_type, nuclide=[nuclide], subdomain=subdomain) if 'delayed-nu-fission' in self.mgxs_types: mymgxs = self.get_mgxs(domain, 'delayed-nu-fission') - xsdata.set_delayed_nu_fission_mgxs(mymgxs, xs_type=xs_type, + xsdata.set_delayed_nu_fission_mgxs(mymgxs, temperature=temperature, + xs_type=xs_type, nuclide=[nuclide], subdomain=subdomain) if 'delayed-nu-fission matrix' in self.mgxs_types: mymgxs = self.get_mgxs(domain, 'delayed-nu-fission matrix') - xsdata.set_delayed_nu_fission_mgxs(mymgxs, xs_type=xs_type, + xsdata.set_delayed_nu_fission_mgxs(mymgxs, temperature=temperature, + xs_type=xs_type, nuclide=[nuclide], subdomain=subdomain) if 'beta' in self.mgxs_types: mymgxs = self.get_mgxs(domain, 'beta') - xsdata.set_beta_mgxs(mymgxs, xs_type=xs_type, nuclide=[nuclide], - subdomain=subdomain) + xsdata.set_beta_mgxs(mymgxs, temperature=temperature, xs_type=xs_type, + nuclide=[nuclide], subdomain=subdomain) if 'decay-rate' in self.mgxs_types: mymgxs = self.get_mgxs(domain, 'decay-rate') - xsdata.set_decay_rate_mgxs(mymgxs, xs_type=xs_type, nuclide=[nuclide], - subdomain=subdomain) + xsdata.set_decay_rate_mgxs(mymgxs, temperature=temperature, xs_type=xs_type, + nuclide=[nuclide], subdomain=subdomain) # If multiplicity matrix is available, prefer that if 'multiplicity matrix' in self.mgxs_types: mymgxs = self.get_mgxs(domain, 'multiplicity matrix') - xsdata.set_multiplicity_matrix_mgxs(mymgxs, xs_type=xs_type, + xsdata.set_multiplicity_matrix_mgxs(mymgxs, temperature=temperature, + xs_type=xs_type, nuclide=[nuclide], subdomain=subdomain) using_multiplicity = True @@ -1176,6 +1209,7 @@ class Library: scatt_mgxs = self.get_mgxs(domain, 'scatter matrix') nuscatt_mgxs = self.get_mgxs(domain, 'nu-scatter matrix') xsdata.set_multiplicity_matrix_mgxs(nuscatt_mgxs, scatt_mgxs, + temperature=temperature, xs_type=xs_type, nuclide=[nuclide], subdomain=subdomain) @@ -1188,6 +1222,7 @@ class Library: nuscatt_mgxs = \ self.get_mgxs(domain, 'consistent nu-scatter matrix') xsdata.set_multiplicity_matrix_mgxs(nuscatt_mgxs, scatt_mgxs, + temperature=temperature, xs_type=xs_type, nuclide=[nuclide], subdomain=subdomain) @@ -1202,7 +1237,8 @@ class Library: else: nuscatt_mgxs = \ self.get_mgxs(domain, 'consistent nu-scatter matrix') - xsdata.set_scatter_matrix_mgxs(nuscatt_mgxs, xs_type=xs_type, + xsdata.set_scatter_matrix_mgxs(nuscatt_mgxs, temperature=temperature, + xs_type=xs_type, nuclide=[nuclide], subdomain=subdomain) else: @@ -1213,7 +1249,8 @@ class Library: else: nuscatt_mgxs = \ self.get_mgxs(domain, 'consistent nu-scatter matrix') - xsdata.set_scatter_matrix_mgxs(nuscatt_mgxs, xs_type=xs_type, + xsdata.set_scatter_matrix_mgxs(nuscatt_mgxs, temperature=temperature, + xs_type=xs_type, nuclide=[nuclide], subdomain=subdomain) @@ -1253,14 +1290,15 @@ class Library: 'are ignored since multiplicity or nu-scatter matrices '\ 'were not tallied for ' + xsdata_name warn(msg, RuntimeWarning) - xsdata.set_scatter_matrix_mgxs(scatt_mgxs, xs_type=xs_type, + xsdata.set_scatter_matrix_mgxs(scatt_mgxs, temperature=temperature, + xs_type=xs_type, nuclide=[nuclide], subdomain=subdomain) return xsdata def create_mg_library(self, xs_type='macro', xsdata_names=None, - apply_domain_chi=False): + apply_domain_chi=False, temperature=ROOM_TEMPERATURE_KELVIN): """Creates an openmc.MGXSLibrary object to contain the MGXS data for the Multi-Group mode of OpenMC. @@ -1286,6 +1324,9 @@ class Library: downstream multigroup solvers that precompute a material-specific chi before the transport solve provides group-wise fluxes. Defaults to False. + temperature : float, optional + The temperature to set in the MGXSLibrary object. Defaults to 294 K + (room temperature). Returns ------- diff --git a/openmc/mgxs_library.py b/openmc/mgxs_library.py index c1a15998e..ab9b58b7a 100644 --- a/openmc/mgxs_library.py +++ b/openmc/mgxs_library.py @@ -515,6 +515,75 @@ class XSdata: if np.sum(array) > 0: self._fissionable = True + def add_temperature_data(self, other): + """This method adds temperature-dependent cross section + values from another XSdata object to this XSdata object. + Note: if a temperature datapoint from 'other' already exists in this + object, it will be overridden. + + Parameters + ---------- + other: openmc.XSdata + The other XSdata object to fetch data from + """ + + # Sanity check to make sure they have the same name, energy group structure, + # and delayed group structure + check_value('name', other.name, self.name) + check_value('energy_groups', other.energy_groups, [self.energy_groups]) + check_value('delayed_groups', other.num_delayed_groups, [self.num_delayed_groups]) + + # Add the temperature data. + for temp in other.temperatures: + if temp not in self.temperatures: + self.add_temperature(temp) + + if np.all(other.absorption[other._temperature_index(temp)] != None): + self.set_absorption(other.absorption[other._temperature_index(temp)], temp) + + if np.all(other.beta[other._temperature_index(temp)] != None): + self.set_beta(other.beta[other._temperature_index(temp)], temp) + + if np.all(other.chi[other._temperature_index(temp)] != None): + self.set_chi(other.chi[other._temperature_index(temp)], temp) + + if np.all(other.chi_delayed[other._temperature_index(temp)] != None): + self.set_chi_delayed(other.chi_delayed[other._temperature_index(temp)], temp) + + if np.all(other.chi_prompt[other._temperature_index(temp)] != None): + self.set_chi_prompt(other.chi_prompt[other._temperature_index(temp)], temp) + + if np.all(other.decay_rate[other._temperature_index(temp)] != None): + self.set_decay_rate(other.decay_rate[other._temperature_index(temp)], temp) + + if np.all(other.delayed_nu_fission[other._temperature_index(temp)] != None): + self.set_delayed_nu_fission(other.delayed_nu_fission[other._temperature_index(temp)], temp) + + if np.all(other.fission[other._temperature_index(temp)] != None): + self.set_fission(other.fission[other._temperature_index(temp)], temp) + + if np.all(other.inverse_velocity[other._temperature_index(temp)] != None): + self.set_inverse_velocity(other.inverse_velocity[other._temperature_index(temp)], temp) + + if np.all(other.kappa_fission[other._temperature_index(temp)] != None): + self.set_kappa_fission(other.kappa_fission[other._temperature_index(temp)], temp) + + if np.all(other.multiplicity_matrix[other._temperature_index(temp)] != None): + self.set_multiplicity_matrix(other.multiplicity_matrix[other._temperature_index(temp)], temp) + + if np.all(other.nu_fission[other._temperature_index(temp)] != None): + self.set_nu_fission(other.nu_fission[other._temperature_index(temp)], temp) + + if np.all(other.prompt_nu_fission[other._temperature_index(temp)] != None): + self.set_prompt_nu_fission(other.prompt_nu_fission[other._temperature_index(temp)], temp) + + if np.all(other.scatter_matrix[other._temperature_index(temp)] != None): + self.set_scatter_matrix(other.scatter_matrix[other._temperature_index(temp)], temp) + + if np.all(other.fission[other._temperature_index(temp)] != None): + self.set_total(other.total[other._temperature_index(temp)], temp) + + def set_total(self, total, temperature=ROOM_TEMPERATURE_KELVIN): """This method sets the cross section for this XSdata object at the provided temperature. diff --git a/openmc/model/model.py b/openmc/model/model.py index 67da2f73a..82fe87ca5 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1695,8 +1695,8 @@ class Model: self.geometry.get_all_materials().values() ) + @staticmethod def _auto_generate_mgxs_lib( - self, model: openmc.model.model, groups: openmc.mgxs.EnergyGroups, correction: str | none, @@ -1861,6 +1861,85 @@ class Model: return sources + @staticmethod + def _isothermal_infinite_media_mgxs( + material: openmc.Material, + groups: openmc.mgxs.EnergyGroups, + nparticles: int, + correction: str | None, + directory: PathLike, + source: openmc.IndependentSource, + temperature_settings: dict, + temperature: float | None = None, + ) -> openmc.XSdata: + """Generate a single MGXS set for one material, where the geometry is an + infinite medium composed of that material at an isothermal temperature value. + + Parameters + ---------- + material : openmc.Material + The material to generate MGXS for + groups : openmc.mgxs.EnergyGroups + Energy group structure for the MGXS. + nparticles : int + Number of particles to simulate per batch when generating MGXS. + correction : str + Transport correction to apply to the MGXS. Options are None and + "P0". + directory : str + Directory to run the simulation in, so as to contain XML files. + source : openmc.IndependentSource + Source to use when generating MGXS. + temperature_settings : dict + A dictionary of temperature settings to use when generating MGXS. + Valid entries for temperature_settings are the same as the valid + entries in openmc.Settings.temperature_settings. + temperature : float, optional + The isothermal temperature value to apply to the material. If not specified, + defaults to the temperature in the material. + + Returns + ------- + data : openmc.XSdata + The material MGXS for the given temperature isotherm. + """ + model = openmc.Model() + + # Set materials on the model + model.materials = [material] + if temperature != None: + model.materials[-1].temperature = temperature + + # Settings + model.settings.batches = 100 + model.settings.particles = nparticles + + model.settings.source = source + + model.settings.run_mode = 'fixed source' + model.settings.create_fission_neutrons = False + + model.settings.output = {'summary': True, 'tallies': False} + model.settings.temperature = temperature_settings + + # Geometry + box = openmc.model.RectangularPrism( + 100000.0, 100000.0, boundary_type='reflective') + name = material.name + infinite_cell = openmc.Cell(name=name, fill=model.materials[-1], region=-box) + infinite_universe = openmc.Universe(name=name, cells=[infinite_cell]) + model.geometry.root_universe = infinite_universe + + # Generate MGXS + mgxs_lib = Model._auto_generate_mgxs_lib( + model, groups, correction, directory) + + if temperature != None: + return mgxs_lib.get_xsdata(domain=material, xsdata_name=name, + temperature=temperature) + else: + return mgxs_lib.get_xsdata(domain=material, xsdata_name=name) + def _generate_infinite_medium_mgxs( self, groups: openmc.mgxs.EnergyGroups, @@ -1869,13 +1948,17 @@ class Model: correction: str | None, directory: PathLike, source_energy: openmc.stats.Univariate | None = None, - ): + temperatures: Sequence[float] | None = None, + temperature_settings: dict | None = None, + ) -> None: """Generate a MGXS library by running multiple OpenMC simulations, each representing an infinite medium simulation of a single isolated material. A discrete source is used to sample particles, with an equal strength spread across each of the energy groups. This is a highly naive method that ignores all spatial self shielding effects and all resonance - shielding effects between materials. + shielding effects between materials. If temperature data points are provided, + isothermal cross sections are generated at each temperature point for + each material to build a temperature interpolation table. Note that in all cases, a discrete source that is uniform over all energy groups is created (strength = 0.01) to ensure that total cross @@ -1907,50 +1990,78 @@ class Model: source_energy : openmc.stats.Univariate, optional Energy distribution to use when generating MGXS data, replacing any existing sources in the model. + temperatures : Sequence[float], optional + A list of temperatures to generate MGXS at. Each infinite material region + is isothermal at a given temperature data point for cross + section generation. + temperature_settings : dict, optional + A dictionary of temperature settings to use when generating MGXS. + Valid entries for temperature_settings are the same as the valid + entries in openmc.Settings.temperature_settings. """ - mgxs_sets = [] - for material in self.materials: - model = openmc.Model() - # Set materials on the model - model.materials = [material] + src = self._create_mgxs_sources( + groups, + spatial_dist=openmc.stats.Point(), + source_energy=source_energy + ) - # Settings - model.settings.batches = 100 - model.settings.particles = nparticles + temp_settings = {} + if temperature_settings == None: + temp_settings = self.settings.temperature + else: + temp_settings = temperature_settings - model.settings.source = self._create_mgxs_sources( - groups, - spatial_dist=openmc.stats.Point(), - source_energy=source_energy - ) + if temperatures == None: + mgxs_sets = [] + for material in self.materials: + xs_data = Model._isothermal_infinite_media_mgxs( + material, + groups, + nparticles, + correction, + directory, + src, + temp_settings + ) + mgxs_sets.append(xs_data) - model.settings.run_mode = 'fixed source' - model.settings.create_fission_neutrons = False + # Write the file to disk. + mgxs_file = openmc.MGXSLibrary(energy_groups=groups) + for mgxs_set in mgxs_sets: + mgxs_file.add_xsdata(mgxs_set) + mgxs_file.export_to_hdf5(mgxs_path) + else: + # Build a series of XSData objects, one for each isothermal temperature value. + raw_mgxs_sets = {} + for temperature in temperatures: + raw_mgxs_sets[temperature] = [] + for material in self.materials: + xs_data = Model._isothermal_infinite_media_mgxs( + material, + groups, + nparticles, + correction, + directory, + src, + temp_settings, + temperature + ) + raw_mgxs_sets[temperature].append(xs_data) - model.settings.output = {'summary': True, 'tallies': False} + # Unpack the isothermal XSData objects and build a single XSData object per material. + mgxs_sets = [] + for m in range(len(self.materials)): + mgxs_sets.append(openmc.XSdata(self.materials[m].name, groups)) + mgxs_sets[-1].order = 0 + for temperature in temperatures: + mgxs_sets[-1].add_temperature_data(raw_mgxs_sets[temperature][m]) - # Geometry - box = openmc.model.RectangularPrism( - 100000.0, 100000.0, boundary_type='reflective') - name = material.name - infinite_cell = openmc.Cell(name=name, fill=material, region=-box) - infinite_universe = openmc.Universe(name=name, cells=[infinite_cell]) - model.geometry.root_universe = infinite_universe - - # Add MGXS Tallies - mgxs_lib = self._auto_generate_mgxs_lib( - model, groups, correction, directory) - - # Create a MGXS File which can then be written to disk - mgxs_set = mgxs_lib.get_xsdata(domain=material, xsdata_name=name) - mgxs_sets.append(mgxs_set) - - # Write the file to disk - mgxs_file = openmc.MGXSLibrary(energy_groups=groups) - for mgxs_set in mgxs_sets: - mgxs_file.add_xsdata(mgxs_set) - mgxs_file.export_to_hdf5(mgxs_path) + # Write the file to disk. + mgxs_file = openmc.MGXSLibrary(energy_groups=groups) + for mgxs_set in mgxs_sets: + mgxs_file.add_xsdata(mgxs_set) + mgxs_file.export_to_hdf5(mgxs_path) @staticmethod def _create_stochastic_slab_geometry( @@ -2026,6 +2137,89 @@ class Model: return geometry, box + @staticmethod + def _isothermal_stochastic_slab_mgxs( + stoch_geom: openmc.Geometry, + groups: openmc.mgxs.EnergyGroups, + nparticles: int, + correction: str | None, + directory: PathLike, + source: openmc.IndependentSource, + temperature_settings: dict, + temperature: float | None = None, + ) -> dict[str, openmc.XSdata]: + """Generate MGXS assuming a stochastic "sandwich" of materials in a layered + slab geometry. If a temperature is specified, all materials in the slab have + their temperatures set to be isothermal at this temperature. + + Parameters + ---------- + stoch_geom : openmc.Geometry + The stochastic slab geometry. + groups : openmc.mgxs.EnergyGroups + Energy group structure for the MGXS. + nparticles : int + Number of particles to simulate per batch when generating MGXS. + correction : str + Transport correction to apply to the MGXS. Options are None and + "P0". + directory : str + Directory to run the simulation in, so as to contain XML files. + source : openmc.IndependentSource + Source to use when generating MGXS. + temperature_settings : dict + A dictionary of temperature settings to use when generating MGXS. + Valid entries for temperature_settings are the same as the valid + entries in openmc.Settings.temperature_settings. + temperature : float, optional + The isothermal temperature value to apply to the materials in the + slab. If not specified, defaults to the temperature in the materials. + + Returns + ------- + data : dict[str, openmc.XSdata] + A dictionary where the key is the name of the material and the value is the isothermal MGXS. + """ + + model = openmc.Model() + model.geometry = stoch_geom + + if temperature != None: + for material in model.geometry.get_all_materials().values(): + material.temperature = temperature + + # Settings + model.settings.batches = 200 + model.settings.inactive = 100 + model.settings.particles = nparticles + model.settings.output = {'summary': True, 'tallies': False} + model.settings.temperature = temperature_settings + + # Define the sources + model.settings.source = source + + model.settings.run_mode = 'fixed source' + model.settings.create_fission_neutrons = False + + model.settings.output = {'summary': True, 'tallies': False} + + # Generate MGXS + mgxs_lib = Model._auto_generate_mgxs_lib( + model, groups, correction, directory) + + # Fetch all of the isothermal results. + if temperature != None: + return { + mat.name : mgxs_lib.get_xsdata(domain=mat, xsdata_name=mat.name, + temperature=temperature) + for mat in mgxs_lib.domains + } + else: + return { + mat.name : mgxs_lib.get_xsdata(domain=mat, xsdata_name=mat.name) + for mat in mgxs_lib.domains + } + def _generate_stochastic_slab_mgxs( self, groups: openmc.mgxs.EnergyGroups, @@ -2034,6 +2228,8 @@ class Model: correction: str | None, directory: PathLike, source_energy: openmc.stats.Univariate | None = None, + temperatures: Sequence[float] | None = None, + temperature_settings: dict | None = None, ) -> None: """Generate MGXS assuming a stochastic "sandwich" of materials in a layered slab geometry. While geometry-specific spatial shielding effects are not @@ -2043,7 +2239,9 @@ class Model: will generate cross sections for all materials in the problem regardless of type. If this is a fixed source problem, a discrete source is used to sample particles, with an equal strength spread across each of the - energy groups. + energy groups. If temperature data points are provided, + isothermal cross sections are generated at each temperature point for + the stochastic slab to build a temperature interpolation table. Parameters ---------- @@ -2075,41 +2273,152 @@ class Model: no sources are defined on the model and the run mode is 'eigenvalue', then a default Watt spectrum source (strength = 0.99) is added. + temperatures : Sequence[float], optional + A list of temperatures to generate MGXS at. Each infinite material region + is isothermal at a given temperature data point for cross + section generation. + temperature_settings : dict, optional + A dictionary of temperature settings to use when generating MGXS. + Valid entries for temperature_settings are the same as the valid + entries in openmc.Settings.temperature_settings. """ - model = openmc.Model() - model.materials = self.materials + + # Stochastic slab geometry + geo, spatial_distribution = Model._create_stochastic_slab_geometry( + self.materials) + + src = self._create_mgxs_sources( + groups, + spatial_dist=spatial_distribution, + source_energy=source_energy + ) + + temp_settings = {} + if temperature_settings == None: + temp_settings = self.settings.temperature + else: + temp_settings = temperature_settings + + if temperatures == None: + mgxs_sets = Model._isothermal_stochastic_slab_mgxs( + geo, + groups, + nparticles, + correction, + directory, + src, + temp_settings + ).values() + + # Write the file to disk. + mgxs_file = openmc.MGXSLibrary(energy_groups=groups) + for mgxs_set in mgxs_sets: + mgxs_file.add_xsdata(mgxs_set) + mgxs_file.export_to_hdf5(mgxs_path) + else: + # Build a series of XSData objects, one for each isothermal temperature value. + raw_mgxs_sets = {} + for temperature in temperatures: + raw_mgxs_sets[temperature] = Model._isothermal_stochastic_slab_mgxs( + geo, + groups, + nparticles, + correction, + directory, + src, + temp_settings, + temperature + ) + + # Unpack the isothermal XSData objects and build a single XSData object per material. + mgxs_sets = [] + for mat in self.materials: + mgxs_sets.append(openmc.XSdata(mat.name, groups)) + mgxs_sets[-1].order = 0 + for temperature in temperatures: + mgxs_sets[-1].add_temperature_data(raw_mgxs_sets[temperature][mat.name]) + + # Write the file to disk. + mgxs_file = openmc.MGXSLibrary(energy_groups=groups) + for mgxs_set in mgxs_sets: + mgxs_file.add_xsdata(mgxs_set) + mgxs_file.export_to_hdf5(mgxs_path) + + @staticmethod + def _isothermal_materialwise_mgxs( + input_model: openmc.Model, + groups: openmc.mgxs.EnergyGroups, + nparticles: int, + correction: str | None, + directory: PathLike, + temperature_settings: dict, + temperature: float | None = None, + ) -> dict[str, openmc.XSdata]: + """Generate a material-wise MGXS library for the model by running the + original continuous energy OpenMC simulation. If a temperature is + specified, each material in the input model is set to that temperature. + Otherwise, the original material temperatures are used. If temperature + data points are provided, isothermal cross sections are generated at + each temperature point for the whole model to build a temperature + interpolation table. + + Parameters + ---------- + input_model : openmc.Model + The model to use when computing material-wise MGXS. + groups : openmc.mgxs.EnergyGroups + Energy group structure for the MGXS. + nparticles : int + Number of particles to simulate per batch when generating MGXS. + correction : str + Transport correction to apply to the MGXS. Options are None and + "P0". + directory : str + Directory to run the simulation in, so as to contain XML files. + temperature_settings : dict + A dictionary of temperature settings to use when generating MGXS. + Valid entries for temperature_settings are the same as the valid + entries in openmc.Settings.temperature_settings. + temperature : float, optional + The isothermal temperature value to apply to the materials in the + input model. If not specified, defaults to the temperatures in the + materials. + + Returns + ------- + data : dict[str, openmc.XSdata] + A dictionary where the key is the name of the material and the value is the isothermal MGXS. + """ + model = copy.deepcopy(input_model) + model.tallies = openmc.Tallies() + + if temperature != None: + for material in model.geometry.get_all_materials().values(): + material.temperature = temperature # Settings model.settings.batches = 200 model.settings.inactive = 100 model.settings.particles = nparticles model.settings.output = {'summary': True, 'tallies': False} + model.settings.temperature = temperature_settings - # Stochastic slab geometry - model.geometry, spatial_distribution = Model._create_stochastic_slab_geometry( - model.materials) - - # Define the sources - model.settings.source = self._create_mgxs_sources( - groups, - spatial_dist=spatial_distribution, - source_energy=source_energy - ) - - model.settings.run_mode = 'fixed source' - model.settings.create_fission_neutrons = False - - model.settings.output = {'summary': True, 'tallies': False} - - # Add MGXS Tallies - mgxs_lib = self._auto_generate_mgxs_lib( + # Generate MGXS + mgxs_lib = Model._auto_generate_mgxs_lib( model, groups, correction, directory) - names = [mat.name for mat in mgxs_lib.domains] - - # Create a MGXS File which can then be written to disk - mgxs_file = mgxs_lib.create_mg_library(xs_type='macro', xsdata_names=names) - mgxs_file.export_to_hdf5(mgxs_path) + # Fetch all of the isothermal results. + if temperature != None: + return { + mat.name : mgxs_lib.get_xsdata(domain=mat, xsdata_name=mat.name, + temperature=temperature) + for mat in mgxs_lib.domains + } + else: + return { + mat.name : mgxs_lib.get_xsdata(domain=mat, xsdata_name=mat.name) + for mat in mgxs_lib.domains + } def _generate_material_wise_mgxs( self, @@ -2118,6 +2427,8 @@ class Model: mgxs_path: PathLike, correction: str | None, directory: PathLike, + temperatures: Sequence[float] | None = None, + temperature_settings: dict | None = None, ) -> None: """Generate a material-wise MGXS library for the model by running the original continuous energy OpenMC simulation of the full material @@ -2142,26 +2453,63 @@ class Model: "P0". directory : PathLike Directory to run the simulation in, so as to contain XML files. + temperatures : Sequence[float], optional + A list of temperatures to generate MGXS at. Each infinite material region + is isothermal at a given temperature data point for cross + section generation. + temperature_settings : dict, optional + A dictionary of temperature settings to use when generating MGXS. + Valid entries for temperature_settings are the same as the valid + entries in openmc.Settings.temperature_settings. """ - model = copy.deepcopy(self) - model.tallies = openmc.Tallies() + temp_settings = {} + if temperature_settings == None: + temp_settings = self.settings.temperature + else: + temp_settings = temperature_settings - # Settings - model.settings.batches = 200 - model.settings.inactive = 100 - model.settings.particles = nparticles - model.settings.output = {'summary': True, 'tallies': False} + if temperatures == None: + mgxs_sets = Model._isothermal_materialwise_mgxs( + self, + groups, + nparticles, + correction, + directory, + temp_settings + ).values() - # Add MGXS Tallies - mgxs_lib = self._auto_generate_mgxs_lib( - model, groups, correction, directory) + # Write the file to disk. + mgxs_file = openmc.MGXSLibrary(energy_groups=groups) + for mgxs_set in mgxs_sets: + mgxs_file.add_xsdata(mgxs_set) + mgxs_file.export_to_hdf5(mgxs_path) + else: + # Build a series of XSData objects, one for each isothermal temperature value. + raw_mgxs_sets = {} + for temperature in temperatures: + raw_mgxs_sets[temperature] = Model._isothermal_materialwise_mgxs( + self, + groups, + nparticles, + correction, + directory, + temp_settings, + temperature + ) - names = [mat.name for mat in mgxs_lib.domains] + # Unpack the isothermal XSData objects and build a single XSData object per material. + mgxs_sets = [] + for mat in self.materials: + mgxs_sets.append(openmc.XSdata(mat.name, groups)) + mgxs_sets[-1].order = 0 + for temperature in temperatures: + mgxs_sets[-1].add_temperature_data(raw_mgxs_sets[temperature][mat.name]) - # Create a MGXS File which can then be written to disk - mgxs_file = mgxs_lib.create_mg_library( - xs_type='macro', xsdata_names=names) - mgxs_file.export_to_hdf5(mgxs_path) + # Write the file to disk. + mgxs_file = openmc.MGXSLibrary(energy_groups=groups) + for mgxs_set in mgxs_sets: + mgxs_file.add_xsdata(mgxs_set) + mgxs_file.export_to_hdf5(mgxs_path) def convert_to_multigroup( self, @@ -2172,6 +2520,8 @@ class Model: mgxs_path: PathLike = "mgxs.h5", correction: str | None = None, source_energy: openmc.stats.Univariate | None = None, + temperatures: Sequence[float] | None = None, + temperature_settings: dict | None = None, ): """Convert all materials from continuous energy to multigroup. @@ -2212,6 +2562,14 @@ class Model: 'eigenvalue', then a default Watt spectrum source (strength = 0.99) is added. Note that this argument is only used when using the "stochastic_slab" or "infinite_medium" MGXS generation methods. + temperatures : Sequence[float], optional + A list of temperatures to generate MGXS at. Each infinite material region + is isothermal at a given temperature data point for cross + section generation. + temperature_settings : dict, optional + A dictionary of temperature settings to use when generating MGXS. + Valid entries for temperature_settings are the same as the valid + entries in openmc.Settings.temperature_settings. """ if isinstance(groups, str): groups = openmc.mgxs.EnergyGroups(groups) @@ -2241,13 +2599,16 @@ class Model: if not Path(mgxs_path).is_file() or overwrite_mgxs_library: if method == "infinite_medium": self._generate_infinite_medium_mgxs( - groups, nparticles, mgxs_path, correction, tmpdir, source_energy) + groups, nparticles, mgxs_path, correction, tmpdir, source_energy, + temperatures, temperature_settings) elif method == "material_wise": self._generate_material_wise_mgxs( - groups, nparticles, mgxs_path, correction, tmpdir) + groups, nparticles, mgxs_path, correction, tmpdir, + temperatures, temperature_settings) elif method == "stochastic_slab": self._generate_stochastic_slab_mgxs( - groups, nparticles, mgxs_path, correction, tmpdir, source_energy) + groups, nparticles, mgxs_path, correction, tmpdir, source_energy, + temperatures, temperature_settings) else: raise ValueError( f'MGXS generation method "{method}" not recognized') diff --git a/src/mgxs_interface.cpp b/src/mgxs_interface.cpp index 34f87d179..08c64413a 100644 --- a/src/mgxs_interface.cpp +++ b/src/mgxs_interface.cpp @@ -169,13 +169,13 @@ vector> MgxsInterface::get_mat_kTs() continue; // Get temperature of cell (rounding to nearest integer) - double sqrtkT = - cell->sqrtkT_.size() == 1 ? cell->sqrtkT_[j] : cell->sqrtkT_[0]; - double kT = sqrtkT * sqrtkT; + for (int k = 0; k < cell->sqrtkT_.size(); ++k) { + double kT = cell->sqrtkT_[k] * cell->sqrtkT_[k]; - // Add temperature if it hasn't already been added - if (!contains(kTs[i_material], kT)) { - kTs[i_material].push_back(kT); + // Add temperature if it hasn't already been added + if (!contains(kTs[i_material], kT)) { + kTs[i_material].push_back(kT); + } } } } diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 32b334933..13a1b0163 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -109,11 +109,13 @@ void FlatSourceDomain::update_single_neutron_source(SourceRegionHandle& srh) // Add scattering + fission source int material = srh.material(); + int temp = srh.temperature_idx(); double density_mult = srh.density_mult(); if (material != MATERIAL_VOID) { double inverse_k_eff = 1.0 / k_eff_; - int material_offset = material * negroups_; - int scatter_offset = material * negroups_ * negroups_; + const int material_offset = (material * ntemperature_ + temp) * negroups_; + const int scatter_offset = + (material * ntemperature_ + temp) * negroups_ * negroups_; for (int g_out = 0; g_out < negroups_; g_out++) { double sigma_t = sigma_t_[material_offset + g_out] * density_mult; double scatter_source = 0.0; @@ -193,6 +195,7 @@ void FlatSourceDomain::set_flux_to_flux_plus_source( int64_t sr, double volume, int g) { int material = source_regions_.material(sr); + int temp = source_regions_.temperature_idx(sr); if (material == MATERIAL_VOID) { source_regions_.scalar_flux_new(sr, g) /= volume; if (settings::run_mode == RunMode::FIXED_SOURCE) { @@ -201,8 +204,9 @@ void FlatSourceDomain::set_flux_to_flux_plus_source( source_regions_.volume_sq(sr); } } else { - double sigma_t = sigma_t_[source_regions_.material(sr) * negroups_ + g] * - source_regions_.density_mult(sr); + double sigma_t = + sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] * + source_regions_.density_mult(sr); source_regions_.scalar_flux_new(sr, g) /= (sigma_t * volume); source_regions_.scalar_flux_new(sr, g) += source_regions_.source(sr, g); } @@ -328,6 +332,7 @@ void FlatSourceDomain::compute_k_eff() } int material = source_regions_.material(sr); + int temp = source_regions_.temperature_idx(sr); if (material == MATERIAL_VOID) { continue; } @@ -336,8 +341,9 @@ void FlatSourceDomain::compute_k_eff() double sr_fission_source_new = 0; for (int g = 0; g < negroups_; g++) { - double nu_sigma_f = nu_sigma_f_[material * negroups_ + g] * - source_regions_.density_mult(sr); + double nu_sigma_f = + nu_sigma_f_[(material * ntemperature_ + temp) * negroups_ + g] * + source_regions_.density_mult(sr); sr_fission_source_old += nu_sigma_f * source_regions_.scalar_flux_old(sr, g); sr_fission_source_new += @@ -560,6 +566,7 @@ double FlatSourceDomain::compute_fixed_source_normalization_factor() const #pragma omp parallel for reduction(+ : simulation_external_source_strength) for (int64_t sr = 0; sr < n_source_regions(); sr++) { int material = source_regions_.material(sr); + int temp = source_regions_.temperature_idx(sr); double volume = source_regions_.volume(sr) * simulation_volume_; for (int g = 0; g < negroups_; g++) { // For non-void regions, we store the external source pre-divided by @@ -567,8 +574,8 @@ double FlatSourceDomain::compute_fixed_source_normalization_factor() const // to get the total source strength in the expected units. double sigma_t = 1.0; if (material != MATERIAL_VOID) { - sigma_t = - sigma_t_[material * negroups_ + g] * source_regions_.density_mult(sr); + sigma_t = sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] * + source_regions_.density_mult(sr); } simulation_external_source_strength += source_regions_.external_source(sr, g) * sigma_t * volume; @@ -624,9 +631,9 @@ void FlatSourceDomain::random_ray_tally() // source strength. double volume = source_regions_.volume(sr) * simulation_volume_; - int material = source_regions_.material(sr); + double material = source_regions_.material(sr); + int temp = source_regions_.temperature_idx(sr); double density_mult = source_regions_.density_mult(sr); - for (int g = 0; g < negroups_; g++) { double flux = source_regions_.scalar_flux_new(sr, g) * source_normalization_factor; @@ -643,21 +650,27 @@ void FlatSourceDomain::random_ray_tally() case SCORE_TOTAL: if (material != MATERIAL_VOID) { score = - flux * volume * sigma_t_[material * negroups_ + g] * density_mult; + flux * volume * + sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] * + density_mult; } break; case SCORE_FISSION: if (material != MATERIAL_VOID) { score = - flux * volume * sigma_f_[material * negroups_ + g] * density_mult; + flux * volume * + sigma_f_[(material * ntemperature_ + temp) * negroups_ + g] * + density_mult; } break; case SCORE_NU_FISSION: if (material != MATERIAL_VOID) { - score = flux * volume * nu_sigma_f_[material * negroups_ + g] * - density_mult; + score = + flux * volume * + nu_sigma_f_[(material * ntemperature_ + temp) * negroups_ + g] * + density_mult; } break; @@ -666,8 +679,10 @@ void FlatSourceDomain::random_ray_tally() break; case SCORE_KAPPA_FISSION: - score = flux * volume * kappa_fission_[material * negroups_ + g] * - density_mult; + score = + flux * volume * + kappa_fission_[(material * ntemperature_ + temp) * negroups_ + g] * + density_mult; break; default: @@ -925,12 +940,14 @@ void FlatSourceDomain::output_to_vtk() const float total_fission = 0.0; if (fsr >= 0) { int mat = source_regions_.material(fsr); + int temp = source_regions_.temperature_idx(fsr); if (mat != MATERIAL_VOID) { for (int g = 0; g < negroups_; g++) { int64_t source_element = fsr * negroups_ + g; float flux = evaluate_flux_at_point(voxel_positions[i], fsr, g); - double sigma_f = sigma_f_[mat * negroups_ + g] * - source_regions_.density_mult(fsr); + double sigma_f = + sigma_f_[(mat * ntemperature_ + temp) * negroups_ + g] * + source_regions_.density_mult(fsr); total_fission += sigma_f * flux; } } @@ -944,6 +961,7 @@ void FlatSourceDomain::output_to_vtk() const for (int i = 0; i < Nx * Ny * Nz; i++) { int64_t fsr = voxel_indices[i]; int mat = source_regions_.material(fsr); + int temp = source_regions_.temperature_idx(fsr); float total_external = 0.0f; if (fsr >= 0) { for (int g = 0; g < negroups_; g++) { @@ -951,7 +969,7 @@ void FlatSourceDomain::output_to_vtk() const // multiply it back to get the true external source. double sigma_t = 1.0; if (mat != MATERIAL_VOID) { - sigma_t = sigma_t_[mat * negroups_ + g] * + sigma_t = sigma_t_[(mat * ntemperature_ + temp) * negroups_ + g] * source_regions_.density_mult(fsr); } total_external += source_regions_.external_source(fsr, g) * sigma_t; @@ -1131,67 +1149,74 @@ void FlatSourceDomain::flatten_xs() { // Temperature and angle indices, if using multiple temperature // data sets and/or anisotropic data sets. - // TODO: Currently assumes we are only using single temp/single angle data. - const int t = 0; + // TODO: Currently assumes we are only using single angle data. const int a = 0; n_materials_ = data::mg.macro_xs_.size(); + ntemperature_ = 1; + for (int i = 0; i < n_materials_; i++) { + ntemperature_ = + std::max(ntemperature_, data::mg.macro_xs_[i].n_temperature_points()); + } + for (int i = 0; i < n_materials_; i++) { auto& m = data::mg.macro_xs_[i]; - for (int g_out = 0; g_out < negroups_; g_out++) { - if (m.exists_in_model) { - double sigma_t = - m.get_xs(MgxsType::TOTAL, g_out, NULL, NULL, NULL, t, a); - sigma_t_.push_back(sigma_t); + for (int t = 0; t < ntemperature_; t++) { + for (int g_out = 0; g_out < negroups_; g_out++) { + if (m.exists_in_model && t < m.n_temperature_points()) { + double sigma_t = + m.get_xs(MgxsType::TOTAL, g_out, NULL, NULL, NULL, t, a); + sigma_t_.push_back(sigma_t); - if (sigma_t < MINIMUM_MACRO_XS) { - Material* mat = model::materials[i].get(); - warning(fmt::format( - "Material \"{}\" (id: {}) has a group {} total cross section " - "({:.3e}) below the minimum threshold " - "({:.3e}). Material will be treated as pure void.", - mat->name(), mat->id(), g_out, sigma_t, MINIMUM_MACRO_XS)); - } + if (sigma_t < MINIMUM_MACRO_XS) { + Material* mat = model::materials[i].get(); + warning(fmt::format( + "Material \"{}\" (id: {}) has a group {} total cross section " + "({:.3e}) below the minimum threshold " + "({:.3e}). Material will be treated as pure void.", + mat->name(), mat->id(), g_out, sigma_t, MINIMUM_MACRO_XS)); + } - double nu_sigma_f = - m.get_xs(MgxsType::NU_FISSION, g_out, NULL, NULL, NULL, t, a); - nu_sigma_f_.push_back(nu_sigma_f); + double nu_sigma_f = + m.get_xs(MgxsType::NU_FISSION, g_out, NULL, NULL, NULL, t, a); + nu_sigma_f_.push_back(nu_sigma_f); - double sigma_f = - m.get_xs(MgxsType::FISSION, g_out, NULL, NULL, NULL, t, a); - sigma_f_.push_back(sigma_f); + double sigma_f = + m.get_xs(MgxsType::FISSION, g_out, NULL, NULL, NULL, t, a); + sigma_f_.push_back(sigma_f); - double chi = - m.get_xs(MgxsType::CHI_PROMPT, g_out, &g_out, NULL, NULL, t, a); - if (!std::isfinite(chi)) { - // MGXS interface may return NaN in some cases, such as when material - // is fissionable but has very small sigma_f. - chi = 0.0; - } - chi_.push_back(chi); + double chi = + m.get_xs(MgxsType::CHI_PROMPT, g_out, &g_out, NULL, NULL, t, a); + if (!std::isfinite(chi)) { + // MGXS interface may return NaN in some cases, such as when + // material is fissionable but has very small sigma_f. + chi = 0.0; + } + chi_.push_back(chi); - double kappa_fission = - m.get_xs(MgxsType::KAPPA_FISSION, g_out, NULL, NULL, NULL, t, a); - kappa_fission_.push_back(kappa_fission); + double kappa_fission = + m.get_xs(MgxsType::KAPPA_FISSION, g_out, NULL, NULL, NULL, t, a); + kappa_fission_.push_back(kappa_fission); - for (int g_in = 0; g_in < negroups_; g_in++) { - double sigma_s = - m.get_xs(MgxsType::NU_SCATTER, g_in, &g_out, NULL, NULL, t, a); - sigma_s_.push_back(sigma_s); - // For transport corrected XS data, diagonal elements may be negative. - // In this case, set a flag to enable transport stabilization for the - // simulation. - if (g_out == g_in && sigma_s < 0.0) - is_transport_stabilization_needed_ = true; - } - } else { - sigma_t_.push_back(0); - nu_sigma_f_.push_back(0); - sigma_f_.push_back(0); - chi_.push_back(0); - kappa_fission_.push_back(0); - for (int g_in = 0; g_in < negroups_; g_in++) { - sigma_s_.push_back(0); + for (int g_in = 0; g_in < negroups_; g_in++) { + double sigma_s = + m.get_xs(MgxsType::NU_SCATTER, g_in, &g_out, NULL, NULL, t, a); + sigma_s_.push_back(sigma_s); + // For transport corrected XS data, diagonal elements may be + // negative. In this case, set a flag to enable transport + // stabilization for the simulation. + if (g_out == g_in && sigma_s < 0.0) + is_transport_stabilization_needed_ = true; + } + } else { + sigma_t_.push_back(0); + nu_sigma_f_.push_back(0); + sigma_f_.push_back(0); + chi_.push_back(0); + kappa_fission_.push_back(0); + for (int g_in = 0; g_in < negroups_; g_in++) { + sigma_s_.push_back(0); + } } } } @@ -1263,12 +1288,14 @@ void FlatSourceDomain::set_adjoint_sources() #pragma omp parallel for for (int64_t sr = 0; sr < n_source_regions(); sr++) { int material = source_regions_.material(sr); + int temp = source_regions_.temperature_idx(sr); if (material == MATERIAL_VOID) { continue; } for (int g = 0; g < negroups_; g++) { double sigma_t = - sigma_t_[material * negroups_ + g] * source_regions_.density_mult(sr); + sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] * + source_regions_.density_mult(sr); source_regions_.external_source(sr, g) /= sigma_t; } } @@ -1279,15 +1306,17 @@ void FlatSourceDomain::transpose_scattering_matrix() // Transpose the inner two dimensions for each material #pragma omp parallel for for (int m = 0; m < n_materials_; ++m) { - int material_offset = m * negroups_ * negroups_; - for (int i = 0; i < negroups_; ++i) { - for (int j = i + 1; j < negroups_; ++j) { - // Calculate indices of the elements to swap - int idx1 = material_offset + i * negroups_ + j; - int idx2 = material_offset + j * negroups_ + i; + for (int t = 0; t < ntemperature_; t++) { + int material_offset = (m * ntemperature_ + t) * negroups_ * negroups_; + for (int i = 0; i < negroups_; ++i) { + for (int j = i + 1; j < negroups_; ++j) { + // Calculate indices of the elements to swap + int idx1 = material_offset + i * negroups_ + j; + int idx2 = material_offset + j * negroups_ + i; - // Swap the elements to transpose the matrix - std::swap(sigma_s_[idx1], sigma_s_[idx2]); + // Swap the elements to transpose the matrix + std::swap(sigma_s_[idx1], sigma_s_[idx2]); + } } } } @@ -1507,18 +1536,26 @@ SourceRegionHandle FlatSourceDomain::get_subdivided_source_region_handle( int gs_i_cell = gs.lowest_coord().cell(); Cell& cell = *model::cells[gs_i_cell]; int material = cell.material(gs.cell_instance()); + int temp = 0; // If material total XS is extremely low, just set it to void to avoid // problems with 1/Sigma_t - for (int g = 0; g < negroups_; g++) { - double sigma_t = sigma_t_[material * negroups_ + g]; - if (sigma_t < MINIMUM_MACRO_XS) { - material = MATERIAL_VOID; - break; + if (material != MATERIAL_VOID) { + temp = data::mg.macro_xs_[material].get_temperature_index( + cell.sqrtkT(gs.cell_instance())); + for (int g = 0; g < negroups_; g++) { + double sigma_t = + sigma_t_[(material * ntemperature_ + temp) * negroups_ + g]; + if (sigma_t < MINIMUM_MACRO_XS) { + material = MATERIAL_VOID; + temp = 0; + break; + } } } handle.material() = material; + handle.temperature_idx() = temp; handle.density_mult() = cell.density_mult(gs.cell_instance()); @@ -1551,7 +1588,8 @@ SourceRegionHandle FlatSourceDomain::get_subdivided_source_region_handle( if (material != C_NONE) { for (int g = 0; g < negroups_; g++) { double sigma_t = - sigma_t_[material * negroups_ + g] * handle.density_mult(); + sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] * + handle.density_mult(); handle.external_source(g) /= sigma_t; } } @@ -1626,6 +1664,7 @@ void FlatSourceDomain::apply_transport_stabilization() #pragma omp parallel for for (int64_t sr = 0; sr < n_source_regions(); sr++) { int material = source_regions_.material(sr); + int temp = source_regions_.temperature_idx(sr); double density_mult = source_regions_.density_mult(sr); if (material == MATERIAL_VOID) { continue; @@ -1634,10 +1673,14 @@ void FlatSourceDomain::apply_transport_stabilization() // Only apply stabilization if the diagonal (in-group) scattering XS is // negative double sigma_s = - sigma_s_[material * negroups_ * negroups_ + g * negroups_ + g] * + sigma_s_[((material * ntemperature_ + temp) * negroups_ + g) * + negroups_ + + g] * density_mult; if (sigma_s < 0.0) { - double sigma_t = sigma_t_[material * negroups_ + g] * density_mult; + double sigma_t = + sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] * + density_mult; double phi_new = source_regions_.scalar_flux_new(sr, g); double phi_old = source_regions_.scalar_flux_old(sr, g); diff --git a/src/random_ray/linear_source_domain.cpp b/src/random_ray/linear_source_domain.cpp index 02f4c9e23..b4701ed1f 100644 --- a/src/random_ray/linear_source_domain.cpp +++ b/src/random_ray/linear_source_domain.cpp @@ -43,13 +43,16 @@ void LinearSourceDomain::update_single_neutron_source(SourceRegionHandle& srh) // Add scattering + fission source int material = srh.material(); + int temp = srh.temperature_idx(); double density_mult = srh.density_mult(); if (material != MATERIAL_VOID) { double inverse_k_eff = 1.0 / k_eff_; MomentMatrix invM = srh.mom_matrix().inverse(); for (int g_out = 0; g_out < negroups_; g_out++) { - double sigma_t = sigma_t_[material * negroups_ + g_out] * density_mult; + double sigma_t = + sigma_t_[(material * ntemperature_ + temp) * negroups_ + g_out] * + density_mult; double scatter_flat = 0.0f; double fission_flat = 0.0f; @@ -62,12 +65,16 @@ void LinearSourceDomain::update_single_neutron_source(SourceRegionHandle& srh) MomentArray flux_linear = srh.flux_moments_old(g_in); // Handles for cross sections - double sigma_s = sigma_s_[material * negroups_ * negroups_ + - g_out * negroups_ + g_in] * - density_mult; + double sigma_s = + sigma_s_[((material * ntemperature_ + temp) * negroups_ + g_out) * + negroups_ + + g_in] * + density_mult; double nu_sigma_f = - nu_sigma_f_[material * negroups_ + g_in] * density_mult; - double chi = chi_[material * negroups_ + g_out]; + nu_sigma_f_[(material * ntemperature_ + temp) * negroups_ + g_in] * + density_mult; + double chi = + chi_[(material * ntemperature_ + temp) * negroups_ + g_out]; // Compute source terms for flat and linear components of the flux scatter_flat += sigma_s * flux_flat; diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp index 1b61d8c20..47d901d63 100644 --- a/src/random_ray/random_ray.cpp +++ b/src/random_ray/random_ray.cpp @@ -432,11 +432,13 @@ void RandomRay::attenuate_flux_flat_source( // Get material int material = srh.material(); + int temp = srh.temperature_idx(); // MOC incoming flux attenuation + source contribution/attenuation equation for (int g = 0; g < negroups_; g++) { float sigma_t = - domain_->sigma_t_[material * negroups_ + g] * srh.density_mult(); + domain_->sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] * + srh.density_mult(); float tau = sigma_t * distance; float exponential = cjosey_exponential(tau); // exponential = 1 - exp(-tau) float new_delta_psi = (angular_flux_[g] - srh.source(g)) * exponential; @@ -531,6 +533,7 @@ void RandomRay::attenuate_flux_linear_source( n_event()++; int material = srh.material(); + int temp = srh.temperature_idx(); Position& centroid = srh.centroid(); Position midpoint = r + u() * (distance / 2.0); @@ -560,7 +563,8 @@ void RandomRay::attenuate_flux_linear_source( // Compute tau, the optical thickness of the ray segment float sigma_t = - domain_->sigma_t_[material * negroups_ + g] * srh.density_mult(); + domain_->sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] * + srh.density_mult(); float tau = sigma_t * distance; // If tau is very small, set it to zero to avoid numerical issues. @@ -765,6 +769,7 @@ void RandomRay::attenuate_flux_linear_source_void( void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain) { domain_ = domain; + ntemperature_ = domain->ntemperature_; // Reset particle event counter n_event() = 0; diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index af3916e3f..5970dc65a 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -79,10 +79,6 @@ void validate_random_ray_inputs() fatal_error("Anisotropic MGXS detected. Only isotropic XS data sets " "supported in random ray mode."); } - if (material.get_xsdata().size() > 1) { - warning("Non-isothermal MGXS detected. Only isothermal XS data sets " - "supported in random ray mode. Using lowest temperature."); - } for (int g = 0; g < data::mg.num_energy_groups_; g++) { if (material.exists_in_model) { // Temperature and angle indices, if using multiple temperature diff --git a/src/random_ray/source_region.cpp b/src/random_ray/source_region.cpp index 15c65221a..78543c5ab 100644 --- a/src/random_ray/source_region.cpp +++ b/src/random_ray/source_region.cpp @@ -11,11 +11,11 @@ namespace openmc { //============================================================================== SourceRegionHandle::SourceRegionHandle(SourceRegion& sr) : negroups_(sr.scalar_flux_old_.size()), material_(&sr.material_), - density_mult_(&sr.density_mult_), is_small_(&sr.is_small_), - n_hits_(&sr.n_hits_), is_linear_(sr.source_gradients_.size() > 0), - lock_(&sr.lock_), volume_(&sr.volume_), volume_t_(&sr.volume_t_), - volume_sq_(&sr.volume_sq_), volume_sq_t_(&sr.volume_sq_t_), - volume_naive_(&sr.volume_naive_), + temperature_idx_(&sr.temperature_idx_), density_mult_(&sr.density_mult_), + is_small_(&sr.is_small_), n_hits_(&sr.n_hits_), + is_linear_(sr.source_gradients_.size() > 0), lock_(&sr.lock_), + volume_(&sr.volume_), volume_t_(&sr.volume_t_), volume_sq_(&sr.volume_sq_), + volume_sq_t_(&sr.volume_sq_t_), volume_naive_(&sr.volume_naive_), position_recorded_(&sr.position_recorded_), external_source_present_(&sr.external_source_present_), position_(&sr.position_), centroid_(&sr.centroid_), @@ -71,6 +71,7 @@ void SourceRegionContainer::push_back(const SourceRegion& sr) // Scalar fields material_.push_back(sr.material_); + temperature_idx_.push_back(sr.temperature_idx_); density_mult_.push_back(sr.density_mult_); is_small_.push_back(sr.is_small_); n_hits_.push_back(sr.n_hits_); @@ -125,6 +126,7 @@ void SourceRegionContainer::assign( // Clear existing data n_source_regions_ = 0; material_.clear(); + temperature_idx_.clear(); density_mult_.clear(); is_small_.clear(); n_hits_.clear(); @@ -183,6 +185,7 @@ SourceRegionHandle SourceRegionContainer::get_source_region_handle(int64_t sr) SourceRegionHandle handle; handle.negroups_ = negroups(); handle.material_ = &material(sr); + handle.temperature_idx_ = &temperature_idx(sr); handle.density_mult_ = &density_mult(sr); handle.is_small_ = &is_small(sr); handle.n_hits_ = &n_hits(sr); diff --git a/tests/regression_tests/random_ray_auto_convert_temperature/__init__.py b/tests/regression_tests/random_ray_auto_convert_temperature/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_auto_convert_temperature/infinite_medium/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_temperature/infinite_medium/inputs_true.dat new file mode 100644 index 000000000..08a30176b --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_temperature/infinite_medium/inputs_true.dat @@ -0,0 +1,70 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + 395.0 + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + + + -0.63 -0.63 -1 0.63 0.63 1 + + + true + + + multi-group + nearest + true + 200.0 400.0 + 200.0 + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 + 150.0 + + + + + + linear + + + 2 2 + -0.63 -0.63 + 0.63 0.63 + + + diff --git a/tests/regression_tests/random_ray_auto_convert_temperature/infinite_medium/results_true.dat b/tests/regression_tests/random_ray_auto_convert_temperature/infinite_medium/results_true.dat new file mode 100644 index 000000000..fee8bf670 --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_temperature/infinite_medium/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +7.499800E-01 1.615317E-02 diff --git a/tests/regression_tests/random_ray_auto_convert_temperature/material_wise/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_temperature/material_wise/inputs_true.dat new file mode 100644 index 000000000..08a30176b --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_temperature/material_wise/inputs_true.dat @@ -0,0 +1,70 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + 395.0 + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + + + -0.63 -0.63 -1 0.63 0.63 1 + + + true + + + multi-group + nearest + true + 200.0 400.0 + 200.0 + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 + 150.0 + + + + + + linear + + + 2 2 + -0.63 -0.63 + 0.63 0.63 + + + diff --git a/tests/regression_tests/random_ray_auto_convert_temperature/material_wise/results_true.dat b/tests/regression_tests/random_ray_auto_convert_temperature/material_wise/results_true.dat new file mode 100644 index 000000000..ef0a4b87a --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_temperature/material_wise/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +7.367927E-01 6.850805E-03 diff --git a/tests/regression_tests/random_ray_auto_convert_temperature/stochastic_slab/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_temperature/stochastic_slab/inputs_true.dat new file mode 100644 index 000000000..08a30176b --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_temperature/stochastic_slab/inputs_true.dat @@ -0,0 +1,70 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + 395.0 + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + + + -0.63 -0.63 -1 0.63 0.63 1 + + + true + + + multi-group + nearest + true + 200.0 400.0 + 200.0 + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 + 150.0 + + + + + + linear + + + 2 2 + -0.63 -0.63 + 0.63 0.63 + + + diff --git a/tests/regression_tests/random_ray_auto_convert_temperature/stochastic_slab/results_true.dat b/tests/regression_tests/random_ray_auto_convert_temperature/stochastic_slab/results_true.dat new file mode 100644 index 000000000..a702ec851 --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_temperature/stochastic_slab/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +6.431774E-01 2.076589E-02 diff --git a/tests/regression_tests/random_ray_auto_convert_temperature/test.py b/tests/regression_tests/random_ray_auto_convert_temperature/test.py new file mode 100644 index 000000000..99c99e614 --- /dev/null +++ b/tests/regression_tests/random_ray_auto_convert_temperature/test.py @@ -0,0 +1,73 @@ +import os + +import openmc +from openmc.examples import pwr_pin_cell +from openmc import RegularMesh +from openmc.utility_funcs import change_directory +import pytest + +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) + + +@pytest.mark.parametrize("method", ["material_wise", "stochastic_slab", "infinite_medium"]) +def test_random_ray_auto_convert(method): + with change_directory(method): + openmc.reset_auto_ids() + + # Start with a normal continuous energy model + model = pwr_pin_cell() + + temp_settings = { + 'method' : 'nearest', + 'tolerance' : 200.0, + 'range' : (200.0, 400.0), + 'multipole' : True + } + + # Convert to a multi-group model + model.convert_to_multigroup( + method=method, groups='CASMO-2', nparticles=100, + overwrite_mgxs_library=False, mgxs_path="mgxs.h5", + temperatures=[294.0, 394.0], temperature_settings=temp_settings + ) + + # Convert to a random ray model + model.convert_to_random_ray() + model.settings.temperature = temp_settings + + # Set all material temperatures to room temperature + for mat in model.geometry.get_all_materials().values(): + mat.temperature = 294.0 + + # Set the cell temperature of the fuel such that it moves up to the next + # temperature bin. + for cell in model.geometry.get_all_cells().values(): + if cell.name == "Fuel": + cell.temperature = [395.0] + + # Set the number of particles + model.settings.particles = 100 + + # Overlay a basic 2x2 mesh + n = 2 + mesh = RegularMesh() + mesh.dimension = (n, n) + bbox = model.geometry.bounding_box + mesh.lower_left = (bbox.lower_left[0], bbox.lower_left[1]) + mesh.upper_right = (bbox.upper_right[0], bbox.upper_right[1]) + model.settings.random_ray['source_region_meshes'] = [ + (mesh, [model.geometry.root_universe])] + + # Set the source shape to linear + model.settings.random_ray['source_shape'] = 'linear' + + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_cell_density/test.py b/tests/regression_tests/random_ray_cell_density/test.py index cb6062cdc..48ebe0baa 100644 --- a/tests/regression_tests/random_ray_cell_density/test.py +++ b/tests/regression_tests/random_ray_cell_density/test.py @@ -22,7 +22,7 @@ def test_random_ray_basic(run_mode): if run_mode == "eigen": openmc.reset_auto_ids() model = random_ray_lattice() - # Double the densities of the lower-left fuel pin -> cell instances [0, 9). + # Double the densities of the lower-left fuel pin -> cell instances [0, 8). for id, cell in model.geometry.get_all_cells().items(): if cell.fill.name == "UO2 fuel": cell.density = [((i < 8) + 1.0) for i in range(24)] diff --git a/tests/regression_tests/random_ray_cell_temperature/__init__.py b/tests/regression_tests/random_ray_cell_temperature/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_cell_temperature/inputs_true.dat b/tests/regression_tests/random_ray_cell_temperature/inputs_true.dat new file mode 100644 index 000000000..e8674e6ea --- /dev/null +++ b/tests/regression_tests/random_ray_cell_temperature/inputs_true.dat @@ -0,0 +1,118 @@ + + + + mgxs.h5 + + + + + + + + + + + + 395.0 395.0 395.0 395.0 395.0 395.0 395.0 395.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 + + + 395.0 395.0 395.0 395.0 395.0 395.0 395.0 395.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 + + + 395.0 395.0 395.0 395.0 395.0 395.0 395.0 395.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 295.0 + + + + + + + + + + + + + + + + + 0.126 0.126 + 10 10 + -0.63 -0.63 + +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 + + + 1.26 1.26 + 2 2 + -1.26 -1.26 + +2 2 +2 5 + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + multi-group + nearest + 200.0 400.0 + 10.0 + + 100.0 + 20.0 + + + -1.26 -1.26 -1 1.26 1.26 1 + + + true + + + + + 2 2 + -1.26 -1.26 + 1.26 1.26 + + + 1 + + + 1e-05 0.0635 10.0 100.0 1000.0 500000.0 1000000.0 20000000.0 + + + 1 2 + flux fission nu-fission + analog + + + diff --git a/tests/regression_tests/random_ray_cell_temperature/results_true.dat b/tests/regression_tests/random_ray_cell_temperature/results_true.dat new file mode 100644 index 000000000..50476beb6 --- /dev/null +++ b/tests/regression_tests/random_ray_cell_temperature/results_true.dat @@ -0,0 +1,171 @@ +k-combined: +8.721099E-01 6.686066E-03 +tally 1: +1.530055E+00 +4.684582E-01 +2.918032E-01 +1.703772E-02 +7.101906E-01 +1.009209E-01 +7.732582E-01 +1.197247E-01 +5.787213E-02 +6.705930E-04 +1.408492E-01 +3.972179E-03 +4.223488E-01 +3.605283E-02 +6.818063E-03 +9.396342E-06 +1.659380E-02 +5.565812E-05 +5.958551E-01 +7.215230E-02 +9.932163E-03 +2.004773E-05 +2.417290E-02 +1.187504E-04 +1.685106E+00 +5.752729E-01 +9.834826E-03 +1.960236E-05 +2.393629E-02 +1.161151E-04 +4.400457E+00 +3.886563E+00 +3.318560E-03 +2.211175E-06 +8.211544E-03 +1.353859E-05 +2.814971E+00 +1.585202E+00 +1.876190E-02 +7.040316E-05 +5.218528E-02 +5.446713E-04 +1.970406E+00 +7.765020E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +8.643086E-01 +1.494764E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.421562E-01 +3.961223E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +6.421077E-01 +8.385255E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.717683E+00 +5.974687E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.024715E+00 +3.251629E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.507885E+00 +1.258277E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.237600E+00 +3.066006E-01 +4.632318E-01 +4.294997E-02 +1.127413E+00 +2.544090E-01 +7.085300E-01 +1.005838E-01 +1.066845E-01 +2.280854E-03 +2.596486E-01 +1.351037E-02 +4.080999E-01 +3.359223E-02 +1.334606E-02 +3.590848E-05 +3.248163E-02 +2.126996E-04 +5.587686E-01 +6.339264E-02 +1.868162E-02 +7.084921E-05 +4.546734E-02 +4.196670E-04 +1.647852E+00 +5.503007E-01 +1.940765E-02 +7.635704E-05 +4.723492E-02 +4.523030E-04 +4.672431E+00 +4.381400E+00 +7.228567E-03 +1.048470E-05 +1.788658E-02 +6.419579E-05 +3.054158E+00 +1.866081E+00 +4.202691E-02 +3.534383E-04 +1.168957E-01 +2.734362E-03 +1.313678E+00 +3.454374E-01 +4.964081E-01 +4.934325E-02 +1.208158E+00 +2.922789E-01 +7.298176E-01 +1.067017E-01 +1.106319E-01 +2.452869E-03 +2.692559E-01 +1.452928E-02 +4.129038E-01 +3.441066E-02 +1.357107E-02 +3.713729E-05 +3.302926E-02 +2.199783E-04 +5.677464E-01 +6.543807E-02 +1.908992E-02 +7.390190E-05 +4.646105E-02 +4.377492E-04 +1.651067E+00 +5.522453E-01 +1.956802E-02 +7.754346E-05 +4.762523E-02 +4.593308E-04 +4.583305E+00 +4.217589E+00 +7.135836E-03 +1.022408E-05 +1.765713E-02 +6.260005E-05 +2.988394E+00 +1.786305E+00 +4.141550E-02 +3.431964E-04 +1.151951E-01 +2.655125E-03 diff --git a/tests/regression_tests/random_ray_cell_temperature/test.py b/tests/regression_tests/random_ray_cell_temperature/test.py new file mode 100644 index 000000000..ef36d9238 --- /dev/null +++ b/tests/regression_tests/random_ray_cell_temperature/test.py @@ -0,0 +1,36 @@ +import os + +import openmc +from openmc.examples import random_ray_lattice, random_ray_three_region_cube +from openmc.utility_funcs import change_directory +import pytest + +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_basic(): + openmc.reset_auto_ids() + model = random_ray_lattice(second_temp=True) + # Set the temperature of the lower-left pin to 395 K -> cell instances [0, 8). + # All other pins are set to 295. + for id, cell in model.geometry.get_all_cells().items(): + if cell.fill.name == "UO2 fuel": + cell.temperature = [(100.0 * (i < 8) + 295.0) for i in range(24)] + + model.settings.temperature = { + 'method' : 'nearest', + 'tolerance' : 10.0, + 'range' : (200.0, 400.0) + } + + # Gold file was generated with manually scaled fuel cross sections. + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() From 8053111f2d0b120962fbdbcc81b5066e37dac874 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Tue, 10 Feb 2026 02:08:25 +0000 Subject: [PATCH 532/671] making use of endf-python package more (#3786) Co-authored-by: GuySten --- openmc/data/ace.py | 2 +- openmc/data/endf.py | 294 ++++---------------------------------------- 2 files changed, 24 insertions(+), 272 deletions(-) diff --git a/openmc/data/ace.py b/openmc/data/ace.py index 6ccb76c92..d3de6a192 100644 --- a/openmc/data/ace.py +++ b/openmc/data/ace.py @@ -24,7 +24,7 @@ import numpy as np import openmc.checkvalue as cv from openmc.mixin import EqualityMixin from .data import ATOMIC_SYMBOL, gnds_name, EV_PER_MEV, K_BOLTZMANN -from .endf import ENDF_FLOAT_RE +from endf.records import ENDF_FLOAT_RE def get_metadata(zaid, metastable_scheme='nndc'): diff --git a/openmc/data/endf.py b/openmc/data/endf.py index eca374469..bccd6b36b 100644 --- a/openmc/data/endf.py +++ b/openmc/data/endf.py @@ -10,11 +10,31 @@ import io from pathlib import PurePath import re -import numpy as np - from .data import gnds_name from .function import Tabulated1D -from endf.records import float_endf +from endf.records import ( + float_endf, + py_float_endf, + int_endf, + get_text_record, + get_cont_record, + get_head_record, + get_list_record, + get_tab1_record as _get_tab1_record, + get_tab2_record, + get_intg_record, +) + + +def get_tab1_record(file_obj): + """Return data from a TAB1 record in an ENDF-6 file. + + This wraps the endf package's get_tab1_record to return an + openmc.data.Tabulated1D (which has HDF5 support and is a Function1D) + instead of endf.Tabulated1D. + """ + params, tab = _get_tab1_record(file_obj) + return params, Tabulated1D(tab.x, tab.y, tab.breakpoints, tab.interpolation) _LIBRARY = {0: 'ENDF/B', 1: 'ENDF/A', 2: 'JEFF', 3: 'EFF', @@ -61,274 +81,6 @@ SUM_RULES = {1: [2, 3], 106: list(range(750, 800)), 107: list(range(800, 850))} -ENDF_FLOAT_RE = re.compile(r'([\s\-\+]?\d*\.\d+)([\+\-]) ?(\d+)') - - -def py_float_endf(s): - """Convert string of floating point number in ENDF to float. - - The ENDF-6 format uses an 'e-less' floating point number format, - e.g. -1.23481+10. Trying to convert using the float built-in won't work - because of the lack of an 'e'. This function allows such strings to be - converted while still allowing numbers that are not in exponential notation - to be converted as well. - - Parameters - ---------- - s : str - Floating-point number from an ENDF file - - Returns - ------- - float - The number - - """ - return float(ENDF_FLOAT_RE.sub(r'\1e\2\3', s)) - - -def int_endf(s): - """Convert string of integer number in ENDF to int. - - The ENDF-6 format technically allows integers to be represented by a field - of all blanks. This function acts like int(s) except when s is a string of - all whitespace, in which case zero is returned. - - Parameters - ---------- - s : str - Integer or spaces - - Returns - ------- - integer - The number or 0 - """ - return 0 if s.isspace() else int(s) - - -def get_text_record(file_obj): - """Return data from a TEXT record in an ENDF-6 file. - - Parameters - ---------- - file_obj : file-like object - ENDF-6 file to read from - - Returns - ------- - str - Text within the TEXT record - - """ - return file_obj.readline()[:66] - - -def get_cont_record(file_obj, skip_c=False): - """Return data from a CONT record in an ENDF-6 file. - - Parameters - ---------- - file_obj : file-like object - ENDF-6 file to read from - skip_c : bool - Determine whether to skip the first two quantities (C1, C2) of the CONT - record. - - Returns - ------- - tuple - The six items within the CONT record - - """ - line = file_obj.readline() - if skip_c: - C1 = None - C2 = None - else: - C1 = float_endf(line[:11]) - C2 = float_endf(line[11:22]) - L1 = int_endf(line[22:33]) - L2 = int_endf(line[33:44]) - N1 = int_endf(line[44:55]) - N2 = int_endf(line[55:66]) - return (C1, C2, L1, L2, N1, N2) - - -def get_head_record(file_obj): - """Return data from a HEAD record in an ENDF-6 file. - - Parameters - ---------- - file_obj : file-like object - ENDF-6 file to read from - - Returns - ------- - tuple - The six items within the HEAD record - - """ - line = file_obj.readline() - ZA = int(float_endf(line[:11])) - AWR = float_endf(line[11:22]) - L1 = int_endf(line[22:33]) - L2 = int_endf(line[33:44]) - N1 = int_endf(line[44:55]) - N2 = int_endf(line[55:66]) - return (ZA, AWR, L1, L2, N1, N2) - - -def get_list_record(file_obj): - """Return data from a LIST record in an ENDF-6 file. - - Parameters - ---------- - file_obj : file-like object - ENDF-6 file to read from - - Returns - ------- - list - The six items within the header - list - The values within the list - - """ - # determine how many items are in list - items = get_cont_record(file_obj) - NPL = items[4] - - # read items - b = [] - for i in range((NPL - 1)//6 + 1): - line = file_obj.readline() - n = min(6, NPL - 6*i) - for j in range(n): - b.append(float_endf(line[11*j:11*(j + 1)])) - - return (items, b) - - -def get_tab1_record(file_obj): - """Return data from a TAB1 record in an ENDF-6 file. - - Parameters - ---------- - file_obj : file-like object - ENDF-6 file to read from - - Returns - ------- - list - The six items within the header - openmc.data.Tabulated1D - The tabulated function - - """ - # Determine how many interpolation regions and total points there are - line = file_obj.readline() - C1 = float_endf(line[:11]) - C2 = float_endf(line[11:22]) - L1 = int_endf(line[22:33]) - L2 = int_endf(line[33:44]) - n_regions = int_endf(line[44:55]) - n_pairs = int_endf(line[55:66]) - params = [C1, C2, L1, L2] - - # Read the interpolation region data, namely NBT and INT - breakpoints = np.zeros(n_regions, dtype=int) - interpolation = np.zeros(n_regions, dtype=int) - m = 0 - for i in range((n_regions - 1)//3 + 1): - line = file_obj.readline() - to_read = min(3, n_regions - m) - for j in range(to_read): - breakpoints[m] = int_endf(line[0:11]) - interpolation[m] = int_endf(line[11:22]) - line = line[22:] - m += 1 - - # Read tabulated pairs x(n) and y(n) - x = np.zeros(n_pairs) - y = np.zeros(n_pairs) - m = 0 - for i in range((n_pairs - 1)//3 + 1): - line = file_obj.readline() - to_read = min(3, n_pairs - m) - for j in range(to_read): - x[m] = float_endf(line[:11]) - y[m] = float_endf(line[11:22]) - line = line[22:] - m += 1 - - return params, Tabulated1D(x, y, breakpoints, interpolation) - - -def get_tab2_record(file_obj): - # Determine how many interpolation regions and total points there are - params = get_cont_record(file_obj) - n_regions = params[4] - - # Read the interpolation region data, namely NBT and INT - breakpoints = np.zeros(n_regions, dtype=int) - interpolation = np.zeros(n_regions, dtype=int) - m = 0 - for i in range((n_regions - 1)//3 + 1): - line = file_obj.readline() - to_read = min(3, n_regions - m) - for j in range(to_read): - breakpoints[m] = int(line[0:11]) - interpolation[m] = int(line[11:22]) - line = line[22:] - m += 1 - - return params, Tabulated2D(breakpoints, interpolation) - - -def get_intg_record(file_obj): - """ - Return data from an INTG record in an ENDF-6 file. Used to store the - covariance matrix in a compact format. - - Parameters - ---------- - file_obj : file-like object - ENDF-6 file to read from - - Returns - ------- - numpy.ndarray - The correlation matrix described in the INTG record - """ - # determine how many items are in list and NDIGIT - items = get_cont_record(file_obj) - ndigit = items[2] - npar = items[3] # Number of parameters - nlines = items[4] # Lines to read - NROW_RULES = {2: 18, 3: 12, 4: 11, 5: 9, 6: 8} - nrow = NROW_RULES[ndigit] - - # read lines and build correlation matrix - corr = np.identity(npar) - for i in range(nlines): - line = file_obj.readline() - ii = int_endf(line[:5]) - 1 # -1 to account for 0 indexing - jj = int_endf(line[5:10]) - 1 - factor = 10**ndigit - for j in range(nrow): - if jj+j >= ii: - break - element = int_endf(line[11+(ndigit+1)*j:11+(ndigit+1)*(j+1)]) - if element > 0: - corr[ii, jj] = (element+0.5)/factor - elif element < 0: - corr[ii, jj] = (element-0.5)/factor - - # Symmetrize the correlation matrix - corr = corr + corr.T - np.diag(corr.diagonal()) - return corr - def get_evaluations(filename): """Return a list of all evaluations within an ENDF file. From 3f20a5e22829c856a5b5bc3cda25a485650d1900 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Tue, 10 Feb 2026 12:41:04 +0000 Subject: [PATCH 533/671] Use several data variables from endf package (#3787) --- openmc/data/data.py | 73 ++------------------------------------------- 1 file changed, 3 insertions(+), 70 deletions(-) diff --git a/openmc/data/data.py b/openmc/data/data.py index 5ecadd37b..03d67d7a9 100644 --- a/openmc/data/data.py +++ b/openmc/data/data.py @@ -6,6 +6,9 @@ from pathlib import Path from math import sqrt, log from warnings import warn +from endf.data import (ATOMIC_NUMBER, ATOMIC_SYMBOL, ELEMENT_SYMBOL, + EV_PER_MEV, K_BOLTZMANN) + # Isotopic abundances from Meija J, Coplen T B, et al, "Isotopic compositions # of the elements 2013 (IUPAC Technical Report)", Pure. Appl. Chem. 88 (3), # pp. 293-306 (2013). The "representative isotopic abundance" values from @@ -112,72 +115,6 @@ NATURAL_ABUNDANCE = { 'U238': 0.992742 } -# Dictionary to give element symbols from IUPAC names -# (and some common mispellings) -ELEMENT_SYMBOL = {'neutron': 'n', 'hydrogen': 'H', 'helium': 'He', - 'lithium': 'Li', 'beryllium': 'Be', 'boron': 'B', - 'carbon': 'C', 'nitrogen': 'N', 'oxygen': 'O', 'fluorine': 'F', - 'neon': 'Ne', 'sodium': 'Na', 'magnesium': 'Mg', - 'aluminium': 'Al', 'aluminum': 'Al', 'silicon': 'Si', - 'phosphorus': 'P', 'sulfur': 'S', 'sulphur': 'S', - 'chlorine': 'Cl', 'argon': 'Ar', 'potassium': 'K', - 'calcium': 'Ca', 'scandium': 'Sc', 'titanium': 'Ti', - 'vanadium': 'V', 'chromium': 'Cr', 'manganese': 'Mn', - 'iron': 'Fe', 'cobalt': 'Co', 'nickel': 'Ni', 'copper': 'Cu', - 'zinc': 'Zn', 'gallium': 'Ga', 'germanium': 'Ge', - 'arsenic': 'As', 'selenium': 'Se', 'bromine': 'Br', - 'krypton': 'Kr', 'rubidium': 'Rb', 'strontium': 'Sr', - 'yttrium': 'Y', 'zirconium': 'Zr', 'niobium': 'Nb', - 'molybdenum': 'Mo', 'technetium': 'Tc', 'ruthenium': 'Ru', - 'rhodium': 'Rh', 'palladium': 'Pd', 'silver': 'Ag', - 'cadmium': 'Cd', 'indium': 'In', 'tin': 'Sn', 'antimony': 'Sb', - 'tellurium': 'Te', 'iodine': 'I', 'xenon': 'Xe', - 'caesium': 'Cs', 'cesium': 'Cs', 'barium': 'Ba', - 'lanthanum': 'La', 'cerium': 'Ce', 'praseodymium': 'Pr', - 'neodymium': 'Nd', 'promethium': 'Pm', 'samarium': 'Sm', - 'europium': 'Eu', 'gadolinium': 'Gd', 'terbium': 'Tb', - 'dysprosium': 'Dy', 'holmium': 'Ho', 'erbium': 'Er', - 'thulium': 'Tm', 'ytterbium': 'Yb', 'lutetium': 'Lu', - 'hafnium': 'Hf', 'tantalum': 'Ta', 'tungsten': 'W', - 'wolfram': 'W', 'rhenium': 'Re', 'osmium': 'Os', - 'iridium': 'Ir', 'platinum': 'Pt', 'gold': 'Au', - 'mercury': 'Hg', 'thallium': 'Tl', 'lead': 'Pb', - 'bismuth': 'Bi', 'polonium': 'Po', 'astatine': 'At', - 'radon': 'Rn', 'francium': 'Fr', 'radium': 'Ra', - 'actinium': 'Ac', 'thorium': 'Th', 'protactinium': 'Pa', - 'uranium': 'U', 'neptunium': 'Np', 'plutonium': 'Pu', - 'americium': 'Am', 'curium': 'Cm', 'berkelium': 'Bk', - 'californium': 'Cf', 'einsteinium': 'Es', 'fermium': 'Fm', - 'mendelevium': 'Md', 'nobelium': 'No', 'lawrencium': 'Lr', - 'rutherfordium': 'Rf', 'dubnium': 'Db', 'seaborgium': 'Sg', - 'bohrium': 'Bh', 'hassium': 'Hs', 'meitnerium': 'Mt', - 'darmstadtium': 'Ds', 'roentgenium': 'Rg', 'copernicium': 'Cn', - 'nihonium': 'Nh', 'flerovium': 'Fl', 'moscovium': 'Mc', - 'livermorium': 'Lv', 'tennessine': 'Ts', 'oganesson': 'Og'} - -ATOMIC_SYMBOL = {0: 'n', 1: 'H', 2: 'He', 3: 'Li', 4: 'Be', 5: 'B', 6: 'C', - 7: 'N', 8: 'O', 9: 'F', 10: 'Ne', 11: 'Na', 12: 'Mg', 13: 'Al', - 14: 'Si', 15: 'P', 16: 'S', 17: 'Cl', 18: 'Ar', 19: 'K', - 20: 'Ca', 21: 'Sc', 22: 'Ti', 23: 'V', 24: 'Cr', 25: 'Mn', - 26: 'Fe', 27: 'Co', 28: 'Ni', 29: 'Cu', 30: 'Zn', 31: 'Ga', - 32: 'Ge', 33: 'As', 34: 'Se', 35: 'Br', 36: 'Kr', 37: 'Rb', - 38: 'Sr', 39: 'Y', 40: 'Zr', 41: 'Nb', 42: 'Mo', 43: 'Tc', - 44: 'Ru', 45: 'Rh', 46: 'Pd', 47: 'Ag', 48: 'Cd', 49: 'In', - 50: 'Sn', 51: 'Sb', 52: 'Te', 53: 'I', 54: 'Xe', 55: 'Cs', - 56: 'Ba', 57: 'La', 58: 'Ce', 59: 'Pr', 60: 'Nd', 61: 'Pm', - 62: 'Sm', 63: 'Eu', 64: 'Gd', 65: 'Tb', 66: 'Dy', 67: 'Ho', - 68: 'Er', 69: 'Tm', 70: 'Yb', 71: 'Lu', 72: 'Hf', 73: 'Ta', - 74: 'W', 75: 'Re', 76: 'Os', 77: 'Ir', 78: 'Pt', 79: 'Au', - 80: 'Hg', 81: 'Tl', 82: 'Pb', 83: 'Bi', 84: 'Po', 85: 'At', - 86: 'Rn', 87: 'Fr', 88: 'Ra', 89: 'Ac', 90: 'Th', 91: 'Pa', - 92: 'U', 93: 'Np', 94: 'Pu', 95: 'Am', 96: 'Cm', 97: 'Bk', - 98: 'Cf', 99: 'Es', 100: 'Fm', 101: 'Md', 102: 'No', - 103: 'Lr', 104: 'Rf', 105: 'Db', 106: 'Sg', 107: 'Bh', - 108: 'Hs', 109: 'Mt', 110: 'Ds', 111: 'Rg', 112: 'Cn', - 113: 'Nh', 114: 'Fl', 115: 'Mc', 116: 'Lv', 117: 'Ts', - 118: 'Og'} -ATOMIC_NUMBER = {value: key for key, value in ATOMIC_SYMBOL.items()} - DADZ = { '(n,2nd)': (-3, -1), '(n,2n)': (-1, 0), @@ -268,11 +205,7 @@ DADZ = { # Values here are from the Committee on Data for Science and Technology # (CODATA) 2018 recommendation (https://physics.nist.gov/cuu/Constants/). -# The value of the Boltzman constant in units of eV / K -K_BOLTZMANN = 8.617333262e-5 - # Unit conversions -EV_PER_MEV = 1.0e6 JOULE_PER_EV = 1.602176634e-19 # Avogadro's constant From 44da7022b8a4371ba68da364596b5591d0e20d29 Mon Sep 17 00:00:00 2001 From: Matthew Feickert Date: Wed, 11 Feb 2026 08:21:13 -0700 Subject: [PATCH 534/671] FIX: Remove setuptools from run dependencies (#3794) --- AGENTS.md | 2 +- pyproject.toml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 44962d1ad..575a693c7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -236,7 +236,7 @@ When modifying C++ public APIs, update corresponding ctypes signatures in `openm - **Docstrings**: numpydoc format for all public functions/methods - **Type hints**: Use sparingly, primarily for complex signatures - **Path handling**: Use `pathlib.Path` for filesystem operations, accept `str | os.PathLike` in function arguments -- **Dependencies**: Core dependencies only (numpy, scipy, h5py, pandas, matplotlib, lxml, ipython, uncertainties, setuptools, endf). Other packages must be optional +- **Dependencies**: Core dependencies only (numpy, scipy, h5py, pandas, matplotlib, lxml, ipython, uncertainties, endf). Other packages must be optional - **Python version**: Minimum 3.11 (as of Nov 2025) ### ID Management Pattern (Python) diff --git a/pyproject.toml b/pyproject.toml index 2d67e8340..21342155a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,6 @@ dependencies = [ "pandas", "lxml", "uncertainties", - "setuptools", "endf", ] From 360ec24b41634795917c8cdcc9fe75687fb9264f Mon Sep 17 00:00:00 2001 From: azim-givron Date: Wed, 11 Feb 2026 17:00:18 +0100 Subject: [PATCH 535/671] Implement vector fitting to replace external `vectfit` package (#3493) Co-authored-by: azim_givron Co-authored-by: Paul Romano Co-authored-by: GuySten Co-authored-by: GuySten <62616591+GuySten@users.noreply.github.com> --- .github/workflows/ci.yml | 14 +- openmc/data/__init__.py | 1 + openmc/data/multipole.py | 57 +- openmc/data/vectfit.py | 811 ++++++++++++++++++++++++ tests/unit_tests/test_data_multipole.py | 36 +- tests/unit_tests/test_vectfit.py | 224 +++++++ tools/ci/gha-install-vectfit.sh | 46 -- tools/ci/gha-install.sh | 5 - 8 files changed, 1102 insertions(+), 92 deletions(-) create mode 100644 openmc/data/vectfit.py create mode 100644 tests/unit_tests/test_vectfit.py delete mode 100755 tools/ci/gha-install-vectfit.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 580d409c5..b5b7f3a87 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,6 @@ jobs: dagmc: [n] libmesh: [n] event: [n] - vectfit: [n] include: - python-version: "3.12" @@ -74,14 +73,9 @@ jobs: python-version: "3.11" omp: y mpi: n - - vectfit: y - python-version: "3.11" - omp: n - mpi: y name: "Python ${{ matrix.python-version }} (omp=${{ matrix.omp }}, mpi=${{ matrix.mpi }}, dagmc=${{ matrix.dagmc }}, - libmesh=${{ matrix.libmesh }}, event=${{ matrix.event }} - vectfit=${{ matrix.vectfit }})" + libmesh=${{ matrix.libmesh }}, event=${{ matrix.event }}" env: MPI: ${{ matrix.mpi }} @@ -89,7 +83,6 @@ jobs: OMP: ${{ matrix.omp }} DAGMC: ${{ matrix.dagmc }} EVENT: ${{ matrix.event }} - VECTFIT: ${{ matrix.vectfit }} LIBMESH: ${{ matrix.libmesh }} NPY_DISABLE_CPU_FEATURES: "AVX512F AVX512_SKX" OPENBLAS_NUM_THREADS: 1 @@ -150,11 +143,6 @@ jobs: sudo update-alternatives --set mpirun /usr/bin/mpirun.mpich sudo update-alternatives --set mpi-x86_64-linux-gnu /usr/include/x86_64-linux-gnu/mpich - - name: Optional apt dependencies for vectfit - shell: bash - if: ${{ matrix.vectfit == 'y' }} - run: sudo apt install -y libblas-dev liblapack-dev - - name: install shell: bash run: | diff --git a/openmc/data/__init__.py b/openmc/data/__init__.py index c2d35565a..a45d026a0 100644 --- a/openmc/data/__init__.py +++ b/openmc/data/__init__.py @@ -33,5 +33,6 @@ from .resonance_covariance import * from .multipole import * from .grid import * from .function import * +from .vectfit import * from .effective_dose.dose import dose_coefficients diff --git a/openmc/data/multipole.py b/openmc/data/multipole.py index dd14e0d19..d843b5971 100644 --- a/openmc/data/multipole.py +++ b/openmc/data/multipole.py @@ -15,7 +15,7 @@ from . import WMP_VERSION, WMP_VERSION_MAJOR from .data import K_BOLTZMANN from .neutron import IncidentNeutron from .resonance import ResonanceRange - +from .vectfit import vectfit, evaluate # Constants that determine which value to access _MP_EA = 0 # Pole @@ -174,10 +174,6 @@ def _vectfit_xs(energy, ce_xs, mts, rtol=1e-3, atol=1e-5, orders=None, (poles, residues) """ - - # import vectfit package: https://github.com/liangjg/vectfit - import vectfit as vf - ne = energy.size nmt = len(mts) if ce_xs.shape != (nmt, ne): @@ -194,8 +190,8 @@ def _vectfit_xs(energy, ce_xs, mts, rtol=1e-3, atol=1e-5, orders=None, test_xs_ref[i] = np.interp(test_energy, energy, ce_xs[i]) if log: - print(f" energy: {energy[0]:.3e} to {energy[-1]:.3e} eV ({ne} points)") - print(f" error tolerance: rtol={rtol}, atol={atol}") + print(f"\tenergy: {energy[0]:.3e} to {energy[-1]:.3e} eV ({ne} points)") + print(f"\terror tolerance: rtol={rtol}, atol={atol}") # transform xs (sigma) and energy (E) to f (sigma*E) and s (sqrt(E)) to be # compatible with the multipole representation @@ -251,7 +247,7 @@ def _vectfit_xs(energy, ce_xs, mts, rtol=1e-3, atol=1e-5, orders=None, print(f"VF iteration {i_vf + 1}/{n_vf_iter}") # call vf - poles, residues, cf, f_fit, rms = vf.vectfit(f, s, poles, weight) + poles, residues, *_ = vectfit(f, s, poles, weight) # convert real pole to conjugate pairs n_real_poles = 0 @@ -268,11 +264,11 @@ def _vectfit_xs(energy, ce_xs, mts, rtol=1e-3, atol=1e-5, orders=None, if n_real_poles > 0: if log >= DETAILED_LOGGING: print(f" # real poles: {n_real_poles}") - new_poles, residues, cf, f_fit, rms = \ - vf.vectfit(f, s, new_poles, weight, skip_pole=True) + new_poles, residues, *_ = \ + vectfit(f, s, new_poles, weight, skip_pole_update=True) # assess the result on test grid - test_xs = vf.evaluate(test_s, new_poles, residues) / test_energy + test_xs = evaluate(test_s, new_poles, residues) / test_energy abserr = np.abs(test_xs - test_xs_ref) with np.errstate(invalid='ignore', divide='ignore'): relerr = abserr / test_xs_ref @@ -388,9 +384,9 @@ def _vectfit_xs(energy, ce_xs, mts, rtol=1e-3, atol=1e-5, orders=None, return (mp_poles, mp_residues) - def vectfit_nuclide(endf_file, njoy_error=5e-4, vf_pieces=None, - log=False, path_out=None, mp_filename=None, **kwargs): + log=False, path_out=None, mp_filename=None, + **kwargs): r"""Generate multipole data for a nuclide from ENDF. Parameters @@ -571,10 +567,6 @@ def _windowing(mp_data, n_cf, rtol=1e-3, atol=1e-5, n_win=None, spacing=None, format. """ - - # import vectfit package: https://github.com/liangjg/vectfit - import vectfit as vf - # unpack multipole data name = mp_data["name"] awr = mp_data["AWR"] @@ -645,7 +637,7 @@ def _windowing(mp_data, n_cf, rtol=1e-3, atol=1e-5, n_win=None, spacing=None, # reference xs from multipole form, note the residue terms in the # multipole and vector fitting representations differ by a 1j - xs_ref = vf.evaluate(energy_sqrt, poles, residues*1j) / energy + xs_ref = evaluate(energy_sqrt, poles, residues*1j) / energy # curve fit matrix matrix = np.vstack([energy**(0.5*i - 1) for i in range(n_cf + 1)]).T @@ -659,7 +651,7 @@ def _windowing(mp_data, n_cf, rtol=1e-3, atol=1e-5, n_win=None, spacing=None, # calculate the cross sections contributed by the windowed poles if rp > lp: - xs_wp = vf.evaluate(energy_sqrt, poles[lp:rp], + xs_wp = evaluate(energy_sqrt, poles[lp:rp], residues[:, lp:rp]*1j) / energy else: xs_wp = np.zeros_like(xs_ref) @@ -1054,7 +1046,15 @@ class WindowedMultipole(EqualityMixin): return cls.from_multipole(mp_data, **wmp_options) @classmethod - def from_multipole(cls, mp_data, search=None, log=False, **kwargs): + def from_multipole( + cls, + mp_data, + search=None, + log=False, + search_n_win=20, + search_cf_orders=None, + **kwargs, + ): """Generate windowed multipole neutron data from multipole data. Parameters @@ -1066,8 +1066,14 @@ class WindowedMultipole(EqualityMixin): Defaults to True if no windowing parameters are specified. log : bool or int, optional Whether to print running logs (use int for verbosity control) + search_n_win : int, optional + Number of window sizes to consider in the search grid when + ``search`` is True. + search_cf_orders : iterable of int, optional + Curve-fit orders to consider in the search grid when ``search`` is + True. Defaults to integers from 10 down to 2. **kwargs - Keyword arguments passed to :func:`openmc.data.multipole._windowing` + Keyword arguments passed to :func:`openmc.data.multipole._windowing`. Returns ------- @@ -1098,12 +1104,17 @@ class WindowedMultipole(EqualityMixin): # search optimal WMP from a range of window sizes and CF orders if log: print("Start searching ...") + if search_cf_orders is None: + search_cf_orders = range(10, 1, -1) + n_poles = sum([p.size for p in mp_data["poles"]]) n_win_min = max(5, n_poles // 20) n_win_max = 2000 if n_poles < 2000 else 8000 best_wmp = best_metric = None - for n_w in np.unique(np.linspace(n_win_min, n_win_max, 20, dtype=int)): - for n_cf in range(10, 1, -1): + for n_w in np.unique( + np.linspace(n_win_min, n_win_max, search_n_win, dtype=int) + ): + for n_cf in search_cf_orders: if log: print(f"Testing N_win={n_w} N_cf={n_cf}") diff --git a/openmc/data/vectfit.py b/openmc/data/vectfit.py new file mode 100644 index 000000000..c5a783a3c --- /dev/null +++ b/openmc/data/vectfit.py @@ -0,0 +1,811 @@ +""" +Fast Relaxed Vector Fitting function + +Approximate f(s) with a rational function: + f(s)=R*(s*I-A)^(-1) + Polynomials*s +where f(s) is a vector of elements. + +When f(s) is a vector, all elements become fitted with a common pole set. The +identification is done using the pole relocating method known as Vector Fitting +[1] with relaxed non-triviality constraint for faster convergence and smaller +fitting errors [2], and utilization of matrix structure for fast solution of the +pole identifion step [3]. + +[1] B. Gustavsen and A. Semlyen, "Rational approximation of frequency + domain responses by Vector Fitting", IEEE Trans. Power Delivery, vol. 14, + no. 3, pp. 1052-1061, July 1999. +[2] B. Gustavsen, "Improving the pole relocating properties of vector + fitting", IEEE Trans. Power Delivery, vol. 21, no. 3, pp. 1587-1592, July + 2006. +[3] D. Deschrijver, M. Mrozowski, T. Dhaene, and D. De Zutter, + "Macromodeling of Multiport Systems Using a Fast Implementation of the + Vector Fitting Method", IEEE Microwave and Wireless Components Letters, vol. + 18, no. 6, pp. 383-385, June 2008. + +All credit goes to: + - Bjorn Gustavsen for his MATLAB implementation. + (http://www.sintef.no/Projectweb/VECTFIT/) + - Jingang Liang for his C++ implementation. + (https://github.com/mit-crpg/vectfit.git) + +""" + +from typing import Tuple + +import numpy as np +from scipy.linalg import eigvals, lstsq, norm, qr + + +def wlstsq(a, b): + """Apply least-squares solve with column normalization. + + Notes + ----- + This routine rescales columns of `a` to improve conditioning. Columns with + zero norm are left unscaled to avoid divide-by-zero warnings. + """ + col_norm = np.linalg.norm(a, axis=0) + scale = np.ones_like(col_norm, dtype=float) + nonzero = col_norm > 0.0 + scale[nonzero] = 1.0 / col_norm[nonzero] + scale = np.nan_to_num(scale, nan=1.0, posinf=1.0, neginf=1.0) + + sol = lstsq(a * scale, b) + return (sol[0] * scale, sol[1:]) + + +def evaluate( + eval_points: np.ndarray, + pole_values: np.ndarray, + residue_matrix: np.ndarray, + poly_coefficients: np.ndarray | None = None, +) -> np.ndarray: + """Evaluate the rational function approximation: + f(s) ≈ sum(residue / (s - pole)) + sum(poly_coefficients * s^j) + + Parameters + ---------- + eval_points : np.ndarray + 1D array of real scalar frequency values (s). + pole_values : np.ndarray + 1D array of complex poles. + residue_matrix : np.ndarray + 2D or 1D array of complex residues (shape: [num_vectors, num_poles] or [num_poles]). + poly_coefficients : np.ndarray, optional + 2D or 1D array of real polynomial coefficients (shape: [num_vectors, num_polys]). + + Returns + ------- + np.ndarray + 2D array of evaluated real function values (shape: [num_vectors, num_samples]). + + Raises + ------ + ValueError + If input arrays have incompatible shapes. + """ + eval_points = np.asarray(eval_points) + pole_values = np.asarray(pole_values) + residue_matrix = np.asarray(residue_matrix) + + if eval_points.ndim != 1: + raise ValueError("eval_points must be a 1D array") + if pole_values.ndim != 1: + raise ValueError("pole_values must be a 1D array") + + if residue_matrix.ndim == 1: + residue_matrix = residue_matrix.reshape((1, -1)) + num_vectors, _ = residue_matrix.shape + num_samples = len(eval_points) + + if poly_coefficients is not None and isinstance(poly_coefficients, list): + poly_coefficients = np.array(poly_coefficients) + if poly_coefficients is None or poly_coefficients.size == 0: + poly_coefficients = np.zeros((num_vectors, 0)) + else: + poly_coefficients = np.asarray(poly_coefficients) + if poly_coefficients.ndim == 1: + poly_coefficients = poly_coefficients.reshape((1, -1)) + elif poly_coefficients.shape[0] != num_vectors: + raise ValueError("Mismatch in residues and poly_coefficients shapes") + + num_coeffs = poly_coefficients.shape[1] + result = np.zeros((num_vectors, num_samples)) + + # term: sum over poles of (residues / (eval_points - poles)) + denominator = ( + eval_points[np.newaxis, :] - pole_values[:, np.newaxis] + ) # shape: (num_poles, num_eval) + pole_terms = residue_matrix @ (1.0 / denominator) # shape: (num_vectors, num_eval) + result = np.real(pole_terms) + + # polynomial part: sum over poly_idx of (coeff * eval_points**poly_idx) + if num_coeffs > 0: + powers = ( + eval_points[np.newaxis, :] ** np.arange(num_coeffs)[:, np.newaxis] + ) # shape: (num_coeffs, num_eval) + result += poly_coefficients @ powers # shape: (num_vectors, num_eval) + + return result + + +def vectfit( + response_matrix: np.ndarray, + eval_points: np.ndarray, + initial_poles: np.ndarray, + weights: np.ndarray, + n_polys: int = 0, + skip_pole_update: bool = False, + skip_residue_update: bool = False, +) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, float]: + """Perform vector fitting using the Fast Relaxed Vector Fitting algorithm. + + Parameters + ---------- + response_matrix : np.ndarray + Complex matrix of frequency responses (shape: [num_vectors, num_samples]). + eval_points : np.ndarray + Real frequency samples (s), shape (num_samples,). + initial_poles : np.ndarray + Initial guess for poles (complex), shape (num_poles,). + weights : np.ndarray + Weighting matrix for fitting (same shape as response_matrix). + n_polys : int, optional + Number of real polynomial terms to include. + skip_pole_update : bool, optional + Whether to skip pole relocation step. + skip_residue_update : bool, optional + Whether to skip residue fitting step. + + Returns + ------- + Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, float] + - Updated poles (np.ndarray) + - Residues (np.ndarray) + - Polynomial coefficients (np.ndarray) + - Fitted response matrix (np.ndarray) + - Root-mean-square error (float) + """ + tol_low = 1e-18 + tol_high = 1e18 + response_matrix = np.asarray(response_matrix) + eval_points = np.asarray(eval_points) + initial_poles = np.asarray(initial_poles) + weights = np.asarray(weights) + + num_vectors, num_samples = response_matrix.shape + num_poles = len(initial_poles) + + if n_polys < 0 or n_polys > 11: + raise ValueError("n_polys must be in [0, 11]") + + residue_matrix = np.zeros((num_vectors, num_poles), dtype=np.complex128) + poly_coefficients = np.zeros((num_vectors, n_polys)) + fit_result = np.zeros_like(response_matrix) + rms_error = 0.0 + + if num_poles == 0 and n_polys == 0: + rms_error = norm(response_matrix) / np.sqrt(num_vectors * num_samples) + return initial_poles, residue_matrix, poly_coefficients, fit_result, rms_error + + if not skip_pole_update and num_poles > 0: + updated_poles = identify_poles( + num_poles, + num_samples, + n_polys, + initial_poles, + eval_points, + tol_high, + weights, + response_matrix, + num_vectors, + tol_low, + ) + else: + updated_poles = initial_poles + + if not skip_residue_update: + fit_result, rms_error = identify_residues( + num_poles, + updated_poles, + num_samples, + n_polys, + eval_points, + num_vectors, + weights, + response_matrix, + poly_coefficients, + residue_matrix, + ) + + return updated_poles, residue_matrix, poly_coefficients, fit_result, rms_error + + +def compute_dk_matrix( + dk_matrix: np.ndarray, + eval_points: np.ndarray, + poles: np.ndarray, + conj_index: np.ndarray, + num_poles: int, + num_polys: int, + tol_high: float = None, +): + """Compute the dk_matrix used in windowed multipole evaluations. + + Parameters + ---------- + dk_matrix : ndarray of shape (len(eval_points), M) + The full matrix used in least-squares fitting or evaluation. + eval_points : ndarray of shape (N,) + Energy points at which to evaluate. + poles : ndarray of shape (num_poles,) + Complex poles used in the resonance model. + conj_index : ndarray of shape (num_poles,) + Index array indicating pole conjugacy behavior: 0 (normal), 1 (add conjugate), 2 (imaginary part). + num_poles : int + Number of complex poles. + num_polys : int + Number of polynomial terms (including constant term). + tol_high : float + Replacement value for infinities. + """ + # Broadcast shapes + eval_points_col = eval_points[:, np.newaxis] + poles_row = poles[np.newaxis, :] + + # Compute base terms + term1 = 1.0 / (eval_points_col - poles_row) + term2 = 1.0 / (eval_points_col - np.conj(poles_row)) + term3 = 1j / (eval_points_col - np.conj(poles_row)) - 1j / ( + eval_points_col - poles_row + ) + + # Masks for different conjugacy types + mask0 = conj_index == 0 + mask1 = conj_index == 1 + mask2 = conj_index == 2 + + # Fill dk_matrix with pole terms + dk_matrix[:, :num_poles][:, mask0] = term1[:, mask0] + dk_matrix[:, :num_poles][:, mask1] = term1[:, mask1] + term2[:, mask1] + dk_matrix[:, :num_poles][:, mask2] = term3[:, mask2] + + # Replace infinities with high tolerance value + if tol_high is not None: + inf_mask = np.isinf(dk_matrix) + dk_matrix[inf_mask] = tol_high + 0j + + # Add polynomial basis (Chebyshev-like, just powers here) + powers = np.arange(num_polys) + dk_matrix[:, num_poles : num_poles + num_polys] = eval_points_col**powers + 0j + return dk_matrix + + +def row_block_matrix( + dk_matrix: np.ndarray, + weights: np.ndarray, + response_matrix: np.ndarray, + vec_idx: int, + num_poles: int, + num_polys: int, +) -> np.ndarray: + """ + Construct a single matrix row block for the given vector index. + + Parameters + ---------- + dk_matrix : ndarray of shape (num_samples, num_poles + num_polys) + Basis function evaluations at each sample point. + weights : ndarray of shape (num_vectors, num_samples) + Sample weights for each vector. + response_matrix : ndarray of shape (num_vectors, num_samples) + Response values at each sample point. + vec_idx : int + Index of the vector to construct the A1 block for. + num_poles : int + Number of poles used in the model. + num_polys : int + Number of polynomial basis terms. + + Returns + ------- + A : ndarray of shape (num_samples, num_poles + num_polys + num_poles + 1) + Weighted and assembled matrix block for the current vector. + """ + num_samples = dk_matrix.shape[0] + A = np.zeros( + (num_samples, num_poles + num_polys + num_poles + 1), dtype=np.complex128 + ) + + # Weighted basis terms + A[:, : num_poles + num_polys] = ( + weights[vec_idx][:, np.newaxis] * dk_matrix[:, : num_poles + num_polys] + ) + + # Weighted response terms (includes poles + 1) + A[:, num_poles + num_polys : num_poles + num_polys + num_poles + 1] = ( + -weights[vec_idx][:, np.newaxis] + * dk_matrix[:, : num_poles + 1] + * response_matrix[vec_idx][:, np.newaxis] + ) + + return A + + +def process_constrained_block( + vec_idx: int, + dk_matrix: np.ndarray, + weights: np.ndarray, + response_matrix: np.ndarray, + num_samples: int, + num_poles: int, + num_polys: int, + scale_factor: float, + num_vectors: int, +) -> Tuple[int, np.ndarray, np.ndarray]: + """ + Construct a constrained least-squares system block for the given vector index. + + This function computes the A matrix using weighted evaluations of the basis functions + and response terms. It appends a constraint row to enforce physical properties + (e.g., normalization) **only for the final vector index**. The full matrix A is + decomposed via QR, and the resulting triangular block is returned. + + This routine is intended for use in the main vector fitting loop when the denominator + is well-conditioned but requires an additional constraint row for physical consistency. + + Parameters + ---------- + vec_idx : int + Index of the vector to process. + dk_matrix : ndarray of shape (num_samples, num_poles + num_polys) + Evaluated basis functions at sample points. + weights : ndarray of shape (num_vectors, num_samples) + Weight matrix per vector. + response_matrix : ndarray of shape (num_vectors, num_samples) + Response function values for each vector. + num_samples : int + Number of sample points. + num_poles : int + Number of poles in the model. + num_polys : int + Number of polynomial terms in the model. + scale_factor : float + Scaling factor applied to the final constraint row. + num_vectors : int + Total number of vectors to process. + + Returns + ------- + vec_idx : int + Index of the processed vector. + lhs_block : ndarray of shape (num_poles + 1, num_poles + 1) + Triangular matrix block from QR decomposition. + rhs_block : ndarray of shape (num_poles + 1,) or None + Right-hand side vector block (only returned for final vec_idx), else None. + """ + A1 = row_block_matrix( + dk_matrix, weights, response_matrix, vec_idx, num_poles, num_polys + ) + A = np.zeros((2 * num_samples + 1, num_poles + num_polys + num_poles + 1)) + A[:num_samples] = A1.real + A[num_samples : 2 * num_samples] = A1.imag + # Handle final row only if vec_idx is last + if vec_idx == num_vectors - 1: + A[ + 2 * num_samples, + num_poles + num_polys : num_poles + num_polys + num_poles + 1, + ] = scale_factor * np.real(dk_matrix[:, : num_poles + 1].sum(axis=0)) + + Q, R = qr(A, mode="economic") + + lhs_block = R[ + num_poles + num_polys : num_poles + num_polys + num_poles + 1, + num_poles + num_polys : num_poles + num_polys + num_poles + 1, + ] + + if vec_idx == num_vectors - 1: + rhs_block = ( + num_samples + * scale_factor + * Q[-1, num_poles + num_polys : num_poles + num_polys + num_poles + 1] + ) + else: + rhs_block = np.zeros_like( + Q[-1, num_poles + num_polys : num_poles + num_polys + num_poles + 1] + ) + + return vec_idx, lhs_block, rhs_block + + +def process_unconstrained_block( + vec_idx: int, + dk_matrix: np.ndarray, + weights: np.ndarray, + response_matrix: np.ndarray, + denom: float, + num_poles: int, + num_polys: int, +) -> Tuple[int, np.ndarray, np.ndarray]: + """ + Construct an unconstrained least-squares system block for the given vector index. + + This function is used when the fitting denominator becomes ill-conditioned + (too small or too large), and the original constrained system is replaced by + an alternative regularized least-squares problem. The A matrix is built by stacking + the real and imaginary parts of the basis evaluations, and the RHS vector b is + scaled by `denom`. + + A standard QR decomposition is used to extract the square block of the system, + which can be solved independently from the constrained system. + + Parameters + ---------- + vec_idx : int + Index of the vector to process. + dk_matrix : ndarray of shape (num_samples, num_poles + num_polys) + Evaluated basis functions at sample points. + weights : ndarray of shape (num_vectors, num_samples) + Weight matrix per vector. + response_matrix : ndarray of shape (num_vectors, num_samples) + Response function values for each vector. + denom : float + Scaling factor applied to the right-hand side vector b. + num_poles : int + Number of poles in the model. + num_polys : int + Number of polynomial terms in the model. + + Returns + ------- + vec_idx : int + Index of the processed vector. + lhs_block : ndarray of shape (num_poles, num_poles) + Triangular matrix block from QR decomposition. + rhs_block : ndarray of shape (num_poles,) + Right-hand side vector block for this vector. + """ + A1 = row_block_matrix( + dk_matrix, weights, response_matrix, vec_idx, num_poles, num_polys + ) + A = np.vstack((A1.real, A1.imag)) + + b1 = denom * weights[vec_idx] * response_matrix[vec_idx] + b = np.concatenate((b1.real, b1.imag)) + + Q, R = qr(A, mode="economic") + + lhs_block = R[ + num_poles + num_polys : num_poles + num_polys + num_poles, + num_poles + num_polys : num_poles + num_polys + num_poles, + ] + rhs_block = Q[:, num_poles + num_polys : num_poles + num_polys + num_poles].T @ b + return vec_idx, lhs_block, rhs_block + + +def identify_poles( + num_poles: int, + num_samples: int, + num_polys: int, + poles: np.ndarray, + eval_points: np.ndarray, + tol_high: float, + weights: np.ndarray, + response_matrix: np.ndarray, + num_vectors: int, + tol_low: float, +) -> np.ndarray: + """ + Internal routine to update poles via relaxed vector fitting. + + Parameters + ---------- + num_poles : int + Number of poles. + num_samples : int + Number of frequency samples. + num_polys : int + Number of polynomial terms. + poles : np.ndarray + Initial poles (complex), shape (num_poles,). + eval_points : np.ndarray + Real frequency values, shape (num_samples,). + tol_high : float + Upper tolerance threshold for denominator. + weights : np.ndarray + Weighting matrix, shape (num_vectors, num_samples). + response_matrix : np.ndarray + Complex frequency responses, shape (num_vectors, num_samples). + num_vectors : int + Number of response vectors. + tol_low : float + Lower tolerance threshold for denominator. + + Returns + ------- + np.ndarray + Updated poles as eigenvalues (shape: [num_poles]). + """ + conj_index = label_conjugate_poles(poles) + dk_matrix = np.zeros( + (num_samples, num_poles + max(num_polys, 1)), dtype=np.complex128 + ) + compute_dk_matrix( + dk_matrix, eval_points, poles, conj_index, num_poles, num_polys, tol_high + ) + # For relaxed vector fitting, include the constant term in the sigma(s) + # function even when no polynomial terms are requested. This ensures the + # constrained system is well-posed when n_polys == 0. + if num_polys == 0: + dk_matrix[:, num_poles] = 1.0 + 0j + + scale_factor = ( + np.sqrt( + sum(norm(weights[m] * response_matrix[m]) ** 2 for m in range(num_vectors)) + ) + / num_samples + ) + lhs_matrix = np.zeros((num_vectors * (num_poles + 1), num_poles + 1)) + rhs_vector = np.zeros(num_vectors * (num_poles + 1)) + + for vec_idx in range(num_vectors): + vec_idx, lhs_block, rhs_block = process_constrained_block( + vec_idx, + dk_matrix, + weights, + response_matrix, + num_samples, + num_poles, + num_polys, + scale_factor, + num_vectors, + ) + i0 = vec_idx * (num_poles + 1) + i1 = (vec_idx + 1) * (num_poles + 1) + lhs_matrix[i0:i1] = lhs_block + rhs_vector[i0:i1] = rhs_block + + solution, *_ = wlstsq(lhs_matrix, rhs_vector) + coeffs = solution[:-1] + denom = solution[-1] + + if abs(denom) < tol_low or abs(denom) > tol_high: + lhs_matrix = np.zeros((num_vectors * num_poles, num_poles)) + rhs_vector = np.zeros(num_vectors * num_poles) + # Adjust denom + if denom == 0.0: + denom = 1.0 + elif abs(denom) < tol_low: + denom = np.sign(denom) * tol_low + elif abs(denom) > tol_high: + denom = np.sign(denom) * tol_high + + # Allocate output + lhs_matrix = np.zeros((num_vectors * num_poles, num_poles)) + rhs_vector = np.zeros(num_vectors * num_poles) + + for vec_idx in range(num_vectors): + vec_idx, lhs_block, rhs_block = process_unconstrained_block( + vec_idx, + dk_matrix, + weights, + response_matrix, + denom, + num_poles, + num_polys, + ) + i0 = vec_idx * num_poles + i1 = (vec_idx + 1) * num_poles + lhs_matrix[i0:i1] = lhs_block + rhs_vector[i0:i1] = rhs_block + + coeffs, *_ = wlstsq(lhs_matrix, rhs_vector) + + lambda_matrix = np.zeros((num_poles, num_poles)) + scale_vector = np.ones((num_poles, 1)) + + # Mask for real poles (conj_index == 0) + mask_real = conj_index == 0 + real_indices = np.where(mask_real)[0] + lambda_matrix[real_indices, real_indices] = np.real(poles[real_indices]) + + # Mask for start of complex conjugate pairs (conj_index == 1) + mask_cplx_start = conj_index == 1 + cplx_indices = np.where(mask_cplx_start)[0] + + # Extract real and imaginary parts of complex conjugate poles + real_parts = np.real(poles[cplx_indices]) + imag_parts = np.imag(poles[cplx_indices]) + + # Diagonal assignments + lambda_matrix[cplx_indices, cplx_indices] = real_parts + lambda_matrix[cplx_indices + 1, cplx_indices + 1] = real_parts + + # Off-diagonal assignments + lambda_matrix[cplx_indices, cplx_indices + 1] = imag_parts + lambda_matrix[cplx_indices + 1, cplx_indices] = -imag_parts + + # Scaling vector adjustments + scale_vector[cplx_indices, 0] = 2.0 + scale_vector[cplx_indices + 1, 0] = 0.0 + + residue_matrix = lambda_matrix - np.outer(scale_vector.squeeze(), coeffs) / denom + return eigvals(residue_matrix) + + +def solve_vector_block( + vec_idx: int, + dk_matrix: np.ndarray, + weights: np.ndarray, + response_matrix: np.ndarray, + num_poles: int, + num_polys: int, +) -> Tuple[int, np.ndarray, np.ndarray]: + """ + Solve the least-squares system for a single vector index. + + Parameters + ---------- + vec_idx : int + Index of the vector to solve. + dk_matrix : ndarray + Basis function evaluations of shape (num_samples, num_poles + num_polys). + weights : ndarray + Weight array of shape (num_vectors, num_samples). + response_matrix : ndarray + Response array of shape (num_vectors, num_samples). + num_poles : int + Number of poles. + num_polys : int + Number of polynomial coefficients. + + Returns + ------- + vec_idx : int + The index of the solved vector. + residues : ndarray + Solution vector for the residues (length = num_poles). + poly_coeffs : ndarray or None + Solution vector for polynomial coefficients (length = num_polys), or None if num_polys == 0. + """ + A = dk_matrix * weights[vec_idx][:, np.newaxis] + b = weights[vec_idx] * response_matrix[vec_idx] + + lhs_matrix = np.vstack((A.real, A.imag)) + rhs_vector = np.concatenate((b.real, b.imag)) + + x = wlstsq(lhs_matrix, rhs_vector)[0] + + residues = x[:num_poles] + poly_coeffs = x[num_poles : num_poles + num_polys] if num_polys > 0 else None + + return vec_idx, residues, poly_coeffs + + +def identify_residues( + num_poles: int, + poles: np.ndarray, + num_samples: int, + num_polys: int, + eval_points: np.ndarray, + num_vectors: int, + weights: np.ndarray, + response_matrix: np.ndarray, + poly_coefficients: np.ndarray, + residue_matrix: np.ndarray, +) -> Tuple[np.ndarray, float]: + """ + Internal routine to compute residues and polynomial coefficients. + + Parameters + ---------- + num_poles : int + Number of poles. + poles : np.ndarray + Current poles (complex), shape (num_poles,). + num_samples : int + Number of frequency samples. + num_polys : int + Number of polynomial terms. + eval_points : np.ndarray + Real frequency values, shape (num_samples,). + num_vectors : int + Number of response vectors. + weights : np.ndarray + Weighting matrix, shape (num_vectors, num_samples). + response_matrix : np.ndarray + Complex frequency responses, shape (num_vectors, num_samples). + poly_coefficients : np.ndarray + Array to store output polynomial coefficients (in-place). + residue_matrix : np.ndarray + Array to store output residues (in-place). + + Returns + ------- + Tuple[np.ndarray, float] + - Fitted response matrix (np.ndarray) + - Root-mean-square fitting error (float) + """ + conj_index = label_conjugate_poles(poles) + dk_matrix = np.zeros((num_samples, num_poles + num_polys), dtype=np.complex128) + + compute_dk_matrix(dk_matrix, eval_points, poles, conj_index, num_poles, num_polys) + + real_residues = np.zeros((num_vectors, num_poles), dtype=np.float64) + + for vec_idx in range(num_vectors): + vec_idx, residues, poly_coeffs = solve_vector_block( + vec_idx, + dk_matrix, + weights, + response_matrix, + num_poles, + num_polys, + ) + real_residues[vec_idx] = residues + if poly_coeffs is not None: + poly_coefficients[vec_idx] = poly_coeffs + + # Mask for real poles + mask_real = conj_index == 0 + real_indices = np.where(mask_real)[0] + residue_matrix[:, real_indices] = real_residues[:, real_indices] + + # Mask for first of complex conjugate pairs + mask_cplx_start = conj_index == 1 + cplx_indices = np.where(mask_cplx_start)[0] + + # Compute complex residues using vectorized operations + residue_matrix[:, cplx_indices] = ( + real_residues[:, cplx_indices] + 1j * real_residues[:, cplx_indices + 1] + ) + residue_matrix[:, cplx_indices + 1] = ( + real_residues[:, cplx_indices] - 1j * real_residues[:, cplx_indices + 1] + ) + + fit_result = evaluate(eval_points, poles, residue_matrix, poly_coefficients) + rms_error = norm(fit_result - response_matrix) / np.sqrt(num_vectors * num_samples) + return fit_result, rms_error + + +def label_conjugate_poles(poles: np.ndarray) -> np.ndarray: + """ + Ensure complex poles appear in conjugate pairs and label them accordingly. + + Parameters + ---------- + poles : np.ndarray + 1D array of complex poles. + + Returns + ------- + np.ndarray + Array of integers indicating pole type: + - 0: real pole + - 1: first in a complex-conjugate pair + - 2: second in a complex-conjugate pair + + Raises + ------ + ValueError + If any complex pole does not have a valid conjugate pair. + """ + num_poles = len(poles) + conj_index = np.zeros(num_poles, dtype=int) + + # Identify complex poles (nonzero imaginary part) + is_complex = np.imag(poles) != 0.0 + + # Find conjugate pairs: poles[i+1] ≈ conj(poles[i]) + is_pair_start = is_complex[:-1] & np.isclose(np.conj(poles[:-1]), poles[1:]) + + # Mark valid conjugate pair entries + conj_index[:-1][is_pair_start] = 1 # mark i with 1 + conj_index[1:][is_pair_start] = 2 # mark i+1 with 2 + + # Now validate: all complex poles must be part of valid conjugate pairs + unmatched_complex = is_complex & (conj_index == 0) + if np.any(unmatched_complex): + raise ValueError("Complex poles must appear in conjugate pairs") + + return conj_index diff --git a/tests/unit_tests/test_data_multipole.py b/tests/unit_tests/test_data_multipole.py index 105099bb9..a58ab4865 100644 --- a/tests/unit_tests/test_data_multipole.py +++ b/tests/unit_tests/test_data_multipole.py @@ -49,14 +49,40 @@ def test_export_to_hdf5(tmpdir, u235): def test_from_endf(endf_data): - pytest.importorskip('vectfit') endf_file = os.path.join(endf_data, 'neutrons', 'n-001_H_001.endf') assert openmc.data.WindowedMultipole.from_endf( - endf_file, log=True, wmp_options={"n_win": 400, "n_cf": 3}) + endf_file, + log=True, + # Keep the test lightweight + vf_options={ + "njoy_error": 5e-3, + "vf_pieces": 1, + "rtol": 5e-2, + "atol": 1e-3, + "orders": [8, 12], + "n_vf_iter": 6, + }, + wmp_options={"n_win": 50, "n_cf": 3, "rtol": 5e-2, "atol": 1e-3}, + ) def test_from_endf_search(endf_data): - pytest.importorskip('vectfit') - endf_file = os.path.join(endf_data, 'neutrons', 'n-095_Am_244.endf') + endf_file = os.path.join(endf_data, 'neutrons', 'n-001_H_001.endf') assert openmc.data.WindowedMultipole.from_endf( - endf_file, log=True, wmp_options={"search": True, 'rtol':1e-2}) + endf_file, + log=True, + vf_options={ + "njoy_error": 5e-3, + "vf_pieces": 1, + "rtol": 5e-2, + "atol": 1e-3, + "orders": [8, 12], + "n_vf_iter": 6, + }, + wmp_options={ + "search": True, + "rtol": 5e-2, + "search_n_win": 3, + "search_cf_orders": [5, 3], + }, + ) diff --git a/tests/unit_tests/test_vectfit.py b/tests/unit_tests/test_vectfit.py new file mode 100644 index 000000000..f7b2e905d --- /dev/null +++ b/tests/unit_tests/test_vectfit.py @@ -0,0 +1,224 @@ +""" +Initially from Jingang Liang: https://github.com/mit-crpg/vectfit.git +""" + +import numpy as np +import pytest +from openmc.data.vectfit import evaluate, vectfit + + +@pytest.fixture +def ref_poles(): + """Reference poles for real-pole test.""" + return np.array( + [ + 9.709261771920490e02 + 0.0j, + -1.120960794075339e03 + 0.0j, + 1.923889557426567e00 + 7.543700246109742e01j, + 1.923889557426567e00 - 7.543700246109742e01j, + 1.159741300380281e02 + 3.595650922556496e-02j, + 1.159741300380281e02 - 3.595650922556496e-02j, + 1.546932165729394e02 + 8.728391144940301e-02j, + 1.546932165729394e02 - 8.728391144940301e-02j, + 2.280349190818197e02 + 2.814037559718684e-01j, + 2.280349190818197e02 - 2.814037559718684e-01j, + 2.313004772627853e02 + 3.004628477692201e-01j, + 2.313004772627853e02 - 3.004628477692201e-01j, + 2.787470098364861e02 + 3.414179169920170e-01j, + 2.787470098364861e02 - 3.414179169920170e-01j, + 3.570711338764254e02 + 4.485587371149193e-01j, + 3.570711338764254e02 - 4.485587371149193e-01j, + 4.701059001346060e02 + 6.598089307174224e-01j, + 4.701059001346060e02 - 6.598089307174224e-01j, + 7.275819506342254e02 + 1.189678974845038e03j, + 7.275819506342254e02 - 1.189678974845038e03j, + ] + ) + + +@pytest.fixture +def ref_residues(): + """Reference residues for real-pole test.""" + return np.array( + [ + [ + -3.269879776751686e07 + 0.0j, + 1.131087935798761e09 + 0.0j, + 1.634151281869857e04 + 2.251103589277891e05j, + 1.634151281869857e04 - 2.251103589277891e05j, + 3.281792303833561e03 - 1.756079516325274e04j, + 3.281792303833561e03 + 1.756079516325274e04j, + 1.110800880243503e04 - 4.324813594540043e04j, + 1.110800880243503e04 + 4.324813594540043e04j, + 8.812700704117636e04 - 2.256520243571103e05j, + 8.812700704117636e04 + 2.256520243571103e05j, + 5.842090495551535e04 - 1.442159380741478e05j, + 5.842090495551535e04 + 1.442159380741478e05j, + 1.339410514130921e05 - 2.640767909713812e05j, + 1.339410514130921e05 + 2.640767909713812e05j, + 2.211245633333130e05 - 3.222447758311512e05j, + 2.211245633333130e05 + 3.222447758311512e05j, + 4.124430059785149e05 - 4.076023108323907e05j, + 4.124430059785149e05 + 4.076023108323907e05j, + 1.607378314999252e09 - 1.401163320110452e08j, + 1.607378314999252e09 + 1.401163320110452e08j, + ] + ] + ) + + +@pytest.fixture +def vector_test_data(): + """Simple 2-signal test with known poles and residues.""" + Ns = 101 + s = np.linspace(3.0, 7.0, Ns) + poles = [5.0 + 0.1j, 5.0 - 0.1j] + residues = [[0.5 - 11.0j, 0.5 + 11.0j], [1.5 - 20.0j, 1.5 + 20.0j]] + f = np.zeros((2, Ns)) + for i in range(2): + f[i, :] = np.real( + residues[i][0] / (s - poles[0]) + residues[i][1] / (s - poles[1]) + ) + weight = 1.0 / f + init_poles = [3.5 + 0.035j, 3.5 - 0.035j] + return s, poles, residues, f, weight, init_poles + + +@pytest.fixture +def poly_test_data(): + """Test data with rational function plus polynomial terms.""" + Ns = 201 + s = np.linspace(0.0, 5.0, Ns) + poles = [-20.0 + 30.0j, -20.0 - 30.0j] + residues = [[5.0 + 10.0j, 5.0 - 10.0j]] + polys = [[1.0, 2.0, 0.3]] + f = evaluate(s, poles, residues, polys) + weight = 1.0 / f + init_poles = [2.5 + 0.025j, 2.5 - 0.025j] + return s, poles, residues, polys, f, weight, init_poles + + +@pytest.fixture +def real_poles_data(ref_poles, ref_residues): + """Large-scale signal using complex and real poles.""" + Ns = 2000 + s = np.linspace(1.0e-2, 5.0e3, Ns) + f = np.zeros((1, Ns)) + for p, r in zip(ref_poles, ref_residues[0]): + f[0] += (r / (s - p)).real + weight = 1.0 / f + poles = np.linspace(1.1e-2, 4.8e3, 10) + poles = poles + poles * 0.01j + poles = np.sort(np.append(poles, np.conj(poles))) + return s, f, weight, poles + + +@pytest.fixture +def large_test_data(): + """Stress test data with thousands of poles and samples.""" + Ns = 3000 + N = 200 + s = np.linspace(1.0e-2, 5.0e3, Ns) + poles = np.linspace(1.1e-2, 4.8e3, N // 2) + 0.01j * np.linspace( + 1.1e-2, 4.8e3, N // 2 + ) + poles = np.sort(np.append(poles, np.conj(poles))) + residues = np.linspace(1e2, 1e6, N // 2) + 0.5j * np.linspace(1e2, 1e6, N // 2) + residues = np.sort(np.append(residues, np.conj(residues))).reshape((1, N)) + f = np.zeros((1, Ns)) + for p, r in zip(poles, residues[0]): + f[0] += (r / (s - p)).real + weight = 1.0 / f + init_poles = np.linspace(1.2e-2, 4.7e3, N // 2) + 0.01j * np.linspace( + 1.2e-2, 4.7e3, N // 2 + ) + init_poles = np.sort(np.append(init_poles, np.conj(init_poles))) + return s, f, weight, init_poles + + +@pytest.fixture +def eval_test_data(): + """Reference data for evaluating rational + polynomial models.""" + Ns = 101 + s = np.linspace(-5.0, 5.0, Ns) + poles = [-2.0 + 30.0j, -2.0 - 30.0j] + residues = [5.0 + 10.0j, 5.0 - 10.0j] + polys = [1.0, 2.0, 0.3] + return s, poles, residues, polys + + +def test_vector(vector_test_data): + """Test vectfit with vector samples and simple poles. + It is expected to get exact results with one iteration. + """ + s, expected_poles, expected_residues, f, weight, init_poles = vector_test_data + poles, residues, _, fit, _ = vectfit(f, s, init_poles, weight) + assert np.allclose( + np.sort_complex(poles), np.sort_complex(expected_poles), rtol=1e-7 + ) + assert np.allclose(f, evaluate(s, poles, residues), rtol=1e-7) + assert np.allclose(f, fit, rtol=1e-5) + + +def test_poly(poly_test_data): + """Test vectfit with polynomials.""" + s, expected_poles, expected_residues, expected_polys, f, weight, init_poles = ( + poly_test_data + ) + poles, residues, cf, fit, _ = vectfit(f, s, init_poles, weight, n_polys=3) + poles, residues, cf, fit, _ = vectfit(f, s, poles, weight, n_polys=3) + assert np.allclose( + np.sort_complex(poles), np.sort_complex(expected_poles), rtol=1e-5 + ) + assert np.allclose(f, evaluate(s, poles, residues, cf), rtol=1e-5) + assert np.allclose(cf, expected_polys, rtol=1e-5) + assert np.allclose(f, fit, rtol=1e-4) + + +def test_real_poles(real_poles_data, ref_poles, ref_residues): + """Test vectfit with more poles including real poles""" + s, f, weight, poles = real_poles_data + for _ in range(6): + poles, residues, _, fit, _ = vectfit(f, s, poles, weight) + assert np.allclose( + np.sort_complex(poles), np.sort_complex(ref_poles), rtol=1e-5, atol=1e-8 + ) + assert np.allclose(f, evaluate(s, poles, residues), rtol=1e-4) + assert np.allclose(f, fit, rtol=1e-3) + + +def test_large(large_test_data): + """Test vectfit with a large set of poles and samples""" + s, f, weight, init_poles = large_test_data + poles_fit, residues_fit, _, f_fit, _ = vectfit(f, s, init_poles, weight) + assert np.allclose(f, f_fit, rtol=1e-3) + + +def test_evaluate(eval_test_data): + """Test evaluate function""" + s, poles, residues, polys = eval_test_data + + # Single signal, no polynomial + f_ref = np.real(residues[0] / (s - poles[0]) + residues[1] / (s - poles[1])) + f = evaluate(s, poles, residues) + assert np.allclose(f[0], f_ref) + + # Single signal, with polynomial + for n, c in enumerate(polys): + f_ref += c * np.power(s, n) + f = evaluate(s, poles, residues, polys) + assert np.allclose(f[0], f_ref) + + # Multi-signal, multi-residue, multi-poly + poles = [5.0 + 0.1j, 5.0 - 0.1j] + residues = [[0.5 - 11.0j, 0.5 + 11.0j], [1.5 - 20.0j, 1.5 + 20.0j]] + polys = [[1.0, 2.0, 0.3], [4.0, -2.0, -10.0]] + f_ref = np.zeros((2, len(s))) + for i in range(2): + f_ref[i, :] = np.real( + residues[i][0] / (s - poles[0]) + residues[i][1] / (s - poles[1]) + ) + for n, c in enumerate(polys[i]): + f_ref[i, :] += c * np.power(s, n) + f = evaluate(s, poles, residues, polys) + assert np.allclose(f, f_ref) diff --git a/tools/ci/gha-install-vectfit.sh b/tools/ci/gha-install-vectfit.sh deleted file mode 100755 index bd38e1ea8..000000000 --- a/tools/ci/gha-install-vectfit.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash -set -ex - -PYBIND_BRANCH='master' -PYBIND_REPO='https://github.com/pybind/pybind11' - -XTL_BRANCH='0.6.13' -XTL_REPO='https://github.com/xtensor-stack/xtl' - -XTENSOR_BRANCH='0.21.3' -XTENSOR_REPO='https://github.com/xtensor-stack/xtensor' - -XTENSOR_PYTHON_BRANCH='0.24.1' -XTENSOR_PYTHON_REPO='https://github.com/xtensor-stack/xtensor-python' - -XTENSOR_BLAS_BRANCH='0.17.1' -XTENSOR_BLAS_REPO='https://github.com/xtensor-stack/xtensor-blas' - -cd $HOME -git clone -b $PYBIND_BRANCH $PYBIND_REPO -cd pybind11 && mkdir build && cd build && cmake .. && sudo make install -pip install $HOME/pybind11 - -cd $HOME -git clone -b $XTL_BRANCH $XTL_REPO -cd xtl && mkdir build && cd build && cmake .. && sudo make install - -cd $HOME -git clone -b $XTENSOR_BRANCH $XTENSOR_REPO -cd xtensor && mkdir build && cd build && cmake .. && sudo make install - -cd $HOME -git clone -b $XTENSOR_PYTHON_BRANCH $XTENSOR_PYTHON_REPO -cd xtensor-python && mkdir build && cd build && cmake .. && sudo make install - -cd $HOME -git clone -b $XTENSOR_BLAS_BRANCH $XTENSOR_BLAS_REPO -cd xtensor-blas && mkdir build && cd build && cmake .. && sudo make install - -# Install wheel (remove when vectfit supports installation with build isolation) -pip install wheel - -# Install vectfit -cd $HOME -git clone https://github.com/liangjg/vectfit.git -pip install --no-build-isolation ./vectfit diff --git a/tools/ci/gha-install.sh b/tools/ci/gha-install.sh index cccac866c..f405baedd 100755 --- a/tools/ci/gha-install.sh +++ b/tools/ci/gha-install.sh @@ -18,11 +18,6 @@ fi pip install 'ncrystal>=4.1.0' nctool --test -# Install vectfit for WMP generation if needed -if [[ $VECTFIT = 'y' ]]; then - ./tools/ci/gha-install-vectfit.sh -fi - # Install libMesh if needed if [[ $LIBMESH = 'y' ]]; then ./tools/ci/gha-install-libmesh.sh From 96383fcb2bb40da733b55241779ce75d2d9ab9c3 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Wed, 11 Feb 2026 23:05:07 +0200 Subject: [PATCH 536/671] More interpolation types in Tabular. (#3413) Co-authored-by: Paul Romano --- include/openmc/math_functions.h | 12 +++ openmc/stats/univariate.py | 155 +++++++++++++++++++++++++------- src/distribution.cpp | 43 +++++++-- src/math_functions.cpp | 18 ++++ tests/unit_tests/test_stats.py | 18 +++- 5 files changed, 201 insertions(+), 45 deletions(-) diff --git a/include/openmc/math_functions.h b/include/openmc/math_functions.h index 0d960c33d..d67b8305e 100644 --- a/include/openmc/math_functions.h +++ b/include/openmc/math_functions.h @@ -201,6 +201,18 @@ std::complex faddeeva(std::complex z); //! \return Derivative of Faddeeva function evaluated at z std::complex w_derivative(std::complex z, int order); +//! Evaluate relative exponential function +//! +//! \param x Real argument +//! \return (exp(x)-1)/x without loss of precision near 0 +double exprel(double x); + +//! Evaluate relative logarithm function +//! +//! \param x Real argument +//! \return log(1+x)/x without loss of precision near 0 +double log1prel(double x); + //! Helper function to get index and interpolation function on an incident //! energy grid //! diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index 94258c833..3d46024a4 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -10,6 +10,7 @@ from warnings import warn import lxml.etree as ET import numpy as np from scipy.integrate import trapezoid +from scipy.special import exprel, hyp1f1, lambertw import scipy import openmc.checkvalue as cv @@ -25,6 +26,16 @@ _INTERPOLATION_SCHEMES = { } +def exprel2(x): + """Evaluate 2*(exp(x)-1-x)/x^2 without loss of precision near 0""" + return hyp1f1(1, 3, x) + + +def log1prel(x): + """Evaluate log(1+x)/x without loss of precision near 0""" + return np.where(np.abs(x) < 1e-16, 1.0, np.log1p(x) / x) + + class Univariate(EqualityMixin, ABC): """Probability distribution of a single random variable. @@ -1299,44 +1310,64 @@ class Tabular(Univariate): c[1:] = p[:x.size-1] * np.diff(x) elif self.interpolation == 'linear-linear': c[1:] = 0.5 * (p[:-1] + p[1:]) * np.diff(x) + elif self.interpolation == "linear-log": + m = np.diff(p) / np.diff(np.log(x)) + c[1:] = p[:-1] * np.diff(x) + m * ( + x[1:] * (np.diff(np.log(x)) - 1.0) + x[:-1] + ) + elif self.interpolation == "log-linear": + m = np.diff(np.log(p)) / np.diff(x) + c[1:] = p[:-1] * np.diff(x) * exprel(m * np.diff(x)) + elif self.interpolation == "log-log": + m = np.diff(np.log(x * p)) / np.diff(np.log(x)) + c[1:] = (x * p)[:-1] * np.diff(np.log(x)) * exprel(m * np.diff(np.log(x))) else: - raise NotImplementedError('Can only generate CDFs for tabular ' - 'distributions using histogram or ' - 'linear-linear interpolation') - + raise NotImplementedError( + f"Cannot generate CDFs for tabular " + f"distributions using {self.interpolation} interpolation" + ) return np.cumsum(c) def mean(self): """Compute the mean of the tabular distribution""" - if self.interpolation == 'linear-linear': - mean = 0.0 - for i in range(1, len(self.x)): - y_min = self.p[i-1] - y_max = self.p[i] - x_min = self.x[i-1] - x_max = self.x[i] - m = (y_max - y_min) / (x_max - x_min) + # use normalized probabilities when computing mean + p = self.p / self.cdf().max() + x = self.x + x_min = x[:-1] + x_max = x[1:] + p_min = p[: x.size - 1] - exp_val = (1./3.) * m * (x_max**3 - x_min**3) - exp_val += 0.5 * m * x_min * (x_min**2 - x_max**2) - exp_val += 0.5 * y_min * (x_max**2 - x_min**2) - mean += exp_val - - elif self.interpolation == 'histogram': - x_l = self.x[:-1] - x_r = self.x[1:] - p_l = self.p[:self.x.size-1] - mean = (0.5 * (x_l + x_r) * (x_r - x_l) * p_l).sum() + if self.interpolation == "linear-linear": + m = np.diff(p) / np.diff(x) + mean = ((1.0 / 3.0) * m * np.diff(x**3) + + 0.5 * (p_min - m * x_min) * np.diff(x**2)).sum() + elif self.interpolation == "linear-log": + m = np.diff(p) / np.diff(np.log(x)) + mean = ( + (1.0 / 4.0) * m * x_min**2 + * ((x_max / x_min)**2 * (2 * np.diff(np.log(x)) - 1) + 1) + + 0.5 * p_min * np.diff(x**2) + ).sum() + elif self.interpolation == "log-linear": + m = np.diff(np.log(p)) / np.diff(x) + mean = (p_min * ( + np.diff(x) ** 2 + * ((0.5 * exprel2(m * np.diff(x)) * (m * np.diff(x) - 1) + 1)) + + np.diff(x) * x_min * exprel(m * np.diff(x))) + ).sum() + elif self.interpolation == "log-log": + m = np.diff(np.log(p)) / np.diff(np.log(x)) + mean = (p_min * x_min**2 * np.diff(np.log(x)) + * exprel((m + 2) * np.diff(np.log(x)))).sum() + elif self.interpolation == "histogram": + mean = (0.5 * (x_min + x_max) * np.diff(x) * p_min).sum() else: - raise NotImplementedError('Can only compute mean for tabular ' - 'distributions using histogram ' - 'or linear-linear interpolation.') - - # Normalize for when integral of distribution is not 1 - mean /= self.integral() - + raise NotImplementedError( + f"Cannot compute mean for tabular " + f"distributions using {self.interpolation} interpolation" + ) return mean def normalize(self): @@ -1395,11 +1426,56 @@ class Tabular(Univariate): quad[quad < 0.0] = 0.0 m[non_zero] = x_i[non_zero] + (np.sqrt(quad) - p_i[non_zero]) / m[non_zero] samples_out = m + elif self.interpolation == "linear-log": + # get variable and probability values for the + # next entry + x_i1 = self.x[cdf_idx + 1] + p_i1 = p[cdf_idx + 1] + # compute slope between entries + m = (p_i1 - p_i) / np.log(x_i1 / x_i) + # set values for zero slope + zero = m == 0.0 + m[zero] = x_i[zero] + (xi[zero] - c_i[zero]) / p_i[zero] + positive = m > 0 + negative = m < 0 + a = p_i / m - 1 + m[positive] = ( + x_i + * ((xi - c_i) / (m * x_i) + a) + / np.real(lambertw((((xi - c_i) / (m * x_i) + a)) * np.exp(a))) + )[positive] + m[negative] = ( + x_i + * ((xi - c_i) / (m * x_i) + a) + / np.real(lambertw((((xi - c_i) / (m * x_i) + a)) * np.exp(a), -1.0)) + )[negative] + samples_out = m + elif self.interpolation == "log-linear": + # get variable and probability values for the + # next entry + x_i1 = self.x[cdf_idx + 1] + p_i1 = p[cdf_idx + 1] + # compute slope between entries + m = np.log(p_i1 / p_i) / (x_i1 - x_i) + f = (xi - c_i) / p_i + + samples_out = x_i + f * log1prel(m * f) + elif self.interpolation == "log-log": + # get variable and probability values for the + # next entry + x_i1 = self.x[cdf_idx + 1] + p_i1 = p[cdf_idx + 1] + # compute slope between entries + m = np.log((x_i1 * p_i1) / (x_i * p_i)) / np.log(x_i1 / x_i) + f = (xi - c_i) / (x_i * p_i) + + samples_out = x_i * np.exp(f * log1prel(m * f)) else: - raise NotImplementedError('Can only sample tabular distributions ' - 'using histogram or ' - 'linear-linear interpolation') + raise NotImplementedError( + f"Cannot sample tabular distributions " + f"for {self.inteprolation} interpolation " + ) assert all(samples_out < self.x[-1]) return samples_out @@ -1497,9 +1573,22 @@ class Tabular(Univariate): return np.sum(np.diff(self.x) * self.p[:self.x.size-1]) elif self.interpolation == 'linear-linear': return trapezoid(self.p, self.x) + elif self.interpolation == "linear-log": + m = np.diff(self.p) / np.diff(np.log(self.x)) + return np.sum( + self.p[:-1] * np.diff(self.x) + + m * (self.x[1:] * (np.diff(np.log(self.x)) - 1.0) + self.x[:-1]) + ) + elif self.interpolation == "log-linear": + m = np.diff(np.log(self.p)) / np.diff(self.x) + return np.sum(self.p[:-1] * np.diff(self.x) * exprel(m * np.diff(self.x))) + elif self.interpolation == "log-log": + m = np.diff(np.log(self.p)) / np.diff(np.log(self.x)) + return np.sum(self.p[:-1] * self.x[:-1] * np.diff(np.log(self.x)) + * exprel((m + 1) * np.diff(np.log(self.x)))) else: raise NotImplementedError( - f'integral() not supported for {self.inteprolation} interpolation') + f'integral() not supported for {self.interpolation} interpolation') class Legendre(Univariate): diff --git a/src/distribution.cpp b/src/distribution.cpp index 537a56171..756b098ed 100644 --- a/src/distribution.cpp +++ b/src/distribution.cpp @@ -401,6 +401,10 @@ Tabular::Tabular(pugi::xml_node node) interp_ = Interpolation::histogram; } else if (temp == "linear-linear") { interp_ = Interpolation::lin_lin; + } else if (temp == "log-linear") { + interp_ = Interpolation::log_lin; + } else if (temp == "log-log") { + interp_ = Interpolation::log_log; } else { openmc::fatal_error( "Unsupported interpolation type for distribution: " + temp); @@ -437,13 +441,6 @@ void Tabular::init( std::copy(x, x + n, std::back_inserter(x_)); std::copy(p, p + n, std::back_inserter(p_)); - // Check interpolation parameter - if (interp_ != Interpolation::histogram && - interp_ != Interpolation::lin_lin) { - openmc::fatal_error("Only histogram and linear-linear interpolation " - "for tabular distribution is supported."); - } - // Calculate cumulative distribution function if (c) { std::copy(c, c + n, std::back_inserter(c_)); @@ -455,6 +452,18 @@ void Tabular::init( c_[i] = c_[i - 1] + p_[i - 1] * (x_[i] - x_[i - 1]); } else if (interp_ == Interpolation::lin_lin) { c_[i] = c_[i - 1] + 0.5 * (p_[i - 1] + p_[i]) * (x_[i] - x_[i - 1]); + } else if (interp_ == Interpolation::log_lin) { + double m = std::log(p_[i] / p_[i - 1]) / (x_[i] - x_[i - 1]); + c_[i] = c_[i - 1] + p_[i - 1] * (x_[i] - x_[i - 1]) * + exprel(m * (x_[i] - x_[i - 1])); + } else if (interp_ == Interpolation::log_log) { + double m = std::log((x_[i] * p_[i]) / (x_[i - 1] * p_[i - 1])) / + std::log(x_[i] / x_[i - 1]); + c_[i] = c_[i - 1] + x_[i - 1] * p_[i - 1] * + std::log(x_[i] / x_[i - 1]) * + exprel(m * std::log(x_[i] / x_[i - 1])); + } else { + UNREACHABLE(); } } } @@ -495,7 +504,7 @@ double Tabular::sample_unbiased(uint64_t* seed) const } else { return x_i; } - } else { + } else if (interp_ == Interpolation::lin_lin) { // Linear-linear interpolation double x_i1 = x_[i + 1]; double p_i1 = p_[i + 1]; @@ -508,6 +517,24 @@ double Tabular::sample_unbiased(uint64_t* seed) const (std::sqrt(std::max(0.0, p_i * p_i + 2 * m * (c - c_i))) - p_i) / m; } + } else if (interp_ == Interpolation::log_lin) { + // Log-linear interpolation + double x_i1 = x_[i + 1]; + double p_i1 = p_[i + 1]; + + double m = std::log(p_i1 / p_i) / (x_i1 - x_i); + double f = (c - c_i) / p_i; + return x_i + f * log1prel(m * f); + } else if (interp_ == Interpolation::log_log) { + // Log-Log interpolation + double x_i1 = x_[i + 1]; + double p_i1 = p_[i + 1]; + + double m = std::log((x_i1 * p_i1) / (x_i * p_i)) / std::log(x_i1 / x_i); + double f = (c - c_i) / (p_i * x_i); + return x_i * std::exp(f * log1prel(m * f)); + } else { + UNREACHABLE(); } } diff --git a/src/math_functions.cpp b/src/math_functions.cpp index 9473f928b..f45165414 100644 --- a/src/math_functions.cpp +++ b/src/math_functions.cpp @@ -919,6 +919,24 @@ std::complex w_derivative(std::complex z, int order) } } +double exprel(double x) +{ + if (std::abs(x) < 1e-16) + return 1.0; + else { + return std::expm1(x) / x; + } +} + +double log1prel(double x) +{ + if (std::abs(x) < 1e-16) + return 1.0; + else { + return std::log1p(x) / x; + } +} + // Helper function to get index and interpolation function on an incident energy // grid void get_energy_index( diff --git a/tests/unit_tests/test_stats.py b/tests/unit_tests/test_stats.py index 507e85743..bdbdaa0f2 100644 --- a/tests/unit_tests/test_stats.py +++ b/tests/unit_tests/test_stats.py @@ -4,6 +4,7 @@ import numpy as np import pytest import openmc import openmc.stats +from openmc.stats.univariate import _INTERPOLATION_SCHEMES from scipy.integrate import trapezoid from tests.unit_tests import assert_sample_mean @@ -273,7 +274,7 @@ def test_watt(): @pytest.mark.flaky(reruns=1) def test_tabular(): # test linear-linear sampling - x = np.array([0.0, 5.0, 7.0, 10.0]) + x = np.array([0.001, 5.0, 7.0, 10.0]) p = np.array([10.0, 20.0, 5.0, 6.0]) d = openmc.stats.Tabular(x, p, 'linear-linear') n_samples = 100_000 @@ -281,9 +282,12 @@ def test_tabular(): assert_sample_mean(samples, d.mean()) assert np.all(weights == 1.0) - # test linear-linear normalization - d.normalize() - assert d.integral() == pytest.approx(1.0) + for scheme in _INTERPOLATION_SCHEMES: + # test sampling + d = openmc.stats.Tabular(x, p, scheme) + n_samples = 100_000 + samples = d.sample(n_samples)[0] + assert_sample_mean(samples, d.mean()) # test histogram sampling d = openmc.stats.Tabular(x, p, interpolation='histogram') @@ -291,6 +295,12 @@ def test_tabular(): assert_sample_mean(samples, d.mean()) assert np.all(weights == 1.0) + # Multiplying the probabilities should preserve the mean but change the integral + d2 = openmc.stats.Tabular(x, p*2, interpolation='histogram') + assert d2.mean() == pytest.approx(d.mean()) + assert d2.integral() == pytest.approx(2.0*d.integral()) + + # Normalizing should result in an integral of 1 d.normalize() assert d.integral() == pytest.approx(1.0) From 26b13083f3c585a95ab6f3bfa8a961cbc9008725 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Thu, 12 Feb 2026 06:58:00 +0100 Subject: [PATCH 537/671] Use `gnds_name` and `zam` from endf package (#3796) --- openmc/data/data.py | 60 +++------------------------------------------ 1 file changed, 4 insertions(+), 56 deletions(-) diff --git a/openmc/data/data.py b/openmc/data/data.py index 03d67d7a9..c22e54e7d 100644 --- a/openmc/data/data.py +++ b/openmc/data/data.py @@ -7,7 +7,10 @@ from math import sqrt, log from warnings import warn from endf.data import (ATOMIC_NUMBER, ATOMIC_SYMBOL, ELEMENT_SYMBOL, - EV_PER_MEV, K_BOLTZMANN) + EV_PER_MEV, K_BOLTZMANN, gnds_name, zam) + +gnds_name.__module__ = __name__ +zam.__module__ = __name__ # Isotopic abundances from Meija J, Coplen T B, et al, "Isotopic compositions # of the elements 2013 (IUPAC Technical Report)", Pure. Appl. Chem. 88 (3), @@ -217,9 +220,6 @@ NEUTRON_MASS = 1.00866491595 # Used in atomic_mass function as a cache _ATOMIC_MASS: dict[str, float] = {} -# Regex for GNDS nuclide names (used in zam function) -_GNDS_NAME_RE = re.compile(r'([A-Zn][a-z]*)(\d+)((?:_[em]\d+)?)') - # Used in half_life function as a cache _HALF_LIFE: dict[str, float] = {} _LOG_TWO = log(2.0) @@ -456,33 +456,6 @@ def water_density(temperature, pressure=0.1013): return coeff / pi / gamma1_pi -def gnds_name(Z, A, m=0): - """Return nuclide name using GNDS convention - - .. versionchanged:: 0.14.0 - Function name changed from ``gnd_name`` to ``gnds_name`` - - Parameters - ---------- - Z : int - Atomic number - A : int - Mass number - m : int, optional - Metastable state - - Returns - ------- - str - Nuclide name in GNDS convention, e.g., 'Am242_m1' - - """ - if m > 0: - return f'{ATOMIC_SYMBOL[Z]}{A}_m{m}' - return f'{ATOMIC_SYMBOL[Z]}{A}' - - - def _get_element_symbol(element: str) -> str: if len(element) > 2: symbol = ELEMENT_SYMBOL.get(element.lower()) @@ -525,28 +498,3 @@ def isotopes(element: str) -> list[tuple[str, float]]: return result -def zam(name): - """Return tuple of (atomic number, mass number, metastable state) - - Parameters - ---------- - name : str - Name of nuclide using GNDS convention, e.g., 'Am242_m1' - - Returns - ------- - 3-tuple of int - Atomic number, mass number, and metastable state - - """ - try: - symbol, A, state = _GNDS_NAME_RE.fullmatch(name).groups() - except AttributeError: - raise ValueError(f"'{name}' does not appear to be a nuclide name in " - "GNDS format") - - if symbol not in ATOMIC_NUMBER: - raise ValueError(f"'{symbol}' is not a recognized element symbol") - - metastable = int(state[2:]) if state else 0 - return (ATOMIC_NUMBER[symbol], int(A), metastable) From 8d0fe6c71d4c41ba039d9d1f36fcf55a4a8f8833 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Thu, 12 Feb 2026 11:11:28 +0100 Subject: [PATCH 538/671] making use of sum_rules in endf package (#3799) --- openmc/data/endf.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/openmc/data/endf.py b/openmc/data/endf.py index bccd6b36b..1ac07a651 100644 --- a/openmc/data/endf.py +++ b/openmc/data/endf.py @@ -12,6 +12,7 @@ import re from .data import gnds_name from .function import Tabulated1D +from endf.incident_neutron import SUM_RULES from endf.records import ( float_endf, py_float_endf, @@ -63,24 +64,6 @@ _SUBLIBRARY = { 20040: 'Incident-alpha data' } -SUM_RULES = {1: [2, 3], - 3: [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], - 4: list(range(50, 92)), - 16: list(range(875, 892)), - 18: [19, 20, 21, 38], - 27: [18, 101], - 101: [102, 103, 104, 105, 106, 107, 108, 109, 111, 112, 113, 114, - 115, 116, 117, 155, 182, 191, 192, 193, 197], - 103: list(range(600, 650)), - 104: list(range(650, 700)), - 105: list(range(700, 750)), - 106: list(range(750, 800)), - 107: list(range(800, 850))} - def get_evaluations(filename): """Return a list of all evaluations within an ENDF file. From a3426cf833c1e6b95efa3bd077a13677fa88429d Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Thu, 12 Feb 2026 06:30:31 -0600 Subject: [PATCH 539/671] Fix weight windows regression test (#3798) --- .../weightwindows/inputs_true.dat | 22 +- .../weightwindows/results_true.dat | 2 +- tests/regression_tests/weightwindows/test.py | 13 +- tests/regression_tests/weightwindows/ww_n.txt | 314 +++++++-------- tests/regression_tests/weightwindows/ww_p.txt | 372 +++++++++--------- 5 files changed, 359 insertions(+), 364 deletions(-) diff --git a/tests/regression_tests/weightwindows/inputs_true.dat b/tests/regression_tests/weightwindows/inputs_true.dat index eb9393179..aa7a0379d 100644 --- a/tests/regression_tests/weightwindows/inputs_true.dat +++ b/tests/regression_tests/weightwindows/inputs_true.dat @@ -4,7 +4,6 @@ - @@ -14,14 +13,9 @@ - - - - - @@ -32,7 +26,7 @@ fixed source - 200 + 500 2 @@ -47,8 +41,8 @@ 2 neutron 0.0 0.5 20000000.0 - -1.0 0.007673145137236567 6.542700644645627e-07 0.0017541380096893788 0.0007245451610090619 7.476545089278482e-06 2.253382683081525e-06 2.1040609012865134e-05 0.08285899874014539 -1.0 1.0262431550731535e-13 3.3164825349503764e-16 0.0064803314120165335 3.5981031766416143e-06 -1.0 1.6205064883026195e-11 1.7770136411255912e-05 0.0001670700632870335 4.323138781337963e-05 -1.0 -1.0 6.922231352729155e-05 0.06550426960512012 -1.0 2.5639656866250453e-06 1.4508262073256659e-13 -1.0 -1.0 0.0009127050763987511 3.8523247550378815e-12 -1.0 0.0008175033526488423 -1.0 0.015148978428271613 2.5157355790201773e-07 2.1612150425221703e-06 1.3181562352445092e-10 2.9282097947816224e-05 2.762955748645507e-05 2.936268747135548e-14 1.6859219614058024e-19 3.937160538176268e-07 2.6011787393916806e-06 0.001570829313580841 -1.0 1.7041669224797384e-09 1.6421491993751715e-11 0.4899999304038166 6.948292975998466e-07 -1.0 -1.0 0.08657573566388353 0.006639379668946672 1.681305446488884e-11 2.2487617828938326e-05 4.109066205029878e-05 0.08288806458368345 -1.0 0.007385384405891666 -1.0 -1.0 1.008394964750947e-06 6.655926357459275e-08 0.0016279794552427574 1.4825219199133353e-12 -1.0 1.93847081892941e-05 1.284759166582202e-05 5.1161740516205715e-05 2.5447281241730366e-07 3.853097455417877e-06 -1.0 8.887234042637684e-05 0.01621821782442112 -1.0 1.8747824667478637e-14 5.97653814110781e-06 -1.0 -1.0 1.2296747260635405e-06 -1.0 -1.0 7.754700849337902e-06 7.091263906557291e-05 3.2799045883372075e-06 0.00020573181450240786 5.6949597066784976e-05 1.271128669411295e-07 0.0010019432401508087 -1.0 -1.0 6.848642863465585e-07 5.783974359937857e-06 2.380955631371226e-06 4.756872959630257e-05 2.1360401784344975e-18 0.00046029617115127556 1.0092905083158804e-11 0.08095280568305474 3.89841667138598e-08 4.354946981329799e-05 -1.0 0.5 0.0015447823995470042 -1.0 -1.0 0.001959411999677113 0.48573842213878143 -1.0 7.962899789920809e-06 5.619835883512353e-14 0.0835648710074336 2.7207433165796702e-11 0.00027464244062887683 4.076762579823379e-17 2.124999182004655e-05 8.037131813528501e-07 8.846493399850663e-06 1.080711295768551e-05 -1.0 -1.0 0.0010737081832502356 2.679340942769232e-06 2.1648918040467363e-05 0.0002668579837809081 1.3455341306162382e-05 2.5629068330446215e-05 1.3228013765525015e-05 -1.0 -1.0 7.002861620919232e-08 1.5172912057312398e-14 -1.0 7.989579285134734e-06 2.639268952045579e-10 -1.0 0.013330619853379314 0.00021534568699157805 2.4469456008493614e-09 8.211982683649991e-09 1.951356547084204e-15 0.00016419031150495913 5.985108617124989e-06 1.28584557092134e-05 1.5476769237571295e-14 5.5379154134462336e-08 0.0016637126543321873 2.499931357628383e-07 8.020767115181574e-07 2.7697233171399416e-13 -1.0 0.007197662162700777 -1.0 0.0833931797381589 2.8226276944532696e-05 3.353295403842037e-05 5.504813693036933e-12 0.007136750029575816 0.08636569925236791 -1.0 -1.0 6.315154082213375e-07 0.4877856021170283 -1.0 3.7051518220083886e-06 -1.0 0.0017950613169359904 7.84289689494925e-06 9.041149893307806e-07 -1.0 8.48176145671274e-10 9.03108551826469e-05 3.634644995026692e-05 2.0295213621716706e-09 1.0305672698745657e-11 1.5550455959178486e-06 0.01664109232689093 8.533030345699353e-17 0.0009252532821720597 -1.0 7.695031155172816e-14 0.0007151459162818186 -1.0 -1.0 5.182269672663266e-10 3.0160758454323657e-07 -1.0 0.06539005235506369 3.452537179033895e-05 -1.0 -1.0 3.855884234344845e-06 0.00017699858357744463 2.594951085377588e-06 1.1333627604823673e-09 -1.0 2.4306375693722205e-06 0.0063790654507599135 2.2060992580622125e-11 -1.0 -1.0 0.0848872967381878 3.932389299316285e-06 1.2911827198500372e-08 1.957092814065688e-05 0.0007213722022489493 0.0018327593437608 1.6699254277734109e-06 0.007188410694198128 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 - -10.0 0.07673145137236567 6.5427006446456266e-06 0.017541380096893787 0.0072454516100906195 7.476545089278483e-05 2.253382683081525e-05 0.00021040609012865135 0.828589987401454 -10.0 1.0262431550731535e-12 3.3164825349503764e-15 0.06480331412016534 3.5981031766416144e-05 -10.0 1.6205064883026195e-10 0.0001777013641125591 0.001670700632870335 0.0004323138781337963 -10.0 -10.0 0.0006922231352729155 0.6550426960512012 -10.0 2.5639656866250452e-05 1.4508262073256658e-12 -10.0 -10.0 0.009127050763987512 3.852324755037882e-11 -10.0 0.008175033526488424 -10.0 0.15148978428271614 2.515735579020177e-06 2.1612150425221703e-05 1.3181562352445092e-09 0.00029282097947816227 0.0002762955748645507 2.9362687471355483e-13 1.6859219614058023e-18 3.937160538176268e-06 2.6011787393916804e-05 0.015708293135808408 -10.0 1.7041669224797384e-08 1.6421491993751715e-10 4.899999304038166 6.948292975998466e-06 -10.0 -10.0 0.8657573566388354 0.06639379668946672 1.681305446488884e-10 0.00022487617828938325 0.0004109066205029878 0.8288806458368345 -10.0 0.07385384405891665 -10.0 -10.0 1.0083949647509469e-05 6.655926357459275e-07 0.016279794552427576 1.4825219199133352e-11 -10.0 0.000193847081892941 0.0001284759166582202 0.0005116174051620571 2.5447281241730365e-06 3.853097455417877e-05 -10.0 0.0008887234042637684 0.16218217824421122 -10.0 1.8747824667478637e-13 5.97653814110781e-05 -10.0 -10.0 1.2296747260635405e-05 -10.0 -10.0 7.754700849337902e-05 0.0007091263906557291 3.2799045883372076e-05 0.0020573181450240785 0.0005694959706678497 1.271128669411295e-06 0.010019432401508087 -10.0 -10.0 6.848642863465585e-06 5.7839743599378564e-05 2.380955631371226e-05 0.0004756872959630257 2.1360401784344976e-17 0.004602961711512756 1.0092905083158804e-10 0.8095280568305474 3.89841667138598e-07 0.00043549469813297995 -10.0 5.0 0.015447823995470043 -10.0 -10.0 0.01959411999677113 4.8573842213878144 -10.0 7.96289978992081e-05 5.619835883512353e-13 0.8356487100743359 2.7207433165796704e-10 0.002746424406288768 4.076762579823379e-16 0.00021249991820046548 8.037131813528501e-06 8.846493399850663e-05 0.0001080711295768551 -10.0 -10.0 0.010737081832502356 2.679340942769232e-05 0.00021648918040467362 0.0026685798378090807 0.00013455341306162382 0.00025629068330446217 0.00013228013765525016 -10.0 -10.0 7.002861620919232e-07 1.51729120573124e-13 -10.0 7.989579285134734e-05 2.639268952045579e-09 -10.0 0.13330619853379314 0.0021534568699157807 2.4469456008493615e-08 8.211982683649991e-08 1.951356547084204e-14 0.0016419031150495913 5.985108617124989e-05 0.000128584557092134 1.5476769237571296e-13 5.537915413446234e-07 0.016637126543321872 2.499931357628383e-06 8.020767115181574e-06 2.7697233171399415e-12 -10.0 0.07197662162700777 -10.0 0.8339317973815891 0.000282262769445327 0.00033532954038420373 5.504813693036933e-11 0.07136750029575815 0.8636569925236791 -10.0 -10.0 6.3151540822133754e-06 4.877856021170283 -10.0 3.7051518220083885e-05 -10.0 0.017950613169359905 7.84289689494925e-05 9.041149893307805e-06 -10.0 8.48176145671274e-09 0.000903108551826469 0.0003634644995026692 2.0295213621716707e-08 1.0305672698745658e-10 1.5550455959178486e-05 0.1664109232689093 8.533030345699353e-16 0.009252532821720597 -10.0 7.695031155172816e-13 0.007151459162818186 -10.0 -10.0 5.182269672663266e-09 3.0160758454323656e-06 -10.0 0.6539005235506369 0.0003452537179033895 -10.0 -10.0 3.855884234344845e-05 0.0017699858357744464 2.594951085377588e-05 1.1333627604823672e-08 -10.0 2.4306375693722206e-05 0.06379065450759913 2.2060992580622125e-10 -10.0 -10.0 0.848872967381878 3.9323892993162844e-05 1.2911827198500372e-07 0.0001957092814065688 0.0072137220224894934 0.018327593437608 1.669925427773411e-05 0.07188410694198127 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 + -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -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.0008758251780046591 -1.0 -1.0 -1.0 -1.0 0.00044494017319042853 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 1.6620271125067025e-05 0.0006278777700923334 3.3816651814344154e-05 -1.0 -1.0 0.0024363004681119066 0.04404124277352227 0.002389900091734746 -1.0 -1.0 0.001096572213298872 0.04862206884590391 0.0011530054432113332 -1.0 -1.0 -1.0 0.00029204950777272335 2.2332845991385424e-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.0001298973206651331 0.0028826281529980655 0.00018941326535850932 -1.0 5.4657521927731274e-05 0.014670839729146354 0.49999999999999994 0.011271060592765664 -1.0 -1.0 0.014846491082523524 0.4897211700090108 0.013284874451810723 -1.0 -1.0 5.14657989105535e-05 0.0010115278438204965 0.0008429411802845685 -1.0 -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.0009344129479768359 -1.0 -1.0 -1.0 0.0024828273390431962 0.04299740304329489 0.0020539079252555113 -1.0 -1.0 0.003034165905819096 0.04870927636605937 0.00313101668086297 -1.0 -1.0 -1.0 6.339910999436737e-05 7.442176086066386e-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 -1.0 -1.0 -1.0 -1.0 -1.0 0.00011276372995589871 0.0002236100157024887 9.56304298913265e-08 -1.0 -1.0 0.0001596955110781239 9.960576598335084e-06 5.3833030623153676e-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 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 4.0453018186074215e-05 4.6013290110121894e-05 2.709738801641897e-05 -1.0 -1.0 -1.0 9.538410811938703e-05 1.992978141244964e-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 -1.0 -1.0 -1.0 -1.0 -1.0 1.0723231851298364e-05 -1.0 -1.0 -1.0 -1.0 0.0008030100296698905 0.017386220476187222 0.0005027758935317528 -1.0 -1.0 0.00103568411326969 0.015609071124009234 0.0007473731339616588 -1.0 -1.0 -1.0 6.979537549963374e-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 -1.0 -1.0 0.0014257756927429828 5.9098070539559466e-05 -1.0 5.0168071459366625e-06 0.005588863190166347 0.5 0.003930588100414563 -1.0 -1.0 0.005163345217476071 0.48250750993887326 0.003510432129522241 -1.0 -1.0 3.371977841720992e-05 0.0006640103276501794 9.554899988419713e-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 -1.0 0.000187872910697387 -1.0 -1.0 -1.0 0.0009134587866163143 0.017081858078684467 0.0002972747357542354 -1.0 -1.0 0.0007973472495507653 0.019300903254809747 0.000902203032280694 -1.0 -1.0 6.170015233787292e-05 1.898984300674969e-05 9.85411583595722e-07 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 1.0173913765490095e-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 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 + -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.008758251780046591 -10.0 -10.0 -10.0 -10.0 0.004449401731904285 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00016620271125067024 0.006278777700923334 0.00033816651814344155 -10.0 -10.0 0.024363004681119065 0.4404124277352227 0.02389900091734746 -10.0 -10.0 0.01096572213298872 0.4862206884590391 0.011530054432113333 -10.0 -10.0 -10.0 0.0029204950777272335 0.00022332845991385425 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.0012989732066513312 0.028826281529980655 0.0018941326535850931 -10.0 0.0005465752192773128 0.14670839729146354 4.999999999999999 0.11271060592765664 -10.0 -10.0 0.14846491082523525 4.897211700090108 0.13284874451810724 -10.0 -10.0 0.000514657989105535 0.010115278438204964 0.008429411802845685 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00934412947976836 -10.0 -10.0 -10.0 0.024828273390431962 0.4299740304329489 0.020539079252555114 -10.0 -10.0 0.03034165905819096 0.48709276366059373 0.0313101668086297 -10.0 -10.0 -10.0 0.0006339910999436737 0.0007442176086066385 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.0011276372995589871 0.002236100157024887 9.56304298913265e-07 -10.0 -10.0 0.001596955110781239 9.960576598335084e-05 0.0005383303062315367 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00040453018186074213 0.00046013290110121896 0.0002709738801641897 -10.0 -10.0 -10.0 0.0009538410811938703 0.00019929781412449641 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00010723231851298364 -10.0 -10.0 -10.0 -10.0 0.008030100296698905 0.17386220476187222 0.0050277589353175285 -10.0 -10.0 0.010356841132696899 0.15609071124009233 0.007473731339616587 -10.0 -10.0 -10.0 0.0006979537549963374 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.014257756927429827 0.0005909807053955946 -10.0 5.016807145936663e-05 0.055888631901663474 5.0 0.039305881004145636 -10.0 -10.0 0.05163345217476071 4.825075099388733 0.03510432129522241 -10.0 -10.0 0.00033719778417209923 0.006640103276501793 0.0009554899988419713 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00187872910697387 -10.0 -10.0 -10.0 0.009134587866163143 0.17081858078684467 0.002972747357542354 -10.0 -10.0 0.007973472495507653 0.19300903254809748 0.009022030322806941 -10.0 -10.0 0.0006170015233787292 0.0001898984300674969 9.85411583595722e-06 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00010173913765490096 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 3.0 1.5 10 @@ -61,15 +55,19 @@ 2 - neutron + photon 0.0 0.5 20000000.0 - -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -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.06827568359008944 0.00017329528628152923 0.018541112907970003 0.011113024641218336 8.76041935303527e-05 8.168081568291806e-05 0.00046693784586018913 0.4952432605379829 2.2512327083641865e-10 4.6258592060517866e-07 2.7044817176109556e-05 0.054902816619122045 0.00014126153603265097 8.304845400451923e-08 2.2950716170641463e-05 0.0001335621430311088 0.003432919201293737 0.0005197081842709918 5.726152701849347e-16 -1.0 0.0013332554437480327 0.4346984808472855 7.691527781829036e-17 0.00011042047857307253 3.0571050855707084e-06 -1.0 4.986698316296507e-06 0.011128424196387618 1.3106335518133802e-05 5.447493368067179e-10 0.011440381012864086 4.670673837764874e-08 0.12364069326350118 0.00012558879474108074 3.136829920007009e-05 2.34750482787598e-05 0.001537590300149361 0.0017693279949094218 6.717625881826167e-05 4.587143797469485e-08 2.6211239246796104e-05 0.00030002326388383244 0.01729816946709781 4.5757238372058234e-11 6.92296299242814e-05 3.792209399652396e-06 -1.0 9.256616392487858e-05 8.541606727879512e-07 -1.0 0.49317081643076643 0.05500108683629957 1.298062961617927e-05 0.0006870228272606287 0.0008231482263927564 0.5 3.419181071650816e-06 0.06433819434431046 6.574706543646946e-16 4.0404626147344e-13 0.0003782322077390033 0.00012637534695610975 0.017602971074583987 1.0044390728858277e-05 -1.0 0.0008787411098754915 0.00023686818807101741 0.0009304497896205121 6.949587369646497e-05 0.00012918433677593975 2.0707648529746894e-06 0.0023295401596005313 0.11819165666519223 -1.0 2.1606550732170693e-05 0.00022057970761624768 -1.0 1.0325207105027347e-08 0.0002029396545382813 2.1234980618763222e-13 -1.0 0.00024175998138228048 0.001178616843705956 0.0001399807456616496 0.0038811025614225057 0.0011076796092102186 0.00010814452764860558 0.010955637485319249 -1.0 9.382800796426366e-17 5.098747893974248e-05 0.00027052054298385255 0.00021305826462238593 0.000808100901275472 4.233410073487772e-06 0.0051513292132259495 8.616027294555462e-07 0.4873897581604287 2.9093345975075226e-05 0.000469589568463265 -1.0 -1.0 0.017802624844035688 1.3116807133030035e-15 2.617878055106949e-16 0.01884234453311008 -1.0 2.027381220364608e-16 0.0004299195570308035 1.4628847959717225e-05 0.4899952354672477 2.774067049392004e-07 0.004441328419270369 4.279460329782799e-09 0.0012297303081805393 0.0002792896409558745 0.0006934466049804711 8.370896020975689e-05 3.8270536025799805e-15 -1.0 0.01208261392046878 0.0001497958445994315 0.0015016500045374082 0.005384625561469684 0.00025519632243117644 0.0011028020390507682 0.0002362355168421952 -1.0 -1.0 8.862760180332873e-05 1.3806577785135177e-06 -1.0 0.0001319677450153934 2.6866894852481906e-05 -1.0 0.11510120013926417 0.002672923773363389 1.2257360246712472e-05 8.544159282151438e-05 2.777791741571174e-05 0.0014957033281168012 0.0003257646134837451 0.0008919466185572302 1.2907171695294846e-13 4.6842350882367666e-05 0.01721806295736377 0.00025106741008066317 0.00022234471595443313 2.101790478097517e-09 5.752120832330886e-11 0.06428782790571179 3.2478301680021854e-08 0.4903495510314233 0.001146714816952061 0.0004777967757842694 1.0882011018684852e-06 0.05708498338352203 0.4981497862127628 -1.0 3.2790914744011534e-06 0.00010077009726691265 -1.0 2.210707325798542e-07 4.3166052799897375e-05 -1.0 0.01825795827020865 0.0003182108781955478 3.8561696337086434e-05 1.0995322456277382e-06 9.53380658669839e-06 0.001550036616828061 0.0012648333883989995 5.272021975060514e-06 1.9257248009114363e-05 0.00016757997590048827 0.11891251244906934 5.515522196186723e-06 0.01102706140784316 9.904481358675478e-13 3.520674728719278e-05 0.011294708566804167 7.239087311622819e-08 -1.0 3.5740607788414942e-06 0.00035983547999740665 1.0588257623722672e-07 0.42372713683725016 0.0011273340972061078 8.582011716859219e-15 1.1514579117272292e-10 0.00043130977711323417 0.0027847329307350423 0.00017287873387910357 6.401055165502384e-06 1.722191123879782e-08 0.00020848309220162036 0.05639071613735579 1.8064550990294753e-05 1.289814511028015e-06 1.078687446476881e-14 0.4897735047310072 0.0002981891705231209 0.00016079768514218546 9.332371598523186e-05 0.011724714376848187 0.019040707071165303 0.00014242053195529864 0.06684484191345574 -1.0 - -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.6827568359008944 0.0017329528628152924 0.18541112907970003 0.11113024641218336 0.0008760419353035269 0.0008168081568291806 0.004669378458601891 4.952432605379829 2.2512327083641864e-09 4.625859206051786e-06 0.00027044817176109554 0.5490281661912204 0.0014126153603265096 8.304845400451923e-07 0.00022950716170641464 0.001335621430311088 0.03432919201293737 0.0051970818427099184 5.726152701849347e-15 -10.0 0.013332554437480326 4.346984808472855 7.6915277818290365e-16 0.0011042047857307254 3.0571050855707086e-05 -10.0 4.986698316296507e-05 0.11128424196387618 0.000131063355181338 5.447493368067179e-09 0.11440381012864086 4.670673837764874e-07 1.2364069326350118 0.0012558879474108074 0.0003136829920007009 0.000234750482787598 0.01537590300149361 0.01769327994909422 0.0006717625881826168 4.587143797469485e-07 0.000262112392467961 0.0030002326388383245 0.1729816946709781 4.5757238372058235e-10 0.0006922962992428139 3.792209399652396e-05 -10.0 0.0009256616392487858 8.541606727879512e-06 -10.0 4.931708164307665 0.5500108683629957 0.00012980629616179268 0.006870228272606287 0.008231482263927564 5.0 3.419181071650816e-05 0.6433819434431045 6.574706543646946e-15 4.0404626147344005e-12 0.003782322077390033 0.0012637534695610975 0.17602971074583987 0.00010044390728858278 -10.0 0.008787411098754914 0.002368681880710174 0.00930449789620512 0.0006949587369646498 0.0012918433677593976 2.0707648529746893e-05 0.023295401596005315 1.1819165666519222 -10.0 0.00021606550732170693 0.0022057970761624767 -10.0 1.0325207105027347e-07 0.0020293965453828133 2.123498061876322e-12 -10.0 0.0024175998138228046 0.01178616843705956 0.0013998074566164962 0.03881102561422506 0.011076796092102187 0.0010814452764860557 0.10955637485319249 -10.0 9.382800796426367e-16 0.0005098747893974248 0.0027052054298385255 0.0021305826462238594 0.00808100901275472 4.233410073487772e-05 0.0515132921322595 8.61602729455546e-06 4.873897581604287 0.0002909334597507523 0.0046958956846326495 -10.0 -10.0 0.1780262484403569 1.3116807133030034e-14 2.6178780551069492e-15 0.1884234453311008 -10.0 2.027381220364608e-15 0.004299195570308035 0.00014628847959717226 4.899952354672477 2.774067049392004e-06 0.04441328419270369 4.279460329782799e-08 0.012297303081805393 0.002792896409558745 0.006934466049804711 0.0008370896020975689 3.8270536025799805e-14 -10.0 0.1208261392046878 0.001497958445994315 0.015016500045374082 0.05384625561469684 0.0025519632243117644 0.011028020390507681 0.002362355168421952 -10.0 -10.0 0.0008862760180332873 1.3806577785135176e-05 -10.0 0.001319677450153934 0.00026866894852481903 -10.0 1.1510120013926417 0.026729237733633893 0.00012257360246712473 0.0008544159282151438 0.0002777791741571174 0.014957033281168012 0.0032576461348374514 0.008919466185572301 1.2907171695294846e-12 0.00046842350882367664 0.17218062957363772 0.0025106741008066318 0.0022234471595443312 2.101790478097517e-08 5.752120832330886e-10 0.6428782790571179 3.2478301680021856e-07 4.903495510314233 0.01146714816952061 0.004777967757842694 1.0882011018684853e-05 0.5708498338352204 4.981497862127628 -10.0 3.279091474401153e-05 0.0010077009726691265 -10.0 2.210707325798542e-06 0.00043166052799897375 -10.0 0.1825795827020865 0.0031821087819554777 0.00038561696337086434 1.0995322456277382e-05 9.53380658669839e-05 0.01550036616828061 0.012648333883989995 5.272021975060514e-05 0.00019257248009114364 0.0016757997590048828 1.1891251244906933 5.515522196186723e-05 0.1102706140784316 9.904481358675478e-12 0.0003520674728719278 0.11294708566804167 7.239087311622819e-07 -10.0 3.574060778841494e-05 0.0035983547999740664 1.0588257623722672e-06 4.237271368372502 0.011273340972061077 8.582011716859219e-14 1.151457911727229e-09 0.004313097771132342 0.027847329307350423 0.0017287873387910357 6.401055165502385e-05 1.722191123879782e-07 0.0020848309220162036 0.5639071613735579 0.00018064550990294753 1.289814511028015e-05 1.078687446476881e-13 4.897735047310072 0.002981891705231209 0.0016079768514218546 0.0009332371598523186 0.11724714376848187 0.19040707071165303 0.0014242053195529865 0.6684484191345574 -10.0 + -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -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.0445691096074744e-05 -1.0 -1.0 -1.0 0.00013281328509288383 0.0006267128082339883 -1.0 -1.0 -1.0 -1.0 0.0004209808495056742 5.340221214300681e-05 -1.0 -1.0 -1.0 1.553693492042344e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 3.7096847855980484e-05 -1.0 -1.0 5.41969614042385e-05 0.0007918090215756442 9.763572147337743e-05 -1.0 -1.0 0.0026398319229115233 0.04039871468258457 0.002806654735003014 -1.0 -1.0 0.0027608366482897244 0.040190978087723005 0.0025084416103979975 -1.0 -1.0 9.774868433298412e-05 0.0006126404916879651 0.00014841730774317252 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 9.75342408717107e-06 1.7610849405640072e-05 -1.0 -1.0 0.0001160370359598608 0.002822592083222468 0.0003897177301239376 -1.0 -1.0 0.014965179733172532 0.5 0.011723491704290401 -1.0 2.260493623875525e-05 0.015157364795884922 0.49376993953402387 0.013111419473204967 -1.0 -1.0 0.00014919915366377564 0.002197964829133137 0.0003209711829219673 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 1.4806422867046436e-05 -1.0 -1.0 -1.0 9.207268175745281e-05 0.0007066033561638983 -1.0 -1.0 -1.0 0.003246064903858183 0.03905493028065908 0.0025380574501073605 -1.0 -1.0 0.0032499260211917933 0.04335567738779479 0.0031577214557017056 4.521352809838806e-05 -1.0 0.00018268090756001903 0.0004353654810741932 0.00011273259573092372 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 9.650194311662424e-06 0.00029403373970835075 0.00013506203419326438 -1.0 -1.0 4.5065302725431816e-06 0.00027725323638744237 3.547290864255937e-05 -1.0 -1.0 -1.0 8.786172732576295e-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 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 + -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00020445691096074745 -10.0 -10.0 -10.0 0.0013281328509288383 0.006267128082339883 -10.0 -10.0 -10.0 -10.0 0.004209808495056742 0.0005340221214300681 -10.0 -10.0 -10.0 0.00015536934920423439 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00037096847855980485 -10.0 -10.0 0.000541969614042385 0.007918090215756443 0.0009763572147337743 -10.0 -10.0 0.026398319229115234 0.4039871468258457 0.02806654735003014 -10.0 -10.0 0.027608366482897245 0.40190978087723006 0.025084416103979976 -10.0 -10.0 0.0009774868433298411 0.0061264049168796506 0.0014841730774317252 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 9.753424087171071e-05 0.00017610849405640073 -10.0 -10.0 0.001160370359598608 0.02822592083222468 0.003897177301239376 -10.0 -10.0 0.14965179733172532 5.0 0.11723491704290401 -10.0 0.0002260493623875525 0.15157364795884923 4.937699395340239 0.13111419473204966 -10.0 -10.0 0.0014919915366377564 0.02197964829133137 0.003209711829219673 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00014806422867046437 -10.0 -10.0 -10.0 0.0009207268175745281 0.007066033561638983 -10.0 -10.0 -10.0 0.03246064903858183 0.39054930280659084 0.025380574501073606 -10.0 -10.0 0.03249926021191793 0.4335567738779479 0.03157721455701706 0.0004521352809838806 -10.0 0.0018268090756001904 0.004353654810741932 0.001127325957309237 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 9.650194311662424e-05 0.0029403373970835075 0.0013506203419326437 -10.0 -10.0 4.5065302725431816e-05 0.002772532363874424 0.0003547290864255937 -10.0 -10.0 -10.0 0.0008786172732576295 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 3.0 1.5 10 1e-38 + + true + true + 200 diff --git a/tests/regression_tests/weightwindows/results_true.dat b/tests/regression_tests/weightwindows/results_true.dat index 122c5d890..897089e05 100644 --- a/tests/regression_tests/weightwindows/results_true.dat +++ b/tests/regression_tests/weightwindows/results_true.dat @@ -1 +1 @@ -5df0c08573ccee3fd3495c877c7dc0c4b69c245faf8473b848c89fb837dea7fd437936ffebeb6455c793e66046c46eb0ad6941cc5ef1699fc5fc8a7fcb9b5a0c \ No newline at end of file +a4a3ccca43666e2ca1e71800201b152cca20c387b93d67522c5339807348dcee5cada9acbed3238f37e2e86e76b374b06988742f07d4ea1b413e4e75d0c180b1 \ No newline at end of file diff --git a/tests/regression_tests/weightwindows/test.py b/tests/regression_tests/weightwindows/test.py index cfb651338..c22ca1b75 100644 --- a/tests/regression_tests/weightwindows/test.py +++ b/tests/regression_tests/weightwindows/test.py @@ -7,7 +7,6 @@ from openmc.stats import Discrete, Point from tests.testing_harness import HashedPyAPITestHarness - @pytest.fixture def model(): model = openmc.Model() @@ -16,7 +15,6 @@ def model(): m4 = openmc.Material() m4.set_density('g/cc', 2.3) m4.add_nuclide('H1', 0.168018676) - m4.add_nuclide("H2", 1.93244e-05) m4.add_nuclide("O16", 0.561814465) m4.add_nuclide("O17", 0.00021401) m4.add_nuclide("Na23", 0.021365) @@ -26,14 +24,9 @@ def model(): m4.add_nuclide("Si30", 0.006273944) m4.add_nuclide("Ca40", 0.018026179) m4.add_nuclide("Ca42", 0.00012031) - m4.add_nuclide("Ca43", 2.51033e-05) m4.add_nuclide("Ca44", 0.000387892) - m4.add_nuclide("Ca46", 7.438e-07) - m4.add_nuclide("Ca48", 3.47727e-05) m4.add_nuclide("Fe54", 0.000248179) m4.add_nuclide("Fe56", 0.003895875) - m4.add_nuclide("Fe57", 8.99727e-05) - m4.add_nuclide("Fe58", 1.19737e-05) s0 = openmc.Sphere(r=240) s1 = openmc.Sphere(r=250, boundary_type='vacuum') @@ -46,10 +39,12 @@ def model(): # settings settings = model.settings settings.run_mode = 'fixed source' - settings.particles = 200 + settings.particles = 500 settings.batches = 2 settings.max_history_splits = 200 settings.photon_transport = True + settings.weight_window_checkpoints = {'surface': True, + 'collision': True} space = Point((0.001, 0.001, 0.001)) energy = Discrete([14E6], [1.0]) @@ -93,6 +88,7 @@ def model(): None, 10.0, e_bnds, + 'neutron', max_lower_bound_ratio=1.5) ww_p = openmc.WeightWindows(ww_mesh, @@ -100,6 +96,7 @@ def model(): None, 10.0, e_bnds, + 'photon', max_lower_bound_ratio=1.5) model.settings.weight_windows = [ww_n, ww_p] diff --git a/tests/regression_tests/weightwindows/ww_n.txt b/tests/regression_tests/weightwindows/ww_n.txt index dbb49537b..36d7673c6 100644 --- a/tests/regression_tests/weightwindows/ww_n.txt +++ b/tests/regression_tests/weightwindows/ww_n.txt @@ -10,43 +10,30 @@ -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -7.695031155172815952e-14 -1.000000000000000000e+00 -7.476545089278482294e-06 -1.000000000000000000e+00 -2.161215042522170282e-06 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -2.136040178434497494e-18 -1.000000000000000000e+00 -1.345534130616238158e-05 -1.000000000000000000e+00 -3.353295403842037074e-05 -1.000000000000000000e+00 -3.016075845432365699e-07 -1.000000000000000000e+00 -1.026243155073153453e-13 -1.000000000000000000e+00 -1.685921961405802360e-19 -1.000000000000000000e+00 -3.853097455417877125e-06 -1.000000000000000000e+00 -4.354946981329799215e-05 -1.000000000000000000e+00 -7.002861620919231721e-08 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.620506488302619488e-11 -1.000000000000000000e+00 -1.704166922479738429e-09 -1.000000000000000000e+00 -1.874782466747863741e-14 -1.000000000000000000e+00 -1.000000000000000000e+00 +5.465752192773127422e-05 +5.016807145936662541e-06 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 @@ -60,301 +47,329 @@ -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -5.619835883512352867e-14 -1.000000000000000000e+00 -1.951356547084204088e-15 -1.000000000000000000e+00 -8.481761456712739755e-10 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.450826207325665860e-13 -1.000000000000000000e+00 -4.109066205029878035e-05 -1.000000000000000000e+00 -2.057318145024078612e-04 -1.000000000000000000e+00 -2.124999182004654892e-05 -1.000000000000000000e+00 -5.537915413446233643e-08 -1.000000000000000000e+00 -1.555045595917848555e-06 -1.000000000000000000e+00 -7.213722022489493227e-04 -1.000000000000000000e+00 -7.673145137236566868e-03 -1.000000000000000000e+00 -8.175033526488423340e-04 -1.000000000000000000e+00 -1.008394964750946906e-06 -1.000000000000000000e+00 -6.848642863465584904e-07 -1.000000000000000000e+00 -1.073708183250235556e-03 -1.000000000000000000e+00 -7.197662162700776967e-03 -1.000000000000000000e+00 -7.151459162818185784e-04 -1.000000000000000000e+00 -2.253382683081525085e-06 -1.000000000000000000e+00 -1.318156235244509166e-10 -1.000000000000000000e+00 -1.938470818929409920e-05 -1.000000000000000000e+00 -4.602961711512755646e-04 -1.000000000000000000e+00 -2.562906833044621533e-05 -1.000000000000000000e+00 -5.504813693036932910e-12 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -3.316482534950376351e-16 -1.000000000000000000e+00 -3.937160538176268057e-07 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.517291205731239772e-14 -1.000000000000000000e+00 -6.315154082213375425e-07 -1.000000000000000000e+00 -3.855884234344844889e-06 -1.000000000000000000e+00 -1.777013641125591191e-05 -1.000000000000000000e+00 -1.642149199375171522e-11 -1.000000000000000000e+00 -5.976538141107809983e-06 -1.000000000000000000e+00 -1.959411999677113103e-03 -1.000000000000000000e+00 -1.333061985337931354e-02 -1.000000000000000000e+00 -1.795061316935990395e-03 -1.000000000000000000e+00 -2.430637569372220458e-06 -1.000000000000000000e+00 -6.922231352729155390e-05 -1.000000000000000000e+00 -8.657573566388353237e-02 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -8.356487100743359431e-02 -1.000000000000000000e+00 -1.641903115049591277e-04 -1.000000000000000000e+00 -9.031085518264690009e-05 -1.000000000000000000e+00 -8.488729673818780352e-02 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -8.288806458368344621e-02 +1.662027112506702498e-05 +1.072323185129836366e-05 +1.298973206651331117e-04 -1.000000000000000000e+00 -5.694959706678497628e-05 -1.000000000000000000e+00 -8.037131813528500897e-07 -1.000000000000000000e+00 -1.663712654332187308e-03 -1.000000000000000000e+00 -1.664109232689093068e-02 -1.000000000000000000e+00 -1.832759343760799985e-03 -1.000000000000000000e+00 -6.542700644645626997e-07 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -6.655926357459274907e-08 -1.000000000000000000e+00 -5.783974359937856763e-06 +4.045301818607421548e-05 +2.436300468111906627e-03 +8.030100296698905113e-04 +1.467083972914635416e-02 +5.588863190166347417e-03 +2.482827339043196246e-03 +9.134587866163143164e-04 +1.127637299558987091e-04 -1.000000000000000000e+00 -2.679340942769232109e-06 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -2.104060901286513389e-05 -1.000000000000000000e+00 -2.928209794781622450e-05 +1.096572213298871923e-03 +1.035684113269689892e-03 +1.484649108252352433e-02 +5.163345217476070746e-03 +3.034165905819096072e-03 +7.973472495507652599e-04 +1.596955110781238946e-04 -1.000000000000000000e+00 -1.284759166582201940e-05 -1.000000000000000000e+00 -1.009290508315880392e-11 -1.000000000000000000e+00 -1.322801376552501482e-05 -1.000000000000000000e+00 -7.136750029575815620e-03 -1.000000000000000000e+00 -6.539005235506369085e-02 -1.000000000000000000e+00 -6.480331412016533503e-03 -1.000000000000000000e+00 -2.601178739391680563e-06 -1.000000000000000000e+00 -8.887234042637683843e-05 -1.000000000000000000e+00 +5.146579891055349870e-05 +3.371977841720992210e-05 +-1.000000000000000000e+00 +6.170015233787291603e-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 +6.278777700923334273e-04 +-1.000000000000000000e+00 +2.882628152998065532e-03 +1.425775692742982772e-03 +9.344129479768359435e-04 +1.878729106973870018e-04 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +8.758251780046590789e-04 +4.601329011012189428e-05 +4.404124277352226835e-02 +1.738622047618722244e-02 +4.999999999999999445e-01 5.000000000000000000e-01 +4.299740304329489199e-02 +1.708185807868446704e-02 +2.236100157024886888e-04 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -4.877856021170283163e-01 -1.000000000000000000e+00 -1.769985835774446302e-04 -1.000000000000000000e+00 -1.670700632870335029e-04 +4.449401731904285297e-04 +9.538410811938702620e-05 +4.862206884590390688e-02 +1.560907112400923384e-02 +4.897211700090107755e-01 +4.825075099388732580e-01 +4.870927636605937305e-02 +1.930090325480974742e-02 +9.960576598335083693e-06 -1.000000000000000000e+00 -4.899999304038166192e-01 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -4.857384221387814338e-01 -1.000000000000000000e+00 -2.153456869915780514e-04 -1.000000000000000000e+00 -7.842896894949250204e-06 -1.000000000000000000e+00 -6.379065450759913505e-03 +2.920495077727233452e-04 +6.979537549963373846e-05 +1.011527843820496462e-03 +6.640103276501793748e-04 +6.339910999436736800e-05 +1.898984300674969044e-05 -1.000000000000000000e+00 -6.550426960512012453e-02 -1.000000000000000000e+00 -6.639379668946672301e-03 -1.000000000000000000e+00 -7.754700849337902260e-06 -1.000000000000000000e+00 -2.720743316579670241e-11 -1.000000000000000000e+00 -5.985108617124988932e-06 -1.000000000000000000e+00 -3.634644995026691954e-05 -1.000000000000000000e+00 -3.932389299316284718e-06 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.271128669411294917e-07 -1.000000000000000000e+00 -8.846493399850662704e-06 -1.000000000000000000e+00 -2.499931357628383163e-07 -1.000000000000000000e+00 -8.533030345699353466e-17 -1.000000000000000000e+00 -1.669925427773410862e-06 -1.000000000000000000e+00 -1.754138009689378778e-03 -1.000000000000000000e+00 -1.514897842827161306e-02 -1.000000000000000000e+00 -1.627979455242757438e-03 -1.000000000000000000e+00 -2.380955631371225994e-06 -1.000000000000000000e+00 -2.164891804046736287e-05 -1.000000000000000000e+00 -8.339317973815890683e-02 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -8.285899874014539257e-02 -1.000000000000000000e+00 -2.762955748645507000e-05 -1.000000000000000000e+00 -5.116174051620571477e-05 -1.000000000000000000e+00 -8.095280568305474045e-02 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -8.636569925236790846e-02 -1.000000000000000000e+00 -3.452537179033895228e-05 -1.000000000000000000e+00 -3.598103176641614321e-06 -1.000000000000000000e+00 -1.570829313580840913e-03 -1.000000000000000000e+00 -1.621821782442112170e-02 -1.000000000000000000e+00 -1.544782399547004175e-03 +3.381665181434415378e-05 +-1.000000000000000000e+00 +1.894132653585093196e-04 +5.909807053955946611e-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 +2.709738801641897098e-05 +2.389900091734746025e-03 +5.027758935317528299e-04 +1.127106059276566409e-02 +3.930588100414563434e-03 +2.053907925255511312e-03 +2.972747357542354098e-04 +9.563042989132649486e-08 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +1.992978141244964075e-05 +1.153005443211333218e-03 +7.473731339616587841e-04 +1.328487445181072285e-02 +3.510432129522240985e-03 +3.131016680862970091e-03 +9.022030322806940360e-04 +5.383303062315367556e-05 +1.017391376549009493e-05 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +2.233284599138542407e-05 +-1.000000000000000000e+00 +8.429411802845684617e-04 +9.554899988419712775e-05 +7.442176086066385897e-05 +9.854115835957219834e-07 +-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 -7.989579285134733983e-06 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -2.594951085377587857e-06 -1.000000000000000000e+00 -4.323138781337962887e-05 -1.000000000000000000e+00 -6.948292975998465777e-07 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -2.446945600849361415e-09 -1.000000000000000000e+00 -9.041149893307805599e-07 -1.000000000000000000e+00 -2.206099258062212484e-11 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.681305446488883933e-11 -1.000000000000000000e+00 -7.091263906557290927e-05 -1.000000000000000000e+00 -2.746424406288768276e-04 -1.000000000000000000e+00 -1.285845570921339974e-05 -1.000000000000000000e+00 -2.029521362171670568e-09 -1.000000000000000000e+00 -1.291182719850037242e-08 -1.000000000000000000e+00 -9.127050763987511325e-04 -1.000000000000000000e+00 -7.385384405891665810e-03 -1.000000000000000000e+00 -1.001943240150808675e-03 -1.000000000000000000e+00 -1.080711295768550941e-05 -1.000000000000000000e+00 -8.020767115181573972e-07 -1.000000000000000000e+00 -9.252532821720597109e-04 -1.000000000000000000e+00 -7.188410694198127913e-03 -1.000000000000000000e+00 -7.245451610090619275e-04 -1.000000000000000000e+00 -2.515735579020177281e-07 -1.000000000000000000e+00 -1.482521919913335348e-12 -1.000000000000000000e+00 -4.756872959630257218e-05 -1.000000000000000000e+00 -2.668579837809080925e-04 -1.000000000000000000e+00 -2.822627694453269613e-05 -1.000000000000000000e+00 -5.182269672663266089e-10 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -2.936268747135548187e-14 -1.000000000000000000e+00 -2.544728124173036570e-07 -1.000000000000000000e+00 -3.898416671385980035e-08 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 @@ -370,41 +385,27 @@ -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -2.639268952045578987e-10 -1.000000000000000000e+00 -3.705151822008388643e-06 -1.000000000000000000e+00 -1.133362760482367266e-09 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.229674726063540458e-06 -1.000000000000000000e+00 -7.962899789920809096e-06 -1.000000000000000000e+00 -8.211982683649991481e-09 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -2.563965686625045276e-06 -1.000000000000000000e+00 -2.248761782893832567e-05 -1.000000000000000000e+00 -3.279904588337207471e-06 -1.000000000000000000e+00 -4.076762579823379031e-17 -1.000000000000000000e+00 -1.547676923757129466e-14 -1.000000000000000000e+00 -1.030567269874565733e-11 -1.000000000000000000e+00 -1.957092814065688094e-05 -1.000000000000000000e+00 -3.852324755037881526e-12 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 @@ -412,7 +413,6 @@ -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -2.769723317139941641e-13 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 diff --git a/tests/regression_tests/weightwindows/ww_p.txt b/tests/regression_tests/weightwindows/ww_p.txt index 09ff01ebd..da69b9e13 100644 --- a/tests/regression_tests/weightwindows/ww_p.txt +++ b/tests/regression_tests/weightwindows/ww_p.txt @@ -1,205 +1,241 @@ -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -5.447493368067179064e-10 -1.000000000000000000e+00 -4.040462614734400249e-13 -1.000000000000000000e+00 -9.382800796426366364e-17 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -5.752120832330885919e-11 -1.000000000000000000e+00 -3.520674728719278012e-05 -1.000000000000000000e+00 -8.760419353035269667e-05 -1.000000000000000000e+00 -3.136829920007008993e-05 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -4.233410073487771971e-06 -1.000000000000000000e+00 -2.551963224311764450e-04 -1.000000000000000000e+00 -4.777967757842694024e-04 -1.000000000000000000e+00 -3.598354799974066488e-04 -1.000000000000000000e+00 -4.625859206051786618e-07 -1.000000000000000000e+00 -4.587143797469485258e-08 -1.000000000000000000e+00 -1.291843367759397518e-04 -1.000000000000000000e+00 -4.695895684632649786e-04 -1.000000000000000000e+00 -8.862760180332873163e-05 -1.000000000000000000e+00 -3.279091474401153391e-06 -1.000000000000000000e+00 -1.151457911727229161e-10 -1.000000000000000000e+00 -2.295071617064146262e-05 -1.000000000000000000e+00 -6.922962992428139458e-05 -1.000000000000000000e+00 -2.160655073217069276e-05 -1.000000000000000000e+00 -2.617878055106949035e-16 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.722191123879781983e-08 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -2.123498061876322157e-13 -1.000000000000000000e+00 -1.462884795971722476e-05 -1.000000000000000000e+00 -2.777791741571173956e-05 -1.000000000000000000e+00 -9.533806586698390583e-06 -1.000000000000000000e+00 -1.078687446476880940e-14 -1.000000000000000000e+00 -3.057105085570708401e-06 -1.000000000000000000e+00 -8.231482263927564300e-04 -1.000000000000000000e+00 -3.881102561422505696e-03 -1.000000000000000000e+00 -1.229730308180539307e-03 -1.000000000000000000e+00 -4.684235088236766580e-05 -1.000000000000000000e+00 -1.675799759004882713e-04 -1.000000000000000000e+00 -1.172471437684818700e-02 +2.260493623875524987e-05 -1.000000000000000000e+00 -6.827568359008943932e-02 -1.000000000000000000e+00 -1.144038101286408600e-02 -1.000000000000000000e+00 -3.782322077390032930e-04 -1.000000000000000000e+00 -5.098747893974247782e-05 -1.000000000000000000e+00 -1.208261392046878005e-02 -1.000000000000000000e+00 -6.428782790571178907e-02 -1.000000000000000000e+00 -1.129470856680416663e-02 -1.000000000000000000e+00 -8.168081568291806404e-05 -1.000000000000000000e+00 -2.347504827875979927e-05 -1.000000000000000000e+00 -8.787411098754914583e-04 -1.000000000000000000e+00 -5.151329213225949548e-03 -1.000000000000000000e+00 -1.102802039050768210e-03 -1.000000000000000000e+00 -1.088201101868485208e-06 -1.000000000000000000e+00 -1.058825762372267240e-07 -1.000000000000000000e+00 -2.704481717610955638e-05 -1.000000000000000000e+00 -2.621123924679610439e-05 -1.000000000000000000e+00 -2.070764852974689408e-06 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.380657778513517658e-06 -1.000000000000000000e+00 -1.007700972669126494e-04 -1.000000000000000000e+00 -4.313097771132341687e-04 -1.000000000000000000e+00 -1.335621430311087867e-04 -1.000000000000000000e+00 -3.792209399652395984e-06 -1.000000000000000000e+00 -2.205797076162476780e-04 -1.000000000000000000e+00 -1.884234453311008084e-02 -1.000000000000000000e+00 -1.151012001392641704e-01 -1.000000000000000000e+00 -1.825795827020864834e-02 -1.000000000000000000e+00 -2.084830922016203648e-04 -1.000000000000000000e+00 -1.333255443748032664e-03 -1.000000000000000000e+00 -4.931708164307664344e-01 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -4.899952354672477139e-01 -1.000000000000000000e+00 -1.495703328116801227e-03 -1.000000000000000000e+00 -1.550036616828060930e-03 -1.000000000000000000e+00 -4.897735047310072254e-01 -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 +5.419696140423850144e-05 +-1.000000000000000000e+00 +1.160370359598607949e-04 +-1.000000000000000000e+00 +9.207268175745281452e-05 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +1.328132850928838340e-04 +-1.000000000000000000e+00 +2.639831922911523281e-03 +-1.000000000000000000e+00 +1.496517973317253170e-02 +-1.000000000000000000e+00 +3.246064903858183071e-03 +-1.000000000000000000e+00 +9.650194311662423557e-06 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +2.760836648289724440e-03 +-1.000000000000000000e+00 +1.515736479588492176e-02 +-1.000000000000000000e+00 +3.249926021191793316e-03 +-1.000000000000000000e+00 +4.506530272543181565e-06 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +9.774868433298411941e-05 +-1.000000000000000000e+00 +1.491991536637756388e-04 +-1.000000000000000000e+00 +1.826809075600190318e-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 +-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 +9.753424087171070478e-06 +-1.000000000000000000e+00 +1.480642286704643632e-05 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +2.044569109607474437e-05 +-1.000000000000000000e+00 +7.918090215756442467e-04 +-1.000000000000000000e+00 +2.822592083222468049e-03 +-1.000000000000000000e+00 +7.066033561638982710e-04 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +-1.000000000000000000e+00 +6.267128082339882807e-04 +-1.000000000000000000e+00 +4.039871468258456749e-02 +-1.000000000000000000e+00 5.000000000000000000e-01 -1.000000000000000000e+00 -1.107679609210218599e-03 +3.905493028065908090e-02 -1.000000000000000000e+00 -2.792896409558744781e-04 +2.940337397083507467e-04 -1.000000000000000000e+00 -1.721806295736377085e-02 -1.000000000000000000e+00 -1.189125124490693353e-01 -1.000000000000000000e+00 -1.904070707116530328e-02 -1.000000000000000000e+00 -1.732952862815292309e-04 -1.000000000000000000e+00 -4.670673837764873805e-08 +4.209808495056741907e-04 -1.000000000000000000e+00 -1.263753469561097525e-04 +4.019097808772300467e-02 -1.000000000000000000e+00 -2.705205429838525473e-04 +4.937699395340238717e-01 -1.000000000000000000e+00 -1.497958445994315019e-04 +4.335567738779479152e-02 -1.000000000000000000e+00 -3.247830168002185438e-08 +2.772532363874423726e-04 -1.000000000000000000e+00 -7.239087311622818554e-08 -1.000000000000000000e+00 -4.669378458601891341e-04 -1.000000000000000000e+00 -1.537590300149361015e-03 -1.000000000000000000e+00 -2.368681880710174145e-04 -1.000000000000000000e+00 -8.616027294555461632e-07 +1.553693492042343928e-05 -1.000000000000000000e+00 -2.362355168421952122e-04 +6.126404916879650987e-04 -1.000000000000000000e+00 -5.708498338352203244e-02 +2.197964829133136882e-03 -1.000000000000000000e+00 -4.237271368372501623e-01 +4.353654810741931802e-04 -1.000000000000000000e+00 -5.490281661912204542e-02 +8.786172732576294898e-05 -1.000000000000000000e+00 -3.000232638838324392e-04 -1.000000000000000000e+00 -2.329540159600531311e-03 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 @@ -207,9 +243,7 @@ -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -2.784732930735042255e-03 -1.000000000000000000e+00 -3.432919201293736875e-03 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 @@ -217,204 +251,170 @@ -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -2.672923773363389179e-03 -1.000000000000000000e+00 -3.182108781955477806e-04 -1.000000000000000000e+00 -5.639071613735578692e-02 -1.000000000000000000e+00 -4.346984808472855177e-01 -1.000000000000000000e+00 -5.500108683629956891e-02 +3.709684785598048377e-05 -1.000000000000000000e+00 -2.417599813822804812e-04 +1.761084940564007186e-05 -1.000000000000000000e+00 -2.774067049392004042e-07 -1.000000000000000000e+00 -3.257646134837451210e-04 -1.000000000000000000e+00 -1.264833388398999524e-03 -1.000000000000000000e+00 -2.981891705231208794e-04 -1.000000000000000000e+00 -4.986698316296506713e-06 -1.000000000000000000e+00 -3.419181071650815928e-06 -1.000000000000000000e+00 -1.081445276486055809e-04 -1.000000000000000000e+00 -6.934466049804711161e-04 -1.000000000000000000e+00 -2.510674100806631662e-04 -1.000000000000000000e+00 -5.515522196186722918e-06 -1.000000000000000000e+00 -1.424205319552986396e-04 +9.763572147337742902e-05 -1.000000000000000000e+00 -1.854111290797000322e-02 +3.897177301239375825e-04 -1.000000000000000000e+00 -1.236406932635011752e-01 -1.000000000000000000e+00 -1.760297107458398680e-02 -1.000000000000000000e+00 -2.130582646223859260e-04 -1.000000000000000000e+00 -1.501650004537408200e-03 -1.000000000000000000e+00 -4.903495510314233030e-01 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -4.952432605379828989e-01 -1.000000000000000000e+00 -1.769327994909421784e-03 -1.000000000000000000e+00 -9.304497896205120647e-04 -1.000000000000000000e+00 -4.873897581604286766e-01 +2.806654735003014187e-03 -1.000000000000000000e+00 +1.172349170429040112e-02 -1.000000000000000000e+00 +2.538057450107360485e-03 -1.000000000000000000e+00 -4.981497862127627907e-01 +1.350620341932643788e-04 -1.000000000000000000e+00 -1.127334097206107765e-03 -1.000000000000000000e+00 -1.412615360326509664e-04 -1.000000000000000000e+00 -1.729816946709781048e-02 -1.000000000000000000e+00 -1.181916566651922268e-01 -1.000000000000000000e+00 -1.780262484403568810e-02 +5.340221214300680991e-05 -1.000000000000000000e+00 -1.319677450153934042e-04 +2.508441610397997481e-03 -1.000000000000000000e+00 -2.210707325798542070e-07 +1.311141947320496742e-02 -1.000000000000000000e+00 -1.728787338791035720e-04 +3.157721455701705589e-03 -1.000000000000000000e+00 -5.197081842709918220e-04 +3.547290864255936898e-05 -1.000000000000000000e+00 -9.256616392487857544e-05 -1.000000000000000000e+00 -1.032520710502734699e-08 -1.000000000000000000e+00 -2.027381220364608057e-16 -1.000000000000000000e+00 -1.225736024671247218e-05 -1.000000000000000000e+00 -3.856169633708643398e-05 -1.000000000000000000e+00 -1.806455099029475317e-05 -1.000000000000000000e+00 -7.691527781829036254e-17 +1.484173077431725238e-04 -1.000000000000000000e+00 -1.298062961617926916e-05 +3.209711829219673144e-04 -1.000000000000000000e+00 -1.178616843705955963e-03 +1.127325957309237157e-04 -1.000000000000000000e+00 -4.441328419270368887e-03 -1.000000000000000000e+00 -8.919466185572301909e-04 -1.000000000000000000e+00 -5.272021975060514019e-06 -1.000000000000000000e+00 -1.607976851421854571e-04 -1.000000000000000000e+00 -1.112842419638761758e-02 -1.000000000000000000e+00 -6.433819434431045647e-02 -1.000000000000000000e+00 -1.095563748531924904e-02 -1.000000000000000000e+00 -8.370896020975688591e-05 -1.000000000000000000e+00 -2.223447159544331337e-04 -1.000000000000000000e+00 -1.102706140784315975e-02 -1.000000000000000000e+00 -6.684484191345574366e-02 -1.000000000000000000e+00 -1.111302464121833623e-02 -1.000000000000000000e+00 -1.255887947410807431e-04 -1.000000000000000000e+00 -1.004439072885827748e-05 -1.000000000000000000e+00 -8.081009012754719698e-04 -1.000000000000000000e+00 -5.384625561469683769e-03 -1.000000000000000000e+00 -1.146714816952060971e-03 -1.000000000000000000e+00 -3.574060778841494232e-06 -1.000000000000000000e+00 -2.251232708364186507e-10 -1.000000000000000000e+00 -6.717625881826167172e-05 -1.000000000000000000e+00 -6.949587369646497293e-05 -1.000000000000000000e+00 -2.909334597507522605e-05 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -8.582011716859218622e-15 -1.000000000000000000e+00 -8.304845400451923449e-08 -1.000000000000000000e+00 -4.575723837205823402e-11 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.311680713303003522e-15 -1.000000000000000000e+00 -2.686689485248190559e-05 -1.000000000000000000e+00 -4.316605279989737501e-05 -1.000000000000000000e+00 -6.401055165502384025e-06 -1.000000000000000000e+00 -5.726152701849347207e-16 -1.000000000000000000e+00 -8.541606727879512040e-07 -1.000000000000000000e+00 -2.029396545382813118e-04 -1.000000000000000000e+00 -4.299195570308034968e-04 -1.000000000000000000e+00 -8.544159282151438236e-05 -1.000000000000000000e+00 -1.099532245627738225e-06 -1.000000000000000000e+00 -1.289814511028014912e-06 -1.000000000000000000e+00 -1.104204785730725317e-04 -1.000000000000000000e+00 -6.870228272606287295e-04 -1.000000000000000000e+00 -1.399807456616496116e-04 -1.000000000000000000e+00 -4.279460329782799377e-09 -1.000000000000000000e+00 -1.290717169529484602e-13 -1.000000000000000000e+00 -1.925724800911436302e-05 -1.000000000000000000e+00 -9.332371598523186474e-05 -1.000000000000000000e+00 -1.310633551813380206e-05 -1.000000000000000000e+00 -6.574706543646946099e-16 -1.000000000000000000e+00 -1.000000000000000000e+00 -1.000000000000000000e+00 -3.827053602579980514e-15 -1.000000000000000000e+00 -2.101790478097516960e-09 -1.000000000000000000e+00 -9.904481358675477728e-13 +-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 +4.521352809838806062e-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 From 8198e6021dae83e4e9519108ca1483ad656f06a4 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 12 Feb 2026 10:25:56 -0600 Subject: [PATCH 540/671] Add truncated normal distribution support (#3761) --- .../parameterized_source_ring.cpp | 4 +- include/openmc/distribution.h | 27 +++-- include/openmc/math_functions.h | 10 ++ openmc/stats/univariate.py | 105 +++++++++++++++--- src/distribution.cpp | 74 +++++++++++- src/math_functions.cpp | 7 ++ tests/cpp_unit_tests/test_distribution.cpp | 100 +++++++++++++++++ tests/unit_tests/test_stats.py | 86 ++++++++++++++ 8 files changed, 385 insertions(+), 28 deletions(-) diff --git a/examples/parameterized_custom_source/parameterized_source_ring.cpp b/examples/parameterized_custom_source/parameterized_source_ring.cpp index a983414f1..9756e1b97 100644 --- a/examples/parameterized_custom_source/parameterized_source_ring.cpp +++ b/examples/parameterized_custom_source/parameterized_source_ring.cpp @@ -1,5 +1,5 @@ -#include // for M_PI -#include // for unique_ptr +#include +#include #include #include "openmc/particle.h" diff --git a/include/openmc/distribution.h b/include/openmc/distribution.h index 80fe70bae..cd81d8801 100644 --- a/include/openmc/distribution.h +++ b/include/openmc/distribution.h @@ -265,23 +265,29 @@ private: }; //============================================================================== -//! Normal distributions with form 1/2*std_dev*sqrt(pi) exp -//! (-(e-E0)/2*std_dev)^2 +//! Normal distribution with optional truncation bounds. +//! +//! The standard normal PDF is 1/(sqrt(2*pi)*sigma) * +//! exp(-(x-mu)^2/(2*sigma^2)). When truncated to [lower, upper], the PDF is +//! renormalized so that it integrates to 1 over the truncation interval. //============================================================================== class Normal : public Distribution { public: explicit Normal(pugi::xml_node node); - Normal(double mean_value, double std_dev) - : mean_value_ {mean_value}, std_dev_ {std_dev} {}; + Normal(double mean_value, double std_dev, double lower = -INFTY, + double upper = INFTY); //! Evaluate probability density, f(x), at a point //! \param x Point to evaluate f(x) - //! \return f(x) + //! \return f(x), accounting for truncation normalization double evaluate(double x) const override; double mean_value() const { return mean_value_; } double std_dev() const { return std_dev_; } + double lower() const { return lower_; } + double upper() const { return upper_; } + bool is_truncated() const { return is_truncated_; } protected: //! Sample a value (unbiased) from the distribution @@ -290,8 +296,15 @@ protected: double sample_unbiased(uint64_t* seed) const override; private: - double mean_value_; //!< middle of distribution [eV] - double std_dev_; //!< standard deviation [eV] + double mean_value_; //!< Mean of distribution + double std_dev_; //!< Standard deviation + double lower_; //!< Lower truncation bound (default: -INFTY) + double upper_; //!< Upper truncation bound (default: +INFTY) + bool is_truncated_; //!< True if bounds are finite + double norm_factor_; //!< Normalization factor for truncated distribution + + //! Compute normalization factor for truncated distribution + void compute_normalization(); }; //============================================================================== diff --git a/include/openmc/math_functions.h b/include/openmc/math_functions.h index d67b8305e..dcc0e21fe 100644 --- a/include/openmc/math_functions.h +++ b/include/openmc/math_functions.h @@ -223,5 +223,15 @@ double log1prel(double x); void get_energy_index( const vector& energies, double E, int& i, double& f); +//============================================================================== +//! Calculate the cumulative distribution function of the standard normal +//! distribution at a given value. +//! +//! \param z The value at which to evaluate the CDF +//! \return Phi(z) = P(X <= z) for X ~ N(0,1) +//============================================================================== + +double standard_normal_cdf(double z); + } // namespace openmc #endif // OPENMC_MATH_FUNCTIONS_H diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index 3d46024a4..c6ce217fb 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -1049,18 +1049,27 @@ class Watt(Univariate): class Normal(Univariate): - r"""Normally distributed sampling. + r"""Normally distributed sampling with optional truncation. - The Normal Distribution is characterized by two parameters - :math:`\mu` and :math:`\sigma` and has density function - :math:`p(X) dX = 1/(\sqrt{2\pi}\sigma) e^{-(X-\mu)^2/(2\sigma^2)}` + The normal distribution is characterized by parameters :math:`\mu` and + :math:`\sigma` and has density function :math:`p(X) = 1/(\sqrt{2\pi}\sigma) + e^{-(X-\mu)^2/(2\sigma^2)}`. When truncated to the interval [lower, upper], + the distribution is renormalized so that the PDF integrates to 1 over the + truncation interval. + + .. versionchanged:: 0.15.4 + Added optional truncation bounds via `lower` and `upper` parameters. Parameters ---------- mean_value : float - Mean value of the distribution + Mean value of the distribution std_dev : float Standard deviation of the Normal distribution + lower : float, optional + Lower truncation bound. Defaults to -infinity (no lower bound). + upper : float, optional + Upper truncation bound. Defaults to +infinity (no upper bound). bias : openmc.stats.Univariate, optional Distribution for biased sampling. @@ -1070,6 +1079,10 @@ class Normal(Univariate): Mean of the Normal distribution std_dev : float Standard deviation of the Normal distribution + lower : float + Lower truncation bound + upper : float + Upper truncation bound support : tuple of float A 2-tuple (lower, upper) defining the interval over which the distribution is nonzero-valued @@ -1077,12 +1090,18 @@ class Normal(Univariate): Distribution for biased sampling """ - def __init__(self, mean_value, std_dev, bias: Univariate | None = None): + def __init__(self, mean_value, std_dev, lower=-np.inf, upper=np.inf, + bias: Univariate | None = None): self.mean_value = mean_value self.std_dev = std_dev + self.lower = lower + self.upper = upper + self._compute_normalization() super().__init__(bias) def __len__(self): + if self._is_truncated: + return 4 return 2 @property @@ -1104,16 +1123,69 @@ class Normal(Univariate): cv.check_greater_than('Normal std_dev', std_dev, 0.0) self._std_dev = std_dev + @property + def lower(self): + return self._lower + + @lower.setter + def lower(self, lower): + cv.check_type('Normal lower bound', lower, Real) + self._lower = lower + + @property + def upper(self): + return self._upper + + @upper.setter + def upper(self, upper): + cv.check_type('Normal upper bound', upper, Real) + self._upper = upper + + def _compute_normalization(self): + """Compute normalization factor for truncated distribution.""" + # Check if truncation bounds are finite + self._is_truncated = (self._lower > -np.inf or self._upper < np.inf) + + if self._lower >= self._upper: + raise ValueError("Normal distribution lower bound must be less " + "than upper bound.") + + if self._is_truncated: + alpha = (self._lower - self._mean_value) / self._std_dev + beta = (self._upper - self._mean_value) / self._std_dev + cdf_diff = scipy.stats.norm.cdf(beta) - scipy.stats.norm.cdf(alpha) + if cdf_diff <= 0: + raise ValueError("Truncation bounds exclude entire distribution") + self._norm_factor = 1.0 / cdf_diff + else: + self._norm_factor = 1.0 + @property def support(self): - return (-np.inf, np.inf) + return (self._lower, self._upper) def _sample_unbiased(self, n_samples=1, seed=None): rng = np.random.RandomState(seed) - return rng.normal(self.mean_value, self.std_dev, n_samples) + if not self._is_truncated: + return rng.normal(self.mean_value, self.std_dev, n_samples) + else: + # Use scipy's truncated normal for efficient direct sampling + a = (self._lower - self._mean_value) / self._std_dev + b = (self._upper - self._mean_value) / self._std_dev + return scipy.stats.truncnorm.rvs( + a, b, loc=self._mean_value, scale=self._std_dev, + size=n_samples, random_state=rng + ) def evaluate(self, x): - return scipy.stats.norm.pdf(x, self.mean_value, self.std_dev) + """Evaluate PDF at x, returning normalized value for truncated dist.""" + x = np.asarray(x) + f = scipy.stats.norm.pdf(x, self.mean_value, self.std_dev) + if self._is_truncated: + # PDF is zero outside bounds + in_bounds = (x >= self._lower) & (x <= self._upper) + f = np.where(in_bounds, f * self._norm_factor, 0.0) + return f def to_xml_element(self, element_name: str): """Return XML representation of the Normal distribution @@ -1126,12 +1198,16 @@ class Normal(Univariate): Returns ------- element : lxml.etree._Element - XML element containing Watt distribution data + XML element containing Normal distribution data """ element = ET.Element(element_name) element.set("type", "normal") - element.set("parameters", f'{self.mean_value} {self.std_dev}') + if self._is_truncated: + element.set("parameters", + f'{self.mean_value} {self.std_dev} {self.lower} {self.upper}') + else: + element.set("parameters", f'{self.mean_value} {self.std_dev}') self._append_bias_to_xml(element) return element @@ -1152,7 +1228,10 @@ class Normal(Univariate): """ params = get_elem_list(elem, "parameters", float) bias_dist = cls._read_bias_from_xml(elem) - return cls(*map(float, params), bias=bias_dist) + if len(params) == 4: + return cls(params[0], params[1], params[2], params[3], bias=bias_dist) + else: + return cls(params[0], params[1], bias=bias_dist) def muir(e0: float, m_rat: float, kt: float, bias: Univariate | None = None): @@ -1184,7 +1263,7 @@ def muir(e0: float, m_rat: float, kt: float, bias: Univariate | None = None): """ # https://permalink.lanl.gov/object/tr?what=info:lanl-repo/lareport/LA-05411-MS std_dev = sqrt(2 * e0 * kt / m_rat) - return Normal(e0, std_dev, bias) + return Normal(e0, std_dev, bias=bias) # Retain deprecated name for the time being diff --git a/src/distribution.cpp b/src/distribution.cpp index 756b098ed..c722d0366 100644 --- a/src/distribution.cpp +++ b/src/distribution.cpp @@ -363,30 +363,92 @@ double Watt::evaluate(double x) const //============================================================================== // Normal implementation //============================================================================== + +Normal::Normal(double mean_value, double std_dev, double lower, double upper) + : mean_value_ {mean_value}, std_dev_ {std_dev}, lower_ {lower}, upper_ {upper} +{ + compute_normalization(); +} + Normal::Normal(pugi::xml_node node) { auto params = get_node_array(node, "parameters"); - if (params.size() != 2) { + if (params.size() != 2 && params.size() != 4) { openmc::fatal_error("Normal energy distribution must have two " - "parameters specified."); + "parameters (mean, std_dev) or four parameters " + "(mean, std_dev, lower, upper) specified."); } mean_value_ = params.at(0); std_dev_ = params.at(1); + // Optional truncation bounds + if (params.size() == 4) { + lower_ = params.at(2); + upper_ = params.at(3); + } else { + lower_ = -INFTY; + upper_ = INFTY; + } + + compute_normalization(); read_bias_from_xml(node); } +void Normal::compute_normalization() +{ + // Validate bounds + if (lower_ >= upper_) { + openmc::fatal_error( + "Normal distribution lower bound must be less than upper bound."); + } + + // Check if truncation bounds are finite + is_truncated_ = (lower_ > -INFTY || upper_ < INFTY); + + if (is_truncated_) { + double alpha = (lower_ - mean_value_) / std_dev_; + double beta = (upper_ - mean_value_) / std_dev_; + double cdf_diff = standard_normal_cdf(beta) - standard_normal_cdf(alpha); + + if (cdf_diff <= 0.0) { + openmc::fatal_error( + "Normal distribution truncation bounds exclude entire distribution."); + } + norm_factor_ = 1.0 / cdf_diff; + } else { + norm_factor_ = 1.0; + } +} + double Normal::sample_unbiased(uint64_t* seed) const { - return normal_variate(mean_value_, std_dev_, seed); + if (!is_truncated_) { + return normal_variate(mean_value_, std_dev_, seed); + } + + // Rejection sampling for truncated normal + double x; + do { + x = normal_variate(mean_value_, std_dev_, seed); + } while (x < lower_ || x > upper_); + return x; } double Normal::evaluate(double x) const { - return (1.0 / (std::sqrt(2.0 / PI) * std_dev_)) * - std::exp(-(std::pow((x - mean_value_), 2.0)) / - (2.0 * std::pow(std_dev_, 2.0))); + // Return 0 outside truncation bounds + if (x < lower_ || x > upper_) { + return 0.0; + } + + // Standard normal PDF value + double pdf = (1.0 / (std::sqrt(2.0 * PI) * std_dev_)) * + std::exp(-std::pow((x - mean_value_), 2.0) / + (2.0 * std::pow(std_dev_, 2.0))); + + // Apply normalization for truncation + return pdf * norm_factor_; } //============================================================================== diff --git a/src/math_functions.cpp b/src/math_functions.cpp index f45165414..81f08fa04 100644 --- a/src/math_functions.cpp +++ b/src/math_functions.cpp @@ -95,6 +95,13 @@ double t_percentile(double p, int df) return t; } +double standard_normal_cdf(double z) +{ + // Use the complementary error function to compute the standard normal CDF + // Phi(z) = 0.5 * (1 + erf(z / sqrt(2))) = 0.5 * erfc(-z / sqrt(2)) + return 0.5 * std::erfc(-z / std::sqrt(2.0)); +} + void calc_pn_c(int n, double x, double pnx[]) { pnx[0] = 1.; diff --git a/tests/cpp_unit_tests/test_distribution.cpp b/tests/cpp_unit_tests/test_distribution.cpp index f7c480c8d..e46088a63 100644 --- a/tests/cpp_unit_tests/test_distribution.cpp +++ b/tests/cpp_unit_tests/test_distribution.cpp @@ -92,3 +92,103 @@ TEST_CASE("Test construction of SpatialBox with parameters") REQUIRE(box.upper_right() == openmc::Position {30, 15, 5}); REQUIRE_FALSE(box.only_fissionable()); } + +TEST_CASE("Test Normal distribution") +{ + // Test untruncated normal distribution + openmc::Normal normal_unbounded(0.0, 1.0); + + // Check PDF at mean (should be 1/sqrt(2*pi) ≈ 0.3989) + REQUIRE_THAT( + normal_unbounded.evaluate(0.0), Catch::Matchers::WithinRel(0.3989, 0.001)); + + // Check that it's not truncated + REQUIRE_FALSE(normal_unbounded.is_truncated()); + + // Check accessors + REQUIRE(normal_unbounded.mean_value() == 0.0); + REQUIRE(normal_unbounded.std_dev() == 1.0); + REQUIRE(normal_unbounded.lower() == -openmc::INFTY); + REQUIRE(normal_unbounded.upper() == openmc::INFTY); +} + +TEST_CASE("Test truncated Normal distribution") +{ + // Create a truncated normal: mean=0, std=1, bounds=[-1, 1] + openmc::Normal normal_truncated(0.0, 1.0, -1.0, 1.0); + + // Check that it's truncated + REQUIRE(normal_truncated.is_truncated()); + + // Check accessors + REQUIRE(normal_truncated.lower() == -1.0); + REQUIRE(normal_truncated.upper() == 1.0); + + // PDF should be zero outside bounds + REQUIRE(normal_truncated.evaluate(-2.0) == 0.0); + REQUIRE(normal_truncated.evaluate(2.0) == 0.0); + + // PDF inside bounds should be higher than untruncated (due to + // renormalization) + openmc::Normal normal_unbounded(0.0, 1.0); + REQUIRE(normal_truncated.evaluate(0.0) > normal_unbounded.evaluate(0.0)); + + // The truncated PDF at mean should be approximately 0.3989 / 0.6827 ≈ 0.584 + // (0.6827 is the probability mass of N(0,1) in [-1,1]) + REQUIRE_THAT( + normal_truncated.evaluate(0.0), Catch::Matchers::WithinRel(0.584, 0.01)); +} + +TEST_CASE("Test truncated Normal sampling") +{ + constexpr int n_samples = 10000; + openmc::Normal normal_truncated(0.0, 1.0, -1.0, 1.0); + uint64_t seed = openmc::init_seed(0, 0); + + // Sample and verify all samples are within bounds + for (int i = 0; i < n_samples; ++i) { + auto [x, w] = normal_truncated.sample(&seed); + REQUIRE(x >= -1.0); + REQUIRE(x <= 1.0); + REQUIRE(w == 1.0); // Unbiased sampling should have weight 1 + } +} + +TEST_CASE("Test one-sided truncated Normal") +{ + // Test lower-bounded only (positive half-normal) + openmc::Normal lower_bounded(0.0, 1.0, 0.0, openmc::INFTY); + REQUIRE(lower_bounded.is_truncated()); + REQUIRE(lower_bounded.evaluate(-1.0) == 0.0); + REQUIRE(lower_bounded.evaluate(1.0) > 0.0); + + // PDF at 0 should be approximately 2 * 0.3989 ≈ 0.798 (half-normal) + REQUIRE_THAT( + lower_bounded.evaluate(0.0), Catch::Matchers::WithinRel(0.798, 0.01)); + + // Test upper-bounded only + openmc::Normal upper_bounded(0.0, 1.0, -openmc::INFTY, 0.0); + REQUIRE(upper_bounded.is_truncated()); + REQUIRE(upper_bounded.evaluate(1.0) == 0.0); + REQUIRE(upper_bounded.evaluate(-1.0) > 0.0); +} + +TEST_CASE("Test Normal XML constructor with truncation") +{ + // XML doc node for truncated Normal + pugi::xml_document doc; + pugi::xml_node energy = doc.append_child("energy"); + energy.append_child("type") + .append_child(pugi::node_pcdata) + .set_value("normal"); + energy.append_child("parameters") + .append_child(pugi::node_pcdata) + .set_value("1.0e6 1.0e5 0.8e6 1.2e6"); + + openmc::Normal dist(energy); + REQUIRE(dist.mean_value() == 1.0e6); + REQUIRE(dist.std_dev() == 1.0e5); + REQUIRE(dist.lower() == 0.8e6); + REQUIRE(dist.upper() == 1.2e6); + REQUIRE(dist.is_truncated()); +} diff --git a/tests/unit_tests/test_stats.py b/tests/unit_tests/test_stats.py index bdbdaa0f2..b9d9c4e1f 100644 --- a/tests/unit_tests/test_stats.py +++ b/tests/unit_tests/test_stats.py @@ -591,6 +591,92 @@ def test_normal(): assert np.all(weights != 1.0) +@pytest.mark.flaky(reruns=1) +def test_normal_truncated(): + mean = 10.0 + std_dev = 2.0 + lower = 6.0 + upper = 14.0 + + d = openmc.stats.Normal(mean, std_dev, lower, upper) + + # Check attributes + assert d.mean_value == pytest.approx(mean) + assert d.std_dev == pytest.approx(std_dev) + assert d.lower == pytest.approx(lower) + assert d.upper == pytest.approx(upper) + assert len(d) == 4 + assert d.support == (lower, upper) + + # Test XML round-trip + elem = d.to_xml_element('distribution') + assert elem.attrib['type'] == 'normal' + params = elem.attrib['parameters'].split() + assert len(params) == 4 + + d2 = openmc.stats.Normal.from_xml_element(elem) + assert d2.mean_value == pytest.approx(mean) + assert d2.std_dev == pytest.approx(std_dev) + assert d2.lower == pytest.approx(lower) + assert d2.upper == pytest.approx(upper) + + # Test PDF evaluation + # PDF should be zero outside bounds + assert d.evaluate(lower - 1.0) == 0.0 + assert d.evaluate(upper + 1.0) == 0.0 + + # PDF should be positive inside bounds + assert d.evaluate(mean) > 0.0 + + # PDF should be higher than untruncated at the mean (due to renormalization) + d_unbounded = openmc.stats.Normal(mean, std_dev) + assert d.evaluate(mean) > d_unbounded.evaluate(mean) + + # Verify that PDF integrates to approximately 1 + x = np.linspace(lower, upper, 1000) + integral = trapezoid(d.evaluate(x), x) + assert integral == pytest.approx(1.0, rel=0.01) + + # Sample truncated distribution + n_samples = 10_000 + samples, weights = d.sample(n_samples) + + # All samples should be within bounds + assert np.all(samples >= lower) + assert np.all(samples <= upper) + + # Weights should all be 1 (no biasing) + assert np.all(weights == 1.0) + + +def test_normal_truncated_one_sided(): + # Test lower-bounded only (positive half-normal centered at 0) + d_lower = openmc.stats.Normal(0.0, 1.0, lower=0.0) + assert d_lower.lower == 0.0 + assert d_lower.upper == np.inf + assert d_lower.evaluate(-1.0) == 0.0 + assert d_lower.evaluate(1.0) > 0.0 + + # PDF at 0 should be approximately 2 * 0.3989 ≈ 0.798 (half-normal) + assert d_lower.evaluate(0.0) == pytest.approx(0.798, rel=0.01) + + # Test upper-bounded only + d_upper = openmc.stats.Normal(0.0, 1.0, upper=0.0) + assert d_upper.lower == -np.inf + assert d_upper.upper == 0.0 + assert d_upper.evaluate(1.0) == 0.0 + assert d_upper.evaluate(-1.0) > 0.0 + + +def test_normal_truncated_errors(): + # Invalid bounds (lower >= upper) + with pytest.raises(ValueError): + openmc.stats.Normal(0.0, 1.0, lower=1.0, upper=0.0) + + with pytest.raises(ValueError): + openmc.stats.Normal(0.0, 1.0, lower=1.0, upper=1.0) + + @pytest.mark.flaky(reruns=1) def test_muir(): mean = 10.0 From 4e5deed0ccb9cfaa03941b8b396578d3d84224e3 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Fri, 13 Feb 2026 04:25:49 +0100 Subject: [PATCH 541/671] Using ```_LIBRARY``` and ```_SUBLIBRARY``` from endf package (#3804) --- openmc/data/endf.py | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/openmc/data/endf.py b/openmc/data/endf.py index 1ac07a651..73ae210b6 100644 --- a/openmc/data/endf.py +++ b/openmc/data/endf.py @@ -12,6 +12,7 @@ import re from .data import gnds_name from .function import Tabulated1D +from endf.material import _LIBRARY, _SUBLIBRARY from endf.incident_neutron import SUM_RULES from endf.records import ( float_endf, @@ -38,33 +39,6 @@ def get_tab1_record(file_obj): return params, Tabulated1D(tab.x, tab.y, tab.breakpoints, tab.interpolation) -_LIBRARY = {0: 'ENDF/B', 1: 'ENDF/A', 2: 'JEFF', 3: 'EFF', - 4: 'ENDF/B High Energy', 5: 'CENDL', 6: 'JENDL', - 17: 'TENDL', 18: 'ROSFOND', 21: 'SG-21', 31: 'INDL/V', - 32: 'INDL/A', 33: 'FENDL', 34: 'IRDF', 35: 'BROND', - 36: 'INGDB-90', 37: 'FENDL/A', 41: 'BROND'} - -_SUBLIBRARY = { - 0: 'Photo-nuclear data', - 1: 'Photo-induced fission product yields', - 3: 'Photo-atomic data', - 4: 'Radioactive decay data', - 5: 'Spontaneous fission product yields', - 6: 'Atomic relaxation data', - 10: 'Incident-neutron data', - 11: 'Neutron-induced fission product yields', - 12: 'Thermal neutron scattering data', - 19: 'Neutron standards', - 113: 'Electro-atomic data', - 10010: 'Incident-proton data', - 10011: 'Proton-induced fission product yields', - 10020: 'Incident-deuteron data', - 10030: 'Incident-triton data', - 20030: 'Incident-helion (3He) data', - 20040: 'Incident-alpha data' -} - - def get_evaluations(filename): """Return a list of all evaluations within an ENDF file. From b145fdd99994fd1a6e98b24bff20ee844ac4bb42 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Fri, 13 Feb 2026 07:10:20 +0200 Subject: [PATCH 542/671] Improve radial crossing checks in SphericalMesh and CylindricalMesh (#3792) Co-authored-by: Paul Romano --- src/mesh.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/mesh.cpp b/src/mesh.cpp index acd90033f..789113ccc 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -1893,18 +1893,19 @@ double CylindricalMesh::find_r_crossing( const double inv_denominator = 1.0 / denominator; const double p = (u.x * r.x + u.y * r.y) * inv_denominator; - double c = r.x * r.x + r.y * r.y - r0 * r0; - double D = p * p - c * inv_denominator; + double R = std::sqrt(r.x * r.x + r.y * r.y); + double D = p * p - (R - r0) * (R + r0) * inv_denominator; if (D < 0.0) return INFTY; D = std::sqrt(D); - // the solution -p - D is always smaller as -p + D : Check this one first - if (std::abs(c) <= RADIAL_MESH_TOL) + // Particle is already on the shell surface; avoid spurious crossing + if (std::abs(R - r0) <= RADIAL_MESH_TOL * (1.0 + std::abs(r0))) return INFTY; + // Check -p - D first because it is always smaller as -p + D if (-p - D > l) return -p - D; if (-p + D > l) @@ -2178,15 +2179,16 @@ double SphericalMesh::find_r_crossing( if (r0 == 0.0) return INFTY; const double p = r.dot(u); - double c = r.dot(r) - r0 * r0; - double D = p * p - c; + double R = r.norm(); + double D = p * p - (R - r0) * (R + r0); - if (std::abs(c) <= RADIAL_MESH_TOL) + // Particle is already on the shell surface; avoid spurious crossing + if (std::abs(R - r0) <= RADIAL_MESH_TOL * (1.0 + std::abs(r0))) return INFTY; if (D >= 0.0) { D = std::sqrt(D); - // the solution -p - D is always smaller as -p + D : Check this one first + // Check -p - D first because it is always smaller as -p + D if (-p - D > l) return -p - D; if (-p + D > l) From bcb93952078b632f2d97238143c3e1b74b527e2d Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Thu, 12 Feb 2026 23:54:27 -0600 Subject: [PATCH 543/671] SolidRayTracePlot CAPI (#3789) Co-authored-by: GuySten <62616591+GuySten@users.noreply.github.com> --- docs/source/capi/index.rst | 273 ++++++++++++++ docs/source/pythonapi/capi.rst | 7 + include/openmc/capi.h | 44 +++ include/openmc/plot.h | 30 +- openmc/lib/plot.py | 386 +++++++++++++++++++- src/plot.cpp | 636 ++++++++++++++++++++++++++++++++- tests/unit_tests/test_lib.py | 54 +++ 7 files changed, 1402 insertions(+), 28 deletions(-) diff --git a/docs/source/capi/index.rst b/docs/source/capi/index.rst index 0a1962f77..1777e795a 100644 --- a/docs/source/capi/index.rst +++ b/docs/source/capi/index.rst @@ -580,6 +580,279 @@ Functions :return: Return status (negative if an error occurs) :rtype: int +.. c:function:: int openmc_get_plot_index(int32_t id, int32_t* index) + + Get the index in the plots array for a plot with a given ID. + + :param int32_t id: Plot ID + :param int32_t* index: Index in the plots array + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_plot_get_id(int32_t index, int32_t* id) + + Get the ID of a plot. + + :param int32_t index: Index in the plots array + :param int32_t* id: Plot ID + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_plot_set_id(int32_t index, int32_t id) + + Set the ID of a plot. + + :param int32_t index: Index in the plots array + :param int32_t id: Plot ID + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: size_t openmc_plots_size() + + Number of plots currently allocated. + + :return: Number of plots in the plots array + :rtype: size_t + +.. c:function:: int openmc_solidraytrace_plot_create(int32_t* index) + + Create a new solid raytrace plot. + + :param int32_t* index: Index of the newly created plot + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_get_pixels(int32_t index, int32_t* width, int32_t* height) + + Get output pixel dimensions for a solid raytrace plot. + + :param int32_t index: Index in the plots array + :param int32_t* width: Image width in pixels + :param int32_t* height: Image height in pixels + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_set_pixels(int32_t index, int32_t width, int32_t height) + + Set output pixel dimensions for a solid raytrace plot. + + :param int32_t index: Index in the plots array + :param int32_t width: Image width in pixels + :param int32_t height: Image height in pixels + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_get_color_by(int32_t index, int32_t* color_by) + + Get the domain type used for coloring (0=materials, 1=cells). + + :param int32_t index: Index in the plots array + :param int32_t* color_by: Coloring mode + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_set_color_by(int32_t index, int32_t color_by) + + Set the domain type used for coloring (0=materials, 1=cells). + + :param int32_t index: Index in the plots array + :param int32_t color_by: Coloring mode + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_set_default_colors(int32_t index) + + Set default random colors for the current ``color_by`` mode. + + :param int32_t index: Index in the plots array + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_set_all_opaque(int32_t index) + + Mark all domains in the current ``color_by`` mode as opaque. + + :param int32_t index: Index in the plots array + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_set_opaque(int32_t index, int32_t id, bool visible) + + Set whether a specific domain ID is opaque (visible) in the rendered image. + + :param int32_t index: Index in the plots array + :param int32_t id: Cell/material ID (based on ``color_by``) + :param bool visible: Whether the domain is opaque + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_set_color(int32_t index, int32_t id, uint8_t r, uint8_t g, uint8_t b) + + Set RGB color for a specific domain ID. + + :param int32_t index: Index in the plots array + :param int32_t id: Cell/material ID (based on ``color_by``) + :param uint8_t r: Red channel + :param uint8_t g: Green channel + :param uint8_t b: Blue channel + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_get_color(int32_t index, int32_t id, uint8_t* r, uint8_t* g, uint8_t* b) + + Get RGB color for a specific domain ID. + + :param int32_t index: Index in the plots array + :param int32_t id: Cell/material ID (based on ``color_by``) + :param uint8_t* r: Red channel + :param uint8_t* g: Green channel + :param uint8_t* b: Blue channel + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_get_camera_position(int32_t index, double* x, double* y, double* z) + + Get camera position. + + :param int32_t index: Index in the plots array + :param double* x: X coordinate + :param double* y: Y coordinate + :param double* z: Z coordinate + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_set_camera_position(int32_t index, double x, double y, double z) + + Set camera position. + + :param int32_t index: Index in the plots array + :param double x: X coordinate + :param double y: Y coordinate + :param double z: Z coordinate + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_get_look_at(int32_t index, double* x, double* y, double* z) + + Get camera target point. + + :param int32_t index: Index in the plots array + :param double* x: X coordinate + :param double* y: Y coordinate + :param double* z: Z coordinate + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_set_look_at(int32_t index, double x, double y, double z) + + Set camera target point. + + :param int32_t index: Index in the plots array + :param double x: X coordinate + :param double y: Y coordinate + :param double z: Z coordinate + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_get_up(int32_t index, double* x, double* y, double* z) + + Get the camera up vector. + + :param int32_t index: Index in the plots array + :param double* x: X component + :param double* y: Y component + :param double* z: Z component + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_set_up(int32_t index, double x, double y, double z) + + Set the camera up vector. + + :param int32_t index: Index in the plots array + :param double x: X component + :param double y: Y component + :param double z: Z component + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_get_light_position(int32_t index, double* x, double* y, double* z) + + Get light source position. + + :param int32_t index: Index in the plots array + :param double* x: X coordinate + :param double* y: Y coordinate + :param double* z: Z coordinate + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_set_light_position(int32_t index, double x, double y, double z) + + Set light source position. + + :param int32_t index: Index in the plots array + :param double x: X coordinate + :param double y: Y coordinate + :param double z: Z coordinate + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_get_fov(int32_t index, double* fov) + + Get horizontal field of view in degrees. + + :param int32_t index: Index in the plots array + :param double* fov: Field of view in degrees + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_set_fov(int32_t index, double fov) + + Set horizontal field of view in degrees. + + :param int32_t index: Index in the plots array + :param double fov: Field of view in degrees + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_get_diffuse_fraction(int32_t index, double* diffuse_fraction) + + Get diffuse-light fraction. + + :param int32_t index: Index in the plots array + :param double* diffuse_fraction: Diffuse fraction in [0, 1] + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_set_diffuse_fraction(int32_t index, double diffuse_fraction) + + Set diffuse-light fraction. + + :param int32_t index: Index in the plots array + :param double diffuse_fraction: Diffuse fraction in [0, 1] + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_update_view(int32_t index) + + Recompute internal camera/view transforms after camera changes. + + :param int32_t index: Index in the plots array + :return: Return status (negative if an error occurred) + :rtype: int + +.. c:function:: int openmc_solidraytrace_plot_create_image(int32_t index, uint8_t* data_out, int32_t width, int32_t height) + + Render the plot to an RGB image buffer. + + :param int32_t index: Index in the plots array + :param uint8_t* data_out: Output buffer of shape ``height*width*3`` + :param int32_t width: Image width in pixels + :param int32_t height: Image height in pixels + :return: Return status (negative if an error occurred) + :rtype: int + .. c:function:: int openmc_reset() Resets all tally scores diff --git a/docs/source/pythonapi/capi.rst b/docs/source/pythonapi/capi.rst index 6f18db56f..465df14d8 100644 --- a/docs/source/pythonapi/capi.rst +++ b/docs/source/pythonapi/capi.rst @@ -88,6 +88,7 @@ Classes SpatialLegendreFilter SphericalHarmonicsFilter SphericalMesh + SolidRayTracePlot SurfaceFilter Tally TemporarySession @@ -125,6 +126,12 @@ Data :type: dict +.. data:: plots + + Mapping of plot ID to :class:`openmc.lib.SolidRayTracePlot` instances. + + :type: dict + .. data:: nuclides Mapping of nuclide name to :class:`openmc.lib.Nuclide` instances. diff --git a/include/openmc/capi.h b/include/openmc/capi.h index 533ebb65a..911654d31 100644 --- a/include/openmc/capi.h +++ b/include/openmc/capi.h @@ -125,6 +125,49 @@ int openmc_nuclide_name(int index, const char** name); int openmc_plot_geometry(); int openmc_id_map(const void* slice, int32_t* data_out); int openmc_property_map(const void* slice, double* data_out); +int openmc_get_plot_index(int32_t id, int32_t* index); +int openmc_plot_get_id(int32_t index, int32_t* id); +int openmc_plot_set_id(int32_t index, int32_t id); +int openmc_solidraytrace_plot_create(int32_t* index); +int openmc_solidraytrace_plot_get_pixels( + int32_t index, int32_t* width, int32_t* height); +int openmc_solidraytrace_plot_set_pixels( + int32_t index, int32_t width, int32_t height); +int openmc_solidraytrace_plot_get_color_by(int32_t index, int32_t* color_by); +int openmc_solidraytrace_plot_set_color_by(int32_t index, int32_t color_by); +int openmc_solidraytrace_plot_set_default_colors(int32_t index); +int openmc_solidraytrace_plot_set_all_opaque(int32_t index); +int openmc_solidraytrace_plot_set_opaque( + int32_t index, int32_t id, bool visible); +int openmc_solidraytrace_plot_set_color( + int32_t index, int32_t id, uint8_t r, uint8_t g, uint8_t b); +int openmc_solidraytrace_plot_get_camera_position( + int32_t index, double* x, double* y, double* z); +int openmc_solidraytrace_plot_set_camera_position( + int32_t index, double x, double y, double z); +int openmc_solidraytrace_plot_get_look_at( + int32_t index, double* x, double* y, double* z); +int openmc_solidraytrace_plot_set_look_at( + int32_t index, double x, double y, double z); +int openmc_solidraytrace_plot_get_up( + int32_t index, double* x, double* y, double* z); +int openmc_solidraytrace_plot_set_up( + int32_t index, double x, double y, double z); +int openmc_solidraytrace_plot_get_light_position( + int32_t index, double* x, double* y, double* z); +int openmc_solidraytrace_plot_set_light_position( + int32_t index, double x, double y, double z); +int openmc_solidraytrace_plot_get_fov(int32_t index, double* fov); +int openmc_solidraytrace_plot_set_fov(int32_t index, double fov); +int openmc_solidraytrace_plot_update_view(int32_t index); +int openmc_solidraytrace_plot_create_image( + int32_t index, uint8_t* data_out, int32_t width, int32_t height); +int openmc_solidraytrace_plot_get_color( + int32_t index, int32_t id, uint8_t* r, uint8_t* g, uint8_t* b); +int openmc_solidraytrace_plot_get_diffuse_fraction( + int32_t index, double* diffuse_fraction); +int openmc_solidraytrace_plot_set_diffuse_fraction( + int32_t index, double diffuse_fraction); int openmc_rectilinear_mesh_get_grid(int32_t index, double** grid_x, int* nx, double** grid_y, int* ny, double** grid_z, int* nz); int openmc_rectilinear_mesh_set_grid(int32_t index, const double* grid_x, @@ -219,6 +262,7 @@ int openmc_weight_windows_set_weight_cutoff(int32_t index, double cutoff); int openmc_weight_windows_get_max_split(int32_t index, int* max_split); int openmc_weight_windows_set_max_split(int32_t index, int max_split); size_t openmc_weight_windows_size(); +size_t openmc_plots_size(); int openmc_weight_windows_export(const char* filename = nullptr); int openmc_weight_windows_import(const char* filename = nullptr); int openmc_zernike_filter_get_order(int32_t index, int* order); diff --git a/include/openmc/plot.h b/include/openmc/plot.h index 02ff2848d..3813101c6 100644 --- a/include/openmc/plot.h +++ b/include/openmc/plot.h @@ -83,10 +83,11 @@ const RGBColor BLACK {0, 0, 0}; * \class PlottableInterface * \brief Interface for plottable objects. * - * PlottableInterface classes must have a unique ID in the plots.xml file. - * They guarantee the ability to create output in some form. This interface - * is designed to be implemented by classes that produce plot-relevant data - * which can be visualized. + * PlottableInterface classes must have unique IDs. If no ID (or -1) is + * provided, the next available ID is assigned automatically. They guarantee + * the ability to create output in some form. This interface is designed to be + * implemented by classes that produce plot-relevant data which can be + * visualized. */ typedef xt::xtensor ImageData; @@ -98,7 +99,7 @@ public: private: void set_id(pugi::xml_node plot_node); - int id_; // unique plot ID + int id_ {C_NONE}; // unique plot ID void set_bg_color(pugi::xml_node plot_node); void set_universe(pugi::xml_node plot_node); @@ -129,18 +130,19 @@ public: const std::string& path_plot() const { return path_plot_; } std::string& path_plot() { return path_plot_; } int id() const { return id_; } + void set_id(int id = C_NONE); int level() const { return level_; } PlotColorBy color_by() const { return color_by_; } // Public color-related data PlottableInterface(pugi::xml_node plot_node); virtual ~PlottableInterface() = default; - int level_ {-1}; // Universe level to plot - bool color_overlaps_ {false}; // Show overlapping cells? - PlotColorBy color_by_; // Plot coloring (cell/material) - RGBColor not_found_ {WHITE}; // Plot background color - RGBColor overlap_color_ {RED}; // Plot overlap color - vector colors_; // Plot colors + int level_ {-1}; // Universe level to plot + bool color_overlaps_ {false}; // Show overlapping cells? + PlotColorBy color_by_ {PlotColorBy::mats}; // Plot coloring (cell/material) + RGBColor not_found_ {WHITE}; // Plot background color + RGBColor overlap_color_ {RED}; // Plot overlap color + vector colors_; // Plot colors }; struct IdData { @@ -371,9 +373,9 @@ private: double horizontal_field_of_view_ {70.0}; // horiz. f.o.v. in degrees Position camera_position_; // where camera is - Position look_at_; // point camera is centered looking at - std::array pixels_; // pixel dimension of resulting image - Direction up_ {0.0, 0.0, 1.0}; // which way is up + Position look_at_; // point camera is centered looking at + std::array pixels_ {100, 100}; // pixel dimension of resulting image + Direction up_ {0.0, 0.0, 1.0}; // which way is up /* The horizontal thickness, if using an orthographic projection. * If set to zero, we assume using a perspective projection. diff --git a/openmc/lib/plot.py b/openmc/lib/plot.py index 68f61821c..90af80d5b 100644 --- a/openmc/lib/plot.py +++ b/openmc/lib/plot.py @@ -1,7 +1,11 @@ +from collections.abc import Mapping from ctypes import (c_bool, c_int, c_size_t, c_int32, - c_double, Structure, POINTER) + c_double, c_uint8, Structure, POINTER) +from weakref import WeakValueDictionary +from ..exceptions import AllocationError, InvalidIDError from . import _dll +from .core import _FortranObjectWithID from .error import _error_handler import numpy as np @@ -258,3 +262,383 @@ def property_map(plot): prop_data = np.zeros((plot.v_res, plot.h_res, 2)) _dll.openmc_property_map(plot, prop_data.ctypes.data_as(POINTER(c_double))) return prop_data + +_dll.openmc_get_plot_index.argtypes = [c_int32, POINTER(c_int32)] +_dll.openmc_get_plot_index.restype = c_int +_dll.openmc_get_plot_index.errcheck = _error_handler + +_dll.openmc_plot_get_id.argtypes = [c_int32, POINTER(c_int32)] +_dll.openmc_plot_get_id.restype = c_int +_dll.openmc_plot_get_id.errcheck = _error_handler + +_dll.openmc_plot_set_id.argtypes = [c_int32, c_int32] +_dll.openmc_plot_set_id.restype = c_int +_dll.openmc_plot_set_id.errcheck = _error_handler + +_dll.openmc_plots_size.restype = c_size_t + +_dll.openmc_solidraytrace_plot_create.argtypes = [POINTER(c_int32)] +_dll.openmc_solidraytrace_plot_create.restype = c_int +_dll.openmc_solidraytrace_plot_create.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_get_pixels.argtypes = [ + c_int32, POINTER(c_int32), POINTER(c_int32)] +_dll.openmc_solidraytrace_plot_get_pixels.restype = c_int +_dll.openmc_solidraytrace_plot_get_pixels.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_set_pixels.argtypes = [c_int32, c_int32, c_int32] +_dll.openmc_solidraytrace_plot_set_pixels.restype = c_int +_dll.openmc_solidraytrace_plot_set_pixels.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_get_color_by.argtypes = [c_int32, POINTER(c_int32)] +_dll.openmc_solidraytrace_plot_get_color_by.restype = c_int +_dll.openmc_solidraytrace_plot_get_color_by.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_set_color_by.argtypes = [c_int32, c_int32] +_dll.openmc_solidraytrace_plot_set_color_by.restype = c_int +_dll.openmc_solidraytrace_plot_set_color_by.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_set_default_colors.argtypes = [c_int32] +_dll.openmc_solidraytrace_plot_set_default_colors.restype = c_int +_dll.openmc_solidraytrace_plot_set_default_colors.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_set_all_opaque.argtypes = [c_int32] +_dll.openmc_solidraytrace_plot_set_all_opaque.restype = c_int +_dll.openmc_solidraytrace_plot_set_all_opaque.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_set_opaque.argtypes = [c_int32, c_int32, c_bool] +_dll.openmc_solidraytrace_plot_set_opaque.restype = c_int +_dll.openmc_solidraytrace_plot_set_opaque.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_set_color.argtypes = [c_int32, c_int32, c_uint8, c_uint8, c_uint8] +_dll.openmc_solidraytrace_plot_set_color.restype = c_int +_dll.openmc_solidraytrace_plot_set_color.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_get_camera_position.argtypes = [ + c_int32, POINTER(c_double), POINTER(c_double), POINTER(c_double)] +_dll.openmc_solidraytrace_plot_get_camera_position.restype = c_int +_dll.openmc_solidraytrace_plot_get_camera_position.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_set_camera_position.argtypes = [c_int32, c_double, c_double, c_double] +_dll.openmc_solidraytrace_plot_set_camera_position.restype = c_int +_dll.openmc_solidraytrace_plot_set_camera_position.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_get_look_at.argtypes = [ + c_int32, POINTER(c_double), POINTER(c_double), POINTER(c_double)] +_dll.openmc_solidraytrace_plot_get_look_at.restype = c_int +_dll.openmc_solidraytrace_plot_get_look_at.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_set_look_at.argtypes = [c_int32, c_double, c_double, c_double] +_dll.openmc_solidraytrace_plot_set_look_at.restype = c_int +_dll.openmc_solidraytrace_plot_set_look_at.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_get_up.argtypes = [ + c_int32, POINTER(c_double), POINTER(c_double), POINTER(c_double)] +_dll.openmc_solidraytrace_plot_get_up.restype = c_int +_dll.openmc_solidraytrace_plot_get_up.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_set_up.argtypes = [c_int32, c_double, c_double, c_double] +_dll.openmc_solidraytrace_plot_set_up.restype = c_int +_dll.openmc_solidraytrace_plot_set_up.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_get_light_position.argtypes = [ + c_int32, POINTER(c_double), POINTER(c_double), POINTER(c_double)] +_dll.openmc_solidraytrace_plot_get_light_position.restype = c_int +_dll.openmc_solidraytrace_plot_get_light_position.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_set_light_position.argtypes = [c_int32, c_double, c_double, c_double] +_dll.openmc_solidraytrace_plot_set_light_position.restype = c_int +_dll.openmc_solidraytrace_plot_set_light_position.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_get_fov.argtypes = [c_int32, POINTER(c_double)] +_dll.openmc_solidraytrace_plot_get_fov.restype = c_int +_dll.openmc_solidraytrace_plot_get_fov.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_set_fov.argtypes = [c_int32, c_double] +_dll.openmc_solidraytrace_plot_set_fov.restype = c_int +_dll.openmc_solidraytrace_plot_set_fov.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_update_view.argtypes = [c_int32] +_dll.openmc_solidraytrace_plot_update_view.restype = c_int +_dll.openmc_solidraytrace_plot_update_view.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_create_image.argtypes = [c_int32, POINTER(c_uint8), c_int32, c_int32] +_dll.openmc_solidraytrace_plot_create_image.restype = c_int +_dll.openmc_solidraytrace_plot_create_image.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_get_color.argtypes = [c_int32, c_int32, + POINTER(c_uint8), POINTER(c_uint8), POINTER(c_uint8)] +_dll.openmc_solidraytrace_plot_get_color.restype = c_int +_dll.openmc_solidraytrace_plot_get_color.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_get_diffuse_fraction.argtypes = [ + c_int32, POINTER(c_double)] +_dll.openmc_solidraytrace_plot_get_diffuse_fraction.restype = c_int +_dll.openmc_solidraytrace_plot_get_diffuse_fraction.errcheck = _error_handler + +_dll.openmc_solidraytrace_plot_set_diffuse_fraction.argtypes = [c_int32, c_double] +_dll.openmc_solidraytrace_plot_set_diffuse_fraction.restype = c_int +_dll.openmc_solidraytrace_plot_set_diffuse_fraction.errcheck = _error_handler + + +class SolidRayTracePlot(_FortranObjectWithID): + """Solid ray-traced plot stored internally. + + This class exposes a solid ray-traced plot that is stored internally in + the OpenMC library. To obtain a view of an existing plot with a given ID, + use the :data:`openmc.lib.plots` mapping. + + Parameters + ---------- + uid : int or None + Unique ID of the plot + new : bool + When `index` is None, this argument controls whether a new object is + created or a view of an existing object is returned. + index : int or None + Index in the internal plots array. + + Attributes + ---------- + id : int + Unique ID of the plot. + pixels : tuple of int + Plot image dimensions as ``(width, height)``. + color_by : int + Coloring mode. Use :attr:`COLOR_BY_MATERIAL` or + :attr:`COLOR_BY_CELL`. + camera_position : tuple of float + Camera position as ``(x, y, z)``. + look_at : tuple of float + Point the camera is aimed at as ``(x, y, z)``. + up : tuple of float + Up direction as ``(x, y, z)``. + light_position : tuple of float + Position of the light source as ``(x, y, z)``. + fov : float + Horizontal field-of-view angle in degrees. + diffuse_fraction : float + Fraction of reflected light treated as diffuse (0 to 1). + """ + + COLOR_BY_MATERIAL = 0 + COLOR_BY_CELL = 1 + __instances = WeakValueDictionary() + + def __new__(cls, uid=None, new=True, index=None): + mapping = plots + if index is None: + if new: + if uid is not None and uid in mapping: + raise AllocationError( + f'A plot with ID={uid} has already been allocated.' + ) + index = c_int32() + _dll.openmc_solidraytrace_plot_create(index) + index = index.value + else: + index = mapping[uid]._index + + if index not in cls.__instances: + instance = super().__new__(cls) + instance._index = index + if uid is not None: + instance.id = uid + cls.__instances[index] = instance + + return cls.__instances[index] + + def __init__(self, uid=None, new=True, index=None): + super().__init__(uid, new, index) + + @property + def id(self): + plot_id = c_int32() + _dll.openmc_plot_get_id(self._index, plot_id) + return plot_id.value + + @id.setter + def id(self, plot_id): + _dll.openmc_plot_set_id(self._index, plot_id) + + @staticmethod + def _get_xyz(getter, index): + x = c_double() + y = c_double() + z = c_double() + getter(index, x, y, z) + return (x.value, y.value, z.value) + + @staticmethod + def _set_xyz(setter, index, xyz): + x, y, z = xyz + setter(index, float(x), float(y), float(z)) + + @property + def pixels(self): + width = c_int32() + height = c_int32() + _dll.openmc_solidraytrace_plot_get_pixels(self._index, width, height) + return (width.value, height.value) + + @pixels.setter + def pixels(self, pixels): + width, height = pixels + _dll.openmc_solidraytrace_plot_set_pixels( + self._index, int(width), int(height)) + + @property + def color_by(self): + color_by = c_int32() + _dll.openmc_solidraytrace_plot_get_color_by(self._index, color_by) + return color_by.value + + @color_by.setter + def color_by(self, color_by): + _dll.openmc_solidraytrace_plot_set_color_by(self._index, int(color_by)) + + def set_default_colors(self): + _dll.openmc_solidraytrace_plot_set_default_colors(self._index) + + def set_all_opaque(self): + _dll.openmc_solidraytrace_plot_set_all_opaque(self._index) + + def set_visibility(self, domain_id, visible): + _dll.openmc_solidraytrace_plot_set_opaque( + self._index, int(domain_id), bool(visible) + ) + + def set_color(self, domain_id, color): + r, g, b = [int(c) for c in color] + _dll.openmc_solidraytrace_plot_set_color( + self._index, int(domain_id), r, g, b) + + @property + def camera_position(self): + return self._get_xyz(_dll.openmc_solidraytrace_plot_get_camera_position, + self._index) + + @camera_position.setter + def camera_position(self, position): + self._set_xyz(_dll.openmc_solidraytrace_plot_set_camera_position, + self._index, position) + + @property + def look_at(self): + return self._get_xyz(_dll.openmc_solidraytrace_plot_get_look_at, + self._index) + + @look_at.setter + def look_at(self, position): + self._set_xyz(_dll.openmc_solidraytrace_plot_set_look_at, + self._index, position) + + @property + def up(self): + return self._get_xyz(_dll.openmc_solidraytrace_plot_get_up, self._index) + + @up.setter + def up(self, direction): + self._set_xyz(_dll.openmc_solidraytrace_plot_set_up, self._index, + direction) + + @property + def light_position(self): + return self._get_xyz(_dll.openmc_solidraytrace_plot_get_light_position, + self._index) + + @light_position.setter + def light_position(self, position): + self._set_xyz(_dll.openmc_solidraytrace_plot_set_light_position, + self._index, position) + + @property + def fov(self): + fov = c_double() + _dll.openmc_solidraytrace_plot_get_fov(self._index, fov) + return fov.value + + @fov.setter + def fov(self, fov): + _dll.openmc_solidraytrace_plot_set_fov(self._index, float(fov)) + + def update_view(self): + _dll.openmc_solidraytrace_plot_update_view(self._index) + + def create_image(self): + width, height = self.pixels + image = np.zeros((height, width, 3), dtype=np.uint8) + _dll.openmc_solidraytrace_plot_create_image( + self._index, + image.ctypes.data_as(POINTER(c_uint8)), + width, + height + ) + return image + + def get_color(self, domain_id): + r = c_uint8() + g = c_uint8() + b = c_uint8() + _dll.openmc_solidraytrace_plot_get_color( + self._index, int(domain_id), r, g, b) + return int(r.value), int(g.value), int(b.value) + + @property + def diffuse_fraction(self): + value = c_double() + _dll.openmc_solidraytrace_plot_get_diffuse_fraction(self._index, value) + return value.value + + @diffuse_fraction.setter + def diffuse_fraction(self, value): + _dll.openmc_solidraytrace_plot_set_diffuse_fraction( + self._index, float(value)) + + # Backward-compatible setter aliases + def set_pixels(self, width, height): + self.pixels = (width, height) + + def set_color_by(self, color_by): + self.color_by = color_by + + def set_camera_position(self, x, y, z): + self.camera_position = (x, y, z) + + def set_look_at(self, x, y, z): + self.look_at = (x, y, z) + + def set_up(self, x, y, z): + self.up = (x, y, z) + + def set_light_position(self, x, y, z): + self.light_position = (x, y, z) + + def set_fov(self, fov): + self.fov = fov + + def set_diffuse_fraction(self, value): + self.diffuse_fraction = value + + +class _PlotMapping(Mapping): + def __getitem__(self, key): + index = c_int32() + try: + _dll.openmc_get_plot_index(key, index) + except (AllocationError, InvalidIDError) as e: + raise KeyError(str(e)) + return SolidRayTracePlot(index=index.value) + + def __iter__(self): + for i in range(len(self)): + yield SolidRayTracePlot(index=i).id + + def __len__(self): + return _dll.openmc_plots_size() + + def __repr__(self): + return repr(dict(self)) + + +plots = _PlotMapping() diff --git a/src/plot.cpp b/src/plot.cpp index daeb0bde7..e3b1e84c8 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -211,8 +211,11 @@ void read_plots_xml() void read_plots_xml(pugi::xml_node root) { for (auto node : root.children("plot")) { - std::string id_string = get_node_value(node, "id", true); - int id = std::stoi(id_string); + std::string plot_desc = ""; + if (check_for_node(node, "id")) { + plot_desc = get_node_value(node, "id", true); + } + if (check_for_node(node, "type")) { std::string type_str = get_node_value(node, "type", true); if (type_str == "slice") { @@ -227,12 +230,12 @@ void read_plots_xml(pugi::xml_node root) } else if (type_str == "solid_raytrace") { model::plots.emplace_back(std::make_unique(node)); } else { - fatal_error( - fmt::format("Unsupported plot type '{}' in plot {}", type_str, id)); + fatal_error(fmt::format( + "Unsupported plot type '{}' in plot {}", type_str, plot_desc)); } model::plot_map[model::plots.back()->id()] = model::plots.size() - 1; } else { - fatal_error(fmt::format("Must specify plot type in plot {}", id)); + fatal_error(fmt::format("Must specify plot type in plot {}", plot_desc)); } } } @@ -290,20 +293,43 @@ ImageData Plot::create_image() const void PlottableInterface::set_id(pugi::xml_node plot_node) { - // Copy data into plots + int id {C_NONE}; if (check_for_node(plot_node, "id")) { - id_ = std::stoi(get_node_value(plot_node, "id")); - } else { - fatal_error("Must specify plot id in plots XML file."); + id = std::stoi(get_node_value(plot_node, "id")); } - // Check to make sure 'id' hasn't been used - if (model::plot_map.find(id_) != model::plot_map.end()) { - fatal_error( - fmt::format("Two or more plots use the same unique ID: {}", id_)); + try { + set_id(id); + } catch (const std::runtime_error& e) { + fatal_error(e.what()); } } +void PlottableInterface::set_id(int id) +{ + if (id < 0 && id != C_NONE) { + throw std::runtime_error {fmt::format("Invalid plot ID: {}", id)}; + } + + if (id == C_NONE) { + id = 1; + for (const auto& p : model::plots) { + id = std::max(id, p->id() + 1); + } + } + + if (id_ == id) + return; + + // Check to make sure this ID doesn't already exist + if (model::plot_map.find(id) != model::plot_map.end()) { + throw std::runtime_error { + fmt::format("Two or more plots use the same unique ID: {}", id)}; + } + + id_ = id; +} + // Checks if png or ppm is already present bool file_extension_present( const std::string& filename, const std::string& extension) @@ -1927,4 +1953,588 @@ extern "C" int openmc_property_map(const void* plot, double* data_out) return 0; } +extern "C" int openmc_get_plot_index(int32_t id, int32_t* index) +{ + auto it = model::plot_map.find(id); + if (it == model::plot_map.end()) { + set_errmsg("No plot exists with ID=" + std::to_string(id) + "."); + return OPENMC_E_INVALID_ID; + } + + *index = it->second; + return 0; +} + +extern "C" int openmc_plot_get_id(int32_t index, int32_t* id) +{ + if (index < 0 || index >= model::plots.size()) { + set_errmsg("Index in plots array is out of bounds."); + return OPENMC_E_OUT_OF_BOUNDS; + } + + *id = model::plots[index]->id(); + return 0; +} + +extern "C" int openmc_plot_set_id(int32_t index, int32_t id) +{ + if (index < 0 || index >= model::plots.size()) { + set_errmsg("Index in plots array is out of bounds."); + return OPENMC_E_OUT_OF_BOUNDS; + } + + if (id < 0 && id != C_NONE) { + set_errmsg("Invalid plot ID."); + return OPENMC_E_INVALID_ARGUMENT; + } + + auto* plot = model::plots[index].get(); + int32_t old_id = plot->id(); + if (id == old_id) + return 0; + + model::plot_map.erase(old_id); + try { + plot->set_id(id); + } catch (const std::runtime_error& e) { + model::plot_map[old_id] = index; + set_errmsg(e.what()); + return OPENMC_E_INVALID_ID; + } + model::plot_map[plot->id()] = index; + return 0; +} + +extern "C" size_t openmc_plots_size() +{ + return model::plots.size(); +} + +int map_phong_domain_id( + const SolidRayTracePlot* plot, int32_t id, int32_t* index_out) +{ + if (!plot || !index_out) { + set_errmsg("Invalid plot pointer passed to map_phong_domain_id"); + return OPENMC_E_INVALID_ARGUMENT; + } + + if (plot->color_by_ == PlottableInterface::PlotColorBy::mats) { + auto it = model::material_map.find(id); + if (it == model::material_map.end()) { + set_errmsg("Invalid material ID for SolidRayTracePlot"); + return OPENMC_E_INVALID_ID; + } + *index_out = it->second; + return 0; + } + + if (plot->color_by_ == PlottableInterface::PlotColorBy::cells) { + auto it = model::cell_map.find(id); + if (it == model::cell_map.end()) { + set_errmsg("Invalid cell ID for SolidRayTracePlot"); + return OPENMC_E_INVALID_ID; + } + *index_out = it->second; + return 0; + } + + set_errmsg("Unsupported color_by for SolidRayTracePlot"); + return OPENMC_E_INVALID_TYPE; +} + +int get_solidraytrace_plot_by_index(int32_t index, SolidRayTracePlot** plot) +{ + if (!plot) { + set_errmsg("Null output pointer passed to get_solidraytrace_plot_by_index"); + return OPENMC_E_INVALID_ARGUMENT; + } + + if (index < 0 || index >= model::plots.size()) { + set_errmsg("Index in plots array is out of bounds."); + return OPENMC_E_OUT_OF_BOUNDS; + } + + auto* plottable = model::plots[index].get(); + auto* solid_plot = dynamic_cast(plottable); + if (!solid_plot) { + set_errmsg("Plot at index=" + std::to_string(index) + + " is not a solid raytrace plot."); + return OPENMC_E_INVALID_TYPE; + } + + *plot = solid_plot; + return 0; +} + +extern "C" int openmc_solidraytrace_plot_create(int32_t* index) +{ + if (!index) { + set_errmsg( + "Null output pointer passed to openmc_solidraytrace_plot_create"); + return OPENMC_E_INVALID_ARGUMENT; + } + + try { + auto new_plot = std::make_unique(); + new_plot->set_id(); + int32_t new_plot_id = new_plot->id(); +#ifdef USE_LIBPNG + new_plot->path_plot() = fmt::format("plot_{}.png", new_plot_id); +#else + new_plot->path_plot() = fmt::format("plot_{}.ppm", new_plot_id); +#endif + int32_t new_plot_index = model::plots.size(); + model::plots.emplace_back(std::move(new_plot)); + model::plot_map[new_plot_id] = new_plot_index; + *index = new_plot_index; + } catch (const std::exception& e) { + set_errmsg(e.what()); + return OPENMC_E_ALLOCATE; + } + + return 0; +} + +extern "C" int openmc_solidraytrace_plot_get_pixels( + int32_t index, int32_t* width, int32_t* height) +{ + if (!width || !height) { + set_errmsg( + "Invalid arguments passed to openmc_solidraytrace_plot_get_pixels"); + return OPENMC_E_INVALID_ARGUMENT; + } + + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + *width = plt->pixels()[0]; + *height = plt->pixels()[1]; + return 0; +} + +extern "C" int openmc_solidraytrace_plot_set_pixels( + int32_t index, int32_t width, int32_t height) +{ + if (width <= 0 || height <= 0) { + set_errmsg( + "Invalid arguments passed to openmc_solidraytrace_plot_set_pixels"); + return OPENMC_E_INVALID_ARGUMENT; + } + + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + plt->pixels()[0] = width; + plt->pixels()[1] = height; + return 0; +} + +extern "C" int openmc_solidraytrace_plot_get_color_by( + int32_t index, int32_t* color_by) +{ + if (!color_by) { + set_errmsg( + "Invalid arguments passed to openmc_solidraytrace_plot_get_color_by"); + return OPENMC_E_INVALID_ARGUMENT; + } + + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + if (plt->color_by_ == PlottableInterface::PlotColorBy::mats) { + *color_by = 0; + } else if (plt->color_by_ == PlottableInterface::PlotColorBy::cells) { + *color_by = 1; + } else { + set_errmsg("Unsupported color_by for SolidRayTracePlot"); + return OPENMC_E_INVALID_TYPE; + } + + return 0; +} + +extern "C" int openmc_solidraytrace_plot_set_color_by( + int32_t index, int32_t color_by) +{ + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + if (color_by == 0) { + plt->color_by_ = PlottableInterface::PlotColorBy::mats; + } else if (color_by == 1) { + plt->color_by_ = PlottableInterface::PlotColorBy::cells; + } else { + set_errmsg("Invalid color_by value for SolidRayTracePlot"); + return OPENMC_E_INVALID_ARGUMENT; + } + + return 0; +} + +extern "C" int openmc_solidraytrace_plot_set_default_colors(int32_t index) +{ + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + plt->set_default_colors(); + return 0; +} + +extern "C" int openmc_solidraytrace_plot_set_all_opaque(int32_t index) +{ + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + plt->opaque_ids().clear(); + if (plt->color_by_ == PlottableInterface::PlotColorBy::mats) { + for (int32_t i = 0; i < model::materials.size(); ++i) { + plt->opaque_ids().insert(i); + } + return 0; + } + + if (plt->color_by_ == PlottableInterface::PlotColorBy::cells) { + for (int32_t i = 0; i < model::cells.size(); ++i) { + plt->opaque_ids().insert(i); + } + return 0; + } + + set_errmsg("Unsupported color_by for SolidRayTracePlot"); + return OPENMC_E_INVALID_TYPE; +} + +extern "C" int openmc_solidraytrace_plot_set_opaque( + int32_t index, int32_t id, bool visible) +{ + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + int32_t domain_index = -1; + err = map_phong_domain_id(plt, id, &domain_index); + if (err) + return err; + + if (visible) { + plt->opaque_ids().insert(domain_index); + } else { + plt->opaque_ids().erase(domain_index); + } + + return 0; +} + +extern "C" int openmc_solidraytrace_plot_set_color( + int32_t index, int32_t id, uint8_t r, uint8_t g, uint8_t b) +{ + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + int32_t domain_index = -1; + err = map_phong_domain_id(plt, id, &domain_index); + if (err) + return err; + + if (domain_index < 0 || + static_cast(domain_index) >= plt->colors_.size()) { + set_errmsg("Color index out of range for SolidRayTracePlot"); + return OPENMC_E_OUT_OF_BOUNDS; + } + + plt->colors_[domain_index] = RGBColor(r, g, b); + return 0; +} + +extern "C" int openmc_solidraytrace_plot_get_camera_position( + int32_t index, double* x, double* y, double* z) +{ + if (!x || !y || !z) { + set_errmsg("Invalid arguments passed to " + "openmc_solidraytrace_plot_get_camera_position"); + return OPENMC_E_INVALID_ARGUMENT; + } + + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + const auto& camera_position = plt->camera_position(); + *x = camera_position.x; + *y = camera_position.y; + *z = camera_position.z; + return 0; +} + +extern "C" int openmc_solidraytrace_plot_set_camera_position( + int32_t index, double x, double y, double z) +{ + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + plt->camera_position() = {x, y, z}; + return 0; +} + +extern "C" int openmc_solidraytrace_plot_get_look_at( + int32_t index, double* x, double* y, double* z) +{ + if (!x || !y || !z) { + set_errmsg( + "Invalid arguments passed to openmc_solidraytrace_plot_get_look_at"); + return OPENMC_E_INVALID_ARGUMENT; + } + + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + const auto& look_at = plt->look_at(); + *x = look_at.x; + *y = look_at.y; + *z = look_at.z; + return 0; +} + +extern "C" int openmc_solidraytrace_plot_set_look_at( + int32_t index, double x, double y, double z) +{ + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + plt->look_at() = {x, y, z}; + return 0; +} + +extern "C" int openmc_solidraytrace_plot_get_up( + int32_t index, double* x, double* y, double* z) +{ + if (!x || !y || !z) { + set_errmsg("Invalid arguments passed to openmc_solidraytrace_plot_get_up"); + return OPENMC_E_INVALID_ARGUMENT; + } + + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + const auto& up = plt->up(); + *x = up.x; + *y = up.y; + *z = up.z; + return 0; +} + +extern "C" int openmc_solidraytrace_plot_set_up( + int32_t index, double x, double y, double z) +{ + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + plt->up() = {x, y, z}; + return 0; +} + +extern "C" int openmc_solidraytrace_plot_get_light_position( + int32_t index, double* x, double* y, double* z) +{ + if (!x || !y || !z) { + set_errmsg("Invalid arguments passed to " + "openmc_solidraytrace_plot_get_light_position"); + return OPENMC_E_INVALID_ARGUMENT; + } + + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + const auto& light_position = plt->light_location(); + *x = light_position.x; + *y = light_position.y; + *z = light_position.z; + return 0; +} + +extern "C" int openmc_solidraytrace_plot_set_light_position( + int32_t index, double x, double y, double z) +{ + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + plt->light_location() = {x, y, z}; + return 0; +} + +extern "C" int openmc_solidraytrace_plot_get_fov(int32_t index, double* fov) +{ + if (!fov) { + set_errmsg("Invalid arguments passed to openmc_solidraytrace_plot_get_fov"); + return OPENMC_E_INVALID_ARGUMENT; + } + + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + *fov = plt->horizontal_field_of_view(); + return 0; +} + +extern "C" int openmc_solidraytrace_plot_set_fov(int32_t index, double fov) +{ + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + plt->horizontal_field_of_view() = fov; + return 0; +} + +extern "C" int openmc_solidraytrace_plot_update_view(int32_t index) +{ + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + plt->update_view(); + return 0; +} + +extern "C" int openmc_solidraytrace_plot_create_image( + int32_t index, uint8_t* data_out, int32_t width, int32_t height) +{ + if (!data_out || width <= 0 || height <= 0) { + set_errmsg( + "Invalid arguments passed to openmc_solidraytrace_plot_create_image"); + return OPENMC_E_INVALID_ARGUMENT; + } + + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + if (plt->pixels()[0] != width || plt->pixels()[1] != height) { + set_errmsg( + "Requested image size does not match SolidRayTracePlot pixel settings"); + return OPENMC_E_INVALID_SIZE; + } + + ImageData data = plt->create_image(); + if (static_cast(data.shape()[0]) != width || + static_cast(data.shape()[1]) != height) { + set_errmsg("Unexpected image size from SolidRayTracePlot create_image"); + return OPENMC_E_INVALID_SIZE; + } + + for (int32_t y = 0; y < height; ++y) { + for (int32_t x = 0; x < width; ++x) { + const auto& color = data(x, y); + size_t idx = (static_cast(y) * width + x) * 3; + data_out[idx + 0] = color.red; + data_out[idx + 1] = color.green; + data_out[idx + 2] = color.blue; + } + } + + return 0; +} + +extern "C" int openmc_solidraytrace_plot_get_color( + int32_t index, int32_t id, uint8_t* r, uint8_t* g, uint8_t* b) +{ + if (!r || !g || !b) { + set_errmsg( + "Invalid arguments passed to openmc_solidraytrace_plot_get_color"); + return OPENMC_E_INVALID_ARGUMENT; + } + + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + int32_t domain_index = -1; + err = map_phong_domain_id(plt, id, &domain_index); + if (err) + return err; + + if (domain_index < 0 || + static_cast(domain_index) >= plt->colors_.size()) { + set_errmsg("Color index out of range for SolidRayTracePlot"); + return OPENMC_E_OUT_OF_BOUNDS; + } + + const auto& color = plt->colors_[domain_index]; + *r = color.red; + *g = color.green; + *b = color.blue; + return 0; +} + +extern "C" int openmc_solidraytrace_plot_get_diffuse_fraction( + int32_t index, double* diffuse_fraction) +{ + if (!diffuse_fraction) { + set_errmsg("Invalid arguments passed to " + "openmc_solidraytrace_plot_get_diffuse_fraction"); + return OPENMC_E_INVALID_ARGUMENT; + } + + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + *diffuse_fraction = plt->diffuse_fraction(); + return 0; +} + +extern "C" int openmc_solidraytrace_plot_set_diffuse_fraction( + int32_t index, double diffuse_fraction) +{ + SolidRayTracePlot* plt = nullptr; + int err = get_solidraytrace_plot_by_index(index, &plt); + if (err) + return err; + + if (diffuse_fraction < 0.0 || diffuse_fraction > 1.0) { + set_errmsg("Diffuse fraction must be between 0 and 1"); + return OPENMC_E_INVALID_ARGUMENT; + } + + plt->diffuse_fraction() = diffuse_fraction; + return 0; +} + } // namespace openmc diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index 3ec2a9ae4..8ef8d5927 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -934,6 +934,60 @@ def test_property_map(lib_init): assert np.allclose(expected_properties, properties, atol=1e-04) +def test_solid_raytrace_plot(lib_init, pincell_model): + # Ensure plot mapping can be accessed and grows after allocation + n0 = len(openmc.lib.plots) + plot = openmc.lib.SolidRayTracePlot() + assert len(openmc.lib.plots) == n0 + 1 + assert plot.id in openmc.lib.plots + assert openmc.lib.plots[plot.id] is plot + + # Exercise plot property getters/setters + plot.pixels = (8, 6) + assert plot.pixels == (8, 6) + + plot.color_by = openmc.lib.SolidRayTracePlot.COLOR_BY_MATERIAL + assert plot.color_by == openmc.lib.SolidRayTracePlot.COLOR_BY_MATERIAL + + plot.camera_position = (2.0, 0.0, 1.0) + plot.look_at = (0.0, 0.0, 0.0) + plot.up = (0.0, 0.0, 1.0) + plot.light_position = (3.0, 2.0, 4.0) + plot.fov = 60.0 + plot.diffuse_fraction = 0.4 + assert plot.camera_position == pytest.approx((2.0, 0.0, 1.0)) + assert plot.look_at == pytest.approx((0.0, 0.0, 0.0)) + assert plot.up == pytest.approx((0.0, 0.0, 1.0)) + assert plot.light_position == pytest.approx((3.0, 2.0, 4.0)) + assert plot.fov == pytest.approx(60.0) + assert plot.diffuse_fraction == pytest.approx(0.4) + + # Exercise color/visibility CAPI wrappers + plot.set_default_colors() + plot.set_color(1, (12, 34, 56)) + assert plot.get_color(1) == (12, 34, 56) + plot.set_visibility(1, False) + plot.set_visibility(1, True) + + # Confirm image creation path works and dimensions match pixels + plot.update_view() + image = plot.create_image() + assert image.shape == (6, 8, 3) + assert image.dtype == np.uint8 + + # Change some properties and confirm image changes + plot.set_color(1, (255, 0, 0)) + plot.update_view() + image2 = plot.create_image() + assert not np.array_equal(image, image2) + + # Solid raytrace uses Phong/diffuse shading, so rendered RGB values are + # generally modulated and need not exactly match the assigned palette. + changed = np.any(image != image2, axis=2) + assert np.any(changed) + assert np.mean(image2[..., 0][changed]) > np.mean(image[..., 0][changed]) + + def test_position(lib_init): pos = openmc.lib.plot._Position(1.0, 2.0, 3.0) From 19c0aafdc6f181ef298e625111364902da3e04ef Mon Sep 17 00:00:00 2001 From: Kevin Sawatzky <66632997+nuclearkevin@users.noreply.github.com> Date: Fri, 13 Feb 2026 10:52:10 -0600 Subject: [PATCH 544/671] Fix `None` values appearing in cross section data generated with `Model.autoconvert` (#3802) --- openmc/model/model.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index 82fe87ca5..a512c4d38 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -2052,7 +2052,8 @@ class Model: # Unpack the isothermal XSData objects and build a single XSData object per material. mgxs_sets = [] for m in range(len(self.materials)): - mgxs_sets.append(openmc.XSdata(self.materials[m].name, groups)) + mgxs_sets.append(openmc.XSdata(self.materials[m].name, groups, + temperatures=temperatures)) mgxs_sets[-1].order = 0 for temperature in temperatures: mgxs_sets[-1].add_temperature_data(raw_mgxs_sets[temperature][m]) @@ -2333,7 +2334,7 @@ class Model: # Unpack the isothermal XSData objects and build a single XSData object per material. mgxs_sets = [] for mat in self.materials: - mgxs_sets.append(openmc.XSdata(mat.name, groups)) + mgxs_sets.append(openmc.XSdata(mat.name, groups, temperatures=temperatures)) mgxs_sets[-1].order = 0 for temperature in temperatures: mgxs_sets[-1].add_temperature_data(raw_mgxs_sets[temperature][mat.name]) @@ -2500,7 +2501,7 @@ class Model: # Unpack the isothermal XSData objects and build a single XSData object per material. mgxs_sets = [] for mat in self.materials: - mgxs_sets.append(openmc.XSdata(mat.name, groups)) + mgxs_sets.append(openmc.XSdata(mat.name, groups, temperatures=temperatures)) mgxs_sets[-1].order = 0 for temperature in temperatures: mgxs_sets[-1].add_temperature_data(raw_mgxs_sets[temperature][mat.name]) From 7fc5b948777468b1579bca78e85cfd1c2a754a6b Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Sat, 14 Feb 2026 05:14:03 +0200 Subject: [PATCH 545/671] Store atomic mass in ParticleType. (#3765) Co-authored-by: Paul Romano --- CMakeLists.txt | 1 + include/openmc/atomic_mass.h | 28 + include/openmc/constants.h | 7 +- include/openmc/particle_type.h | 12 + src/atomic_mass.cpp | 3569 ++++++++++++++++++++++++++++++++ src/particle.cpp | 10 +- 6 files changed, 3617 insertions(+), 10 deletions(-) create mode 100644 include/openmc/atomic_mass.h create mode 100644 src/atomic_mass.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d3119fb87..b204b45fc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -332,6 +332,7 @@ endif() #=============================================================================== list(APPEND libopenmc_SOURCES + src/atomic_mass.cpp src/bank.cpp src/boundary_condition.cpp src/bremsstrahlung.cpp diff --git a/include/openmc/atomic_mass.h b/include/openmc/atomic_mass.h new file mode 100644 index 000000000..b4d515d2e --- /dev/null +++ b/include/openmc/atomic_mass.h @@ -0,0 +1,28 @@ +//============================================================================== +// atomic masses definitions +//============================================================================== + +#ifndef OPENMC_ATOMIC_MASS_H +#define OPENMC_ATOMIC_MASS_H + +#include +#include + +namespace openmc { + +// Values here are from the Committee on Data for Science and Technology +// (CODATA) 2018 recommendation (https://physics.nist.gov/cuu/Constants/). + +// Physical constants +constexpr double MASS_ELECTRON {5.48579909065e-4}; // mass of an electron in amu +constexpr double MASS_NEUTRON {1.00866491595}; // mass of a neutron in amu +constexpr double MASS_PROTON {1.007276466621}; // mass of a proton in amu +constexpr double MASS_DEUTRON {2.013553212745}; // mass of a deutron in amu +constexpr double MASS_HELION {3.014932247175}; // mass of a helion in amu +constexpr double MASS_ALPHA {4.001506179127}; // mass of an alpha in amu + +extern std::unordered_map ATOMIC_MASS; + +} // namespace openmc + +#endif // OPENMC_ATOMIC_MASS_H diff --git a/include/openmc/constants.h b/include/openmc/constants.h index 8185214fc..38238598e 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -9,6 +9,7 @@ #include #include "openmc/array.h" +#include "openmc/atomic_mass.h" #include "openmc/vector.h" #include "openmc/version.h" @@ -86,10 +87,10 @@ constexpr double INFTY {std::numeric_limits::max()}; // (CODATA) 2018 recommendation (https://physics.nist.gov/cuu/Constants/). // Physical constants -constexpr double MASS_NEUTRON {1.00866491595}; // mass of a neutron in amu +constexpr double AMU_EV { + 9.3149410242e8}; // atomic mass unit energy equivalent in eV/c^2 constexpr double MASS_NEUTRON_EV { - 939.56542052e6}; // mass of a neutron in eV/c^2 -constexpr double MASS_PROTON {1.007276466621}; // mass of a proton in amu + 939.56542052e6}; // neutron mass energy equivalent in eV/c^2 constexpr double MASS_ELECTRON_EV { 0.51099895000e6}; // electron mass energy equivalent in eV/c^2 constexpr double FINE_STRUCTURE { diff --git a/include/openmc/particle_type.h b/include/openmc/particle_type.h index fed3c9284..0fd896c57 100644 --- a/include/openmc/particle_type.h +++ b/include/openmc/particle_type.h @@ -12,6 +12,7 @@ #include #include "openmc/constants.h" +#include "openmc/error.h" namespace openmc { @@ -61,6 +62,17 @@ public: //---------------------------------------------------------------------------- // Methods + // Get particle mass in [u] + double mass() const + { + int32_t p = std::abs(pdg_number_); + if (ATOMIC_MASS.count(p)) { + return ATOMIC_MASS[p]; + } else { + fatal_error("Unknown mass for particle " + str()); + } + } + // Convert to string representation std::string str() const; diff --git a/src/atomic_mass.cpp b/src/atomic_mass.cpp new file mode 100644 index 000000000..0e63c9603 --- /dev/null +++ b/src/atomic_mass.cpp @@ -0,0 +1,3569 @@ +#include "openmc/atomic_mass.h" + +namespace openmc { + +// Atomic masses in [u] from AME2020 and CODATA 2018 +std::unordered_map ATOMIC_MASS = { + {11, MASS_ELECTRON}, + {22, 0.0}, + {2112, MASS_NEUTRON}, + {2212, MASS_PROTON}, + {1000010020, MASS_DEUTRON}, + {1000020030, MASS_HELION}, + {1000020040, MASS_ALPHA}, + {1000010030, 3.01604928132}, + {1000030030, 3.030775}, + {1000010040, 4.026431867}, + {1000030040, 4.027185561}, + {1000010050, 5.035311492}, + {1000020050, 5.012057224}, + {1000030050, 5.0125378}, + {1000040050, 5.03987}, + {1000010060, 6.044955437}, + {1000020060, 6.018885889}, + {1000030060, 6.01512288742}, + {1000040060, 6.0197264090000004}, + {1000050060, 6.0508}, + {1000010070, 7.052749}, + {1000020070, 7.027990652}, + {1000030070, 7.01600343426}, + {1000040070, 7.016928714}, + {1000050070, 7.029712}, + {1000020080, 8.033934388}, + {1000030080, 8.022486244}, + {1000040080, 8.005305102}, + {1000050080, 8.024607315}, + {1000060080, 8.037643039}, + {1000020090, 9.043946414}, + {1000030090, 9.026790191}, + {1000040090, 9.012183062}, + {1000050090, 9.013329645}, + {1000060090, 9.031037202}, + {1000020100, 10.052815306}, + {1000030100, 10.035483453}, + {1000040100, 10.013534692}, + {1000050100, 10.012936862}, + {1000060100, 10.016853217}, + {1000070100, 10.04165354}, + {1000030110, 11.043723581}, + {1000040110, 11.02166108}, + {1000050110, 11.009305166}, + {1000060110, 11.011432597}, + {1000070110, 11.026157593}, + {1000080110, 11.051249828}, + {1000030120, 12.052613942}, + {1000040120, 12.026922082}, + {1000050120, 12.014352638}, + {1000060120, 12.0}, + {1000070120, 12.01861318}, + {1000080120, 12.034367726}, + {1000030130, 13.061171503}, + {1000040130, 13.036134506}, + {1000050130, 13.017779981}, + {1000060130, 13.00335483534}, + {1000070130, 13.005738609}, + {1000080130, 13.024815435}, + {1000090130, 13.045121}, + {1000040140, 14.04289292}, + {1000050140, 14.02540401}, + {1000060140, 14.00324198862}, + {1000070140, 14.00307400425}, + {1000080140, 14.008596706}, + {1000090140, 14.034315196}, + {1000040150, 15.053490215}, + {1000050150, 15.031087023}, + {1000060150, 15.010599256}, + {1000070150, 15.00010889827}, + {1000080150, 15.003065636}, + {1000090150, 15.017785139}, + {1000100150, 15.043172977}, + {1000040160, 16.061672036}, + {1000050160, 16.039841045}, + {1000060160, 16.014701255}, + {1000070160, 16.006101925}, + {1000080160, 15.99491461926}, + {1000090160, 16.011460278}, + {1000100160, 16.02575086}, + {1000050170, 17.046931399}, + {1000060170, 17.02257865}, + {1000070170, 17.008448876}, + {1000080170, 16.99913175595}, + {1000090170, 17.002095237}, + {1000100170, 17.017713962}, + {1000110170, 17.037273}, + {1000050180, 18.055601683}, + {1000060180, 18.02675193}, + {1000070180, 18.014077563}, + {1000080180, 17.99915961214}, + {1000090180, 18.000937324}, + {1000100180, 18.005708696}, + {1000110180, 18.026879388}, + {1000050190, 19.064166}, + {1000060190, 19.034797594}, + {1000070190, 19.017022389}, + {1000080190, 19.003577969}, + {1000090190, 18.99840316207}, + {1000100190, 19.001880906}, + {1000110190, 19.013880264}, + {1000120190, 19.03417992}, + {1000050200, 20.074505644}, + {1000060200, 20.040261732}, + {1000070200, 20.023367295}, + {1000080200, 20.004075357}, + {1000090200, 19.999981252}, + {1000100200, 19.99244017525}, + {1000110200, 20.007354301}, + {1000120200, 20.018763075}, + {1000050210, 21.084147485}, + {1000060210, 21.049}, + {1000070210, 21.027087573}, + {1000080210, 21.008654948}, + {1000090210, 20.999948893}, + {1000100210, 20.993846685}, + {1000110210, 20.997654459}, + {1000120210, 21.011705764}, + {1000130210, 21.029082}, + {1000060220, 22.05755399}, + {1000070220, 22.034100918}, + {1000080220, 22.009965744}, + {1000090220, 22.002998812}, + {1000100220, 21.991385113}, + {1000110220, 21.994437547}, + {1000120220, 21.999570597}, + {1000130220, 22.01954}, + {1000140220, 22.036114}, + {1000060230, 23.06889}, + {1000070230, 23.039421}, + {1000080230, 23.015696686}, + {1000090230, 23.003526875}, + {1000100230, 22.994466905}, + {1000110230, 22.98976928195}, + {1000120230, 22.994123768}, + {1000130230, 23.007244351}, + {1000140230, 23.025711}, + {1000070240, 24.05039}, + {1000080240, 24.019861}, + {1000090240, 24.00809937}, + {1000100240, 23.993610649}, + {1000110240, 23.990963012}, + {1000120240, 23.985041689}, + {1000130240, 23.999947598}, + {1000140240, 24.01153543}, + {1000150240, 24.036522}, + {1000070250, 25.0601}, + {1000080250, 25.029338919}, + {1000090250, 25.012167727}, + {1000100250, 24.997814797}, + {1000110250, 24.989953974}, + {1000120250, 24.985836966}, + {1000130250, 24.990428308}, + {1000140250, 25.004108798}, + {1000150250, 25.021675}, + {1000080260, 26.037210155}, + {1000090260, 26.020048065}, + {1000100260, 26.000516496}, + {1000110260, 25.992634649}, + {1000120260, 25.982592972}, + {1000130260, 25.986891876}, + {1000140260, 25.992333818}, + {1000150260, 26.01178}, + {1000160260, 26.029716}, + {1000080270, 27.047955}, + {1000090270, 27.026981897}, + {1000100270, 27.007569462}, + {1000110270, 26.994076408}, + {1000120270, 26.984340647}, + {1000130270, 26.981538408}, + {1000140270, 26.986704687}, + {1000150270, 26.999292499}, + {1000160270, 27.018777}, + {1000080280, 28.05591}, + {1000090280, 28.035860448}, + {1000100280, 28.012130767}, + {1000110280, 27.998939}, + {1000120280, 27.983875426}, + {1000130280, 27.981910009}, + {1000140280, 27.97692653442}, + {1000150280, 27.99232646}, + {1000160280, 28.004372762}, + {1000170280, 28.030349}, + {1000090290, 29.043103}, + {1000100290, 29.019753}, + {1000110290, 29.002877091}, + {1000120290, 28.988607163}, + {1000130290, 28.980453164}, + {1000140290, 28.97649466434}, + {1000150290, 28.981800368}, + {1000160290, 28.996678}, + {1000170290, 29.015053}, + {1000180290, 29.040761}, + {1000090300, 30.052561}, + {1000100300, 30.024992235}, + {1000110300, 30.009097931}, + {1000120300, 29.990465454}, + {1000130300, 29.982969171}, + {1000140300, 29.973770137}, + {1000150300, 29.97831349}, + {1000160300, 29.98490677}, + {1000170300, 30.005018333}, + {1000180300, 30.023694}, + {1000090310, 31.061023}, + {1000100310, 31.033474816}, + {1000110310, 31.013146654}, + {1000120310, 30.996648232}, + {1000130310, 30.983949754}, + {1000140310, 30.975363196}, + {1000150310, 30.97376199768}, + {1000160310, 30.979557002}, + {1000170310, 30.992448097}, + {1000180310, 31.012158}, + {1000190310, 31.03678}, + {1000100320, 32.03972}, + {1000110320, 32.020011024}, + {1000120320, 31.999110138}, + {1000130320, 31.988084338}, + {1000140320, 31.974151538}, + {1000150320, 31.973907643}, + {1000160320, 31.97207117354}, + {1000170320, 31.985684605}, + {1000180320, 31.997637824}, + {1000190320, 32.023607}, + {1000100330, 33.049523}, + {1000110330, 33.025529}, + {1000120330, 33.005327862}, + {1000130330, 32.990877685}, + {1000140330, 32.977976964}, + {1000150330, 32.971725692}, + {1000160330, 32.97145890862}, + {1000170330, 32.977451988}, + {1000180330, 32.989925545}, + {1000190330, 33.008095}, + {1000200330, 33.033312}, + {1000100340, 34.056728}, + {1000110340, 34.03401}, + {1000120340, 34.008935455}, + {1000130340, 33.996781924}, + {1000140340, 33.978538045}, + {1000150340, 33.973645886}, + {1000160340, 33.967867011}, + {1000170340, 33.97376249}, + {1000180340, 33.980270092}, + {1000190340, 33.99869}, + {1000200340, 34.015985}, + {1000110350, 35.040614}, + {1000120350, 35.01679}, + {1000130350, 34.999759816}, + {1000140350, 34.984550111}, + {1000150350, 34.973314045}, + {1000160350, 34.969032321}, + {1000170350, 34.968852694}, + {1000180350, 34.975257719}, + {1000190350, 34.988005406}, + {1000200350, 35.005572}, + {1000210350, 35.029093}, + {1000110360, 36.049279}, + {1000120360, 36.021879}, + {1000130360, 36.006388}, + {1000140360, 35.986649271}, + {1000150360, 35.97825961}, + {1000160360, 35.967080692}, + {1000170360, 35.968306822}, + {1000180360, 35.967545106}, + {1000190360, 35.981301887}, + {1000200360, 35.993074388}, + {1000210360, 36.017338}, + {1000110370, 37.057042}, + {1000120370, 37.030286265}, + {1000130370, 37.010531}, + {1000140370, 36.992945191}, + {1000150370, 36.979606942}, + {1000160370, 36.9711255}, + {1000170370, 36.965902573}, + {1000180370, 36.966776301}, + {1000190370, 36.97337589}, + {1000200370, 36.985897849}, + {1000210370, 37.004058}, + {1000220370, 37.027021}, + {1000110380, 38.066458}, + {1000120380, 38.03658}, + {1000130380, 38.017681}, + {1000140380, 37.995523}, + {1000150380, 37.984303105}, + {1000160380, 37.9711633}, + {1000170380, 37.968010408}, + {1000180380, 37.962732102}, + {1000190380, 37.969081114}, + {1000200380, 37.976319223}, + {1000210380, 37.995438}, + {1000220380, 38.012206}, + {1000110390, 39.075123}, + {1000120390, 39.045921}, + {1000130390, 39.02307}, + {1000140390, 39.002491}, + {1000150390, 38.986285865}, + {1000160390, 38.97513385}, + {1000170390, 38.968008151}, + {1000180390, 38.964313037}, + {1000190390, 38.96370648482}, + {1000200390, 38.970710811}, + {1000210390, 38.984784953}, + {1000220390, 39.002684}, + {1000230390, 39.02423}, + {1000120400, 40.053194}, + {1000130400, 40.03094}, + {1000140400, 40.006083641}, + {1000150400, 39.991262221}, + {1000160400, 39.975482561}, + {1000170400, 39.970415466}, + {1000180400, 39.96238312204}, + {1000190400, 39.963998165}, + {1000200400, 39.96259085}, + {1000210400, 39.977967275}, + {1000220400, 39.990345146}, + {1000230400, 40.013387}, + {1000120410, 41.062373}, + {1000130410, 41.037134}, + {1000140410, 41.014171}, + {1000150410, 40.994654}, + {1000160410, 40.979593451}, + {1000170410, 40.970684525}, + {1000180410, 40.96450057}, + {1000190410, 40.96182525611}, + {1000200410, 40.962277905}, + {1000210410, 40.969251163}, + {1000220410, 40.983148}, + {1000230410, 41.000333}, + {1000240410, 41.021911}, + {1000130420, 42.045078}, + {1000140420, 42.018078}, + {1000150420, 42.00117214}, + {1000160420, 41.9810651}, + {1000170420, 41.973342}, + {1000180420, 41.963045737}, + {1000190420, 41.962402305}, + {1000200420, 41.95861778}, + {1000210420, 41.965516686}, + {1000220420, 41.973049369}, + {1000230420, 41.99182}, + {1000240420, 42.007579}, + {1000130430, 43.05182}, + {1000140430, 43.026119}, + {1000150430, 43.005411}, + {1000160430, 42.986907635}, + {1000170430, 42.9740637}, + {1000180430, 42.965636056}, + {1000190430, 42.960734701}, + {1000200430, 42.958766381}, + {1000210430, 42.961150425}, + {1000220430, 42.96852842}, + {1000230430, 42.980766}, + {1000240430, 42.997885}, + {1000250430, 43.018647}, + {1000140440, 44.031466}, + {1000150440, 44.011927}, + {1000160440, 43.990118846}, + {1000170440, 43.978014918}, + {1000180440, 43.964923814}, + {1000190440, 43.961586984}, + {1000200440, 43.955481489}, + {1000210440, 43.959402818}, + {1000220440, 43.959689936}, + {1000230440, 43.974440977}, + {1000240440, 43.985591}, + {1000250440, 44.008009}, + {1000140450, 45.039818}, + {1000150450, 45.017134}, + {1000160450, 44.996414}, + {1000170450, 44.980394353}, + {1000180450, 44.968039731}, + {1000190450, 44.960691491}, + {1000200450, 44.95618627}, + {1000210450, 44.955907051}, + {1000220450, 44.958120758}, + {1000230450, 44.965768498}, + {1000240450, 44.97905}, + {1000250450, 44.994654}, + {1000260450, 45.015467}, + {1000150460, 46.02452}, + {1000160460, 46.000687}, + {1000170460, 45.985254926}, + {1000180460, 45.968039243999996}, + {1000190460, 45.961981584}, + {1000200460, 45.953687726}, + {1000210460, 45.955167034}, + {1000220460, 45.952626356}, + {1000230460, 45.960197389}, + {1000240460, 45.968360969}, + {1000250460, 45.986669}, + {1000260460, 46.001299}, + {1000150470, 47.030929}, + {1000160470, 47.00773}, + {1000170470, 46.989715}, + {1000180470, 46.972767112}, + {1000190470, 46.961661612}, + {1000200470, 46.954541134}, + {1000210470, 46.952402444}, + {1000220470, 46.951757491}, + {1000230470, 46.954903558}, + {1000240470, 46.962894995}, + {1000250470, 46.975774}, + {1000260470, 46.992346}, + {1000270470, 47.011401}, + {1000160480, 48.013301}, + {1000170480, 47.995405}, + {1000180480, 47.976001}, + {1000190480, 47.965341184}, + {1000200480, 47.952522654}, + {1000210480, 47.952222903}, + {1000220480, 47.947940677}, + {1000230480, 47.9522509}, + {1000240480, 47.954029431}, + {1000250480, 47.96854876}, + {1000260480, 47.980667}, + {1000270480, 48.001857}, + {1000280480, 48.019515}, + {1000160490, 49.021891}, + {1000170490, 49.000794}, + {1000180490, 48.981685}, + {1000190490, 48.968210753}, + {1000200490, 48.955662625}, + {1000210490, 48.950013159}, + {1000220490, 48.947864391}, + {1000230490, 48.948510509}, + {1000240490, 48.95133372}, + {1000250490, 48.95961335}, + {1000260490, 48.973429}, + {1000270490, 48.989501}, + {1000280490, 49.009157}, + {1000170500, 50.008266}, + {1000180500, 49.985797}, + {1000190500, 49.972380015}, + {1000200500, 49.957499215}, + {1000210500, 49.952187437}, + {1000220500, 49.944785622}, + {1000230500, 49.947156681}, + {1000240500, 49.946042209}, + {1000250500, 49.954238157}, + {1000260500, 49.962988}, + {1000270500, 49.981117}, + {1000280500, 49.996286}, + {1000170510, 51.015341}, + {1000180510, 50.993033}, + {1000190510, 50.975828664}, + {1000200510, 50.960995663}, + {1000210510, 50.953568838}, + {1000220510, 50.946609468}, + {1000230510, 50.943957664}, + {1000240510, 50.944765388}, + {1000250510, 50.94820877}, + {1000260510, 50.956855137}, + {1000270510, 50.970647}, + {1000280510, 50.987493}, + {1000170520, 52.024004}, + {1000180520, 51.998519}, + {1000190520, 51.981602}, + {1000200520, 51.963213646}, + {1000210520, 51.95649617}, + {1000220520, 51.946883509}, + {1000230520, 51.944773636}, + {1000240520, 51.940504714}, + {1000250520, 51.94555909}, + {1000260520, 51.948113364}, + {1000270520, 51.963130224}, + {1000280520, 51.975781}, + {1000290520, 51.997982}, + {1000180530, 53.00729}, + {1000190530, 52.9868}, + {1000200530, 52.968451}, + {1000210530, 52.958379173}, + {1000220530, 52.949670714}, + {1000230530, 52.94433494}, + {1000240530, 52.940646304}, + {1000250530, 52.941287497}, + {1000260530, 52.945305629}, + {1000270530, 52.954203278}, + {1000280530, 52.96819}, + {1000290530, 52.985894}, + {1000180540, 54.013484}, + {1000190540, 53.994471}, + {1000200540, 53.972989}, + {1000210540, 53.963029359}, + {1000220540, 53.950892}, + {1000230540, 53.946432009}, + {1000240540, 53.938877359}, + {1000250540, 53.940355772}, + {1000260540, 53.939608189}, + {1000270540, 53.948459075}, + {1000280540, 53.957833}, + {1000290540, 53.977198}, + {1000300540, 53.993879}, + {1000190550, 55.000505}, + {1000200550, 54.979978}, + {1000210550, 54.966889637}, + {1000220550, 54.955091}, + {1000230550, 54.947262}, + {1000240550, 54.940836637}, + {1000250550, 54.93804304}, + {1000260550, 54.938291158}, + {1000270550, 54.941996416}, + {1000280550, 54.951329846}, + {1000290550, 54.966038}, + {1000300550, 54.984681}, + {1000190560, 56.008567}, + {1000200560, 55.985496}, + {1000210560, 55.972607611}, + {1000220560, 55.957677675}, + {1000230560, 55.950420082}, + {1000240560, 55.940648977}, + {1000250560, 55.938902816}, + {1000260560, 55.934935537}, + {1000270560, 55.939838032}, + {1000280560, 55.942127761}, + {1000290560, 55.958529278}, + {1000300560, 55.972743}, + {1000310560, 55.995878}, + {1000190570, 57.015169}, + {1000200570, 56.992958}, + {1000210570, 56.977047999999996}, + {1000220570, 56.963068098}, + {1000230570, 56.952297}, + {1000240570, 56.943612112}, + {1000250570, 56.938285944}, + {1000260570, 56.93539195}, + {1000270570, 56.936289819}, + {1000280570, 56.939791394}, + {1000290570, 56.949211686}, + {1000300570, 56.965056}, + {1000310570, 56.983457}, + {1000190580, 58.023543}, + {1000200580, 57.998357}, + {1000210580, 57.983382}, + {1000220580, 57.966808519}, + {1000230580, 57.956595985}, + {1000240580, 57.944184501}, + {1000250580, 57.940066643}, + {1000260580, 57.933273575}, + {1000270580, 57.935751292}, + {1000280580, 57.93534165}, + {1000290580, 57.944532283}, + {1000300580, 57.954590296}, + {1000310580, 57.974728999999996}, + {1000320580, 57.991863}, + {1000190590, 59.030864}, + {1000200590, 59.006237}, + {1000210590, 58.988374}, + {1000220590, 58.972217}, + {1000230590, 58.959623343}, + {1000240590, 58.948345426}, + {1000250590, 58.940391111}, + {1000260590, 58.934873492}, + {1000270590, 58.933193523999996}, + {1000280590, 58.934345442}, + {1000290590, 58.939496713}, + {1000300590, 58.949311886}, + {1000310590, 58.963757}, + {1000320590, 58.982426}, + {1000200600, 60.011809}, + {1000210600, 59.995115}, + {1000220600, 59.976275}, + {1000230600, 59.964479215}, + {1000240600, 59.949641656}, + {1000250600, 59.943136574}, + {1000260600, 59.934070249}, + {1000270600, 59.933815536}, + {1000280600, 59.930785129}, + {1000290600, 59.937363787}, + {1000300600, 59.941841317}, + {1000310600, 59.957498}, + {1000320600, 59.970445}, + {1000330600, 59.993945}, + {1000200610, 61.020408}, + {1000210610, 61.000537}, + {1000220610, 60.982426}, + {1000230610, 60.967603529}, + {1000240610, 60.95437813}, + {1000250610, 60.944452541}, + {1000260610, 60.936746241}, + {1000270610, 60.932476031}, + {1000280610, 60.931054819}, + {1000290610, 60.933457375}, + {1000300610, 60.939506964}, + {1000310610, 60.949398861}, + {1000320610, 60.963725}, + {1000330610, 60.981535}, + {1000210620, 62.007848}, + {1000220620, 61.986903}, + {1000230620, 61.972932556}, + {1000240620, 61.95614292}, + {1000250620, 61.947907384}, + {1000260620, 61.936791809}, + {1000270620, 61.934058198}, + {1000280620, 61.928344753}, + {1000290620, 61.932594803}, + {1000300620, 61.934333359}, + {1000310620, 61.944189639}, + {1000320620, 61.954761}, + {1000330620, 61.973784}, + {1000210630, 63.014031}, + {1000220630, 62.993709}, + {1000230630, 62.976661}, + {1000240630, 62.961161}, + {1000250630, 62.949664672}, + {1000260630, 62.940272698}, + {1000270630, 62.93359963}, + {1000280630, 62.929669021}, + {1000290630, 62.929597119}, + {1000300630, 62.93321114}, + {1000310630, 62.939294194}, + {1000320630, 62.949628}, + {1000330630, 62.964036}, + {1000340630, 62.981911}, + {1000220640, 63.998411}, + {1000230640, 63.98248}, + {1000240640, 63.963886}, + {1000250640, 63.953849369}, + {1000260640, 63.940987761}, + {1000270640, 63.935810176}, + {1000280640, 63.927966228}, + {1000290640, 63.929764001}, + {1000300640, 63.929141776}, + {1000310640, 63.936840366}, + {1000320640, 63.941689912}, + {1000330640, 63.95756}, + {1000340640, 63.971165}, + {1000220650, 65.005593}, + {1000230650, 64.986999}, + {1000240650, 64.969608}, + {1000250650, 64.956019749}, + {1000260650, 64.945015323}, + {1000270650, 64.936462071}, + {1000280650, 64.930084585}, + {1000290650, 64.927789476}, + {1000300650, 64.929240534}, + {1000310650, 64.932734424}, + {1000320650, 64.939368136}, + {1000330650, 64.949611}, + {1000340650, 64.964552}, + {1000350650, 64.982297}, + {1000230660, 65.993237}, + {1000240660, 65.973011}, + {1000250660, 65.960546833}, + {1000260660, 65.946249958}, + {1000270660, 65.939442943}, + {1000280660, 65.929139333}, + {1000290660, 65.928868804}, + {1000300660, 65.926033639}, + {1000310660, 65.931589766}, + {1000320660, 65.933862124}, + {1000330660, 65.944148778}, + {1000340660, 65.955276}, + {1000350660, 65.974697}, + {1000230670, 66.998128}, + {1000240670, 66.979313}, + {1000250670, 66.96395}, + {1000260670, 66.95093}, + {1000270670, 66.940609625}, + {1000280670, 66.931569413}, + {1000290670, 66.92772949}, + {1000300670, 66.927127422}, + {1000310670, 66.928202276}, + {1000320670, 66.932716999}, + {1000330670, 66.93925111}, + {1000340670, 66.949994}, + {1000350670, 66.965078}, + {1000360670, 66.983305}, + {1000240680, 67.983156}, + {1000250680, 67.968953}, + {1000260680, 67.952875}, + {1000270680, 67.944559401}, + {1000280680, 67.931868787}, + {1000290680, 67.929610887}, + {1000300680, 67.924844232}, + {1000310680, 67.927980161}, + {1000320680, 67.928095305}, + {1000330680, 67.936774127}, + {1000340680, 67.941825236}, + {1000350680, 67.958356}, + {1000360680, 67.972489}, + {1000240690, 68.989662}, + {1000250690, 68.972775}, + {1000260690, 68.957918}, + {1000270690, 68.945909}, + {1000280690, 68.935610267}, + {1000290690, 68.929429267}, + {1000300690, 68.92655036}, + {1000310690, 68.925573528}, + {1000320690, 68.927964467}, + {1000330690, 68.932246289}, + {1000340690, 68.939414845}, + {1000350690, 68.95033841}, + {1000360690, 68.965496}, + {1000240700, 69.993945}, + {1000250700, 69.978046}, + {1000260700, 69.960397}, + {1000270700, 69.9500534}, + {1000280700, 69.9364313}, + {1000290700, 69.932392078}, + {1000300700, 69.925319175}, + {1000310700, 69.926021914}, + {1000320700, 69.924248542}, + {1000330700, 69.930934642}, + {1000340700, 69.933515521}, + {1000350700, 69.944792321}, + {1000360700, 69.955877}, + {1000250710, 70.982158}, + {1000260710, 70.965722}, + {1000270710, 70.952366923}, + {1000280710, 70.940518962}, + {1000290710, 70.932676831}, + {1000300710, 70.927719578}, + {1000310710, 70.924702554}, + {1000320710, 70.92495212}, + {1000330710, 70.927113594}, + {1000340710, 70.932209431}, + {1000350710, 70.939342153}, + {1000360710, 70.950265695}, + {1000370710, 70.965335}, + {1000250720, 71.988009}, + {1000260720, 71.968599}, + {1000270720, 71.956736}, + {1000280720, 71.941785924}, + {1000290720, 71.935820306}, + {1000300720, 71.926842806}, + {1000310720, 71.926367452}, + {1000320720, 71.922075824}, + {1000330720, 71.926752291}, + {1000340720, 71.927140506}, + {1000350720, 71.936594606}, + {1000360720, 71.942092406}, + {1000370720, 71.958851}, + {1000250730, 72.992807}, + {1000260730, 72.974246}, + {1000270730, 72.959238}, + {1000280730, 72.946206681}, + {1000290730, 72.936674376}, + {1000300730, 72.92958258}, + {1000310730, 72.92517468}, + {1000320730, 72.923458954}, + {1000330730, 72.923829086}, + {1000340730, 72.926754881}, + {1000350730, 72.931673441}, + {1000360730, 72.939289193}, + {1000370730, 72.950604506}, + {1000380730, 72.9657}, + {1000260740, 73.977821}, + {1000270740, 73.963993}, + {1000280740, 73.947718}, + {1000290740, 73.93987486}, + {1000300740, 73.92940726}, + {1000310740, 73.926945725}, + {1000320740, 73.92117776}, + {1000330740, 73.923928596}, + {1000340740, 73.922475933}, + {1000350740, 73.929910279}, + {1000360740, 73.933084016}, + {1000370740, 73.944265867}, + {1000380740, 73.95617}, + {1000260750, 74.984219}, + {1000270750, 74.967192}, + {1000280750, 74.952506}, + {1000290750, 74.941523817}, + {1000300750, 74.932840244}, + {1000310750, 74.926504484}, + {1000320750, 74.92285837}, + {1000330750, 74.921594562}, + {1000340750, 74.92252287}, + {1000350750, 74.925810566}, + {1000360750, 74.930945744}, + {1000370750, 74.9385732}, + {1000380750, 74.949952767}, + {1000390750, 74.96584}, + {1000260760, 75.988631}, + {1000270760, 75.972453}, + {1000280760, 75.954707}, + {1000290760, 75.945268974}, + {1000300760, 75.933114956}, + {1000310760, 75.928827624}, + {1000320760, 75.921402725}, + {1000330760, 75.922392011}, + {1000340760, 75.919213702}, + {1000350760, 75.924541574}, + {1000360760, 75.925910743}, + {1000370760, 75.935073031}, + {1000380760, 75.94176276}, + {1000390760, 75.958937}, + {1000270770, 76.976479}, + {1000280770, 76.959903}, + {1000290770, 76.947543599}, + {1000300770, 76.936887197}, + {1000310770, 76.929154299}, + {1000320770, 76.923549843}, + {1000330770, 76.920647555}, + {1000340770, 76.91991415}, + {1000350770, 76.921379193}, + {1000360770, 76.924669999}, + {1000370770, 76.930401599}, + {1000380770, 76.937945454}, + {1000390770, 76.950146}, + {1000400770, 76.966076}, + {1000270780, 77.983553}, + {1000280780, 77.962555}, + {1000290780, 77.951916524}, + {1000300780, 77.938289204}, + {1000310780, 77.931610854}, + {1000320780, 77.922852911}, + {1000330780, 77.921827771}, + {1000340780, 77.917309244}, + {1000350780, 77.921145858}, + {1000360780, 77.920366341}, + {1000370780, 77.928141866}, + {1000380780, 77.932179979}, + {1000390780, 77.94399}, + {1000400780, 77.956146}, + {1000280790, 78.969769}, + {1000290790, 78.9544731}, + {1000300790, 78.942638067}, + {1000310790, 78.932851582}, + {1000320790, 78.925359506}, + {1000330790, 78.920948419}, + {1000340790, 78.918499252}, + {1000350790, 78.918337574}, + {1000360790, 78.920082919}, + {1000370790, 78.923990095}, + {1000380790, 78.929704692}, + {1000390790, 78.937946}, + {1000400790, 78.94979}, + {1000410790, 78.966022}, + {1000280800, 79.975051}, + {1000290800, 79.960623}, + {1000300800, 79.944552929}, + {1000310800, 79.936420773}, + {1000320800, 79.925350773}, + {1000330800, 79.92247444}, + {1000340800, 79.916521761}, + {1000350800, 79.918529784}, + {1000360800, 79.91637794}, + {1000370800, 79.922516442}, + {1000380800, 79.924517538}, + {1000390800, 79.93435475}, + {1000400800, 79.941213}, + {1000410800, 79.958754}, + {1000280810, 80.982727}, + {1000290810, 80.965743}, + {1000300810, 80.950402617}, + {1000310810, 80.938133841}, + {1000320810, 80.928832941}, + {1000330810, 80.922132288}, + {1000340810, 80.917993019}, + {1000350810, 80.916288197}, + {1000360810, 80.916589703}, + {1000370810, 80.9189939}, + {1000380810, 80.923211393}, + {1000390810, 80.929454283}, + {1000400810, 80.938245}, + {1000410810, 80.95023}, + {1000420810, 80.966226}, + {1000280820, 81.988492}, + {1000290820, 81.972378}, + {1000300820, 81.954574097}, + {1000310820, 81.943176531}, + {1000320820, 81.929774031}, + {1000330820, 81.924738731}, + {1000340820, 81.916699531}, + {1000350820, 81.916801752}, + {1000360820, 81.91348115368}, + {1000370820, 81.918209023}, + {1000380820, 81.918399845}, + {1000390820, 81.926930189}, + {1000400820, 81.931707497}, + {1000410820, 81.94438}, + {1000420820, 81.956661}, + {1000290830, 82.97811}, + {1000300830, 82.961041}, + {1000310830, 82.9471203}, + {1000320830, 82.9345391}, + {1000330830, 82.9252069}, + {1000340830, 82.919118604}, + {1000350830, 82.915175285}, + {1000360830, 82.914126516}, + {1000370830, 82.915114181}, + {1000380830, 82.917554372}, + {1000390830, 82.922484026}, + {1000400830, 82.929240926}, + {1000410830, 82.93815}, + {1000420830, 82.950252}, + {1000430830, 82.966377}, + {1000290840, 83.985271}, + {1000300840, 83.965829}, + {1000310840, 83.952663}, + {1000320840, 83.93757509}, + {1000330840, 83.92930329000001}, + {1000340840, 83.918466761}, + {1000350840, 83.916496417}, + {1000360840, 83.91149772708}, + {1000370840, 83.914375223}, + {1000380840, 83.913419118}, + {1000390840, 83.92067106}, + {1000400840, 83.923325663}, + {1000410840, 83.934305711}, + {1000420840, 83.941846}, + {1000430840, 83.959527}, + {1000300850, 84.973054}, + {1000310850, 84.957333}, + {1000320850, 84.942969658}, + {1000330850, 84.932163658}, + {1000340850, 84.922260758}, + {1000350850, 84.915645758}, + {1000360850, 84.91252726}, + {1000370850, 84.91178973604}, + {1000380850, 84.912932041}, + {1000390850, 84.916433039}, + {1000400850, 84.921443199}, + {1000410850, 84.928845836}, + {1000420850, 84.938260736}, + {1000430850, 84.950778}, + {1000440850, 84.967117}, + {1000300860, 85.978463}, + {1000310860, 85.963757}, + {1000320860, 85.946967}, + {1000330860, 85.936701532}, + {1000340860, 85.924311732}, + {1000350860, 85.918805432}, + {1000360860, 85.91061062468}, + {1000370860, 85.911167443}, + {1000380860, 85.90926072473}, + {1000390860, 85.914886095}, + {1000400860, 85.916296814}, + {1000410860, 85.925781536}, + {1000420860, 85.931174092}, + {1000430860, 85.944637}, + {1000440860, 85.957305}, + {1000310870, 86.969007}, + {1000320870, 86.953204}, + {1000330870, 86.940291716}, + {1000340870, 86.928688616}, + {1000350870, 86.920674016}, + {1000360870, 86.913354759}, + {1000370870, 86.909180529}, + {1000380870, 86.90887749454}, + {1000390870, 86.9108761}, + {1000400870, 86.914817338}, + {1000410870, 86.920692473}, + {1000420870, 86.928196198}, + {1000430870, 86.938067185}, + {1000440870, 86.950907}, + {1000310880, 87.975963}, + {1000320880, 87.957574}, + {1000330880, 87.94584}, + {1000340880, 87.93141749}, + {1000350880, 87.92408329}, + {1000360880, 87.914447879}, + {1000370880, 87.91131559}, + {1000380880, 87.905612253}, + {1000390880, 87.909501274}, + {1000400880, 87.910220715}, + {1000410880, 87.918226476}, + {1000420880, 87.921967779}, + {1000430880, 87.933794211}, + {1000440880, 87.941664}, + {1000450880, 87.960429}, + {1000320890, 88.96453}, + {1000330890, 88.950048}, + {1000340890, 88.936669058}, + {1000350890, 88.926704558}, + {1000360890, 88.917835449}, + {1000370890, 88.912278136}, + {1000380890, 88.907450808}, + {1000390890, 88.905838156}, + {1000400890, 88.908879751}, + {1000410890, 88.913444696}, + {1000420890, 88.919468149}, + {1000430890, 88.927648649}, + {1000440890, 88.937337849}, + {1000450890, 88.950992}, + {1000320900, 89.969436}, + {1000330900, 89.955995}, + {1000340900, 89.940096}, + {1000350900, 89.931292848}, + {1000360900, 89.919527929}, + {1000370900, 89.914797557}, + {1000380900, 89.90772787}, + {1000390900, 89.907141749}, + {1000400900, 89.904698755}, + {1000410900, 89.911259201}, + {1000420900, 89.91393127}, + {1000430900, 89.924073919}, + {1000440900, 89.930344378}, + {1000450900, 89.944569}, + {1000460900, 89.95737}, + {1000330910, 90.960816}, + {1000340910, 90.9457}, + {1000350910, 90.934398617}, + {1000360910, 90.923806309}, + {1000370910, 90.916537261}, + {1000380910, 90.910195942}, + {1000390910, 90.907298048}, + {1000400910, 90.905640205}, + {1000410910, 90.906990256}, + {1000420910, 90.91174519}, + {1000430910, 90.918424972}, + {1000440910, 90.92674153}, + {1000450910, 90.937123}, + {1000460910, 90.950435}, + {1000330920, 91.967386}, + {1000340920, 91.94984}, + {1000350920, 91.939631595}, + {1000360920, 91.926173092}, + {1000370920, 91.919728477}, + {1000380920, 91.911038222}, + {1000390920, 91.908945752}, + {1000400920, 91.905035336}, + {1000410920, 91.90718858}, + {1000420920, 91.906807153}, + {1000430920, 91.915269777}, + {1000440920, 91.920234373}, + {1000450920, 91.932367692}, + {1000460920, 91.941192225}, + {1000470920, 91.95971}, + {1000340930, 92.956135}, + {1000350930, 92.94322}, + {1000360930, 92.931147172}, + {1000370930, 92.922039334}, + {1000380930, 92.914024314}, + {1000390930, 92.909578434}, + {1000400930, 92.906470661}, + {1000410930, 92.90637317}, + {1000420930, 92.906808772}, + {1000430930, 92.910245147}, + {1000440930, 92.917104442}, + {1000450930, 92.925912778}, + {1000460930, 92.936680426}, + {1000470930, 92.950188}, + {1000340940, 93.96049}, + {1000350940, 93.948846}, + {1000360940, 93.934140452}, + {1000370940, 93.926394819}, + {1000380940, 93.915355641}, + {1000390940, 93.911592062}, + {1000400940, 93.906312523}, + {1000410940, 93.907279001}, + {1000420940, 93.905083586}, + {1000430940, 93.909652319}, + {1000440940, 93.91134286}, + {1000450940, 93.92173045}, + {1000460940, 93.929036286}, + {1000470940, 93.943744}, + {1000480940, 93.956586}, + {1000340950, 94.9673}, + {1000350950, 94.952925}, + {1000360950, 94.939710922}, + {1000370950, 94.929263849}, + {1000380950, 94.919358282}, + {1000390950, 94.912819697}, + {1000400950, 94.908040276}, + {1000410950, 94.90683111}, + {1000420950, 94.905837436}, + {1000430950, 94.907652281}, + {1000440950, 94.910404415}, + {1000450950, 94.915897893}, + {1000460950, 94.924888506}, + {1000470950, 94.935688}, + {1000480950, 94.949483}, + {1000350960, 95.95898}, + {1000360960, 95.943014473}, + {1000370960, 95.934133398}, + {1000380960, 95.921719045}, + {1000390960, 95.915909305}, + {1000400960, 95.908277615}, + {1000410960, 95.908101586}, + {1000420960, 95.90467477}, + {1000430960, 95.907866675}, + {1000440960, 95.90758891}, + {1000450960, 95.914451705}, + {1000460960, 95.918213739}, + {1000470960, 95.930743903}, + {1000480960, 95.940341}, + {1000490960, 95.959109}, + {1000350970, 96.963499}, + {1000360970, 96.949088782}, + {1000370970, 96.937177117}, + {1000380970, 96.926375621}, + {1000390970, 96.918286702}, + {1000400970, 96.910963802}, + {1000410970, 96.908101622}, + {1000420970, 96.906016903}, + {1000430970, 96.90636072}, + {1000440970, 96.907545776}, + {1000450970, 96.911327872}, + {1000460970, 96.916471985}, + {1000470970, 96.9238814}, + {1000480970, 96.934799343}, + {1000490970, 96.949125}, + {1000350980, 97.969887}, + {1000360980, 97.952635}, + {1000370980, 97.941632317}, + {1000380980, 97.928692636}, + {1000390980, 97.922394841}, + {1000400980, 97.912740448}, + {1000410980, 97.910332645}, + {1000420980, 97.905403609}, + {1000430980, 97.907211206}, + {1000440980, 97.905286709}, + {1000450980, 97.910707734}, + {1000460980, 97.912698335}, + {1000470980, 97.92155997}, + {1000480980, 97.927389315}, + {1000490980, 97.942129}, + {1000360990, 98.958776}, + {1000370990, 98.94511919}, + {1000380990, 98.932883604}, + {1000390990, 98.924160839}, + {1000400990, 98.916675081}, + {1000410990, 98.911609377}, + {1000420990, 98.907707299}, + {1000430990, 98.906249681}, + {1000440990, 98.905930284}, + {1000450990, 98.908121241}, + {1000460990, 98.911773073}, + {1000470990, 98.917645766}, + {1000480990, 98.924925845}, + {1000490990, 98.93411}, + {1000500990, 98.948495}, + {1000361000, 99.962995}, + {1000371000, 99.950331532}, + {1000381000, 99.93578327}, + {1000391000, 99.927727678}, + {1000401000, 99.918010499}, + {1000411000, 99.914340578}, + {1000421000, 99.907467982}, + {1000431000, 99.907652715}, + {1000441000, 99.90421046}, + {1000451000, 99.908114147}, + {1000461000, 99.908520438}, + {1000471000, 99.916115443}, + {1000481000, 99.920348829}, + {1000491000, 99.931101929}, + {1000501000, 99.938648944}, + {1000361010, 100.969318}, + {1000371010, 100.954302}, + {1000381010, 100.940606264}, + {1000391010, 100.930160817}, + {1000401010, 100.921458454}, + {1000411010, 100.915306508}, + {1000421010, 100.910337648}, + {1000431010, 100.907305271}, + {1000441010, 100.905573086}, + {1000451010, 100.906158903}, + {1000461010, 100.908284824}, + {1000471010, 100.912683951}, + {1000481010, 100.918586209}, + {1000491010, 100.926414025}, + {1000501010, 100.935259252}, + {1000371020, 101.960008}, + {1000381020, 101.944004679}, + {1000391020, 101.934328471}, + {1000401020, 101.923154181}, + {1000411020, 101.918090447}, + {1000421020, 101.910293725}, + {1000431020, 101.909207239}, + {1000441020, 101.904340312}, + {1000451020, 101.906834282}, + {1000461020, 101.905632292}, + {1000471020, 101.911704538}, + {1000481020, 101.914481797}, + {1000491020, 101.924105911}, + {1000501020, 101.930289525}, + {1000511020, 101.945142}, + {1000371030, 102.964401}, + {1000381030, 102.949243}, + {1000391030, 102.937243796}, + {1000401030, 102.927204054}, + {1000411030, 102.919453416}, + {1000421030, 102.913091954}, + {1000431030, 102.90917396}, + {1000441030, 102.906314846}, + {1000451030, 102.905494081}, + {1000461030, 102.906111074}, + {1000471030, 102.908960558}, + {1000481030, 102.913416922}, + {1000491030, 102.91987883}, + {1000501030, 102.927973}, + {1000511030, 102.939162}, + {1000371040, 103.970531}, + {1000381040, 103.953022}, + {1000391040, 103.941943}, + {1000401040, 103.929449193}, + {1000411040, 103.922907728}, + {1000421040, 103.913747443}, + {1000431040, 103.911433718}, + {1000441040, 103.905425312}, + {1000451040, 103.906645309}, + {1000461040, 103.904030393}, + {1000471040, 103.908623715}, + {1000481040, 103.909856228}, + {1000491040, 103.918214538}, + {1000501040, 103.923105195}, + {1000511040, 103.936344}, + {1000521040, 103.946723408}, + {1000381050, 104.959001}, + {1000391050, 104.945711}, + {1000401050, 104.934021832}, + {1000411050, 104.924942577}, + {1000421050, 104.91698198899999}, + {1000431050, 104.911662024}, + {1000441050, 104.907745478}, + {1000451050, 104.905687787}, + {1000461050, 104.905079479}, + {1000471050, 104.906525604}, + {1000481050, 104.909463893}, + {1000491050, 104.914502322}, + {1000501050, 104.921268421}, + {1000511050, 104.931276547}, + {1000521050, 104.943304516}, + {1000381060, 105.963177}, + {1000391060, 105.950842}, + {1000401060, 105.93693}, + {1000411060, 105.928928505}, + {1000421060, 105.918273231}, + {1000431060, 105.914356674}, + {1000441060, 105.907328181}, + {1000451060, 105.907285879}, + {1000461060, 105.903480287}, + {1000471060, 105.906663499}, + {1000481060, 105.906459791}, + {1000491060, 105.913463596}, + {1000501060, 105.916957394}, + {1000511060, 105.928637979}, + {1000521060, 105.937498521}, + {1000531060, 105.953516}, + {1000381070, 106.969672}, + {1000391070, 106.954943}, + {1000401070, 106.942007}, + {1000411070, 106.931589685}, + {1000421070, 106.92211977}, + {1000431070, 106.915458437}, + {1000441070, 106.909969837}, + {1000451070, 106.906747975}, + {1000461070, 106.905128058}, + {1000471070, 106.905091509}, + {1000481070, 106.906612049}, + {1000491070, 106.910287497}, + {1000501070, 106.915713649}, + {1000511070, 106.924150621}, + {1000521070, 106.934882}, + {1000531070, 106.946935}, + {1000391080, 107.960515}, + {1000401080, 107.945303}, + {1000411080, 107.936075604}, + {1000421080, 107.924047508}, + {1000431080, 107.918493493}, + {1000441080, 107.910185793}, + {1000451080, 107.908715304}, + {1000461080, 107.903891806}, + {1000471080, 107.905950245}, + {1000481080, 107.904183588}, + {1000491080, 107.909693654}, + {1000501080, 107.91189429}, + {1000511080, 107.922226731}, + {1000521080, 107.929380469}, + {1000531080, 107.943348}, + {1000541080, 107.954232285}, + {1000391090, 108.965131}, + {1000401090, 108.950907}, + {1000411090, 108.939141}, + {1000421090, 108.928438318}, + {1000431090, 108.920254107}, + {1000441090, 108.913323707}, + {1000451090, 108.908749555}, + {1000461090, 108.905950576}, + {1000471090, 108.904755778}, + {1000481090, 108.904986697}, + {1000491090, 108.907149679}, + {1000501090, 108.911292857}, + {1000511090, 108.918141203}, + {1000521090, 108.927304532}, + {1000531090, 108.938086022}, + {1000541090, 108.950434955}, + {1000401100, 109.954675}, + {1000411100, 109.943843}, + {1000421100, 109.930717956}, + {1000431100, 109.923741263}, + {1000441100, 109.914038501}, + {1000451100, 109.911079745}, + {1000461100, 109.905172878}, + {1000471100, 109.906110724}, + {1000481100, 109.90300747}, + {1000491100, 109.907170674}, + {1000501100, 109.907844835}, + {1000511100, 109.916854283}, + {1000521100, 109.922458102}, + {1000531100, 109.935085102}, + {1000541100, 109.944258759}, + {1000401110, 110.960837}, + {1000411110, 110.947439}, + {1000421110, 110.935651966}, + {1000431110, 110.925898966}, + {1000441110, 110.917567566}, + {1000451110, 110.911643164}, + {1000461110, 110.907690358}, + {1000471110, 110.905296827}, + {1000481110, 110.904183776}, + {1000491110, 110.905107236}, + {1000501110, 110.907741143}, + {1000511110, 110.913218187}, + {1000521110, 110.921000587}, + {1000531110, 110.930269236}, + {1000541110, 110.94147}, + {1000551110, 110.953945}, + {1000401120, 111.965196}, + {1000411120, 111.952689}, + {1000421120, 111.938293}, + {1000431120, 111.929941658}, + {1000441120, 111.918806922}, + {1000451120, 111.914405199}, + {1000461120, 111.907330557}, + {1000471120, 111.907048548}, + {1000481120, 111.902763896}, + {1000491120, 111.905538718}, + {1000501120, 111.904824894}, + {1000511120, 111.912399903}, + {1000521120, 111.916727848}, + {1000531120, 111.928004548}, + {1000541120, 111.935559068}, + {1000551120, 111.950172}, + {1000401130, 112.971723}, + {1000411130, 112.956833}, + {1000421130, 112.943478}, + {1000431130, 112.932569032}, + {1000441130, 112.922846729}, + {1000451130, 112.915440212}, + {1000461130, 112.910261912}, + {1000471130, 112.906572865}, + {1000481130, 112.904408105}, + {1000491130, 112.904060451}, + {1000501130, 112.905175857}, + {1000511130, 112.909374664}, + {1000521130, 112.915891}, + {1000531130, 112.923650062}, + {1000541130, 112.933221663}, + {1000551130, 112.944428484}, + {1000561130, 112.95737}, + {1000411140, 113.962469}, + {1000421140, 113.946666}, + {1000431140, 113.93709}, + {1000441140, 113.92461443}, + {1000451140, 113.91872168}, + {1000461140, 113.91036943}, + {1000471140, 113.908823029}, + {1000481140, 113.903364998}, + {1000491140, 113.904916405}, + {1000501140, 113.90278013}, + {1000511140, 113.909289155}, + {1000521140, 113.91208782}, + {1000531140, 113.9220189}, + {1000541140, 113.927980329}, + {1000551140, 113.941292244}, + {1000561140, 113.950718489}, + {1000411150, 114.966849}, + {1000421150, 114.952174}, + {1000431150, 114.9401}, + {1000441150, 114.929033049}, + {1000451150, 114.920311649}, + {1000461150, 114.913659333}, + {1000471150, 114.908767445}, + {1000481150, 114.905437426}, + {1000491150, 114.903878772}, + {1000501150, 114.903344695}, + {1000511150, 114.906598}, + {1000521150, 114.911902}, + {1000531150, 114.918048}, + {1000541150, 114.926293943}, + {1000551150, 114.93591}, + {1000561150, 114.947482}, + {1000411160, 115.972914}, + {1000421160, 115.955759}, + {1000431160, 115.94502}, + {1000441160, 115.931219191}, + {1000451160, 115.92406206}, + {1000461160, 115.914297872}, + {1000471160, 115.911386809}, + {1000481160, 115.90476323}, + {1000491160, 115.905259992}, + {1000501160, 115.901742825}, + {1000511160, 115.906792732}, + {1000521160, 115.908465558}, + {1000531160, 115.916885513}, + {1000541160, 115.921580955}, + {1000551160, 115.933395}, + {1000561160, 115.941621}, + {1000571160, 115.957005}, + {1000421170, 116.961686}, + {1000431170, 116.94832}, + {1000441170, 116.936135}, + {1000451170, 116.926036291}, + {1000461170, 116.917955584}, + {1000471170, 116.911774086}, + {1000481170, 116.907226039}, + {1000491170, 116.904515729}, + {1000501170, 116.902954036}, + {1000511170, 116.904841519}, + {1000521170, 116.908646227}, + {1000531170, 116.913645649}, + {1000541170, 116.920358758}, + {1000551170, 116.928616723}, + {1000561170, 116.938316403}, + {1000571170, 116.950326}, + {1000421180, 117.965249}, + {1000431180, 117.953526}, + {1000441180, 117.938808}, + {1000451180, 117.930341116}, + {1000461180, 117.919067273}, + {1000471180, 117.914595484}, + {1000481180, 117.906921956}, + {1000491180, 117.906356705}, + {1000501180, 117.90160663}, + {1000511180, 117.905532194}, + {1000521180, 117.905860104}, + {1000531180, 117.913074}, + {1000541180, 117.916178678}, + {1000551180, 117.926559517}, + {1000561180, 117.933226}, + {1000571180, 117.946731}, + {1000421190, 118.971465}, + {1000431190, 118.956876}, + {1000441190, 118.94409}, + {1000451190, 118.932556951}, + {1000461190, 118.923341138}, + {1000471190, 118.915570309}, + {1000481190, 118.909847052}, + {1000491190, 118.905851622}, + {1000501190, 118.903311266}, + {1000511190, 118.903944062}, + {1000521190, 118.906405699}, + {1000531190, 118.91006091}, + {1000541190, 118.915410641}, + {1000551190, 118.922377327}, + {1000561190, 118.930659683}, + {1000571190, 118.940934}, + {1000581190, 118.952957}, + {1000431200, 119.962426}, + {1000441200, 119.946623}, + {1000451200, 119.937069}, + {1000461200, 119.924551745}, + {1000471200, 119.918784765}, + {1000481200, 119.909868065}, + {1000491200, 119.907967489}, + {1000501200, 119.902202557}, + {1000511200, 119.905080308}, + {1000521200, 119.904065779}, + {1000531200, 119.910093729}, + {1000541200, 119.911784267}, + {1000551200, 119.920677277}, + {1000561200, 119.926044997}, + {1000571200, 119.938196}, + {1000581200, 119.946613}, + {1000431210, 120.96614}, + {1000441210, 120.952098}, + {1000451210, 120.939613}, + {1000461210, 120.928950342}, + {1000471210, 120.920125279}, + {1000481210, 120.91296366}, + {1000491210, 120.907852778}, + {1000501210, 120.904243488}, + {1000511210, 120.903811353}, + {1000521210, 120.904945065}, + {1000531210, 120.907411492}, + {1000541210, 120.911453012}, + {1000551210, 120.917227235}, + {1000561210, 120.924052286}, + {1000571210, 120.933236}, + {1000581210, 120.943435}, + {1000591210, 120.955393}, + {1000431220, 121.97176}, + {1000441220, 121.955147}, + {1000451220, 121.944305}, + {1000461220, 121.930631693}, + {1000471220, 121.923664446}, + {1000481220, 121.91345905}, + {1000491220, 121.910282458}, + {1000501220, 121.903445494}, + {1000511220, 121.905169335}, + {1000521220, 121.903044708}, + {1000531220, 121.907590094}, + {1000541220, 121.908367655}, + {1000551220, 121.916108144}, + {1000561220, 121.919904}, + {1000571220, 121.93071}, + {1000581220, 121.93787}, + {1000591220, 121.951927}, + {1000441230, 122.960762}, + {1000451230, 122.947192}, + {1000461230, 122.935126}, + {1000471230, 122.92531506}, + {1000481230, 122.91689246}, + {1000491230, 122.910435252}, + {1000501230, 122.905727065}, + {1000511230, 122.904215292}, + {1000521230, 122.904271022}, + {1000531230, 122.905589753}, + {1000541230, 122.908482235}, + {1000551230, 122.91299606}, + {1000561230, 122.91878106}, + {1000571230, 122.9263}, + {1000581230, 122.93528}, + {1000591230, 122.946076}, + {1000441240, 123.96394}, + {1000451240, 123.952002}, + {1000461240, 123.937305}, + {1000471240, 123.928899227}, + {1000481240, 123.917659772}, + {1000491240, 123.913184873}, + {1000501240, 123.905279619}, + {1000511240, 123.905937065}, + {1000521240, 123.902818341}, + {1000531240, 123.906210297}, + {1000541240, 123.905885174}, + {1000551240, 123.912247366}, + {1000561240, 123.915093627}, + {1000571240, 123.924574275}, + {1000581240, 123.93031}, + {1000591240, 123.94294}, + {1000601240, 123.951873}, + {1000441250, 124.969544}, + {1000451250, 124.955094}, + {1000461250, 124.942072}, + {1000471250, 124.930735}, + {1000481250, 124.92125759}, + {1000491250, 124.913673841}, + {1000501250, 124.90778937}, + {1000511250, 124.905254264}, + {1000521250, 124.904431178}, + {1000531250, 124.90463061}, + {1000541250, 124.90638764}, + {1000551250, 124.909725953}, + {1000561250, 124.91447184}, + {1000571250, 124.92081593099999}, + {1000581250, 124.92844}, + {1000591250, 124.937659}, + {1000601250, 124.948395}, + {1000451260, 125.960064}, + {1000461260, 125.944401}, + {1000471260, 125.934814}, + {1000481260, 125.92243029}, + {1000491260, 125.916468202}, + {1000501260, 125.907658958}, + {1000511260, 125.907253158}, + {1000521260, 125.903312144}, + {1000531260, 125.905624205}, + {1000541260, 125.904297422}, + {1000551260, 125.909445821}, + {1000561260, 125.911250202}, + {1000571260, 125.919512667}, + {1000581260, 125.923971}, + {1000591260, 125.93524}, + {1000601260, 125.942694}, + {1000611260, 125.957327}, + {1000451270, 126.963789}, + {1000461270, 126.949307}, + {1000471270, 126.937037}, + {1000481270, 126.926203291}, + {1000491270, 126.91746604}, + {1000501270, 126.910391726}, + {1000511270, 126.906925557}, + {1000521270, 126.905226993}, + {1000531270, 126.904472592}, + {1000541270, 126.905183636}, + {1000551270, 126.907417527}, + {1000561270, 126.911091272}, + {1000571270, 126.916375083}, + {1000581270, 126.922727}, + {1000591270, 126.93071}, + {1000601270, 126.939978}, + {1000611270, 126.951358}, + {1000451280, 127.970649}, + {1000461280, 127.952345}, + {1000471280, 127.941266}, + {1000481280, 127.927816778}, + {1000491280, 127.920353637}, + {1000501280, 127.910507828}, + {1000511280, 127.909146121}, + {1000521280, 127.904461237}, + {1000531280, 127.905809355}, + {1000541280, 127.90353075341}, + {1000551280, 127.907748452}, + {1000561280, 127.908352446}, + {1000571280, 127.915592123}, + {1000581280, 127.918911}, + {1000591280, 127.928791}, + {1000601280, 127.935018}, + {1000611280, 127.948234}, + {1000621280, 127.957971}, + {1000461290, 128.959334}, + {1000471290, 128.944315}, + {1000481290, 128.932235597}, + {1000491290, 128.921808534}, + {1000501290, 128.91348244}, + {1000511290, 128.909146623}, + {1000521290, 128.906596419}, + {1000531290, 128.904983643}, + {1000541290, 128.90478085742}, + {1000551290, 128.90606591}, + {1000561290, 128.908683409}, + {1000571290, 128.912695592}, + {1000581290, 128.918102}, + {1000591290, 128.925095}, + {1000601290, 128.933038}, + {1000611290, 128.942909}, + {1000621290, 128.954557}, + {1000461300, 129.964863}, + {1000471300, 129.950727}, + {1000481300, 129.934387563}, + {1000491300, 129.924952257}, + {1000501300, 129.913974531}, + {1000511300, 129.911662686}, + {1000521300, 129.906222745}, + {1000531300, 129.906670168}, + {1000541300, 129.903509346}, + {1000551300, 129.906709281}, + {1000561300, 129.906326002}, + {1000571300, 129.912369413}, + {1000581300, 129.914736}, + {1000591300, 129.92359}, + {1000601300, 129.928506}, + {1000611300, 129.940451}, + {1000621300, 129.948792}, + {1000631300, 129.964022}, + {1000461310, 130.972367}, + {1000471310, 130.956253}, + {1000481310, 130.94072774}, + {1000491310, 130.926972839}, + {1000501310, 130.917053067}, + {1000511310, 130.911989339}, + {1000521310, 130.90852221}, + {1000531310, 130.906126375}, + {1000541310, 130.90508412808}, + {1000551310, 130.905468457}, + {1000561310, 130.906946315}, + {1000571310, 130.91007}, + {1000581310, 130.914429465}, + {1000591310, 130.92023496}, + {1000601310, 130.92724802}, + {1000611310, 130.935834}, + {1000621310, 130.946022}, + {1000631310, 130.957634}, + {1000471320, 131.96307}, + {1000481320, 131.945823136}, + {1000491320, 131.932998444}, + {1000501320, 131.917823898}, + {1000511320, 131.914508013}, + {1000521320, 131.908546713}, + {1000531320, 131.907993511}, + {1000541320, 131.90415508346}, + {1000551320, 131.90643774}, + {1000561320, 131.905061231}, + {1000571320, 131.910119047}, + {1000581320, 131.911466226}, + {1000591320, 131.91924}, + {1000601320, 131.923321237}, + {1000611320, 131.93384}, + {1000621320, 131.940805}, + {1000631320, 131.954696}, + {1000471330, 132.968781}, + {1000481330, 132.952614}, + {1000491330, 132.938067}, + {1000501330, 132.923913753}, + {1000511330, 132.915272128}, + {1000521330, 132.91096333}, + {1000531330, 132.9078284}, + {1000541330, 132.905910748}, + {1000551330, 132.905451958}, + {1000561330, 132.906007443}, + {1000571330, 132.908218}, + {1000581330, 132.911520402}, + {1000591330, 132.916330558}, + {1000601330, 132.922348}, + {1000611330, 132.929782}, + {1000621330, 132.93856}, + {1000631330, 132.94929}, + {1000641330, 132.961288}, + {1000481340, 133.957638}, + {1000491340, 133.944208}, + {1000501340, 133.92868043}, + {1000511340, 133.920537334}, + {1000521340, 133.911396376}, + {1000531340, 133.90977566}, + {1000541340, 133.90539303}, + {1000551340, 133.906718501}, + {1000561340, 133.904508249}, + {1000571340, 133.908514011}, + {1000581340, 133.908928142}, + {1000591340, 133.915696729}, + {1000601340, 133.918790207}, + {1000611340, 133.928326}, + {1000621340, 133.93411}, + {1000631340, 133.946537}, + {1000641340, 133.955416}, + {1000481350, 134.964766}, + {1000491350, 134.949425}, + {1000501350, 134.934908603}, + {1000511350, 134.925184354}, + {1000521350, 134.916554715}, + {1000531350, 134.910059355}, + {1000541350, 134.907231441}, + {1000551350, 134.905976907}, + {1000561350, 134.905688447}, + {1000571350, 134.906984427}, + {1000581350, 134.909160662}, + {1000591350, 134.913111772}, + {1000601350, 134.918181318}, + {1000611350, 134.92478499999999}, + {1000621350, 134.93252}, + {1000631350, 134.94187}, + {1000641350, 134.952496}, + {1000651350, 134.964516}, + {1000491360, 135.956017}, + {1000501360, 135.939699}, + {1000511360, 135.930749009}, + {1000521360, 135.92010118}, + {1000531360, 135.914604693}, + {1000541360, 135.907214474}, + {1000551360, 135.907311431}, + {1000561360, 135.9045758}, + {1000571360, 135.907634962}, + {1000581360, 135.907129256}, + {1000591360, 135.91267747}, + {1000601360, 135.914976061}, + {1000611360, 135.923595949}, + {1000621360, 135.928275553}, + {1000631360, 135.93962}, + {1000641360, 135.9473}, + {1000651360, 135.96146}, + {1000491370, 136.961535}, + {1000501370, 136.946162}, + {1000511370, 136.935522519}, + {1000521370, 136.925599354}, + {1000531370, 136.918028178}, + {1000541370, 136.911557771}, + {1000551370, 136.907089296}, + {1000561370, 136.905827207}, + {1000571370, 136.906450438}, + {1000581370, 136.907762416}, + {1000591370, 136.910679183}, + {1000601370, 136.914563099}, + {1000611370, 136.920479519}, + {1000621370, 136.927007959}, + {1000631370, 136.935430719}, + {1000641370, 136.94502}, + {1000651370, 136.95602}, + {1000501380, 137.951143}, + {1000511380, 137.941331}, + {1000521380, 137.929472452}, + {1000531380, 137.922726392}, + {1000541380, 137.914146268}, + {1000551380, 137.911017119}, + {1000561380, 137.905247059}, + {1000571380, 137.907124041}, + {1000581380, 137.90599418}, + {1000591380, 137.910757495}, + {1000601380, 137.911950938}, + {1000611380, 137.919576119}, + {1000621380, 137.923243988}, + {1000631380, 137.933709}, + {1000641380, 137.940247}, + {1000651380, 137.953193}, + {1000661380, 137.9625}, + {1000501390, 138.957799}, + {1000511390, 138.946269}, + {1000521390, 138.935367191}, + {1000531390, 138.9264934}, + {1000541390, 138.9187922}, + {1000551390, 138.913363822}, + {1000561390, 138.908841164}, + {1000571390, 138.906362927}, + {1000581390, 138.906647029}, + {1000591390, 138.9089327}, + {1000601390, 138.911951208}, + {1000611390, 138.916799228}, + {1000621390, 138.922296631}, + {1000631390, 138.929792307}, + {1000641390, 138.93813}, + {1000651390, 138.94833}, + {1000661390, 138.959527}, + {1000501400, 139.962973}, + {1000511400, 139.952345}, + {1000521400, 139.939487057}, + {1000531400, 139.931715914}, + {1000541400, 139.921645814}, + {1000551400, 139.917283707}, + {1000561400, 139.910608231}, + {1000571400, 139.909487285}, + {1000581400, 139.905448433}, + {1000591400, 139.9090856}, + {1000601400, 139.90954613}, + {1000611400, 139.916035918}, + {1000621400, 139.918994714}, + {1000631400, 139.928087633}, + {1000641400, 139.933674}, + {1000651400, 139.945805048}, + {1000661400, 139.95402}, + {1000671400, 139.968526}, + {1000511410, 140.957552}, + {1000521410, 140.945604}, + {1000531410, 140.935666081}, + {1000541410, 140.926787181}, + {1000551410, 140.920045279}, + {1000561410, 140.914403653}, + {1000571410, 140.910971155}, + {1000581410, 140.908285991}, + {1000591410, 140.907659604}, + {1000601410, 140.90961669}, + {1000611410, 140.913555081}, + {1000621410, 140.918481545}, + {1000631410, 140.924931734}, + {1000641410, 140.932126}, + {1000651410, 140.941448}, + {1000661410, 140.95128}, + {1000671410, 140.963108}, + {1000511420, 141.963918}, + {1000521420, 141.950027}, + {1000531420, 141.941166595}, + {1000541420, 141.929973095}, + {1000551420, 141.924299514}, + {1000561420, 141.916432904}, + {1000571420, 141.91409076}, + {1000581420, 141.909250208}, + {1000591420, 141.91005164}, + {1000601420, 141.907728824}, + {1000611420, 141.912890982}, + {1000621420, 141.915209415}, + {1000631420, 141.923446719}, + {1000641420, 141.928116}, + {1000651420, 141.939280858}, + {1000661420, 141.946194}, + {1000671420, 141.96001}, + {1000681420, 141.970016}, + {1000521430, 142.956489}, + {1000531430, 142.945475}, + {1000541430, 142.93536955}, + {1000551430, 142.927347346}, + {1000561430, 142.920625149}, + {1000571430, 142.916079482}, + {1000581430, 142.912391953}, + {1000591430, 142.910822624}, + {1000601430, 142.909819815}, + {1000611430, 142.910938068}, + {1000621430, 142.914634848}, + {1000631430, 142.920298678}, + {1000641430, 142.926750678}, + {1000651430, 142.935137332}, + {1000661430, 142.943994332}, + {1000671430, 142.95486}, + {1000681430, 142.966548}, + {1000521440, 143.961116}, + {1000531440, 143.951336}, + {1000541440, 143.938945076}, + {1000551440, 143.932075402}, + {1000561440, 143.922954821}, + {1000571440, 143.919645589}, + {1000581440, 143.913652763}, + {1000591440, 143.913310682}, + {1000601440, 143.910092798}, + {1000611440, 143.912596208}, + {1000621440, 143.912006285}, + {1000631440, 143.918819481}, + {1000641440, 143.922963}, + {1000651440, 143.933045}, + {1000661440, 143.939269512}, + {1000671440, 143.952109712}, + {1000681440, 143.9607}, + {1000691440, 143.976211}, + {1000521450, 144.967783}, + {1000531450, 144.955845}, + {1000541450, 144.944719631}, + {1000551450, 144.935528927}, + {1000561450, 144.9275184}, + {1000571450, 144.921808065}, + {1000581450, 144.917265113}, + {1000591450, 144.914517987}, + {1000601450, 144.912579151}, + {1000611450, 144.912755748}, + {1000621450, 144.913417157}, + {1000631450, 144.916272659}, + {1000641450, 144.921710051}, + {1000651450, 144.928717001}, + {1000661450, 144.937473992}, + {1000671450, 144.947267392}, + {1000681450, 144.957874}, + {1000691450, 144.970389}, + {1000531460, 145.961846}, + {1000541460, 145.948518245}, + {1000551460, 145.940621867}, + {1000561460, 145.9303632}, + {1000571460, 145.925688017}, + {1000581460, 145.918812294}, + {1000591460, 145.91768763}, + {1000601460, 145.913122459}, + {1000611460, 145.91470224}, + {1000621460, 145.913046835}, + {1000631460, 145.917210852}, + {1000641460, 145.918318513}, + {1000651460, 145.927252739}, + {1000661460, 145.932844526}, + {1000671460, 145.944993503}, + {1000681460, 145.952418357}, + {1000691460, 145.966661}, + {1000531470, 146.966505}, + {1000541470, 146.954482}, + {1000551470, 146.944261512}, + {1000561470, 146.9353039}, + {1000571470, 146.9284178}, + {1000581470, 146.9226899}, + {1000591470, 146.919007438}, + {1000601470, 146.916105969}, + {1000611470, 146.915144944}, + {1000621470, 146.914904401}, + {1000631470, 146.91675244}, + {1000641470, 146.919101014}, + {1000651470, 146.92405462}, + {1000661470, 146.931082712}, + {1000671470, 146.940142293}, + {1000681470, 146.949964456}, + {1000691470, 146.961379887}, + {1000541480, 147.958508}, + {1000551480, 147.949639026}, + {1000561480, 147.938223}, + {1000571480, 147.9326794}, + {1000581480, 147.924424186}, + {1000591480, 147.922129992}, + {1000601480, 147.916899027}, + {1000611480, 147.917481091}, + {1000621480, 147.914829233}, + {1000631480, 147.918091288}, + {1000641480, 147.918121414}, + {1000651480, 147.924275476}, + {1000661480, 147.927149944}, + {1000671480, 147.937743925}, + {1000681480, 147.944735026}, + {1000691480, 147.958384026}, + {1000701480, 147.967547}, + {1000541490, 148.964573}, + {1000551490, 148.953516}, + {1000561490, 148.943284}, + {1000571490, 148.935351259}, + {1000581490, 148.9284269}, + {1000591490, 148.9237361}, + {1000601490, 148.920154583}, + {1000611490, 148.918341507}, + {1000621490, 148.917191211}, + {1000631490, 148.917936875}, + {1000641490, 148.919347666}, + {1000651490, 148.923253792}, + {1000661490, 148.927327516}, + {1000671490, 148.933820457}, + {1000681490, 148.942306}, + {1000691490, 148.952828}, + {1000701490, 148.964219}, + {1000541500, 149.968878}, + {1000551500, 149.959023}, + {1000561500, 149.9464411}, + {1000571500, 149.9395475}, + {1000581500, 149.930384032}, + {1000591500, 149.926676391}, + {1000601500, 149.920901322}, + {1000611500, 149.920990014}, + {1000621500, 149.917281993}, + {1000631500, 149.919707092}, + {1000641500, 149.918663949}, + {1000651500, 149.923664799}, + {1000661500, 149.925593068}, + {1000671500, 149.933498353}, + {1000681500, 149.937915524}, + {1000691500, 149.95009}, + {1000701500, 149.958314}, + {1000711500, 149.973407}, + {1000551510, 150.963199}, + {1000561510, 150.951755}, + {1000571510, 150.942769}, + {1000581510, 150.9342722}, + {1000591510, 150.928309066}, + {1000601510, 150.923839363}, + {1000611510, 150.921216613}, + {1000621510, 150.919938859}, + {1000631510, 150.919856606}, + {1000641510, 150.920354922}, + {1000651510, 150.92310897}, + {1000661510, 150.926191279}, + {1000671510, 150.931698176}, + {1000681510, 150.937448567}, + {1000691510, 150.945494433}, + {1000701510, 150.955402453}, + {1000711510, 150.967471}, + {1000551520, 151.968728}, + {1000561520, 151.95533}, + {1000571520, 151.947085}, + {1000581520, 151.936682}, + {1000591520, 151.9315529}, + {1000601520, 151.924691242}, + {1000611520, 151.923505185}, + {1000621520, 151.919738646}, + {1000631520, 151.92175098}, + {1000641520, 151.919798414}, + {1000651520, 151.924081855}, + {1000661520, 151.924725274}, + {1000671520, 151.931717618}, + {1000681520, 151.935050347}, + {1000691520, 151.944476}, + {1000701520, 151.950326699}, + {1000711520, 151.96412}, + {1000561530, 152.960848}, + {1000571530, 152.950553}, + {1000581530, 152.941052}, + {1000591530, 152.933903511}, + {1000601530, 152.927717868}, + {1000611530, 152.924156252}, + {1000621530, 152.922103576}, + {1000631530, 152.921236789}, + {1000641530, 152.921756945}, + {1000651530, 152.923441694}, + {1000661530, 152.925771729}, + {1000671530, 152.930206671}, + {1000681530, 152.93508635}, + {1000691530, 152.942058023}, + {1000701530, 152.949372}, + {1000711530, 152.958802248}, + {1000721530, 152.970692}, + {1000561540, 153.964659}, + {1000571540, 153.955416}, + {1000581540, 153.94394}, + {1000591540, 153.937885165}, + {1000601540, 153.929597404}, + {1000611540, 153.926712791}, + {1000621540, 153.922215756}, + {1000631540, 153.922985699}, + {1000641540, 153.920872974}, + {1000651540, 153.924683681}, + {1000661540, 153.92442892}, + {1000671540, 153.930606776}, + {1000681540, 153.932790799}, + {1000691540, 153.941570062}, + {1000701540, 153.946395696}, + {1000711540, 153.957416}, + {1000721540, 153.964863}, + {1000571550, 154.95928}, + {1000581550, 154.948706}, + {1000591550, 154.940509193}, + {1000601550, 154.933135598}, + {1000611550, 154.928136951}, + {1000621550, 154.924646645}, + {1000631550, 154.922899847}, + {1000641550, 154.922629356}, + {1000651550, 154.923509511}, + {1000661550, 154.925758049}, + {1000671550, 154.929103363}, + {1000681550, 154.93321571}, + {1000691550, 154.939209576}, + {1000701550, 154.945783216}, + {1000711550, 154.954326005}, + {1000721550, 154.963167}, + {1000731550, 154.974248}, + {1000571560, 155.964519}, + {1000581560, 155.951884}, + {1000591560, 155.9447669}, + {1000601560, 155.935370358}, + {1000611560, 155.931114059}, + {1000621560, 155.925538191}, + {1000631560, 155.924762976}, + {1000641560, 155.92213012}, + {1000651560, 155.924754209}, + {1000661560, 155.924283593}, + {1000671560, 155.929641634}, + {1000681560, 155.931065926}, + {1000691560, 155.938985746}, + {1000701560, 155.942817096}, + {1000711560, 155.953086606}, + {1000721560, 155.959399083}, + {1000731560, 155.972087}, + {1000571570, 156.968792}, + {1000581570, 156.957133}, + {1000591570, 156.9480031}, + {1000601570, 156.939351074}, + {1000611570, 156.933121298}, + {1000621570, 156.928418598}, + {1000631570, 156.925432556}, + {1000641570, 156.923967424}, + {1000651570, 156.924031888}, + {1000661570, 156.925469555}, + {1000671570, 156.928251974}, + {1000681570, 156.931922652}, + {1000691570, 156.936973}, + {1000701570, 156.94265136799999}, + {1000711570, 156.950144807}, + {1000721570, 156.958288}, + {1000731570, 156.968227445}, + {1000741570, 156.978862}, + {1000581580, 157.960773}, + {1000591580, 157.952603}, + {1000601580, 157.94220562}, + {1000611580, 157.936546948}, + {1000621580, 157.929949262}, + {1000631580, 157.927782192}, + {1000641580, 157.9241112}, + {1000651580, 157.925419942}, + {1000661580, 157.924414817}, + {1000671580, 157.92894491}, + {1000681580, 157.929893474}, + {1000691580, 157.936979525}, + {1000701580, 157.939871202}, + {1000711580, 157.94931562}, + {1000721580, 157.954801217}, + {1000731580, 157.966593}, + {1000741580, 157.974565}, + {1000581590, 158.966355}, + {1000591590, 158.956232}, + {1000601590, 158.946619085}, + {1000611590, 158.939286409}, + {1000621590, 158.93321713}, + {1000631590, 158.929099512}, + {1000641590, 158.926395822}, + {1000651590, 158.925353707}, + {1000661590, 158.925745938}, + {1000671590, 158.927718683}, + {1000681590, 158.93069079}, + {1000691590, 158.934975}, + {1000701590, 158.940060257}, + {1000711590, 158.946635615}, + {1000721590, 158.953995837}, + {1000731590, 158.963028046}, + {1000741590, 158.972696}, + {1000751590, 158.984106}, + {1000591600, 159.961138}, + {1000601600, 159.949839172}, + {1000611600, 159.943215272}, + {1000621600, 159.935337032}, + {1000631600, 159.931836982}, + {1000641600, 159.927061202}, + {1000651600, 159.927174553}, + {1000661600, 159.925203578}, + {1000671600, 159.928735538}, + {1000681600, 159.929077193}, + {1000691600, 159.935264177}, + {1000701600, 159.93755921}, + {1000711600, 159.946033}, + {1000721600, 159.950682728}, + {1000731600, 159.961541678}, + {1000741600, 159.968513946}, + {1000751600, 159.98188}, + {1000591610, 160.965121}, + {1000601610, 160.954664}, + {1000611610, 160.946229837}, + {1000621610, 160.939160062}, + {1000631610, 160.933663991}, + {1000641610, 160.929676267}, + {1000651610, 160.927576806}, + {1000661610, 160.926939425}, + {1000671610, 160.927861815}, + {1000681610, 160.93000353}, + {1000691610, 160.933549}, + {1000701610, 160.937912384}, + {1000711610, 160.943572}, + {1000721610, 160.950277927}, + {1000731610, 160.958369489}, + {1000741610, 160.967249}, + {1000751610, 160.977624313}, + {1000761610, 160.989054}, + {1000601620, 161.958121}, + {1000611620, 161.950574}, + {1000621620, 161.941621687}, + {1000631620, 161.936958329}, + {1000641620, 161.930991812}, + {1000651620, 161.9292754}, + {1000661620, 161.92680450699999}, + {1000671620, 161.929102543}, + {1000681620, 161.928787299}, + {1000691620, 161.934001211}, + {1000701620, 161.935779342}, + {1000711620, 161.943282776}, + {1000721620, 161.947215526}, + {1000731620, 161.957292907}, + {1000741620, 161.963500341}, + {1000751620, 161.975896}, + {1000761620, 161.984434}, + {1000601630, 162.963414}, + {1000611630, 162.953881}, + {1000621630, 162.945679085}, + {1000631630, 162.93926551}, + {1000641630, 162.93409664}, + {1000651630, 162.930653609}, + {1000661630, 162.928737221}, + {1000671630, 162.92874026}, + {1000681630, 162.930039908}, + {1000691630, 162.932658282}, + {1000701630, 162.936345406}, + {1000711630, 162.941179}, + {1000721630, 162.947107211}, + {1000731630, 162.954337194}, + {1000741630, 162.962524251}, + {1000751630, 162.972085434}, + {1000761630, 162.982462}, + {1000771630, 162.994299}, + {1000611640, 163.958819}, + {1000621640, 163.948550061}, + {1000631640, 163.942852943}, + {1000641640, 163.935916193}, + {1000651640, 163.933327561}, + {1000661640, 163.929180819}, + {1000671640, 163.930240548}, + {1000681640, 163.929207739}, + {1000691640, 163.933538019}, + {1000701640, 163.934500743}, + {1000711640, 163.941339}, + {1000721640, 163.944370709}, + {1000731640, 163.953534}, + {1000741640, 163.958952445}, + {1000751640, 163.970507122}, + {1000761640, 163.978073158}, + {1000771640, 163.991966}, + {1000611650, 164.96278}, + {1000621650, 164.95329}, + {1000631650, 164.94554007}, + {1000641650, 164.93931708}, + {1000651650, 164.934955198}, + {1000661650, 164.931709402}, + {1000671650, 164.930329116}, + {1000681650, 164.930733482}, + {1000691650, 164.932441843}, + {1000701650, 164.935270241}, + {1000711650, 164.939406758}, + {1000721650, 164.944567}, + {1000731650, 164.950780287}, + {1000741650, 164.958280663}, + {1000751650, 164.967085831}, + {1000761650, 164.976654}, + {1000771650, 164.987552}, + {1000781650, 164.999658}, + {1000621660, 165.956575}, + {1000631660, 165.949813}, + {1000641660, 165.941630413}, + {1000651660, 165.937939727}, + {1000661660, 165.93281281}, + {1000671660, 165.932291209}, + {1000681660, 165.930301067}, + {1000691660, 165.933562136}, + {1000701660, 165.933876439}, + {1000711660, 165.939859}, + {1000721660, 165.94218}, + {1000731660, 165.950512}, + {1000741660, 165.955031952}, + {1000751660, 165.965821216}, + {1000761660, 165.972698135}, + {1000771660, 165.985716}, + {1000781660, 165.994866}, + {1000621670, 166.962072}, + {1000631670, 166.953011}, + {1000641670, 166.945490012}, + {1000651670, 166.940007046}, + {1000661670, 166.935682415}, + {1000671670, 166.933140254}, + {1000681670, 166.932056192}, + {1000691670, 166.932857206}, + {1000701670, 166.934954069}, + {1000711670, 166.938243}, + {1000721670, 166.9426}, + {1000731670, 166.948093}, + {1000741670, 166.95481108}, + {1000751670, 166.962604}, + {1000761670, 166.971552304}, + {1000771670, 166.981671973}, + {1000781670, 166.99275}, + {1000621680, 167.966033}, + {1000631680, 167.957863}, + {1000641680, 167.948309}, + {1000651680, 167.943337074}, + {1000661680, 167.937134977}, + {1000671680, 167.935523766}, + {1000681680, 167.932378282}, + {1000691680, 167.934178457}, + {1000701680, 167.933891297}, + {1000711680, 167.938729798}, + {1000721680, 167.940568}, + {1000731680, 167.948047}, + {1000741680, 167.951805459}, + {1000751680, 167.961572607}, + {1000761680, 167.96779905}, + {1000771680, 167.979960978}, + {1000781680, 167.988180196}, + {1000791680, 168.002716}, + {1000631690, 168.961717}, + {1000641690, 168.952882}, + {1000651690, 168.945807}, + {1000661690, 168.940315231}, + {1000671690, 168.93687989}, + {1000681690, 168.934598444}, + {1000691690, 168.934218956}, + {1000701690, 168.935184208}, + {1000711690, 168.937645845}, + {1000721690, 168.941259}, + {1000731690, 168.946011}, + {1000741690, 168.951778689}, + {1000751690, 168.958765979}, + {1000761690, 168.967017521}, + {1000771690, 168.976281743}, + {1000781690, 168.986619}, + {1000791690, 168.99808}, + {1000631700, 169.96687}, + {1000641700, 169.956146}, + {1000651700, 169.949855}, + {1000661700, 169.94234}, + {1000671700, 169.939626548}, + {1000681700, 169.935471933}, + {1000691700, 169.935807093}, + {1000701700, 169.934767242}, + {1000711700, 169.93847923}, + {1000721700, 169.939609}, + {1000731700, 169.946175}, + {1000741700, 169.949231235}, + {1000751700, 169.958234844}, + {1000761700, 169.963579273}, + {1000771700, 169.975113}, + {1000781700, 169.982502087}, + {1000791700, 169.996024}, + {1000801700, 170.005814}, + {1000641710, 170.961127}, + {1000651710, 170.953011}, + {1000661710, 170.946312}, + {1000671710, 170.941472713}, + {1000681710, 170.938037372}, + {1000691710, 170.936435162}, + {1000701710, 170.936331515}, + {1000711710, 170.937918591}, + {1000721710, 170.940492}, + {1000731710, 170.944476}, + {1000741710, 170.949451}, + {1000751710, 170.955716}, + {1000761710, 170.963180402}, + {1000771710, 170.97164552}, + {1000781710, 170.981248868}, + {1000791710, 170.991881533}, + {1000801710, 171.003585}, + {1000641720, 171.964605}, + {1000651720, 171.957391}, + {1000661720, 171.948728}, + {1000671720, 171.94473}, + {1000681720, 171.939363461}, + {1000691720, 171.938406959}, + {1000701720, 171.936386654}, + {1000711720, 171.93909132}, + {1000721720, 171.939449716}, + {1000731720, 171.944895}, + {1000741720, 171.947292}, + {1000751720, 171.955376165}, + {1000761720, 171.960017309}, + {1000771720, 171.970607035}, + {1000781720, 171.977341059}, + {1000791720, 171.989996704}, + {1000801720, 171.998860581}, + {1000651730, 172.960805}, + {1000661730, 172.953043}, + {1000671730, 172.94702}, + {1000681730, 172.9424}, + {1000691730, 172.93960663}, + {1000701730, 172.938216211}, + {1000711730, 172.938935722}, + {1000721730, 172.940513}, + {1000731730, 172.94375}, + {1000741730, 172.947689}, + {1000751730, 172.953243}, + {1000761730, 172.959808387}, + {1000771730, 172.967505477}, + {1000781730, 172.976449922}, + {1000791730, 172.986224263}, + {1000801730, 172.997143}, + {1000651740, 173.965679}, + {1000661740, 173.955845}, + {1000671740, 173.950757}, + {1000681740, 173.94423}, + {1000691740, 173.942174061}, + {1000701740, 173.938867545}, + {1000711740, 173.94034284}, + {1000721740, 173.940048377}, + {1000731740, 173.944454}, + {1000741740, 173.946079}, + {1000751740, 173.953115}, + {1000761740, 173.957063192}, + {1000771740, 173.966949939}, + {1000781740, 173.972820431}, + {1000791740, 173.984908}, + {1000801740, 173.992870575}, + {1000661750, 174.960569}, + {1000671750, 174.953516}, + {1000681750, 174.94777}, + {1000691750, 174.94384231}, + {1000701750, 174.941281907}, + {1000711750, 174.940777211}, + {1000721750, 174.941511424}, + {1000731750, 174.943737}, + {1000741750, 174.946717}, + {1000751750, 174.951381}, + {1000761750, 174.956945126}, + {1000771750, 174.964149519}, + {1000781750, 174.972400593}, + {1000791750, 174.981316375}, + {1000801750, 174.991444451}, + {1000661760, 175.963918}, + {1000671760, 175.957713}, + {1000681760, 175.94994}, + {1000691760, 175.946997707}, + {1000701760, 175.942574706}, + {1000711760, 175.942691711}, + {1000721760, 175.941409797}, + {1000731760, 175.944857}, + {1000741760, 175.945634}, + {1000751760, 175.951623}, + {1000761760, 175.954770315}, + {1000771760, 175.963626261}, + {1000781760, 175.968938162}, + {1000791760, 175.980116925}, + {1000801760, 175.98734867}, + {1000811760, 176.000627731}, + {1000671770, 176.961052}, + {1000681770, 176.95399}, + {1000691770, 176.948932}, + {1000701770, 176.945263846}, + {1000711770, 176.94376357}, + {1000721770, 176.943230187}, + {1000731770, 176.94448194}, + {1000741770, 176.946643}, + {1000751770, 176.950328}, + {1000761770, 176.954957902}, + {1000771770, 176.9613015}, + {1000781770, 176.968469541}, + {1000791770, 176.976869701}, + {1000801770, 176.98628459}, + {1000811770, 176.996414252}, + {1000671780, 177.965507}, + {1000681780, 177.956779}, + {1000691780, 177.952506}, + {1000701780, 177.9466694}, + {1000711780, 177.945960065}, + {1000721780, 177.943708322}, + {1000731780, 177.94568}, + {1000741780, 177.945885791}, + {1000751780, 177.950989}, + {1000761780, 177.953253334}, + {1000771780, 177.961079395}, + {1000781780, 177.965649288}, + {1000791780, 177.976056714}, + {1000801780, 177.982484756}, + {1000811780, 177.995047}, + {1000821780, 178.003836171}, + {1000681790, 178.961267}, + {1000691790, 178.955018}, + {1000701790, 178.94993}, + {1000711790, 178.947332985}, + {1000721790, 178.945825705}, + {1000731790, 178.94593905}, + {1000741790, 178.947079378}, + {1000751790, 178.949989686}, + {1000761790, 178.953815985}, + {1000771790, 178.959117594}, + {1000781790, 178.965358742}, + {1000791790, 178.973173666}, + {1000801790, 178.981821759}, + {1000811790, 178.991122185}, + {1000821790, 179.002202492}, + {1000681800, 179.96438}, + {1000691800, 179.959023}, + {1000701800, 179.951991}, + {1000711800, 179.949890744}, + {1000721800, 179.946559537}, + {1000731800, 179.947467589}, + {1000741800, 179.946713304}, + {1000751800, 179.950791568}, + {1000761800, 179.952381665}, + {1000771800, 179.959229446}, + {1000781800, 179.96303801}, + {1000791800, 179.972489738}, + {1000801800, 179.97826018}, + {1000811800, 179.98991895}, + {1000821800, 179.997916177}, + {1000691810, 180.961954}, + {1000701810, 180.95589}, + {1000711810, 180.951908}, + {1000721810, 180.949110834}, + {1000731810, 180.947998528}, + {1000741810, 180.948218733}, + {1000751810, 180.950061507}, + {1000761810, 180.953247188}, + {1000771810, 180.957634691}, + {1000781810, 180.963089946}, + {1000791810, 180.970079102}, + {1000801810, 180.977819368}, + {1000811810, 180.986259978}, + {1000821810, 180.9966606}, + {1000691820, 181.966194}, + {1000701820, 181.958239}, + {1000711820, 181.955158}, + {1000721820, 181.950563684}, + {1000731820, 181.950154612}, + {1000741820, 181.948205636}, + {1000751820, 181.95121156}, + {1000761820, 181.952110154}, + {1000771820, 181.958076296}, + {1000781820, 181.961171605}, + {1000791820, 181.969614433}, + {1000801820, 181.974689173}, + {1000811820, 181.985692649}, + {1000821820, 181.992673537}, + {1000701830, 182.962426}, + {1000711830, 182.957363}, + {1000721830, 182.953533203}, + {1000731830, 182.95137538}, + {1000741830, 182.950224416}, + {1000751830, 182.950821306}, + {1000761830, 182.953125028}, + {1000771830, 182.956841231}, + {1000781830, 182.961595895}, + {1000791830, 182.967588106}, + {1000801830, 182.974444652}, + {1000811830, 182.982192843}, + {1000821830, 182.991862527}, + {1000701840, 183.965002}, + {1000711840, 183.96103}, + {1000721840, 183.955448507}, + {1000731840, 183.954009958}, + {1000741840, 183.95093318}, + {1000751840, 183.952528073}, + {1000761840, 183.952492919}, + {1000771840, 183.957476}, + {1000781840, 183.959921929}, + {1000791840, 183.967451523}, + {1000801840, 183.971717709}, + {1000811840, 183.981874973}, + {1000821840, 183.988135634}, + {1000831840, 184.001347}, + {1000701850, 184.969425}, + {1000711850, 184.963542}, + {1000721850, 184.958862}, + {1000731850, 184.955561317}, + {1000741850, 184.953421206}, + {1000751850, 184.95295832}, + {1000761850, 184.954045969}, + {1000771850, 184.956698}, + {1000781850, 184.960613659}, + {1000791850, 184.965798871}, + {1000801850, 184.971890696}, + {1000811850, 184.978789189}, + {1000821850, 184.98761}, + {1000831850, 184.9976}, + {1000711860, 185.96745}, + {1000721860, 185.960897}, + {1000731860, 185.958553036}, + {1000741860, 185.95436514}, + {1000751860, 185.954989172}, + {1000761860, 185.953837569}, + {1000771860, 185.957946754}, + {1000781860, 185.959350845}, + {1000791860, 185.965952703}, + {1000801860, 185.969362061}, + {1000811860, 185.978654787}, + {1000821860, 185.984239409}, + {1000831860, 185.996623169}, + {1000841860, 186.004403174}, + {1000711870, 186.970188}, + {1000721870, 186.964573}, + {1000731870, 186.960391}, + {1000741870, 186.957161249}, + {1000751870, 186.955752217}, + {1000761870, 186.955749569}, + {1000771870, 186.957542}, + {1000781870, 186.960616646}, + {1000791870, 186.964542147}, + {1000801870, 186.96981354}, + {1000811870, 186.97590474}, + {1000821870, 186.983910842}, + {1000831870, 186.993147272}, + {1000841870, 187.003031482}, + {1000711880, 187.974428}, + {1000721880, 187.966903}, + {1000731880, 187.963596}, + {1000741880, 187.958488325}, + {1000751880, 187.958113658}, + {1000761880, 187.955837292}, + {1000771880, 187.958834999}, + {1000781880, 187.959397521}, + {1000791880, 187.965247966}, + {1000801880, 187.967580738}, + {1000811880, 187.976020886}, + {1000821880, 187.980879079}, + {1000831880, 187.992276064}, + {1000841880, 187.999415586}, + {1000721890, 188.970853}, + {1000731890, 188.96569}, + {1000741890, 188.961557}, + {1000751890, 188.959227764}, + {1000761890, 188.958145949}, + {1000771890, 188.958722602}, + {1000781890, 188.960848485}, + {1000791890, 188.963948286}, + {1000801890, 188.968194776}, + {1000811890, 188.973573525}, + {1000821890, 188.980843658}, + {1000831890, 188.989195139}, + {1000841890, 188.998473425}, + {1000721900, 189.973376}, + {1000731900, 189.969168}, + {1000741900, 189.963103542}, + {1000751900, 189.961800064}, + {1000761900, 189.958445442}, + {1000771900, 189.960543374}, + {1000781900, 189.959949823}, + {1000791900, 189.964751746}, + {1000801900, 189.96632225}, + {1000811900, 189.973841771}, + {1000821900, 189.978081872}, + {1000831900, 189.988624828}, + {1000841900, 189.995101731}, + {1000731910, 190.97153}, + {1000741910, 190.966531}, + {1000751910, 190.963123322}, + {1000761910, 190.960928105}, + {1000771910, 190.960591455}, + {1000781910, 190.961676261}, + {1000791910, 190.963716452}, + {1000801910, 190.967158301}, + {1000811910, 190.971784093}, + {1000821910, 190.978216455}, + {1000831910, 190.985786972}, + {1000841910, 190.994558494}, + {1000851910, 191.004148081}, + {1000731920, 191.975201}, + {1000741920, 191.968202}, + {1000751920, 191.966088}, + {1000761920, 191.961478765}, + {1000771920, 191.962602414}, + {1000781920, 191.961042667}, + {1000791920, 191.964817615}, + {1000801920, 191.965634263}, + {1000811920, 191.972225}, + {1000821920, 191.975789598}, + {1000831920, 191.985470077}, + {1000841920, 191.991340274}, + {1000851920, 192.003140912}, + {1000731930, 192.97766}, + {1000741930, 192.971884}, + {1000751930, 192.967545}, + {1000761930, 192.964149637}, + {1000771930, 192.962923753}, + {1000781930, 192.962984546}, + {1000791930, 192.964138442}, + {1000801930, 192.966653395}, + {1000811930, 192.970501994}, + {1000821930, 192.976135914}, + {1000831930, 192.98294722}, + {1000841930, 192.991062421}, + {1000851930, 192.999927725}, + {1000861930, 193.009707973}, + {1000731940, 193.98161}, + {1000741940, 193.973795}, + {1000751940, 193.970735}, + {1000761940, 193.965179407}, + {1000771940, 193.965075703}, + {1000781940, 193.962683498}, + {1000791940, 193.965419051}, + {1000801940, 193.965449108}, + {1000811940, 193.971081408}, + {1000821940, 193.974011788}, + {1000831940, 193.982798581}, + {1000841940, 193.988186058}, + {1000851940, 193.999230816}, + {1000861940, 194.006145636}, + {1000741950, 194.977735}, + {1000751950, 194.97256}, + {1000761950, 194.968318}, + {1000771950, 194.965976898}, + {1000781950, 194.964794325}, + {1000791950, 194.965037823}, + {1000801950, 194.966705809}, + {1000811950, 194.969774052}, + {1000821950, 194.974516167}, + {1000831950, 194.980648759}, + {1000841950, 194.988065781}, + {1000851950, 194.99627448}, + {1000861950, 195.005421703}, + {1000741960, 195.979882}, + {1000751960, 195.975996}, + {1000761960, 195.969643261}, + {1000771960, 195.968399669}, + {1000781960, 195.964954648}, + {1000791960, 195.966571213}, + {1000801960, 195.965833445}, + {1000811960, 195.970481189}, + {1000821960, 195.972787552}, + {1000831960, 195.980666509}, + {1000841960, 195.985540722}, + {1000851960, 195.995799034}, + {1000861960, 196.002120431}, + {1000741970, 196.984036}, + {1000751970, 196.978153}, + {1000761970, 196.973076}, + {1000771970, 196.969657217}, + {1000781970, 196.96734303}, + {1000791970, 196.966570103}, + {1000801970, 196.967213715}, + {1000811970, 196.969560492}, + {1000821970, 196.973434737}, + {1000831970, 196.978864927}, + {1000841970, 196.985621939}, + {1000851970, 196.993177353}, + {1000861970, 197.001621446}, + {1000871970, 197.011008086}, + {1000751980, 197.98176}, + {1000761980, 197.974664}, + {1000771980, 197.972399}, + {1000781980, 197.967896718}, + {1000791980, 197.968243714}, + {1000801980, 197.966769177}, + {1000811980, 197.970446669}, + {1000821980, 197.97201545}, + {1000831980, 197.979201316}, + {1000841980, 197.983388753}, + {1000851980, 197.992797864}, + {1000861980, 197.998679197}, + {1000871980, 198.010282081}, + {1000751990, 198.984187}, + {1000761990, 198.978239}, + {1000771990, 198.973807097}, + {1000781990, 198.970597022}, + {1000791990, 198.968766573}, + {1000801990, 198.968280994}, + {1000811990, 198.969877}, + {1000821990, 198.97291262}, + {1000831990, 198.977672841}, + {1000841990, 198.983640445}, + {1000851990, 198.990527715}, + {1000861990, 198.998325436}, + {1000871990, 199.007269384}, + {1000762000, 199.980086}, + {1000772000, 199.976844}, + {1000782000, 199.971444609}, + {1000792000, 199.970756558}, + {1000802000, 199.968326941}, + {1000812000, 199.970963608}, + {1000822000, 199.971818546}, + {1000832000, 199.97813129}, + {1000842000, 199.981812355}, + {1000852000, 199.990351099}, + {1000862000, 199.995705335}, + {1000872000, 200.006584666}, + {1000762010, 200.984069}, + {1000772010, 200.978701}, + {1000782010, 200.974513305}, + {1000792010, 200.971657678}, + {1000802010, 200.970303054}, + {1000812010, 200.970820235}, + {1000822010, 200.972870431}, + {1000832010, 200.976995017}, + {1000842010, 200.982263799}, + {1000852010, 200.988417058}, + {1000862010, 200.995590511}, + {1000872010, 201.003852491}, + {1000882010, 201.012814699}, + {1000762020, 201.986548}, + {1000772020, 201.982136}, + {1000782020, 201.975639}, + {1000792020, 201.973856}, + {1000802020, 201.970643604}, + {1000812020, 201.972108874}, + {1000822020, 201.972151613}, + {1000832020, 201.977723042}, + {1000842020, 201.980738934}, + {1000852020, 201.988625686}, + {1000862020, 201.993263982}, + {1000872020, 202.003329637}, + {1000882020, 202.009742305}, + {1000762030, 202.992195}, + {1000772030, 202.984573}, + {1000782030, 202.979055}, + {1000792030, 202.975154492}, + {1000802030, 202.972872396}, + {1000812030, 202.972344098}, + {1000822030, 202.973390617}, + {1000832030, 202.976892077}, + {1000842030, 202.981416072}, + {1000852030, 202.986942904}, + {1000862030, 202.993361155}, + {1000872030, 203.000940867}, + {1000882030, 203.009233907}, + {1000772040, 203.989726}, + {1000782040, 203.981084}, + {1000792040, 203.97811}, + {1000802040, 203.973494037}, + {1000812040, 203.97386342}, + {1000822040, 203.973043506}, + {1000832040, 203.977835687}, + {1000842040, 203.980310078}, + {1000852040, 203.987251393}, + {1000862040, 203.991443729}, + {1000872040, 204.000651972}, + {1000882040, 204.006506855}, + {1000772050, 204.993988}, + {1000782050, 204.986237}, + {1000792050, 204.980064}, + {1000802050, 204.976073151}, + {1000812050, 204.974427318}, + {1000822050, 204.974481682}, + {1000832050, 204.977385182}, + {1000842050, 204.981190006}, + {1000852050, 204.986060546}, + {1000862050, 204.991723228}, + {1000872050, 204.998593854}, + {1000882050, 205.006230692}, + {1000892050, 205.015144152}, + {1000782060, 205.99008}, + {1000792060, 205.984766}, + {1000802060, 205.977513837}, + {1000812060, 205.976110108}, + {1000822060, 205.97446521}, + {1000832060, 205.978498843}, + {1000842060, 205.980473662}, + {1000852060, 205.986645768}, + {1000862060, 205.990195409}, + {1000872060, 205.998661441}, + {1000882060, 206.003827842}, + {1000892060, 206.014476477}, + {1000782070, 206.995556}, + {1000792070, 206.988577}, + {1000802070, 206.9823}, + {1000812070, 206.977418605}, + {1000822070, 206.975896821}, + {1000832070, 206.978470551}, + {1000842070, 206.981593334}, + {1000852070, 206.985799715}, + {1000862070, 206.990730224}, + {1000872070, 206.99694145}, + {1000882070, 207.00377242}, + {1000892070, 207.011965967}, + {1000782080, 207.999463}, + {1000792080, 207.993655}, + {1000802080, 207.985759}, + {1000812080, 207.982018006}, + {1000822080, 207.976652005}, + {1000832080, 207.97974206}, + {1000842080, 207.981246035}, + {1000852080, 207.986613011}, + {1000862080, 207.989634513}, + {1000872080, 207.997139082}, + {1000882080, 208.001855012}, + {1000892080, 208.011552251}, + {1000902080, 208.017915348}, + {1000792090, 208.997606}, + {1000802090, 208.990757}, + {1000812090, 208.985351713}, + {1000822090, 208.981089978}, + {1000832090, 208.980398599}, + {1000842090, 208.982430361}, + {1000852090, 208.986168701}, + {1000862090, 208.990401389}, + {1000872090, 208.995939701}, + {1000882090, 209.001994902}, + {1000892090, 209.009495375}, + {1000902090, 209.017601}, + {1000792100, 210.002877}, + {1000802100, 209.99431}, + {1000812100, 209.990072942}, + {1000822100, 209.984188381}, + {1000832100, 209.984120237}, + {1000842100, 209.982873686}, + {1000852100, 209.987147423}, + {1000862100, 209.989688862}, + {1000872100, 209.996410596}, + {1000882100, 210.000475406}, + {1000892100, 210.009408625}, + {1000902100, 210.015093515}, + {1000802110, 210.999581}, + {1000812110, 210.993475}, + {1000822110, 210.988735288}, + {1000832110, 210.987268715}, + {1000842110, 210.986653171}, + {1000852110, 210.987496226}, + {1000862110, 210.990600767}, + {1000872110, 210.995555189}, + {1000882110, 211.000893049}, + {1000892110, 211.007668846}, + {1000902110, 211.014896923}, + {1000912110, 211.023674036}, + {1000802120, 212.003242}, + {1000812120, 211.998335}, + {1000822120, 211.991895891}, + {1000832120, 211.99128503}, + {1000842120, 211.988867982}, + {1000852120, 211.990737301}, + {1000862120, 211.990703946}, + {1000872120, 211.99622542}, + {1000882120, 211.999786619}, + {1000892120, 212.007836442}, + {1000902120, 212.01300157}, + {1000912120, 212.023184819}, + {1000802130, 213.008803}, + {1000812130, 213.001915}, + {1000822130, 212.996560796}, + {1000832130, 212.99438357}, + {1000842130, 212.992857154}, + {1000852130, 212.992936593}, + {1000862130, 212.993885147}, + {1000872130, 212.99618441}, + {1000882130, 213.000370971}, + {1000892130, 213.006592665}, + {1000902130, 213.01301147}, + {1000912130, 213.021099644}, + {1000802140, 214.012636}, + {1000812140, 214.00694}, + {1000822140, 213.999803521}, + {1000832140, 213.998710909}, + {1000842140, 213.995201287}, + {1000852140, 213.996372331}, + {1000862140, 213.99536265}, + {1000872140, 213.998971193}, + {1000882140, 214.00009956}, + {1000892140, 214.0069064}, + {1000902140, 214.01148148}, + {1000912140, 214.020891055}, + {1000802150, 215.018368}, + {1000812150, 215.010768}, + {1000822150, 215.004661591}, + {1000832150, 215.001749095}, + {1000842150, 214.999418385}, + {1000852150, 214.998651002}, + {1000862150, 214.998745037}, + {1000872150, 215.000341534}, + {1000882150, 215.002718208}, + {1000892150, 215.006474061}, + {1000902150, 215.01172464}, + {1000912150, 215.019113955}, + {1000922150, 215.026719774}, + {1000802160, 216.022459}, + {1000812160, 216.015964}, + {1000822160, 216.008062}, + {1000832160, 216.006305985}, + {1000842160, 216.001913416}, + {1000852160, 216.002422643}, + {1000862160, 216.000271942}, + {1000872160, 216.003189523}, + {1000882160, 216.003533534}, + {1000892160, 216.008749101}, + {1000902160, 216.011055933}, + {1000912160, 216.019134633}, + {1000922160, 216.024762829}, + {1000812170, 217.020032}, + {1000822170, 217.013162}, + {1000832170, 217.009372}, + {1000842170, 217.006316145}, + {1000852170, 217.004717794}, + {1000862170, 217.003927632}, + {1000872170, 217.00463198}, + {1000882170, 217.006322676}, + {1000892170, 217.009342325}, + {1000902170, 217.013103443}, + {1000912170, 217.018309024}, + {1000922170, 217.02466}, + {1000812180, 218.025454}, + {1000822180, 218.016779}, + {1000832180, 218.014188}, + {1000842180, 218.008971234}, + {1000852180, 218.008695941}, + {1000862180, 218.005601123}, + {1000872180, 218.00757862}, + {1000882180, 218.007134297}, + {1000892180, 218.01164886}, + {1000902180, 218.013276248}, + {1000912180, 218.020021133}, + {1000922180, 218.023504877}, + {1000822190, 219.022136}, + {1000832190, 219.01752}, + {1000842190, 219.013614}, + {1000852190, 219.011160587}, + {1000862190, 219.009478683}, + {1000872190, 219.009250664}, + {1000882190, 219.010084715}, + {1000892190, 219.012420425}, + {1000902190, 219.015526432}, + {1000912190, 219.019949909}, + {1000922190, 219.025009233}, + {1000932190, 219.031601865}, + {1000822200, 220.025905}, + {1000832200, 220.022501}, + {1000842200, 220.016386}, + {1000852200, 220.015433}, + {1000862200, 220.011392443}, + {1000872200, 220.012326789}, + {1000882200, 220.011027542}, + {1000892200, 220.014754527}, + {1000902200, 220.015769866}, + {1000912200, 220.021769753}, + {1000922200, 220.024706}, + {1000932200, 220.03271628}, + {1000832210, 221.02598}, + {1000842210, 221.021228}, + {1000852210, 221.018017}, + {1000862210, 221.015535637}, + {1000872210, 221.014253714}, + {1000882210, 221.013917293}, + {1000892210, 221.015599721}, + {1000902210, 221.018185757}, + {1000912210, 221.021873393}, + {1000922210, 221.026323297}, + {1000932210, 221.03211}, + {1000942210, 221.038572}, + {1000832220, 222.031079}, + {1000842220, 222.02414}, + {1000852220, 222.022494}, + {1000862220, 222.017576017}, + {1000872220, 222.017582615}, + {1000882220, 222.015373371}, + {1000892220, 222.017844232}, + {1000902220, 222.01846822}, + {1000912220, 222.023687064}, + {1000922220, 222.026057957}, + {1000932220, 222.033574706}, + {1000942220, 222.037638}, + {1000832230, 223.034611}, + {1000842230, 223.02907}, + {1000852230, 223.025151}, + {1000862230, 223.021889283}, + {1000872230, 223.019734241}, + {1000882230, 223.018500648}, + {1000892230, 223.019135982}, + {1000902230, 223.020811083}, + {1000912230, 223.023980414}, + {1000922230, 223.027960754}, + {1000932230, 223.03291334}, + {1000942230, 223.038777}, + {1000952230, 223.04584}, + {1000832240, 224.039796}, + {1000842240, 224.03211}, + {1000852240, 224.029749}, + {1000862240, 224.024095803}, + {1000872240, 224.023348096}, + {1000882240, 224.020210361}, + {1000892240, 224.021722249}, + {1000902240, 224.021466137}, + {1000912240, 224.025617286}, + {1000922240, 224.027635913}, + {1000932240, 224.03438803}, + {1000942240, 224.037875}, + {1000952240, 224.046442}, + {1000842250, 225.037123}, + {1000852250, 225.032528}, + {1000862250, 225.028485572}, + {1000872250, 225.025572466}, + {1000882250, 225.023610502}, + {1000892250, 225.023228601}, + {1000902250, 225.023950975}, + {1000912250, 225.026147927}, + {1000922250, 225.02938505}, + {1000932250, 225.033943422}, + {1000942250, 225.03897}, + {1000952250, 225.045508}, + {1000842260, 226.04031}, + {1000852260, 226.037209}, + {1000862260, 226.03086138}, + {1000872260, 226.029544512}, + {1000882260, 226.025408186}, + {1000892260, 226.026096999}, + {1000902260, 226.024903699}, + {1000912260, 226.027948217}, + {1000922260, 226.029338669}, + {1000932260, 226.035230364}, + {1000942260, 226.03825}, + {1000952260, 226.04613}, + {1000842270, 227.04539}, + {1000852270, 227.040183}, + {1000862270, 227.035304393}, + {1000872270, 227.031865413}, + {1000882270, 227.029176205}, + {1000892270, 227.027750594}, + {1000902270, 227.027702546}, + {1000912270, 227.028803586}, + {1000922270, 227.031181124}, + {1000932270, 227.034975012}, + {1000942270, 227.039474}, + {1000952270, 227.045282}, + {1000852280, 228.04496}, + {1000862280, 228.037835415}, + {1000872280, 228.035839433}, + {1000882280, 228.031068574}, + {1000892280, 228.031019685}, + {1000902280, 228.028739741}, + {1000912280, 228.031050758}, + {1000922280, 228.031368959}, + {1000932280, 228.036313}, + {1000942280, 228.038763325}, + {1000952280, 228.046001}, + {1000852290, 229.048191}, + {1000862290, 229.042257272}, + {1000872290, 229.038291443}, + {1000882290, 229.034956703}, + {1000892290, 229.032947}, + {1000902290, 229.031761357}, + {1000912290, 229.032095585}, + {1000922290, 229.033505976}, + {1000932290, 229.036287269}, + {1000942290, 229.040145099}, + {1000952290, 229.045282534}, + {1000862300, 230.045271}, + {1000872300, 230.042390787}, + {1000882300, 230.037054776}, + {1000892300, 230.036327}, + {1000902300, 230.033132267}, + {1000912300, 230.034539717}, + {1000922300, 230.033940114}, + {1000932300, 230.03782806}, + {1000942300, 230.039648313}, + {1000952300, 230.046025}, + {1000862310, 231.049973}, + {1000872310, 231.045175353}, + {1000882310, 231.041027085}, + {1000892310, 231.038393}, + {1000902310, 231.036302764}, + {1000912310, 231.0358825}, + {1000922310, 231.03629218}, + {1000932310, 231.038243598}, + {1000942310, 231.041125946}, + {1000952310, 231.045529}, + {1000962310, 231.050746}, + {1000872320, 232.049461219}, + {1000882320, 232.043475267}, + {1000892320, 232.042034}, + {1000902320, 232.038053606}, + {1000912320, 232.038590205}, + {1000922320, 232.037154765}, + {1000932320, 232.040107}, + {1000942320, 232.041182133}, + {1000952320, 232.046613}, + {1000962320, 232.04974}, + {1000872330, 233.052517833}, + {1000882330, 233.04759457}, + {1000892330, 233.044346}, + {1000902330, 233.041580126}, + {1000912330, 233.040246535}, + {1000922330, 233.039634294}, + {1000932330, 233.040739421}, + {1000942330, 233.042997411}, + {1000952330, 233.046468}, + {1000962330, 233.050771485}, + {1000972330, 233.056652}, + {1000882340, 234.0503821}, + {1000892340, 234.048139}, + {1000902340, 234.043599801}, + {1000912340, 234.043305555}, + {1000922340, 234.040950296}, + {1000932340, 234.042893245}, + {1000942340, 234.043317489}, + {1000952340, 234.047731}, + {1000962340, 234.050158568}, + {1000972340, 234.057322}, + {1000882350, 235.05489}, + {1000892350, 235.05084}, + {1000902350, 235.047255}, + {1000912350, 235.045399}, + {1000922350, 235.043928117}, + {1000932350, 235.044061518}, + {1000942350, 235.045284609}, + {1000952350, 235.047906478}, + {1000962350, 235.051545}, + {1000972350, 235.056651}, + {1000892360, 236.054988}, + {1000902360, 236.049657}, + {1000912360, 236.048668}, + {1000922360, 236.04556613}, + {1000932360, 236.046568296}, + {1000942360, 236.046056661}, + {1000952360, 236.049427}, + {1000962360, 236.051372112}, + {1000972360, 236.057479}, + {1000892370, 237.057993}, + {1000902370, 237.053629}, + {1000912370, 237.051023}, + {1000922370, 237.048728309}, + {1000932370, 237.04817164}, + {1000942370, 237.048407888}, + {1000952370, 237.049995}, + {1000962370, 237.052868988}, + {1000972370, 237.057123}, + {1000982370, 237.062199272}, + {1000902380, 238.056388}, + {1000912380, 238.054637}, + {1000922380, 238.050786936}, + {1000932380, 238.050944603}, + {1000942380, 238.049558175}, + {1000952380, 238.051982531}, + {1000962380, 238.053081606}, + {1000972380, 238.058204}, + {1000982380, 238.06149}, + {1000902390, 239.060655}, + {1000912390, 239.05726}, + {1000922390, 239.054291989}, + {1000932390, 239.052937538}, + {1000942390, 239.052161596}, + {1000952390, 239.053022729}, + {1000962390, 239.054908519}, + {1000972390, 239.058239}, + {1000982390, 239.062482}, + {1000992390, 239.06831}, + {1000912400, 240.061203}, + {1000922400, 240.056592411}, + {1000932400, 240.056163778}, + {1000942400, 240.05381174}, + {1000952400, 240.055298374}, + {1000962400, 240.055528233}, + {1000972400, 240.059758}, + {1000982400, 240.062253447}, + {1000992400, 240.068949}, + {1000912410, 241.064134}, + {1000922410, 241.06033}, + {1000932410, 241.058309671}, + {1000942410, 241.056849651}, + {1000952410, 241.056827343}, + {1000962410, 241.057651218}, + {1000972410, 241.060098}, + {1000982410, 241.06369}, + {1000992410, 241.068592}, + {1001002410, 241.074311}, + {1000922420, 242.062931}, + {1000932420, 242.061639548}, + {1000942420, 242.058740979}, + {1000952420, 242.059547358}, + {1000962420, 242.058834187}, + {1000972420, 242.061999}, + {1000982420, 242.063754544}, + {1000992420, 242.069567}, + {1001002420, 242.07343}, + {1000922430, 243.067075}, + {1000932430, 243.064204}, + {1000942430, 243.062002068}, + {1000952430, 243.061379889}, + {1000962430, 243.061387329}, + {1000972430, 243.063005905}, + {1000982430, 243.065475}, + {1000992430, 243.069508}, + {1001002430, 243.074414}, + {1000932440, 244.067891}, + {1000942440, 244.064204401}, + {1000952440, 244.064282892}, + {1000962440, 244.062750622}, + {1000972440, 244.065178969}, + {1000982440, 244.065999447}, + {1000992440, 244.070881}, + {1001002440, 244.074036}, + {1001012440, 244.081157}, + {1000932450, 245.070693}, + {1000942450, 245.067824554}, + {1000952450, 245.066452827}, + {1000962450, 245.065491047}, + {1000972450, 245.066359814}, + {1000982450, 245.068046755}, + {1000992450, 245.071192}, + {1001002450, 245.075354}, + {1001012450, 245.080864}, + {1000942460, 246.070204172}, + {1000952460, 246.069774}, + {1000962460, 246.067222016}, + {1000972460, 246.0686713}, + {1000982460, 246.068803685}, + {1000992460, 246.072806474}, + {1001002460, 246.075353334}, + {1001012460, 246.081713}, + {1000942470, 247.0743}, + {1000952470, 247.072092}, + {1000962470, 247.070352678}, + {1000972470, 247.070305889}, + {1000982470, 247.070971348}, + {1000992470, 247.073621929}, + {1001002470, 247.076944}, + {1001012470, 247.08152}, + {1000952480, 248.075752}, + {1000962480, 248.072349086}, + {1000972480, 248.073141689}, + {1000982480, 248.072182905}, + {1000992480, 248.075469}, + {1001002480, 248.077185451}, + {1001012480, 248.082607}, + {1001022480, 248.086623}, + {1000952490, 249.07848}, + {1000962490, 249.075953992}, + {1000972490, 249.074983118}, + {1000982490, 249.074850428}, + {1000992490, 249.076409}, + {1001002490, 249.078926042}, + {1001012490, 249.082857155}, + {1001022490, 249.087802}, + {1000962500, 250.078357541}, + {1000972500, 250.078317195}, + {1000982500, 250.076404494}, + {1000992500, 250.078611}, + {1001002500, 250.079519765}, + {1001012500, 250.084164934}, + {1001022500, 250.087565}, + {1000962510, 251.082284988}, + {1000972510, 251.080760555}, + {1000982510, 251.079587171}, + {1000992510, 251.079991431}, + {1001002510, 251.08154513}, + {1001012510, 251.084774287}, + {1001022510, 251.088942}, + {1001032510, 251.094289}, + {1000962520, 252.08487}, + {1000972520, 252.08431}, + {1000982520, 252.081626507}, + {1000992520, 252.082979173}, + {1001002520, 252.082466019}, + {1001012520, 252.086385}, + {1001022520, 252.08896607}, + {1001032520, 252.095048}, + {1000972530, 253.08688}, + {1000982530, 253.085133723}, + {1000992530, 253.084821241}, + {1001002530, 253.085180945}, + {1001012530, 253.087143}, + {1001022530, 253.09056278}, + {1001032530, 253.09503385}, + {1001042530, 253.100528}, + {1000972540, 254.0906}, + {1000982540, 254.087323575}, + {1000992540, 254.088024337}, + {1001002540, 254.086852424}, + {1001012540, 254.08959}, + {1001022540, 254.090954211}, + {1001032540, 254.096238813}, + {1001042540, 254.100055}, + {1000982550, 255.091046}, + {1000992550, 255.090273504}, + {1001002550, 255.089963495}, + {1001012550, 255.091081702}, + {1001022550, 255.093196439}, + {1001032550, 255.096562399}, + {1001042550, 255.101267}, + {1001052550, 255.106919}, + {1000982560, 256.093442}, + {1000992560, 256.093597}, + {1001002560, 256.091771699}, + {1001012560, 256.093888}, + {1001022560, 256.094281912}, + {1001032560, 256.098494024}, + {1001042560, 256.101151464}, + {1001052560, 256.107674}, + {1000992570, 257.095979}, + {1001002570, 257.095105419}, + {1001012570, 257.095537343}, + {1001022570, 257.096884203}, + {1001032570, 257.09948}, + {1001042570, 257.102916796}, + {1001052570, 257.107520042}, + {1000992580, 258.09952}, + {1001002580, 258.097077}, + {1001012580, 258.098433634}, + {1001022580, 258.098205}, + {1001032580, 258.101753}, + {1001042580, 258.103429895}, + {1001052580, 258.108972995}, + {1001062580, 258.11304}, + {1001002590, 259.100596}, + {1001012590, 259.100445}, + {1001022590, 259.100998364}, + {1001032590, 259.1029}, + {1001042590, 259.105601}, + {1001052590, 259.109491859}, + {1001062590, 259.114353}, + {1001002600, 260.102809}, + {1001012600, 260.10365}, + {1001022600, 260.102641}, + {1001032600, 260.105504}, + {1001042600, 260.10644}, + {1001052600, 260.111297}, + {1001062600, 260.114383435}, + {1001072600, 260.121443}, + {1001012610, 261.105828}, + {1001022610, 261.105696}, + {1001032610, 261.106879}, + {1001042610, 261.108769591}, + {1001052610, 261.111979}, + {1001062610, 261.115948135}, + {1001072610, 261.121395733}, + {1001012620, 262.109144}, + {1001022620, 262.107463}, + {1001032620, 262.109615}, + {1001042620, 262.109923}, + {1001052620, 262.114067}, + {1001062620, 262.116338978}, + {1001072620, 262.122654688}, + {1001022630, 263.110714}, + {1001032630, 263.111293}, + {1001042630, 263.112461}, + {1001052630, 263.114987}, + {1001062630, 263.118299}, + {1001072630, 263.122916}, + {1001082630, 263.128479}, + {1001022640, 264.112734}, + {1001032640, 264.114198}, + {1001042640, 264.113876}, + {1001052640, 264.117297}, + {1001062640, 264.11893}, + {1001072640, 264.124486}, + {1001082640, 264.12835633}, + {1001032650, 265.116193}, + {1001042650, 265.116683}, + {1001052650, 265.1185}, + {1001062650, 265.121089}, + {1001072650, 265.124955}, + {1001082650, 265.129791744}, + {1001092650, 265.135937}, + {1001032660, 266.119874}, + {1001042660, 266.118236}, + {1001052660, 266.121032}, + {1001062660, 266.121973}, + {1001072660, 266.12679}, + {1001082660, 266.130048783}, + {1001092660, 266.137062253}, + {1001042670, 267.121787}, + {1001052670, 267.122399}, + {1001062670, 267.124323}, + {1001072670, 267.127499}, + {1001082670, 267.131678}, + {1001092670, 267.137189}, + {1001102670, 267.143726}, + {1001042680, 268.123968}, + {1001052680, 268.125669}, + {1001062680, 268.125389}, + {1001072680, 268.129584}, + {1001082680, 268.132011}, + {1001092680, 268.138649}, + {1001102680, 268.143477}, + {1001052690, 269.127911}, + {1001062690, 269.128495}, + {1001072690, 269.130411}, + {1001082690, 269.133649}, + {1001092690, 269.138809}, + {1001102690, 269.144750965}, + {1001052700, 270.131399}, + {1001062700, 270.130362}, + {1001072700, 270.133366}, + {1001082700, 270.134313}, + {1001092700, 270.140322}, + {1001102700, 270.14458662}, + {1001062710, 271.133782}, + {1001072710, 271.135115}, + {1001082710, 271.137082}, + {1001092710, 271.140741}, + {1001102710, 271.145951}, + {1001062720, 272.135825}, + {1001072720, 272.138259}, + {1001082720, 272.138492}, + {1001092720, 272.143298}, + {1001102720, 272.146091}, + {1001112720, 272.153273}, + {1001062730, 273.139475}, + {1001072730, 273.140294}, + {1001082730, 273.141458}, + {1001092730, 273.144695}, + {1001102730, 273.148455}, + {1001112730, 273.153393}, + {1001072740, 274.143599}, + {1001082740, 274.143217}, + {1001092740, 274.147343}, + {1001102740, 274.149434}, + {1001112740, 274.155247}, + {1001072750, 275.145766}, + {1001082750, 275.14653}, + {1001092750, 275.148972}, + {1001102750, 275.152085}, + {1001112750, 275.156088}, + {1001072760, 276.149169}, + {1001082760, 276.148348}, + {1001092760, 276.151705}, + {1001102760, 276.153022}, + {1001112760, 276.158226}, + {1001122760, 276.161418}, + {1001072770, 277.151477}, + {1001082770, 277.151772}, + {1001092770, 277.153525}, + {1001102770, 277.155763}, + {1001112770, 277.159322}, + {1001122770, 277.163535}, + {1001072780, 278.154988}, + {1001082780, 278.153753}, + {1001092780, 278.156487}, + {1001102780, 278.157007}, + {1001112780, 278.16159}, + {1001122780, 278.164083}, + {1001132780, 278.170725}, + {1001082790, 279.157274}, + {1001092790, 279.158439}, + {1001102790, 279.159984}, + {1001112790, 279.16288}, + {1001122790, 279.166422}, + {1001132790, 279.171187}, + {1001082800, 280.159335}, + {1001092800, 280.161579}, + {1001102800, 280.161375}, + {1001112800, 280.165204}, + {1001122800, 280.167102}, + {1001132800, 280.173098}, + {1001092810, 281.163608}, + {1001102810, 281.164545}, + {1001112810, 281.166757}, + {1001122810, 281.169563}, + {1001132810, 281.17371}, + {1001092820, 282.166888}, + {1001102820, 282.166174}, + {1001112820, 282.169343}, + {1001122820, 282.170507}, + {1001132820, 282.17577}, + {1001102830, 283.169437}, + {1001112830, 283.171101}, + {1001122830, 283.173202}, + {1001132830, 283.176666}, + {1001102840, 284.171187}, + {1001112840, 284.173882}, + {1001122840, 284.17436}, + {1001132840, 284.178843}, + {1001142840, 284.181192}, + {1001112850, 285.175771}, + {1001122850, 285.177227}, + {1001132850, 285.180106}, + {1001142850, 285.183503}, + {1001112860, 286.178756}, + {1001122860, 286.178691}, + {1001132860, 286.182456}, + {1001142860, 286.184226}, + {1001122870, 287.181826}, + {1001132870, 287.184064}, + {1001142870, 287.18672}, + {1001152870, 287.19082}, + {1001122880, 288.183501}, + {1001132880, 288.186764}, + {1001142880, 288.187781}, + {1001152880, 288.192879}, + {1001132890, 289.188461}, + {1001142890, 289.190517}, + {1001152890, 289.193971}, + {1001162890, 289.198023}, + {1001132900, 290.191429}, + {1001142900, 290.191875}, + {1001152900, 290.196235}, + {1001162900, 290.198635}, + {1001142910, 291.194848}, + {1001152910, 291.197725}, + {1001162910, 291.201014}, + {1001172910, 291.205748}, + {1001152920, 292.200323}, + {1001162920, 292.201969}, + {1001172920, 292.207861}, + {1001162930, 293.204583}, + {1001172930, 293.208727}, + {1001182930, 293.213423}, + {1001172940, 294.21084}, + {1001182940, 294.213979}, + {1001182950, 295.216178}, +}; + +} // namespace openmc diff --git a/src/particle.cpp b/src/particle.cpp index a035e76b9..8d0e5b3f0 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -48,20 +48,16 @@ double Particle::speed() const if (settings::run_CE) { // Determine mass in eV/c^2 double mass; - switch (this->type().pdg_number()) { + switch (type().pdg_number()) { case PDG_NEUTRON: mass = MASS_NEUTRON_EV; - break; - case PDG_PHOTON: - mass = 0.0; - break; case PDG_ELECTRON: case PDG_POSITRON: mass = MASS_ELECTRON_EV; - break; default: - fatal_error("Unsupported particle for speed calculation."); + mass = this->type().mass() * AMU_EV; } + // Equivalent to C * sqrt(1-(m/(m+E))^2) without problem at E<E() * (this->E() + 2 * mass)) / (this->E() + mass); From a35927aad3098f6aa40546edfc38e19f9293c0d4 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 13 Feb 2026 22:18:29 -0600 Subject: [PATCH 546/671] Extend ParticleProductionFilter to support multiple particle types (#3780) --- CMakeLists.txt | 1 + docs/source/io_formats/tallies.rst | 32 ++- include/openmc/tallies/filter_energy.h | 31 --- .../tallies/filter_particle_production.h | 53 +++++ openmc/filter.py | 224 ++++++++++++++---- src/tallies/filter.cpp | 1 + src/tallies/filter_energy.cpp | 44 ---- src/tallies/filter_particle_production.cpp | 108 +++++++++ src/tallies/tally_scoring.cpp | 21 +- .../mg_tallies/results_true.dat | 80 +++---- .../photon_production_inputs_true.dat | 4 +- .../photon_production/inputs_true.dat | 4 +- .../photon_production/results_true.dat | 10 + .../photon_production/test.py | 2 +- tests/unit_tests/test_filters.py | 82 ++++++- 15 files changed, 512 insertions(+), 185 deletions(-) create mode 100644 include/openmc/tallies/filter_particle_production.h create mode 100644 src/tallies/filter_particle_production.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b204b45fc..e29c0d81c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -427,6 +427,7 @@ list(APPEND libopenmc_SOURCES src/tallies/filter_musurface.cpp src/tallies/filter_parent_nuclide.cpp src/tallies/filter_particle.cpp + src/tallies/filter_particle_production.cpp src/tallies/filter_polar.cpp src/tallies/filter_sph_harm.cpp src/tallies/filter_sptl_legendre.cpp diff --git a/docs/source/io_formats/tallies.rst b/docs/source/io_formats/tallies.rst index 0ba0e061f..dc57e5775 100644 --- a/docs/source/io_formats/tallies.rst +++ b/docs/source/io_formats/tallies.rst @@ -142,9 +142,9 @@ attributes/sub-elements: :type: The type of the filter. Accepted options are "cell", "cellfrom", - "cellborn", "surface", "material", "universe", "energy", "energyout", "mu", - "polar", "azimuthal", "mesh", "distribcell", "delayedgroup", - "energyfunction", and "particle". + "cellborn", "surface", "material", "universe", "energy", "energyout", + "mu", "polar", "azimuthal", "mesh", "distribcell", "delayedgroup", + "energyfunction", "particle", and "particleproduction". :bins: A description of the bins for each type of filter can be found in @@ -321,6 +321,32 @@ should be set to: A list of particle identifiers to tally, specified as strings (e.g., ``neutron``, ``photon``, ``He4``) or as integer PDG numbers. +:particleproduction: + This filter tallies secondary particles produced in reactions, binned by + particle type and, optionally, by energy. Unlike other energy filters, the + weight applied is the weight of the secondary particle. To obtain secondary + particle production rates, use this filter with the ``events`` score. + + The filter uses the following sub-elements instead of ``bins``: + + :particles: + A space-separated list of secondary particle types to tally (e.g., + ``photon``, ``neutron``, ``electron``). + + :energies: + An optional monotonically increasing list of energy boundaries in [eV] + for binning the secondary particle energies. If omitted, total production + is tallied without energy binning. + + For example, to tally photon and neutron production in three energy groups: + + .. code-block:: xml + + + photon neutron + 0.0 1.0e5 1.0e6 20.0e6 + + ------------------ ```` Element ------------------ diff --git a/include/openmc/tallies/filter_energy.h b/include/openmc/tallies/filter_energy.h index 6bc9ff6b1..3e410d9fd 100644 --- a/include/openmc/tallies/filter_energy.h +++ b/include/openmc/tallies/filter_energy.h @@ -73,36 +73,5 @@ public: std::string text_label(int bin) const override; }; -//============================================================================== -//! Bins the outgoing energy of secondary particles -//! -//! This filter can be used to get the photon production matrix for multigroup -//! photon transport, the energy distribution of secondary neutrons, etc. Unlike -//! other energy filters, the weight that is applied is equal to the weight of -//! the secondary particle. Thus, to get secondary production it should be used -//! in conjunction with the "events" score. -//============================================================================== - -class ParticleProductionFilter : public EnergyFilter { -public: - //---------------------------------------------------------------------------- - // Methods - - std::string type_str() const override { return "particleproduction"; } - FilterType type() const override { return FilterType::PARTICLE_PRODUCTION; } - - void get_all_bins(const Particle& p, TallyEstimator estimator, - FilterMatch& match) const override; - - std::string text_label(int bin) const override; - - void from_xml(pugi::xml_node node) override; - - void to_statepoint(hid_t filter_group) const override; - -protected: - ParticleType secondary_type_; //!< Type of secondary particle to filter -}; - } // namespace openmc #endif // OPENMC_TALLIES_FILTER_ENERGY_H diff --git a/include/openmc/tallies/filter_particle_production.h b/include/openmc/tallies/filter_particle_production.h new file mode 100644 index 000000000..00fac9f7c --- /dev/null +++ b/include/openmc/tallies/filter_particle_production.h @@ -0,0 +1,53 @@ +#ifndef OPENMC_TALLIES_FILTER_PARTICLE_PRODUCTION_H +#define OPENMC_TALLIES_FILTER_PARTICLE_PRODUCTION_H + +#include + +#include "openmc/particle.h" +#include "openmc/tallies/filter.h" +#include "openmc/vector.h" + +namespace openmc { + +//============================================================================== +//! Bins the outgoing energy of secondary particles +//! +//! This filter bins secondary particles by type and, optionally, by energy. It +//! can be used to get the photon production matrix for multigroup photon +//! transport, the energy distribution of secondary neutrons, etc. The weight +//! that is applied is equal to the weight of the secondary particle. Thus, to +//! get secondary production it should be used in conjunction with the "events" +//! score. +//============================================================================== + +class ParticleProductionFilter : public Filter { +public: + //---------------------------------------------------------------------------- + // Methods + + std::string type_str() const override { return "particleproduction"; } + FilterType type() const override { return FilterType::PARTICLE_PRODUCTION; } + + void get_all_bins(const Particle& p, TallyEstimator estimator, + FilterMatch& match) const override; + + std::string text_label(int bin) const override; + + void from_xml(pugi::xml_node node) override; + + void to_statepoint(hid_t filter_group) const override; + +private: + //---------------------------------------------------------------------------- + // Data members + + vector secondary_types_; //!< Types of secondary particles + + //! Map from PDG number to index in secondary_types_ for O(1) lookup + std::unordered_map type_to_index_; + + vector energy_bins_; //!< Energy bin boundaries (optional) +}; + +} // namespace openmc +#endif // OPENMC_TALLIES_FILTER_PARTICLE_PRODUCTION_H diff --git a/openmc/filter.py b/openmc/filter.py index 7561f6be6..9a2836e57 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -24,9 +24,9 @@ _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', 'cellinstance', - 'collision', 'time', 'parentnuclide', 'weight', 'meshborn', 'meshsurface', - 'meshmaterial', + 'sphericalharmonics', 'zernike', 'zernikeradial', 'particle', + 'particleproduction', 'cellinstance', 'collision', 'time', 'parentnuclide', + 'weight', 'meshborn', 'meshsurface', 'meshmaterial', ) _CURRENT_NAMES = ( @@ -1748,15 +1748,16 @@ class EnergyoutFilter(EnergyFilter): """ -class ParticleProductionFilter(EnergyFilter): - """Bins tally events based on energy of secondary particles. +class ParticleProductionFilter(Filter): + """Bins tally events based on secondary particle type and energy. - This filter bins the energies of secondary particles (e.g., photons, - electrons, or recoils) produced in a reaction. This is useful for - constructing production matrices or analyzing secondary particle spectra. - Note that unlike other energy filters, the weight that is applied is equal - to the weight of the secondary particle. Thus, to obtain secondary particle - production, it should be used in conjunction with the "events" score. + This filter bins secondary particles (e.g., photons, electrons, or recoils) + produced in a reaction by particle type and, optionally, by energy. This is + useful for constructing production matrices or analyzing secondary particle + spectra. Note that unlike other energy filters, the weight that is applied + is equal to the weight of the secondary particle. Thus, to obtain secondary + particle production, it should be used in conjunction with the "events" + score. The incident particle type can be filtered using :class:`ParticleFilter`. @@ -1764,63 +1765,206 @@ class ParticleProductionFilter(EnergyFilter): Parameters ---------- - particle : str, int, openmc.ParticleType - Type of secondary particle to tally ('photon', 'neutron', etc.) - values : Iterable of Real + particles : str, int, openmc.ParticleType, or iterable thereof + Type(s) of secondary particle(s) to tally ('photon', 'neutron', etc.) + energies : Iterable of Real or str, optional A list of energy boundaries in [eV]; each successive pair defines a bin. + Alternatively, the name of the group structure can be given as a string + (must be a key in :data:`openmc.mgxs.GROUP_STRUCTURES`). If not + provided, the filter tallies total secondary particle production without + energy binning. filter_id : int, optional Unique identifier for the filter Attributes ---------- - values : numpy.ndarray - Energy boundaries in [eV] - bins : numpy.ndarray - Array of (low, high) energy bin pairs + particles : list of openmc.ParticleType + The secondary particle types this filter applies to + energies : numpy.ndarray or None + Energy boundaries in [eV], or None if no energy binning + bins : list + A list of bins; each element fully describes one bin. When energies are + specified, each element is a tuple ``(particle, energy_low, + energy_high)``. When no energies are specified, each element is a + particle name string. num_bins : int - Number of filter bins - particle : str - The secondary particle type this filter applies to + Total number of filter bins + num_energy_bins : int + Number of energy bins (1 if no energies specified) + shape : tuple of int + Shape of the filter as (n_particles, n_energy_bins) """ - def __init__(self, particle, values, filter_id=None): - super().__init__(values, filter_id) - self.particle = particle + def __init__(self, particles, energies=None, filter_id=None): + self.particles = particles + self.energies = energies + self.id = filter_id def __repr__(self): string = type(self).__name__ + '\n' - string += '{: <16}=\t{}\n'.format('\tParticle', self.particle) - string += '{: <16}=\t{}\n'.format('\tValues', self.values) + string += '{: <16}=\t{}\n'.format('\tParticles', + [str(p) for p in self.particles]) + if self.energies is not None: + string += '{: <16}=\t{}\n'.format('\tEnergies', self.energies) string += '{: <16}=\t{}\n'.format('\tID', self.id) return string @property - def particle(self) -> openmc.ParticleType: - return self._particle + def particles(self): + return self._particles - @particle.setter - def particle(self, particle): - self._particle = openmc.ParticleType(particle) + @particles.setter + def particles(self, particles): + if isinstance(particles, (str, int, openmc.ParticleType)): + self._particles = [openmc.ParticleType(particles)] + else: + self._particles = [openmc.ParticleType(p) for p in particles] + + @property + def energies(self): + return self._energies + + @energies.setter + def energies(self, energies): + if energies is None: + self._energies = None + elif isinstance(energies, str): + cv.check_value('energies', energies, + openmc.mgxs.GROUP_STRUCTURES.keys()) + self._energies = np.array( + openmc.mgxs.GROUP_STRUCTURES[energies.upper()]) + else: + energies = np.asarray(energies, dtype=float) + cv.check_length('energies', energies, 2) + for i in range(len(energies) - 1): + if energies[i + 1] <= energies[i]: + raise ValueError("Energy bins must be monotonically " + "increasing.") + self._energies = energies + + @property + def bins(self): + if self.energies is None: + return [str(p) for p in self.particles] + else: + result = [] + energy_pairs = np.vstack( + (self.energies[:-1], self.energies[1:])).T + for particle in self.particles: + for e_low, e_high in energy_pairs: + result.append((str(particle), e_low, e_high)) + return result + + @bins.setter + def bins(self, bins): + # bins is set indirectly through particles/energies + pass + + def check_bins(self, bins): + pass + + @property + def num_energy_bins(self): + if self.energies is None: + return 1 + else: + return len(self.energies) - 1 + + @property + def num_bins(self): + return len(self.particles) * self.num_energy_bins + + @property + def shape(self): + return (len(self.particles), self.num_energy_bins) def to_xml_element(self): - element = super().to_xml_element() - subelement = ET.SubElement(element, 'particle') - subelement.text = str(self.particle) + element = ET.Element('filter') + element.set('id', str(self.id)) + element.set('type', self.short_name.lower()) + + subelement = ET.SubElement(element, 'particles') + subelement.text = ' '.join(str(p) for p in self.particles) + + if self.energies is not None: + subelement = ET.SubElement(element, 'energies') + subelement.text = ' '.join(str(e) for e in self.energies) + return element @classmethod def from_xml_element(cls, elem, **kwargs): filter_id = int(elem.get('id')) - values = [float(x) for x in get_text(elem, 'bins').split()] - particle = get_text(elem, 'particle') - return cls(particle, values, filter_id=filter_id) + particles = get_text(elem, 'particles').split() + + bins_elem = elem.find('energies') + if bins_elem is not None: + energies = [float(x) for x in bins_elem.text.split()] + else: + energies = None + + return cls(particles, energies=energies, filter_id=filter_id) @classmethod def from_hdf5(cls, group, **kwargs): filter_id = int(group.name.split('/')[-1].lstrip('filter ')) - bins = group['bins'][()] - particle = group['particle'][()].decode() - return cls(particle, bins, filter_id=filter_id) + + # Read particle types + particles = [b.decode() if isinstance(b, bytes) else b + for b in group['particles'][()]] + + # Read energy bins if present + if 'energies' in group: + energies = group['energies'][()] + else: + energies = None + + return cls(particles, energies=energies, filter_id=filter_id) + + def get_pandas_dataframe(self, data_size, stride, **kwargs): + """Builds a Pandas DataFrame for the Filter's bins. + + This method constructs a Pandas DataFrame object for the filter with + columns annotated by filter bin information. This is a helper method for + :meth:`Tally.get_pandas_dataframe`. + + Parameters + ---------- + data_size : int + The total number of bins in the tally corresponding to this filter + stride : int + Stride in memory for the filter + + Returns + ------- + pandas.DataFrame + A Pandas DataFrame with columns for particle type and, if energy + bins are specified, energy bin boundaries. + + See also + -------- + Tally.get_pandas_dataframe(), CrossFilter.get_pandas_dataframe() + + """ + filter_dict = {} + key = self.short_name.lower() + n_ebins = self.num_energy_bins + + # Particle column — outer dimension (changes slowest) + particle_names = [str(p) for p in self.particles] + filter_dict[key, 'particle'] = _repeat_and_tile( + np.array(particle_names), n_ebins * stride, data_size) + + # Energy columns only if energies were specified + if self.energies is not None: + energy_pairs = np.vstack( + (self.energies[:-1], self.energies[1:])).T + filter_dict[key, 'energy low [eV]'] = _repeat_and_tile( + energy_pairs[:, 0], stride, data_size) + filter_dict[key, 'energy high [eV]'] = _repeat_and_tile( + energy_pairs[:, 1], stride, data_size) + + return pd.DataFrame(filter_dict) class TimeFilter(RealFilter): diff --git a/src/tallies/filter.cpp b/src/tallies/filter.cpp index 16e4c0550..b0e9624c9 100644 --- a/src/tallies/filter.cpp +++ b/src/tallies/filter.cpp @@ -31,6 +31,7 @@ #include "openmc/tallies/filter_musurface.h" #include "openmc/tallies/filter_parent_nuclide.h" #include "openmc/tallies/filter_particle.h" +#include "openmc/tallies/filter_particle_production.h" #include "openmc/tallies/filter_polar.h" #include "openmc/tallies/filter_sph_harm.h" #include "openmc/tallies/filter_sptl_legendre.h" diff --git a/src/tallies/filter_energy.cpp b/src/tallies/filter_energy.cpp index 0b039440a..0b954cce3 100644 --- a/src/tallies/filter_energy.cpp +++ b/src/tallies/filter_energy.cpp @@ -117,50 +117,6 @@ std::string EnergyoutFilter::text_label(int bin) const "Outgoing Energy [{}, {})", bins_.at(bin), bins_.at(bin + 1)); } -//============================================================================== -// ParticleProductionFilter implementation -//============================================================================== - -void ParticleProductionFilter::get_all_bins( - const Particle& p, TallyEstimator estimator, FilterMatch& match) const -{ - int start_idx = p.secondary_bank_index(); - int end_idx = start_idx + p.n_secondaries(); - - // Loop over secondary bank entries - for (int bank_idx = start_idx; bank_idx < end_idx; bank_idx++) { - // Check if this is the correct type of secondary, then match its energy if - // it's the right type - const auto& site = p.secondary_bank(bank_idx); - if (site.particle == secondary_type_) { - if (site.E >= bins_.front() && site.E <= bins_.back()) { - auto bin = lower_bound_index(bins_.begin(), bins_.end(), site.E); - match.bins_.push_back(bin); - match.weights_.push_back(site.wgt); - } - } - } -} - -std::string ParticleProductionFilter::text_label(int bin) const -{ - return fmt::format("Secondary {}, Energy [{}, {})", secondary_type_.str(), - bins_.at(bin), bins_.at(bin + 1)); -} - -void ParticleProductionFilter::from_xml(pugi::xml_node node) -{ - EnergyFilter::from_xml(node); - std::string p = get_node_value(node, "particle"); - secondary_type_ = ParticleType {p}; -} - -void ParticleProductionFilter::to_statepoint(hid_t filter_group) const -{ - EnergyFilter::to_statepoint(filter_group); - write_dataset(filter_group, "particle", secondary_type_.str()); -} - //============================================================================== // C-API functions //============================================================================== diff --git a/src/tallies/filter_particle_production.cpp b/src/tallies/filter_particle_production.cpp new file mode 100644 index 000000000..f8d82fdd9 --- /dev/null +++ b/src/tallies/filter_particle_production.cpp @@ -0,0 +1,108 @@ +#include "openmc/tallies/filter_particle_production.h" + +#include + +#include "openmc/search.h" +#include "openmc/xml_interface.h" + +namespace openmc { + +//============================================================================== +// ParticleProductionFilter implementation +//============================================================================== + +void ParticleProductionFilter::get_all_bins( + const Particle& p, TallyEstimator estimator, FilterMatch& match) const +{ + int start_idx = p.secondary_bank_index(); + int end_idx = start_idx + p.n_secondaries(); + + // Loop over secondary bank entries + for (int bank_idx = start_idx; bank_idx < end_idx; bank_idx++) { + const auto& site = p.secondary_bank(bank_idx); + + // Find which particle-type slot this secondary belongs to + auto it = type_to_index_.find(site.particle.pdg_number()); + if (it == type_to_index_.end()) + continue; + + int particle_idx = it->second; + if (energy_bins_.empty()) { + // No energy binning, just particle type + match.bins_.push_back(particle_idx); + match.weights_.push_back(site.wgt); + } else { + // Bin the energy + if (site.E >= energy_bins_.front() && site.E <= energy_bins_.back()) { + int n_energies = static_cast(energy_bins_.size()) - 1; + auto energy_idx = + lower_bound_index(energy_bins_.begin(), energy_bins_.end(), site.E); + match.bins_.push_back(particle_idx * n_energies + energy_idx); + match.weights_.push_back(site.wgt); + } + } + } +} + +std::string ParticleProductionFilter::text_label(int bin) const +{ + if (energy_bins_.empty()) { + return fmt::format("Secondary {}", secondary_types_.at(bin).str()); + } else { + int n_energies = static_cast(energy_bins_.size()) - 1; + int particle_idx = bin / n_energies; + int energy_idx = bin % n_energies; + return fmt::format("Secondary {}, Energy [{}, {})", + secondary_types_.at(particle_idx).str(), energy_bins_.at(energy_idx), + energy_bins_.at(energy_idx + 1)); + } +} + +void ParticleProductionFilter::from_xml(pugi::xml_node node) +{ + // Read energy bins if present (optional) + if (check_for_node(node, "energies")) { + auto bins = get_node_array(node, "energies"); + for (int64_t i = 1; i < bins.size(); ++i) { + if (bins[i] <= bins[i - 1]) { + throw std::runtime_error { + "Energy bins must be monotonically increasing."}; + } + } + energy_bins_.assign(bins.begin(), bins.end()); + } + + // Read particle types (required) + auto names = get_node_array(node, "particles"); + for (const auto& name : names) { + int idx = secondary_types_.size(); + secondary_types_.emplace_back(name); + type_to_index_[secondary_types_.back().pdg_number()] = idx; + } + + // Compute total bins + if (energy_bins_.empty()) { + n_bins_ = secondary_types_.size(); + } else { + n_bins_ = secondary_types_.size() * (energy_bins_.size() - 1); + } +} + +void ParticleProductionFilter::to_statepoint(hid_t filter_group) const +{ + Filter::to_statepoint(filter_group); + + // Write energy bins if present + if (!energy_bins_.empty()) { + write_dataset(filter_group, "energies", energy_bins_); + } + + // Write particle types + vector names; + for (const auto& pt : secondary_types_) { + names.push_back(pt.str()); + } + write_dataset(filter_group, "particles", names); +} + +} // namespace openmc diff --git a/src/tallies/tally_scoring.cpp b/src/tallies/tally_scoring.cpp index 51b5d9ffc..ccc70fc8f 100644 --- a/src/tallies/tally_scoring.cpp +++ b/src/tallies/tally_scoring.cpp @@ -903,10 +903,9 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, break; case SCORE_EVENTS: -// Simply count the number of scoring events -#pragma omp atomic - tally.results_(filter_index, score_index, TallyResult::VALUE) += 1.0; - continue; + // Simply count the number of scoring events + score = 1.0; + break; case ELASTIC: if (!p.type().is_neutron()) @@ -1513,10 +1512,9 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index, break; case SCORE_EVENTS: -// Simply count the number of scoring events -#pragma omp atomic - tally.results_(filter_index, score_index, TallyResult::VALUE) += 1.0; - continue; + // Simply count the number of scoring events + score = 1.0; + break; case ELASTIC: if (!p.type().is_neutron()) @@ -2291,10 +2289,9 @@ void score_general_mg(Particle& p, int i_tally, int start_index, break; case SCORE_EVENTS: -// Simply count the number of scoring events -#pragma omp atomic - tally.results_(filter_index, score_index, TallyResult::VALUE) += 1.0; - continue; + // Simply count the number of scoring events + score = 1.0; + break; default: continue; diff --git a/tests/regression_tests/mg_tallies/results_true.dat b/tests/regression_tests/mg_tallies/results_true.dat index 07fe22ce5..c1591cca2 100644 --- a/tests/regression_tests/mg_tallies/results_true.dat +++ b/tests/regression_tests/mg_tallies/results_true.dat @@ -238,8 +238,8 @@ tally 2: 3.994127E-08 4.815195E+06 6.359497E+12 -1.373000E+00 -4.448330E-01 +1.340228E+00 +4.256704E-01 1.788012E-04 8.768717E-09 3.941968E+00 @@ -260,8 +260,8 @@ tally 2: 1.493588E-07 1.064740E+07 2.378109E+13 -2.753000E+00 -1.542179E+00 +2.698926E+00 +1.484990E+00 3.953669E-04 3.279028E-08 8.155605E+00 @@ -282,8 +282,8 @@ tally 2: 1.319523E-07 1.005866E+07 2.100960E+13 -2.829000E+00 -1.702143E+00 +2.795871E+00 +1.662994E+00 3.735055E-04 2.896884E-08 8.365907E+00 @@ -304,8 +304,8 @@ tally 2: 2.314228E-07 1.328300E+07 3.684741E+13 -3.246000E+00 -2.132054E+00 +3.162533E+00 +2.024101E+00 4.932336E-04 5.080661E-08 9.080077E+00 @@ -326,8 +326,8 @@ tally 2: 1.984988E-07 1.206905E+07 3.160522E+13 -3.759000E+00 -2.944479E+00 +3.670108E+00 +2.815133E+00 4.481564E-04 4.357848E-08 1.076370E+01 @@ -348,8 +348,8 @@ tally 2: 1.474939E-07 1.003129E+07 2.348415E+13 -2.577000E+00 -1.509201E+00 +2.523640E+00 +1.451617E+00 3.724888E-04 3.238084E-08 7.654439E+00 @@ -370,8 +370,8 @@ tally 2: 1.993067E-07 1.242911E+07 3.173385E+13 -3.266000E+00 -2.172442E+00 +3.198836E+00 +2.085466E+00 4.615266E-04 4.375584E-08 9.265396E+00 @@ -392,8 +392,8 @@ tally 2: 2.277785E-07 1.329858E+07 3.626717E+13 -3.485000E+00 -2.510947E+00 +3.467276E+00 +2.484377E+00 4.938123E-04 5.000655E-08 9.871966E+00 @@ -414,8 +414,8 @@ tally 2: 1.293793E-09 7.073280E+05 2.059993E+11 -3.000000E-01 -4.514200E-02 +2.978788E-01 +4.498561E-02 2.626500E-05 2.840396E-10 9.197485E-01 @@ -436,8 +436,8 @@ tally 2: 2.205650E-10 2.942995E+05 3.511863E+10 -1.300000E-01 -5.822000E-03 +1.270281E-01 +5.517180E-03 1.092814E-05 4.842290E-11 3.746117E-01 @@ -908,8 +908,8 @@ tally 12: 3.994127E-08 4.815195E+06 6.359497E+12 -1.373000E+00 -4.448330E-01 +1.340228E+00 +4.256704E-01 1.788012E-04 8.768717E-09 2.806910E+00 @@ -928,8 +928,8 @@ tally 12: 1.493588E-07 1.064740E+07 2.378109E+13 -2.753000E+00 -1.542179E+00 +2.698926E+00 +1.484990E+00 3.953669E-04 3.279028E-08 2.869647E+00 @@ -948,8 +948,8 @@ tally 12: 1.319523E-07 1.005866E+07 2.100960E+13 -2.829000E+00 -1.702143E+00 +2.795871E+00 +1.662994E+00 3.735055E-04 2.896884E-08 3.141043E+00 @@ -968,8 +968,8 @@ tally 12: 2.314228E-07 1.328300E+07 3.684741E+13 -3.246000E+00 -2.132054E+00 +3.162533E+00 +2.024101E+00 4.932336E-04 5.080661E-08 3.682383E+00 @@ -988,8 +988,8 @@ tally 12: 1.984988E-07 1.206905E+07 3.160522E+13 -3.759000E+00 -2.944479E+00 +3.670108E+00 +2.815133E+00 4.481564E-04 4.357848E-08 2.634850E+00 @@ -1008,8 +1008,8 @@ tally 12: 1.474939E-07 1.003129E+07 2.348415E+13 -2.577000E+00 -1.509201E+00 +2.523640E+00 +1.451617E+00 3.724888E-04 3.238084E-08 3.192584E+00 @@ -1028,8 +1028,8 @@ tally 12: 1.993067E-07 1.242911E+07 3.173385E+13 -3.266000E+00 -2.172442E+00 +3.198836E+00 +2.085466E+00 4.615266E-04 4.375584E-08 3.402213E+00 @@ -1048,8 +1048,8 @@ tally 12: 2.277785E-07 1.329858E+07 3.626717E+13 -3.485000E+00 -2.510947E+00 +3.467276E+00 +2.484377E+00 4.938123E-04 5.000655E-08 3.110378E-01 @@ -1068,8 +1068,8 @@ tally 12: 1.293793E-09 7.073280E+05 2.059993E+11 -3.000000E-01 -4.514200E-02 +2.978788E-01 +4.498561E-02 2.626500E-05 2.840396E-10 1.267544E-01 @@ -1088,8 +1088,8 @@ tally 12: 2.205650E-10 2.942995E+05 3.511863E+10 -1.300000E-01 -5.822000E-03 +1.270281E-01 +5.517180E-03 1.092814E-05 4.842290E-11 tally 13: diff --git a/tests/regression_tests/model_xml/photon_production_inputs_true.dat b/tests/regression_tests/model_xml/photon_production_inputs_true.dat index cc618eaff..0ae0079f4 100644 --- a/tests/regression_tests/model_xml/photon_production_inputs_true.dat +++ b/tests/regression_tests/model_xml/photon_production_inputs_true.dat @@ -48,8 +48,8 @@ 0.0 20000000.0 - 0.0 100000.0 300000.0 500000.0 2000000.0 20000000.0 - photon + neutron photon + 0.0 100000.0 300000.0 500000.0 2000000.0 20000000.0 1 2 diff --git a/tests/regression_tests/photon_production/inputs_true.dat b/tests/regression_tests/photon_production/inputs_true.dat index cc618eaff..0ae0079f4 100644 --- a/tests/regression_tests/photon_production/inputs_true.dat +++ b/tests/regression_tests/photon_production/inputs_true.dat @@ -48,8 +48,8 @@ 0.0 20000000.0 - 0.0 100000.0 300000.0 500000.0 2000000.0 20000000.0 - photon + neutron photon + 0.0 100000.0 300000.0 500000.0 2000000.0 20000000.0 1 2 diff --git a/tests/regression_tests/photon_production/results_true.dat b/tests/regression_tests/photon_production/results_true.dat index ec319aa0e..c4f5fafa2 100644 --- a/tests/regression_tests/photon_production/results_true.dat +++ b/tests/regression_tests/photon_production/results_true.dat @@ -139,6 +139,16 @@ tally 4: 0.000000E+00 0.000000E+00 tally 5: +5.000000E-04 +2.500000E-07 +1.300000E-03 +1.690000E-06 +8.000000E-04 +6.400000E-07 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 2.000000E-02 4.000000E-04 8.200000E-03 diff --git a/tests/regression_tests/photon_production/test.py b/tests/regression_tests/photon_production/test.py index 65d0688e9..5ed5c5ecf 100644 --- a/tests/regression_tests/photon_production/test.py +++ b/tests/regression_tests/photon_production/test.py @@ -72,7 +72,7 @@ def model(): # Track source energies of secondary gammas ene2_filter = openmc.ParticleProductionFilter( - 'photon', [0.0, 100e3, 300e3, 500e3, 2e6, 20e6]) + ['neutron', 'photon'], [0.0, 100e3, 300e3, 500e3, 2e6, 20e6]) neutron_only = openmc.ParticleFilter(['neutron']) tally_gam_ene = openmc.Tally() diff --git a/tests/unit_tests/test_filters.py b/tests/unit_tests/test_filters.py index ba3226e3a..2e7481c87 100644 --- a/tests/unit_tests/test_filters.py +++ b/tests/unit_tests/test_filters.py @@ -321,12 +321,22 @@ def test_energy_filter(): def test_particle_production_filter(): energy_bins = [1e3, 1e4, 1e5, 1e6] + + # --- Single particle with energy bins --- f = openmc.ParticleProductionFilter('photon', energy_bins) - assert f.particle == openmc.ParticleType.PHOTON + # particles getter always returns a list + assert isinstance(f.particles, list) + assert len(f.particles) == 1 + assert f.particles[0] == openmc.ParticleType.PHOTON + + assert f.num_energy_bins == 3 assert f.num_bins == 3 - assert f.bins.shape == (3, 2) - assert np.allclose(f.values, energy_bins) + assert f.shape == (1, 3) + assert len(f.bins) == 3 + # Each bin is (particle_name, e_low, e_high) + assert f.bins[0] == ('photon', 1e3, 1e4) + assert f.bins[2] == ('photon', 1e5, 1e6) # __repr__ check repr(f) @@ -335,20 +345,72 @@ def test_particle_production_filter(): elem = f.to_xml_element() assert elem.tag == 'filter' assert elem.attrib['type'] == 'particleproduction' - assert elem.find('particle').text == 'photon' - assert elem.find('bins').text.split()[0] == str(energy_bins[0]) + assert elem.find('particles').text == 'photon' + assert elem.find('energies').text.split()[0] == str(energy_bins[0]) # from_xml_element() new_f = openmc.Filter.from_xml_element(elem) assert new_f.id == f.id - assert new_f.particle == f.particle - assert np.allclose(new_f.bins, f.bins) + assert new_f.particles == f.particles + assert np.allclose(new_f.energies, f.energies) - # pandas output + # pandas output (with energy bins -> 3 MultiIndex columns) df = f.get_pandas_dataframe(data_size=3, stride=1) assert df.shape[0] == 3 - assert "particleproduction low [eV]" in df.columns - assert "particleproduction high [eV]" in df.columns + assert ('particleproduction', 'particle') in df.columns + assert ('particleproduction', 'energy low [eV]') in df.columns + assert ('particleproduction', 'energy high [eV]') in df.columns + + # --- Multiple particles with energy bins --- + f2 = openmc.ParticleProductionFilter(['photon', 'neutron'], energy_bins) + assert len(f2.particles) == 2 + assert f2.num_bins == 6 # 2 particles * 3 energy bins + assert f2.shape == (2, 3) + assert len(f2.bins) == 6 + # First 3 bins are photon, next 3 are neutron + assert f2.bins[0] == ('photon', 1e3, 1e4) + assert f2.bins[3] == ('neutron', 1e3, 1e4) + + df2 = f2.get_pandas_dataframe(data_size=6, stride=1) + assert df2.shape[0] == 6 + assert list(df2[('particleproduction', 'particle')]) == \ + ['photon'] * 3 + ['neutron'] * 3 + + # XML round-trip + elem2 = f2.to_xml_element() + new_f2 = openmc.Filter.from_xml_element(elem2) + assert len(new_f2.particles) == 2 + assert np.allclose(new_f2.energies, energy_bins) + + # --- Multiple particles without energy bins --- + f3 = openmc.ParticleProductionFilter(['photon', 'neutron', 'electron']) + assert f3.energies is None + assert f3.num_bins == 3 + assert f3.num_energy_bins == 1 + assert f3.shape == (3, 1) + assert f3.bins == ['photon', 'neutron', 'electron'] + + repr(f3) + + df3 = f3.get_pandas_dataframe(data_size=3, stride=1) + assert df3.shape[0] == 3 + assert ('particleproduction', 'particle') in df3.columns + # Should not have energy columns + assert ('particleproduction', 'energy low [eV]') not in df3.columns + + # XML round-trip without energies + elem3 = f3.to_xml_element() + assert elem3.find('energies') is None + new_f3 = openmc.Filter.from_xml_element(elem3) + assert new_f3.energies is None + assert len(new_f3.particles) == 3 + + # --- Energies from group structure name --- + f4 = openmc.ParticleProductionFilter('photon', energies='CCFE-709') + expected = openmc.mgxs.GROUP_STRUCTURES['CCFE-709'] + assert np.allclose(f4.energies, expected) + assert f4.num_energy_bins == len(expected) - 1 + assert f4.num_bins == len(expected) - 1 def test_weight(): From c6ef84d1d5ad47cc528573332a539ea1f25a8e44 Mon Sep 17 00:00:00 2001 From: Kevin Sawatzky <66632997+nuclearkevin@users.noreply.github.com> Date: Sun, 15 Feb 2026 15:59:08 -0600 Subject: [PATCH 547/671] Set upper and lower interpolation bounds for MGXS data. (#3803) --- src/mgxs.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/mgxs.cpp b/src/mgxs.cpp index a2c479f21..ea78238b8 100644 --- a/src/mgxs.cpp +++ b/src/mgxs.cpp @@ -14,6 +14,7 @@ #include "openmc/error.h" #include "openmc/math_functions.h" #include "openmc/mgxs_interface.h" +#include "openmc/nuclide.h" #include "openmc/random_lcg.h" #include "openmc/settings.h" #include "openmc/string_utils.h" @@ -85,6 +86,13 @@ void Mgxs::metadata_from_hdf5(hid_t xs_id, const vector& temperature, delete[] dset_names; std::sort(temps_available.begin(), temps_available.end()); + // Set the global upper and lower interpolation bounds to avoid errors + // involving C-API functions. + data::temperature_min = + std::min(data::temperature_min, temps_available.front()); + data::temperature_max = + std::max(data::temperature_max, temps_available.back()); + // If only one temperature is available, lets just use nearest temperature // interpolation if ((num_temps == 1) && From 977ade79a1c66db0ff8542b3d231cccf6d7d1200 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Tue, 17 Feb 2026 09:50:38 -0600 Subject: [PATCH 548/671] 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 f84d09bb1..2bc123894 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 e29c0d81c..5d6dd2a0e 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 837a39c78..cc837bba7 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 0f887940e..6cb774b5b 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 2f7e41bf0..d3b586831 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 9b08ed039..efacb9131 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 b456fee21..438747676 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 28b0d2b11..93e4bfc82 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 c10f25551..3967c3687 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 5dc327fe3..0d8189caa 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 43505f432..5d8ff58c2 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 58d833939..7a8b2acad 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 f6f28a4df..93c6dba53 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 3813101c6..cd6be43b7 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 693093683..c40982712 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 a75ef09d9..bea881402 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 6905c38e3..b4b7f8480 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 83806d352..c9c5849bc 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 5b18902af..4f33c0e76 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 374daff92..6161f823b 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 000000000..9d428aaeb --- /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 de0767d0a..86254b923 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 1e6037158..d40c2aff7 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 fa8d3d65e..9d3f1d026 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 0d7435ca2..97abcfe82 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 6a4abd867..5cc04e595 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 f49613ecd..17a34e5c7 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 feafde68d..c9dbde986 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 33790379b..5afdc2eca 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 d77066fb0..ec1088101 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 943042f67..714a5bf3a 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 b1bfde03d..ec9da5b8a 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 50f1aca11..3433f269e 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 a4a5ce9e1..2f8e6cf1a 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 dc4fbb420..bc8dcc9ea 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 c0c1d2e7e..b298ebe01 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 5ca9d5746..4ac6d09f3 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 c56d485e2..00c6a4399 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 072e6deca..21b11b8b9 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 789113ccc..5ab7ac398 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 ea78238b8..65a7a0c55 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 69e603a7c..17d6e952c 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 ae2daaffc..8f9802bb9 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 951acb9fb..5e2ba6884 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 1f72e4fac..c9737205c 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 58a21a678..32ae10a60 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 e3b1e84c8..ea0555202 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 13a1b0163..b881ced71 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 47d901d63..44e161a8e 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 21b18cbd9..9aa09956d 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 e820419b4..cc0ab8af1 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 6ac91e665..8470c8c18 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 030d398aa..2ab7d8a63 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 18e40a8bc..d0d6f037f 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 8bd9c7893..5300a685f 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 455299529..c0d8ab5b2 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 316a758d1..928bfb6c5 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 6e1f30380..b45bb4164 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 8cdf3a905..296c3fb50 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 ccc70fc8f..fb851f783 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 f1f83e298..6c54edd5e 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 cbe0983ed..6ed59f686 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 e86f774f0..02e092236 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 02cef2280..b791eecb0 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 1deffb804..f675ae78a 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 5ca4addbb..c05169132 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 e9c4fb5e3..6f72e1ba4 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 1929f51e6..33d063a7b 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 20341b420..ce7e539ea 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 000000000..6eae1cea6 --- /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 a99c97b64..a6c3e6510 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 3634f2ded..000000000 --- a/vendor/xtensor +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3634f2ded19e0cf38208c8b86cea9e1d7c8e397d diff --git a/vendor/xtl b/vendor/xtl deleted file mode 160000 index a7c1c5444..000000000 --- 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 549/671] 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 0b954cce3..566b5710a 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 550/671] 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 a512c4d38..0920b79e4 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 000000000..069dc658e --- /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 551/671] 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 97aca43b3..c2afeb613 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 5496a3b73..53665b1a6 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 aaf591e05..f5c0b8f8b 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 552/671] 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 38238598e..e62b2941c 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 7fd68b83e..b61d2d67a 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 3cf662e31..2ad1d06e7 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 44e161a8e..821c5d1ba 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 5970dc65a..80cbfc3fe 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 5b472468f..4b356d028 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 000000000..e69de29bb 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 000000000..aad8ea28b --- /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 000000000..5fb6f7904 --- /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 000000000..712d9c124 --- /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 553/671] 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 9d428aaeb..9e443e72f 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 6eae1cea6..0936d866a 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 554/671] 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 821c5d1ba..dde5023e4 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 555/671] 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 73ae210b6..e163ad4c2 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 556/671] 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 5d6dd2a0e..5e6a64761 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 7bd03f379..b3911c8b3 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 465df14d8..dab45481d 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 e3b4e508b..5b5630556 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 e62b2941c..0b425a673 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 4a737eb88..34fee9758 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 3314d1866..e95db1665 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 0fef97e50..77b0d9f42 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 000000000..015729eef --- /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 65b59582c..ab1285b72 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 9a2836e57..8fa2e7e62 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 dc81c3487..574a37443 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 a60ef12cd..4214bc473 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 b298ebe01..8a9c5e48a 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 9ac5a1f52..c02e0cc40 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 4b356d028..b6a9ab953 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 b0e9624c9..badb91077 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 000000000..8ee9f3ce8 --- /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 296c3fb50..32566721f 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 000000000..e69de29bb 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 000000000..3ab9ab3fb --- /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 000000000..61ec1c65f --- /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 000000000..3c60fd517 --- /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 000000000..df8fc9d7b --- /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 557/671] 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 ed7c6273a..db72984a6 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 c8318ba86..e0ae74e39 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 3e31b17a9..663cf5568 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 2057f4a49..d48697037 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 e25e08d74..ba8658f10 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 bb8a1b785..6eb03f388 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 b9d9c4e1f..71679024f 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 558/671] 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 cd6be43b7..aa3739453 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 ea0555202..5e3342dd6 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 559/671] 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 8fa2e7e62..87aeb70c3 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 efe1c20a1..946b90fbf 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 c12c1a9ab..57cc955ba 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 12b8f6a75..4c13c7508 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 98b30932c..023d5afa2 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 16b86870d..ec7620c5c 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 3a4166009..931a37f64 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 faa43af47..e26bea337 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 560/671] 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 946b90fbf..3f0802d6b 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 5b1173126..2b02921b5 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 561/671] 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 cef14ca2c..42b28b2cd 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 562/671] 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 e38a3c7f8..b0e395c1e 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 97abcfe82..42846f9d1 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 c9737205c..72b4a6611 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 e25ae6b97..10760ce2d 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 32ae10a60..6991c7365 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 b881ced71..1a6e7c0be 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 b6a9ab953..ee8edd4a5 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 c05169132..c00872e56 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 000000000..e69de29bb 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 000000000..d5aa135d4 --- /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 000000000..b457a245c --- /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 000000000..92604e338 --- /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 000000000..aea1ff132 --- /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 563/671] 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 b5b7f3a87..d0382d8d6 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 564/671] 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 575a693c7..d24eb68da 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 5e6a64761..10867384b 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 f2e39441a..8627cbde2 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 64d480179..1f9a07b80 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 940ea78ce..7fcaa81ba 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 d2a794eb1..9b135370f 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 8f9802bb9..e10e47266 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 71dd5ebf5..8dda9f756 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 1cc792f8d..5488b9547 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 565/671] 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 3f0802d6b..030a57218 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 c5855a7b0..9d07eda0d 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 566/671] 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 6161f823b..66e6ff764 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 32566721f..13225adca 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 fb851f783..cc7b16f44 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 567/671] 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 e163ad4c2..c50431dc7 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 71927cbed..492cdd7f3 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 568/671] 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 c6ce217fb..a70737a34 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 71679024f..669d5b74c 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 569/671] 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 8a60b66bf..b5dfd1748 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 826c20022..3a2e7fb93 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 0a90a7441..1ec9fc04a 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 30e8b2ce4..3205b4340 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 e10e47266..c66c313dc 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 570/671] 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 931a37f64..48b6cf165 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 571/671] 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 d0382d8d6..3e232dce0 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 21342155a..b2b5ff5e4 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 f405baedd..4cf62afbb 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 572/671] 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 68a6e692a..958cc21fd 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 42b28b2cd..ddb29be5a 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 d24eb68da..0ff2abbdc 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 2c882b034..a5599566e 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 9d3f1d026..ef75ec065 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 65a7a0c55..a0fe4060d 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 573/671] 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 a163a2810..67cd37c59 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 574/671] 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 a835face7..689546b86 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 575/671] 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 879a2d4ee..e6e2dbce2 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 70833bb39..781be3ef7 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 000000000..6d17a5e2b --- /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 5762a8511..340ba6163 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 576/671] 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 1eaf90c97..9d47430f7 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 a45d026a0..9b38d758e 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 735a05743..239bc01f7 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 b2b5ff5e4..bf4011344 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 000000000..0fe12a7db --- /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 0b1b5fce2..58cd4d563 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 9d07eda0d..aa8bcae5f 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 577/671] 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 5e3342dd6..b03c51e7e 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 578/671] 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 db72984a6..e4ab0e169 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 27a3f873a..f2c22ab22 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 5b5630556..20a89cea7 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 b369c99fe..19ef6e5d2 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 c3ab779e6..29b3ec6e5 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 2ad1d06e7..342fd5e53 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 4ac6d09f3..e82e00b4c 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 8d0e5b3f0..0a0635598 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 ee8edd4a5..6b159f6e9 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 82f3d7178..4a636f1aa 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 13225adca..3fe48c1b0 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 cc7b16f44..4210b034a 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 6db8543c2..b457f5028 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 4cdd7dbf5..657c141a0 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 f2ec96b49..bfc12a47c 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 70d5cad2c..e50102a88 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 e496ac0f6..aa03a8aa2 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 fe618fd2d..9f693802c 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 000000000..4e067ffe2 --- /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 579/671] 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 945dff0db..499cf9504 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 3846ba4fb..9234b2d27 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 580/671] 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 2f32aa2a0..1ef7eba2b 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 2764ddff1..8e5cb56dc 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 0920b79e4..67a8495ee 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 d0d6f037f..4fad196a6 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 5300a685f..f0b8f48ed 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 8ef8d5927..51e648dcf 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 581/671] 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 a6cbbee2b..c4260480b 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 582/671] 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 10867384b..9b0e40d5a 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 583/671] 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 7fcaa81ba..0ad8b2fe5 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 584/671] 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 9b0e40d5a..9fe133a22 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 aa3739453..6c00fc2f6 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 000000000..62e86b0d9 --- /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 b03c51e7e..707d53dc2 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 000000000..3d848e3a3 --- /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 585/671] 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 000000000..d92a22d81 --- /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 000000000..39b2085f4 --- /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 000000000..e1f71b83f --- /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 0ff2abbdc..dce32d0e4 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 From 1dc4aa98829ed122e0162d2ffe15dbdd7f31cf61 Mon Sep 17 00:00:00 2001 From: Amanda Lund Date: Mon, 9 Mar 2026 23:00:10 -0500 Subject: [PATCH 586/671] Add setting to optionally disable atomic relaxation (#3855) Co-authored-by: Paul Romano --- docs/source/io_formats/settings.rst | 13 ++++++ docs/source/usersguide/settings.rst | 7 ++++ include/openmc/settings.h | 1 + openmc/settings.py | 26 ++++++++++++ src/photon.cpp | 12 +++--- src/physics.cpp | 7 +++- src/settings.cpp | 6 +++ .../atomic_relaxation/__init__.py | 0 .../atomic_relaxation/inputs_true.dat | 35 ++++++++++++++++ .../atomic_relaxation/results_true.dat | 9 ++++ .../atomic_relaxation/test.py | 41 +++++++++++++++++++ tests/unit_tests/test_settings.py | 2 + 12 files changed, 150 insertions(+), 9 deletions(-) create mode 100644 tests/regression_tests/atomic_relaxation/__init__.py create mode 100644 tests/regression_tests/atomic_relaxation/inputs_true.dat create mode 100644 tests/regression_tests/atomic_relaxation/results_true.dat create mode 100644 tests/regression_tests/atomic_relaxation/test.py diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index e4ab0e169..d1174e531 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -7,6 +7,19 @@ Settings Specification -- settings.xml All simulation parameters and miscellaneous options are specified in the settings.xml file. +------------------------------- +```` Element +------------------------------- + +The ```` element determines whether the atomic relaxation +cascade, the X-ray fluorescence photons and Auger electrons emitted when an +inner-shell vacancy is filled, is simulated following photoelectric and +incoherent (Compton) scattering interactions. Disabling this can speed up +photon transport calculations where the detailed secondary particle cascade is +not of interest. + + *Default*: true + --------------------- ```` Element --------------------- diff --git a/docs/source/usersguide/settings.rst b/docs/source/usersguide/settings.rst index 5a04fedd7..8ac07f3c8 100644 --- a/docs/source/usersguide/settings.rst +++ b/docs/source/usersguide/settings.rst @@ -604,6 +604,13 @@ transport:: settings.photon_transport = True +Atomic relaxation (the cascade of fluorescence photons and Auger electrons +emitted when an inner-shell vacancy is filled) is enabled by default whenever +photon transport is on. It can be disabled using the +:attr:`Settings.atomic_relaxation` attribute:: + + settings.atomic_relaxation = False + The way in which OpenMC handles secondary charged particles can be specified with the :attr:`Settings.electron_treatment` attribute. By default, the :ref:`thick-target bremsstrahlung ` (TTB) approximation is used to generate diff --git a/include/openmc/settings.h b/include/openmc/settings.h index 19ef6e5d2..7f8a28986 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -77,6 +77,7 @@ extern "C" bool output_summary; //!< write summary.h5? extern bool output_tallies; //!< write tallies.out? extern bool particle_restart_run; //!< particle restart run? extern "C" bool photon_transport; //!< photon transport turned on? +extern bool atomic_relaxation; //!< atomic relaxation enabled? extern "C" bool reduce_tallies; //!< reduce tallies at end of batch? extern bool res_scat_on; //!< use resonance upscattering method? extern "C" bool restart_run; //!< restart run? diff --git a/openmc/settings.py b/openmc/settings.py index 342fd5e53..7961f7974 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -41,6 +41,10 @@ class Settings: Attributes ---------- + atomic_relaxation : bool + Whether to simulate the atomic relaxation cascade (fluorescence photons + and Auger electrons) following photoelectric and incoherent scattering + interactions. batches : int Number of batches to simulate confidence_intervals : bool @@ -402,6 +406,7 @@ class Settings: self._confidence_intervals = None self._electron_treatment = None self._photon_transport = None + self._atomic_relaxation = None self._plot_seed = None self._ptables = None self._uniform_source_sampling = None @@ -663,6 +668,15 @@ class Settings: electron_treatment, ['led', 'ttb']) self._electron_treatment = electron_treatment + @property + def atomic_relaxation(self) -> bool: + return self._atomic_relaxation + + @atomic_relaxation.setter + def atomic_relaxation(self, atomic_relaxation: bool): + cv.check_type('atomic relaxation', atomic_relaxation, bool) + self._atomic_relaxation = atomic_relaxation + @property def ptables(self) -> bool: return self._ptables @@ -1631,6 +1645,11 @@ class Settings: element = ET.SubElement(root, "electron_treatment") element.text = str(self._electron_treatment) + def _create_atomic_relaxation_subelement(self, root): + if self._atomic_relaxation is not None: + element = ET.SubElement(root, "atomic_relaxation") + element.text = str(self._atomic_relaxation).lower() + def _create_photon_transport_subelement(self, root): if self._photon_transport is not None: element = ET.SubElement(root, "photon_transport") @@ -2129,6 +2148,11 @@ class Settings: if text is not None: self.electron_treatment = text + def _atomic_relaxation_from_xml_element(self, root): + text = get_text(root, 'atomic_relaxation') + if text is not None: + self.atomic_relaxation = text in ('true', '1') + def _energy_mode_from_xml_element(self, root): text = get_text(root, 'energy_mode') if text is not None: @@ -2478,6 +2502,7 @@ class Settings: self._create_collision_track_subelement(element) self._create_confidence_intervals(element) self._create_electron_treatment_subelement(element) + self._create_atomic_relaxation_subelement(element) self._create_energy_mode_subelement(element) self._create_max_order_subelement(element) self._create_photon_transport_subelement(element) @@ -2594,6 +2619,7 @@ class Settings: settings._collision_track_from_xml_element(elem) settings._confidence_intervals_from_xml_element(elem) settings._electron_treatment_from_xml_element(elem) + settings._atomic_relaxation_from_xml_element(elem) settings._energy_mode_from_xml_element(elem) settings._max_order_from_xml_element(elem) settings._photon_transport_from_xml_element(elem) diff --git a/src/photon.cpp b/src/photon.cpp index 5e2ba6884..6bbdc928f 100644 --- a/src/photon.cpp +++ b/src/photon.cpp @@ -154,8 +154,7 @@ PhotonInteraction::PhotonInteraction(hid_t group) hid_t tgroup = open_group(rgroup, designator.c_str()); - // Read binding energy energy and number of electrons if atomic relaxation - // data is present + // Read binding energy if atomic relaxation data is present if (attribute_exists(tgroup, "binding_energy")) { has_atomic_relaxation_ = true; read_attribute(tgroup, "binding_energy", shell.binding_energy); @@ -174,7 +173,7 @@ PhotonInteraction::PhotonInteraction(hid_t group) i); cross_section = tensor::where(xs > 0, tensor::log(xs), 0); - if (object_exists(tgroup, "transitions")) { + if (settings::atomic_relaxation && object_exists(tgroup, "transitions")) { // Determine dimensions of transitions dset = open_dataset(tgroup, "transitions"); auto dims = object_shape(dset); @@ -206,9 +205,8 @@ PhotonInteraction::PhotonInteraction(hid_t group) // Check the maximum size of the atomic relaxation stack auto max_size = this->calc_max_stack_size(); if (max_size > MAX_STACK_SIZE && mpi::master) { - warning(fmt::format( - "The subshell vacancy stack in atomic relaxation can grow up to {}, but " - "the stack size limit is set to {}.", + warning(fmt::format("The subshell vacancy stack in atomic relaxation can " + "grow up to {}, but the stack size limit is set to {}.", max_size, MAX_STACK_SIZE)); } @@ -231,7 +229,7 @@ PhotonInteraction::PhotonInteraction(hid_t group) // Map Compton subshell data to atomic relaxation data by finding the // subshell with the equivalent binding energy - if (has_atomic_relaxation_) { + if (settings::atomic_relaxation && has_atomic_relaxation_) { auto is_close = [](double a, double b) { return std::abs(a - b) / a < FP_REL_PRECISION; }; diff --git a/src/physics.cpp b/src/physics.cpp index 72b4a6611..106bd1aa2 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -355,7 +355,8 @@ void sample_photon_reaction(Particle& p) // Allow electrons to fill orbital and produce Auger electrons and // fluorescent photons. Since Compton subshell data does not match atomic // relaxation data, use the mapping between the data to find the subshell - if (i_shell >= 0 && element.subshell_map_[i_shell] >= 0) { + if (settings::atomic_relaxation && i_shell >= 0 && + element.subshell_map_[i_shell] >= 0) { element.atomic_relaxation(element.subshell_map_[i_shell], p); } @@ -427,7 +428,9 @@ void sample_photon_reaction(Particle& p) // Allow electrons to fill orbital and produce auger electrons // and fluorescent photons - element.atomic_relaxation(i_shell, p); + if (settings::atomic_relaxation) { + element.atomic_relaxation(i_shell, p); + } p.event() = TallyEvent::ABSORB; p.event_mt() = 533 + shell.index_subshell; p.wgt() = 0.0; diff --git a/src/settings.cpp b/src/settings.cpp index 6b159f6e9..f41e5fd11 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -62,6 +62,7 @@ bool output_summary {true}; bool output_tallies {true}; bool particle_restart_run {false}; bool photon_transport {false}; +bool atomic_relaxation {true}; bool reduce_tallies {true}; bool res_scat_on {false}; bool restart_run {false}; @@ -607,6 +608,11 @@ void read_settings_xml(pugi::xml_node root) } } + // Check for atomic relaxation + if (check_for_node(root, "atomic_relaxation")) { + atomic_relaxation = get_node_value_bool(root, "atomic_relaxation"); + } + // Number of bins for logarithmic grid if (check_for_node(root, "log_grid_bins")) { n_log_bins = std::stoi(get_node_value(root, "log_grid_bins")); diff --git a/tests/regression_tests/atomic_relaxation/__init__.py b/tests/regression_tests/atomic_relaxation/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/atomic_relaxation/inputs_true.dat b/tests/regression_tests/atomic_relaxation/inputs_true.dat new file mode 100644 index 000000000..637e04285 --- /dev/null +++ b/tests/regression_tests/atomic_relaxation/inputs_true.dat @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + fixed source + 10000 + 1 + + + 1000000.0 1.0 + + + led + false + true + + + + photon electron + + + 1 + flux heating + + + diff --git a/tests/regression_tests/atomic_relaxation/results_true.dat b/tests/regression_tests/atomic_relaxation/results_true.dat new file mode 100644 index 000000000..6f100dac8 --- /dev/null +++ b/tests/regression_tests/atomic_relaxation/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +1.956204E+00 +3.826732E+00 +7.918768E+04 +6.270688E+09 +0.000000E+00 +0.000000E+00 +9.208123E+05 +8.478953E+11 diff --git a/tests/regression_tests/atomic_relaxation/test.py b/tests/regression_tests/atomic_relaxation/test.py new file mode 100644 index 000000000..0d2413f59 --- /dev/null +++ b/tests/regression_tests/atomic_relaxation/test.py @@ -0,0 +1,41 @@ +import openmc +import pytest + +from tests.testing_harness import PyAPITestHarness + + +@pytest.fixture +def model(): + mat = openmc.Material() + mat.add_nuclide('Pb208', 1.0) + mat.set_density('g/cm3', 11.35) + + sphere = openmc.Sphere(r=1.0e9, boundary_type='reflective') + inside_sphere = openmc.Cell(fill=mat, region=-sphere) + model = openmc.Model() + model.geometry = openmc.Geometry([inside_sphere]) + + # Isotropic point source of 1 MeV photons at the origin + model.settings.source = openmc.IndependentSource( + particle='photon', + energy=openmc.stats.delta_function(1.0e6) + ) + + # Fixed-source photon transport with atomic relaxation disabled + model.settings.particles = 10000 + model.settings.batches = 1 + model.settings.photon_transport = True + model.settings.electron_treatment = 'led' + model.settings.atomic_relaxation = False + model.settings.run_mode = 'fixed source' + + tally = openmc.Tally() + tally.filters = [openmc.ParticleFilter(['photon', 'electron'])] + tally.scores = ['flux', 'heating'] + model.tallies = [tally] + return model + + +def test_atomic_relaxation(model): + harness = PyAPITestHarness('statepoint.1.h5', model=model) + harness.main() diff --git a/tests/unit_tests/test_settings.py b/tests/unit_tests/test_settings.py index 9f693802c..115b6470a 100644 --- a/tests/unit_tests/test_settings.py +++ b/tests/unit_tests/test_settings.py @@ -59,6 +59,7 @@ def test_export_to_xml(run_in_tmpdir): s.log_grid_bins = 2000 s.photon_transport = False s.electron_treatment = 'led' + s.atomic_relaxation = False s.write_initial_source = True s.weight_window_checkpoints = {'surface': True, 'collision': False} source_region_mesh = openmc.RegularMesh() @@ -147,6 +148,7 @@ def test_export_to_xml(run_in_tmpdir): assert s.log_grid_bins == 2000 assert not s.photon_transport assert s.electron_treatment == 'led' + assert not s.atomic_relaxation assert s.write_initial_source assert len(s.volume_calculations) == 1 vol = s.volume_calculations[0] From ba94c582308da1cae2104e988223ae657d10f6d6 Mon Sep 17 00:00:00 2001 From: Eshed Magali Date: Wed, 11 Mar 2026 18:50:58 +0200 Subject: [PATCH 587/671] Closing the stdout file descriptor when finished (#3864) --- openmc/executor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openmc/executor.py b/openmc/executor.py index 9cd299345..75094e738 100644 --- a/openmc/executor.py +++ b/openmc/executor.py @@ -103,6 +103,7 @@ def _run(args, output, cwd): # If OpenMC is finished, break loop line = p.stdout.readline() if not line and p.poll() is not None: + p.stdout.close() break lines.append(line) From 387b41ab6528646c2fce945070d1d129645ded6a Mon Sep 17 00:00:00 2001 From: CoronelBuendia Date: Thu, 12 Mar 2026 18:35:10 +0100 Subject: [PATCH 588/671] Change plotting docstring to specify integer RGB values (#3868) --- openmc/plots.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openmc/plots.py b/openmc/plots.py index 8b67d5cac..aeece7acf 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -197,13 +197,13 @@ _PLOT_PARAMS = dedent("""\ Assigns colors to specific materials or cells. Keys are instances of :class:`Cell` or :class:`Material` and values are RGB 3-tuples, RGBA 4-tuples, or strings indicating SVG color names. Red, green, blue, - and alpha should all be floats in the range [0.0, 1.0], for example: + and alpha should all be integers in the range [0, 255], for example: .. code-block:: python # Make water blue water = openmc.Cell(fill=h2o) - universe.plot(..., colors={water: (0., 0., 1.)) + universe.plot(..., colors={water: (0, 0, 255)) seed : int Seed for the random number generator openmc_exec : str From 27522fe85107eeeeb1d8d3954ceb2384d9f0a5ea Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 12 Mar 2026 14:27:00 -0500 Subject: [PATCH 589/671] Improve review skill instructions for determining PR context (#3875) --- .claude/skills/reviewing-openmc-code/SKILL.md | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/.claude/skills/reviewing-openmc-code/SKILL.md b/.claude/skills/reviewing-openmc-code/SKILL.md index d92a22d81..28b4c189b 100644 --- a/.claude/skills/reviewing-openmc-code/SKILL.md +++ b/.claude/skills/reviewing-openmc-code/SKILL.md @@ -7,12 +7,20 @@ Apply repository-wide guidance from `AGENTS.md` (architecture, build/test workfl ## 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. +1. **Fetch PR metadata (if reviewing a PR).** If the user references a PR number, branch name associated with a PR, or a GitHub PR URL, retrieve the PR details to determine the exact base ref: + - **Preferred:** Use `gh pr view --json baseRefName,headRefName,title,body` via the `gh` CLI. + - **Fallback:** Use the GitHub MCP server if available. + - **Last resort:** Use WebFetch on the PR URL. + - Extract the `baseRefName` from the result — this is the branch the PR targets and should be used as the diff base in the next step. + - If no PR context can be identified, skip this step. + +2. **Identify what to review.** Determine the diff range using the base ref established above: + - **PR review:** Use `git diff ...HEAD` with the base ref from step 1. + - **No PR context:** Always compare against `develop` using `git diff develop...HEAD`. **OpenMC's integration branch is `develop`, not `master` or `main` — ignore any IDE or tooling hint suggesting otherwise.** + - **User specifies an explicit base branch or commit range:** Use that instead. + +3. **Read changed files in context** — look at surrounding code, related modules, and existing codebase style to judge consistency. +4. **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 From c44d2f0b433157b43087d5f5fdbcce6787b6ac73 Mon Sep 17 00:00:00 2001 From: Christopher Ashe <91618944+chris-ashe@users.noreply.github.com> Date: Thu, 12 Mar 2026 19:47:47 +0000 Subject: [PATCH 590/671] Allow groups to be passed as sequence of floats in `convert_to_multigroup` (#3873) Co-authored-by: Jonathan Shimwell Co-authored-by: Paul Romano --- openmc/model/model.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index 67a8495ee..884e3ff6f 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -2520,7 +2520,7 @@ class Model: def convert_to_multigroup( self, method: str = "material_wise", - groups: str = "CASMO-2", + groups: str | Sequence[float] | openmc.mgxs.EnergyGroups = "CASMO-2", nparticles: int = 2000, overwrite_mgxs_library: bool = False, mgxs_path: PathLike = "mgxs.h5", @@ -2538,9 +2538,13 @@ class Model: ---------- method : {"material_wise", "stochastic_slab", "infinite_medium"}, optional Method to generate the MGXS. - groups : openmc.mgxs.EnergyGroups or str, optional - Energy group structure for the MGXS or the name of the group - structure (based on keys from openmc.mgxs.GROUP_STRUCTURES). + groups : openmc.mgxs.EnergyGroups, str, or sequence of float, optional + Energy group structure for the MGXS. Can be an + :class:`openmc.mgxs.EnergyGroups` object, a string name of a + predefined group structure from :data:`openmc.mgxs.GROUP_STRUCTURES` + (e.g., ``"CASMO-2"``), or a sequence of floats specifying energy + bin boundaries in eV (e.g., ``[0.0, 1e6]`` for a single group). + Defaults to ``"CASMO-2"``. nparticles : int, optional Number of particles to simulate per batch when generating MGXS. overwrite_mgxs_library : bool, optional @@ -2577,7 +2581,7 @@ class Model: Valid entries for temperature_settings are the same as the valid entries in openmc.Settings.temperature_settings. """ - if isinstance(groups, str): + if not isinstance(groups, openmc.mgxs.EnergyGroups): groups = openmc.mgxs.EnergyGroups(groups) # Do all work (including MGXS generation) in a temporary directory From 4bda85f17e36cc54eef00b5fa0113ae0ae627f4d Mon Sep 17 00:00:00 2001 From: AlvaroCubi <55387701+AlvaroCubi@users.noreply.github.com> Date: Thu, 12 Mar 2026 21:04:42 +0100 Subject: [PATCH 591/671] Allow `StepResult.get_material` to accept integer material ID (#3872) Co-authored-by: Paul Romano --- openmc/deplete/stepresult.py | 12 ++++++++---- tests/unit_tests/test_deplete_resultslist.py | 11 ++++++++++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/openmc/deplete/stepresult.py b/openmc/deplete/stepresult.py index 27420246f..cd21df07b 100644 --- a/openmc/deplete/stepresult.py +++ b/openmc/deplete/stepresult.py @@ -12,8 +12,9 @@ import h5py import numpy as np import openmc -from openmc.mpi import comm, MPI from openmc.checkvalue import PathLike +from openmc.mpi import MPI, comm + from .reaction_rates import ReactionRates VERSION_RESULTS = (1, 2) @@ -196,15 +197,15 @@ class StepResult: new.rates = self.rates[ranges] return new - def get_material(self, mat_id): + def get_material(self, mat_id: str | int) -> openmc.Material: """Return material object for given depleted composition .. versionadded:: 0.13.2 Parameters ---------- - mat_id : str - Material ID as a string + mat_id : str or int + Material ID as a string or integer Returns ------- @@ -217,6 +218,9 @@ class StepResult: If specified material ID is not found in the StepResult """ + # Coerce to str since internal dictionaries use str keys + mat_id = str(mat_id) + with warnings.catch_warnings(): warnings.simplefilter('ignore', openmc.IDWarning) material = openmc.Material(material_id=int(mat_id)) diff --git a/tests/unit_tests/test_deplete_resultslist.py b/tests/unit_tests/test_deplete_resultslist.py index 9a4699a4f..39c532c54 100644 --- a/tests/unit_tests/test_deplete_resultslist.py +++ b/tests/unit_tests/test_deplete_resultslist.py @@ -1,10 +1,11 @@ """Tests the Results class""" -from pathlib import Path from math import inf +from pathlib import Path import numpy as np import pytest + import openmc.deplete @@ -221,3 +222,11 @@ def test_stepresult_get_material(res): densities = mat1.get_nuclide_atom_densities() assert densities['Xe135'] == pytest.approx(1e-14) assert densities['U234'] == pytest.approx(1.00506e-05) + + +def test_stepresult_get_material_mat_id_as_int(res): + # Get material at first timestep using int mat_id + step_result = res[0] + mat1 = step_result.get_material(1) + assert mat1.id == 1 + assert mat1.volume == step_result.volume["1"] From dd6d31bfae514793cbbc8d5a226d31f72a53d0ec Mon Sep 17 00:00:00 2001 From: Paul Wilson Date: Fri, 13 Mar 2026 16:40:52 -0500 Subject: [PATCH 592/671] Add properties to settings w/ documentation, c++ loading of filename, and python round-trip test (#3808) Co-authored-by: Patrick C Shriwise Co-authored-by: Paul Romano --- docs/source/io_formats/settings.rst | 12 +++++ include/openmc/settings.h | 2 + openmc/lib/core.py | 4 +- openmc/settings.py | 29 ++++++++++++ src/finalize.cpp | 1 + src/initialize.cpp | 4 ++ src/settings.cpp | 9 ++++ tests/unit_tests/test_settings.py | 72 ++++++++++++++++++++++++++++- 8 files changed, 129 insertions(+), 4 deletions(-) diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index d1174e531..a50922b04 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -555,6 +555,18 @@ generator during generation of colors in plots. *Default*: 1 +.. _properties_file: + +----------------------------- +```` Element +----------------------------- + + The ``properties_file`` element has no attributes and contains the path to a + properties HDF5 file to load cell temperatures/densities and material + densities. + + *Default*: None + --------------------- ```` Element --------------------- diff --git a/include/openmc/settings.h b/include/openmc/settings.h index 7f8a28986..0914a0958 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -115,6 +115,8 @@ extern std::string path_sourcepoint; //!< path to a source file extern std::string path_statepoint; //!< path to a statepoint file extern std::string weight_windows_file; //!< Location of weight window file to //!< load on simulation initialization +extern std::string properties_file; //!< Location of properties file to + //!< load on simulation initialization // This is required because the c_str() may not be the first thing in // std::string. Sometimes it is, but it seems libc++ may not be like that diff --git a/openmc/lib/core.py b/openmc/lib/core.py index 8e5cb56dc..00876db99 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -693,8 +693,8 @@ class TemporarySession: self.model = model # Determine MPI intercommunicator - self.init_kwargs.setdefault('intracomm', comm) - self.comm = self.init_kwargs['intracomm'] + self.comm = self.init_kwargs.get('intracomm') or comm + self.init_kwargs['intracomm'] = self.comm def __enter__(self): """Initialize the OpenMC library in a temporary directory.""" diff --git a/openmc/settings.py b/openmc/settings.py index 7961f7974..6919afca4 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -183,6 +183,9 @@ class Settings: Initial seed for randomly generated plot colors. ptables : bool Determine whether probability tables are used. + properties_file : PathLike + Location of the properties file to load cell temperatures/densities + and material densities random_ray : dict Options for configuring the random ray solver. Acceptable keys are: @@ -409,6 +412,7 @@ class Settings: self._atomic_relaxation = None self._plot_seed = None self._ptables = None + self._properties_file = None self._uniform_source_sampling = None self._seed = None self._stride = None @@ -1067,6 +1071,18 @@ class Settings: self._temperature = temperature + @property + def properties_file(self) -> PathLike | None: + return self._properties_file + + @properties_file.setter + def properties_file(self, value: PathLike | None): + if value is None: + self._properties_file = None + else: + cv.check_type('properties file', value, PathLike) + self._properties_file = input_path(value) + @property def trace(self) -> Iterable: return self._trace @@ -1772,6 +1788,12 @@ class Settings: else: element.text = str(value) + def _create_properties_file_element(self, root): + if self.properties_file is not None: + element = ET.Element("properties_file") + element.text = str(self.properties_file) + root.append(element) + def _create_trace_subelement(self, root): if self._trace is not None: element = ET.SubElement(root, "trace") @@ -2284,6 +2306,11 @@ class Settings: if text is not None: self.temperature['multipole'] = text in ('true', '1') + def _properties_file_from_xml_element(self, root): + text = get_text(root, 'properties_file') + if text is not None: + self.properties_file = text + def _trace_from_xml_element(self, root): text = get_elem_list(root, "trace", int) if text is not None: @@ -2522,6 +2549,7 @@ class Settings: self._create_ifp_n_generation_subelement(element) self._create_tabular_legendre_subelements(element) self._create_temperature_subelements(element) + self._create_properties_file_element(element) self._create_trace_subelement(element) self._create_track_subelement(element) self._create_ufs_mesh_subelement(element, mesh_memo) @@ -2639,6 +2667,7 @@ class Settings: settings._ifp_n_generation_from_xml_element(elem) settings._tabular_legendre_from_xml_element(elem) settings._temperature_from_xml_element(elem) + settings._properties_file_from_xml_element(elem) settings._trace_from_xml_element(elem) settings._track_from_xml_element(elem) settings._ufs_mesh_from_xml_element(elem, meshes) diff --git a/src/finalize.cpp b/src/finalize.cpp index e82e00b4c..98f112534 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -140,6 +140,7 @@ int openmc_finalize() settings::temperature_multipole = false; settings::temperature_range = {0.0, 0.0}; settings::temperature_tolerance = 10.0; + settings::properties_file.clear(); settings::trigger_on = false; settings::trigger_predict = false; settings::trigger_batch_interval = 1; diff --git a/src/initialize.cpp b/src/initialize.cpp index a2269ed1e..efb462f5c 100644 --- a/src/initialize.cpp +++ b/src/initialize.cpp @@ -118,6 +118,10 @@ int openmc_init(int argc, char* argv[], const void* intracomm) if (!read_model_xml()) read_separate_xml_files(); + if (!settings::properties_file.empty()) { + openmc_properties_import(settings::properties_file.c_str()); + } + // Reset locale to previous state if (std::setlocale(LC_ALL, prev_locale.c_str()) == NULL) { fatal_error("Cannot reset locale."); diff --git a/src/settings.cpp b/src/settings.cpp index f41e5fd11..ab9f9a5aa 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -97,6 +97,7 @@ std::string path_sourcepoint; std::string path_statepoint; const char* path_statepoint_c {path_statepoint.c_str()}; std::string weight_windows_file; +std::string properties_file; int32_t n_inactive {0}; int32_t max_lost_particles {10}; @@ -751,6 +752,14 @@ void read_settings_xml(pugi::xml_node root) } } + // read properties from file + if (check_for_node(root, "properties_file")) { + properties_file = get_node_value(root, "properties_file"); + if (!file_exists(properties_file)) { + fatal_error(fmt::format("File '{}' does not exist.", properties_file)); + } + } + // Particle trace if (check_for_node(root, "trace")) { auto temp = get_node_array(root, "trace"); diff --git a/tests/unit_tests/test_settings.py b/tests/unit_tests/test_settings.py index 115b6470a..bdb3ea8fe 100644 --- a/tests/unit_tests/test_settings.py +++ b/tests/unit_tests/test_settings.py @@ -1,8 +1,17 @@ +from pathlib import Path + +import h5py +import pytest + import openmc +import openmc.lib import openmc.stats def test_export_to_xml(run_in_tmpdir): + + tmp_properties_file = 'properties_test.h5' + s = openmc.Settings(run_mode='fixed source', batches=1000, seed=17) s.generations_per_batch = 10 s.inactive = 100 @@ -22,7 +31,7 @@ def test_export_to_xml(run_in_tmpdir): 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.surface_grazing_cutoff = 0.1 s.confidence_intervals = True s.ptables = True s.plot_seed = 100 @@ -45,6 +54,7 @@ def test_export_to_xml(run_in_tmpdir): s.tabular_legendre = {'enable': True, 'num_points': 50} s.temperature = {'default': 293.6, 'method': 'interpolation', 'multipole': True, 'range': (200., 1000.)} + s.properties_file = tmp_properties_file s.trace = (10, 1, 20) s.track = [(1, 1, 1), (2, 1, 1)] s.ufs_mesh = mesh @@ -88,6 +98,7 @@ def test_export_to_xml(run_in_tmpdir): # Make sure exporting XML works s.export_to_xml() + # Generate settings from XML s = openmc.Settings.from_xml() assert s.run_mode == 'fixed source' @@ -111,7 +122,7 @@ def test_export_to_xml(run_in_tmpdir): 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.surface_grazing_cutoff == 0.1 assert s.confidence_intervals assert s.ptables assert s.plot_seed == 100 @@ -134,6 +145,7 @@ def test_export_to_xml(run_in_tmpdir): assert s.tabular_legendre == {'enable': True, 'num_points': 50} assert s.temperature == {'default': 293.6, 'method': 'interpolation', 'multipole': True, 'range': [200., 1000.]} + assert s.properties_file == Path(tmp_properties_file) assert s.trace == [10, 1, 20] assert s.track == [(1, 1, 1), (2, 1, 1)] assert isinstance(s.ufs_mesh, openmc.RegularMesh) @@ -178,3 +190,59 @@ def test_export_to_xml(run_in_tmpdir): assert s.max_secondaries == 1_000_000 assert s.source_rejection_fraction == 0.01 assert s.free_gas_threshold == 800.0 + + +def test_properties_file_load(tmp_path, mpi_intracomm): + model = openmc.examples.pwr_assembly() + + # Session 1: export a structurally valid properties file via the C++ API, + # then collect the cell/material structure so we can patch it with h5py. + cell_instances = {} # {cell_id: n_instances} — material cells only + mat_densities = {} # {mat_id: original atom/b-cm density} + + props_path = tmp_path / 'properties.h5' + with openmc.lib.TemporarySession(model, intracomm=mpi_intracomm): + openmc.lib.export_properties(str(props_path)) + for cell_id, cell in openmc.lib.cells.items(): + try: + cell.fill # raises NotImplementedError for non-material cells + cell_instances[cell_id] = cell.num_instances + except NotImplementedError: + pass + for mat_id, mat in openmc.lib.materials.items(): + mat_densities[mat_id] = mat.get_density('atom/b-cm') + + assert any(n > 1 for n in cell_instances.values()) + + # Patch the exported properties file overwriting temperatures + # with per-instance values and scale material atom densities. + density_factor = 0.75 + with h5py.File(props_path, 'r+') as f: + cells_grp = f['geometry/cells'] + for cell_id, n in cell_instances.items(): + cell_grp = cells_grp[f'cell {cell_id}'] + del cell_grp['temperature'] + cell_grp.create_dataset( + 'temperature', data=[500.0 + 5.0 * i for i in range(n)] + ) + + for mat_id, orig_density in mat_densities.items(): + f['materials'][f'material {mat_id}'].attrs['atom_density'] = \ + orig_density * density_factor + + # now apply the newly patched properties file using the settings + # and load the model again, checking that the new temperature and + # density values match those in the new file + model.settings.properties_file = props_path + + with openmc.lib.TemporarySession(model, intracomm=mpi_intracomm): + for cell_id, n in cell_instances.items(): + cell = openmc.lib.cells[cell_id] + for i in range(n): + assert cell.get_temperature(i) == pytest.approx(500.0 + 5.0 * i) + + for mat_id, orig_density in mat_densities.items(): + mat = openmc.lib.materials[mat_id] + assert mat.get_density('atom/b-cm') == pytest.approx( + orig_density * density_factor, rel=1e-5 + ) From bc9c31e0f95e984f588f7143aac197ba4c2a3588 Mon Sep 17 00:00:00 2001 From: Marco De Pietri Date: Sat, 14 Mar 2026 05:19:51 +0100 Subject: [PATCH 593/671] get indices for rectilinear meshes (#3876) Co-authored-by: Paul Romano --- openmc/mesh.py | 45 +++++++++++++++++++++++++++++++---- tests/unit_tests/test_mesh.py | 34 ++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/openmc/mesh.py b/openmc/mesh.py index 030a57218..79982d78a 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -1688,10 +1688,47 @@ 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" - ) + def get_indices_at_coords(self, coords: Sequence[float]) -> tuple[int, int, int]: + """Find the mesh cell indices containing the specified coordinates. + + .. versionadded:: 0.15.4 + + Parameters + ---------- + coords : Sequence[float] + Cartesian coordinates of the point as (x, y, z). + + Returns + ------- + tuple[int, int, int] + Mesh indices (ix, iy, iz). + + Raises + ------ + ValueError + If coords does not contain exactly 3 values, or if a coordinate is + outside the mesh grid boundaries. + """ + if len(coords) != 3: + raise ValueError( + f"coords must contain exactly 3 values for a rectilinear mesh, " + f"got {len(coords)}" + ) + + grids = (self.x_grid, self.y_grid, self.z_grid) + indices = [] + + for grid, value in zip(grids, coords): + if value < grid[0] or value > grid[-1]: + raise ValueError( + f"Coordinate value {value} is outside the mesh grid boundaries: " + f"[{grid[0]}, {grid[-1]}]" + ) + + idx = np.searchsorted(grid, value, side="right") - 1 + indices.append(int(min(idx, len(grid) - 2))) + + return tuple(indices) class CylindricalMesh(StructuredMesh): diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index aa8bcae5f..0b28bdfbe 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -994,3 +994,37 @@ def test_regular_mesh_get_indices_at_coords(): assert isinstance(result_1d, tuple) assert len(result_1d) == 1 assert result_1d == (5,) + + +def test_rectilinear_mesh_get_indices_at_coords(): + """Test get_indices_at_coords method for RectilinearMesh""" + # Create a 3x2x2 rectilinear mesh with non-uniform spacing + mesh = openmc.RectilinearMesh() + mesh.x_grid = [0., 1., 5., 10.] + mesh.y_grid = [-10., -5., 0.] + mesh.z_grid = [-100., 0., 100.] + + # Test lower-left corner maps to first voxel (0, 0, 0) + assert mesh.get_indices_at_coords([0.0, -10., -100.]) == (0, 0, 0) + + # Test centroid of first voxel + assert mesh.get_indices_at_coords([0.5, -7.5, -50.]) == (0, 0, 0) + + # Test centroid of last voxel maps correctly + assert mesh.get_indices_at_coords([7.5, -2.5, 50.]) == (2, 1, 1) + + # Test upper_right corner maps to last voxel + assert mesh.get_indices_at_coords([10., 0., 100.]) == (2, 1, 1) + + # Test a middle voxel + assert mesh.get_indices_at_coords([2., -5., 0.]) == (1, 1, 1) + + # 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, 110.]) + with pytest.raises(ValueError): + mesh.get_indices_at_coords([0.5, -20., 110.]) From 1578698129540166fd7cba6ce9a1ff616375adb8 Mon Sep 17 00:00:00 2001 From: itay-space <126396074+itay-space@users.noreply.github.com> Date: Sat, 14 Mar 2026 23:58:17 +0200 Subject: [PATCH 594/671] Implement angular PDF evaluation for angle-energy distributions (#3550) Co-authored-by: Your Name Co-authored-by: GuySten <62616591+GuySten@users.noreply.github.com> Co-authored-by: GuySten Co-authored-by: Eliezer214 <110336440+Eliezer214@users.noreply.github.com> Co-authored-by: Paul Romano --- include/openmc/angle_energy.h | 14 ++ include/openmc/chain.h | 9 ++ include/openmc/distribution_angle.h | 6 + include/openmc/distribution_multi.h | 19 +++ include/openmc/reaction_product.h | 15 ++ include/openmc/secondary_correlated.h | 16 ++ include/openmc/secondary_kalbach.h | 18 +++ include/openmc/secondary_nbody.h | 15 ++ include/openmc/secondary_thermal.h | 207 ++++++++++++++++++++++++ include/openmc/secondary_uncorrelated.h | 9 ++ include/openmc/thermal.h | 20 ++- src/chain.cpp | 7 + src/distribution_angle.cpp | 15 ++ src/distribution_multi.cpp | 25 ++- src/reaction_product.cpp | 29 ++-- src/secondary_correlated.cpp | 21 ++- src/secondary_kalbach.cpp | 22 ++- src/secondary_nbody.cpp | 28 +++- src/secondary_thermal.cpp | 150 ++++++++++++++--- src/secondary_uncorrelated.cpp | 18 +++ src/thermal.cpp | 20 ++- 21 files changed, 632 insertions(+), 51 deletions(-) diff --git a/include/openmc/angle_energy.h b/include/openmc/angle_energy.h index ac931b1b5..55deb5d41 100644 --- a/include/openmc/angle_energy.h +++ b/include/openmc/angle_energy.h @@ -14,8 +14,22 @@ namespace openmc { class AngleEnergy { public: + //! Sample an outgoing energy and scattering cosine + //! \param[in] E_in Incoming energy in [eV] + //! \param[out] E_out Outgoing energy in [eV] + //! \param[out] mu Outgoing cosine with respect to current direction + //! \param[inout] seed Pseudorandom seed pointer virtual void sample( double E_in, double& E_out, double& mu, uint64_t* seed) const = 0; + + //! Sample an outgoing energy and evaluate the angular PDF + //! \param[in] E_in Incoming energy in [eV] + //! \param[in] mu Scattering cosine with respect to current direction + //! \param[out] E_out Outgoing energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Probability density for the scattering cosine + virtual double sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const = 0; virtual ~AngleEnergy() = default; }; diff --git a/include/openmc/chain.h b/include/openmc/chain.h index a3bc6f3a3..6f5830358 100644 --- a/include/openmc/chain.h +++ b/include/openmc/chain.h @@ -71,6 +71,15 @@ public: void sample( double E_in, double& E_out, double& mu, uint64_t* seed) const override; + //! Sample an outgoing energy and evaluate the angular PDF + //! \param[in] E_in Incoming energy in [eV] + //! \param[in] mu Scattering cosine with respect to current direction + //! \param[out] E_out Outgoing energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Probability density for the scattering cosine + double sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const override; + private: const Distribution* photon_energy_; }; diff --git a/include/openmc/distribution_angle.h b/include/openmc/distribution_angle.h index efd4e5842..78de70c42 100644 --- a/include/openmc/distribution_angle.h +++ b/include/openmc/distribution_angle.h @@ -26,6 +26,12 @@ public: //! \return Cosine of the angle in the range [-1,1] double sample(double E, uint64_t* seed) const; + //! Evaluate the angular PDF at a given energy and cosine + //! \param[in] E Particle energy in [eV] + //! \param[in] mu Cosine of the scattering angle + //! \return Probability density for the scattering cosine + double evaluate(double E, double mu) const; + //! Determine whether angle distribution is empty //! \return Whether distribution is empty bool empty() const { return energy_.empty(); } diff --git a/include/openmc/distribution_multi.h b/include/openmc/distribution_multi.h index 7b9c2abf8..a72780737 100644 --- a/include/openmc/distribution_multi.h +++ b/include/openmc/distribution_multi.h @@ -6,6 +6,7 @@ #include "pugixml.hpp" #include "openmc/distribution.h" +#include "openmc/error.h" #include "openmc/position.h" namespace openmc { @@ -29,6 +30,14 @@ public: //! \return (sampled Direction, sample weight) virtual std::pair sample(uint64_t* seed) const = 0; + //! Evaluate the probability density for a given direction + //! \param[in] u Direction on the unit sphere + //! \return Probability density at the given direction + virtual double evaluate(Direction u) const + { + fatal_error("evaluate not available for this UnitSphereDistribution type"); + } + Direction u_ref_ {0.0, 0.0, 1.0}; //!< reference direction }; @@ -52,6 +61,11 @@ public: //! \return (sampled Direction, value of the PDF at this Direction) std::pair sample_as_bias(uint64_t* seed) const; + //! Evaluate the probability density for a given direction + //! \param[in] u Direction on the unit sphere + //! \return Probability density at the given direction + double evaluate(Direction u) const override; + // Observing pointers Distribution* mu() const { return mu_.get(); } Distribution* phi() const { return phi_.get(); } @@ -87,6 +101,11 @@ public: //! \return (sampled direction, sample weight) std::pair sample(uint64_t* seed) const override; + //! Evaluate the probability density for a given direction + //! \param[in] u Direction on the unit sphere + //! \return Probability density at the given direction + double evaluate(Direction u) const override; + // Set or get bias distribution void set_bias(std::unique_ptr bias) { diff --git a/include/openmc/reaction_product.h b/include/openmc/reaction_product.h index 9a8eab7d9..79a6160e2 100644 --- a/include/openmc/reaction_product.h +++ b/include/openmc/reaction_product.h @@ -49,6 +49,21 @@ public: //! \param[inout] seed Pseudorandom seed pointer void sample(double E_in, double& E_out, double& mu, uint64_t* seed) const; + //! Select which angle-energy distribution to sample + //! \param[in] E_in Incoming energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Reference to the selected angle-energy distribution + AngleEnergy& sample_dist(double E_in, uint64_t* seed) const; + + //! Sample an outgoing energy and evaluate the angular PDF + //! \param[in] E_in Incoming energy in [eV] + //! \param[in] mu Scattering cosine with respect to current direction + //! \param[out] E_out Outgoing energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Probability density for the scattering cosine + double sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const; + ParticleType particle_; //!< Particle type EmissionMode emission_mode_; //!< Emission mode double decay_rate_; //!< Decay rate (for delayed neutron precursors) in [1/s] diff --git a/include/openmc/secondary_correlated.h b/include/openmc/secondary_correlated.h index b4b7f8480..69b22981a 100644 --- a/include/openmc/secondary_correlated.h +++ b/include/openmc/secondary_correlated.h @@ -41,6 +41,22 @@ public: void sample( double E_in, double& E_out, double& mu, uint64_t* seed) const override; + //! Sample the outgoing energy and return the angular distribution + //! \param[in] E_in Incoming energy in [eV] + //! \param[out] E_out Outgoing energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Reference to the angular distribution at the sampled energy bin + Distribution& sample_dist(double E_in, double& E_out, uint64_t* seed) const; + + //! Sample an outgoing energy and evaluate the angular PDF + //! \param[in] E_in Incoming energy in [eV] + //! \param[in] mu Scattering cosine with respect to current direction + //! \param[out] E_out Outgoing energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Probability density for the scattering cosine + double sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const override; + // energy property vector& energy() { return energy_; } const vector& energy() const { return energy_; } diff --git a/include/openmc/secondary_kalbach.h b/include/openmc/secondary_kalbach.h index c9c5849bc..b25352be9 100644 --- a/include/openmc/secondary_kalbach.h +++ b/include/openmc/secondary_kalbach.h @@ -32,6 +32,24 @@ public: void sample( double E_in, double& E_out, double& mu, uint64_t* seed) const override; + //! Sample outgoing energy and Kalbach-Mann parameters + //! \param[in] E_in Incoming energy in [eV] + //! \param[out] E_out Outgoing energy in [eV] + //! \param[out] km_a Kalbach-Mann 'a' parameter + //! \param[out] km_r Kalbach-Mann pre-compound fraction 'r' + //! \param[inout] seed Pseudorandom seed pointer + void sample_params(double E_in, double& E_out, double& km_a, double& km_r, + uint64_t* seed) const; + + //! Sample an outgoing energy and evaluate the angular PDF + //! \param[in] E_in Incoming energy in [eV] + //! \param[in] mu Scattering cosine with respect to current direction + //! \param[out] E_out Outgoing energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Probability density for the scattering cosine + double sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const override; + private: //! Outgoing energy/angle at a single incoming energy struct KMTable { diff --git a/include/openmc/secondary_nbody.h b/include/openmc/secondary_nbody.h index efb4fd75b..9d033a6b8 100644 --- a/include/openmc/secondary_nbody.h +++ b/include/openmc/secondary_nbody.h @@ -28,6 +28,21 @@ public: void sample( double E_in, double& E_out, double& mu, uint64_t* seed) const override; + //! Sample an outgoing energy from the N-body phase space distribution + //! \param[in] E_in Incoming energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Sampled outgoing energy in [eV] + double sample_energy(double E_in, uint64_t* seed) const; + + //! Sample an outgoing energy and evaluate the angular PDF + //! \param[in] E_in Incoming energy in [eV] + //! \param[in] mu Scattering cosine with respect to current direction + //! \param[out] E_out Outgoing energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Probability density for the scattering cosine + double sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const override; + private: int n_bodies_; //!< Number of particles distributed double mass_ratio_; //!< Total mass of particles [neutron mass] diff --git a/include/openmc/secondary_thermal.h b/include/openmc/secondary_thermal.h index 4f33c0e76..45d2c5260 100644 --- a/include/openmc/secondary_thermal.h +++ b/include/openmc/secondary_thermal.h @@ -6,6 +6,7 @@ #include "openmc/angle_energy.h" #include "openmc/endf.h" +#include "openmc/search.h" #include "openmc/secondary_correlated.h" #include "openmc/vector.h" @@ -33,8 +34,20 @@ public: void sample( double E_in, double& E_out, double& mu, uint64_t* seed) const override; + //! Sample an outgoing energy and evaluate the angular PDF + //! \param[in] E_in Incoming energy in [eV] + //! \param[in] mu Scattering cosine with respect to current direction + //! \param[out] E_out Outgoing energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Probability density for the scattering cosine + double sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const override; + private: const CoherentElasticXS& xs_; //!< Coherent elastic scattering cross section + tensor::Tensor bragg_edges_; //!< Copy of Bragg edges for slicing + tensor::Tensor + factors_diff_; //!< Differences over elastic scattering factors }; //============================================================================== @@ -56,6 +69,15 @@ public: void sample( double E_in, double& E_out, double& mu, uint64_t* seed) const override; + //! Sample an outgoing energy and evaluate the angular PDF + //! \param[in] E_in Incoming energy in [eV] + //! \param[in] mu Scattering cosine with respect to current direction + //! \param[out] E_out Outgoing energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Probability density for the scattering cosine + double sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const override; + private: double debye_waller_; }; @@ -81,6 +103,15 @@ public: void sample( double E_in, double& E_out, double& mu, uint64_t* seed) const override; + //! Sample an outgoing energy and evaluate the angular PDF + //! \param[in] E_in Incoming energy in [eV] + //! \param[in] mu Scattering cosine with respect to current direction + //! \param[out] E_out Outgoing energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Probability density for the scattering cosine + double sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const override; + private: const vector& energy_; //!< Energies at which cosines are tabulated tensor::Tensor mu_out_; //!< Cosines for each incident energy @@ -106,6 +137,21 @@ public: //! \param[inout] seed Pseudorandom number seed pointer void sample( double E_in, double& E_out, double& mu, uint64_t* seed) const override; + //! Sample outgoing energy bin parameters + //! \param[in] E_in Incoming energy in [eV] + //! \param[out] E_out Outgoing energy in [eV] + //! \param[out] j Sampled outgoing energy bin index + //! \param[inout] seed Pseudorandom seed pointer + void sample_params(double E_in, double& E_out, int& j, uint64_t* seed) const; + + //! Sample an outgoing energy and evaluate the angular PDF + //! \param[in] E_in Incoming energy in [eV] + //! \param[in] mu Scattering cosine with respect to current direction + //! \param[out] E_out Outgoing energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Probability density for the scattering cosine + double sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const override; private: const vector& energy_; //!< Incident energies @@ -135,6 +181,25 @@ public: void sample( double E_in, double& E_out, double& mu, uint64_t* seed) const override; + //! Sample outgoing energy bin parameters + //! \param[in] E_in Incoming energy in [eV] + //! \param[out] E_out Outgoing energy in [eV] + //! \param[out] f Interpolation factor within sampled energy bin + //! \param[out] l Index of the closer incident energy + //! \param[out] j Sampled outgoing energy bin index + //! \param[inout] seed Pseudorandom seed pointer + void sample_params(double E_in, double& E_out, double& f, int& l, int& j, + uint64_t* seed) const; + + //! Sample an outgoing energy and evaluate the angular PDF + //! \param[in] E_in Incoming energy in [eV] + //! \param[in] mu Scattering cosine with respect to current direction + //! \param[out] E_out Outgoing energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Probability density for the scattering cosine + double sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const override; + private: //! Secondary energy/angle distribution struct DistEnergySab { @@ -170,6 +235,21 @@ public: void sample( double E_in, double& E_out, double& mu, uint64_t* seed) const override; + //! Select the coherent or incoherent elastic distribution to sample + //! \param[in] E_in Incoming energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Reference to the selected angle-energy distribution + const AngleEnergy& sample_dist(double E_in, uint64_t* seed) const; + + //! Sample an outgoing energy and evaluate the angular PDF + //! \param[in] E_in Incoming energy in [eV] + //! \param[in] mu Scattering cosine with respect to current direction + //! \param[out] E_out Outgoing energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Probability density for the scattering cosine + double sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const override; + private: CoherentElasticAE coherent_dist_; //!< Coherent distribution unique_ptr incoherent_dist_; //!< Incoherent distribution @@ -178,6 +258,133 @@ private: const Function1D& incoherent_xs_; //!< Polymorphic ref. to incoherent XS }; +//! Internal helper for evaluating a piecewise-constant PDF on discrete points. +//! +//! The underlying discrete points are represented implicitly through a +//! monotonically increasing `center(i)` function and corresponding per-point +//! `weight(i)` values. Each point contributes a rectangular bin whose +//! half-width is half the distance to its nearest neighboring center. +//! +//! \tparam CenterFn Callable returning the location of the i-th discrete value +//! \tparam WeightFn Callable returning the weight of the i-th discrete value +//! \param[in] n Number of discrete values +//! \param[in] mu_0 Point at which to evaluate the PDF +//! \param[in] a Lower bound of the domain (default: -1) +//! \param[in] b Upper bound of the domain (default: 1) +//! \return Probability density at mu_0 +template +double get_pdf_discrete_impl(std::size_t n, double mu_0, double a, double b, + CenterFn center, WeightFn weight) +{ + if (n == 0 || mu_0 < a || mu_0 > b) + return 0.0; + + auto evaluate_bin = [&](std::size_t i) { + double x = center(i); + double left_span = (i == 0) ? 2.0 * (x - a) : x - center(i - 1); + double right_span = (i + 1 == n) ? 2.0 * (b - x) : center(i + 1) - x; + double delta = 0.5 * std::min(left_span, right_span); + if (delta <= 0.0) + return 0.0; + + double left = x - delta; + double right = x + delta; + bool in_bin = + (mu_0 >= left) && ((i + 1 == n) ? (mu_0 <= right) : (mu_0 < right)); + return in_bin ? weight(i) / (2.0 * delta) : 0.0; + }; + + // This is effectively a lower_bound over the sequence center(i), but the + // sequence is implicit rather than stored in a container, so the STL + // algorithms can not be used. + std::size_t low = 0; + std::size_t high = n; + while (low < high) { + std::size_t mid = low + (high - low) / 2; + if (center(mid) < mu_0) { + low = mid + 1; + } else { + high = mid; + } + } + + if (low < n) { + double pdf = evaluate_bin(low); + if (pdf > 0.0) + return pdf; + } + if (low > 0) + return evaluate_bin(low - 1); + return 0.0; +} + +//! Evaluate the PDF of a weighted discrete distribution at a given point. +//! +//! Given a set of discrete values mu[i] with weights w[i], this function +//! computes the probability density at mu_0 by treating each discrete value +//! as a rectangular bin. The bin half-width around each discrete value is +//! half the distance to its nearest neighbor. +//! +//! \tparam T1 Container type for discrete cosine values (must support +//! operator[], size()) +//! \tparam T2 Container type for weights (must support operator[]) +//! \param[in] mu Sorted array of discrete cosine values +//! \param[in] w Weights for each discrete value (need not be normalized) +//! \param[in] mu_0 Point at which to evaluate the PDF +//! \param[in] a Lower bound of the domain (default: -1) +//! \param[in] b Upper bound of the domain (default: 1) +//! \return Probability density at mu_0 +template +double get_pdf_discrete( + const T1 mu, const T2& w, double mu_0, double a = -1.0, double b = 1.0) +{ + // Returns the location of the discrete value for a given index + auto center = [&](std::size_t i) { return mu[i]; }; + auto weight = [&](std::size_t i) { return w[i]; }; + return get_pdf_discrete_impl(mu.size(), mu_0, a, b, center, weight); +} + +//! Evaluate the PDF of a discrete distribution with uniform weights +//! +//! \tparam T1 Container type for discrete cosine values +//! \param[in] mu Sorted array of discrete cosine values +//! \param[in] mu_0 Point at which to evaluate the PDF +//! \param[in] a Lower bound of the domain (default: -1) +//! \param[in] b Upper bound of the domain (default: 1) +//! \return Probability density at mu_0 +template +double get_pdf_discrete( + const T1 mu, double mu_0, double a = -1.0, double b = 1.0) +{ + auto center = [&](std::size_t i) { return mu[i]; }; + auto weight = [&](std::size_t i) { return 1.0 / mu.size(); }; + return get_pdf_discrete_impl(mu.size(), mu_0, a, b, center, weight); +} + +//! Evaluate the PDF of a uniformly weighted distribution on interpolated points +//! +//! \tparam T1 Container type for the lower tabulated cosine values +//! \tparam T2 Container type for the upper tabulated cosine values +//! \param[in] mu0 Sorted array of discrete cosine values at the lower grid +//! \param[in] mu1 Sorted array of discrete cosine values at the upper grid +//! \param[in] f Interpolation factor between mu0 and mu1 +//! \param[in] mu_0 Point at which to evaluate the PDF +//! \param[in] a Lower bound of the domain (default: -1) +//! \param[in] b Upper bound of the domain (default: 1) +//! \return Probability density at mu_0 +template +double get_pdf_discrete_interpolated(const T1 mu0, const T2 mu1, double f, + double mu_0, double a = -1.0, double b = 1.0) +{ + if (mu0.size() != mu1.size()) + return 0.0; + + // Returns interpolated discrete value for a given index + auto center = [&](std::size_t i) { return mu0[i] + f * (mu1[i] - mu0[i]); }; + auto weight = [&](std::size_t i) { return 1.0 / mu0.size(); }; + return get_pdf_discrete_impl(mu0.size(), mu_0, a, b, center, weight); +} + } // namespace openmc #endif // OPENMC_SECONDARY_THERMAL_H diff --git a/include/openmc/secondary_uncorrelated.h b/include/openmc/secondary_uncorrelated.h index 3afa3d9ce..f895ae77f 100644 --- a/include/openmc/secondary_uncorrelated.h +++ b/include/openmc/secondary_uncorrelated.h @@ -32,6 +32,15 @@ public: void sample( double E_in, double& E_out, double& mu, uint64_t* seed) const override; + //! Sample an outgoing energy and evaluate the angular PDF + //! \param[in] E_in Incoming energy in [eV] + //! \param[in] mu Scattering cosine with respect to current direction + //! \param[out] E_out Outgoing energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Probability density for the scattering cosine + double sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const override; + // Accessors AngleDistribution& angle() { return angle_; } diff --git a/include/openmc/thermal.h b/include/openmc/thermal.h index 86254b923..c06a2ee0d 100644 --- a/include/openmc/thermal.h +++ b/include/openmc/thermal.h @@ -51,7 +51,25 @@ public: //! \param[out] mu Outgoing scattering angle cosine //! \param[inout] seed Pseudorandom seed pointer void sample(const NuclideMicroXS& micro_xs, double E_in, double* E_out, - double* mu, uint64_t* seed); + double* mu, uint64_t* seed) const; + + //! Select the elastic or inelastic distribution to sample + //! \param[in] micro_xs Microscopic cross sections + //! \param[in] E Incident neutron energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Reference to the selected angle-energy distribution + AngleEnergy& sample_dist( + const NuclideMicroXS& micro_xs, double E, uint64_t* seed) const; + + //! Sample an outgoing energy and evaluate the angular PDF + //! \param[in] micro_xs Microscopic cross sections + //! \param[in] E_in Incoming energy in [eV] + //! \param[in] mu Scattering cosine with respect to current direction + //! \param[out] E_out Outgoing energy in [eV] + //! \param[inout] seed Pseudorandom seed pointer + //! \return Probability density for the scattering cosine + double sample_energy_and_pdf(const NuclideMicroXS& micro_xs, double E_in, + double mu, double& E_out, uint64_t* seed) const; private: struct Reaction { diff --git a/src/chain.cpp b/src/chain.cpp index 4214bc473..e4d0324d3 100644 --- a/src/chain.cpp +++ b/src/chain.cpp @@ -74,6 +74,13 @@ void DecayPhotonAngleEnergy::sample( mu = Uniform(-1., 1.).sample(seed).first; } +double DecayPhotonAngleEnergy::sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const +{ + E_out = photon_energy_->sample(seed).first; + return 0.5; +} + //============================================================================== // Global variables //============================================================================== diff --git a/src/distribution_angle.cpp b/src/distribution_angle.cpp index 3433f269e..ecb5961f6 100644 --- a/src/distribution_angle.cpp +++ b/src/distribution_angle.cpp @@ -82,4 +82,19 @@ double AngleDistribution::sample(double E, uint64_t* seed) const return mu; } +double AngleDistribution::evaluate(double E, double mu) const +{ + // Find energy bin and calculate interpolation factor + int i; + double r; + get_energy_index(energy_, E, i, r); + + double pdf = 0.0; + if (r > 0.0) + pdf += r * distribution_[i + 1]->evaluate(mu); + if (r < 1.0) + pdf += (1.0 - r) * distribution_[i]->evaluate(mu); + return pdf; +} + } // namespace openmc diff --git a/src/distribution_multi.cpp b/src/distribution_multi.cpp index 857e1c30b..47785649b 100644 --- a/src/distribution_multi.cpp +++ b/src/distribution_multi.cpp @@ -1,6 +1,6 @@ #include "openmc/distribution_multi.h" -#include // for move +#include // for move, clamp #include // for sqrt, sin, cos, max #include "openmc/constants.h" @@ -44,6 +44,7 @@ UnitSphereDistribution::UnitSphereDistribution(pugi::xml_node node) fatal_error("Angular distribution reference direction must have " "three parameters specified."); u_ref_ = Direction(u_ref.data()); + u_ref_ /= u_ref_.norm(); } } @@ -65,6 +66,7 @@ PolarAzimuthal::PolarAzimuthal(pugi::xml_node node) fatal_error("Angular distribution reference v direction must have " "three parameters specified."); v_ref_ = Direction(v_ref.data()); + v_ref_ /= v_ref_.norm(); } w_ref_ = u_ref_.cross(v_ref_); if (check_for_node(node, "mu")) { @@ -116,6 +118,22 @@ std::pair PolarAzimuthal::sample_impl( weight}; } +double PolarAzimuthal::evaluate(Direction u) const +{ + double mu = std::clamp(u.dot(u_ref_), -1.0, 1.0); + double phi = 0.0; + double sin_theta_sq = std::max(0.0, 1.0 - mu * mu); + if (sin_theta_sq > 0.0) { + double sin_theta = std::sqrt(sin_theta_sq); + double cos_phi = u.dot(v_ref_) / sin_theta; + double sin_phi = u.dot(w_ref_) / sin_theta; + phi = std::atan2(sin_phi, cos_phi); + if (phi < 0.0) + phi += 2.0 * PI; + } + return mu_->evaluate(mu) * phi_->evaluate(phi); +} + //============================================================================== // Isotropic implementation //============================================================================== @@ -157,6 +175,11 @@ std::pair Isotropic::sample(uint64_t* seed) const } } +double Isotropic::evaluate(Direction u) const +{ + return 1.0 / (4.0 * PI); +} + //============================================================================== // Monodirectional implementation //============================================================================== diff --git a/src/reaction_product.cpp b/src/reaction_product.cpp index ee560d607..a1c937861 100644 --- a/src/reaction_product.cpp +++ b/src/reaction_product.cpp @@ -1,5 +1,6 @@ #include "openmc/reaction_product.h" +#include #include // for string #include @@ -106,9 +107,10 @@ ReactionProduct::ReactionProduct(const ChainNuclide::Product& product) make_unique(chain_nuc->photon_energy())); } -void ReactionProduct::sample( - double E_in, double& E_out, double& mu, uint64_t* seed) const +AngleEnergy& ReactionProduct::sample_dist(double E_in, uint64_t* seed) const { + assert(!distribution_.empty()); + auto n = applicability_.size(); if (n > 1) { double prob = 0.0; @@ -118,15 +120,24 @@ void ReactionProduct::sample( prob += applicability_[i](E_in); // If i-th distribution is sampled, sample energy from the distribution - if (c <= prob) { - distribution_[i]->sample(E_in, E_out, mu, seed); - break; - } + if (c <= prob) + return *distribution_[i]; } - } else { - // If only one distribution is present, go ahead and sample it - distribution_[0]->sample(E_in, E_out, mu, seed); } + + return *distribution_.back(); +} + +void ReactionProduct::sample( + double E_in, double& E_out, double& mu, uint64_t* seed) const +{ + sample_dist(E_in, seed).sample(E_in, E_out, mu, seed); +} + +double ReactionProduct::sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const +{ + return sample_dist(E_in, seed).sample_energy_and_pdf(E_in, mu, E_out, seed); } } // namespace openmc diff --git a/src/secondary_correlated.cpp b/src/secondary_correlated.cpp index cc0ab8af1..32701791a 100644 --- a/src/secondary_correlated.cpp +++ b/src/secondary_correlated.cpp @@ -155,9 +155,8 @@ CorrelatedAngleEnergy::CorrelatedAngleEnergy(hid_t group) distribution_.push_back(std::move(d)); } // incoming energies } - -void CorrelatedAngleEnergy::sample( - double E_in, double& E_out, double& mu, uint64_t* seed) const +Distribution& CorrelatedAngleEnergy::sample_dist( + double E_in, double& E_out, uint64_t* seed) const { // Find energy bin and calculate interpolation factor int i; @@ -249,10 +248,22 @@ void CorrelatedAngleEnergy::sample( // Find correlated angular distribution for closest outgoing energy bin if (r1 - c_k < c_k1 - r1 || distribution_[l].interpolation == Interpolation::histogram) { - mu = distribution_[l].angle[k]->sample(seed).first; + return *distribution_[l].angle[k]; } else { - mu = distribution_[l].angle[k + 1]->sample(seed).first; + return *distribution_[l].angle[k + 1]; } } +void CorrelatedAngleEnergy::sample( + double E_in, double& E_out, double& mu, uint64_t* seed) const +{ + mu = sample_dist(E_in, E_out, seed).sample(seed).first; +} + +double CorrelatedAngleEnergy::sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const +{ + return sample_dist(E_in, E_out, seed).evaluate(mu); +} + } // namespace openmc diff --git a/src/secondary_kalbach.cpp b/src/secondary_kalbach.cpp index 8470c8c18..018ce1c8a 100644 --- a/src/secondary_kalbach.cpp +++ b/src/secondary_kalbach.cpp @@ -114,8 +114,8 @@ KalbachMann::KalbachMann(hid_t group) } // incoming energies } -void KalbachMann::sample( - double E_in, double& E_out, double& mu, uint64_t* seed) const +void KalbachMann::sample_params( + double E_in, double& E_out, double& km_a, double& km_r, uint64_t* seed) const { // Find energy bin and calculate interpolation factor int i; @@ -170,7 +170,6 @@ void KalbachMann::sample( double E_l_k = distribution_[l].e_out[k]; double p_l_k = distribution_[l].p[k]; - double km_r, km_a; if (distribution_[l].interpolation == Interpolation::histogram) { // Histogram interpolation if (p_l_k > 0.0 && k >= n_discrete) { @@ -216,6 +215,13 @@ void KalbachMann::sample( E_out = E_1 + (E_out - E_i1_1) * (E_K - E_1) / (E_i1_K - E_i1_1); } } +} + +void KalbachMann::sample( + double E_in, double& E_out, double& mu, uint64_t* seed) const +{ + double km_r, km_a; + sample_params(E_in, E_out, km_a, km_r, seed); // Sampled correlated angle from Kalbach-Mann parameters if (prn(seed) > km_r) { @@ -226,5 +232,15 @@ void KalbachMann::sample( mu = std::log(r1 * std::exp(km_a) + (1.0 - r1) * std::exp(-km_a)) / km_a; } } +double KalbachMann::sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const +{ + double km_r, km_a; + sample_params(E_in, E_out, km_a, km_r, seed); + + // https://docs.openmc.org/en/latest/methods/neutron_physics.html#equation-KM-pdf-angle + return km_a / (2 * std::sinh(km_a)) * + (std::cosh(km_a * mu) + km_r * std::sinh(km_a * mu)); +} } // namespace openmc diff --git a/src/secondary_nbody.cpp b/src/secondary_nbody.cpp index da0bb81c4..72f0b0b92 100644 --- a/src/secondary_nbody.cpp +++ b/src/secondary_nbody.cpp @@ -22,13 +22,8 @@ NBodyPhaseSpace::NBodyPhaseSpace(hid_t group) read_attribute(group, "q_value", Q_); } -void NBodyPhaseSpace::sample( - double E_in, double& E_out, double& mu, uint64_t* seed) const +double NBodyPhaseSpace::sample_energy(double E_in, uint64_t* seed) const { - // By definition, the distribution of the angle is isotropic for an N-body - // phase space distribution - mu = uniform_distribution(-1., 1., seed); - // Determine E_max parameter double Ap = mass_ratio_; double E_max = (Ap - 1.0) / Ap * (A_ / (A_ + 1.0) * E_in + Q_); @@ -59,12 +54,29 @@ void NBodyPhaseSpace::sample( std::log(r5) * std::pow(std::cos(PI / 2.0 * r6), 2); break; default: - throw std::runtime_error {"N-body phase space with >5 bodies."}; + fatal_error("N-body phase space with >5 bodies."); } // Now determine v and E_out double v = x / (x + y); - E_out = E_max * v; + return E_max * v; +} + +void NBodyPhaseSpace::sample( + double E_in, double& E_out, double& mu, uint64_t* seed) const +{ + // By definition, the distribution of the angle is isotropic for an N-body + // phase space distribution + mu = uniform_distribution(-1., 1., seed); + + E_out = sample_energy(E_in, seed); +} + +double NBodyPhaseSpace::sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const +{ + E_out = sample_energy(E_in, seed); + return 0.5; } } // namespace openmc diff --git a/src/secondary_thermal.cpp b/src/secondary_thermal.cpp index 2ab7d8a63..b0f601809 100644 --- a/src/secondary_thermal.cpp +++ b/src/secondary_thermal.cpp @@ -4,6 +4,7 @@ #include "openmc/math_functions.h" #include "openmc/random_lcg.h" #include "openmc/search.h" +#include "openmc/vector.h" #include "openmc/tensor.h" @@ -16,16 +17,26 @@ namespace openmc { // CoherentElasticAE implementation //============================================================================== -CoherentElasticAE::CoherentElasticAE(const CoherentElasticXS& xs) : xs_ {xs} {} +CoherentElasticAE::CoherentElasticAE(const CoherentElasticXS& xs) : xs_ {xs} +{ + const auto& bragg = xs_.bragg_edges(); + auto n = bragg.size(); + bragg_edges_ = tensor::Tensor(bragg.data(), n); + + const auto& factors = xs_.factors(); + factors_diff_ = tensor::zeros({n}); + factors_diff_.slice(0) = factors[0]; + for (int i = 1; i < n; ++i) { + factors_diff_.slice(i) = factors[i] - factors[i - 1]; + } +} void CoherentElasticAE::sample( double E_in, double& E_out, double& mu, uint64_t* seed) const { // Energy doesn't change in elastic scattering (ENDF-102, Eq. 7-1) E_out = E_in; - const auto& energies {xs_.bragg_edges()}; - assert(E_in >= energies.front()); const int i = lower_bound_index(energies.begin(), energies.end(), E_in); @@ -42,6 +53,25 @@ void CoherentElasticAE::sample( mu = 1.0 - 2.0 * energies[k] / E_in; } +double CoherentElasticAE::sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const +{ + // Energy doesn't change in elastic scattering (ENDF-102, Eq. 7-1) + E_out = E_in; + const auto& factors = xs_.factors(); + + if (E_in < bragg_edges_.front()) + return 0.0; + + const int i = + lower_bound_index(bragg_edges_.begin(), bragg_edges_.end(), E_in); + double E = 0.5 * (1 - mu) * E_in; + double C = 0.5 * E_in / factors[i]; + + return C * get_pdf_discrete(bragg_edges_.slice(tensor::range(i + 1)), + factors_diff_.slice(tensor::range(i + 1)), E, 0.0, E_in); +} + //============================================================================== // IncoherentElasticAE implementation //============================================================================== @@ -54,12 +84,21 @@ IncoherentElasticAE::IncoherentElasticAE(hid_t group) void IncoherentElasticAE::sample( double E_in, double& E_out, double& mu, uint64_t* seed) const { + E_out = E_in; + // Sample angle by inverting the distribution in ENDF-102, Eq. 7.4 double c = 2 * E_in * debye_waller_; mu = std::log(1.0 + prn(seed) * (std::exp(2.0 * c) - 1)) / c - 1.0; - - // Energy doesn't change in elastic scattering (ENDF-102, Eq. 7.4) +} +double IncoherentElasticAE::sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const +{ E_out = E_in; + + // Sample angle by inverting the distribution in ENDF-102, Eq. 7.4 + double c = 2 * E_in * debye_waller_; + double A = c / (1 - std::exp(-2.0 * c)); // normalization factor + return A * std::exp(-c * (1 - mu)); } //============================================================================== @@ -116,6 +155,20 @@ void IncoherentElasticAEDiscrete::sample( E_out = E_in; } +double IncoherentElasticAEDiscrete::sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const +{ + // Get index and interpolation factor for elastic grid + int i; + double f; + get_energy_index(energy_, E_in, i, f); + // Energy doesn't change in elastic scattering + E_out = E_in; + + return get_pdf_discrete_interpolated( + mu_out_.slice(i, tensor::all), mu_out_.slice(i + 1, tensor::all), f, mu); +} + //============================================================================== // IncoherentInelasticAEDiscrete implementation //============================================================================== @@ -129,8 +182,8 @@ IncoherentInelasticAEDiscrete::IncoherentInelasticAEDiscrete( read_dataset(group, "skewed", skewed_); } -void IncoherentInelasticAEDiscrete::sample( - double E_in, double& E_out, double& mu, uint64_t* seed) const +void IncoherentInelasticAEDiscrete::sample_params( + double E_in, double& E_out, int& j, uint64_t* seed) const { // Get index and interpolation factor for inelastic grid int i; @@ -144,7 +197,6 @@ void IncoherentInelasticAEDiscrete::sample( // for the second and second to last bins, relative to a normal bin // probability of 1). Otherwise, each bin is equally probable. - int j; int n = energy_out_.shape(1); if (!skewed_) { // All bins equally likely @@ -176,6 +228,18 @@ void IncoherentInelasticAEDiscrete::sample( // Outgoing energy E_out = (1 - f) * E_ij + f * E_i1j; +} + +void IncoherentInelasticAEDiscrete::sample( + double E_in, double& E_out, double& mu, uint64_t* seed) const +{ + // Get index and interpolation factor for inelastic grid + int i; + double f; + get_energy_index(energy_, E_in, i, f); + + int j; + sample_params(E_in, E_out, j, seed); // Sample outgoing cosine bin int m = mu_out_.shape(2); @@ -189,6 +253,20 @@ void IncoherentInelasticAEDiscrete::sample( mu = (1 - f) * mu_ijk + f * mu_i1jk; } +double IncoherentInelasticAEDiscrete::sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const +{ + // Get index and interpolation factor for inelastic grid + int i; + double f; + get_energy_index(energy_, E_in, i, f); + int j; + sample_params(E_in, E_out, j, seed); + + return get_pdf_discrete_interpolated(mu_out_.slice(i, j, tensor::all), + mu_out_.slice(i + 1, j, tensor::all), f, mu); +} + //============================================================================== // IncoherentInelasticAE implementation //============================================================================== @@ -231,24 +309,23 @@ IncoherentInelasticAE::IncoherentInelasticAE(hid_t group) } } -void IncoherentInelasticAE::sample( - double E_in, double& E_out, double& mu, uint64_t* seed) const +void IncoherentInelasticAE::sample_params( + double E_in, double& E_out, double& f, int& l, int& j, uint64_t* seed) const { // Get index and interpolation factor for inelastic grid int i; - double f; - get_energy_index(energy_, E_in, i, f); + double f0; + get_energy_index(energy_, E_in, i, f0); // Pick closer energy based on interpolation factor - int l = f > 0.5 ? i + 1 : i; + l = f0 > 0.5 ? i + 1 : i; // Determine outgoing energy bin // (First reset n_energy_out to the right value) - auto n = distribution_[l].n_e_out; + int n = distribution_[l].n_e_out; double r1 = prn(seed); double c_j = distribution_[l].e_out_cdf[0]; double c_j1; - std::size_t j; for (j = 0; j < n - 1; ++j) { c_j1 = distribution_[l].e_out_cdf[j + 1]; if (r1 < c_j1) @@ -286,6 +363,15 @@ void IncoherentInelasticAE::sample( E_out += E_in - E_l; } + f = (r1 - c_j) / (c_j1 - c_j); +} +void IncoherentInelasticAE::sample( + double E_in, double& E_out, double& mu, uint64_t* seed) const +{ + double f; + int l, j; + sample_params(E_in, E_out, f, l, j, seed); + // Sample outgoing cosine bin int n_mu = distribution_[l].mu.shape(1); std::size_t k = prn(seed) * n_mu; @@ -294,7 +380,6 @@ void IncoherentInelasticAE::sample( // a bin of width 0.5*min(mu[k] - mu[k-1], mu[k+1] - mu[k]) centered on the // discrete mu value itself. const auto& mu_l = distribution_[l].mu; - f = (r1 - c_j) / (c_j1 - c_j); // Interpolate kth mu value between distributions at energies j and j+1 mu = mu_l(j, k) + f * (mu_l(j + 1, k) - mu_l(j, k)); @@ -318,6 +403,19 @@ void IncoherentInelasticAE::sample( mu += std::min(mu - mu_left, mu_right - mu) * (prn(seed) - 0.5); } +double IncoherentInelasticAE::sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const +{ + double f; + int l, j; + sample_params(E_in, E_out, f, l, j, seed); + + const auto& mu_l = distribution_[l].mu; + + return get_pdf_discrete_interpolated( + mu_l.slice(j, tensor::all), mu_l.slice(j + 1, tensor::all), f, mu); +} + //============================================================================== // MixedElasticAE implementation //============================================================================== @@ -340,18 +438,30 @@ MixedElasticAE::MixedElasticAE( close_group(incoherent_group); } -void MixedElasticAE::sample( - double E_in, double& E_out, double& mu, uint64_t* seed) const +const AngleEnergy& MixedElasticAE::sample_dist( + double E_in, uint64_t* seed) const { // Evaluate coherent and incoherent elastic cross sections double xs_coh = coherent_xs_(E_in); double xs_incoh = incoherent_xs_(E_in); if (prn(seed) * (xs_coh + xs_incoh) < xs_coh) { - coherent_dist_.sample(E_in, E_out, mu, seed); + return coherent_dist_; } else { - incoherent_dist_->sample(E_in, E_out, mu, seed); + return *incoherent_dist_; } } +void MixedElasticAE::sample( + double E_in, double& E_out, double& mu, uint64_t* seed) const +{ + sample_dist(E_in, seed).sample(E_in, E_out, mu, seed); +} + +double MixedElasticAE::sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const +{ + return sample_dist(E_in, seed).sample_energy_and_pdf(E_in, mu, E_out, seed); +} + } // namespace openmc diff --git a/src/secondary_uncorrelated.cpp b/src/secondary_uncorrelated.cpp index 5cbb76fb9..ec2af7102 100644 --- a/src/secondary_uncorrelated.cpp +++ b/src/secondary_uncorrelated.cpp @@ -65,4 +65,22 @@ void UncorrelatedAngleEnergy::sample( E_out = energy_->sample(E_in, seed); } +double UncorrelatedAngleEnergy::sample_energy_and_pdf( + double E_in, double mu, double& E_out, uint64_t* seed) const +{ + // Sample outgoing energy + if (energy_ != nullptr) { + E_out = energy_->sample(E_in, seed); + } else { + E_out = E_in; + } + + if (!angle_.empty()) { + return angle_.evaluate(E_in, mu); + } else { + // no angle distribution given => assume isotropic for all energies + return 0.5; + } +} + } // namespace openmc diff --git a/src/thermal.cpp b/src/thermal.cpp index 6ed59f686..edfbddf23 100644 --- a/src/thermal.cpp +++ b/src/thermal.cpp @@ -291,16 +291,21 @@ void ThermalData::calculate_xs( *inelastic = (*inelastic_.xs)(E); } -void ThermalData::sample(const NuclideMicroXS& micro_xs, double E, - double* E_out, double* mu, uint64_t* seed) +AngleEnergy& ThermalData::sample_dist( + const NuclideMicroXS& micro_xs, double E, uint64_t* seed) const { // Determine whether inelastic or elastic scattering will occur if (prn(seed) < micro_xs.thermal_elastic / micro_xs.thermal) { - elastic_.distribution->sample(E, *E_out, *mu, seed); + return *elastic_.distribution; } else { - inelastic_.distribution->sample(E, *E_out, *mu, seed); + return *inelastic_.distribution; } +} +void ThermalData::sample(const NuclideMicroXS& micro_xs, double E, + double* E_out, double* mu, uint64_t* seed) const +{ + sample_dist(micro_xs, E, seed).sample(E, *E_out, *mu, seed); // Because of floating-point roundoff, it may be possible for mu to be // outside of the range [-1,1). In these cases, we just set mu to exactly // -1 or 1 @@ -308,6 +313,13 @@ void ThermalData::sample(const NuclideMicroXS& micro_xs, double E, *mu = std::copysign(1.0, *mu); } +double ThermalData::sample_energy_and_pdf(const NuclideMicroXS& micro_xs, + double E_in, double mu, double& E_out, uint64_t* seed) const +{ + return sample_dist(micro_xs, E_in, seed) + .sample_energy_and_pdf(E_in, mu, E_out, seed); +} + void free_memory_thermal() { data::thermal_scatt.clear(); From 3ce6cbfddae13c522e31ca56711b772a7dc769a1 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 17 Mar 2026 16:10:18 -0500 Subject: [PATCH 595/671] Add `fusion_neutron_spectrum` to openmc.stats module (#3862) --- docs/source/pythonapi/stats.rst | 1 + openmc/stats/univariate.py | 136 +++++++++++++++++++++++++++++++- tests/unit_tests/test_stats.py | 83 +++++++++++++++++++ 3 files changed, 218 insertions(+), 2 deletions(-) diff --git a/docs/source/pythonapi/stats.rst b/docs/source/pythonapi/stats.rst index e0ae74e39..203d4fea4 100644 --- a/docs/source/pythonapi/stats.rst +++ b/docs/source/pythonapi/stats.rst @@ -29,6 +29,7 @@ Univariate Probability Distributions :template: myfunction.rst openmc.stats.delta_function + openmc.stats.fusion_neutron_spectrum openmc.stats.muir Angular Distributions diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index a70737a34..1cf9a1ad5 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -2,8 +2,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from collections import defaultdict from collections.abc import Iterable, Sequence -from copy import deepcopy -from math import sqrt, pi, exp +from math import sqrt, pi, exp, log from numbers import Real from warnings import warn @@ -14,6 +13,7 @@ from scipy.special import exprel, hyp1f1, lambertw import scipy import openmc.checkvalue as cv +from openmc.data import atomic_mass, NEUTRON_MASS from .._xml import get_elem_list, get_text from ..mixin import EqualityMixin @@ -1277,6 +1277,138 @@ def Muir(*args, **kwargs): return muir(*args, **kwargs) +def fusion_neutron_spectrum( + ion_temp: float, + reactants: str = 'DD', + bias: Univariate | None = None +) -> Normal: + r"""Return a Gaussian energy distribution for fusion neutron emission. + + Computes the mean energy and spectral width of the neutron energy spectrum + from thermonuclear fusion reactions in a plasma with Maxwellian ion velocity + distributions. The mean neutron energy is calculated as + + .. math:: + + \langle E_n \rangle = E_0 + \Delta E_\text{th} + + where :math:`E_0` is the neutron energy at zero ion temperature and + :math:`\Delta E_\text{th}` is the thermal peak shift due to the motion of + the reacting ions. The spectral width is characterized by the FWHM: + + .. math:: + + W_{1/2} = \omega_0 (1 + \delta_\omega) \sqrt{T_i} + + where :math:`\omega_0` is the width at the :math:`T_i \to 0` limit and + :math:`\delta_\omega` is a temperature-dependent correction term. Both + :math:`\Delta E_\text{th}` and :math:`\delta_\omega` are evaluated using + interpolation formulas from `Ballabio et al. + `_: Table III for :math:`0 < + T_i \le 40` keV and Table IV for :math:`40 < T_i < 100` keV. The returned + distribution is a normal (Gaussian) approximation to the spectrum. + + .. versionadded:: 0.15.4 + + Parameters + ---------- + ion_temp : float + Ion temperature of the plasma in [eV]. + reactants : {'DD', 'DT'} + Fusion reactants. 'DD' corresponds to the D(d,n)\ :sup:`3`\ He reaction + and 'DT' to the T(d,n)\ :math:`\alpha` reaction. + bias : openmc.stats.Univariate, optional + Distribution for biased sampling. + + Returns + ------- + openmc.stats.Normal + Normal distribution with mean and standard deviation corresponding to + the first and second moments of the fusion neutron energy spectrum. Both + the mean and standard deviation are in [eV]. + + """ + if ion_temp < 0.0 or ion_temp > 100e3: + raise ValueError("Ion temperature must be between 0 and 100 keV.") + + # Formulas from doi:10.1088/0029-5515/38/11/310 + mn = NEUTRON_MASS + md = atomic_mass('H2') + ev_per_c2 = 931.49410372*1e6 + if reactants == 'DD': + mhe3 = atomic_mass('He3') + Q = (md + md - mhe3 - mn)*ev_per_c2 + E_n = mhe3/(mhe3 + mn)*Q + w0 = 82.542 + + # Low-T constants for peak shift (Table III) + a1 = 4.69515 + a2 = -0.040729 + a3 = 0.47 + a4 = 0.81844 + + # Low-T constants for width correction (Table III) + b1 = 1.7013e-3 + b2 = 0.16888 + b3 = 0.49 + b4 = 7.9460e-4 + + # High-T constants for peak shift (Table IV) + a5 = 18.225 + a6 = 2.1525 + + # High-T constants for width correction (Table IV) + b5 = 8.4619e-3 + b6 = 8.3241e-4 + + elif reactants == 'DT': + mt = atomic_mass('H3') + ma = atomic_mass('He4') + Q = (md + mt - ma - mn)*ev_per_c2 + E_n = ma/(ma + mn)*Q + w0 = 177.259 + + # Low-T constants for peak shift (Table III) + a1 = 5.30509 + a2 = 2.4736e-3 + a3 = 1.84 + a4 = 1.3818 + + # Low-T constants for width correction (Table III) + b1 = 5.1068e-4 + b2 = 7.6223e-3 + b3 = 1.78 + b4 = 8.7691e-5 + + # High-T constants for peak shift (Table IV) + a5 = 37.771 + a6 = 0.92181 + + # High-T constants for width correction (Table IV) + b5 = 2.0199e-3 + b6 = 5.9501e-5 + else: + raise ValueError("Invalid reactants specified. Must be 'DD' or 'DT'.") + + # Ion temperature in keV + T = ion_temp * 1e-3 + + if T <= 40.0: + # Low-temperature interpolation (Table III, 0 < T_i <= 40 keV) + Delta_E = a1/(1 + a2*T**a3)*T**(2/3) + a4*T + delta_w = b1/(1 + b2*T**b3)*T**(2/3) + b4*T + else: + # High-temperature interpolation (Table IV, 40 < T_i < 100 keV) + Delta_E = a5 + a6*T + delta_w = b5 + b6*T + + # Calculate FWHM + fwhm = (w0*(1 + delta_w) * sqrt(T))*1e3 + + sigma = fwhm / (2*sqrt(2*log(2))) + return Normal(E_n + Delta_E * 1e3, sigma, bias=bias) + + class Tabular(Univariate): """Piecewise continuous probability distribution. diff --git a/tests/unit_tests/test_stats.py b/tests/unit_tests/test_stats.py index 669d5b74c..ca961c8b0 100644 --- a/tests/unit_tests/test_stats.py +++ b/tests/unit_tests/test_stats.py @@ -930,3 +930,86 @@ def test_reference_vwu_normalization(): # reference_v should be unit length assert np.isclose(np.linalg.norm(reference_v), 1.0, atol=1e-12) + + +def test_fusion_spectrum_dd(): + d = openmc.stats.fusion_neutron_spectrum(10e3, 'DD') + assert isinstance(d, openmc.stats.Normal) + + # E_0 for D(d,n)3He is ~2.45 MeV; thermal shift at 10 keV should be + # several tens of keV, so mean should be noticeably above E_0 + assert d.mean_value > 2.45e6 + assert d.mean_value < 2.6e6 + + # Standard deviation should be positive and on order of ~50-100 keV + assert d.std_dev > 30e3 + assert d.std_dev < 200e3 + + +def test_fusion_spectrum_dt(): + d = openmc.stats.fusion_neutron_spectrum(10e3, 'DT') + assert isinstance(d, openmc.stats.Normal) + + # E_0 for T(d,n)alpha is ~14.02 MeV; with thermal shift mean should be + # above E_0 by several tens of keV + assert d.mean_value > 14.02e6 + assert d.mean_value < 14.2e6 + + # Standard deviation should be on order of ~200-400 keV + assert d.std_dev > 100e3 + assert d.std_dev < 500e3 + + +def test_fusion_spectrum_temp_continuity(): + # Verify the low-T and high-T formulas produce nearly identical results + # at the 40 keV switchover point + d_lo = openmc.stats.fusion_neutron_spectrum(39.99e3, 'DT') + d_hi = openmc.stats.fusion_neutron_spectrum(40.01e3, 'DT') + + assert d_lo.mean_value == pytest.approx(d_hi.mean_value, rel=1e-3) + assert d_lo.std_dev == pytest.approx(d_hi.std_dev, rel=1e-3) + + # Same check for DD + d_lo = openmc.stats.fusion_neutron_spectrum(39.99e3, 'DD') + d_hi = openmc.stats.fusion_neutron_spectrum(40.01e3, 'DD') + + assert d_lo.mean_value == pytest.approx(d_hi.mean_value, rel=1e-3) + assert d_lo.std_dev == pytest.approx(d_hi.std_dev, rel=1e-3) + + +def test_fusion_spectrum_high_temp(): + # At T_i = 80 keV (high-T regime), ensure the function still produces + # reasonable results using Table IV formulas + for reactants in ('DD', 'DT'): + d = openmc.stats.fusion_neutron_spectrum(80e3, reactants) + assert isinstance(d, openmc.stats.Normal) + assert d.mean_value > 0 + assert d.std_dev > 0 + + # DT mean at 80 keV should be higher than at 10 keV + d_10 = openmc.stats.fusion_neutron_spectrum(10e3, 'DT') + d_80 = openmc.stats.fusion_neutron_spectrum(80e3, 'DT') + assert d_80.mean_value > d_10.mean_value + assert d_80.std_dev > d_10.std_dev + + +def test_fusion_spectrum_zero_temp(): + # At very low temperature, mean should approach E_0 and width should + # approach zero + d = openmc.stats.fusion_neutron_spectrum(1.0, 'DT') + assert d.mean_value == pytest.approx(14.049e6, rel=1e-3) + assert d.std_dev < 5e3 # width approaches zero at low temperature + + +def test_fusion_spectrum_invalid(): + # Invalid reactant string should raise an error + with pytest.raises(ValueError): + openmc.stats.fusion_neutron_spectrum(10e3, '🐔🧇') + + # Negative temperature should raise an error + with pytest.raises(ValueError): + openmc.stats.fusion_neutron_spectrum(-10e3, 'DT') + + # Temperature above 100 keV should raise an error + with pytest.raises(ValueError): + openmc.stats.fusion_neutron_spectrum(101e3, 'DT') From 6cd39073b3523768748138f90be9e31d19cfdb52 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:16:59 +0200 Subject: [PATCH 596/671] Fix surface tally when crossing lattice (#3895) --- include/openmc/lattice.h | 14 ++++ include/openmc/tallies/tally_scoring.h | 4 +- src/lattice.cpp | 105 +++++++++++++++++++++++++ src/particle.cpp | 34 ++++++-- src/tallies/tally_scoring.cpp | 10 +-- 5 files changed, 153 insertions(+), 14 deletions(-) diff --git a/include/openmc/lattice.h b/include/openmc/lattice.h index f87d28b21..ca40bbc2a 100644 --- a/include/openmc/lattice.h +++ b/include/openmc/lattice.h @@ -113,6 +113,14 @@ public: virtual Position get_local_position( Position r, const array& i_xyz) const = 0; + //! \brief get the normal of the lattice surface crossing + //! \param[in] i_xyz The indices for the lattice translation. + //! \param[out] is_valid is the lattice translation correspond to a valid + //! surface. \return The surface normal corresponding to the lattice + //! translation. + virtual Direction get_normal( + const array& i_xyz, bool& is_valid) const = 0; + //! \brief Check flattened lattice index. //! \param indx The index for a lattice tile. //! \return true if the given index fit within the lattice bounds. False @@ -223,6 +231,9 @@ public: Position get_local_position( Position r, const array& i_xyz) const override; + Direction get_normal( + const array& i_xyz, bool& is_valid) const override; + int32_t& offset(int map, const array& i_xyz) override; int32_t offset(int map, int indx) const override; @@ -268,6 +279,9 @@ public: Position get_local_position( Position r, const array& i_xyz) const override; + Direction get_normal( + const array& i_xyz, bool& is_valid) const override; + bool is_valid_index(int indx) const override; int32_t& offset(int map, const array& i_xyz) override; diff --git a/include/openmc/tallies/tally_scoring.h b/include/openmc/tallies/tally_scoring.h index 29b3ec6e5..d1aed2831 100644 --- a/include/openmc/tallies/tally_scoring.h +++ b/include/openmc/tallies/tally_scoring.h @@ -111,9 +111,9 @@ void score_meshsurface_tally(Particle& p, const vector& tallies); // //! \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 +//! \param normal The normal of the surface being crossed void score_surface_tally( - Particle& p, const vector& tallies, const Surface& surf); + Particle& p, const vector& tallies, const Direction& normal); //! Score the pulse-height tally //! This is triggered at the end of every particle history diff --git a/src/lattice.cpp b/src/lattice.cpp index 92d451f61..e799a340e 100644 --- a/src/lattice.cpp +++ b/src/lattice.cpp @@ -340,6 +340,26 @@ Position RectLattice::get_local_position( //============================================================================== +Direction RectLattice::get_normal( + const array& i_xyz, bool& is_valid) const +{ + is_valid = false; + Direction dir = {0.0, 0.0, 0.0}; + if ((std::abs(i_xyz[0]) == 1) && (i_xyz[1] == 0) && (i_xyz[2] == 0)) { + is_valid = true; + dir[0] = std::copysign(1.0, i_xyz[0]); + } else if ((i_xyz[0] == 0) && (std::abs(i_xyz[1]) == 1) && (i_xyz[2] == 0)) { + is_valid = true; + dir[1] = std::copysign(1.0, i_xyz[1]); + } else if ((i_xyz[0] == 0) && (i_xyz[1] == 0) && (std::abs(i_xyz[2]) == 1)) { + is_valid = true; + dir[2] = std::copysign(1.0, i_xyz[2]); + } + return dir; +} + +//============================================================================== + int32_t& RectLattice::offset(int map, const array& i_xyz) { return offsets_[n_cells_[0] * n_cells_[1] * n_cells_[2] * map + @@ -986,6 +1006,91 @@ Position HexLattice::get_local_position( //============================================================================== +Direction HexLattice::get_normal( + const array& i_xyz, bool& is_valid) const +{ + // Short description of the direction vectors used here. The beta, gamma, and + // delta vectors point towards the flat sides of each hexagonal tile. + // Y - orientation: + // basis0 = (1, 0) + // basis1 = (-1/sqrt(3), 1) = +120 degrees from basis0 + // beta = (sqrt(3)/2, 1/2) = +30 degrees from basis0 + // gamma = (sqrt(3)/2, -1/2) = -60 degrees from beta + // delta = (0, 1) = +60 degrees from beta + // X - orientation: + // basis0 = (1/sqrt(3), -1) + // basis1 = (0, 1) = +120 degrees from basis0 + // beta = (1, 0) = +30 degrees from basis0 + // gamma = (1/2, -sqrt(3)/2) = -60 degrees from beta + // delta = (1/2, sqrt(3)/2) = +60 degrees from beta + + is_valid = false; + Direction dir = {0.0, 0.0, 0.0}; + if ((i_xyz[0] == 0) && (i_xyz[1] == 0) && (std::abs(i_xyz[2]) == 1)) { + is_valid = true; + dir[2] = std::copysign(1.0, i_xyz[2]); + } else if ((i_xyz[2] == 0) && + std::max({std::abs(i_xyz[0]), std::abs(i_xyz[1]), + std::abs(i_xyz[0] + i_xyz[1])}) == 1) { + is_valid = true; + // beta direction + if ((i_xyz[0] == 1) && (i_xyz[1] == 0)) { + if (orientation_ == Orientation::y) { + dir[0] = 0.5 * std::sqrt(3.0); + dir[1] = 0.5; + } else { + dir[0] = 1.0; + dir[1] = 0.0; + } + } else if ((i_xyz[0] == -1) && (i_xyz[1] == 0)) { + if (orientation_ == Orientation::y) { + dir[0] = -0.5 * std::sqrt(3.0); + dir[1] = -0.5; + } else { + dir[0] = -1.0; + dir[1] = 0.0; + } + // gamma direction + } else if ((i_xyz[0] == 1) && (i_xyz[1] == -1)) { + if (orientation_ == Orientation::y) { + dir[0] = 0.5 * std::sqrt(3.0); + dir[1] = -0.5; + } else { + dir[0] = 0.5; + dir[1] = -0.5 * std::sqrt(3.0); + } + } else if ((i_xyz[0] == -1) && (i_xyz[1] == 1)) { + if (orientation_ == Orientation::y) { + dir[0] = -0.5 * std::sqrt(3.0); + dir[1] = 0.5; + } else { + dir[0] = -0.5; + dir[1] = 0.5 * std::sqrt(3.0); + } + // delta direction + } else if ((i_xyz[0] == 0) && (i_xyz[1] == 1)) { + if (orientation_ == Orientation::y) { + dir[0] = 0.0; + dir[1] = 1.0; + } else { + dir[0] = 0.5; + dir[1] = 0.5 * std::sqrt(3.0); + } + } else if ((i_xyz[0] == 0) && (i_xyz[1] == -1)) { + if (orientation_ == Orientation::y) { + dir[0] = 0.0; + dir[1] = -1.0; + } else { + dir[0] = -0.5; + dir[1] = -0.5 * std::sqrt(3.0); + } + } + } + return dir; +} + +//============================================================================== + bool HexLattice::is_valid_index(int indx) const { int nx {2 * n_rings_ - 1}; diff --git a/src/particle.cpp b/src/particle.cpp index 0a0635598..11747e0cc 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -14,6 +14,7 @@ #include "openmc/error.h" #include "openmc/geometry.h" #include "openmc/hdf5_interface.h" +#include "openmc/lattice.h" #include "openmc/material.h" #include "openmc/message_passing.h" #include "openmc/mgxs_interface.h" @@ -302,8 +303,6 @@ 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,7 +311,23 @@ void Particle::event_cross_surface() bool verbose = settings::verbosity >= 10 || trace(); cross_lattice(*this, boundary(), verbose); event() = TallyEvent::LATTICE; + + // Score cell to cell partial currents + if (!model::active_surface_tallies.empty()) { + auto& lat {*model::lattices[lowest_coord().lattice()]}; + bool is_valid; + Direction normal = + lat.get_normal(boundary().lattice_translation(), is_valid); + if (is_valid) { + normal /= normal.norm(); + score_surface_tally(*this, model::active_surface_tallies, normal); + } + } + } else { + + const auto& surf {*model::surfaces[surface_index()].get()}; + // Particle crosses surface // If BC, add particle to surface source before crossing surface if (surf.surf_source_ && surf.bc_) { @@ -327,10 +342,13 @@ void Particle::event_cross_surface() apply_weight_windows(*this); } event() = TallyEvent::SURFACE; - } - // Score cell to cell partial currents - if (!model::active_surface_tallies.empty()) { - score_surface_tally(*this, model::active_surface_tallies, surf); + + // Score cell to cell partial currents + if (!model::active_surface_tallies.empty()) { + Direction normal = surf.normal(r()); + normal /= normal.norm(); + score_surface_tally(*this, model::active_surface_tallies, normal); + } } } @@ -681,7 +699,9 @@ 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, surf); + Direction normal = surf.normal(r()); + normal /= normal.norm(); + score_surface_tally(*this, model::active_surface_tallies, normal); } if (!model::active_meshsurf_tallies.empty()) { diff --git a/src/tallies/tally_scoring.cpp b/src/tallies/tally_scoring.cpp index 4210b034a..d5af99382 100644 --- a/src/tallies/tally_scoring.cpp +++ b/src/tallies/tally_scoring.cpp @@ -2656,19 +2656,19 @@ void score_meshsurface_tally(Particle& p, const vector& tallies) } void score_surface_tally( - Particle& p, const vector& tallies, const Surface& surf) + Particle& p, const vector& tallies, const Direction& normal) { double wgt = p.wgt_last(); + double mu = std::clamp(p.u().dot(normal), -1.0, 1.0); + // 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; + double current_sign = std::copysign(1.0, mu); // 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); + double abs_mu = std::abs(mu); if (abs_mu < settings::surface_grazing_cutoff) abs_mu = settings::surface_grazing_ratio * settings::surface_grazing_cutoff; From 8223099ed95ead5ab0efa97d4114e819524821f9 Mon Sep 17 00:00:00 2001 From: April Novak Date: Thu, 26 Mar 2026 18:35:12 +0100 Subject: [PATCH 597/671] All reduce to print correct number of surface source particles (#3901) --- src/state_point.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/state_point.cpp b/src/state_point.cpp index c0d8ab5b2..da1c141a2 100644 --- a/src/state_point.cpp +++ b/src/state_point.cpp @@ -592,8 +592,16 @@ void write_source_point(std::string filename, span source_bank, const vector& bank_index, bool use_mcpl) { std::string ext = use_mcpl ? "mcpl" : "h5"; + + int total_surf_particles = source_bank.size(); +#ifdef OPENMC_MPI + int num_particles = source_bank.size(); + MPI_Allreduce( + &num_particles, &total_surf_particles, 1, MPI_INT, MPI_SUM, mpi::intracomm); +#endif + write_message("Creating source file {}.{} with {} particles ...", filename, - ext, source_bank.size(), 5); + ext, total_surf_particles, 5); // Dispatch to appropriate function based on file type if (use_mcpl) { From 97d9a839c281e81785837000f3f021b4cfa4adbb Mon Sep 17 00:00:00 2001 From: "Md. Ariful Islam" <49443884+AI-Pranto@users.noreply.github.com> Date: Thu, 2 Apr 2026 12:30:09 -0400 Subject: [PATCH 598/671] Fix MPI depletion hang (#3910) --- openmc/deplete/abc.py | 4 ++-- openmc/deplete/stepresult.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 056f7c273..7736b8c13 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -201,7 +201,7 @@ class TransportOperator(ABC): Returns ------- volume : dict of str to float - Volumes corresponding to materials in burn_list + Volumes corresponding to materials in full_burn_list nuc_list : list of str A list of all nuclide names. Used for sorting the simulation. burn_list : list of int @@ -210,7 +210,7 @@ class TransportOperator(ABC): full_burn_list : list of int All burnable materials in the geometry. name_list : list of str - Material names corresponding to materials in burn_list + Material names corresponding to materials in full_burn_list """ def finalize(self): diff --git a/openmc/deplete/stepresult.py b/openmc/deplete/stepresult.py index cd21df07b..6da7f7adc 100644 --- a/openmc/deplete/stepresult.py +++ b/openmc/deplete/stepresult.py @@ -154,14 +154,14 @@ class StepResult: full_burn_list : list of str List of all burnable material IDs name_list : list of str, optional - Material names corresponding to materials in burn_list + Material names corresponding to materials in full_burn_list """ self.volume = copy.deepcopy(volume) self.index_nuc = {nuc: i for i, nuc in enumerate(nuc_list)} self.index_mat = {mat: i for i, mat in enumerate(burn_list)} self.mat_to_hdf5_ind = {mat: i for i, mat in enumerate(full_burn_list)} - self.mat_to_name = dict(zip(burn_list, name_list)) if name_list is not None else {} + self.mat_to_name = dict(zip(full_burn_list, name_list)) if name_list is not None else {} # Create storage array self.data = np.zeros((self.n_mat, self.n_nuc)) From d9b30bbbd51ace80d6a5babf96c056106ae69e82 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Thu, 2 Apr 2026 19:59:02 +0300 Subject: [PATCH 599/671] Approximate multigroup velocity (#3766) Co-authored-by: Adam Nelson <1037107+nelsonag@users.noreply.github.com> Co-authored-by: Paul Romano --- docs/source/io_formats/mgxs_library.rst | 4 ++ docs/source/methods/cross_sections.rst | 42 ++++++++++++++ include/openmc/mgxs_interface.h | 2 + include/openmc/particle.h | 2 + openmc/mgxs/groups.py | 1 + src/mgxs.cpp | 2 + src/mgxs_interface.cpp | 13 +++++ src/particle.cpp | 33 ++++++----- tests/unit_tests/test_mg_inverse_velocity.py | 59 ++++++++++++++++++++ 9 files changed, 145 insertions(+), 13 deletions(-) create mode 100644 tests/unit_tests/test_mg_inverse_velocity.py diff --git a/docs/source/io_formats/mgxs_library.rst b/docs/source/io_formats/mgxs_library.rst index f7f5387a4..8a26311d1 100644 --- a/docs/source/io_formats/mgxs_library.rst +++ b/docs/source/io_formats/mgxs_library.rst @@ -133,6 +133,10 @@ Temperature-dependent data, provided for temperature K. This dataset is optional. This is a 1-D vector if `representation` is "isotropic", or a 3-D vector if `representation` is "angle" with dimensions of [polar][azimuthal][groups]. + When this data is not available, an approximation using the + group energy boundaries is used. For more information see + the particle speed subsection in the multigroup-data section + of the theory manual. **//K/scatter_data/** diff --git a/docs/source/methods/cross_sections.rst b/docs/source/methods/cross_sections.rst index a66abb3ed..764c4c628 100644 --- a/docs/source/methods/cross_sections.rst +++ b/docs/source/methods/cross_sections.rst @@ -289,6 +289,48 @@ sections. This allows flexibility for the model to use highly anisotropic scattering information in the water while the fuel can be simulated with linear or even isotropic scattering. +Particle Speed +-------------- + +When using a multigroup representation of cross sections, the particle speed has +meaning only in an average sense. The particle speed is important when modeling +dynamic behavior. OpenMC calculates the particle speed using the inverse +velocity multigroup data if it is available. If such data is not available, +OpenMC uses an approximate velocity using the group energy bounds in the +following way: + +.. math:: + + \frac{1}{v_g} = \int_{E_{\text{min}}^g}^{E_{\text{max}}^g} \frac{1}{v(E)} \frac{\alpha}{E} dE + +Where :math:`E_{\text{min}}^g` and :math:`E_{\text{max}}^g` are the group energy +boundaries for group :math:`g`. :math:`v(E)` is the neutron velocity calculated +using relativistic kinematics, :math:`\alpha` is a normalization constant for the +:math:`\frac{1}{E}` spectrum. + +This equation is valid when inside the group boundaries the neutron spectrum +follows a typical :math:`\frac{1}{E}` slowing down spectrum. This assumption is +widely used when generating fine group neutron cross section data libraries from +continuous energy data. + +The solution to this equation is: + +.. math:: + + \frac{1}{v_g} = \frac{1}{c \log\left(\frac{E_{\text{max}}^g}{E_{\text{min}}^g}\right)} + \left[ 2(\operatorname{arctanh}(k_{\text{max}}^{-1}) - \operatorname{arctanh}(k_{\text{min}}^{-1})) + - (k_{\text{max}}-k_{\text{min}}) \right] + +where :math:`c` is the speed of light and :math:`k_{\text{max}}`, +:math:`k_{\text{min}}` are defined by a change of variables: + +.. math:: + + k = \sqrt{1+\frac{2 m_n c^2}{E}} + +where :math:`E` is the particle kinetic energy and :math:`m_n` is the neutron +rest mass. + .. _logarithmic mapping technique: https://mcnp.lanl.gov/pdf_files/TechReport_2014_LANL_LA-UR-14-24530_Brown.pdf .. _Hwang: https://doi.org/10.13182/NSE87-A16381 diff --git a/include/openmc/mgxs_interface.h b/include/openmc/mgxs_interface.h index da074f825..117ac503d 100644 --- a/include/openmc/mgxs_interface.h +++ b/include/openmc/mgxs_interface.h @@ -61,6 +61,8 @@ public: vector energy_bin_avg_; vector rev_energy_bins_; vector> nuc_temps_; // all available temperatures + vector + default_inverse_velocity_; // approximate default inverse-velocity data }; namespace data { diff --git a/include/openmc/particle.h b/include/openmc/particle.h index 2f6e6196b..e75f3785a 100644 --- a/include/openmc/particle.h +++ b/include/openmc/particle.h @@ -39,6 +39,8 @@ public: double speed() const; + double mass() const; + //! create a secondary particle // //! stores the current phase space attributes of the particle in the diff --git a/openmc/mgxs/groups.py b/openmc/mgxs/groups.py index 8910c7d42..aea7a6d29 100644 --- a/openmc/mgxs/groups.py +++ b/openmc/mgxs/groups.py @@ -78,6 +78,7 @@ class EnergyGroups: @group_edges.setter def group_edges(self, edges): cv.check_type('group edges', edges, Iterable, Real) + cv.check_increasing('group edges', edges) cv.check_greater_than('number of group edges', len(edges), 1) self._group_edges = np.array(edges) diff --git a/src/mgxs.cpp b/src/mgxs.cpp index a0fe4060d..1dc090ab9 100644 --- a/src/mgxs.cpp +++ b/src/mgxs.cpp @@ -510,6 +510,8 @@ double Mgxs::get_xs(MgxsType xstype, int gin, const int* gout, const double* mu, break; case MgxsType::INVERSE_VELOCITY: val = xs_t->inverse_velocity(a, gin); + if (!(val > 0)) + val = data::mg.default_inverse_velocity_[gin]; break; case MgxsType::DECAY_RATE: if (dg != nullptr) { diff --git a/src/mgxs_interface.cpp b/src/mgxs_interface.cpp index 08c64413a..865c56580 100644 --- a/src/mgxs_interface.cpp +++ b/src/mgxs_interface.cpp @@ -237,6 +237,19 @@ void MgxsInterface::read_header(const std::string& path_cross_sections) "library file!"); } + // Calculate approximate default inverse velocity data + for (int i = 0; i < energy_bins_.size() - 1; ++i) { + double e_min = std::max(energy_bins_[i + 1], 1e-5); + double e_max = energy_bins_[i]; + double alpha = 1.0 / (C_LIGHT * std::log(e_max / e_min)); + double k_max = std::sqrt(1 + 2.0 * MASS_NEUTRON_EV / e_max); + double k_min = std::sqrt(1 + 2.0 * MASS_NEUTRON_EV / e_min); + double inv_v = + alpha * (2.0 * (std::atanh(1.0 / k_max) - std::atanh(1.0 / k_min)) - + (k_max - k_min)); + default_inverse_velocity_.push_back(inv_v); + } + // Close MGXS HDF5 file file_close(file_id); } diff --git a/src/particle.cpp b/src/particle.cpp index 11747e0cc..46df63cd1 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -48,26 +48,33 @@ double Particle::speed() const { if (settings::run_CE) { // Determine mass in eV/c^2 - double mass; - switch (type().pdg_number()) { - case PDG_NEUTRON: - mass = MASS_NEUTRON_EV; - case PDG_ELECTRON: - case PDG_POSITRON: - mass = MASS_ELECTRON_EV; - default: - mass = this->type().mass() * AMU_EV; - } + double mass = this->mass(); // Equivalent to C * sqrt(1-(m/(m+E))^2) without problem at E<E() * (this->E() + 2 * mass)) / (this->E() + mass); } else { - auto& macro_xs = data::mg.macro_xs_[this->material()]; + auto mat = this->material(); + if (mat == MATERIAL_VOID) + return 1.0 / data::mg.default_inverse_velocity_[this->g()]; + auto& macro_xs = data::mg.macro_xs_[mat]; int macro_t = this->mg_xs_cache().t; int macro_a = macro_xs.get_angle_index(this->u()); - return 1.0 / macro_xs.get_xs(MgxsType::INVERSE_VELOCITY, this->g(), nullptr, - nullptr, nullptr, macro_t, macro_a); + return 1.0 / macro_xs.get_xs( + MgxsType::INVERSE_VELOCITY, this->g(), macro_t, macro_a); + } +} + +double Particle::mass() const +{ + switch (type().pdg_number()) { + case PDG_NEUTRON: + return MASS_NEUTRON_EV; + case PDG_ELECTRON: + case PDG_POSITRON: + return MASS_ELECTRON_EV; + default: + return this->type().mass() * AMU_EV; } } diff --git a/tests/unit_tests/test_mg_inverse_velocity.py b/tests/unit_tests/test_mg_inverse_velocity.py new file mode 100644 index 000000000..b520e48d1 --- /dev/null +++ b/tests/unit_tests/test_mg_inverse_velocity.py @@ -0,0 +1,59 @@ +import openmc +import numpy as np +import pytest + +@pytest.fixture +def one_group_lib(): + groups = openmc.mgxs.EnergyGroups([0.0, 20.0e6]) + xsdata = openmc.XSdata('slab_mat', groups) + xsdata.order = 0 + xsdata.set_total([0.0]) + xsdata.set_absorption([0.0]) + xsdata.set_scatter_matrix([[[0.0]]]) + + mg_library = openmc.MGXSLibrary(groups) + mg_library.add_xsdata(xsdata) + name = 'mgxs.h5' + mg_library.export_to_hdf5(name) + yield name + +@pytest.fixture +def slab_model(one_group_lib): + model = openmc.Model() + mat = openmc.Material(name='slab_material') + mat.set_density('macro', 1.0) + mat.add_macroscopic('slab_mat') + + model.materials = openmc.Materials([mat]) + model.materials.cross_sections = one_group_lib + + x_min = openmc.XPlane(x0=0.0, boundary_type='vacuum') + x_max = openmc.XPlane(x0=10.0, boundary_type='vacuum') + + y_min = openmc.YPlane(y0=-10.0, boundary_type='vacuum') + y_max = openmc.YPlane(y0=10.0, boundary_type='vacuum') + z_min = openmc.ZPlane(z0=-10.0, boundary_type='vacuum') + z_max = openmc.ZPlane(z0=19.0, boundary_type='vacuum') + + cell = openmc.Cell(fill=mat, region=+z_min & -x_max & +y_min & -y_max & +z_min & -z_max) + model.geometry = openmc.Geometry([cell]) + + model.settings = openmc.Settings() + model.settings.energy_mode = 'multi-group' + model.settings.run_mode = 'fixed source' + model.settings.batches = 3 + model.settings.particles = 10 + + source = openmc.IndependentSource() + source.space = openmc.stats.Point((5.0, 0.0, 0.0)) + model.settings.source = source + return model + +def test_inverse_velocity(run_in_tmpdir, slab_model): + tally = openmc.Tally() + tally.scores = ['flux','inverse-velocity'] + slab_model.tallies = [tally] + slab_model.run(apply_tally_results=True) + inverse_velocity = tally.mean.squeeze()[1]/tally.mean.squeeze()[0] + + assert inverse_velocity == pytest.approx(1.6144e-5, rel=1e-4) From ca22a5174a56899fe2b88d650526a012a8c72170 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Fri, 3 Apr 2026 00:01:30 +0200 Subject: [PATCH 600/671] adding pdf to read the docs (#3893) --- .readthedocs.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 3578144b2..61301bdb4 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -10,6 +10,9 @@ build: sphinx: configuration: docs/source/conf.py +formats: + - pdf + python: install: - method: pip From df985e10b340895298eb77209571f0cf9856307b Mon Sep 17 00:00:00 2001 From: bessemoa Date: Fri, 3 Apr 2026 00:51:33 +0200 Subject: [PATCH 601/671] Add from_bounding_box classmethod to structured mesh classes (#3903) Co-authored-by: Paul Romano --- openmc/mesh.py | 271 ++++++++++++++-------- tests/unit_tests/test_mesh_from_domain.py | 14 ++ 2 files changed, 189 insertions(+), 96 deletions(-) diff --git a/openmc/mesh.py b/openmc/mesh.py index 79982d78a..3c3c0a1ac 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -936,6 +936,87 @@ class StructuredMesh(MeshBase): f"with dimensions {self.dimension}" ) + @classmethod + def from_domain( + cls, + domain: HasBoundingBox | BoundingBox, + dimension: Sequence[int] | int | None = None, + mesh_id: int | None = None, + name: str = '', + **kwargs + ) -> StructuredMesh: + """Create a structured mesh from a domain using its bounding box. + + Parameters + ---------- + domain : HasBoundingBox | openmc.BoundingBox + Object used as a template for the mesh extents. If ``domain`` has a + ``bounding_box`` attribute, that bounding box is used directly. + dimension : Iterable of int or int, optional + Number of mesh cells. When omitted, the subclass-specific default is + used. If provided as a single integer, subclasses that support it + interpret it as a target total number of mesh cells. + mesh_id : int, optional + Unique identifier for the mesh. + name : str, optional + Name of the mesh. + **kwargs + Additional keyword arguments forwarded to + :meth:`from_bounding_box`. + + Returns + ------- + openmc.StructuredMesh + Structured mesh instance. + """ + if isinstance(domain, BoundingBox): + bbox = domain + elif hasattr(domain, 'bounding_box'): + bbox = domain.bounding_box + else: + raise TypeError("Domain must be a BoundingBox or have a " + "bounding_box property") + + if dimension is None: + return cls.from_bounding_box( + bbox, mesh_id=mesh_id, name=name, **kwargs) + + return cls.from_bounding_box( + bbox, dimension=dimension, mesh_id=mesh_id, name=name, **kwargs) + + @classmethod + @abstractmethod + def from_bounding_box( + cls, + bbox: openmc.BoundingBox, + dimension: Sequence[int] | int, + mesh_id: int | None = None, + name: str = '', + **kwargs + ) -> StructuredMesh: + """Create a structured mesh from a bounding box. + + Parameters + ---------- + bbox : openmc.BoundingBox + Bounding box used to define the mesh extents. + dimension : Iterable of int or int + Number of mesh cells. The interpretation and any default value are + defined by the concrete mesh type. + mesh_id : int, optional + Unique identifier for the mesh. + name : str, optional + Name of the mesh. + **kwargs + Additional keyword arguments accepted by specific subclasses. + + Returns + ------- + openmc.StructuredMesh + Structured mesh instance. + """ + pass + class HasBoundingBox(Protocol): """Object that has a ``bounding_box`` attribute.""" @@ -1190,62 +1271,47 @@ class RegularMesh(StructuredMesh): return mesh @classmethod - def from_domain( + def from_bounding_box( cls, - domain: HasBoundingBox | BoundingBox, + bbox: openmc.BoundingBox, dimension: Sequence[int] | int = 1000, mesh_id: int | None = None, - name: str = '' - ): - """Create RegularMesh from a domain using its bounding box. + name: str = '', + ) -> RegularMesh: + """Create a RegularMesh from a bounding box. Parameters ---------- - 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. - 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 - will will be divided into that many mesh cells with roughly equal - lengths in each direction (cubes). - mesh_id : int - Unique identifier for the mesh - name : str - Name of the mesh + bbox : openmc.BoundingBox + Bounding box used to set the mesh extents. + dimension : Iterable of int or int, optional + The number of mesh cells in each direction (x, y, z). If a single + integer is provided, the total number of cells is distributed + across directions to produce cells with roughly equal widths. + mesh_id : int, optional + Unique identifier for the mesh. + name : str, optional + Name of the mesh. Returns ------- openmc.RegularMesh - RegularMesh instance - + RegularMesh instance. """ - 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 = bb[0] - mesh.upper_right = bb[1] + mesh.lower_left = bbox[0] + mesh.upper_right = bbox[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 = bb.volume / dimension + ideal_cube_volume = bbox.volume / dimension ideal_cube_size = ideal_cube_volume ** (1 / 3) dimension = [ max(1, int(round(side / ideal_cube_size))) - for side in bb.width + for side in bbox.width ] mesh.dimension = dimension - return mesh def to_xml_element(self): @@ -1730,6 +1796,48 @@ class RectilinearMesh(StructuredMesh): return tuple(indices) + @classmethod + def from_bounding_box( + cls, + bbox: openmc.BoundingBox, + dimension: Sequence[int] | int = 1000, + mesh_id: int | None = None, + name: str = '', + ) -> RectilinearMesh: + """Create a RectilinearMesh from a bounding box with uniform grids. + + Parameters + ---------- + bbox : openmc.BoundingBox + Bounding box used to set the mesh extents. + dimension : Iterable of int or int, optional + The number of mesh cells in each direction (x, y, z). If a single + integer is provided, the total number of cells is distributed across + the three directions proportionally to the side lengths. + mesh_id : int, optional + Unique identifier for the mesh. + name : str, optional + Name of the mesh. + + Returns + ------- + openmc.RectilinearMesh + RectilinearMesh instance with uniform grids along each axis. + """ + if isinstance(dimension, int): + cv.check_greater_than("dimension", dimension, 1, equality=True) + ideal_cube_volume = bbox.volume / dimension + ideal_cube_size = ideal_cube_volume ** (1 / 3) + dimension = [ + max(1, int(round(side / ideal_cube_size))) + for side in bbox.width + ] + mesh = cls(mesh_id=mesh_id, name=name) + mesh.x_grid = np.linspace(bbox[0][0], bbox[1][0], num=dimension[0] + 1) + mesh.y_grid = np.linspace(bbox[0][1], bbox[1][1], num=dimension[1] + 1) + mesh.z_grid = np.linspace(bbox[0][2], bbox[1][2], num=dimension[2] + 1) + return mesh + class CylindricalMesh(StructuredMesh): """A 3D cylindrical mesh @@ -1996,34 +2104,31 @@ class CylindricalMesh(StructuredMesh): return mesh @classmethod - def from_domain( + def from_bounding_box( cls, - domain: HasBoundingBox | BoundingBox, + bbox: openmc.BoundingBox, dimension: Sequence[int] = (10, 10, 10), mesh_id: int | None = None, - phi_grid_bounds: Sequence[float] = (0.0, 2*pi), name: str = '', - enclose_domain: bool = False - ): - """Create CylindricalMesh from a domain using its bounding box. + phi_grid_bounds: Sequence[float] = (0.0, 2*pi), + enclose_domain: bool = False, + ) -> CylindricalMesh: + """Create CylindricalMesh from a bounding box. Parameters ---------- - 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. Alternatively, a - :class:`openmc.BoundingBox` can be passed directly. + bbox : openmc.BoundingBox + Bounding box used to set the r_grid and z_grid ranges. dimension : Iterable of int The number of equally spaced mesh cells in each direction (r_grid, phi_grid, z_grid) - mesh_id : int + mesh_id : int, optional Unique identifier for the mesh + name : str, optional + Name of the mesh phi_grid_bounds : numpy.ndarray Mesh bounds points along the phi-axis in radians. The default value is (0, 2π), i.e., the full phi range. - name : str - Name of the mesh enclose_domain : bool If True, the mesh will encompass the bounding box of the domain. If False, the mesh will be inscribed within the domain's bounding box. @@ -2034,40 +2139,28 @@ class CylindricalMesh(StructuredMesh): CylindricalMesh instance """ - 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]) + outer_radius = 0.5 * np.linalg.norm(bbox.width[:2]) else: - outer_radius = 0.5 * min(cached_bb.width[:2]) + outer_radius = 0.5 * min(bbox.width[:2]) - r_grid = np.linspace( - 0, - outer_radius, - num=dimension[0]+1 - ) + r_grid = np.linspace(0, outer_radius, num=dimension[0]+1) phi_grid = np.linspace( phi_grid_bounds[0], phi_grid_bounds[1], num=dimension[1]+1 ) z_grid = np.linspace( - cached_bb[0][2], - cached_bb[1][2], + bbox[0][2], + bbox[1][2], num=dimension[2]+1 ) - origin = (cached_bb.center[0], cached_bb.center[1], z_grid[0]) + origin = (bbox.center[0], bbox.center[1], z_grid[0]) # make z-grid relative to the origin z_grid -= origin[2] - mesh = cls( + return cls( r_grid=r_grid, z_grid=z_grid, phi_grid=phi_grid, @@ -2076,8 +2169,6 @@ class CylindricalMesh(StructuredMesh): origin=origin ) - return mesh - def to_xml_element(self): """Return XML representation of the mesh @@ -2385,39 +2476,36 @@ class SphericalMesh(StructuredMesh): return mesh @classmethod - def from_domain( + def from_bounding_box( cls, - domain: HasBoundingBox | BoundingBox, + bbox: openmc.BoundingBox, dimension: Sequence[int] = (10, 10, 10), mesh_id: int | None = None, + name: str = '', phi_grid_bounds: Sequence[float] = (0.0, 2*pi), theta_grid_bounds: Sequence[float] = (0.0, pi), - name: str = '', - enclose_domain: bool = False - ): - """Create SphericalMesh from a domain using its bounding box. + enclose_domain: bool = False, + ) -> SphericalMesh: + """Create SphericalMesh from a bounding box. Parameters ---------- - 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. Alternatively, a - :class:`openmc.BoundingBox` can be passed directly. + bbox : openmc.BoundingBox + Bounding box used to set the r_grid, phi_grid, and theta_grid ranges. 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 phi and theta, and in absolute space for r. - mesh_id : int + mesh_id : int, optional Unique identifier for the mesh + name : str, optional + Name of the mesh phi_grid_bounds : numpy.ndarray Mesh bounds points along the phi-axis in radians. The default value is (0, 2π), i.e., the full phi range. theta_grid_bounds : numpy.ndarray Mesh bounds points along the theta-axis in radians. The default value is (0, π), i.e., the full theta range. - name : str - Name of the mesh enclose_domain : bool If True, the mesh will encompass the bounding box of the domain. If False, the mesh will be inscribed within the domain's bounding box. @@ -2428,18 +2516,10 @@ class SphericalMesh(StructuredMesh): SphericalMesh instance """ - 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) + outer_radius = 0.5 * np.linalg.norm(bbox.width) else: - outer_radius = 0.5 * min(cached_bb.width) + outer_radius = 0.5 * min(bbox.width) r_grid = np.linspace(0, outer_radius, num=dimension[0] + 1) theta_grid = np.linspace( @@ -2452,8 +2532,7 @@ class SphericalMesh(StructuredMesh): phi_grid_bounds[1], num=dimension[2]+1 ) - origin = np.array([ - cached_bb.center[0], cached_bb.center[1], cached_bb.center[2]]) + origin = np.array([bbox.center[0], bbox.center[1], bbox.center[2]]) return cls(r_grid=r_grid, phi_grid=phi_grid, theta_grid=theta_grid, origin=origin, mesh_id=mesh_id, name=name) diff --git a/tests/unit_tests/test_mesh_from_domain.py b/tests/unit_tests/test_mesh_from_domain.py index 2b02921b5..46d22f0d2 100644 --- a/tests/unit_tests/test_mesh_from_domain.py +++ b/tests/unit_tests/test_mesh_from_domain.py @@ -27,6 +27,20 @@ def test_reg_mesh_from_bounding_box(): assert np.array_equal(mesh.upper_right, bb[1]) +def test_rectilinear_mesh_from_bounding_box(): + """Tests a RectilinearMesh can be made from a BoundingBox directly.""" + bb = openmc.BoundingBox([-8, -7, -5], [12, 13, 15]) + + mesh = openmc.RectilinearMesh.from_bounding_box(bb, dimension=[2, 4, 5]) + assert isinstance(mesh, openmc.RectilinearMesh) + assert np.array_equal(mesh.dimension, (2, 4, 5)) + assert np.array_equal(mesh.lower_left, bb[0]) + assert np.array_equal(mesh.upper_right, bb[1]) + assert np.array_equal(mesh.x_grid, [-8., 2., 12.]) + assert np.array_equal(mesh.y_grid, [-7., -2., 3., 8., 13.]) + assert np.array_equal(mesh.z_grid, [-5., -1., 3., 7., 11., 15.]) + + def test_cylindrical_mesh_from_cell(): """Tests a CylindricalMesh can be made from a Cell and the specified dimensions are propagated through.""" From 9ff50499e133e1b977391dd1ff284b9eccb08958 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Fri, 3 Apr 2026 02:00:02 +0200 Subject: [PATCH 602/671] Adding per m3 to material functions (#3912) Co-authored-by: Jon Shimwell Co-authored-by: Paul Romano --- openmc/deplete/results.py | 4 ++-- openmc/material.py | 27 ++++++++++++++++----------- tests/unit_tests/test_material.py | 6 ++++++ 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index e1fcb26b6..adb0d3dbc 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -113,7 +113,7 @@ class Results(list): ---------- mat : openmc.Material, str Material object or material id to evaluate - units : {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3'} + units : {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3', 'Bq/m3'} Specifies the type of activity to return, options include total activity [Bq], specific [Bq/g, Bq/kg] or volumetric activity [Bq/cm3]. by_nuclide : bool @@ -231,7 +231,7 @@ class Results(list): ---------- mat : openmc.Material, str Material object or material id to evaluate. - units : {'W', 'W/g', 'W/kg', 'W/cm3'} + units : {'W', 'W/g', 'W/kg', 'W/cm3', 'W/m3'} Specifies the units of decay heat to return. Options include total heat [W], specific [W/g, W/kg] or volumetric heat [W/cm3]. by_nuclide : bool diff --git a/openmc/material.py b/openmc/material.py index 239bc01f7..2dbe691f5 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -349,7 +349,7 @@ class Material(IDManagerMixin): clip_tolerance : float Maximum fraction of :math:`\sum_i x_i p_i` for discrete distributions that will be discarded. - units : {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3'} + units : {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3', 'Bq/m3'} Specifies the units on the integral of the distribution. volume : float, optional Volume of the material. If not passed, defaults to using the @@ -367,7 +367,7 @@ class Material(IDManagerMixin): the total intensity of the photon source in the requested units. """ - cv.check_value('units', units, {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3'}) + cv.check_value('units', units, {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3', 'Bq/m3'}) if exclude_nuclides is not None and include_nuclides is not None: raise ValueError("Cannot specify both exclude_nuclides and include_nuclides") @@ -378,6 +378,8 @@ class Material(IDManagerMixin): raise ValueError("volume must be specified if units='Bq'") elif units == 'Bq/cm3': multiplier = 1 + elif units == 'Bq/m3': + multiplier = 1e6 elif units == 'Bq/g': multiplier = 1.0 / self.get_mass_density() elif units == 'Bq/kg': @@ -1383,16 +1385,16 @@ class Material(IDManagerMixin): def get_activity(self, units: str = 'Bq/cm3', by_nuclide: bool = False, volume: float | None = None) -> dict[str, float] | float: - """Returns the activity of the material or of each nuclide within. + """Return the activity of the material or each nuclide within. .. versionadded:: 0.13.1 Parameters ---------- - units : {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3', 'Ci', 'Ci/m3'} + units : {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3', 'Bq/m3', 'Ci', 'Ci/m3'} Specifies the type of activity to return, options include total activity [Bq,Ci], specific [Bq/g, Bq/kg] or volumetric activity - [Bq/cm3,Ci/m3]. Default is volumetric activity [Bq/cm3]. + [Bq/cm3, Bq/m3, Ci/m3]. Default is volumetric activity [Bq/cm3]. by_nuclide : bool Specifies if the activity should be returned for the material as a whole or per nuclide. Default is False. @@ -1410,7 +1412,7 @@ class Material(IDManagerMixin): of the material is returned as a float. """ - cv.check_value('units', units, {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3', 'Ci', 'Ci/m3'}) + cv.check_value('units', units, {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3', 'Bq/m3', 'Ci', 'Ci/m3'}) cv.check_type('by_nuclide', by_nuclide, bool) if volume is None: @@ -1420,6 +1422,8 @@ class Material(IDManagerMixin): multiplier = volume elif units == 'Bq/cm3': multiplier = 1 + elif units == 'Bq/m3': + multiplier = 1e6 elif units == 'Bq/g': multiplier = 1.0 / self.get_mass_density() elif units == 'Bq/kg': @@ -1438,16 +1442,15 @@ class Material(IDManagerMixin): def get_decay_heat(self, units: str = 'W', by_nuclide: bool = False, volume: float | None = None) -> dict[str, float] | float: - """Returns the decay heat of the material or for each nuclide in the - material in units of [W], [W/g], [W/kg] or [W/cm3]. + """Return the decay heat of the material or each nuclide within. .. versionadded:: 0.13.3 Parameters ---------- - units : {'W', 'W/g', 'W/kg', 'W/cm3'} + units : {'W', 'W/g', 'W/kg', 'W/cm3', 'W/m3'} Specifies the units of decay heat to return. Options include total - heat [W], specific [W/g, W/kg] or volumetric heat [W/cm3]. + heat [W], specific [W/g, W/kg] or volumetric heat [W/cm3, W/m3]. Default is total heat [W]. by_nuclide : bool Specifies if the decay heat should be returned for the material as a @@ -1466,13 +1469,15 @@ class Material(IDManagerMixin): of the material is returned as a float. """ - cv.check_value('units', units, {'W', 'W/g', 'W/kg', 'W/cm3'}) + cv.check_value('units', units, {'W', 'W/g', 'W/kg', 'W/cm3', 'W/m3'}) cv.check_type('by_nuclide', by_nuclide, bool) if units == 'W': multiplier = volume if volume is not None else self.volume elif units == 'W/cm3': multiplier = 1 + elif units == 'W/m3': + multiplier = 1e6 elif units == 'W/g': multiplier = 1.0 / self.get_mass_density() elif units == 'W/kg': diff --git a/tests/unit_tests/test_material.py b/tests/unit_tests/test_material.py index 58cd4d563..911b8867f 100644 --- a/tests/unit_tests/test_material.py +++ b/tests/unit_tests/test_material.py @@ -594,6 +594,8 @@ def test_get_activity(): assert pytest.approx(m4.get_activity(units='Bq/g', by_nuclide=True)["H3"]) == 355978108155965.94 # [Bq/g] assert pytest.approx(m4.get_activity(units='Bq/cm3')) == 355978108155965.94*3/2 # [Bq/cc] assert pytest.approx(m4.get_activity(units='Bq/cm3', by_nuclide=True)["H3"]) == 355978108155965.94*3/2 # [Bq/cc] + assert pytest.approx(m4.get_activity(units='Bq/m3')) == 355978108155965.94*3/2*1e6 # [Bq/m3] + assert pytest.approx(m4.get_activity(units='Bq/m3', by_nuclide=True)["H3"]) == 355978108155965.94*3/2*1e6 # [Bq/m3] # volume is required to calculate total activity m4.volume = 10. assert pytest.approx(m4.get_activity(units='Bq')) == 355978108155965.94*3/2*10 # [Bq] @@ -650,6 +652,8 @@ def test_get_decay_heat(): assert pytest.approx(m4.get_decay_heat(units='W/g', by_nuclide=True)["I135"]) == 40175.15720273193 # [W/g] assert pytest.approx(m4.get_decay_heat(units='W/cm3')) == 40175.15720273193*3/2 # [W/cc] assert pytest.approx(m4.get_decay_heat(units='W/cm3', by_nuclide=True)["I135"]) == 40175.15720273193*3/2 #[W/cc] + assert pytest.approx(m4.get_decay_heat(units='W/m3')) == 40175.15720273193*3/2*1e6 # [W/m3] + assert pytest.approx(m4.get_decay_heat(units='W/m3', by_nuclide=True)["I135"]) == 40175.15720273193*3/2*1e6 # [W/m3] # volume is required to calculate total decay heat m4.volume = 10. assert pytest.approx(m4.get_decay_heat(units='W')) == 40175.15720273193*3/2*10 # [W] @@ -680,6 +684,8 @@ def test_decay_photon_energy(): src_per_bqg = m.get_decay_photon_energy(units='Bq/g') src_per_bqkg = m.get_decay_photon_energy(units='Bq/kg') assert pytest.approx(src_per_bqg.integral()) == src_per_bqkg.integral() / 1000. + src_per_bqm3 = m.get_decay_photon_energy(units='Bq/m3') + assert pytest.approx(src_per_bqm3.integral()) == src_per_cm3.integral() * 1e6 # If we add Xe135 (which has a tabular distribution), the photon source # should be a mixture distribution From b215f132180e85791334bcf10dcfa4d5af3e08b1 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Fri, 3 Apr 2026 16:18:19 -0500 Subject: [PATCH 603/671] RAG search tool for agents (#3861) Co-authored-by: John Tramm Co-authored-by: Claude Opus 4.6 --- .claude/tools/openmc_mcp_server.py | 250 +++++++++++++++++++++++++ .claude/tools/rag/chunker.py | 105 +++++++++++ .claude/tools/rag/embeddings.py | 120 ++++++++++++ .claude/tools/rag/indexer.py | 136 ++++++++++++++ .claude/tools/rag/openmc_search.py | 202 ++++++++++++++++++++ .claude/tools/requirements.txt | 8 + .claude/tools/start_server.sh | 34 ++++ .gitignore | 3 + .mcp.json | 9 + AGENTS.md | 50 +++++ CLAUDE.md | 14 ++ docs/source/devguide/agentic-tools.rst | 104 ++++++++++ docs/source/devguide/index.rst | 1 + 13 files changed, 1036 insertions(+) create mode 100644 .claude/tools/openmc_mcp_server.py create mode 100644 .claude/tools/rag/chunker.py create mode 100644 .claude/tools/rag/embeddings.py create mode 100644 .claude/tools/rag/indexer.py create mode 100644 .claude/tools/rag/openmc_search.py create mode 100644 .claude/tools/requirements.txt create mode 100755 .claude/tools/start_server.sh create mode 100644 .mcp.json create mode 100644 CLAUDE.md create mode 100644 docs/source/devguide/agentic-tools.rst diff --git a/.claude/tools/openmc_mcp_server.py b/.claude/tools/openmc_mcp_server.py new file mode 100644 index 000000000..37917abc1 --- /dev/null +++ b/.claude/tools/openmc_mcp_server.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +"""MCP server that exposes OpenMC's RAG semantic search to AI coding agents. + +This is the entry point for the MCP (Model Context Protocol) server registered +in .mcp.json at the repo root. When an MCP-capable agent (e.g. Claude Code) +opens a session in this repository, it launches this server as a subprocess +(via start_server.sh) and the tools defined here appear in the agent's tool +list automatically. + +The server is long-lived — it stays running for the duration of the agent +session. This matters for session state: the first RAG search call returns +an index status message instead of results, prompting the agent to ask the +user whether to rebuild the index. That first-call flag resets each session. + +Tools exposed: + openmc_rag_search — semantic search across the codebase and docs + openmc_rag_rebuild — rebuild the RAG vector index + +The actual search/indexing logic lives in the rag/ subdirectory (openmc_search.py, +indexer.py, chunker.py, embeddings.py). This file is just the MCP interface +layer and session state management. +""" + +from mcp.server.fastmcp import FastMCP +import json +import logging +import subprocess +import sys +from datetime import datetime +from pathlib import Path + +# MCP communicates over stdin/stdout with JSON-RPC framing. Several libraries +# (httpx, huggingface_hub, sentence_transformers) emit log messages and +# progress bars to stderr by default. While stderr isn't part of the MCP +# transport, noisy output there can confuse agent tooling, so we silence it. +logging.getLogger("httpx").setLevel(logging.WARNING) +logging.getLogger("huggingface_hub").setLevel(logging.ERROR) +logging.getLogger("sentence_transformers").setLevel(logging.WARNING) + +# Path constants. This file lives at .claude/tools/openmc_mcp_server.py, +# so parents[2] is the OpenMC repo root. +OPENMC_ROOT = Path(__file__).resolve().parents[2] +CACHE_DIR = OPENMC_ROOT / ".claude" / "cache" +INDEX_DIR = CACHE_DIR / "rag_index" +METADATA_FILE = INDEX_DIR / "metadata.json" + +# The RAG modules (openmc_search, indexer, etc.) live in .claude/tools/rag/. +# We add that directory to sys.path so we can import them directly. +TOOLS_DIR = Path(__file__).resolve().parent +sys.path.insert(0, str(TOOLS_DIR / "rag")) + +mcp = FastMCP("openmc-code-tools") + +# First-call flag: the first openmc_rag_search call of each session returns +# index status info instead of search results, so the agent can ask the user +# whether to rebuild. This resets when the server process restarts (i.e. each +# new agent session). +_rag_first_call = True + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _get_current_branch(): + """Get the current git branch name.""" + try: + result = subprocess.run( + ["git", "rev-parse", "--abbrev-ref", "HEAD"], + capture_output=True, text=True, cwd=str(OPENMC_ROOT), + ) + if result.returncode != 0 or not result.stdout.strip(): + return "unknown" + return result.stdout.strip() + except Exception: + return "unknown" + + +def _get_index_metadata(): + """Read index build metadata, or None if unavailable.""" + if not METADATA_FILE.exists(): + return None + try: + return json.loads(METADATA_FILE.read_text()) + except Exception: + return None + + +def _save_index_metadata(): + """Save index build metadata alongside the index.""" + metadata = { + "built_at": datetime.now().strftime("%Y-%m-%d %H:%M"), + "branch": _get_current_branch(), + } + METADATA_FILE.write_text(json.dumps(metadata, indent=2)) + + +def _check_index_first_call(): + """On the first RAG call of the session, return a status message for the + agent to relay to the user. Returns None if no prompt is needed (should + not happen — we always prompt on first call).""" + current_branch = _get_current_branch() + + if not INDEX_DIR.exists(): + return ( + "No RAG index found. Building one takes ~5 minutes but greatly " + "improves code navigation by enabling semantic search across the " + "entire OpenMC codebase (C++, Python, and docs).\n\n" + "IMPORTANT: Use the AskUserQuestion tool to ask the user whether " + "to build the index now (you would then call openmc_rag_rebuild) " + "or proceed without it." + ) + + meta = _get_index_metadata() + if meta: + built_at = meta.get("built_at", "unknown time") + built_branch = meta.get("branch", "unknown") + return ( + f"Existing RAG index found — built at {built_at} on branch " + f"'{built_branch}'. Current branch is '{current_branch}'.\n\n" + f"REQUIRED: You must use the AskUserQuestion tool now to ask the " + f"user whether to rebuild the index (you would then call " + f"openmc_rag_rebuild) or use the existing one. Do not skip this " + f"step — the user may have uncommitted changes. Do not decide " + f"on their behalf." + ) + + return ( + f"RAG index found but has no build metadata. " + f"Current branch is '{current_branch}'.\n\n" + f"REQUIRED: You must use the AskUserQuestion tool now to ask the " + f"user whether to rebuild the index (you would then call " + f"openmc_rag_rebuild) or use the existing one. Do not skip this " + f"step. Do not decide on their behalf." + ) + + +# --------------------------------------------------------------------------- +# Tools +# --------------------------------------------------------------------------- + +@mcp.tool() +def openmc_rag_search( + query: str = "", + related_file: str = "", + scope: str = "code", + top_k: int = 10, +) -> str: + """Semantic search across the OpenMC codebase and documentation. + + Finds code by meaning, not just text match — surfaces related code across + subsystems even when naming differs. Use for discovery and exploration + before reaching for grep. Covers C++, Python, and RST docs. + + Args: + query: Search query (e.g. "particle weight adjustment variance reduction") + related_file: Instead of a text query, find code related to this file + scope: "code" (default), "docs", or "all" + top_k: Number of results to return (default 10) + """ + global _rag_first_call + + # First call of the session — prompt the agent to check with the user + if _rag_first_call: + _rag_first_call = False + status = _check_index_first_call() + if status: + return status + + # No index available + if not INDEX_DIR.exists(): + return ( + "No RAG index available. Call openmc_rag_rebuild() to build one " + "(takes ~5 minutes)." + ) + + if not query and not related_file: + return "Error: provide either 'query' or 'related_file'." + + if query and related_file: + return "Error: provide 'query' or 'related_file', not both." + + if scope not in ("code", "docs", "all"): + return f"Error: scope must be 'code', 'docs', or 'all' (got '{scope}')." + + if top_k < 1: + return f"Error: top_k must be at least 1 (got {top_k})." + + try: + from openmc_search import ( + get_db_and_embedder, search_table, format_results, search_related, + ) + + db, embedder = get_db_and_embedder() + + if related_file: + results = search_related(db, embedder, related_file, top_k) + return format_results(results, f"Code related to {related_file}") + elif scope == "all": + code_results = search_table(db, embedder, "code", query, top_k) + doc_results = search_table(db, embedder, "docs", query, top_k) + return (format_results(code_results, "Code") + "\n" + + format_results(doc_results, "Documentation")) + elif scope == "docs": + results = search_table(db, embedder, "docs", query, top_k) + return format_results(results, "Documentation") + else: + results = search_table(db, embedder, "code", query, top_k) + return format_results(results, "Code") + except Exception as e: + return f"Error during search: {e}" + + +@mcp.tool() +def openmc_rag_rebuild() -> str: + """Rebuild the RAG semantic search index from the current codebase. + + Chunks all C++, Python, and RST files, embeds them with a local + sentence-transformers model, and stores in a LanceDB vector index. + Takes ~5 minutes on 10 CPU cores. Call this after pulling new code + or switching branches. + """ + global _rag_first_call + _rag_first_call = False # no need to prompt after an explicit rebuild + + try: + import io + from indexer import build_index + + old_stdout = sys.stdout + sys.stdout = captured = io.StringIO() + try: + build_index() + finally: + sys.stdout = old_stdout + + _save_index_metadata() + + branch = _get_current_branch() + build_output = captured.getvalue() + return ( + f"Index rebuilt successfully on branch '{branch}'.\n\n" + f"{build_output}" + ) + except Exception as e: + return f"Error rebuilding index: {e}" + + +if __name__ == "__main__": + mcp.run() diff --git a/.claude/tools/rag/chunker.py b/.claude/tools/rag/chunker.py new file mode 100644 index 000000000..b28ddb0f8 --- /dev/null +++ b/.claude/tools/rag/chunker.py @@ -0,0 +1,105 @@ +"""Split source files into overlapping text chunks for vector embedding. + +The indexer (indexer.py) calls chunk_file() on every C++, Python, and RST file +in the repo. Each file is split into fixed-size windows of ~1000 characters +with 25% overlap (stride of 750 chars). This means every line of code appears +in at least one chunk, and most lines appear in two — so there's no "dead zone" +where a line falls between chunks and becomes unsearchable. + +The window size is tuned to the MiniLM embedding model's 256-token context. +Code averages ~4 characters per token, so 1000 chars ≈ 250 tokens — just +under the model's limit. Chunks are snapped to line boundaries to avoid +splitting mid-line. + +Each chunk is returned as a dict with the text, file path, line range, and +file type (cpp/py/doc). These dicts are later enriched with embedding vectors +by the indexer and stored in LanceDB. +""" + +from pathlib import Path + +# ~256 tokens for MiniLM. 1 token ≈ 4 chars for code. +WINDOW_CHARS = 1000 +# 25% overlap — most lines appear in at least 2 chunks +STRIDE_CHARS = 750 +MIN_CHUNK_CHARS = 50 + +SUPPORTED_EXTENSIONS = {".cpp", ".h", ".py", ".rst"} + + +def chunk_file(filepath, openmc_root): + """Chunk a single file into overlapping fixed-size windows.""" + filepath = Path(filepath) + if filepath.suffix not in SUPPORTED_EXTENSIONS: + return [] + + rel = str(filepath.relative_to(openmc_root)) + try: + content = filepath.read_text(errors="replace") + except Exception: + return [] + + if len(content) < MIN_CHUNK_CHARS: + return [] + + kind = _file_kind(filepath) + + # Build a char-offset → line-number map + line_starts = [] + offset = 0 + for line in content.split("\n"): + line_starts.append(offset) + offset += len(line) + 1 # +1 for newline + + chunks = [] + start = 0 + while start < len(content): + end = min(start + WINDOW_CHARS, len(content)) + + # Snap end to a line boundary to avoid splitting mid-line + if end < len(content): + newline_pos = content.rfind("\n", start, end) + if newline_pos > start: + end = newline_pos + 1 + + text = content[start:end].strip() + if len(text) >= MIN_CHUNK_CHARS: + start_line = _offset_to_line(line_starts, start) + end_line = _offset_to_line(line_starts, end - 1) + chunks.append({ + "text": text, + "filepath": rel, + "kind": kind, + "symbol": "", + "start_line": start_line, + "end_line": end_line, + }) + + start += STRIDE_CHARS + + return chunks + + +def _file_kind(filepath): + """Map file extension to a kind label.""" + ext = filepath.suffix + if ext in (".cpp", ".h"): + return "cpp" + elif ext == ".py": + return "py" + elif ext == ".rst": + return "doc" + return "other" + + +def _offset_to_line(line_starts, offset): + """Convert a character offset to a 1-based line number.""" + # Binary search for the line containing this offset + lo, hi = 0, len(line_starts) - 1 + while lo < hi: + mid = (lo + hi + 1) // 2 + if line_starts[mid] <= offset: + lo = mid + else: + hi = mid - 1 + return lo + 1 # 1-based diff --git a/.claude/tools/rag/embeddings.py b/.claude/tools/rag/embeddings.py new file mode 100644 index 000000000..1fe85b50d --- /dev/null +++ b/.claude/tools/rag/embeddings.py @@ -0,0 +1,120 @@ +"""Thin wrapper around sentence-transformers for embedding text into vectors. + +Uses the all-MiniLM-L6-v2 model — a small (22M param, 384-dim) model that +runs on CPU with no GPU or API key required. + +Network behavior and privacy +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +No user code, queries, or file contents are EVER sent to HuggingFace or any +external service. All embedding computation happens locally. The only network +activity is the one-time model download on first use: + + First run (model not yet cached, ~80MB download): + - Downloads model weight files from huggingface.co. This is a standard + HTTP file download, similar to pip installing a package. + - The only metadata sent in these requests is an HTTP user-agent header + containing library version numbers (e.g. "hf_hub/1.6.0; + python/3.12.3; torch/2.10.0"). No filenames, file contents, queries, + or any user-identifiable information is sent. + - The huggingface_hub library has an optional feature where it can report + anonymous library usage statistics (just version numbers, not user + data) back to HuggingFace. We disable this by setting + HF_HUB_DISABLE_TELEMETRY=1. + + Subsequent runs (model already cached): + - We set HF_HUB_OFFLINE=1 automatically (see _set_offline_if_cached() + below), which prevents ALL network calls. The model loads entirely + from the local cache at ~/.cache/huggingface/hub/. Zero bytes leave + the machine. + +How the model is downloaded +~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The SentenceTransformer() constructor (called in __init__ below) handles +the download automatically on first use. It calls into the huggingface_hub +library, which downloads the model files from: + + https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2 + +The files are saved to ~/.cache/huggingface/hub/ and reused on subsequent +runs. We pass token=False to ensure no authentication token is sent. + +This module is imported by both the MCP server (for search queries) and the +indexer (for bulk embedding of code chunks). The bulk embed() call shows a +progress bar; the single-query embed_query() does not. + +The env vars below must be set before importing transformers or +sentence_transformers. They suppress warnings and progress bars that these +libraries emit by default. Stray stderr output would interfere with the MCP +server's JSON-RPC transport. +""" + +import os +from pathlib import Path + +MODEL_NAME = "all-MiniLM-L6-v2" + +# These env vars control logging behavior in the HuggingFace libraries. +# They must be set before the libraries are imported. +os.environ.setdefault("TRANSFORMERS_VERBOSITY", "error") # suppress warnings +os.environ.setdefault("HF_HUB_VERBOSITY", "error") # suppress warnings +os.environ.setdefault("HF_HUB_DISABLE_PROGRESS_BARS", "1") +os.environ.setdefault("TOKENIZERS_PARALLELISM", "false") # suppress threading warning +# Disable anonymous library usage statistics (version numbers only, not user +# data — but we disable it anyway as a matter of policy). +os.environ.setdefault("HF_HUB_DISABLE_TELEMETRY", "1") + + +def _set_offline_if_cached(): + """If the model has already been downloaded, tell huggingface_hub to + skip all network calls by setting HF_HUB_OFFLINE=1. + + Without this, huggingface_hub makes an HTTP request to huggingface.co + on every load to check if the cached model is still up to date — even + though the model never changes. Setting HF_HUB_OFFLINE=1 prevents this. + + This must run before sentence_transformers is imported, because the + library reads the env var at import time. + """ + # HuggingFace caches downloaded models under ~/.cache/huggingface/hub/ + # in directories named like "models--sentence-transformers--all-MiniLM-L6-v2". + # The HF_HOME env var can override the base cache location. + hf_home = os.environ.get("HF_HOME") + if hf_home: + cache_dir = Path(hf_home) / "hub" + else: + cache_dir = Path.home() / ".cache" / "huggingface" / "hub" + + model_dir = cache_dir / f"models--sentence-transformers--{MODEL_NAME}" + if model_dir.exists(): + os.environ.setdefault("HF_HUB_OFFLINE", "1") + + +_set_offline_if_cached() + +# This import must come after the env vars above are set, because the +# transformers library reads them at import time. +import transformers +transformers.logging.disable_progress_bar() + + +class EmbeddingProvider: + """Sentence-transformers embedder using all-MiniLM-L6-v2.""" + + def __init__(self, model_name: str = MODEL_NAME): + from sentence_transformers import SentenceTransformer + + # This constructor loads the model from the local cache. If the model + # has not been downloaded yet, it downloads it from huggingface.co + # (~80MB, one-time). token=False ensures no auth token is sent. + self.model = SentenceTransformer(model_name, token=False) + self.dim = self.model.get_sentence_embedding_dimension() + + def embed(self, texts: list[str]) -> list[list[float]]: + """Embed a list of texts into vectors.""" + embeddings = self.model.encode(texts, show_progress_bar=True, + batch_size=64) + return embeddings.tolist() + + def embed_query(self, text: str) -> list[float]: + """Embed a single query text.""" + return self.model.encode([text])[0].tolist() diff --git a/.claude/tools/rag/indexer.py b/.claude/tools/rag/indexer.py new file mode 100644 index 000000000..34613092b --- /dev/null +++ b/.claude/tools/rag/indexer.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +"""Build the RAG vector index for the OpenMC codebase. + +This is the index-building half of the RAG pipeline. All operations are local +once the embedding model has been downloaded and cached (see embeddings.py for +details on model download, caching, and network behavior). It walks the repo, +chunks every +C++/Python/RST file (via chunker.py), embeds all chunks into 384-dim vectors +(via embeddings.py), and stores them in a local LanceDB database on disk. The +result is a .claude/cache/rag_index/ directory containing two tables — "code" +and "docs" — that openmc_search.py queries at search time. + +Building the full index takes ~5 minutes on a 10-core machine. The bottleneck +is the embedding step (running all chunks through the MiniLM model on CPU). + +Can be run standalone: python indexer.py +Or called programmatically: from indexer import build_index; build_index() +The MCP server (openmc_mcp_server.py) uses the latter when the agent calls +openmc_rag_rebuild. +""" + +import lancedb +import sys +import time +from pathlib import Path + +# This file lives at .claude/tools/rag/indexer.py. The sys.path insert lets +# us import sibling modules (embeddings, chunker) when run as a standalone +# script. When imported from the MCP server, the server has already done this. +TOOLS_DIR = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(TOOLS_DIR / "rag")) + +from embeddings import EmbeddingProvider +from chunker import chunk_file + + +OPENMC_ROOT = Path(__file__).resolve().parents[3] +CACHE_DIR = OPENMC_ROOT / ".claude" / "cache" +INDEX_DIR = CACHE_DIR / "rag_index" + +CODE_PATTERNS = [ + "src/**/*.cpp", + "include/openmc/**/*.h", + "openmc/**/*.py", + "tests/**/*.py", + "examples/**/*.py", +] + +DOC_PATTERNS = [ + "docs/**/*.rst", +] + + +def collect_chunks(patterns, openmc_root): + """Collect all chunks from files matching the given patterns.""" + chunks = [] + for pattern in patterns: + for filepath in sorted(openmc_root.glob(pattern)): + if "__pycache__" in str(filepath): + continue + file_chunks = chunk_file(filepath, openmc_root) + chunks.extend(file_chunks) + return chunks + + +def build_index(): + """Build or rebuild the complete vector index.""" + start = time.time() + + # Collect all chunks + print("Collecting code chunks...") + code_chunks = collect_chunks(CODE_PATTERNS, OPENMC_ROOT) + print(f" {len(code_chunks)} code chunks") + + print("Collecting doc chunks...") + doc_chunks = collect_chunks(DOC_PATTERNS, OPENMC_ROOT) + print(f" {len(doc_chunks)} doc chunks") + + all_chunks = code_chunks + doc_chunks + if not all_chunks: + print("ERROR: No chunks collected!", file=sys.stderr) + sys.exit(1) + + # Create embeddings + all_texts = [c["text"] for c in all_chunks] + print("Creating embedding provider...") + embedder = EmbeddingProvider() + print(f" dim={embedder.dim}") + + print("Embedding chunks...") + all_embeddings = embedder.embed(all_texts) + + # Build LanceDB tables + INDEX_DIR.mkdir(parents=True, exist_ok=True) + db = lancedb.connect(str(INDEX_DIR)) + + # Separate code vs doc records by index (code_chunks come first in all_chunks) + n_code = len(code_chunks) + code_records = [] + doc_records = [] + for i, (chunk, emb) in enumerate(zip(all_chunks, all_embeddings)): + record = { + "text": chunk["text"], + "filepath": chunk["filepath"], + "kind": chunk["kind"], + "symbol": chunk.get("symbol", ""), + "start_line": chunk.get("start_line", 0), + "end_line": chunk.get("end_line", 0), + "vector": emb, + } + if i < n_code: + code_records.append(record) + else: + doc_records.append(record) + + # Create tables (drop existing) + result = db.table_names() if hasattr(db, "table_names") else db.list_tables() + existing = result.tables if hasattr(result, "tables") else list(result) + for table_name in ("code", "docs"): + if table_name in existing: + db.drop_table(table_name) + + if code_records: + db.create_table("code", code_records) + print(f" Created 'code' table: {len(code_records)} rows") + + if doc_records: + db.create_table("docs", doc_records) + print(f" Created 'docs' table: {len(doc_records)} rows") + + elapsed = time.time() - start + print(f"Done in {elapsed:.1f}s") + + +if __name__ == "__main__": + build_index() diff --git a/.claude/tools/rag/openmc_search.py b/.claude/tools/rag/openmc_search.py new file mode 100644 index 000000000..4125ee966 --- /dev/null +++ b/.claude/tools/rag/openmc_search.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +"""Query the RAG vector index to find semantically related code and docs. + +This is the query-time half of the RAG pipeline (the counterpart to indexer.py, +which builds the index). All operations are local — no network calls are made +once the embedding model has been downloaded (see embeddings.py for details on +model download and caching). Given a natural-language query, it embeds the query +with the same MiniLM model +used at index time, then finds the closest chunks in the local LanceDB vector +database by cosine similarity. + +The core functions (get_db_and_embedder, search_table, format_results, +search_related) are imported by the MCP server for tool calls. The script +can also be run standalone from the command line. + +The "related file" mode works differently from a text query: it reads the +target file's chunks from the index, combines them into a synthetic query +vector, and searches for the nearest chunks from *other* files. This surfaces +files that are semantically similar to the target file. + +Usage: + openmc_search.py "query" # Search code (default) + openmc_search.py "query" --docs # Search documentation + openmc_search.py "query" --all # Search both code and docs + openmc_search.py --related src/particle.cpp # Find related code + openmc_search.py "query" --top-k 20 # Return more results +""" + +import argparse +import sys +from pathlib import Path + +# Same sys.path setup as indexer.py — needed for standalone CLI use. +TOOLS_DIR = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(TOOLS_DIR / "rag")) + +OPENMC_ROOT = Path(__file__).resolve().parents[3] +CACHE_DIR = OPENMC_ROOT / ".claude" / "cache" +INDEX_DIR = CACHE_DIR / "rag_index" + + +def get_db_and_embedder(): + """Load the LanceDB database and embedding provider.""" + import lancedb + from embeddings import EmbeddingProvider + + if not INDEX_DIR.exists(): + raise FileNotFoundError( + "No RAG index found. Call openmc_rag_rebuild() to build one." + ) + + db = lancedb.connect(str(INDEX_DIR)) + + embedder = EmbeddingProvider() + return db, embedder + + +def _table_names(db): + """Return table names as a list, compatible with multiple LanceDB versions.""" + result = db.table_names() if hasattr(db, "table_names") else db.list_tables() + return result.tables if hasattr(result, "tables") else list(result) + + +def search_table(db, embedder, table_name, query, top_k): + """Search a LanceDB table with a text query.""" + if table_name not in _table_names(db): + print(f"Table '{table_name}' not found in index.", file=sys.stderr) + return [] + + table = db.open_table(table_name) + query_vec = embedder.embed_query(query) + results = table.search(query_vec).limit(top_k).to_list() + return results + + +def format_results(results, label=""): + """Format search results for display.""" + if not results: + return "No results found.\n" + + output = [] + if label: + output.append(f"=== {label} ===\n") + + for i, r in enumerate(results, 1): + filepath = r["filepath"] + start = r["start_line"] + end = r["end_line"] + kind = r["kind"] + dist = r.get("_distance", 0) + + header = f"[{i}] {filepath}:{start}-{end} ({kind}, dist={dist:.3f})" + output.append(header) + + # Show text preview (first 500 chars) + text = r["text"][:500] + if len(r["text"]) > 500: + text += "\n ..." + # Indent the text + for line in text.split("\n"): + output.append(f" {line}") + output.append("") + + return "\n".join(output) + + +def search_related(db, embedder, filepath, top_k): + """Find code related to a given file.""" + if "code" not in _table_names(db): + print("No 'code' table in index.", file=sys.stderr) + return [] + + table = db.open_table("code") + + # Normalize filepath + fp = filepath + if Path(filepath).is_absolute(): + try: + fp = str(Path(filepath).relative_to(OPENMC_ROOT)) + except ValueError: + pass + + # Get chunks from target file + try: + safe_fp = fp.replace("'", "''") + target_chunks = table.search().where( + f"filepath = '{safe_fp}'" + ).limit(50).to_list() + except Exception: + # LanceDB where clause might not work in all versions + # Fall back to fetching all and filtering + all_data = table.to_pandas() + target_rows = all_data[all_data["filepath"] == fp] + if target_rows.empty: + print(f"No chunks found for '{fp}'", file=sys.stderr) + return [] + target_chunks = target_rows.head(50).to_dict("records") + + if not target_chunks: + print(f"No chunks found for '{fp}'", file=sys.stderr) + return [] + + # Combine top chunks as the query + combined_text = " ".join(c["text"][:200] for c in target_chunks[:5]) + query_vec = embedder.embed_query(combined_text) + + # Search excluding the source file + results = table.search(query_vec).limit(top_k + 10).to_list() + # Filter out same file + results = [r for r in results if r["filepath"] != fp][:top_k] + return results + + +def main(): + parser = argparse.ArgumentParser( + description="Semantic search across OpenMC codebase and docs", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog="""examples: + %(prog)s "particle random number seed initialization" + %(prog)s "how to define tallies" --docs + %(prog)s "weight window variance reduction" --all + %(prog)s "where is cross section data loaded" --top-k 15 + %(prog)s --related src/simulation.cpp + %(prog)s --related src/particle_restart.cpp --top-k 5""", + ) + parser.add_argument("query", nargs="?", help="Search query") + parser.add_argument("--docs", action="store_true", + help="Search documentation instead of code") + parser.add_argument("--all", action="store_true", + help="Search both code and documentation") + parser.add_argument("--related", metavar="FILE", + help="Find code related to a given file") + parser.add_argument("--top-k", type=int, default=10, + help="Number of results (default: 10)") + args = parser.parse_args() + + if not args.query and not args.related: + parser.print_help() + sys.exit(1) + + db, embedder = get_db_and_embedder() + + if args.related: + results = search_related(db, embedder, args.related, args.top_k) + print(format_results(results, f"Code related to {args.related}")) + elif args.all: + code_results = search_table( + db, embedder, "code", args.query, args.top_k) + doc_results = search_table( + db, embedder, "docs", args.query, args.top_k) + print(format_results(code_results, "Code")) + print(format_results(doc_results, "Documentation")) + elif args.docs: + results = search_table(db, embedder, "docs", args.query, args.top_k) + print(format_results(results, "Documentation")) + else: + results = search_table(db, embedder, "code", args.query, args.top_k) + print(format_results(results, "Code")) + + +if __name__ == "__main__": + main() diff --git a/.claude/tools/requirements.txt b/.claude/tools/requirements.txt new file mode 100644 index 000000000..bd5d38d6c --- /dev/null +++ b/.claude/tools/requirements.txt @@ -0,0 +1,8 @@ +# MCP server +mcp>=1.0.0 + +# Vector database +lancedb>=0.15.0 + +# Embeddings (local, no API key) +sentence-transformers>=2.7.0 diff --git a/.claude/tools/start_server.sh b/.claude/tools/start_server.sh new file mode 100755 index 000000000..c111dd73e --- /dev/null +++ b/.claude/tools/start_server.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# Bootstrap the Python venv (if needed) and start the OpenMC MCP server. +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +CACHE_DIR="$(dirname "$SCRIPT_DIR")/cache" +VENV_DIR="$CACHE_DIR/.venv" +SENTINEL="$VENV_DIR/.installed" + +if ! command -v python3 >/dev/null 2>&1; then + echo "Error: python3 not found on PATH." >&2 + exit 1 +fi + +if ! python3 -c 'import sys; assert sys.version_info >= (3,12)' 2>/dev/null; then + echo "Error: Python 3.12+ is required." >&2 + exit 1 +fi + +if [ ! -f "$SENTINEL" ]; then + rm -rf "$VENV_DIR" + mkdir -p "$CACHE_DIR" + python3 -m venv "$VENV_DIR" + + if ! "$VENV_DIR/bin/pip" install -q -r "$SCRIPT_DIR/requirements.txt"; then + echo "Error: pip install failed. Remove $VENV_DIR and retry." >&2 + rm -rf "$VENV_DIR" + exit 1 + fi + + touch "$SENTINEL" +fi + +exec "$VENV_DIR/bin/python" "$SCRIPT_DIR/openmc_mcp_server.py" diff --git a/.gitignore b/.gitignore index 780059f30..dd8dfb14a 100644 --- a/.gitignore +++ b/.gitignore @@ -104,5 +104,8 @@ CMakeSettings.json # Visual Studio Code configuration files .vscode/ +# Claude Code agent tools (cached/generated artifacts) +.claude/cache/ + # Python pickle files *.pkl diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 000000000..bdfaa538e --- /dev/null +++ b/.mcp.json @@ -0,0 +1,9 @@ +{ + "mcpServers": { + "openmc-code-tools": { + "type": "stdio", + "command": "bash", + "args": [".claude/tools/start_server.sh"] + } + } +} diff --git a/AGENTS.md b/AGENTS.md index dce32d0e4..19abba7d9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,6 +42,56 @@ OpenMC uses a git flow branching model with two primary branches: When reviewing code changes in this repository, use the `reviewing-openmc-code` skill. +## Codebase Navigation Tools + +Two MCP tools are registered in `.mcp.json` at the repo root and appear +automatically in any MCP-capable agent session. + +**`openmc_rag_search`** — Semantic search across the codebase (C++, Python, RST +docs). Finds code by meaning, not just text match. Surfaces related code across +subsystems even when naming differs (e.g., "particle RNG seeding" finds code +across transport, restart, and random ray modes — files you would never find +with `grep "particle seed"`). The index uses a small 22M-param embedding model +(384-dim). Phrase-level natural-language queries work much better than single +keywords or symbol names. + +**`openmc_rag_rebuild`** — Rebuild the RAG vector index. Call after pulling new +code or switching branches. The first RAG search of each session will report +the index status and ask whether to rebuild — you can also call this explicitly. + +### Why RAG matters + +OpenMC is large enough that changes in one subsystem can silently break +invariants that distant subsystems depend on — and those distant files often +use different naming, so grep won't find them. The RAG search finds code by +meaning, surfacing files you wouldn't have thought to open. + +An agent reviewed a large OpenMC PR without RAG. It found 1 of 11 serious +bugs. Its post-mortem: + +> **I treated the diff as a closed system.** I verified internal consistency of +> the changed code obsessively, but never built a global understanding of how +> the changed code fits into the wider codebase. The diff altered assumptions +> that code elsewhere silently relied on — but I couldn't see that because I +> never looked beyond the diff. I couldn't see the forest for the trees. +> +> **Why I resisted RAG:** Overconfidence. My internal model was "I can see the +> diff, I understand the data structures, I can trace the logic." The diff felt +> self-contained. RAG felt like it would return noisy results about tangentially +> related code. But in a codebase this large, changes in one subsystem can +> quietly break invariants that distant subsystems depend on — and you need +> global awareness to foresee that. +> +> **In the post-mortem**, I re-ran the RAG queries I should have run during the +> review. They directly surfaced the files containing the bugs I missed — files +> I never thought to open because they weren't in the diff. + +The takeaway: when reviewing or modifying code, ask yourself "what else in this +codebase might depend on the behavior I'm changing?" If you aren't sure, that's +a good time for a RAG query. It won't replace the grep-based investigation you +should already be doing — but it can surface files you wouldn't have thought to +open. + ### Workflow for contributors 1. Create a feature/bugfix branch off `develop` diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..9538b5ddc --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,14 @@ +## OpenMC Codebase Tools + +Read the FULL `AGENTS.md` in this directory before starting work. It contains +project context, coding conventions, and documentation of the RAG search tools +registered in `.mcp.json`. + +### Claude Code-specific: first-call behavior + +The first `openmc_rag_search` call of each session returns an index status +message instead of search results. When this happens, you MUST use the +`AskUserQuestion` tool to present the rebuild/use-existing choice to the user. +Do not ask conversationally — always use the widget. Do not skip this step even +if the index looks current — the user may have uncommitted changes that warrant +a rebuild. diff --git a/docs/source/devguide/agentic-tools.rst b/docs/source/devguide/agentic-tools.rst new file mode 100644 index 000000000..fed377cc0 --- /dev/null +++ b/docs/source/devguide/agentic-tools.rst @@ -0,0 +1,104 @@ +.. _devguide_agentic_tools: + +=========================== +Agentic Development Tools +=========================== + +OpenMC ships a set of tools designed for AI coding agents (such as +`Claude Code`_) that agents can use to navigate and understand the codebase. + +.. _Claude Code: https://claude.ai/code + +Motivation +---------- + +Agentic tools like Claude Code are skilled at using grep to navigate and +understand large code bases. However, grep can only find exact text matches — +it cannot discover code that is *conceptually* related but uses different +naming. Without a "global view" of the codebase that a human developer will +build up over time, the agent is generally blind to any file it hasn't +tokenized fully. While it can grep to see who else calls a function, it +remains blind if other areas might be related but not share identical naming +conventions. + +This problem is mitigated somewhat by using a model with a longer context +window. OpenMC has somewhere around ~1 million tokens of C++ and ~1 million +tokens of python. While Claude Code in early 2026 only has a context window +of 200k tokens, beta versions have extended context windows of 1M tokens, +and it's not unreasonable to assume that models may be available in the near +future that greatly exceed these limits. + +However, even assuming the entire repository can be fit within a context +window, there are several downsides to doing this. +`Model performance degrades significantly as context size increases`_. +Benchmark results are +greatly improved if the model has less garbage to pick through. Additionally, API usage +is typically billed as tokens in/out per turn. As the context file +grows these costs become much larger. As such, there is still significant +motivation to solving the above problem, so as to ensure only relevant +information is drawn into context so as to maximize model performance and +minimize costs. + +Setup +----- + +The tools are registered as an `MCP (Model Context Protocol)`_ server in +``.mcp.json`` at the repository root. AI agents that support MCP (such as +Claude Code) discover them automatically on session start. The underlying +Python scripts can also be run directly from the command line. + +All tools run entirely locally — no API keys or external service accounts are +required. Python dependencies are installed automatically into an isolated +virtual environment at ``.claude/cache/.venv/`` on first use. + +.. _Model performance degrades significantly as context size increases: https://www.anthropic.com/news/claude-opus-4-6 +.. _MCP (Model Context Protocol): https://modelcontextprotocol.io + +RAG Semantic Search +------------------- + +The RAG (Retrieval-Augmented Generation) semantic search addresses this +problem — it finds code by meaning, not just text match, surfacing related code +across subsystems that ``grep`` would miss entirely. Two MCP tools are provided: + +- **openmc_rag_search** — Given a natural-language query, returns the most + relevant code chunks with file paths, line numbers, and a preview. Can search + code, documentation, or both. Can also find code related to a given file. +- **openmc_rag_rebuild** — Rebuilds the search index. Should be called after + pulling new code or switching branches. + +How it works +^^^^^^^^^^^^ + +The search pipeline runs entirely on your local CPU: + +1. **Chunking.** All C++, Python, and RST files are split into overlapping + fixed-size windows (~1000 characters, 25% overlap). This ensures every line + of code appears in at least one chunk and most lines appear in two. + +2. **Embedding.** Each chunk is embedded into a 384-dimensional vector using + the `all-MiniLM-L6-v2`_ sentence-transformer model (22 million parameters). + This model runs on CPU with no GPU required. No API key is needed — the + model weights are downloaded once from Hugging Face and cached locally. + +3. **Indexing.** The vectors are stored in a local LanceDB_ database on disk. + Building the full index takes approximately 5 minutes on a machine with + 10 CPU cores. The index is stored in ``.claude/cache/rag_index/`` and + persists across sessions. + +4. **Searching.** Your query is embedded using the same model, and the closest + chunks are retrieved by vector similarity. Results include the file path, + line range, file type, similarity distance, and a text preview. + +.. _all-MiniLM-L6-v2: https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2 +.. _LanceDB: https://lancedb.com + +Requirements +^^^^^^^^^^^^ + +No system dependencies beyond **Python 3.12+** with ``pip``. An internet +connection is required on first use to download the Python packages and +embedding model weights; subsequent runs are fully offline. The Python packages +(``sentence-transformers``, ``lancedb``) and their dependencies (including +PyTorch, ~2GB) are installed automatically into an isolated virtual environment +on first use. diff --git a/docs/source/devguide/index.rst b/docs/source/devguide/index.rst index 2e131e094..53b9f5853 100644 --- a/docs/source/devguide/index.rst +++ b/docs/source/devguide/index.rst @@ -14,6 +14,7 @@ other related topics. contributing workflow + agentic-tools styleguide policies tests From 60d1dfba7f6e3faec2e985aec63de4a1b39bc253 Mon Sep 17 00:00:00 2001 From: Ethan Peterson Date: Fri, 3 Apr 2026 17:27:39 -0400 Subject: [PATCH 604/671] Refactor form_matrix method on depletion chain class (#3892) Co-authored-by: Paul Romano --- openmc/deplete/chain.py | 241 +++++++++++++++---------- tests/unit_tests/test_deplete_chain.py | 23 +++ 2 files changed, 171 insertions(+), 93 deletions(-) diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index 689546b86..514f8ed22 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -269,6 +269,7 @@ class Chain: self.reactions = [] self.nuclide_dict = {} self._fission_yields = None + self._decay_matrix = None def __contains__(self, nuclide): return nuclide in self.nuclide_dict @@ -604,8 +605,147 @@ class Chain: out[nuc.name] = dict(yield_obj) return out + @property + def decay_matrix(self): + """Sparse CSC decay transmutation matrix. + + Contains only terms from radioactive decay: diagonal loss terms + and off-diagonal gain terms (branching ratios, alpha/proton + production). Independent of reaction rates, so computed once and + cached. + + See Also + -------- + :meth:`form_rxn_matrix`, :meth:`form_matrix` + """ + if self._decay_matrix is None: + n = len(self) + rows, cols, vals = [], [], [] + + def setval(i, j, val): + rows.append(i) + cols.append(j) + vals.append(val) + + for i, nuc in enumerate(self.nuclides): + # Loss from radioactive decay + if nuc.half_life is not None: + decay_constant = math.log(2) / nuc.half_life + if decay_constant != 0.0: + setval(i, i, -decay_constant) + + # Gain from radioactive decay + if nuc.n_decay_modes != 0: + for decay_type, target, branching_ratio in nuc.decay_modes: + branch_val = branching_ratio * decay_constant + + # Allow for total annihilation for debug purposes + if branch_val != 0.0: + if target is not None: + k = self.nuclide_dict[target] + setval(k, i, branch_val) + + # Produce alphas and protons from decay + if 'alpha' in decay_type: + k = self.nuclide_dict.get('He4') + if k is not None: + count = decay_type.count('alpha') + setval(k, i, count * branch_val) + elif 'p' in decay_type: + k = self.nuclide_dict.get('H1') + if k is not None: + count = decay_type.count('p') + setval(k, i, count * branch_val) + + self._decay_matrix = csc_array((vals, (rows, cols)), shape=(n, n)) + return self._decay_matrix + + def form_rxn_matrix(self, rates, fission_yields=None): + """Form the reaction-rate portion of the transmutation matrix. + + Builds only the terms that depend on reaction rates: transmutation + reactions and fission product yields. Does not include radioactive + decay terms (see :attr:`decay_matrix`). + + Parameters + ---------- + rates : numpy.ndarray + 2D array indexed by (nuclide, reaction) + fission_yields : dict, optional + Option to use a custom set of fission yields. Expected + to be of the form ``{parent : {product : f_yield}}`` + with string nuclide names for ``parent`` and ``product``, + and ``f_yield`` as the respective fission yield + + Returns + ------- + scipy.sparse.csc_array + Sparse matrix representing reaction-rate terms. + + See Also + -------- + :attr:`decay_matrix`, :meth:`form_matrix` + """ + reactions = set() + n = len(self) + + # Accumulate indices/values and then create the matrix at the end to + # avoid expensive index checks scipy otherwise does. + rows, cols, vals = [], [], [] + + def setval(i, j, val): + rows.append(i) + cols.append(j) + vals.append(val) + + if fission_yields is None: + fission_yields = self.get_default_fission_yields() + + for i, nuc in enumerate(self.nuclides): + if nuc.name not in rates.index_nuc: + continue + + nuc_ind = rates.index_nuc[nuc.name] + nuc_rates = rates[nuc_ind, :] + + for r_type, target, _, br in nuc.reactions: + r_id = rates.index_rx[r_type] + path_rate = nuc_rates[r_id] + + # Loss term -- make sure we only count loss once for + # reactions with branching ratios + if r_type not in reactions: + reactions.add(r_type) + if path_rate != 0.0: + setval(i, i, -path_rate) + + # Gain term; allow for total annihilation for debug purposes + if r_type != 'fission': + if target is not None and path_rate != 0.0: + k = self.nuclide_dict[target] + setval(k, i, path_rate * br) + + # Determine light nuclide production, e.g., (n,d) should + # produce H2 + light_nucs = REACTIONS[r_type].secondaries + for light_nuc in light_nucs: + k = self.nuclide_dict.get(light_nuc) + if k is not None: + setval(k, i, path_rate * br) + + else: + for product, y in fission_yields[nuc.name].items(): + yield_val = y * path_rate + if yield_val != 0.0: + k = self.nuclide_dict[product] + setval(k, i, yield_val) + + reactions.clear() + + return csc_array((vals, (rows, cols)), shape=(n, n)) + def form_matrix(self, rates, fission_yields=None): - """Forms depletion matrix. + """Form the full transmutation matrix (decay + reactions). Parameters ---------- @@ -624,96 +764,10 @@ class Chain: See Also -------- + :attr:`decay_matrix`, :meth:`form_rxn_matrix`, :meth:`get_default_fission_yields` """ - reactions = set() - - n = len(self) - - # we accumulate indices and value entries for everything and create the matrix - # in one step at the end to avoid expensive index checks scipy otherwise does. - rows, cols, vals = [], [], [] - def setval(i, j, val): - rows.append(i) - cols.append(j) - vals.append(val) - - if fission_yields is None: - fission_yields = self.get_default_fission_yields() - - for i, nuc in enumerate(self.nuclides): - # Loss from radioactive decay - if nuc.half_life is not None: - decay_constant = math.log(2) / nuc.half_life - if decay_constant != 0.0: - setval(i, i, -decay_constant) - - # Gain from radioactive decay - if nuc.n_decay_modes != 0: - for decay_type, target, branching_ratio in nuc.decay_modes: - branch_val = branching_ratio * decay_constant - - # Allow for total annihilation for debug purposes - if branch_val != 0.0: - if target is not None: - k = self.nuclide_dict[target] - setval(k, i, branch_val) - - # Produce alphas and protons from decay - if 'alpha' in decay_type: - k = self.nuclide_dict.get('He4') - if k is not None: - count = decay_type.count('alpha') - setval(k, i, count * branch_val) - elif 'p' in decay_type: - k = self.nuclide_dict.get('H1') - if k is not None: - count = decay_type.count('p') - setval(k, i, count * branch_val) - - if nuc.name in rates.index_nuc: - # Extract all reactions for this nuclide in this cell - nuc_ind = rates.index_nuc[nuc.name] - nuc_rates = rates[nuc_ind, :] - - for r_type, target, _, br in nuc.reactions: - # Extract reaction index, and then final reaction rate - r_id = rates.index_rx[r_type] - path_rate = nuc_rates[r_id] - - # Loss term -- make sure we only count loss once for - # reactions with branching ratios - if r_type not in reactions: - reactions.add(r_type) - if path_rate != 0.0: - setval(i, i, -path_rate) - - # Gain term; allow for total annihilation for debug purposes - if r_type != 'fission': - if target is not None and path_rate != 0.0: - k = self.nuclide_dict[target] - setval(k, i, path_rate * br) - - # Determine light nuclide production, e.g., (n,d) should - # produce H2 - light_nucs = REACTIONS[r_type].secondaries - for light_nuc in light_nucs: - k = self.nuclide_dict.get(light_nuc) - if k is not None: - setval(k, i, path_rate * br) - - else: - for product, y in fission_yields[nuc.name].items(): - yield_val = y * path_rate - if yield_val != 0.0: - k = self.nuclide_dict[product] - setval(k, i, yield_val) - - # Clear set of reactions - reactions.clear() - - # Return CSC representation instead of DOK - return csc_array((vals, (rows, cols)), shape=(n, n)) + return self.decay_matrix + self.form_rxn_matrix(rates, fission_yields) def add_redox_term(self, matrix, buffer, oxidation_states): r"""Adds a redox term to the depletion matrix from data contained in @@ -807,7 +861,7 @@ 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) @@ -816,8 +870,8 @@ class Chain: else: mat = mats dest_mat = None - - # Build transfer term + + # Build transfer term components = tr_rates.get_components(mat, current_timestep, dest_mat) for i, nuc in enumerate(self.nuclides): @@ -829,7 +883,7 @@ class Chain: 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() @@ -1363,6 +1417,7 @@ def _get_chain( def _invalidate_chain_cache(chain): """Invalidate the cache for a specific Chain (when it is modifed).""" + chain._decay_matrix = None if hasattr(chain, '_xml_path'): # Remove all entries with the same path as self._xml_path for key in list(_CHAIN_CACHE.keys()): diff --git a/tests/unit_tests/test_deplete_chain.py b/tests/unit_tests/test_deplete_chain.py index e90b61022..ef8cb9ef2 100644 --- a/tests/unit_tests/test_deplete_chain.py +++ b/tests/unit_tests/test_deplete_chain.py @@ -246,6 +246,29 @@ def test_form_matrix(simple_chain): assert new_mat[r, c] == mat[r, c] +def test_decay_matrix(simple_chain): + """Test that decay_matrix contains only radioactive decay terms.""" + # Nuclide order: H1(0), A(1), B(2), C(3) + decay_A = log(2) / 2.36520E+04 + decay_B = log(2) / 3.29040E+04 + + expected = np.zeros((4, 4)) + expected[1, 1] = -decay_A # Loss: A decays + expected[2, 1] = decay_A * 0.6 # A -> B (branching ratio 0.6) + expected[3, 1] = decay_A * 0.4 # A -> C (branching ratio 0.4) + expected[1, 2] = decay_B # B -> A (branching ratio 1.0) + expected[2, 2] = -decay_B # Loss: B decays + + assert np.allclose(expected, simple_chain.decay_matrix.toarray()) + + +def test_decay_matrix_cached(simple_chain): + """Test that decay_matrix is lazily computed and returns the same object.""" + m1 = simple_chain.decay_matrix + m2 = simple_chain.decay_matrix + assert m1 is m2 + + def test_getitem(): """Test nuc_by_ind converter function.""" chain = Chain() From efc542825eae13738649e77496f042966cc963c8 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Sat, 4 Apr 2026 23:32:51 +0300 Subject: [PATCH 605/671] Add the ability to tally microscopic cross sections in void materials with tracklength estimator (#3771) Co-authored-by: Jonathan Shimwell Co-authored-by: Paul Romano --- src/tallies/tally_scoring.cpp | 56 +++++++++++++++++------------------ tests/unit_tests/test_void.py | 42 ++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 29 deletions(-) create mode 100644 tests/unit_tests/test_void.py diff --git a/src/tallies/tally_scoring.cpp b/src/tallies/tally_scoring.cpp index d5af99382..5dcd07331 100644 --- a/src/tallies/tally_scoring.cpp +++ b/src/tallies/tally_scoring.cpp @@ -2436,24 +2436,22 @@ void score_tracklength_tally_general( if (p.material() != MATERIAL_VOID) { const auto& mat = model::materials[p.material()]; auto j = mat->mat_nuclide_index_[i_nuclide]; - if (j == C_NONE) { - // Determine log union grid index - if (i_log_union == C_NONE) { - int neutron = ParticleType::neutron().transport_index(); - i_log_union = std::log(p.E() / data::energy_min[neutron]) / - simulation::log_spacing; - } - - // Update micro xs cache - if (!tally.multiply_density()) { - p.update_neutron_xs(i_nuclide, i_log_union); - atom_density = 1.0; - } - } else { - atom_density = tally.multiply_density() - ? mat->atom_density(j, p.density_mult()) - : 1.0; + if (j != C_NONE) + atom_density = mat->atom_density(j, p.density_mult()); + } + if (atom_density > 0) { + if (!tally.multiply_density()) + atom_density = 1.0; + } else if (!tally.multiply_density()) { + // Determine log union grid index + if (i_log_union == C_NONE) { + int neutron = ParticleType::neutron().transport_index(); + i_log_union = std::log(p.E() / data::energy_min[neutron]) / + simulation::log_spacing; } + // Update micro xs cache + p.update_neutron_xs(i_nuclide, i_log_union); + atom_density = 1.0; } } @@ -2565,25 +2563,25 @@ void score_collision_tally(Particle& p) double atom_density = 0.; if (i_nuclide >= 0) { - const auto& mat = model::materials[p.material()]; - auto j = mat->mat_nuclide_index_[i_nuclide]; - if (j == C_NONE) { + if (p.material() != MATERIAL_VOID) { + const auto& mat = model::materials[p.material()]; + auto j = mat->mat_nuclide_index_[i_nuclide]; + if (j != C_NONE) + atom_density = mat->atom_density(j, p.density_mult()); + } + if (atom_density > 0) { + if (!tally.multiply_density()) + atom_density = 1.0; + } else if (!tally.multiply_density()) { // Determine log union grid index if (i_log_union == C_NONE) { int neutron = ParticleType::neutron().transport_index(); i_log_union = std::log(p.E() / data::energy_min[neutron]) / simulation::log_spacing; } - // Update micro xs cache - if (!tally.multiply_density()) { - p.update_neutron_xs(i_nuclide, i_log_union); - atom_density = 1.0; - } - } else { - atom_density = tally.multiply_density() - ? mat->atom_density(j, p.density_mult()) - : 1.0; + p.update_neutron_xs(i_nuclide, i_log_union); + atom_density = 1.0; } } diff --git a/tests/unit_tests/test_void.py b/tests/unit_tests/test_void.py new file mode 100644 index 000000000..8713d4b0b --- /dev/null +++ b/tests/unit_tests/test_void.py @@ -0,0 +1,42 @@ +import numpy as np +import openmc +import pytest + + +@pytest.fixture +def empty_sphere(): + openmc.reset_auto_ids() + model = openmc.Model() + surf = openmc.Sphere(r=10, boundary_type='vacuum') + cell = openmc.Cell(region=-surf) + model.geometry = openmc.Geometry([cell]) + + model.settings.run_mode = 'fixed source' + model.settings.batches = 3 + model.settings.particles = 1000 + + tally = openmc.Tally() + tally.scores = ['total', 'elastic'] + tally.nuclides = ['U235'] + tally.multiply_density = False + model.tallies.append(tally) + + return model + + +def test_equivalent_microxs(empty_sphere, run_in_tmpdir): + sp_file = empty_sphere.run() + with openmc.StatePoint(sp_file) as sp: + tally1 = sp.tallies[1] + + mat = openmc.Material() + mat.add_nuclide('H1', 1e-16) + + empty_sphere.geometry.get_all_cells()[1].fill = mat + + sp_file = empty_sphere.run() + with openmc.StatePoint(sp_file) as sp: + tally2 = sp.tallies[1] + + assert np.isclose(tally1.mean.sum(), tally2.mean.sum(), rtol=1e-10, atol=0) + assert tally1.mean.sum() > 0 From 542f949fa0a76df81aa9de7beb6f084ad5815a02 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Sat, 4 Apr 2026 22:53:50 +0200 Subject: [PATCH 606/671] Use local variable to avoid attribute lookup in form_rxn_matrix loop (#3884) Co-authored-by: Perry Co-authored-by: Paul Romano --- openmc/deplete/chain.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index 514f8ed22..97ec8d961 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -701,15 +701,19 @@ class Chain: if fission_yields is None: fission_yields = self.get_default_fission_yields() + # Save local variables to avoid attribute lookups in loop + index_nuc = rates.index_nuc + index_rx = rates.index_rx + for i, nuc in enumerate(self.nuclides): - if nuc.name not in rates.index_nuc: + if nuc.name not in index_nuc: continue - nuc_ind = rates.index_nuc[nuc.name] + nuc_ind = index_nuc[nuc.name] nuc_rates = rates[nuc_ind, :] for r_type, target, _, br in nuc.reactions: - r_id = rates.index_rx[r_type] + r_id = index_rx[r_type] path_rate = nuc_rates[r_id] # Loss term -- make sure we only count loss once for From 23e8a11102aca7f22a341fd41b57a2bce86f74d4 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Sun, 5 Apr 2026 14:04:34 -0500 Subject: [PATCH 607/671] Update versions of several GHA Actions (#3913) --- .github/workflows/ci.yml | 10 +++++----- .../dockerhub-publish-release-dagmc-libmesh.yml | 5 +++-- .github/workflows/dockerhub-publish-release-dagmc.yml | 7 ++++--- .../workflows/dockerhub-publish-release-libmesh.yml | 5 +++-- .github/workflows/dockerhub-publish-release.yml | 7 ++++--- .github/workflows/format-check.yml | 2 +- 6 files changed, 20 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e232dce0..b8c14279b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,10 +27,10 @@ jobs: source_changed: ${{ steps.filter.outputs.source_changed }} steps: - name: Check out the repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Examine changed files id: filter - uses: dorny/paths-filter@668c092af3649c4b664c54e4b704aa46782f6f7c # latest master commit, not released yet + uses: dorny/paths-filter@v4 with: filters: | source_changed: @@ -102,12 +102,12 @@ jobs: cmake-version: '3.31' - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} @@ -158,7 +158,7 @@ jobs: openmc -v - name: cache-xs - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/nndc_hdf5 diff --git a/.github/workflows/dockerhub-publish-release-dagmc-libmesh.yml b/.github/workflows/dockerhub-publish-release-dagmc-libmesh.yml index db62bb53e..26de24e59 100644 --- a/.github/workflows/dockerhub-publish-release-dagmc-libmesh.yml +++ b/.github/workflows/dockerhub-publish-release-dagmc-libmesh.yml @@ -2,13 +2,14 @@ name: dockerhub-publish-release-dagmc-libmesh on: push: - tags: 'v*.*.*' + tags: + - 'v*.*.*' jobs: main: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set env run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - diff --git a/.github/workflows/dockerhub-publish-release-dagmc.yml b/.github/workflows/dockerhub-publish-release-dagmc.yml index de9593782..4a6c5ff26 100644 --- a/.github/workflows/dockerhub-publish-release-dagmc.yml +++ b/.github/workflows/dockerhub-publish-release-dagmc.yml @@ -2,13 +2,14 @@ name: dockerhub-publish-release-dagmc on: push: - tags: 'v*.*.*' + tags: + - 'v*.*.*' jobs: main: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set env run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - @@ -19,7 +20,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/dockerhub-publish-release-libmesh.yml b/.github/workflows/dockerhub-publish-release-libmesh.yml index e8ea98aeb..f3194833f 100644 --- a/.github/workflows/dockerhub-publish-release-libmesh.yml +++ b/.github/workflows/dockerhub-publish-release-libmesh.yml @@ -2,13 +2,14 @@ name: dockerhub-publish-release-libmesh on: push: - tags: 'v*.*.*' + tags: + - 'v*.*.*' jobs: main: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set env run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - diff --git a/.github/workflows/dockerhub-publish-release.yml b/.github/workflows/dockerhub-publish-release.yml index fab030192..ef9faf00c 100644 --- a/.github/workflows/dockerhub-publish-release.yml +++ b/.github/workflows/dockerhub-publish-release.yml @@ -2,13 +2,14 @@ name: dockerhub-publish-release on: push: - tags: 'v*.*.*' + tags: + - 'v*.*.*' jobs: main: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set env run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - @@ -19,7 +20,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml index ddb29be5a..7d7d46ed9 100644 --- a/.github/workflows/format-check.yml +++ b/.github/workflows/format-check.yml @@ -22,7 +22,7 @@ jobs: contents: read pull-requests: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: cpp-linter/cpp-linter-action@v2 id: linter env: From 1eb368bbd229173b7e1d63e40ac31058c6f29ea2 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 9 Apr 2026 02:31:27 -0500 Subject: [PATCH 608/671] Pin NJOY version in CI to 2016.78 (#3917) --- tools/ci/gha-install-njoy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/ci/gha-install-njoy.sh b/tools/ci/gha-install-njoy.sh index 8255ffea8..168fcd4a7 100755 --- a/tools/ci/gha-install-njoy.sh +++ b/tools/ci/gha-install-njoy.sh @@ -1,7 +1,7 @@ #!/bin/bash set -ex cd $HOME -git clone https://github.com/njoy/NJOY2016 +git clone -b 2016.78 https://github.com/njoy/NJOY2016 cd NJOY2016 mkdir build && cd build cmake -Dstatic=on .. && make 2>/dev/null && sudo make install From fd1bc26af0375f00a51cc5ca094471bb06f66006 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 9 Apr 2026 15:13:18 -0500 Subject: [PATCH 609/671] Pin NJOY version in Dockerfile (#3920) --- .github/workflows/dockerhub-publish-dagmc-libmesh.yml | 3 ++- .github/workflows/dockerhub-publish-dagmc.yml | 5 +++-- .github/workflows/dockerhub-publish-dev.yml | 5 +++-- .../workflows/dockerhub-publish-develop-dagmc-libmesh.yml | 5 +++-- .github/workflows/dockerhub-publish-develop-dagmc.yml | 5 +++-- .github/workflows/dockerhub-publish-develop-libmesh.yml | 5 +++-- .github/workflows/dockerhub-publish-libmesh.yml | 5 +++-- .github/workflows/dockerhub-publish.yml | 5 +++-- Dockerfile | 3 ++- 9 files changed, 25 insertions(+), 16 deletions(-) diff --git a/.github/workflows/dockerhub-publish-dagmc-libmesh.yml b/.github/workflows/dockerhub-publish-dagmc-libmesh.yml index 813596953..e3e476150 100644 --- a/.github/workflows/dockerhub-publish-dagmc-libmesh.yml +++ b/.github/workflows/dockerhub-publish-dagmc-libmesh.yml @@ -2,7 +2,8 @@ name: dockerhub-publish-latest-dagmc-libmesh on: push: - branches: master + branches: + - master jobs: main: diff --git a/.github/workflows/dockerhub-publish-dagmc.yml b/.github/workflows/dockerhub-publish-dagmc.yml index 6757f7727..317cfc0e8 100644 --- a/.github/workflows/dockerhub-publish-dagmc.yml +++ b/.github/workflows/dockerhub-publish-dagmc.yml @@ -2,7 +2,8 @@ name: dockerhub-publish-latest-dagmc on: push: - branches: master + branches: + - master jobs: main: @@ -16,7 +17,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/dockerhub-publish-dev.yml b/.github/workflows/dockerhub-publish-dev.yml index 7a81363a7..7c78c6c99 100644 --- a/.github/workflows/dockerhub-publish-dev.yml +++ b/.github/workflows/dockerhub-publish-dev.yml @@ -2,7 +2,8 @@ name: dockerhub-publish-develop on: push: - branches: develop + branches: + - develop jobs: main: @@ -16,7 +17,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/dockerhub-publish-develop-dagmc-libmesh.yml b/.github/workflows/dockerhub-publish-develop-dagmc-libmesh.yml index a219f2a91..33a3a3b06 100644 --- a/.github/workflows/dockerhub-publish-develop-dagmc-libmesh.yml +++ b/.github/workflows/dockerhub-publish-develop-dagmc-libmesh.yml @@ -2,7 +2,8 @@ name: dockerhub-publish-develop-dagmc-libmesh on: push: - branches: develop + branches: + - develop jobs: main: @@ -16,7 +17,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/dockerhub-publish-develop-dagmc.yml b/.github/workflows/dockerhub-publish-develop-dagmc.yml index a901b8d3f..1d6051a70 100644 --- a/.github/workflows/dockerhub-publish-develop-dagmc.yml +++ b/.github/workflows/dockerhub-publish-develop-dagmc.yml @@ -2,7 +2,8 @@ name: dockerhub-publish-develop-dagmc on: push: - branches: develop + branches: + - develop jobs: main: @@ -16,7 +17,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/dockerhub-publish-develop-libmesh.yml b/.github/workflows/dockerhub-publish-develop-libmesh.yml index 22e9aa68f..2c53636bd 100644 --- a/.github/workflows/dockerhub-publish-develop-libmesh.yml +++ b/.github/workflows/dockerhub-publish-develop-libmesh.yml @@ -2,7 +2,8 @@ name: dockerhub-publish-develop-libmesh on: push: - branches: develop + branches: + - develop jobs: main: @@ -16,7 +17,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/dockerhub-publish-libmesh.yml b/.github/workflows/dockerhub-publish-libmesh.yml index 843ce0f6f..12fbfa579 100644 --- a/.github/workflows/dockerhub-publish-libmesh.yml +++ b/.github/workflows/dockerhub-publish-libmesh.yml @@ -2,7 +2,8 @@ name: dockerhub-publish-latest-libmesh on: push: - branches: master + branches: + - master jobs: main: @@ -16,7 +17,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/dockerhub-publish.yml b/.github/workflows/dockerhub-publish.yml index fd51a9fa7..a5c8f711b 100644 --- a/.github/workflows/dockerhub-publish.yml +++ b/.github/workflows/dockerhub-publish.yml @@ -2,7 +2,8 @@ name: dockerhub-publish-latest on: push: - branches: master + branches: + - master jobs: main: @@ -16,7 +17,7 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/Dockerfile b/Dockerfile index 67cd37c59..688e5a370 100644 --- a/Dockerfile +++ b/Dockerfile @@ -53,6 +53,7 @@ ENV LIBMESH_REPO='https://github.com/libMesh/libmesh' ENV LIBMESH_INSTALL_DIR=$HOME/LIBMESH # NJOY variables +ENV NJOY_TAG='2016.78' ENV NJOY_REPO='https://github.com/njoy/NJOY2016' # Setup environment variables for Docker image @@ -78,7 +79,7 @@ RUN pip install --upgrade pip # Clone and install NJOY2016 RUN cd $HOME \ - && git clone --single-branch --depth 1 ${NJOY_REPO} \ + && git clone --single-branch -b ${NJOY_TAG} --depth 1 ${NJOY_REPO} \ && cd NJOY2016 \ && mkdir build \ && cd build \ From 36e70d8efcfb26e66301323739ac8a001fadc18e Mon Sep 17 00:00:00 2001 From: Luke Labrie-Cleary Date: Sat, 18 Apr 2026 18:01:09 +0200 Subject: [PATCH 610/671] Add Arch Linux User Repository (AUR) installation instructions to documentation (#3921) Co-authored-by: Paul Romano --- docs/source/usersguide/install.rst | 69 ++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/docs/source/usersguide/install.rst b/docs/source/usersguide/install.rst index 1f9a07b80..1ef2f574b 100644 --- a/docs/source/usersguide/install.rst +++ b/docs/source/usersguide/install.rst @@ -158,6 +158,75 @@ feature can be used to access the installed packages. .. _Spack: https://spack.readthedocs.io/en/latest/ .. _setup guide: https://spack.readthedocs.io/en/latest/getting_started.html +.. _install_aur: + +------------------------------------ +Installing on Arch Linux via the AUR +------------------------------------ + +On Arch Linux and Arch-based distributions, OpenMC can be installed from the +`Arch User Repository (AUR) `_. An AUR package named +``openmc-git`` is available, which builds OpenMC directly from the latest +development sources. + +This package provides a full-featured OpenMC stack, including: + +* MPI and DAGMC-enabled OpenMC build +* User-selected nuclear data libraries +* The `CAD_to_OpenMC `_ meshing tool +* All required dependencies for the above components + +To install the package, you will need an AUR helper such as `yay`_ or `paru`_. +For example, using ``yay``:: + + yay -S openmc-git + + +Alternatively, you can manually clone and build the package:: + + git clone https://aur.archlinux.org/openmc-git.git + cd openmc-git + makepkg -si + +Note, ``makepkg`` uses ``pacman`` to resolve dependencies. Therefore, AUR-based +dependencies need to be installed separately with ``yay`` or ``paru`` before +running ``makepkg``. The PKGBUILD will automatically handle all required +dependencies and build OpenMC with MPI and DAGMC support enabled. + +.. tip:: + + If there are failing checks during the build process, you can bypass them + with the ``--nocheck`` flag:: + + yay -S openmc-git --mflags "--nocheck" + + Or:: + + git clone https://aur.archlinux.org/openmc-git.git + cd openmc-git + makepkg -si --nocheck + +.. note:: + + The ``openmc-git`` package tracks the latest development version from the + upstream repository. As such, it may include new features and bug fixes, but + could also introduce instability compared to official releases. + +.. tip:: + + OpenMC is installed under ``/opt``. If you are installing and using it in + the same terminal session, you may need to reload your environment + variables:: + + source /etc/profile + + Alternatively, start a new shell session. + +Once installed, the ``openmc`` executable, nuclear data libraries, and +associated tools will be available in your system :envvar:`PATH`. + +.. _yay: https://github.com/Jguer/yay +.. _paru: https://github.com/Morganamilo/paru .. _install_source: From e431d49bec3d326f255bbde73239d09cc4921ff9 Mon Sep 17 00:00:00 2001 From: Lorenzo Chierici Date: Mon, 20 Apr 2026 14:13:53 +0200 Subject: [PATCH 611/671] Add reactivity control to coupled transport-depletion analyses (#2693) Co-authored-by: Andrew Johnson Co-authored-by: Paul Romano --- docs/source/io_formats/depletion_results.rst | 4 +- openmc/deplete/abc.py | 143 +++++++++++++++++- openmc/deplete/coupled_operator.py | 2 +- openmc/deplete/independent_operator.py | 2 +- openmc/deplete/keff_search_control.py | 128 ++++++++++++++++ openmc/deplete/stepresult.py | 23 ++- .../__init__.py | 0 .../ref_depletion_with_refuel.h5 | Bin 0 -> 28640 bytes .../ref_depletion_with_rotation.h5 | Bin 0 -> 28640 bytes .../ref_depletion_with_translation.h5 | Bin 0 -> 28640 bytes .../deplete_with_keff_search_control/test.py | 140 +++++++++++++++++ .../test_deplete_keff_search_control.py | 116 ++++++++++++++ 12 files changed, 548 insertions(+), 10 deletions(-) create mode 100644 openmc/deplete/keff_search_control.py create mode 100644 tests/regression_tests/deplete_with_keff_search_control/__init__.py create mode 100644 tests/regression_tests/deplete_with_keff_search_control/ref_depletion_with_refuel.h5 create mode 100644 tests/regression_tests/deplete_with_keff_search_control/ref_depletion_with_rotation.h5 create mode 100644 tests/regression_tests/deplete_with_keff_search_control/ref_depletion_with_translation.h5 create mode 100644 tests/regression_tests/deplete_with_keff_search_control/test.py create mode 100644 tests/unit_tests/test_deplete_keff_search_control.py diff --git a/docs/source/io_formats/depletion_results.rst b/docs/source/io_formats/depletion_results.rst index 7fb088268..856993db6 100644 --- a/docs/source/io_formats/depletion_results.rst +++ b/docs/source/io_formats/depletion_results.rst @@ -4,7 +4,7 @@ Depletion Results File Format ============================= -The current version of the depletion results file format is 1.2. +The current version of the depletion results file format is 1.3. **/** @@ -29,6 +29,8 @@ The current version of the depletion results file format is 1.2. - **depletion time** (*double[]*) -- Average process time in [s] spent depleting a material across all burnable materials and, if applicable, MPI processes. + - **keff_search_root** (*double[]*) -- Root of the keff search at the + end of the timestep, if applicable. **/materials//** diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 7736b8c13..80d8474ec 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -31,6 +31,7 @@ from .results import Results, _SECONDS_PER_MINUTE, _SECONDS_PER_HOUR, \ from .pool import deplete from .reaction_rates import ReactionRates from .transfer_rates import TransferRates, ExternalSourceRates +from .keff_search_control import _KeffSearchControl __all__ = [ @@ -159,7 +160,7 @@ class TransportOperator(ABC): self.prev_res = prev_results @abstractmethod - def __call__(self, vec, source_rate): + def __call__(self, vec, source_rate) -> OperatorResult: """Runs a simulation. Parameters @@ -686,6 +687,7 @@ class Integrator(ABC): self.transfer_rates = None self.external_source_rates = None + self._keff_search_control = None if isinstance(solver, str): # Delay importing of cram module, which requires this file @@ -839,6 +841,37 @@ class Integrator(ABC): return (self.operator.prev_res[-1].time[0], len(self.operator.prev_res) - 1) + def _restore_keff_search_control(self, res: StepResult): + """Restore keff search control from restart results.""" + keff_search_root = res.keff_search_root + if keff_search_root is None: + raise ValueError( + "Cannot restore keff search control from restart " + "results because no stored keff_search_root is " + "available." + ) + self._keff_search_control.function(keff_search_root) + return keff_search_root + + def _get_bos_data(self, step_index, source_rate, bos_conc): + """Get beginning-of-step concentrations, rates, and control state.""" + if step_index > 0 or self.operator.prev_res is None: + if self._keff_search_control is not None and source_rate != 0.0: + keff_search_root = self._keff_search_control.run(bos_conc) + else: + keff_search_root = None + bos_conc, res = self._get_bos_data_from_operator( + step_index, source_rate, bos_conc) + else: + bos_conc, res = self._get_bos_data_from_restart( + source_rate, bos_conc) + if self._keff_search_control is not None and source_rate != 0.0: + keff_search_root = self._restore_keff_search_control(self.operator.prev_res[-1]) + else: + keff_search_root = None + + return bos_conc, res, keff_search_root + def integrate( self, final_step: bool = True, @@ -877,11 +910,8 @@ class Integrator(ABC): if output and comm.rank == 0: print(f"[openmc.deplete] t={t} s, dt={dt} s, source={source_rate}") - # Solve transport equation (or obtain result from restart) - if i > 0 or self.operator.prev_res is None: - n, res = self._get_bos_data_from_operator(i, source_rate, n) - else: - n, res = self._get_bos_data_from_restart(source_rate, n) + # Get beginning-of-step data from operator or restart results + n, res, keff_search_root = self._get_bos_data(i, source_rate, n) # Solve Bateman equations over time interval proc_time, n_end = self(n, res.rates, dt, source_rate, i) @@ -895,6 +925,7 @@ class Integrator(ABC): self._i_res + i, proc_time, write_rates=write_rates, + keff_search_root=keff_search_root, path=path ) @@ -908,6 +939,10 @@ class Integrator(ABC): # solve) if output and final_step and comm.rank == 0: print(f"[openmc.deplete] t={t} (final operator evaluation)") + if self._keff_search_control is not None and source_rate != 0.0: + keff_search_root = self._keff_search_control.run(n) + else: + keff_search_root = None res_final = self.operator(n, source_rate if final_step else 0.0) StepResult.save( self.operator, @@ -918,6 +953,7 @@ class Integrator(ABC): self._i_res + len(self), proc_time, write_rates=write_rates, + keff_search_root=keff_search_root, path=path ) self.operator.write_bos_data(len(self) + self._i_res) @@ -1050,6 +1086,101 @@ class Integrator(ABC): self.transfer_rates.set_redox(material, buffer, oxidation_states, timesteps) + def add_keff_search_control( + self, + function: Callable, + x0: float, + x1: float, + bracket: Sequence[float], + **search_kwargs + ): + """Add keff search to the integrator scheme. + + This method causes OpenMC to perform a keff search during depletion to + maintain a target keff by adjusting a model parameter through the + provided function. + + .. important:: + The function **must** modify the model through ``openmc.lib`` (e.g., + ``openmc.lib.cells``, ``openmc.lib.materials``) and **NOT** through + ``openmc.Model``. The function is called within a + :class:`openmc.lib.TemporarySession` context where only the C API + (``openmc.lib``) is available for modifications. + + Parameters + ---------- + function : Callable + Function that takes a single float argument and modifies the model + through :mod:`openmc.lib`. + x0 : float + Initial lower bound for the keff search. + x1 : float + Initial upper bound for the keff search. + bracket : sequence of float + Bracket interval [x_min, x_max] that constrains the allowed parameter + values during the keff search. This is a required parameter + that defines the absolute bounds for the search. The bracket must contain + exactly 2 elements with bracket[0] < bracket[1]. These values are passed + directly to the ``x_min`` and ``x_max`` optional arguments in + :meth:`openmc.Model.keff_search`, which enforce hard limits on the + parameter range. If the keff search converges to a value outside this + bracket, it will be clamped to the nearest bracket bound with a warning. + **search_kwargs + Additional keyword arguments passed to + :meth:`openmc.Model.keff_search`. Common options include: + + * ``target`` : float, optional + Target keff value to search for. Defaults to 1.0. + * ``k_tol`` : float, optional + Stopping criterion on the function value. Defaults to 1e-4. + * ``sigma_final`` : float, optional + Maximum accepted k-effective uncertainty. Defaults to 3e-4. + * ``maxiter`` : int, optional + Maximum number of iterations. Defaults to 50. + + See :meth:`openmc.Model.keff_search` for a complete list of + available options. + + Examples + -------- + Add keff search that adjusts a control rod position: + + >>> def adjust_rod_position(position): + ... openmc.lib.cells[rod_cell.id].translation = [0, 0, position] + >>> integrator.add_keff_search_control( + ... adjust_rod_position, + ... x0=0.0, + ... x1=5.0, + ... bracket=[-10,10], + ... target=1.0, + ... k_tol=1e-4 + ... ) + + Add keff search that adjusts the U235 density: + + >>> def set_u235_density(u235_density): + ... # Get the material from openmc.lib + ... lib_mat = openmc.lib.materials[material_id] + ... # Get current nuclides and densities + ... nuclides = lib_mat.nuclides + ... densities = lib_mat.densities + ... u235_idx = nuclides.index('U235') + ... densities[u235_idx] = u235_density + ... lib_mat.set_densities(nuclides, densities) + >>> integrator.add_keff_search_control( + ... set_u235_density, + ... x0=5.0e-4, + ... x1=1.0e-3, + ... bracket=[1.0e-4, 2.0e-3], + ... target=1.0 + ... ) + + .. versionadded:: 0.15.4 + + """ + self._keff_search_control = _KeffSearchControl( + self.operator, function, x0, x1, bracket, **search_kwargs) + @add_params class SIIntegrator(Integrator): r"""Abstract class for the Stochastic Implicit Euler integrators diff --git a/openmc/deplete/coupled_operator.py b/openmc/deplete/coupled_operator.py index 34bb28b49..a21d57d46 100644 --- a/openmc/deplete/coupled_operator.py +++ b/openmc/deplete/coupled_operator.py @@ -399,7 +399,7 @@ class CoupledOperator(OpenMCOperator): self.materials.export_to_xml(nuclides_to_ignore=self._decay_nucs) - def __call__(self, vec, source_rate): + def __call__(self, vec, source_rate) -> OperatorResult: """Runs a simulation. Simulation will abort under the following circumstances: diff --git a/openmc/deplete/independent_operator.py b/openmc/deplete/independent_operator.py index c192907cf..c12863956 100644 --- a/openmc/deplete/independent_operator.py +++ b/openmc/deplete/independent_operator.py @@ -384,7 +384,7 @@ class IndependentOperator(OpenMCOperator): # Return number density vector return super().initial_condition(self.materials) - def __call__(self, vec, source_rate): + def __call__(self, vec, source_rate) -> OperatorResult: """Obtain the reaction rates Parameters diff --git a/openmc/deplete/keff_search_control.py b/openmc/deplete/keff_search_control.py new file mode 100644 index 000000000..49f7cc4df --- /dev/null +++ b/openmc/deplete/keff_search_control.py @@ -0,0 +1,128 @@ +from typing import Callable +from warnings import warn + +import openmc.lib + + +class _KeffSearchControl: + """Controller for keff search during depletion calculations. + + This class performs keff searches to maintain a target keff by adjusting a + model parameter through a provided function. + + Parameters + ---------- + operator : openmc.deplete.Operator + Depletion operator instance + function : Callable + Function that modifies the model based on a parameter value + x0 : float + Initial lower bound for the keff search + x1 : float + Initial upper bound for the keff search + bracket : list[float] + Absolute bracketing interval lower and upper. If the keff search + solution lies off these limits the closest limit will be set as new + result. + **search_kwargs : dict, optional + Additional keyword arguments to pass to :meth:`openmc.Model.keff_search` + + """ + def __init__(self, operator, function: Callable, x0: float, x1: float, bracket: list[float], **search_kwargs): + if len(bracket) != 2: + raise ValueError(f"bracket must have exactly 2 elements, got {len(bracket)}") + if bracket[0] >= bracket[1]: + raise ValueError(f"bracket[0] must be < bracket[1], got {bracket}") + self.x0 = x0 + self.x1 = x1 + self.operator = operator + self.function = function + self.search_kwargs = search_kwargs + self.search_kwargs['x_min'] = bracket[0] + self.search_kwargs['x_max'] = bracket[1] + + def run(self, x): + """Perform keff search and update the atom density vector. + + Parameters + ---------- + x : list of numpy.ndarray + Current atom density vector (atoms per material) + + Returns + ------- + root : float + Parameter value that achieves target keff + """ + root = self._search_for_keff() + self._update_vec(x) + return root + + def _search_for_keff(self) -> float: + """Perform the keff search using the model's keff_search method. + + Returns + ------- + float + Parameter value that achieves target keff + + Raises + ------ + ValueError + If the keff search fails to converge + """ + with openmc.lib.TemporarySession(self.operator.model): + # Only pass the first 3 required args plus explicitly provided kwargs + result = self.operator.model.keff_search( + self.function, self.x0, self.x1, **self.search_kwargs + ) + if not result.converged: + raise ValueError( + f"Search for keff failed to converge. " + f"Termination reason: {result.flag}" + ) + + root = result.root + + # Check if root is outside the bracket bounds and give a warning + if root < self.search_kwargs['x_min']: + warn(f"keff search result ({root:.6f}) is below the lower bracket " + f"bound ({self.search_kwargs['x_min']:.6f}).", UserWarning) + elif root > self.search_kwargs['x_max']: + warn(f"keff search result ({root:.6f}) is above the upper bracket " + f"bound ({self.search_kwargs['x_max']:.6f}).", UserWarning) + + # Restore the number of initial batches + openmc.lib.settings.set_batches(self.operator.model.settings.batches) + + return root + + def _update_vec(self, x): + """Update the atom density vector from openmc.lib.materials and AtomNumber object. + + The depletion vector ``x`` is rank-local, matching the materials owned + by ``self.operator.number`` on the current MPI rank. We therefore only + update entries for locally owned materials using the compositions + currently stored in ``openmc.lib.materials``. + + Parameters + ---------- + x : list of numpy.ndarray + Atom density vector to update (atoms per material) + + """ + number = self.operator.number + + for mat_idx, mat in enumerate(number.materials): + lib_material = openmc.lib.materials[int(mat)] + nuclides = lib_material.nuclides + densities = 1e24 * lib_material.densities + volume = number.get_mat_volume(mat) + + for nuc_idx, nuc in enumerate(number.burnable_nuclides): + if nuc in nuclides: + lib_nuc_idx = nuclides.index(nuc) + atom_density = densities[lib_nuc_idx] + else: + atom_density = number.get_atom_density(mat, nuc) + x[mat_idx][nuc_idx] = atom_density * volume diff --git a/openmc/deplete/stepresult.py b/openmc/deplete/stepresult.py index 6da7f7adc..9f638a058 100644 --- a/openmc/deplete/stepresult.py +++ b/openmc/deplete/stepresult.py @@ -17,7 +17,7 @@ from openmc.mpi import MPI, comm from .reaction_rates import ReactionRates -VERSION_RESULTS = (1, 2) +VERSION_RESULTS = (1, 3) __all__ = ["StepResult"] @@ -58,6 +58,8 @@ class StepResult: proc_time : int Average time spent depleting a material across all materials and processes + keff_search_root : float + The root returned by the keff search control. """ def __init__(self): @@ -74,6 +76,7 @@ class StepResult: self.name_list = None self.data = None + self.keff_search_root = None def __repr__(self): t = self.time[0] @@ -368,6 +371,10 @@ class StepResult: "depletion time", (1,), maxshape=(None,), dtype="float64") + handle.create_dataset( + "keff_search_root", (1,), maxshape=(None,), + dtype="float64") + def _to_hdf5(self, handle, index, parallel=False, write_rates: bool = False): """Converts results object into an hdf5 object. @@ -400,6 +407,7 @@ class StepResult: time_dset = handle["/time"] source_rate_dset = handle["/source_rate"] proc_time_dset = handle["/depletion time"] + keff_search_root_dset = handle["/keff_search_root"] # Get number of results stored number_shape = list(number_dset.shape) @@ -433,6 +441,10 @@ class StepResult: proc_shape[0] = new_shape proc_time_dset.resize(proc_shape) + keff_search_root_shape = list(keff_search_root_dset.shape) + keff_search_root_shape[0] = new_shape + keff_search_root_dset.resize(keff_search_root_shape) + # If nothing to write, just return if len(self.index_mat) == 0: return @@ -452,6 +464,7 @@ class StepResult: proc_time_dset[index] = ( self.proc_time / (comm.size * self.n_hdf5_mats) ) + keff_search_root_dset[index] = self.keff_search_root @classmethod def from_hdf5(cls, handle, step): @@ -500,6 +513,10 @@ class StepResult: if step < proc_time_dset.shape[0]: results.proc_time = proc_time_dset[step] + if "keff_search_root" in handle: + keff_search_root_dset = handle["/keff_search_root"] + results.keff_search_root = keff_search_root_dset[step] + if results.proc_time is None: results.proc_time = np.array([np.nan]) @@ -554,6 +571,7 @@ class StepResult: step_ind, proc_time=None, write_rates: bool = False, + keff_search_root=None, path: PathLike = "depletion_results.h5" ): """Creates and writes depletion results to disk @@ -578,6 +596,8 @@ class StepResult: processes. write_rates : bool, optional Whether reaction rates should be written to the results file. + keff_search_root : float + The root returned by the keff search control. path : PathLike Path to file to write. Defaults to 'depletion_results.h5'. @@ -605,6 +625,7 @@ class StepResult: results.proc_time = proc_time if results.proc_time is not None: results.proc_time = comm.reduce(proc_time, op=MPI.SUM) + results.keff_search_root = keff_search_root if not Path(path).is_file(): Path(path).parent.mkdir(parents=True, exist_ok=True) diff --git a/tests/regression_tests/deplete_with_keff_search_control/__init__.py b/tests/regression_tests/deplete_with_keff_search_control/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/deplete_with_keff_search_control/ref_depletion_with_refuel.h5 b/tests/regression_tests/deplete_with_keff_search_control/ref_depletion_with_refuel.h5 new file mode 100644 index 0000000000000000000000000000000000000000..a335e3b47800c98e4e001dfc2159f884a31c6bf0 GIT binary patch literal 28640 zcmeHOO-x)>6uxf;+Mz`St7b%vFB%O*(?X$icDzQYm}pDXwyeq<;bBLI88SZvyTX{5 zx-hb2iMuX|x?st|g-ezgO`M%BYGQ0mSXtkF=X`~E%ms(x(J7o+%z5|TbMLw5d-pr{ zyf=5g8Xdm;#Hp@RN`Ddw)uK9rOME@xPbE(^D5Krw3yb9)mUm-xVfiEKwj|eg@c4F| zDpt7u!}mue>Ynoo#Y{e@IubsOf)@DF%zu;lmSi67#kVDj2 z0Q_)xe*g?v|6{0VP*NPwk$^rMCrQE%8tl+K8Z>+4x?}$jJF2ik~-A*LrS$a@8^AC*inB zaVchuOMmHP$p6^Ew^Dz+Nf(HtTrJ{=B7nHG!u}!dq<9@f<+i2bRrxiZXH#d$qRWP5 zUybSx9F1|Yev#*5pIhR%1~0MSVSS^1zmoRV7HkKq_bD0)cE-6p&-3VDLrc8Ap|{56 z%Cn?^e0cS_>hUUER?kztO+syO8RMxgZhw&#np|LC4db=-9RGB%uck}+89P^=`N%8S z*kiS8rhS&TO+Uuejx*zor_1$f2|pMJ5j7Do@x$hIgicZMBfLL0%8wSFuxbBT;{DZSJ>duLu*g~nnE2s@^0cV< z5zY%6w`b5PK`z5mAu)Rk4fuDCw{MdMf?78fi z@PmO6Q4;|ZKNff$p;J`+aBQ;Rvb{$6!9PP7pU*d6V|(nF@PmO6Q4;|ZKbCnNp;J`+ za9<@03v91Ze&9W(Y5z#}kUf_j6MirdB5ERF;>TKdwR%+iSa_Z6EVI2v`GI+w_|ePf zL|k@H_)&eIrBsc8i650Ws`aq$4(k0bMt`f=*MGq;ygk)-VUVRh6ZQ^Ot83@`gA-T2 z)M~mM93U9?NI^^85A$=iQUo9R-K$k0Y;5xT3)l@mNA>a?k~UpH&*3uiNjeRoREqae z=!N%HhlWw(98aJZjxVFEH_!Vd^p?527P_yjerN*k^~3XNG%m*21535iT42X5oHvP6 z#g-m*Xybl3FAls=ANNnf>N?Z;lve+jP~9Gj$AYd2Wd|e1V=c?Uf~BRY+w_o$pfUJz zw=N^R+KC6PV2h^v@H{ba0S+kF??3&L8;`{+m&h;eyB>~xpO4Vny@!5;cqHGY=`}eX zbpNc-xpSwyn)#ARq_`0)l`bAP5Kof`B0Kh!HS7KXmLduWo)0fFAPyaJRdL=LNif zlIMglVp0|a1OY)n5D)|e0YRWS5!i3unEIc2C}^6lTlzdAoI{L=R4P}h~o7ZMY5LshON|8auynit4>%Rh5_rCP75Q|-cc hY=1Ch|6Ka^<(~WZhR%L@BlX@F1sWH2A$QnU&VN6C&0qik literal 0 HcmV?d00001 diff --git a/tests/regression_tests/deplete_with_keff_search_control/ref_depletion_with_rotation.h5 b/tests/regression_tests/deplete_with_keff_search_control/ref_depletion_with_rotation.h5 new file mode 100644 index 0000000000000000000000000000000000000000..c8b4f67ff25dcfec4cd6d02aeea9443077d6feed GIT binary patch literal 28640 zcmeHOO>7%Q6rQz{)@cnS6p*PvtcpOXK;SfK>JxEA0!FH6C`~aEYSv9Q)Zo~Wf1pH4Z zo<&1NRMc7Bvv#qRE#y^q#G#SblB9Nnn89>Vkl+i-QN{YI%jq9cQ`wwd{%pqf260X& zoDMyXM88RU#!+xAnnk-*$(2j2XgC_K4)})oZ!q6t%%etpTcn7E#()+gUHk&C76HqpsU`tH-Is0{5dhptSq0W_YPS zBnbH90QiyQ{Q)pw{g0xeMM-hMjRbV8A0-JpXfS>AXwmF}>yG{3k5^Pt05Q}NCx3~k zY<|+7bG+Y{zxWV|s)Sc#sH5sBr{8QLSDALQlpb@4O(fO8aW!z?7eB8i#!i3!$;G5D zKk>&+ic2AFT>9NjhMdFn&Pv^Vl`arRxmv^lMF4SWnf*iDN$@%d%56i1Yw~M6&n8Zg zMT-r~z8cgWI2hw%{UXoBKDWqm4PIiu!}>=3ZYAxj9oP<3{0SNfcG6s)<9T$mp+#Qb z&|Bql^=VQ-KD_cw?RZr#t7oa+B%#*1jPcY-Zhwvy+FW2?_2aeq9RH-ZuV%`HX)|A$ zzGfFq?6IzEx_y?nO((|Gk27h;(_(xi&M*)n>LQ?vGYY)qUf|(g#ToF!A17MnN9Tk2 z*5}6@#}$k9gdYrqh`I>q_+j!oLZ_hk;ol!y{;xX@PmO6Q5OLnKjwKIp;J)&NSb89VtcLfgMWt5KA&&A#P--R;RgdDqAmhDek}1i zLZ_hkVZBHe=Gk7W{J?um-To0fP4+ByO!&b-h^UKzjvuSNwdz6fWBz5bv&8mViZ_}Uf(~TR^wubJup-!tp#@6 z!1pF$s@T+{4qe<2--`n;)Q6ofVck02_bHvuFQIOGC>-*-MwID|7!GwTc?*`7rrYKY z8SxsuleKvn;MI0KXayTI-3Q+j0~g?ca`XPvFS+4RxO$%aa(#D)W8dcj^tSQP4-t>p z1)5%)<3ab&@O^iVb+P;P5E{L*>HiiU+-Q8$_EBT|`Unh<`Kylr9)Ny2Q?`rQOb-7O z$X6zE*-0BkMV}}rWoJLO^Rt;;g`3J*_v=2TN`*>s!X7U=!+1k({{Mh9KDDQ&#!Ggl zIB|WvSSXZX*CmU4U_Ygj{=AR(Z+VKQ;ZCt_*Rz}d8&3EIe<4$P2y0rZc`8C5_oGH> zOTps*d7%pWkl0tZ00vc&f`A|(2nYg#fFK|U2m*qDAn=e8&^fQsUI1fa-|2^X^O`Zt-CZ{Lrf`A|(2nYg#fFRHY2sE27j5f#@)+AqO zgS#qI5CjAPK|l}?1ojqzX7hz~gM49K@`b&kAP5Kof`B0K2qN%9X7JY!{*4cx zyK^P}=a1hEfAvLvW%=7bhG#FF>MH+qZ|J?Te?I)>yYf(tYq7tQUU}8_$1Yuaqpw8c!!BeU`~LwQ6w!D9 literal 0 HcmV?d00001 diff --git a/tests/regression_tests/deplete_with_keff_search_control/ref_depletion_with_translation.h5 b/tests/regression_tests/deplete_with_keff_search_control/ref_depletion_with_translation.h5 new file mode 100644 index 0000000000000000000000000000000000000000..35285321bc2a30f35f474f4b123ac68750eb1b0c GIT binary patch literal 28640 zcmeHOL2MgE6rHt`#%&BFq@YlNSb?ZgfvD4@sc+ee1dNJk1SFVCvnJV4gJVZ_5>ig# zRwN{H3_?goTzZUvL&O2)2)A;}4c~wRDgq(pMA_Z{UlVUUF^;pToBTcO-4M3l$=d6#}Wz%VVCySTD=$ePw_zLK5A)UW4m8u2Q~5 zL&a3wUEPaLv6L<3RaeZVk>6rbyFtugIw(l+1?9M6eRb09A5$~goHKu8&hZCvPd#pj zZdan;v@_=_xE9T#Q(DN)msrtoH9Q^g4fEe(zV$MX>hW!rA_DuugNrye2!L;C_87ZD zlGhJ_=;sTpOJW6D8<%gses#p-6ztxj{b8!_gWVVRwhP=g0H{XY;`%i>uF=!&yOmDru)_T)4k+!us~KLZj|l?7 zH~@ZFygvX2tp71oG$|<#c#(jP%?^^Vg9bA&k0#9?x$fBigLuUi1rS4>aPya#%I2q? zCD;31`AYzixJvpphU!qK-F}OO+`_DzrF5IaY{F85ooeuIAbws;j-UJJ{i~KPKMBT7 zic1k~T>8^XhTOyS&r1FF0$m`Ea=3ekJXzZP*S};t?7NcG6s4;(2tjp;cbr(A(g0 zT^>`I7t0$=5B%wCBjPcYNZhw*$T3ldX4dS)^9RH}luV&^8vu1u__FboF zVvqG))9tgoZMre0cAQBwo;KqnafX2qQ4;}OoKfH<_W}CeP zIIh^NC;VU_MASq;#}AX&5jus%kKq2;EI-jQ&!of8e59cxAf(;tj&r4Y+Q`62Zm~=wZM)W_}(N+ z72A5$;UxFN_u{|{^-=drSg%g^eM-CgOQ_c#iAMacF=hHAMk8%yf5Fny^xC{3V}7H5 zvbQfoyxNTitzd(ud+&Q<-~t>_Zrp$RB{v$0RxXoYp6}jp?E6xP-qs)bKH|~)8cnao z@u2%>1im}Ry4e4Eh>T5a`@e$+H|yWDL)6&4K0?D|`Pu`32cVzL%sa(wCWrqCCcCF|JG(`8r~FJc0GIfzu|^o@E0 zpBJj24~czs2VhVYDF_Gxf`A|(2nYg#fFK|U2m<#R0p0UMSDktF@pAz5kpBm}-O@ZS z;Qf<4Cj=3bvLGM`2m*qDARq_`0{JGY Date: Tue, 21 Apr 2026 12:47:40 -0400 Subject: [PATCH 612/671] Remove self loops from spontaneous fission decay mode (#3907) Co-authored-by: Paul Romano --- openmc/data/decay.py | 10 +++++----- openmc/deplete/chain.py | 15 +++++++++------ 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/openmc/data/decay.py b/openmc/data/decay.py index 7cd4bf43d..ce20a252c 100644 --- a/openmc/data/decay.py +++ b/openmc/data/decay.py @@ -2,7 +2,6 @@ from collections.abc import Iterable from functools import cached_property from io import StringIO from math import log -import re from warnings import warn import numpy as np @@ -13,7 +12,7 @@ import openmc.checkvalue as cv from openmc.exceptions import DataError from openmc.mixin import EqualityMixin from openmc.stats import Discrete, Tabular, Univariate, combine_distributions -from .data import ATOMIC_NUMBER, gnds_name +from .data import gnds_name, zam from .function import INTERPOLATION_SCHEME from .endf import Evaluation, get_head_record, get_list_record, get_tab1_record @@ -241,9 +240,7 @@ class DecayMode(EqualityMixin): @property def daughter(self): # Determine atomic number and mass number of parent - symbol, A = re.match(r'([A-Zn][a-z]*)(\d+)', self.parent).groups() - A = int(A) - Z = ATOMIC_NUMBER[symbol] + Z, A, _ = zam(self.parent) # Process changes for mode in self.modes: @@ -253,6 +250,9 @@ class DecayMode(EqualityMixin): delta_A, delta_Z = changes A += delta_A Z += delta_Z + break + else: + return None return gnds_name(Z, A, self._daughter_state) diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index 97ec8d961..42d4ab07e 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -413,6 +413,8 @@ class Chain: type_ = ','.join(mode.modes) if mode.daughter in decay_data: target = mode.daughter + elif 'sf' in type_: + target = None else: print('missing {} {} {}'.format( parent, type_, mode.daughter)) @@ -641,7 +643,7 @@ class Chain: # Allow for total annihilation for debug purposes if branch_val != 0.0: - if target is not None: + if target is not None and 'sf' not in decay_type: k = self.nuclide_dict[target] setval(k, i, branch_val) @@ -731,11 +733,12 @@ class Chain: # Determine light nuclide production, e.g., (n,d) should # produce H2 - light_nucs = REACTIONS[r_type].secondaries - for light_nuc in light_nucs: - k = self.nuclide_dict.get(light_nuc) - if k is not None: - setval(k, i, path_rate * br) + if path_rate != 0.0: + light_nucs = REACTIONS[r_type].secondaries + for light_nuc in light_nucs: + k = self.nuclide_dict.get(light_nuc) + if k is not None: + setval(k, i, path_rate * br) else: for product, y in fission_yields[nuc.name].items(): From 1f7ac4215f45dc0c88a36240230cf16d7cb4cc5b Mon Sep 17 00:00:00 2001 From: Perry <100789850+yrrepy@users.noreply.github.com> Date: Tue, 21 Apr 2026 13:24:53 -0700 Subject: [PATCH 613/671] Clip negative atom densities that result from CRAM (#3879) Co-authored-by: Paul Romano --- openmc/deplete/abc.py | 5 +++++ tests/dummy_operator.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 80d8474ec..d2926ba7e 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -738,6 +738,11 @@ class Integrator(ABC): results = deplete( self._solver, self.chain, n, rates, dt, i, matrix_func, self.transfer_rates, self.external_source_rates) + + # Clip unphysical negative number densities + for r in results: + r.clip(min=0.0, out=r) + return time.time() - start, results @abstractmethod diff --git a/tests/dummy_operator.py b/tests/dummy_operator.py index 9595765d7..873633525 100644 --- a/tests/dummy_operator.py +++ b/tests/dummy_operator.py @@ -24,7 +24,7 @@ DepletionSolutionTuple = namedtuple( predictor_solution = DepletionSolutionTuple( PredictorIntegrator, np.array([1.0, 2.46847546272295, 4.11525874568034]), - np.array([1.0, 0.986431226850467, -0.0581692232513460])) + np.array([1.0, 0.986431226850467, 0.0])) cecm_solution = DepletionSolutionTuple( From 2d5c50080cff21001ea88e61adeb4e2e916ab563 Mon Sep 17 00:00:00 2001 From: Perry <100789850+yrrepy@users.noreply.github.com> Date: Wed, 22 Apr 2026 16:24:34 -0700 Subject: [PATCH 614/671] Allow the use of substeps for CRAM (#3908) Co-authored-by: Paul Romano --- openmc/deplete/abc.py | 160 ++++++++++++-------- openmc/deplete/cram.py | 38 ++++- openmc/deplete/pool.py | 11 +- tests/unit_tests/test_deplete_cram.py | 69 ++++++++- tests/unit_tests/test_deplete_integrator.py | 92 ++++++++++- 5 files changed, 288 insertions(+), 82 deletions(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index d2926ba7e..66bd7148d 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -541,17 +541,15 @@ class Integrator(ABC): iterable of float. Alternatively, units can be specified for each step by passing an iterable of (value, unit) tuples. power : float or iterable of float, optional - Power of the reactor in [W]. A single value indicates that - the power is constant over all timesteps. An iterable - indicates potentially different power levels for each timestep. - For a 2D problem, the power can be given in [W/cm] as long - as the "volume" assigned to a depletion material is actually - an area in [cm^2]. Either ``power``, ``power_density``, or + Power of the reactor in [W]. A single value indicates that the power is + constant over all timesteps. An iterable indicates potentially different + power levels for each timestep. For a 2D problem, the power can be given + in [W/cm] as long as the "volume" assigned to a depletion material is + actually an area in [cm^2]. Either ``power``, ``power_density``, or ``source_rates`` must be specified. power_density : float or iterable of float, optional - Power density of the reactor in [W/gHM]. It is multiplied by - initial heavy metal inventory to get total power if ``power`` - is not specified. + Power density of the reactor in [W/gHM]. It is multiplied by initial + heavy metal inventory to get total power if ``power`` is not specified. source_rates : float or iterable of float, optional Source rate in [neutron/sec] or neutron flux in [neutron/s-cm^2] for each interval in :attr:`timesteps` @@ -563,8 +561,8 @@ class Integrator(ABC): and 'MWd/kg' indicates that the values are given in burnup (MW-d of energy deposited per kilogram of initial heavy metal). solver : str or callable, optional - If a string, must be the name of the solver responsible for - solving the Bateman equations. Current options are: + If a string, must be the name of the solver responsible for solving the + Bateman equations. Current options are: * ``cram16`` - 16th order IPF CRAM * ``cram48`` - 48th order IPF CRAM [default] @@ -573,15 +571,22 @@ class Integrator(ABC): :attr:`solver`. .. versionadded:: 0.12 + substeps : int, optional + Number of substeps per depletion interval. When greater than 1, each + interval is subdivided into `substeps` identical sub-intervals and LU + factorizations may be reused across them, improving accuracy for + nuclides with large decay-constant × timestep products. + + .. versionadded:: 0.15.4 continue_timesteps : bool, optional Whether or not to treat the current solve as a continuation of a previous simulation. Defaults to `False`. When `False`, the depletion steps provided are appended to any previous steps. If `True`, the - timesteps provided to the `Integrator` must exacly match any that - exist in the `prev_results` passed to the `Operator`. The `power`, - `power_density`, or `source_rates` must match as well. The - method of specifying `power`, `power_density`, or - `source_rates` should be the same as the initial run. + timesteps provided to the `Integrator` must exacly match any that exist + in the `prev_results` passed to the `Operator`. The `power`, + `power_density`, or `source_rates` must match as well. The method of + specifying `power`, `power_density`, or `source_rates` should be the + same as the initial run. .. versionadded:: 0.15.1 @@ -601,15 +606,19 @@ class Integrator(ABC): :math:`\frac{\partial}{\partial t}\vec{n} = A_i\vec{n}_i` with a step size :math:`t_i`. Can be configured using the ``solver`` argument. User-supplied functions are expected to have the following signature: - ``solver(A, n0, t) -> n1`` where + ``solver(A, n0, t, substeps=1) -> n1``, where - * ``A`` is a :class:`scipy.sparse.csc_array` making up the - depletion matrix - * ``n0`` is a 1-D :class:`numpy.ndarray` of initial compositions - for a given material in atoms/cm3 - * ``t`` is a float of the time step size in seconds, and - * ``n1`` is a :class:`numpy.ndarray` of compositions at the - next time step. Expected to be of the same shape as ``n0`` + * ``A`` is a :class:`scipy.sparse.csc_array` making up the depletion + matrix + * ``n0`` is a 1-D :class:`numpy.ndarray` of initial compositions for + a given material in atoms/cm3 + * ``t`` is a float of the time step size in seconds + * ``substeps`` is an optional integer number of substeps, and + * ``n1`` is a :class:`numpy.ndarray` of compositions at the next + time step. Expected to be of the same shape as ``n0`` + + Solvers that do not support multiple substeps should raise an exception + when ``substeps > 1``. transfer_rates : openmc.deplete.TransferRates Transfer rates for the depletion system used to model continuous @@ -632,6 +641,7 @@ class Integrator(ABC): source_rates: Optional[Union[float, Sequence[float]]] = None, timestep_units: str = 's', solver: str = "cram48", + substeps: int = 1, continue_timesteps: bool = False, ): if continue_timesteps and operator.prev_res is None: @@ -653,6 +663,8 @@ class Integrator(ABC): # Normalize timesteps and source rates seconds, source_rates = _normalize_timesteps( timesteps, source_rates, timestep_units, operator) + check_type("substeps", substeps, Integral) + check_greater_than("substeps", substeps, 0) if continue_timesteps: # Get timesteps and source rates from previous results @@ -684,6 +696,7 @@ class Integrator(ABC): self.timesteps = np.asarray(seconds) self.source_rates = np.asarray(source_rates) + self.substeps = substeps self.transfer_rates = None self.external_source_rates = None @@ -721,23 +734,32 @@ class Integrator(ABC): self._solver = func return - # Inspect arguments - if len(sig.parameters) != 3: - raise ValueError("Function {} does not support three arguments: " - "{!s}".format(func, sig)) + params = list(sig.parameters.values()) - for ix, param in enumerate(sig.parameters.values()): - if param.kind in {param.KEYWORD_ONLY, param.VAR_KEYWORD}: + # Inspect arguments + if len(params) != 4: + raise ValueError( + "Function {} must support four arguments " + "(A, n0, t, substeps=1): {!s}" + .format(func, sig)) + + for ix, param in enumerate(params): + if param.kind in {param.KEYWORD_ONLY, param.VAR_KEYWORD, + param.VAR_POSITIONAL}: raise ValueError( f"Keyword arguments like {ix} at position {param} are not allowed") + if len(params) == 4 and params[3].default != 1: + raise ValueError( + f"Fourth solver argument must default to 1, not {params[3].default}") + self._solver = func def _timed_deplete(self, n, rates, dt, i=None, matrix_func=None): start = time.time() results = deplete( self._solver, self.chain, n, rates, dt, i, matrix_func, - self.transfer_rates, self.external_source_rates) + self.transfer_rates, self.external_source_rates, self.substeps) # Clip unphysical negative number densities for r in results: @@ -1205,17 +1227,15 @@ class SIIntegrator(Integrator): iterable of float. Alternatively, units can be specified for each step by passing an iterable of (value, unit) tuples. power : float or iterable of float, optional - Power of the reactor in [W]. A single value indicates that - the power is constant over all timesteps. An iterable - indicates potentially different power levels for each timestep. - For a 2D problem, the power can be given in [W/cm] as long - as the "volume" assigned to a depletion material is actually - an area in [cm^2]. Either ``power``, ``power_density``, or + Power of the reactor in [W]. A single value indicates that the power is + constant over all timesteps. An iterable indicates potentially different + power levels for each timestep. For a 2D problem, the power can be given + in [W/cm] as long as the "volume" assigned to a depletion material is + actually an area in [cm^2]. Either ``power``, ``power_density``, or ``source_rates`` must be specified. power_density : float or iterable of float, optional - Power density of the reactor in [W/gHM]. It is multiplied by - initial heavy metal inventory to get total power if ``power`` - is not specified. + Power density of the reactor in [W/gHM]. It is multiplied by initial + heavy metal inventory to get total power if ``power`` is not specified. source_rates : float or iterable of float, optional Source rate in [neutron/sec] or neutron flux in [neutron/s-cm^2] for each interval in :attr:`timesteps` @@ -1227,11 +1247,11 @@ class SIIntegrator(Integrator): that the values are given in burnup (MW-d of energy deposited per kilogram of initial heavy metal). n_steps : int, optional - Number of stochastic iterations per depletion interval. - Must be greater than zero. Default : 10 + Number of stochastic iterations per depletion interval. Must be greater + than zero. Default : 10 solver : str or callable, optional - If a string, must be the name of the solver responsible for - solving the Bateman equations. Current options are: + If a string, must be the name of the solver responsible for solving the + Bateman equations. Current options are: * ``cram16`` - 16th order IPF CRAM * ``cram48`` - 48th order IPF CRAM [default] @@ -1240,16 +1260,23 @@ class SIIntegrator(Integrator): :attr:`solver`. .. versionadded:: 0.12 + substeps : int, optional + Number of substeps per depletion interval. When greater than 1, each + interval is subdivided into `substeps` identical sub-intervals and LU + factorizations may be reused across them, improving accuracy for + nuclides with large decay-constant × timestep products. + + .. versionadded:: 0.15.4 continue_timesteps : bool, optional Whether or not to treat the current solve as a continuation of a - previous simulation. Defaults to `False`. If `False`, all time - steps and source rates will be run in an append fashion and will run - after whatever time steps exist, if any. If `True`, the timesteps - provided to the `Integrator` must match exactly those that exist - in the `prev_results` passed to the `Opereator`. The `power`, - `power_density`, or `source_rates` must match as well. The - method of specifying `power`, `power_density`, or - `source_rates` should be the same as the initial run. + previous simulation. Defaults to `False`. If `False`, all time steps and + source rates will be run in an append fashion and will run after + whatever time steps exist, if any. If `True`, the timesteps provided to + the `Integrator` must match exactly those that exist in the + `prev_results` passed to the `Opereator`. The `power`, `power_density`, + or `source_rates` must match as well. The method of specifying `power`, + `power_density`, or `source_rates` should be the same as the initial + run. .. versionadded:: 0.15.1 @@ -1270,15 +1297,19 @@ class SIIntegrator(Integrator): :math:`\frac{\partial}{\partial t}\vec{n} = A_i\vec{n}_i` with a step size :math:`t_i`. Can be configured using the ``solver`` argument. User-supplied functions are expected to have the following signature: - ``solver(A, n0, t) -> n1`` where + ``solver(A, n0, t, substeps=1) -> n1``, where - * ``A`` is a :class:`scipy.sparse.csc_array` making up the - depletion matrix - * ``n0`` is a 1-D :class:`numpy.ndarray` of initial compositions - for a given material in atoms/cm3 - * ``t`` is a float of the time step size in seconds, and - * ``n1`` is a :class:`numpy.ndarray` of compositions at the - next time step. Expected to be of the same shape as ``n0`` + * ``A`` is a :class:`scipy.sparse.csc_array` making up the depletion + matrix + * ``n0`` is a 1-D :class:`numpy.ndarray` of initial compositions for + a given material in atoms/cm3 + * ``t`` is a float of the time step size in seconds + * ``substeps`` is an optional integer number of substeps, and + * ``n1`` is a :class:`numpy.ndarray` of compositions at the next + time step. Expected to be of the same shape as ``n0`` + + Solvers that do not support multiple substeps should raise an exception + when ``substeps > 1``. .. versionadded:: 0.12 @@ -1294,13 +1325,16 @@ class SIIntegrator(Integrator): timestep_units: str = 's', n_steps: int = 10, solver: str = "cram48", + substeps: int = 1, continue_timesteps: bool = False, ): check_type("n_steps", n_steps, Integral) check_greater_than("n_steps", n_steps, 0) super().__init__( operator, timesteps, power, power_density, source_rates, - timestep_units=timestep_units, solver=solver, continue_timesteps=continue_timesteps) + timestep_units=timestep_units, solver=solver, + substeps=substeps, + continue_timesteps=continue_timesteps) self.n_steps = n_steps def _get_bos_data_from_operator(self, step_index, step_power, n_bos): @@ -1430,7 +1464,7 @@ class DepSystemSolver(ABC): """ @abstractmethod - def __call__(self, A, n0, dt): + def __call__(self, A, n0, dt, substeps=1): """Solve the linear system of equations for depletion Parameters @@ -1443,6 +1477,8 @@ class DepSystemSolver(ABC): material or an atom density dt : float Time [s] of the specific interval to be solved + substeps : int, optional + Number of substeps to use when the solver supports substepping. Returns ------- diff --git a/openmc/deplete/cram.py b/openmc/deplete/cram.py index cecc388f4..3594ffbe8 100644 --- a/openmc/deplete/cram.py +++ b/openmc/deplete/cram.py @@ -3,12 +3,13 @@ Implements two different forms of CRAM for use in openmc.deplete. """ +from functools import partial import numbers import numpy as np -import scipy.sparse.linalg as sla +from scipy.sparse.linalg import spsolve, splu -from openmc.checkvalue import check_type, check_length +from openmc.checkvalue import check_type, check_length, check_greater_than from .abc import DepSystemSolver from .._sparse_compat import csc_array, eye_array @@ -24,6 +25,12 @@ class IPFCramSolver(DepSystemSolver): Chebyshev Rational Approximation Method and Application to Burnup Equations `_," Nucl. Sci. Eng., 182:3, 297-318. + When `substeps` > 1, the time interval is split into `substeps` identical + sub-intervals and LU factorizations are reused across them, as described + in: A. Isotalo and M. Pusa, "`Improving the Accuracy of the Chebyshev + Rational Approximation Method Using Substeps + `_," Nucl. Sci. Eng., 183:1, 65-77. + Parameters ---------- alpha : numpy.ndarray @@ -55,7 +62,7 @@ class IPFCramSolver(DepSystemSolver): self.theta = theta self.alpha0 = alpha0 - def __call__(self, A, n0, dt): + def __call__(self, A, n0, dt, substeps=1): """Solve depletion equations using IPF CRAM Parameters @@ -68,6 +75,8 @@ class IPFCramSolver(DepSystemSolver): material or an atom density dt : float Time [s] of the specific interval to be solved + substeps : int, optional + Number of substeps per depletion interval. Returns ------- @@ -75,12 +84,25 @@ class IPFCramSolver(DepSystemSolver): Final compositions after ``dt`` """ - A = dt * csc_array(A, dtype=np.float64) - y = n0.copy() + check_type("substeps", substeps, numbers.Integral) + check_greater_than("substeps", substeps, 0) + + step_dt = dt if substeps == 1 else dt / substeps + A = step_dt * csc_array(A, dtype=np.float64) ident = eye_array(A.shape[0], format='csc') - for alpha, theta in zip(self.alpha, self.theta): - y += 2*np.real(alpha*sla.spsolve(A - theta*ident, y)) - return y * self.alpha0 + + if substeps == 1: + solvers = [partial(spsolve, A - theta * ident) for theta in self.theta] + else: + # Pre-compute LU factorizations and reuse them across substeps. + solvers = [splu(A - theta * ident).solve for theta in self.theta] + + y = n0.copy() + for _ in range(substeps): + for alpha, solve in zip(self.alpha, solvers): + y += 2 * np.real(alpha * solve(y)) + y *= self.alpha0 + return y # Coefficients for IPF Cram 16 diff --git a/openmc/deplete/pool.py b/openmc/deplete/pool.py index 58f90894b..19ad0ada5 100644 --- a/openmc/deplete/pool.py +++ b/openmc/deplete/pool.py @@ -42,14 +42,15 @@ def _distribute(items): j += chunk_size def deplete(func, chain, n, rates, dt, current_timestep=None, matrix_func=None, - transfer_rates=None, external_source_rates=None, *matrix_args): + transfer_rates=None, external_source_rates=None, substeps=1, + *matrix_args): """Deplete materials using given reaction rates for a specified time Parameters ---------- func : callable Function to use to get new compositions. Expected to have the signature - ``func(A, n0, t) -> n1`` + ``func(A, n0, t, substeps=1) -> n1``. chain : openmc.deplete.Chain Depletion chain n : list of numpy.ndarray @@ -74,6 +75,8 @@ def deplete(func, chain, n, rates, dt, current_timestep=None, matrix_func=None, External source rates for continuous removal/feed. .. versionadded:: 0.15.3 + substeps : int, optional + Number of substeps to pass to solvers that support substepping. matrix_args: Any, optional Additional arguments passed to matrix_func @@ -164,7 +167,7 @@ def deplete(func, chain, n, rates, dt, current_timestep=None, matrix_func=None, # Concatenate vectors of nuclides in one n_multi = np.concatenate(n) - n_result = func(matrix, n_multi, dt) + n_result = func(matrix, n_multi, dt, substeps) # Split back the nuclide vector result into the original form n_result = np.split(n_result, np.cumsum([len(i) for i in n])[:-1]) @@ -198,7 +201,7 @@ def deplete(func, chain, n, rates, dt, current_timestep=None, matrix_func=None, matrix.resize(matrix.shape[1], matrix.shape[1]) n[i] = np.append(n[i], 1.0) - inputs = zip(matrices, n, repeat(dt)) + inputs = zip(matrices, n, repeat(dt), repeat(substeps)) if USE_MULTIPROCESSING: with Pool(NUM_PROCESSES) as pool: diff --git a/tests/unit_tests/test_deplete_cram.py b/tests/unit_tests/test_deplete_cram.py index 8987fbd7a..64cff3a8b 100644 --- a/tests/unit_tests/test_deplete_cram.py +++ b/tests/unit_tests/test_deplete_cram.py @@ -1,12 +1,15 @@ -""" Tests for cram.py +"""Tests for cram.py. Compares a few Mathematica matrix exponentials to CRAM16/CRAM48. +Tests substep accuracy against self-converged reference solutions. """ -from pytest import approx import numpy as np +import pytest import scipy.sparse as sp -from openmc.deplete.cram import CRAM16, CRAM48 +from pytest import approx +from openmc.deplete.cram import (CRAM16, CRAM48, Cram16Solver, Cram48Solver, + IPFCramSolver) def test_CRAM16(): @@ -35,3 +38,63 @@ def test_CRAM48(): z0 = np.array((0.904837418035960, 0.576799023327476)) assert z == approx(z0) + + +def test_substeps1_matches_original(): + """substeps=1 must be bitwise identical to original spsolve path.""" + x = np.array([1.0, 1.0]) + mat = sp.csr_matrix([[-1.0, 0.0], [-2.0, -3.0]]) + dt = 0.1 + + z_orig = CRAM48(mat, x, dt) + z_sub1 = CRAM48(mat, x, dt, substeps=1) + + np.testing.assert_array_equal(z_sub1, z_orig) + + +def test_substeps2_matches_two_half_steps(): + """substeps=2 must match two independent CRAM calls with dt/2.""" + x = np.array([1.0, 1.0]) + mat = sp.csr_matrix([[-1.0, 0.0], [-2.0, -3.0]]) + dt = 1.0 + + # Two manual half-steps using original spsolve path + z_half = CRAM48(mat, x, dt / 2) + z_two = CRAM48(mat, z_half, dt / 2) + + # Single call with substeps=2 + z_sub2 = CRAM48(mat, x, dt, substeps=2) + + assert z_sub2 == approx(z_two, rel=1e-12) + + +@pytest.mark.parametrize("substeps", [0, -1]) +def test_invalid_substeps(substeps): + """substeps must be a positive integer at call time.""" + x = np.array([1.0, 1.0]) + mat = sp.csr_matrix([[-1.0, 0.0], [-2.0, -3.0]]) + + with pytest.raises(ValueError, match="substeps"): + CRAM48(mat, x, 0.1, substeps=substeps) + + +def test_substeps_self_convergence(): + """Increasing substeps converges toward reference solution. + + Uses CRAM16 (alpha0 ~ 2e-16) where substep convergence is visible. + CRAM48 (alpha0 ~ 2e-47) is already near machine precision for small + systems; its correctness is verified by the other substep tests. + """ + mat = sp.csr_matrix([[-1.0, 0.0], [-2.0, -3.0]]) + x = np.array([1.0, 1.0]) + dt = 50 # lambda*dt = 50 and 150, stresses CRAM16 + + n_ref = CRAM16(mat, x, dt, substeps=128) + + prev_err = np.inf + for s in [1, 2, 4, 8, 16]: + n_s = CRAM16(mat, x, dt, substeps=s) + err = np.linalg.norm(n_s - n_ref) / np.linalg.norm(n_ref) + assert err < prev_err, \ + f"substeps={s} error {err:.2e} not less than previous {prev_err:.2e}" + prev_err = err diff --git a/tests/unit_tests/test_deplete_integrator.py b/tests/unit_tests/test_deplete_integrator.py index 558cb434b..ec886e707 100644 --- a/tests/unit_tests/test_deplete_integrator.py +++ b/tests/unit_tests/test_deplete_integrator.py @@ -19,7 +19,7 @@ from openmc.mpi import comm from openmc.deplete import ( ReactionRates, StepResult, Results, OperatorResult, PredictorIntegrator, CECMIntegrator, CF4Integrator, CELIIntegrator, EPCRK4Integrator, - LEQIIntegrator, SICELIIntegrator, SILEQIIntegrator, cram) + LEQIIntegrator, SICELIIntegrator, SILEQIIntegrator, cram, pool) from tests import dummy_operator @@ -183,18 +183,42 @@ def test_bad_integrator_inputs(): with pytest.raises(TypeError, match=".*callable.*NoneType"): PredictorIntegrator(op, timesteps, power=1, solver=None) - with pytest.raises(ValueError, match=".*arguments"): + with pytest.raises(ValueError, match="four arguments"): PredictorIntegrator(op, timesteps, power=1, solver=mock_bad_solver_nargs) + with pytest.raises(ValueError, match="default to 1"): + PredictorIntegrator(op, timesteps, power=1, + solver=mock_bad_solver_fourth_required) -def mock_good_solver(A, n, t): - pass + with pytest.raises(ValueError, match="substeps"): + PredictorIntegrator(op, timesteps, power=1, substeps=0) + + with pytest.raises(ValueError, match="substeps"): + PredictorIntegrator(op, timesteps, power=1, substeps=-1) + + +def mock_good_solver(A, n, t, substeps=1): + return n.copy() + + +def mock_good_solver_substeps(A, n, t, substeps=1): + return n + substeps + + +def mock_unsupported_substeps_solver(A, n, t, substeps=1): + if substeps > 1: + raise NotImplementedError("substeps > 1 not supported") + return n.copy() def mock_bad_solver_nargs(A, n): pass +def mock_bad_solver_fourth_required(A, n, t, substeps): + pass + + @pytest.mark.parametrize("scheme", dummy_operator.SCHEMES) def test_integrator(run_in_tmpdir, scheme): """Test the integrators against their expected values""" @@ -226,13 +250,71 @@ def test_integrator(run_in_tmpdir, scheme): integrator = bundle.solver(operator, [0.75], 1, solver="cram16") assert integrator.solver is cram.CRAM16 + integrator = bundle.solver(operator, [0.75], 1, solver=cram.Cram48Solver, + substeps=2) + assert integrator.solver is cram.Cram48Solver + assert integrator.substeps == 2 + integrator.solver = mock_good_solver assert integrator.solver is mock_good_solver - lfunc = lambda A, n, t: mock_good_solver(A, n, t) + lfunc = lambda A, n, t, substeps=1: mock_good_solver(A, n, t, substeps) integrator.solver = lfunc assert integrator.solver is lfunc + integrator.solver = mock_good_solver_substeps + assert integrator.solver is mock_good_solver_substeps + + +def test_custom_solver_with_default_substeps(monkeypatch): + operator = dummy_operator.DummyOperator() + n = operator.initial_condition() + rates = operator(n, 1.0).rates + integrator = PredictorIntegrator( + operator, [0.75], power=1.0, solver=mock_good_solver) + monkeypatch.setattr(pool, "USE_MULTIPROCESSING", False) + + _, result = integrator._timed_deplete(n, rates, 0.75) + + np.testing.assert_array_equal(result[0], n[0]) + + +def test_substep_aware_custom_solver_receives_substeps(monkeypatch): + operator = dummy_operator.DummyOperator() + n = operator.initial_condition() + rates = operator(n, 1.0).rates + integrator = PredictorIntegrator( + operator, [0.75], power=1.0, solver=mock_good_solver_substeps, + substeps=3) + monkeypatch.setattr(pool, "USE_MULTIPROCESSING", False) + + _, result = integrator._timed_deplete(n, rates, 0.75) + + np.testing.assert_array_equal(result[0], n[0] + 3) + + +def test_custom_solver_propagates_substeps_error(monkeypatch): + operator = dummy_operator.DummyOperator() + n = operator.initial_condition() + rates = operator(n, 1.0).rates + integrator = PredictorIntegrator( + operator, [0.75], power=1.0, + solver=mock_unsupported_substeps_solver, substeps=2) + monkeypatch.setattr(pool, "USE_MULTIPROCESSING", False) + + with pytest.raises(NotImplementedError, match="not supported"): + integrator._timed_deplete(n, rates, 0.75) + + +def test_custom_solver_requires_four_args(): + op = MagicMock() + op.prev_res = None + op.chain = None + op.heavy_metal = 1.0 + + with pytest.raises(ValueError, match="four arguments"): + PredictorIntegrator(op, [1], power=1, solver=mock_bad_solver_nargs) + @pytest.mark.parametrize("integrator", INTEGRATORS) def test_timesteps(integrator): From 806fb4ce774dc4e0e4eb7b749b95e98e1ed3cbc3 Mon Sep 17 00:00:00 2001 From: "Travis L." Date: Mon, 27 Apr 2026 08:40:44 -0600 Subject: [PATCH 615/671] Clean up MCPL references (#3927) --- docs/source/usersguide/install.rst | 10 +++++----- src/output.cpp | 5 ----- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/docs/source/usersguide/install.rst b/docs/source/usersguide/install.rst index 1ef2f574b..2a0d301d4 100644 --- a/docs/source/usersguide/install.rst +++ b/docs/source/usersguide/install.rst @@ -331,11 +331,11 @@ Prerequisites This option allows OpenMC to read and write MCPL (Monte Carlo Particle Lists) files instead of .h5 files for sources (external source - distribution, k-eigenvalue source distribution, and surface sources). To - turn this option on in the CMake configuration step, add the following - option:: - - cmake -DOPENMC_USE_MCPL=on .. + distribution, k-eigenvalue source distribution, and surface sources). + OpenMC does not need any particular build option to use this, but MCPL + must be installed on the system in order to do so. Refer to the + `MCPL documentation `_ + for instructions on how to accomplish this. * NCrystal_ library for defining materials with enhanced thermal neutron transport diff --git a/src/output.cpp b/src/output.cpp index c66c313dc..f8c0f2a97 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -321,7 +321,6 @@ void print_build_info() std::string png(n); std::string profiling(n); std::string coverage(n); - std::string mcpl(n); std::string uwuw(n); std::string strict_fp(n); @@ -337,9 +336,6 @@ void print_build_info() #ifdef OPENMC_LIBMESH_ENABLED libmesh = y; #endif -#ifdef OPENMC_MCPL - mcpl = y; -#endif #ifdef USE_LIBPNG png = y; #endif @@ -369,7 +365,6 @@ void print_build_info() fmt::print("PNG support: {}\n", png); fmt::print("DAGMC support: {}\n", dagmc); fmt::print("libMesh support: {}\n", libmesh); - fmt::print("MCPL support: {}\n", mcpl); fmt::print("Coverage testing: {}\n", coverage); fmt::print("Profiling flags: {}\n", profiling); fmt::print("UWUW support: {}\n", uwuw); From 1116c4bdc04e64dfe1dfb528376919de027b3134 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 28 Apr 2026 09:02:08 -0500 Subject: [PATCH 616/671] Support multiple meshes in R2S calculations (#3860) --- docs/source/usersguide/decay_sources.rst | 19 ++ openmc/deplete/microxs.py | 137 +++++++------ openmc/deplete/r2s.py | 234 +++++++++++++---------- tests/unit_tests/test_deplete_microxs.py | 6 +- tests/unit_tests/test_r2s.py | 75 +++++++- 5 files changed, 308 insertions(+), 163 deletions(-) diff --git a/docs/source/usersguide/decay_sources.rst b/docs/source/usersguide/decay_sources.rst index 398680e74..21981fdaa 100644 --- a/docs/source/usersguide/decay_sources.rst +++ b/docs/source/usersguide/decay_sources.rst @@ -190,6 +190,25 @@ we would run:: r2s.run(timesteps, source_rates, mat_vol_kwargs={'n_samples': 10_000_000}) +It is also possible to use multiple meshes by passing a list of meshes instead +of a single mesh. This can be useful, for example, when different regions of the +model require different mesh resolutions. The meshes are assumed to be +**non-overlapping**; each element--material combination across all meshes is +treated as an independent activation region, and all meshes are handled in a +single neutron transport solve. For example:: + + # Fine mesh near the activation target + mesh_fine = openmc.RegularMesh() + mesh_fine.dimension = (10, 10, 10) + ... + + # Coarse mesh for the surrounding region + mesh_coarse = openmc.RegularMesh() + mesh_coarse.dimension = (5, 5, 5) + ... + + r2s = openmc.deplete.R2SManager(model, [mesh_fine, mesh_coarse]) + Direct 1-Step (D1S) Calculations ================================ diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index e6e2dbce2..687cf646f 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -36,7 +36,8 @@ DomainTypes: TypeAlias = Union[ Sequence[openmc.Cell], Sequence[openmc.Universe], openmc.MeshBase, - openmc.Filter + openmc.Filter, + Sequence[openmc.Filter] ] @@ -69,8 +70,12 @@ def get_microxs_and_flux( ---------- model : openmc.Model OpenMC model object. Must contain geometry, materials, and settings. - domains : list of openmc.Material or openmc.Cell or openmc.Universe, or openmc.MeshBase, or openmc.Filter + domains : list of openmc.Material or openmc.Cell or openmc.Universe, or openmc.MeshBase, or openmc.Filter, or list of openmc.Filter Domains in which to tally reaction rates, or a spatial tally filter. + A list of filters can be provided to create one set of tallies per + filter (e.g., one :class:`~openmc.MeshMaterialFilter` per mesh) that + are all evaluated in a single transport solve. Results are + concatenated across all filters in order. nuclides : list of str Nuclides to get cross sections for. If not specified, all burnable nuclides from the depletion chain file are used. @@ -142,26 +147,24 @@ def get_microxs_and_flux( else: energy_filter = openmc.EnergyFilter(energies) + # Build list of domain filters if isinstance(domains, openmc.Filter): - domain_filter = domains + domain_filters = [domains] elif isinstance(domains, openmc.MeshBase): - domain_filter = openmc.MeshFilter(domains) + domain_filters = [openmc.MeshFilter(domains)] + elif isinstance(domains, Sequence) and len(domains) > 0 and \ + isinstance(domains[0], openmc.Filter): + domain_filters = list(domains) elif isinstance(domains[0], openmc.Material): - domain_filter = openmc.MaterialFilter(domains) + domain_filters = [openmc.MaterialFilter(domains)] elif isinstance(domains[0], openmc.Cell): - domain_filter = openmc.CellFilter(domains) + domain_filters = [openmc.CellFilter(domains)] elif isinstance(domains[0], openmc.Universe): - domain_filter = openmc.UniverseFilter(domains) + domain_filters = [openmc.UniverseFilter(domains)] else: raise ValueError(f"Unsupported domain type: {type(domains[0])}") - flux_tally = openmc.Tally(name='MicroXS flux') - flux_tally.filters = [domain_filter, energy_filter] - flux_tally.scores = ['flux'] - model.tallies = [flux_tally] - - # Prepare reaction-rate tally for 'direct' or subset for 'flux' with opts - rr_tally = None + # Prepare reaction-rate nuclides/reactions rr_nuclides: list[str] = [] rr_reactions: list[str] = [] if reaction_rate_mode == 'direct': @@ -177,20 +180,33 @@ def get_microxs_and_flux( 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') - # 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 = rr_reactions - model.tallies.append(rr_tally) + # Use 1-group energy filter for RR in flux mode + has_rr = bool(rr_nuclides and rr_reactions) + if has_rr and reaction_rate_mode == 'flux': + rr_energy_filter = openmc.EnergyFilter( + [energy_filter.values[0], energy_filter.values[-1]]) + else: + rr_energy_filter = energy_filter + + # Create one flux tally (and optionally one RR tally) per domain filter. + flux_tallies = [] + rr_tallies = [] + model.tallies = [] + for i, domain_filter in enumerate(domain_filters): + flux_tally = openmc.Tally(name=f'MicroXS flux {i}') + flux_tally.filters = [domain_filter, energy_filter] + flux_tally.scores = ['flux'] + model.tallies.append(flux_tally) + flux_tallies.append(flux_tally) + + if has_rr: + rr_tally = openmc.Tally(name=f'MicroXS RR {i}') + rr_tally.filters = [domain_filter, rr_energy_filter] + rr_tally.nuclides = rr_nuclides + rr_tally.multiply_density = False + rr_tally.scores = rr_reactions + model.tallies.append(rr_tally) + rr_tallies.append(rr_tally) if openmc.lib.is_initialized: openmc.lib.finalize() @@ -227,40 +243,41 @@ def get_microxs_and_flux( # Read in tally results (on all ranks) with StatePoint(statepoint_path) as sp: - if rr_tally is not None: - rr_tally = sp.tallies[rr_tally.id] - rr_tally._read_results() - flux_tally = sp.tallies[flux_tally.id] - flux_tally._read_results() + for i in range(len(flux_tallies)): + flux_tallies[i] = sp.tallies[flux_tallies[i].id] + flux_tallies[i]._read_results() + if rr_tallies: + rr_tallies[i] = sp.tallies[rr_tallies[i].id] + rr_tallies[i]._read_results() - # Get flux values and make energy groups last dimension - flux = flux_tally.get_reshaped_data() # (domains, groups, 1, 1) - flux = np.moveaxis(flux, 1, -1) # (domains, 1, 1, groups) + # Concatenate results across all domain filters + fluxes = [] + all_flux_arrays = [] + for flux_tally in flux_tallies: + # Get flux values and make energy groups last dimension + flux = flux_tally.get_reshaped_data() # (domains, groups, 1, 1) + flux = np.moveaxis(flux, 1, -1) # (domains, 1, 1, groups) + all_flux_arrays.append(flux) + fluxes.extend(flux.squeeze((1, 2))) - # Create list where each item corresponds to one domain - fluxes = list(flux.squeeze((1, 2))) + # If we built reaction-rate tallies, compute microscopic cross sections + if rr_tallies: + direct_micros = [] + for flux_arr, rr_tally in zip(all_flux_arrays, rr_tallies): + flux = flux_arr + # Get reaction rates and make energy groups last dimension + reaction_rates = rr_tally.get_reshaped_data() # (domains, groups, nuclides, reactions) + reaction_rates = np.moveaxis(reaction_rates, 1, -1) # (domains, nuclides, reactions, groups) - # 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) + # If RR is 1-group, sum flux over groups + if reaction_rate_mode == "flux": + flux = flux.sum(axis=-1, keepdims=True) - # 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.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 - direct_micros = [MicroXS(xs_i, rr_nuclides, rr_reactions) for xs_i in xs] + xs = np.zeros_like(reaction_rates) + d, _, _, g = np.nonzero(flux) + xs[d, ..., g] = reaction_rates[d, ..., g] / flux[d, :, :, g] + direct_micros.extend( + 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': @@ -273,9 +290,9 @@ def get_microxs_and_flux( ) 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: + if reaction_rate_mode == 'flux' and rr_tallies: micros = [m1.merge(m2) for m1, m2 in zip(flux_micros, direct_micros)] - elif rr_tally is not None: + elif rr_tallies: micros = direct_micros else: micros = flux_micros diff --git a/openmc/deplete/r2s.py b/openmc/deplete/r2s.py index 57bbe437f..10d7fdd50 100644 --- a/openmc/deplete/r2s.py +++ b/openmc/deplete/r2s.py @@ -13,11 +13,11 @@ from .results import Results from ..checkvalue import PathLike from ..mpi import comm from openmc.lib import TemporarySession -from openmc.utility_funcs import change_directory def get_activation_materials( - model: openmc.Model, mmv: openmc.MeshMaterialVolumes + model: openmc.Model, + mmv_list: list[openmc.MeshMaterialVolumes] ) -> openmc.Materials: """Get a list of activation materials for each mesh element/material. @@ -31,35 +31,35 @@ def get_activation_materials( ---------- model : openmc.Model The full model containing the geometry and materials. - mmv : openmc.MeshMaterialVolumes - The mesh material volumes object containing the materials and their - volumes for each mesh element. + mmv_list : list of openmc.MeshMaterialVolumes + List of mesh material volumes objects, one per mesh, containing the + materials and their volumes for each mesh element. Returns ------- openmc.Materials A list of materials, each corresponding to a unique mesh element and - material combination. + material combination across all meshes. """ - # Get the material ID, volume, and element index for each element-material - # combination - mat_ids = mmv._materials[mmv._materials > -1] - volumes = mmv._volumes[mmv._materials > -1] - elems, _ = np.where(mmv._materials > -1) - # Get all materials in the model material_dict = model._get_all_materials() # Create a new activation material for each element-material combination + # across all meshes materials = openmc.Materials() - for elem, mat_id, vol in zip(elems, mat_ids, volumes): - mat = material_dict[mat_id] - new_mat = mat.clone() - new_mat.depletable = True - new_mat.name = f'Element {elem}, Material {mat_id}' - new_mat.volume = vol - materials.append(new_mat) + for mesh_idx, mmv in enumerate(mmv_list): + mat_ids = mmv._materials[mmv._materials > -1] + volumes = mmv._volumes[mmv._materials > -1] + elems, _ = np.where(mmv._materials > -1) + + for elem, mat_id, vol in zip(elems, mat_ids, volumes): + mat = material_dict[mat_id] + new_mat = mat.clone() + new_mat.depletable = True + new_mat.name = f'Mesh {mesh_idx}, Element {elem}, Material {mat_id}' + new_mat.volume = vol + materials.append(new_mat) return materials @@ -70,7 +70,9 @@ class R2SManager: This class is responsible for managing the materials and sources needed for mesh-based or cell-based R2S calculations. It provides methods to get activation materials and decay photon sources based on the mesh/cells and - materials in the OpenMC model. + materials in the OpenMC model. Multiple meshes can be specified as domains, + in which case each element--material combination of each mesh is treated as + an activation region (meshes are assumed to be non-overlapping). This class supports the use of a different models for the neutron and photon transport calculation. However, for cell-based calculations, it assumes that @@ -83,17 +85,20 @@ class R2SManager: ---------- neutron_model : openmc.Model The OpenMC model to use for neutron transport. - domains : openmc.MeshBase or Sequence[openmc.Cell] - The mesh or a sequence of cells that represent the spatial units over - which the R2S calculation will be performed. + domains : openmc.MeshBase or Sequence[openmc.MeshBase] or Sequence[openmc.Cell] + The mesh(es) or a sequence of cells that represent the spatial units + over which the R2S calculation will be performed. When a single + :class:`~openmc.MeshBase` or a sequence of meshes is given, each + element--material combination across all meshes is treated as an + activation region. photon_model : openmc.Model, optional The OpenMC model to use for photon transport calculations. If None, a shallow copy of the neutron_model will be created and used. Attributes ---------- - domains : openmc.MeshBase or Sequence[openmc.Cell] - The mesh or a sequence of cells that represent the spatial units over + domains : list of openmc.MeshBase or Sequence[openmc.Cell] + The meshes or a sequence of cells that represent the spatial units over which the R2S calculation will be performed. neutron_model : openmc.Model The OpenMC model used for neutron transport. @@ -101,7 +106,7 @@ class R2SManager: The OpenMC model used for photon transport calculations. method : {'mesh-based', 'cell-based'} Indicates whether the R2S calculation uses mesh elements ('mesh-based') - as the spatial discetization or a list of a cells ('cell-based'). + as the spatial discretization or a list of cells ('cell-based'). results : dict A dictionary that stores results from the R2S calculation. @@ -109,7 +114,7 @@ class R2SManager: def __init__( self, neutron_model: openmc.Model, - domains: openmc.MeshBase | Sequence[openmc.Cell], + domains: openmc.MeshBase | Sequence[openmc.MeshBase] | Sequence[openmc.Cell], photon_model: openmc.Model | None = None, ): self.neutron_model = neutron_model @@ -126,9 +131,14 @@ class R2SManager: self.photon_model = photon_model if isinstance(domains, openmc.MeshBase): self.method = 'mesh-based' + self.domains = [domains] + elif isinstance(domains, Sequence) and len(domains) > 0 and \ + isinstance(domains[0], openmc.MeshBase): + self.method = 'mesh-based' + self.domains = list(domains) else: self.method = 'cell-based' - self.domains = domains + self.domains = list(domains) self.results = {} def run( @@ -243,11 +253,13 @@ class R2SManager: ): """Run the neutron transport step. - This step computes the material volume fractions on the mesh, creates a - mesh-material filter, and retrieves the fluxes and microscopic cross - sections for each mesh/material combination. This step will populate the - 'fluxes' and 'micros' keys in the results dictionary. For a mesh-based - calculation, it will also populate the 'mesh_material_volumes' key. + This step computes the material volume fractions on each mesh, creates + mesh-material filters, and retrieves the fluxes and microscopic cross + sections for each mesh/material combination via a single transport + solve. This step will populate the 'fluxes' and 'micros' keys in the + results dictionary. For a mesh-based calculation, it will also populate + the 'mesh_material_volumes' key (a list of + :class:`~openmc.MeshMaterialVolumes`, one per mesh). Parameters ---------- @@ -266,19 +278,28 @@ class R2SManager: output_dir.mkdir(parents=True, exist_ok=True) if self.method == 'mesh-based': - # Compute material volume fractions on the mesh + # Compute material volume fractions on each mesh if mat_vol_kwargs is None: mat_vol_kwargs = {} mat_vol_kwargs.setdefault('bounding_boxes', True) - self.results['mesh_material_volumes'] = mmv = comm.bcast( - self.domains.material_volumes(self.neutron_model, **mat_vol_kwargs)) - # Save results to file - if comm.rank == 0: - mmv.save(output_dir / 'mesh_material_volumes.npz') + mmv_list = [] + domain_filters = [] + for i, mesh in enumerate(self.domains): + mmv = comm.bcast( + mesh.material_volumes(self.neutron_model, **mat_vol_kwargs)) + mmv_list.append(mmv) - # Create mesh-material filter based on what combos were found - domains = openmc.MeshMaterialFilter.from_volumes(self.domains, mmv) + # Save results to file + if comm.rank == 0: + mmv.save(output_dir / f'mesh_material_volumes_{i}.npz') + + # Create mesh-material filter for this mesh + domain_filters.append( + openmc.MeshMaterialFilter.from_volumes(mesh, mmv)) + + self.results['mesh_material_volumes'] = mmv_list + domains = domain_filters else: domains: Sequence[openmc.Cell] = self.domains @@ -357,8 +378,9 @@ class R2SManager: if self.method == 'mesh-based': # Get unique material for each (mesh, material) combination - mmv = self.results['mesh_material_volumes'] - self.results['activation_materials'] = get_activation_materials(self.neutron_model, mmv) + mmv_list = self.results['mesh_material_volumes'] + self.results['activation_materials'] = get_activation_materials( + self.neutron_model, mmv_list) else: # Create unique material for each cell activation_mats = openmc.Materials() @@ -468,12 +490,20 @@ class R2SManager: # photon model if it is different from the neutron model to account for # potential material changes if self.method == 'mesh-based' and different_photon_model: - self.results['mesh_material_volumes_photon'] = photon_mmv = comm.bcast( - self.domains.material_volumes(self.photon_model, **mat_vol_kwargs)) + if mat_vol_kwargs is None: + mat_vol_kwargs = {} + photon_mmv_list = [] + for i, mesh in enumerate(self.domains): + photon_mmv = comm.bcast( + mesh.material_volumes(self.photon_model, **mat_vol_kwargs)) + photon_mmv_list.append(photon_mmv) - # Save photon MMV results to file - if comm.rank == 0: - photon_mmv.save(output_dir / 'mesh_material_volumes.npz') + # Save photon MMV results to file + if comm.rank == 0: + photon_mmv.save( + output_dir / f'mesh_material_volumes_{i}.npz') + + self.results['mesh_material_volumes_photon'] = photon_mmv_list if comm.rank == 0: tally_ids = [tally.id for tally in self.photon_model.tallies] @@ -543,7 +573,7 @@ class R2SManager: ) -> list[openmc.IndependentSource]: """Create decay photon source for a mesh-based calculation. - For each mesh element-material combination, an + For each mesh element-material combination across all meshes, an :class:`~openmc.IndependentSource` is created with a :class:`~openmc.stats.Box` spatial distribution based on the bounding box of the material within the mesh element. A material constraint is @@ -575,52 +605,56 @@ class R2SManager: index_mat = 0 # Get various results from previous steps - mat_vols = self.results['mesh_material_volumes'] + mmv_list = self.results['mesh_material_volumes'] materials = self.results['activation_materials'] results = self.results['depletion_results'] - photon_mat_vols = self.results.get('mesh_material_volumes_photon') + photon_mmv_list = self.results.get('mesh_material_volumes_photon') - # Total number of mesh elements - n_elements = mat_vols.num_elements + for mesh_idx, mat_vols in enumerate(mmv_list): + photon_mat_vols = photon_mmv_list[mesh_idx] \ + if photon_mmv_list is not None else None - for index_elem in range(n_elements): - # Determine which materials exist in the photon model for this element - if photon_mat_vols is not None: - photon_materials = { - mat_id - for mat_id, _ in photon_mat_vols.by_element(index_elem) - if mat_id is not None - } + # Total number of mesh elements for this mesh + n_elements = mat_vols.num_elements - for mat_id, _, bbox in mat_vols.by_element(index_elem, include_bboxes=True): - # Skip void volume - if mat_id is None: - continue + for index_elem in range(n_elements): + # Determine which materials exist in the photon model for this element + if photon_mat_vols is not None: + photon_materials = { + mat_id + for mat_id, _ in photon_mat_vols.by_element(index_elem) + if mat_id is not None + } - # Skip if this material doesn't exist in photon model - if photon_mat_vols is not None and mat_id not in photon_materials: + for mat_id, _, bbox in mat_vols.by_element(index_elem, include_bboxes=True): + # Skip void volume + if mat_id is None: + continue + + # Skip if this material doesn't exist in photon model + if photon_mat_vols is not None and mat_id not in photon_materials: + index_mat += 1 + continue + + # Get activated material composition + original_mat = materials[index_mat] + activated_mat = results[time_index].get_material(str(original_mat.id)) + + # Create decay photon source + energy = activated_mat.get_decay_photon_energy() + if energy is not None: + strength = energy.integral() + space = openmc.stats.Box(*bbox) + sources.append(openmc.IndependentSource( + space=space, + energy=energy, + particle='photon', + strength=strength, + constraints={'domains': [mat_dict[mat_id]]} + )) + + # Increment index of activated material index_mat += 1 - continue - - # Get activated material composition - original_mat = materials[index_mat] - activated_mat = results[time_index].get_material(str(original_mat.id)) - - # Create decay photon source - energy = activated_mat.get_decay_photon_energy() - if energy is not None: - strength = energy.integral() - space = openmc.stats.Box(*bbox) - sources.append(openmc.IndependentSource( - space=space, - energy=energy, - particle='photon', - strength=strength, - constraints={'domains': [mat_dict[mat_id]]} - )) - - # Increment index of activated material - index_mat += 1 return sources @@ -638,10 +672,13 @@ class R2SManager: # Load neutron transport results neutron_dir = path / 'neutron_transport' if self.method == 'mesh-based': - mmv_file = neutron_dir / 'mesh_material_volumes.npz' - if mmv_file.exists(): - self.results['mesh_material_volumes'] = \ - openmc.MeshMaterialVolumes.from_npz(mmv_file) + mmv_files = sorted(neutron_dir.glob('mesh_material_volumes*.npz'), + key=lambda p: int(p.stem.split('_')[-1]) + if p.stem[-1].isdigit() else 0) + if mmv_files: + self.results['mesh_material_volumes'] = [ + openmc.MeshMaterialVolumes.from_npz(f) for f in mmv_files + ] fluxes_file = neutron_dir / 'fluxes.npy' if fluxes_file.exists(): self.results['fluxes'] = list(np.load(fluxes_file, allow_pickle=True)) @@ -665,10 +702,15 @@ class R2SManager: # Load photon mesh material volumes if they exist (for mesh-based calculations) if self.method == 'mesh-based': - photon_mmv_file = photon_dir / 'mesh_material_volumes.npz' - if photon_mmv_file.exists(): - self.results['mesh_material_volumes_photon'] = \ - openmc.MeshMaterialVolumes.from_npz(photon_mmv_file) + photon_mmv_files = sorted( + photon_dir.glob('mesh_material_volumes*.npz'), + key=lambda p: int(p.stem.split('_')[-1]) + if p.stem[-1].isdigit() else 0) + if photon_mmv_files: + self.results['mesh_material_volumes_photon'] = [ + openmc.MeshMaterialVolumes.from_npz(f) + for f in photon_mmv_files + ] # Load tally IDs from JSON file tally_ids_path = photon_dir / 'tally_ids.json' diff --git a/tests/unit_tests/test_deplete_microxs.py b/tests/unit_tests/test_deplete_microxs.py index 340ba6163..26529e6ce 100644 --- a/tests/unit_tests/test_deplete_microxs.py +++ b/tests/unit_tests/test_deplete_microxs.py @@ -165,11 +165,11 @@ def test_hybrid_tally_setup(): # 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 + assert 'MicroXS flux 0' in tally_names + assert 'MicroXS RR 0' 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') + rr = next(t for t in captured['tallies'] if t.name == 'MicroXS RR 0') assert rr.nuclides == ['U235'] assert rr.scores == ['fission'] diff --git a/tests/unit_tests/test_r2s.py b/tests/unit_tests/test_r2s.py index a94f85c8c..266bd4976 100644 --- a/tests/unit_tests/test_r2s.py +++ b/tests/unit_tests/test_r2s.py @@ -6,7 +6,7 @@ from openmc.deplete import Chain, R2SManager @pytest.fixture -def simple_model_and_mesh(tmp_path): +def simple_model_and_mesh(): # Define two materials: water and Ni h2o = openmc.Material() h2o.add_nuclide("H1", 2.0) @@ -68,7 +68,7 @@ def test_r2s_mesh_expected_output(simple_model_and_mesh, tmp_path): nt = Path(outdir) / 'neutron_transport' assert (nt / 'fluxes.npy').exists() assert (nt / 'micros.h5').exists() - assert (nt / 'mesh_material_volumes.npz').exists() + assert (nt / 'mesh_material_volumes_0.npz').exists() act = Path(outdir) / 'activation' assert (act / 'depletion_results.h5').exists() pt = Path(outdir) / 'photon_transport' @@ -78,7 +78,8 @@ def test_r2s_mesh_expected_output(simple_model_and_mesh, tmp_path): # Basic results structure checks assert len(r2s.results['fluxes']) == 2 assert len(r2s.results['micros']) == 2 - assert len(r2s.results['mesh_material_volumes']) == 2 + assert len(r2s.results['mesh_material_volumes']) == 1 + assert len(r2s.results['mesh_material_volumes'][0]) == 2 assert len(r2s.results['activation_materials']) == 2 assert len(r2s.results['depletion_results']) == 2 @@ -93,11 +94,77 @@ def test_r2s_mesh_expected_output(simple_model_and_mesh, tmp_path): r2s_loaded.load_results(outdir) assert len(r2s_loaded.results['fluxes']) == 2 assert len(r2s_loaded.results['micros']) == 2 - assert len(r2s_loaded.results['mesh_material_volumes']) == 2 + assert len(r2s_loaded.results['mesh_material_volumes']) == 1 + assert len(r2s_loaded.results['mesh_material_volumes'][0]) == 2 assert len(r2s_loaded.results['activation_materials']) == 2 assert len(r2s_loaded.results['depletion_results']) == 2 +def test_r2s_multi_mesh(simple_model_and_mesh, tmp_path): + model, _, _ = simple_model_and_mesh + + # Two 1x1x1 meshes that together cover the full domain, split along y. + # Each mesh element spans the full x range [-10, 10], crossing the x=0 + # material boundary, so both meshes contain both materials within their + # single element. + mesh1 = openmc.RegularMesh() + mesh1.lower_left = (-10.0, -10.0, -10.0) + mesh1.upper_right = (10.0, 0.0, 10.0) + mesh1.dimension = (1, 1, 1) + mesh2 = openmc.RegularMesh() + mesh2.lower_left = (-10.0, 0.0, -10.0) + mesh2.upper_right = (10.0, 10.0, 10.0) + mesh2.dimension = (1, 1, 1) + + r2s = R2SManager(model, [mesh1, mesh2]) + chain = Chain.from_xml(Path(__file__).parents[1] / "chain_ni.xml") + + outdir = r2s.run( + timesteps=[(1.0, 'd')], + source_rates=[1.0], + photon_time_indices=[1], + output_dir=tmp_path, + chain_file=chain, + ) + + # Check that per-mesh MMV files were written + nt = Path(outdir) / 'neutron_transport' + assert (nt / 'fluxes.npy').exists() + assert (nt / 'micros.h5').exists() + assert (nt / 'mesh_material_volumes_0.npz').exists() + assert (nt / 'mesh_material_volumes_1.npz').exists() + act = Path(outdir) / 'activation' + assert (act / 'depletion_results.h5').exists() + pt = Path(outdir) / 'photon_transport' + assert (pt / 'tally_ids.json').exists() + assert (pt / 'time_1' / 'statepoint.10.h5').exists() + + # Two meshes, each with 1 element containing both materials → + # 2 element-material combinations per mesh, 4 total + assert len(r2s.results['mesh_material_volumes']) == 2 + assert len(r2s.results['mesh_material_volumes'][0]) == 2 + assert len(r2s.results['mesh_material_volumes'][1]) == 2 + assert len(r2s.results['fluxes']) == 4 + assert len(r2s.results['micros']) == 4 + assert len(r2s.results['activation_materials']) == 4 + assert len(r2s.results['depletion_results']) == 2 + + # Activation material names encode mesh index + amats = r2s.results['activation_materials'] + assert all(m.depletable for m in amats) + assert any('Mesh 0' in m.name for m in amats) + assert any('Mesh 1' in m.name for m in amats) + + # Check loading results + r2s_loaded = R2SManager(model, [mesh1, mesh2]) + r2s_loaded.load_results(outdir) + assert len(r2s_loaded.results['mesh_material_volumes']) == 2 + assert len(r2s_loaded.results['mesh_material_volumes'][0]) == 2 + assert len(r2s_loaded.results['mesh_material_volumes'][1]) == 2 + assert len(r2s_loaded.results['activation_materials']) == 4 + assert len(r2s_loaded.results['depletion_results']) == 2 + + def test_r2s_cell_expected_output(simple_model_and_mesh, tmp_path): model, (c1, c2), _ = simple_model_and_mesh From 368ea069ca6ab1bdc6145855223d5c6b4dc6aa47 Mon Sep 17 00:00:00 2001 From: Jack Fletcher <115663563+j-fletcher@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:10:03 -0400 Subject: [PATCH 617/671] Local adjoint source for Random Ray (#3717) --- docs/source/io_formats/settings.rst | 39 +- docs/source/methods/random_ray.rst | 46 +- docs/source/methods/variance_reduction.rst | 16 +- docs/source/usersguide/random_ray.rst | 55 +- docs/source/usersguide/variance_reduction.rst | 94 ++- .../openmc/random_ray/flat_source_domain.h | 8 +- .../openmc/random_ray/random_ray_simulation.h | 17 +- include/openmc/source.h | 1 + include/openmc/weight_windows.h | 3 + openmc/examples.py | 309 ++++++++ openmc/model/model.py | 44 ++ openmc/settings.py | 39 +- openmc/tallies.py | 91 ++- openmc/weight_windows.py | 57 +- src/random_ray/flat_source_domain.cpp | 101 ++- src/random_ray/random_ray_simulation.cpp | 217 +++--- src/settings.cpp | 22 +- src/source.cpp | 3 + src/weight_windows.cpp | 9 +- .../inputs_true.dat | 12 +- .../random_ray_adjoint_k_eff/inputs_true.dat | 12 +- .../random_ray_adjoint_local/__init__.py | 0 .../random_ray_adjoint_local/inputs_true.dat | 293 ++++++++ .../random_ray_adjoint_local/results_true.dat | 15 + .../random_ray_adjoint_local/test.py | 35 + .../infinite_medium/inputs_true.dat | 12 +- .../material_wise/inputs_true.dat | 12 +- .../stochastic_slab/inputs_true.dat | 12 +- .../infinite_medium/inputs_true.dat | 12 +- .../material_wise/inputs_true.dat | 12 +- .../stochastic_slab/inputs_true.dat | 12 +- .../infinite_medium/model/inputs_true.dat | 12 +- .../infinite_medium/user/inputs_true.dat | 12 +- .../stochastic_slab/model/inputs_true.dat | 12 +- .../stochastic_slab/user/inputs_true.dat | 12 +- .../infinite_medium/inputs_true.dat | 12 +- .../material_wise/inputs_true.dat | 12 +- .../stochastic_slab/inputs_true.dat | 12 +- .../eigen/inputs_true.dat | 12 +- .../fs/inputs_true.dat | 12 +- .../inputs_true.dat | 12 +- .../inputs_true.dat | 12 +- .../random_ray_entropy/settings.xml | 12 +- .../cell/inputs_true.dat | 12 +- .../material/inputs_true.dat | 12 +- .../universe/inputs_true.dat | 12 +- .../linear/inputs_true.dat | 12 +- .../linear_xy/inputs_true.dat | 12 +- .../flat/inputs_true.dat | 12 +- .../linear/inputs_true.dat | 12 +- .../False/inputs_true.dat | 12 +- .../True/inputs_true.dat | 12 +- .../flat/inputs_true.dat | 12 +- .../linear_xy/inputs_true.dat | 12 +- .../random_ray_halton_samples/inputs_true.dat | 12 +- .../random_ray_k_eff/inputs_true.dat | 12 +- .../random_ray_k_eff_mesh/inputs_true.dat | 12 +- .../random_ray_linear/linear/inputs_true.dat | 12 +- .../linear_xy/inputs_true.dat | 12 +- .../random_ray_low_density/inputs_true.dat | 12 +- .../inputs_true.dat | 12 +- .../random_ray_s2/inputs_true.dat | 12 +- .../random_ray_void/flat/inputs_true.dat | 12 +- .../random_ray_void/linear/inputs_true.dat | 12 +- .../hybrid/inputs_true.dat | 12 +- .../naive/inputs_true.dat | 12 +- .../simulation_averaged/inputs_true.dat | 12 +- .../hybrid/inputs_true.dat | 12 +- .../naive/inputs_true.dat | 12 +- .../simulation_averaged/inputs_true.dat | 12 +- .../weightwindows_fw_cadis/inputs_true.dat | 12 +- .../weightwindows_fw_cadis_local/__init__.py | 0 .../inputs_true.dat | 273 +++++++ .../results_true.dat | 696 ++++++++++++++++++ .../weightwindows_fw_cadis_local/test.py | 42 ++ .../flat/inputs_true.dat | 12 +- .../linear/inputs_true.dat | 12 +- 77 files changed, 2663 insertions(+), 462 deletions(-) create mode 100644 tests/regression_tests/random_ray_adjoint_local/__init__.py create mode 100644 tests/regression_tests/random_ray_adjoint_local/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_adjoint_local/results_true.dat create mode 100644 tests/regression_tests/random_ray_adjoint_local/test.py create mode 100644 tests/regression_tests/weightwindows_fw_cadis_local/__init__.py create mode 100644 tests/regression_tests/weightwindows_fw_cadis_local/inputs_true.dat create mode 100644 tests/regression_tests/weightwindows_fw_cadis_local/results_true.dat create mode 100644 tests/regression_tests/weightwindows_fw_cadis_local/test.py diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index a50922b04..d63376e2b 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -597,7 +597,7 @@ found in the :ref:`random ray user guide `. *Default*: None - :source: + :ray_source: Specifies the starting ray distribution, and follows the format for :ref:`source_element`. It must be uniform in space and angle and cover the full domain. It does not represent a physical neutron or photon source -- it @@ -605,6 +605,35 @@ found in the :ref:`random ray user guide `. *Default*: None + :adjoint_source: + Specifies an adjoint fixed source for adjoint transport simulations, and + follows the format for :ref:`source_element`. The distributions which make + up the adjoint source are subject to the same restrictions as forward + fixed sources in Random Ray mode. + + *Default*: None + + :adjoint: + Specifies whether to perform adjoint transport. The default is 'False', + corresponding to forward transport. + + *Default*: None + + :volume_estimator: + Specifies choice of volume estimator for the random ray solver. Options + are 'naive', 'simulation_averaged', or 'hybrid'. The default is 'hybrid'. + + *Default*: None + + :volume_normalized_flux_tallies: + Specifies whether to normalize flux tallies by volume (bool). The + default is 'False'. When enabled, flux tallies will be reported in units + of cm/cm^3. When disabled, flux tallies will be reported in units of cm + (i.e., total distance traveled by neutrons in the spatial tally + region). + + *Default*: None + :sample_method: Specifies the method for sampling the starting ray distribution. This element can be set to "prng" or "halton". @@ -1696,6 +1725,14 @@ mesh-based weight windows. The ratio of the lower to upper weight window bounds. *Default*: 5.0 + + For FW-CADIS: + + :targets: + A sequence of IDs corresponding to the tallies which cover phase + space regions of interest for local variance reduction. + + *Default*: None --------------------------------------- ```` Element diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index 5e17316aa..8bc2a0a1b 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -1081,28 +1081,32 @@ lifetimes. In OpenMC, the random ray adjoint solver is implemented simply by transposing the scattering matrix, swapping :math:`\nu\Sigma_f` and :math:`\chi`, and then -running a normal transport solve. When no external fixed source is present, no -additional changes are needed in the transport process. However, if an external -fixed forward source is present in the simulation problem, then an additional -step is taken to compute the accompanying fixed adjoint source. In OpenMC, the -adjoint flux does *not* represent a response function for a particular detector -region. Rather, the adjoint flux is the global response, making it appropriate -for use with weight window generation schemes for global variance reduction. -Thus, if using a fixed source, the external source for the adjoint mode is -simply computed as being :math:`1 / \phi`, where :math:`\phi` is the forward -scalar flux that results from a normal forward solve (which OpenMC will run -first automatically when in adjoint mode). The adjoint external source will be -computed for each source region in the simulation mesh, independent of any -tallies. The adjoint external source is always flat, even when a linear -scattering and fission source shape is used. When in adjoint mode, all reported -results (e.g., tallies, eigenvalues, etc.) are derived from the adjoint flux, -even when the physical meaning is not necessarily obvious. These values are -still reported, though we emphasize that the primary use case for adjoint mode -is for producing adjoint flux tallies to support subsequent perturbation studies -and weight window generation. +running a normal transport solve. When no external fixed forward source is +present, or if an adjoint fixed source is specifically provided, no additional +changes are needed in the transport process. This adjoint source can +correspond, for example, to a detector response function in a particular +region. However, if an external fixed forward source is present in the +simulation problem without an adjoint fixed source, an additional step is taken +to compute the accompanying forward-weighted adjoint source. In this case, the +adjoint flux does *not* represent the importance of locations in phase space to +detector response; rather, the "response" in question is a uniform distribution +of Monte Carlo particle density, making the importance provided by the adjoint +flux appropriate for use with weight window generation schemes for global +variance reduction. Thus, if using a fixed source, the forward-weighted +external source for adjoint mode is simply computed as being :math:`1 / \phi`, +where :math:`\phi` is the forward scalar flux that results from a normal +forward solve (which OpenMC will run first automatically when in adjoint mode). +The adjoint external source will be computed for each source region in the +simulation mesh, independent of any tallies. The adjoint external source is +always flat, even when a linear scattering and fission source shape is used. -Note that the adjoint :math:`k_{eff}` is statistically the same as the forward -:math:`k_{eff}`, despite the flux distributions taking different shapes. +When in adjoint mode, all reported results (e.g., tallies, eigenvalues, etc.) +are derived from the adjoint flux, even when the physical meaning is not +necessarily obvious. These values are still reported, though we emphasize that +the primary use case for adjoint mode is for producing adjoint flux tallies to +support subsequent perturbation studies and weight window generation. Note +however that the adjoint :math:`k_{eff}` is statistically the same as the +forward :math:`k_{eff}`, despite the flux distributions taking different shapes. --------------------------- Fundamental Sources of Bias diff --git a/docs/source/methods/variance_reduction.rst b/docs/source/methods/variance_reduction.rst index cdda5ea92..7778e0714 100644 --- a/docs/source/methods/variance_reduction.rst +++ b/docs/source/methods/variance_reduction.rst @@ -82,8 +82,8 @@ where it was born from. The Forward-Weighted Consistent Adjoint Driven Importance Sampling method, or `FW-CADIS method `_, produces weight windows -for global variance reduction given adjoint flux information throughout the -entire domain. The weight window lower bound is defined in Equation +for global or local variance reduction given adjoint flux information throughout +the entire domain. The weight window lower bound is defined in Equation :eq:`fw_cadis`, and also involves a normalization step not shown here. .. math:: @@ -135,6 +135,18 @@ aware of this. \text{FOM} = \frac{1}{\text{Time} \times \sigma^2} +Finally, one unique capability of the FW-CADIS weight window generator is to +produce weight windows for local variance reduction, given a list of the +responses of interest. This is controlled by optionally specifying target +tallies from the :class:`openmc.model.Model` to the +:class:`openmc.WeightWindowGenerator`, as illustrated in the +:ref:`user guide`. If target tallies for local variance +reduction are supplied, then the adjoint sources are only populated after the +initial forward simulation in the source regions associated with those tallies. +In other regions, the adjoint source term is instead set to zero. The Random +Ray solver then determines the adjoint flux map used to generate FW-CADIS +weight windows following the usual technique. + .. _methods_source_biasing: -------------- diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index 0c9a04028..d35aff83b 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -944,6 +944,8 @@ as:: which will greatly improve the quality of the linear source term in 2D simulations. +.. _usersguide_random_ray_run_modes: + --------------------------------- Fixed Source and Eigenvalue Modes --------------------------------- @@ -1073,22 +1075,47 @@ The adjoint flux random ray solver mode can be enabled as:: settings.random_ray['adjoint'] = True -When enabled, OpenMC will first run a forward transport simulation followed by -an adjoint transport simulation. The purpose of the forward solve is to compute -the adjoint external source when an external source is present in the -simulation. Simulation settings (e.g., number of rays, batches, etc.) will be -identical for both simulations. At the conclusion of the run, all results (e.g., -tallies, plots, etc.) will be derived from the adjoint flux rather than the -forward flux but are not labeled any differently. The initial forward flux -solution will not be stored or available in the final statepoint file. Those -wishing to do analysis requiring both the forward and adjoint solutions will -need to run two separate simulations and load both statepoint files. +When enabled, OpenMC will first run a forward transport simulation if there are +no user-specified adjoint sources present, followed by an adjoint transport +simulation. Fixed adjoint sources can be specified on the +:attr:`openmc.Settings.random_ray` dictionary as follows:: + + # Geometry definition + ... + detector_cell = openmc.Cell(fill=detector_mat, name='cell where detector will be') + ... + # Define fixed adjoint neutron source + strengths = [1.0] + midpoints = [1.0e-4] + energy_distribution = openmc.stats.Discrete(x=midpoints, p=strengths) + + adj_source = openmc.IndependentSource( + energy=energy_distribution, + constraints={'domains': [detector_cell]} + ) + + # Add to random_ray dict + settings.random_ray['adjoint_source'] = adj_source + +The same constraints apply to the user-defined adjoint source as to the forward +source, described in the :ref:`Fixed Source and Eigenvalue section +`. If this source is not provided, a forward +solve must take place to compute the adjoint external source when a forward +external source is present in the problem. Simulation settings (e.g., number of +rays, batches, etc.) will be identical for both calculations. At the +conclusion of the run, all results (e.g., tallies, plots, etc.) will be +derived from the adjoint flux rather than the forward flux but are not labeled +any differently. The initial forward flux solution will not be stored or +available in the final statepoint file. Those wishing to do analysis requiring +both the forward and adjoint solutions will need to run two separate +simulations and load both statepoint files. .. note:: - When adjoint mode is selected, OpenMC will always perform a full forward - solve and then run a full adjoint solve immediately afterwards. Statepoint - and tally results will be derived from the adjoint flux, but will not be - labeled any differently. + Use of the automated + :ref:`FW-CADIS weight window generator` is not + currently compatible with user-defined adjoint sources. Instead, the + initial forward calculation is used to assign "forward-weighted" adjoint + sources to the tally regions of interest. --------------------------------------- Putting it All Together: Example Inputs diff --git a/docs/source/usersguide/variance_reduction.rst b/docs/source/usersguide/variance_reduction.rst index 8d41807e1..d551195f5 100644 --- a/docs/source/usersguide/variance_reduction.rst +++ b/docs/source/usersguide/variance_reduction.rst @@ -4,26 +4,27 @@ Variance Reduction ================== -Global variance reduction in OpenMC is accomplished by weight windowing -or source biasing techniques, the latter of which additionally provides a -local variance reduction capability. OpenMC is capable of generating weight -windows using either the MAGIC or FW-CADIS methods. Both techniques will -produce a ``weight_windows.h5`` file that can be loaded and used later on. In +Global and local variance reduction are possible in OpenMC through both weight +windowing and source biasing techniques. OpenMC is capable of generating weight +windows using either the MAGIC or FW-CADIS methods, the latter with an optional +capability for local variance reduction. Both techniques will produce a +``weight_windows.h5`` file that can be loaded and used later on. In this section, we first break down the steps required to generate and apply weight windows, then describe how source biasing may be applied. .. _ww_generator: ------------------------------------- -Generating Weight Windows with MAGIC ------------------------------------- +------------------------------------------- +Generating Global Weight Windows with MAGIC +------------------------------------------- As discussed in the :ref:`methods section `, MAGIC is an iterative method that uses flux tally information from a Monte Carlo -simulation to produce weight windows for a user-defined mesh. While generating -the weight windows, OpenMC is capable of applying the weight windows generated -from a previous batch while processing the next batch, allowing for progressive -improvement in the weight window quality across iterations. +simulation to produce weight windows for a user-defined mesh with the objective +of global variance reduction. While generating the weight windows, OpenMC is +capable of applying the weight windows generated from a previous batch while +processing the next batch, allowing for progressive improvement in the weight +window quality across iterations. The typical way of generating weight windows is to define a mesh and then add an :class:`openmc.WeightWindowGenerator` object to an :attr:`openmc.Settings` @@ -71,15 +72,20 @@ At the end of the simulation, a ``weight_windows.h5`` file will be saved to disk for later use. Loading it in another subsequent simulation will be discussed in the "Using Weight Windows" section below. ------------------------------------------------------- -Generating Weight Windows with FW-CADIS and Random Ray ------------------------------------------------------- +.. _usersguide_fw_cadis: + +---------------------------------------------------------------------- +Generating Global or Local Weight Windows with FW-CADIS and Random Ray +---------------------------------------------------------------------- Weight window generation with FW-CADIS and random ray in OpenMC uses the same -exact strategy as with MAGIC. An :class:`openmc.WeightWindowGenerator` object is -added to the :attr:`openmc.Settings` object, and a ``weight_windows.h5`` will be -generated at the end of the simulation. The only difference is that the code -must be run in random ray mode. A full description of how to enable and setup +exact strategy as with MAGIC. Using FW-CADIS, however, also enables +local variance reduction in fixed source problems through the :attr:`targets` +attribute, which is described later in this section. To enable FW-CADIS, an +:class:`openmc.WeightWindowGenerator` object is added to the +:attr:`openmc.Settings` object, and a ``weight_windows.h5`` will be generated +at the end of the simulation. The only procedural difference is that the code +must be run in random ray mode. A full description of how to enable and setup random ray mode can be found in the :ref:`Random Ray User Guide `. .. note:: @@ -90,7 +96,7 @@ random ray mode can be found in the :ref:`Random Ray User Guide `. ray solver. A high level overview of the current workflow for generation of weight windows with FW-CADIS using random ray is given below. -1. Begin by making a deepy copy of your continuous energy Python model and then +1. Begin by making a deep copy of your continuous energy Python model and then convert the copy to be multigroup and use the random ray transport solver. The conversion process can largely be automated as described in more detail in the :ref:`random ray quick start guide `, summarized below:: @@ -148,7 +154,53 @@ random ray mode can be found in the :ref:`Random Ray User Guide `. assigning to ``model.settings.random_ray['source_region_meshes']``) and for weight window generation. -3. When running your multigroup random ray input deck, OpenMC will automatically +3. (Optional) If local variance reduction is desired in a fixed-source problem, + populate the :attr:`targets` attribute with an :class:`openmc.Tallies` + instance or an iterable of tally IDs indicating the tallies of interest for + variance reduction:: + + # Build a new example and WWG for local variance reduction + from openmc.examples import random_ray_three_region_cube_with_detectors + new_model = random_ray_three_region_cube_with_detectors() + + ww_mesh = openmc.RegularMesh() + n = 7 + width = 35.0 + ww_mesh.dimension = (n, n, n) + ww_mesh.lower_left = (0.0, 0.0, 0.0) + ww_mesh.upper_right = (width, width, width) + + wwg = openmc.WeightWindowGenerator( + method="fw_cadis", + mesh=ww_mesh, + max_realizations=new_model.settings.batches + ) + new_model.settings.weight_window_generators = wwg + new_model.settings.random_ray['volume_estimator'] = 'naive' + + # Get the tallies of interest + target_tallies = openmc.Tallies() + + for tally in list(new_model.tallies): + if tally.name in {"Detector 1 Tally", "Detector 2 Tally"}: + target_tallies.append(tally) + + # Add to WeightWindowGenerator + wwg.targets = target_tallies + +.. warning:: + The tallies designated as FW-CADIS targets to the + :class:`~openmc.WeightWindowGenerator` must be present under the + :class:`~openmc.model.Model.tallies` attribute of the + :class:`~openmc.model.Model` as well in order to be recognized as valid + local variance reduction targets. This check is performed when the + :func:`openmc.model.Model.export_to_model_xml` or + :func:`openmc.model.Model.export_to_xml` functions are called, meaning + that the standalone :func:`openmc.Settings.export_to_xml` and + :func:`openmc.Tallies.export_to_xml` methods should not be used with + FW-CADIS local variance reduction. + +4. When running your multigroup random ray input deck, OpenMC will automatically run a forward solve followed by an adjoint solve, with a ``weight_windows.h5`` file generated at the end. The ``weight_windows.h5`` file will contain FW-CADIS generated weight windows. This file can be used in diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index c40982712..6f51af34d 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -40,9 +40,10 @@ public: void random_ray_tally(); virtual void accumulate_iteration_flux(); void output_to_vtk() const; - void convert_external_sources(); + void convert_external_sources(bool use_adjoint_sources); void count_external_source_regions(); - void set_adjoint_sources(); + void set_fw_adjoint_sources(); + void set_local_adjoint_sources(); void flux_swap(); virtual double evaluate_flux_at_point(Position r, int64_t sr, int g) const; double compute_fixed_source_normalization_factor() const; @@ -76,6 +77,7 @@ public: // Static Data members static bool volume_normalized_flux_tallies_; static bool adjoint_; // If the user wants outputs based on the adjoint flux + static bool fw_cadis_local_; static double diagonal_stabilization_rho_; // Adjusts strength of diagonal stabilization // for transport corrected MGXS data @@ -84,6 +86,8 @@ public: static std::unordered_map>> mesh_domain_map_; + static std::vector fw_cadis_local_targets_; + //---------------------------------------------------------------------------- // Static data members static RandomRayVolumeEstimator volume_estimator_; diff --git a/include/openmc/random_ray/random_ray_simulation.h b/include/openmc/random_ray/random_ray_simulation.h index 68c7779ed..ccd2cbe47 100644 --- a/include/openmc/random_ray/random_ray_simulation.h +++ b/include/openmc/random_ray/random_ray_simulation.h @@ -20,8 +20,9 @@ public: //---------------------------------------------------------------------------- // Methods void apply_fixed_sources_and_mesh_domains(); - void prepare_fixed_sources_adjoint(); - void prepare_adjoint_simulation(); + void prepare_fw_fixed_sources_adjoint(); + void prepare_local_fixed_sources_adjoint(); + void prepare_adjoint_simulation(bool fw_adjoint); void simulate(); void output_simulation_results() const; void instability_check( @@ -34,15 +35,9 @@ public: // Accessors FlatSourceDomain* domain() const { return domain_.get(); } - //---------------------------------------------------------------------------- - // Public data members - - // Flag for adjoint simulation; - bool adjoint_needed_; - private: //---------------------------------------------------------------------------- - // Private data members + // Data members // Contains all flat source region data unique_ptr domain_; @@ -57,9 +52,6 @@ private: // Number of energy groups int negroups_; - // Toggle for first simulation - bool is_first_simulation_; - }; // class RandomRaySimulation //============================================================================ @@ -67,7 +59,6 @@ private: //============================================================================ void validate_random_ray_inputs(); -void print_adjoint_header(); void openmc_finalize_random_ray(); } // namespace openmc diff --git a/include/openmc/source.h b/include/openmc/source.h index 1ef7eba2b..e307b1ed2 100644 --- a/include/openmc/source.h +++ b/include/openmc/source.h @@ -43,6 +43,7 @@ class Source; namespace model { extern vector> external_sources; +extern vector> adjoint_sources; // Probability distribution for selecting external sources extern DiscreteIndex external_sources_probability; diff --git a/include/openmc/weight_windows.h b/include/openmc/weight_windows.h index 42846f9d1..a5d404133 100644 --- a/include/openmc/weight_windows.h +++ b/include/openmc/weight_windows.h @@ -223,6 +223,9 @@ public: double threshold_ {1.0}; // targets_; }; //============================================================================== diff --git a/openmc/examples.py b/openmc/examples.py index f7f2bd48d..90a0bffe7 100644 --- a/openmc/examples.py +++ b/openmc/examples.py @@ -1310,3 +1310,312 @@ def random_ray_three_region_cube() -> openmc.Model: model.tallies = tallies return model + +def random_ray_three_region_cube_with_detectors() -> openmc.Model: + """Create a three region cube model with two external tally regions. + + This is an adaptation of the simple monoenergetic problem of a cube with + three concentric cubic regions. The innermost region is near void (with + Sigma_t around 10^-5) and contains an external isotropic source term, the + middle region is a mild scatterer (with Sigma_t around 10^-3), and the + outer region of the cube is a scatterer and absorber (with Sigma_t around + 1). + + Two cubic "detector" regions are found outside this geometry, one along the + y-axis near z=0, and the other in the upper right corner of the system. + The size of each detector is scaled to be equal to that of the source + region. The model returned by this function contains cell tallies on each + detector. + + Returns + ------- + model : openmc.Model + A three region cube model + + """ + + model = openmc.Model() + + ########################################################################### + # Helper function creates a 3 region cube with different fills in each region + def fill_cube(N, n_1, n_2, fill_1, fill_2, fill_3): + cube = [[[0 for _ in range(N)] for _ in range(N)] for _ in range(N)] + for i in range(N): + for j in range(N): + for k in range(N): + if i < n_1 and j >= (N-n_1) and k < n_1: + cube[i][j][k] = fill_1 + elif i < n_2 and j >= (N-n_2) and k < n_2: + cube[i][j][k] = fill_2 + else: + cube[i][j][k] = fill_3 + return cube + + ########################################################################### + # Create multigroup data + + # Instantiate the energy group data + ebins = [1e-5, 20.0e6] + groups = openmc.mgxs.EnergyGroups(group_edges=ebins) + + cavity_sigma_a = 4.0e-5 + cavity_sigma_s = 3.0e-3 + cavity_mat_data = openmc.XSdata('cavity', groups) + cavity_mat_data.order = 0 + cavity_mat_data.set_total([cavity_sigma_a + cavity_sigma_s]) + cavity_mat_data.set_absorption([cavity_sigma_a]) + cavity_mat_data.set_scatter_matrix( + np.rollaxis(np.array([[[cavity_sigma_s]]]), 0, 3)) + + absorber_sigma_a = 0.50 + absorber_sigma_s = 0.50 + absorber_mat_data = openmc.XSdata('absorber', groups) + absorber_mat_data.order = 0 + absorber_mat_data.set_total([absorber_sigma_a + absorber_sigma_s]) + absorber_mat_data.set_absorption([absorber_sigma_a]) + absorber_mat_data.set_scatter_matrix( + np.rollaxis(np.array([[[absorber_sigma_s]]]), 0, 3)) + + multiplier = 0.01 + source_sigma_a = cavity_sigma_a * multiplier + source_sigma_s = cavity_sigma_s * multiplier + source_mat_data = openmc.XSdata('source', groups) + source_mat_data.order = 0 + source_mat_data.set_total([source_sigma_a + source_sigma_s]) + source_mat_data.set_absorption([source_sigma_a]) + source_mat_data.set_scatter_matrix( + np.rollaxis(np.array([[[source_sigma_s]]]), 0, 3)) + + mg_cross_sections_file = openmc.MGXSLibrary(groups) + mg_cross_sections_file.add_xsdatas( + [source_mat_data, cavity_mat_data, absorber_mat_data]) + mg_cross_sections_file.export_to_hdf5() + + ########################################################################### + # Create materials for the problem + + # Instantiate some Macroscopic Data + source_data = openmc.Macroscopic('source') + cavity_data = openmc.Macroscopic('cavity') + absorber_data = openmc.Macroscopic('absorber') + + # Instantiate some Materials and register the appropriate Macroscopic objects + source_mat = openmc.Material(name='source') + source_mat.set_density('macro', 1.0) + source_mat.add_macroscopic(source_data) + + cavity_mat = openmc.Material(name='cavity') + cavity_mat.set_density('macro', 1.0) + cavity_mat.add_macroscopic(cavity_data) + + absorber_mat = openmc.Material(name='absorber') + absorber_mat.set_density('macro', 1.0) + absorber_mat.add_macroscopic(absorber_data) + + # Instantiate a Materials collection + materials_file = openmc.Materials([source_mat, cavity_mat, absorber_mat]) + materials_file.cross_sections = "mgxs.h5" + + ########################################################################### + # Define problem geometry + + source_cell = openmc.Cell(fill=source_mat, name='infinite source region') + cavity_cell = openmc.Cell(fill=cavity_mat, name='cube cavity region') + absorber_cell = openmc.Cell( + fill=absorber_mat, name='absorber region') + + source_universe = openmc.Universe(name='source universe') + source_universe.add_cells([source_cell]) + + cavity_universe = openmc.Universe() + cavity_universe.add_cells([cavity_cell]) + + absorber_universe = openmc.Universe() + absorber_universe.add_cells([absorber_cell]) + + absorber_width = 30.0 + n_base = 6 + + # This variable can be increased above 1 to refine the FSR mesh resolution further + refinement_level = 2 + + n = n_base * refinement_level + pitch = absorber_width / n + + pattern = fill_cube(n, 1*refinement_level, 5*refinement_level, + source_universe, cavity_universe, absorber_universe) + + lattice = openmc.RectLattice() + lattice.lower_left = [0.0, 0.0, 0.0] + lattice.pitch = [pitch, pitch, pitch] + lattice.universes = pattern + + lattice_cell = openmc.Cell(fill=lattice) + + lattice_uni = openmc.Universe() + lattice_uni.add_cells([lattice_cell]) + + x_low = openmc.XPlane(x0=0.0, boundary_type='reflective') + x_high = openmc.XPlane(x0=absorber_width) + + y_low = openmc.YPlane(y0=0.0, boundary_type='reflective') + y_high = openmc.YPlane(y0=absorber_width) + + z_low = openmc.ZPlane(z0=0.0, boundary_type='reflective') + z_high = openmc.ZPlane(z0=absorber_width) + + cube_domain = openmc.Cell(fill=lattice_uni, region=+x_low & - + x_high & +y_low & -y_high & +z_low & -z_high, name='full domain') + + detect_width = absorber_width / n_base + outer_width = absorber_width + detect_width + + x_outer = openmc.XPlane(x0=outer_width, boundary_type='vacuum') + y_outer = openmc.YPlane(y0=outer_width, boundary_type='vacuum') + z_outer = openmc.ZPlane(z0=outer_width, boundary_type='vacuum') + + detector1_right = openmc.XPlane(x0=detect_width) + detector1_top = openmc.ZPlane(z0=detect_width) + + detector1_region = ( + +x_low & -detector1_right & + +y_high & -y_outer & + +z_low & -detector1_top + ) + detector1 = openmc.Cell( + name='detector 1', + fill=absorber_mat, + region=detector1_region + ) + + detector2_region = ( + +x_high & -x_outer & + +y_high & -y_outer & + +z_high & -z_outer + ) + detector2 = openmc.Cell( + name='detector 2', + fill=absorber_mat, + region=detector2_region + ) + + external_x = ( + +x_high & +y_low & +z_low & -x_outer & + ((-y_outer & -z_high) | (-y_high & +z_high & -z_outer)) + ) + external_y = ( + +y_high & -y_outer & + ( + (+detector1_right & -x_high & +z_low & -z_outer) | + (-detector1_right & +x_low & +detector1_top & -z_outer) | + (+x_high & -x_outer & +z_low & -z_high) + ) + ) + external_z = ( + +x_low & +y_low & +z_high & -z_outer & + ((-y_outer & -x_high) | (-y_high & +x_high & -x_outer)) + ) + external_cell = openmc.Cell(fill=cavity_mat, + region=(external_x | external_y | external_z), + name='outside cube') + + root = openmc.Universe( + name='root universe', + cells=[cube_domain, detector1, detector2, external_cell] + ) + + # Create a geometry with the two cells and export to XML + geometry = openmc.Geometry(root) + + ########################################################################### + # Define problem settings + + # Instantiate a Settings object, set all runtime parameters, and export to XML + settings = openmc.Settings() + settings.energy_mode = "multi-group" + settings.inactive = 5 + settings.batches = 10 + settings.particles = 500 + settings.run_mode = 'fixed source' + + # Create an initial uniform spatial source for ray integration + lower_left_ray = [0.0, 0.0, 0.0] + upper_right_ray = [outer_width, outer_width, outer_width] + uniform_dist_ray = openmc.stats.Box( + lower_left_ray, upper_right_ray, only_fissionable=False) + rr_source = openmc.IndependentSource(space=uniform_dist_ray) + + settings.random_ray['distance_active'] = 800.0 + settings.random_ray['distance_inactive'] = 100.0 + settings.random_ray['ray_source'] = rr_source + settings.random_ray['volume_normalized_flux_tallies'] = True + + # Create a rectilinear source region mesh + sr_mesh = openmc.RegularMesh() + sr_mesh.dimension = (14, 14, 14) + sr_mesh.lower_left = (0.0, 0.0, 0.0) + sr_mesh.upper_right = (outer_width, outer_width, outer_width) + settings.random_ray['source_region_meshes'] = [(sr_mesh, [root])] + + # Create the neutron source in the bottom right of the moderator + # Good - fast group appears largest (besides most thermal) + strengths = [1.0] + midpoints = [100.0] + energy_distribution = openmc.stats.Discrete(x=midpoints, p=strengths) + + source = openmc.IndependentSource(energy=energy_distribution, constraints={ + 'domains': [source_universe]}, strength=3.14) + + settings.source = [source] + + ########################################################################### + # Define tallies + + estimator = 'tracklength' + + detector1_filter = openmc.CellFilter(detector1) + detector1_tally = openmc.Tally(name="Detector 1 Tally") + detector1_tally.filters = [detector1_filter] + detector1_tally.scores = ['flux'] + detector1_tally.estimator = estimator + + detector2_filter = openmc.CellFilter(detector2) + detector2_tally = openmc.Tally(name="Detector 2 Tally") + detector2_tally.filters = [detector2_filter] + detector2_tally.scores = ['flux'] + detector2_tally.estimator = estimator + + absorber_filter = openmc.MaterialFilter(absorber_mat) + absorber_tally = openmc.Tally(name="Absorber Tally") + absorber_tally.filters = [absorber_filter] + absorber_tally.scores = ['flux'] + absorber_tally.estimator = estimator + + cavity_filter = openmc.MaterialFilter(cavity_mat) + cavity_tally = openmc.Tally(name="Cavity Tally") + cavity_tally.filters = [cavity_filter] + cavity_tally.scores = ['flux'] + cavity_tally.estimator = estimator + + source_filter = openmc.MaterialFilter(source_mat) + source_tally = openmc.Tally(name="Source Tally") + source_tally.filters = [source_filter] + source_tally.scores = ['flux'] + source_tally.estimator = estimator + + # Instantiate a Tallies collection and export to XML + tallies = openmc.Tallies([detector1_tally, + detector2_tally, + absorber_tally, + cavity_tally, + source_tally]) + + ########################################################################### + # Assmble Model + + model.geometry = geometry + model.materials = materials_file + model.settings = settings + model.tallies = tallies + + return model \ No newline at end of file diff --git a/openmc/model/model.py b/openmc/model/model.py index 884e3ff6f..c9a24b8b3 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -265,6 +265,46 @@ class Model: denom_tally = openmc.Tally(name='IFP denominator') denom_tally.scores = ['ifp-denominator'] self.tallies.append(denom_tally) + + # TODO: This should also be incorporated into lower-level calls in + # settings.py, but it requires information about the tallies currently + # on the active Model + def _assign_fw_cadis_tally_IDs(self): + # Verify that all tallies assigned as targets on WeightWindowGenerators + # exist within model.tallies. If this is the case, convert the .targets + # attribute of each WeightWindowGenerator to a sequence of tally IDs. + if len(self.settings.weight_window_generators) == 0: + return + + # List of valid tally IDs + reference_tally_ids = np.asarray([tal.id for tal in self.tallies]) + + for wwg in self.settings.weight_window_generators: + # Only proceeds if the "targets" attribute is an openmc.Tallies, + # which means it hasn't been checked against model.tallies. + if isinstance(wwg.targets, openmc.Tallies): + id_vec = [] + for tal in wwg.targets: + # check against model tallies for equivalence + id_next = None + for reference_tal in self.tallies: + if tal == reference_tal: + id_next = reference_tal.id + break + + if id_next == None: + raise RuntimeError( + f'Local FW-CADIS target tally {tal.id} not found on model.tallies!') + else: + id_vec.append(id_next) + + wwg.targets = id_vec + + elif isinstance(wwg.targets, np.ndarray): + invalid = wwg.targets[~np.isin(wwg.targets, reference_tally_ids)] + if len(invalid) > 0: + raise RuntimeError( + f'Local FW-CADIS target tally IDs {invalid} not found on model.tallies!') @classmethod def from_xml( @@ -576,6 +616,7 @@ class Model: if not d.is_dir(): d.mkdir(parents=True, exist_ok=True) + self._assign_fw_cadis_tally_IDs() self.settings.export_to_xml(d) self.geometry.export_to_xml(d, remove_surfs=remove_surfs) @@ -634,6 +675,9 @@ class Model: "set the Geometry.merge_surfaces attribute instead.") self.geometry.merge_surfaces = True + # Link FW-CADIS WeightWindowGenerator target tallies, if present + self._assign_fw_cadis_tally_IDs() + # provide a memo to track which meshes have been written mesh_memo = set() settings_element = self.settings.to_xml_element(mesh_memo) diff --git a/openmc/settings.py b/openmc/settings.py index 6919afca4..1090babda 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -236,6 +236,9 @@ class Settings: stabilization, which may be desirable as stronger diagonal stabilization also tends to dampen the convergence rate of the solver, thus requiring more iterations to converge. + :adjoint_source: + Source object used to define localized adjoint source/detector response + function. .. versionadded:: 0.15.0 resonance_scattering : dict @@ -1421,6 +1424,14 @@ class Settings: cv.check_type('diagonal stabilization rho', value, Real) cv.check_greater_than('diagonal stabilization rho', value, 0.0, True) + elif key == 'adjoint_source': + if not isinstance(value, MutableSequence): + value = [value] + for source in value: + if not isinstance(source, SourceBase): + raise ValueError( + f'Invalid adjoint source type: {type(source)}. ' + 'Expected openmc.SourceBase.') else: raise ValueError(f'Unable to set random ray to "{key}" which is ' 'unsupported by OpenMC') @@ -1973,11 +1984,12 @@ class Settings: element = ET.SubElement(root, "random_ray") for key, value in self._random_ray.items(): if key == 'ray_source' and isinstance(value, SourceBase): + subelement = ET.SubElement(element, 'ray_source') source_element = value.to_xml_element() if source_element.find('bias') is not None: raise RuntimeError( "Ray source distributions should not be biased.") - element.append(source_element) + subelement.append(source_element) elif key == 'source_region_meshes': subelement = ET.SubElement(element, 'source_region_meshes') @@ -1995,8 +2007,20 @@ class Settings: path = f"./mesh[@id='{mesh.id}']" if root.find(path) is None: root.append(mesh.to_xml_element()) - if mesh_memo is not None: + if mesh_memo is not None: mesh_memo.add(mesh.id) + elif key == 'adjoint_source': + subelement = ET.SubElement(element, 'adjoint_source') + # Check that all entries are valid SourceBase instances, in case + # the random_ray setter was not used to populate dict entries. + if not isinstance(value, MutableSequence): + value = [value] + for source in value: + if not isinstance(source, SourceBase): + raise ValueError( + f'Invalid adjoint source type: {type(source)}. ' + 'Expected openmc.SourceBase.') + subelement.append(source.to_xml_element()) elif isinstance(value, bool): subelement = ET.SubElement(element, key) subelement.text = str(value).lower() @@ -2443,8 +2467,9 @@ class Settings: for child in elem: if child.tag in ('distance_inactive', 'distance_active', 'diagonal_stabilization_rho'): self.random_ray[child.tag] = float(child.text) - elif child.tag == 'source': - source = SourceBase.from_xml_element(child) + elif child.tag == 'ray_source': + source_element = child.find('source') + source = SourceBase.from_xml_element(source_element) if child.find('bias') is not None: raise RuntimeError( "Ray source distributions should not be biased.") @@ -2461,6 +2486,12 @@ class Settings: self.random_ray['adjoint'] = ( child.text in ('true', '1') ) + elif child.tag == 'adjoint_source': + self.random_ray['adjoint_source'] = [] + for subelem in child.findall('source'): + src = SourceBase.from_xml_element(subelem) + # add newly constructed source object to the list + self.random_ray['adjoint_source'].append(src) elif child.tag == 'sample_method': self.random_ray['sample_method'] = child.text elif child.tag == 'source_region_meshes': diff --git a/openmc/tallies.py b/openmc/tallies.py index 151add2be..ceced4255 100644 --- a/openmc/tallies.py +++ b/openmc/tallies.py @@ -16,6 +16,23 @@ from scipy.stats import chi2, norm import openmc import openmc.checkvalue as cv +from openmc.filter import ( + Filter, + DistribcellFilter, + EnergyFunctionFilter, + DelayedGroupFilter, + FilterMeta, + MeshFilter, + MeshBornFilter, +) +from openmc.arithmetic import ( + CrossFilter, + AggregateFilter, + CrossScore, + AggregateScore, + CrossNuclide, + AggregateNuclide, +) from ._sparse_compat import lil_array from ._xml import clean_indentation, get_elem_list, get_text from .mixin import IDManagerMixin @@ -31,9 +48,9 @@ _PRODUCT_TYPES = ['tensor', 'entrywise'] # The following indicate acceptable types when setting Tally.scores, # Tally.nuclides, and Tally.filters -_SCORE_CLASSES = (str, openmc.CrossScore, openmc.AggregateScore) -_NUCLIDE_CLASSES = (str, openmc.CrossNuclide, openmc.AggregateNuclide) -_FILTER_CLASSES = (openmc.Filter, openmc.CrossFilter, openmc.AggregateFilter) +_SCORE_CLASSES = (str, CrossScore, AggregateScore) +_NUCLIDE_CLASSES = (str, CrossNuclide, AggregateNuclide) +_FILTER_CLASSES = (Filter, CrossFilter, AggregateFilter) # Valid types of estimators ESTIMATOR_TYPES = {'tracklength', 'collision', 'analog'} @@ -421,7 +438,7 @@ class Tally(IDManagerMixin): self._num_realizations = int(group['n_realizations'][()]) for filt in self.filters: - if isinstance(filt, openmc.DistribcellFilter): + if isinstance(filt, DistribcellFilter): filter_group = f[f'tallies/filters/filter {filt.id}'] filt._num_bins = int(filter_group['n_bins'][()]) @@ -1089,8 +1106,8 @@ class Tally(IDManagerMixin): return False # Return False if only one tally has a delayed group filter - tally1_dg = self.contains_filter(openmc.DelayedGroupFilter) - tally2_dg = other.contains_filter(openmc.DelayedGroupFilter) + tally1_dg = self.contains_filter(DelayedGroupFilter) + tally2_dg = other.contains_filter(DelayedGroupFilter) if tally1_dg != tally2_dg: return False @@ -1602,7 +1619,7 @@ class Tally(IDManagerMixin): # Also check to see if the desired filter is wrapped up in an # aggregate - elif isinstance(test_filter, openmc.AggregateFilter): + elif isinstance(test_filter, AggregateFilter): if isinstance(test_filter.aggregate_filter, filter_type): return test_filter @@ -1704,7 +1721,7 @@ class Tally(IDManagerMixin): """ - cv.check_type('filters', filters, Iterable, openmc.FilterMeta) + cv.check_type('filters', filters, Iterable, FilterMeta) cv.check_type('filter_bins', filter_bins, Iterable, tuple) # If user did not specify any specific Filters, use them all @@ -1787,7 +1804,7 @@ class Tally(IDManagerMixin): """ for score in scores: - if not isinstance(score, (str, openmc.CrossScore)): + if not isinstance(score, (str, CrossScore)): msg = f'Unable to get score indices for score "{score}" in ' \ f'ID="{self.id}" since it is not a string or CrossScore ' \ 'Tally' @@ -1984,9 +2001,9 @@ class Tally(IDManagerMixin): column_name = 'score' for score in self.scores: - if isinstance(score, (str, openmc.CrossScore)): + if isinstance(score, (str, CrossScore)): scores.append(str(score)) - elif isinstance(score, openmc.AggregateScore): + elif isinstance(score, AggregateScore): scores.append(score.name) column_name = f'{score.aggregate_op}(score)' @@ -2086,7 +2103,7 @@ class Tally(IDManagerMixin): for i, f in enumerate(self.filters): if expand_dims: # Mesh filter indices are backwards so we need to flip them - if type(f) in {openmc.MeshFilter, openmc.MeshBornFilter}: + if type(f) in {MeshFilter, MeshBornFilter}: fshape = f.shape[::-1] new_shape += fshape idx0, idx1 = i, i + len(fshape) - 1 @@ -2273,7 +2290,7 @@ class Tally(IDManagerMixin): else: all_filters = [self_copy.filters, other_copy.filters] for self_filter, other_filter in product(*all_filters): - new_filter = openmc.CrossFilter(self_filter, other_filter, + new_filter = CrossFilter(self_filter, other_filter, binary_op) new_tally.filters.append(new_filter) @@ -2284,7 +2301,7 @@ class Tally(IDManagerMixin): else: all_nuclides = [self_copy.nuclides, other_copy.nuclides] for self_nuclide, other_nuclide in product(*all_nuclides): - new_nuclide = openmc.CrossNuclide(self_nuclide, other_nuclide, + new_nuclide = CrossNuclide(self_nuclide, other_nuclide, binary_op) new_tally.nuclides.append(new_nuclide) @@ -2295,9 +2312,9 @@ class Tally(IDManagerMixin): if score1 == score2: return score1 else: - return openmc.CrossScore(score1, score2, binary_op) + return CrossScore(score1, score2, binary_op) else: - return openmc.CrossScore(score1, score2, binary_op) + return CrossScore(score1, score2, binary_op) # Add scores to the new tally if score_product == 'entrywise': @@ -2506,16 +2523,16 @@ class Tally(IDManagerMixin): # Construct lists of tuples for the bins in each of the two filters filters = [type(filter1), type(filter2)] - if isinstance(filter1, openmc.DistribcellFilter): + if isinstance(filter1, DistribcellFilter): filter1_bins = [b for b in range(filter1.num_bins)] - elif isinstance(filter1, openmc.EnergyFunctionFilter): + elif isinstance(filter1, EnergyFunctionFilter): filter1_bins = [None] else: filter1_bins = filter1.bins - if isinstance(filter2, openmc.DistribcellFilter): + if isinstance(filter2, DistribcellFilter): filter2_bins = [b for b in range(filter2.num_bins)] - elif isinstance(filter2, openmc.EnergyFunctionFilter): + elif isinstance(filter2, EnergyFunctionFilter): filter2_bins = [None] else: filter2_bins = filter2.bins @@ -2648,11 +2665,11 @@ class Tally(IDManagerMixin): raise ValueError(msg) # Check that the scores are valid - if not isinstance(score1, (str, openmc.CrossScore)): + if not isinstance(score1, (str, CrossScore)): msg = 'Unable to swap score1 "{}" in Tally ID="{}" since it is ' \ 'not a string or CrossScore'.format(score1, self.id) raise ValueError(msg) - elif not isinstance(score2, (str, openmc.CrossScore)): + elif not isinstance(score2, (str, CrossScore)): msg = 'Unable to swap score2 "{}" in Tally ID="{}" since it is ' \ 'not a string or CrossScore'.format(score2, self.id) raise ValueError(msg) @@ -3296,7 +3313,7 @@ class Tally(IDManagerMixin): new_filter.bins = [f.bins[i] for i in bin_indices] # Set number of bins manually for mesh/distribcell filters - if filter_type is openmc.DistribcellFilter: + if filter_type is DistribcellFilter: new_filter._num_bins = f._num_bins # Replace existing filter with new one @@ -3362,16 +3379,16 @@ class Tally(IDManagerMixin): std_dev = self.get_reshaped_data(value='std_dev') # Sum across any filter bins specified by the user - if isinstance(filter_type, openmc.FilterMeta): + if isinstance(filter_type, FilterMeta): find_filter = self.find_filter(filter_type) # If user did not specify filter bins, sum across all bins if len(filter_bins) == 0: bin_indices = np.arange(find_filter.num_bins) - if isinstance(find_filter, openmc.DistribcellFilter): + if isinstance(find_filter, DistribcellFilter): filter_bins = np.arange(find_filter.num_bins) - elif isinstance(find_filter, openmc.EnergyFunctionFilter): + elif isinstance(find_filter, EnergyFunctionFilter): filter_bins = [None] else: filter_bins = find_filter.bins @@ -3400,7 +3417,7 @@ class Tally(IDManagerMixin): # Add AggregateFilter to the tally sum if not remove_filter: - filter_sum = openmc.AggregateFilter(self_filter, + filter_sum = AggregateFilter(self_filter, [tuple(filter_bins)], 'sum') tally_sum.filters.append(filter_sum) @@ -3423,7 +3440,7 @@ class Tally(IDManagerMixin): std_dev = np.sqrt(std_dev) # Add AggregateNuclide to the tally sum - nuclide_sum = openmc.AggregateNuclide(nuclides, 'sum') + nuclide_sum = AggregateNuclide(nuclides, 'sum') tally_sum.nuclides.append(nuclide_sum) # Add a copy of this tally's nuclides to the tally sum @@ -3441,7 +3458,7 @@ class Tally(IDManagerMixin): std_dev = np.sqrt(std_dev) # Add AggregateScore to the tally sum - score_sum = openmc.AggregateScore(scores, 'sum') + score_sum = AggregateScore(scores, 'sum') tally_sum.scores.append(score_sum) # Add a copy of this tally's scores to the tally sum @@ -3514,16 +3531,16 @@ class Tally(IDManagerMixin): std_dev = self.get_reshaped_data(value='std_dev') # Average across any filter bins specified by the user - if isinstance(filter_type, openmc.FilterMeta): + if isinstance(filter_type, FilterMeta): find_filter = self.find_filter(filter_type) # If user did not specify filter bins, average across all bins if len(filter_bins) == 0: bin_indices = np.arange(find_filter.num_bins) - if isinstance(find_filter, openmc.DistribcellFilter): + if isinstance(find_filter, DistribcellFilter): filter_bins = np.arange(find_filter.num_bins) - elif isinstance(find_filter, openmc.EnergyFunctionFilter): + elif isinstance(find_filter, EnergyFunctionFilter): filter_bins = [None] else: filter_bins = find_filter.bins @@ -3553,7 +3570,7 @@ class Tally(IDManagerMixin): # Add AggregateFilter to the tally avg if not remove_filter: - filter_sum = openmc.AggregateFilter(self_filter, + filter_sum = AggregateFilter(self_filter, [tuple(filter_bins)], 'avg') tally_avg.filters.append(filter_sum) @@ -3577,7 +3594,7 @@ class Tally(IDManagerMixin): std_dev = np.sqrt(std_dev) # Add AggregateNuclide to the tally avg - nuclide_avg = openmc.AggregateNuclide(nuclides, 'avg') + nuclide_avg = AggregateNuclide(nuclides, 'avg') tally_avg.nuclides.append(nuclide_avg) # Add a copy of this tally's nuclides to the tally avg @@ -3596,7 +3613,7 @@ class Tally(IDManagerMixin): std_dev = np.sqrt(std_dev) # Add AggregateScore to the tally avg - score_sum = openmc.AggregateScore(scores, 'avg') + score_sum = AggregateScore(scores, 'avg') tally_avg.scores.append(score_sum) # Add a copy of this tally's scores to the tally avg @@ -3786,7 +3803,7 @@ class Tallies(cv.CheckedList): already_written = memo if memo else set() for tally in self: for f in tally.filters: - if isinstance(f, openmc.MeshFilter): + if isinstance(f, MeshFilter): if f.mesh.id in already_written: continue if len(f.mesh.name) > 0: @@ -3881,7 +3898,7 @@ class Tallies(cv.CheckedList): # Read filter elements filters = {} for e in elem.findall('filter'): - filter = openmc.Filter.from_xml_element(e, meshes=meshes) + filter = Filter.from_xml_element(e, meshes=meshes) filters[filter.id] = filter # Read derivative elements diff --git a/openmc/weight_windows.py b/openmc/weight_windows.py index 7797986df..63af2596e 100644 --- a/openmc/weight_windows.py +++ b/openmc/weight_windows.py @@ -11,6 +11,7 @@ import h5py import openmc from openmc.mesh import MeshBase, RectilinearMesh, CylindricalMesh, SphericalMesh, UnstructuredMesh +from openmc.tallies import Tallies import openmc.checkvalue as cv from openmc.checkvalue import PathLike from ._xml import get_elem_list, get_text, clean_indentation @@ -499,6 +500,8 @@ class WeightWindowGenerator: Particle type the weight windows apply to method : {'magic', 'fw_cadis'} The weight window generation methodology applied during an update. + targets : :class:`openmc.Tallies` or iterable of int + Target tallies for local variance reduction via FW-CADIS. max_realizations : int The upper limit for number of tally realizations when generating weight windows. @@ -518,6 +521,8 @@ class WeightWindowGenerator: Particle type the weight windows apply to method : {'magic', 'fw_cadis'} The weight window generation methodology applied during an update. + targets : :class:`openmc.Tallies` or numpy.ndarray + Target tallies for local variance reduction via FW-CADIS. max_realizations : int The upper limit for number of tally realizations when generating weight windows. @@ -529,7 +534,7 @@ class WeightWindowGenerator: Whether or not to apply weight windows on the fly. """ - _MAGIC_PARAMS = {'value': str, 'threshold': float, 'ratio': float} + _WWG_PARAMS = {'value': str, 'threshold': float, 'ratio': float} def __init__( self, @@ -537,6 +542,7 @@ class WeightWindowGenerator: energy_bounds: Sequence[float] | None = None, particle_type: str | int | openmc.ParticleType = 'neutron', method: str = 'magic', + targets: openmc.Tallies | Iterable[int] | None = None, max_realizations: int = 1, update_interval: int = 1, on_the_fly: bool = True @@ -549,6 +555,7 @@ class WeightWindowGenerator: self.energy_bounds = energy_bounds self.particle_type = particle_type self.method = method + self.targets = targets self.max_realizations = max_realizations self.update_interval = update_interval self.on_the_fly = on_the_fly @@ -611,6 +618,22 @@ class WeightWindowGenerator: self._check_update_parameters() except (TypeError, KeyError): warnings.warn(f'Update parameters are invalid for the "{m}" method.') + + @property + def targets(self) -> openmc.Tallies: + return self._targets + + @targets.setter + def targets(self, t): + if t is None: + self._targets = t + else: + cv.check_type('Local FW-CADIS target tallies', t, Iterable) + cv.check_greater_than('Local FW-CADIS target tallies', len(t), 0) + if not isinstance(t, openmc.Tallies): + cv.check_iterable_type('Local FW-CADIS target tallies', t, int) + t = np.asarray(list(t), dtype=int) + self._targets = t @property def max_realizations(self) -> int: @@ -638,13 +661,13 @@ class WeightWindowGenerator: def _check_update_parameters(self, params: dict): if self.method == 'magic' or self.method == 'fw_cadis': - check_params = self._MAGIC_PARAMS + check_params = self._WWG_PARAMS for key, val in params.items(): if key not in check_params: raise ValueError(f'Invalid param "{key}" for {self.method} ' 'weight window generation') - cv.check_type(f'weight window generation param: "{key}"', val, self._MAGIC_PARAMS[key]) + cv.check_type(f'weight window generation param: "{key}"', val, self._WWG_PARAMS[key]) @update_parameters.setter def update_parameters(self, params: dict): @@ -681,7 +704,7 @@ class WeightWindowGenerator: The update parameters as-read from the XML node (keys: str, values: str) """ if method == 'magic' or method == 'fw_cadis': - check_params = cls._MAGIC_PARAMS + check_params = cls._WWG_PARAMS for param, param_type in check_params.items(): if param in update_parameters: @@ -707,6 +730,20 @@ class WeightWindowGenerator: otf_elem.text = str(self.on_the_fly).lower() method_elem = ET.SubElement(element, 'method') method_elem.text = self.method + if self.targets is not None: + if self.method != 'fw_cadis': + raise ValueError( + "FW-CADIS update method is required in order to use " \ + "target tallies for WeightWindowGenerator.") + elif isinstance(self.targets, openmc.Tallies): + raise RuntimeError( + "FW-CADIS target tallies must be checked to ensure they are " \ + "present on model.tallies. Use model.export_to_xml() or " \ + "model.export_to_model_xml() to link FW-CADIS target tallies.") + else: + targets_elem = ET.SubElement(element, 'targets') + targets_elem.text = ' '.join(str(tally_id) for tally_id in self.targets) + if self.update_parameters is not None: self._update_parameters_subelement(element) @@ -733,8 +770,8 @@ class WeightWindowGenerator: mesh_id = int(get_text(elem, 'mesh')) mesh = meshes[mesh_id] - - energy_bounds = get_elem_list(elem, "energy_bounds, float") + + energy_bounds = get_elem_list(elem, "energy_bounds", float) particle_type = get_text(elem, 'particle_type') wwg = cls(mesh, energy_bounds, particle_type) @@ -743,6 +780,14 @@ class WeightWindowGenerator: wwg.update_interval = int(get_text(elem, 'update_interval')) wwg.on_the_fly = bool(get_text(elem, 'on_the_fly')) wwg.method = get_text(elem, 'method') + targets_elem = elem.find('targets') + if targets_elem is not None: + if wwg.method != 'fw_cadis': + raise ValueError( + "FW-CADIS update method is required in order to use " \ + "target tallies for WeightWindowGenerator.") + else: + wwg.targets = get_elem_list(elem, "targets") if elem.find('update_parameters') is not None: update_parameters = {} diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 1a6e7c0be..06c6ef14d 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -31,9 +31,11 @@ RandomRayVolumeEstimator FlatSourceDomain::volume_estimator_ { RandomRayVolumeEstimator::HYBRID}; bool FlatSourceDomain::volume_normalized_flux_tallies_ {false}; bool FlatSourceDomain::adjoint_ {false}; +bool FlatSourceDomain::fw_cadis_local_ {false}; double FlatSourceDomain::diagonal_stabilization_rho_ {1.0}; std::unordered_map>> FlatSourceDomain::mesh_domain_map_; +std::vector FlatSourceDomain::fw_cadis_local_targets_; FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) { @@ -1000,7 +1002,9 @@ void FlatSourceDomain::output_to_vtk() const void FlatSourceDomain::apply_external_source_to_source_region( int src_idx, SourceRegionHandle& srh) { - auto s = model::external_sources[src_idx].get(); + auto s = (adjoint_ && !model::adjoint_sources.empty()) + ? model::adjoint_sources[src_idx].get() + : model::external_sources[src_idx].get(); auto is = dynamic_cast(s); auto discrete = dynamic_cast(is->energy()); double strength_factor = is->strength(); @@ -1071,13 +1075,17 @@ void FlatSourceDomain::count_external_source_regions() } } -void FlatSourceDomain::convert_external_sources() +void FlatSourceDomain::convert_external_sources(bool use_adjoint_sources) { + // Determine whether forward or (local) adjoint sources are desired + const auto& sources = + use_adjoint_sources ? model::adjoint_sources : model::external_sources; + // Loop over external sources - for (int es = 0; es < model::external_sources.size(); es++) { + for (int es = 0; es < sources.size(); es++) { // Extract source information - Source* s = model::external_sources[es].get(); + Source* s = sources[es].get(); IndependentSource* is = dynamic_cast(s); Discrete* energy = dynamic_cast(is->energy()); const std::unordered_set& domain_ids = is->domain_ids(); @@ -1223,7 +1231,7 @@ void FlatSourceDomain::flatten_xs() } } -void FlatSourceDomain::set_adjoint_sources() +void FlatSourceDomain::set_fw_adjoint_sources() { // Set the adjoint external source to 1/forward_flux. If the forward flux is // negative, zero, or extremely close to zero, set the adjoint source to zero, @@ -1252,6 +1260,10 @@ void FlatSourceDomain::set_adjoint_sources() source_regions_.external_source(sr, g) = 0.0; } else { source_regions_.external_source(sr, g) = 1.0 / flux; + if (!std::isfinite(source_regions_.external_source(sr, g))) { + // If the flux is NaN or Inf, set the adjoint source to zero + source_regions_.external_source(sr, g) = 0.0; + } } if (flux > 0.0) { source_regions_.external_source_present(sr) = 1; @@ -1283,6 +1295,7 @@ void FlatSourceDomain::set_adjoint_sources() source_regions_.external_source_present(sr) = 0; } } + // Divide the fixed source term by sigma t (to save time when applying each // iteration) #pragma omp parallel for @@ -1297,8 +1310,86 @@ void FlatSourceDomain::set_adjoint_sources() sigma_t_[(material * ntemperature_ + temp) * negroups_ + g] * source_regions_.density_mult(sr); source_regions_.external_source(sr, g) /= sigma_t; + if (!std::isfinite(source_regions_.external_source(sr, g))) { + // If the flux is NaN or Inf, set the adjoint source to zero + source_regions_.external_source(sr, g) = 0.0; + } } } + + if (fw_cadis_local_) { +// Only external sources that have a non-mesh type tally task should remain +// non-zero. Everything else gets zero'd out. +#pragma omp parallel for + for (int64_t sr = 0; sr < n_source_regions(); sr++) { + + // If there is already no external source, don't need to do anything + if (source_regions_.external_source_present(sr) == 0) { + continue; + } + + // If there is an adjoint source term here, then we need to check it. + + // We will track if ANY group has a valid local FW-CADIS source term + bool has_any_sources = false; + + // Now, loop over groups + for (int g = 0; g < negroups_; g++) { + + // If there are no tally tasks associated with this source element + // then it is not a local FW-CADIS source, so we continue to the next + // group + if (source_regions_.tally_task(sr, g).empty()) { + source_regions_.external_source(sr, g) = 0.0; + continue; + } + + // If there are tally tasks, we can through them and check if + // any of them are local FW-CADIS targets. + + // We track if ANY of the tasks are local FW-CADIS target tallies + bool local_fw_cadis_target_region = false; + + // Now we loop through + for (const auto& task : source_regions_.tally_task(sr, g)) { + Tally& tally {*model::tallies[task.tally_idx]}; + const auto t_id = tally.id(); + + // Search for target tallies + if (std::find(fw_cadis_local_targets_.begin(), + fw_cadis_local_targets_.end(), + t_id) != fw_cadis_local_targets_.end()) { + local_fw_cadis_target_region = true; + break; + } + } + + // If ANY of the tasks is a local FW-CADIS target, + // Then we keep the source term and set that this + // source region has a valid FW-CADIS source term. + // Otherwise, we zero out the source term. + if (local_fw_cadis_target_region) { + has_any_sources = true; + } else { + source_regions_.external_source(sr, g) = 0.0; + } + } // End loop over groups + + // If there were any valid FW-CADIS source terms for any + // of the groups, then the SR as a whole counts as a source + if (has_any_sources) { + source_regions_.external_source_present(sr) = 1; + } else { + source_regions_.external_source_present(sr) = 0; + } + } // End loop over source regions + } // End local FW-CADIS logic +} + +void FlatSourceDomain::set_local_adjoint_sources() +{ + // Set the external source to user-specified adjoint sources. + convert_external_sources(true); } void FlatSourceDomain::transpose_scattering_matrix() diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index 80cbfc3fe..24650afb8 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -168,14 +168,12 @@ void validate_random_ray_inputs() "constrained by domain id (cell, material, or universe) in " "random ray mode."); } else if (is->domain_ids().size() > 0 && sp) { - // If both a domain constraint and a non-default point source location - // are specified, notify user that domain constraint takes precedence. - if (sp->r().x == 0.0 && sp->r().y == 0.0 && sp->r().z == 0.0) { - warning("Fixed source has both a domain constraint and a point " - "type spatial distribution. The domain constraint takes " - "precedence in random ray mode -- point source coordinate " - "will be ignored."); - } + // If both a domain constraint and a point source location are + // specified, notify user that domain constraint takes precedence. + warning("Fixed source has both a domain constraint and a point " + "type spatial distribution. The domain constraint takes " + "precedence in random ray mode -- point source coordinate " + "will be ignored."); } // Check that a discrete energy distribution was used @@ -189,6 +187,56 @@ void validate_random_ray_inputs() } } + // Validate adjoint sources + /////////////////////////////////////////////////////////////////// + if (FlatSourceDomain::adjoint_ && !model::adjoint_sources.empty()) { + for (int i = 0; i < model::adjoint_sources.size(); i++) { + Source* s = model::adjoint_sources[i].get(); + + // Check for independent source + IndependentSource* is = dynamic_cast(s); + + if (!is) { + fatal_error( + "Only IndependentSource adjoint source types are allowed in " + "random ray mode"); + } + + // Check for isotropic source + UnitSphereDistribution* angle_dist = is->angle(); + Isotropic* id = dynamic_cast(angle_dist); + if (!id) { + fatal_error( + "Invalid source definition -- only isotropic adjoint sources are " + "allowed in random ray mode."); + } + + // Validate that a domain ID was specified OR that it is a point source + auto sp = dynamic_cast(is->space()); + if (is->domain_ids().size() == 0 && !sp) { + fatal_error("Adjoint sources must be point source or spatially " + "constrained by domain id (cell, material, or universe) in " + "random ray mode."); + } else if (is->domain_ids().size() > 0 && sp) { + // If both a domain constraint and a point source location are + // specified, notify user that domain constraint takes precedence. + warning("Adjoint source has both a domain constraint and a point " + "type spatial distribution. The domain constraint takes " + "precedence in random ray mode -- point source coordinate " + "will be ignored."); + } + + // Check that a discrete energy distribution was used + Distribution* d = is->energy(); + Discrete* dd = dynamic_cast(d); + if (!dd) { + fatal_error( + "Only discrete (multigroup) energy distributions are allowed for " + "adjoint sources in random ray mode."); + } + } + } + // Validate plotting files /////////////////////////////////////////////////////////////////// for (int p = 0; p < model::plots.size(); p++) { @@ -232,20 +280,22 @@ void validate_random_ray_inputs() warning( "Linear sources may result in negative fluxes in small source regions " "generated by mesh subdivision. Negative sources may result in low " - "quality FW-CADIS weight windows. We recommend you use flat source mode " - "when generating weight windows with an overlaid mesh tally."); + "quality FW-CADIS weight windows. We recommend you use flat source " + "mode when generating weight windows with an overlaid mesh tally."); } } -void print_adjoint_header() +void openmc_finalize_random_ray() { - if (!FlatSourceDomain::adjoint_) - // If we're going to do an adjoint simulation afterwards, report that this - // is the initial forward flux solve. - header("FORWARD FLUX SOLVE", 3); - else - // Otherwise report that we are doing the adjoint simulation - header("ADJOINT FLUX SOLVE", 3); + FlatSourceDomain::volume_estimator_ = RandomRayVolumeEstimator::HYBRID; + FlatSourceDomain::volume_normalized_flux_tallies_ = false; + FlatSourceDomain::adjoint_ = false; + FlatSourceDomain::fw_cadis_local_ = false; + FlatSourceDomain::fw_cadis_local_targets_.clear(); + FlatSourceDomain::mesh_domain_map_.clear(); + RandomRay::ray_source_.reset(); + RandomRay::source_shape_ = RandomRaySourceShape::FLAT; + RandomRay::sample_method_ = RandomRaySampleMethod::PRNG; } //============================================================================== @@ -278,16 +328,6 @@ RandomRaySimulation::RandomRaySimulation() // Convert OpenMC native MGXS into a more efficient format // internal to the random ray solver domain_->flatten_xs(); - - // Check if adjoint calculation is needed. If it is, we will run the forward - // calculation first and then the adjoint calculation later. - adjoint_needed_ = FlatSourceDomain::adjoint_; - - // Adjoint is always false for the forward calculation - FlatSourceDomain::adjoint_ = false; - - // The first simulation is run after initialization - is_first_simulation_ = true; } void RandomRaySimulation::apply_fixed_sources_and_mesh_domains() @@ -295,30 +335,52 @@ void RandomRaySimulation::apply_fixed_sources_and_mesh_domains() domain_->apply_meshes(); if (settings::run_mode == RunMode::FIXED_SOURCE) { // Transfer external source user inputs onto random ray source regions - domain_->convert_external_sources(); + domain_->convert_external_sources(false); domain_->count_external_source_regions(); } } -void RandomRaySimulation::prepare_fixed_sources_adjoint() +void RandomRaySimulation::prepare_fw_fixed_sources_adjoint() { + // Prepare adjoint fixed sources using forward flux domain_->source_regions_.adjoint_reset(); if (settings::run_mode == RunMode::FIXED_SOURCE) { - domain_->set_adjoint_sources(); + domain_->set_fw_adjoint_sources(); } } -void RandomRaySimulation::prepare_adjoint_simulation() +void RandomRaySimulation::prepare_local_fixed_sources_adjoint() { - // Configure the domain for adjoint simulation - FlatSourceDomain::adjoint_ = true; + if (settings::run_mode == RunMode::FIXED_SOURCE) { + domain_->set_local_adjoint_sources(); + } +} + +void RandomRaySimulation::prepare_adjoint_simulation(bool fw_adjoint) +{ + reset_timers(); + + if (mpi::master) + header("ADJOINT FLUX SOLVE", 3); + + if (fw_adjoint) { + // Forward simulation has already been run; + // Configure the domain for adjoint simulation and + // re-initialize OpenMC general data structures + FlatSourceDomain::adjoint_ = true; + + openmc_simulation_init(); + + prepare_fw_fixed_sources_adjoint(); + } else { + // Initialize adjoint fixed sources + domain_->apply_meshes(); + prepare_local_fixed_sources_adjoint(); + domain_->count_external_source_regions(); + } - // Reset k-eff domain_->k_eff_ = 1.0; - // Initialize adjoint fixed sources, if present - prepare_fixed_sources_adjoint(); - // Transpose scattering matrix domain_->transpose_scattering_matrix(); @@ -328,18 +390,6 @@ void RandomRaySimulation::prepare_adjoint_simulation() void RandomRaySimulation::simulate() { - if (!is_first_simulation_) { - if (mpi::master && adjoint_needed_) - openmc::print_adjoint_header(); - - // Reset the timers and reinitialize the general OpenMC datastructures if - // this is after the first simulation - reset_timers(); - - // Initialize OpenMC general data structures - openmc_simulation_init(); - } - // Begin main simulation timer simulation::time_total.start(); @@ -435,7 +485,7 @@ void RandomRaySimulation::simulate() // End main simulation timer simulation::time_total.stop(); - // Normalize and save the final flux + // Normalize and save the final forward flux double source_normalization_factor = domain_->compute_fixed_source_normalization_factor() / (settings::n_batches - settings::n_inactive); @@ -451,11 +501,6 @@ void RandomRaySimulation::simulate() // Output all simulation results output_simulation_results(); - - // Toggle that the simulation object has been initialized after the first - // simulation - if (is_first_simulation_) - is_first_simulation_ = false; } void RandomRaySimulation::output_simulation_results() const @@ -622,17 +667,6 @@ void RandomRaySimulation::print_results_random_ray( } } -void openmc_finalize_random_ray() -{ - FlatSourceDomain::volume_estimator_ = RandomRayVolumeEstimator::HYBRID; - FlatSourceDomain::volume_normalized_flux_tallies_ = false; - FlatSourceDomain::adjoint_ = false; - FlatSourceDomain::mesh_domain_map_.clear(); - RandomRay::ray_source_.reset(); - RandomRay::source_shape_ = RandomRaySourceShape::FLAT; - RandomRay::sample_method_ = RandomRaySampleMethod::PRNG; -} - } // namespace openmc //============================================================================== @@ -645,12 +679,25 @@ void openmc_run_random_ray() // Run forward simulation ////////////////////////////////////////////////////////// - if (openmc::mpi::master) { - if (openmc::FlatSourceDomain::adjoint_) { - openmc::FlatSourceDomain::adjoint_ = false; - openmc::print_adjoint_header(); - openmc::FlatSourceDomain::adjoint_ = true; - } + // Check if adjoint calculation is needed, and if local adjoint source(s) + // are present. If an adjoint calculation is needed and no sources are + // specified, we will run a forward calculation first to calculate adjoint + // sources for global variance reduction, then perform an adjoint + // calculation later. + bool adjoint_needed = openmc::FlatSourceDomain::adjoint_; + bool fw_adjoint = openmc::model::adjoint_sources.empty() && adjoint_needed; + + // If we're going to do an adjoint simulation with forward-weighted adjoint + // sources afterwards, report that this is the initial forward flux solve. + if (!adjoint_needed || fw_adjoint) { + // Configure the domain for forward simulation + openmc::FlatSourceDomain::adjoint_ = false; + + if (adjoint_needed && openmc::mpi::master) + openmc::header("FORWARD FLUX SOLVE", 3); + } else { + // Configure domain for adjoint simulation (later) + openmc::FlatSourceDomain::adjoint_ = true; } // Initialize OpenMC general data structures @@ -663,21 +710,25 @@ void openmc_run_random_ray() // Initialize Random Ray Simulation Object openmc::RandomRaySimulation sim; - // Initialize fixed sources, if present - sim.apply_fixed_sources_and_mesh_domains(); + if (!adjoint_needed || fw_adjoint) { + // Initialize fixed sources, if present + sim.apply_fixed_sources_and_mesh_domains(); - // Run initial random ray simulation - sim.simulate(); + // Execute random ray simulation + sim.simulate(); + } ////////////////////////////////////////////////////////// // Run adjoint simulation (if enabled) ////////////////////////////////////////////////////////// - if (sim.adjoint_needed_) { - // Setup for adjoint simulation - sim.prepare_adjoint_simulation(); - - // Run adjoint simulation - sim.simulate(); + if (!adjoint_needed) { + return; } + + // Setup for adjoint simulation + sim.prepare_adjoint_simulation(fw_adjoint); + + // Execute random ray simulation + sim.simulate(); } diff --git a/src/settings.cpp b/src/settings.cpp index ab9f9a5aa..bb6991da9 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -280,8 +280,9 @@ void get_run_parameters(pugi::xml_node node_base) } else { fatal_error("Specify random ray inactive distance in settings XML"); } - if (check_for_node(random_ray_node, "source")) { - xml_node source_node = random_ray_node.child("source"); + if (check_for_node(random_ray_node, "ray_source")) { + xml_node ray_source_node = random_ray_node.child("ray_source"); + xml_node source_node = ray_source_node.child("source"); // Get point to list of elements and make sure there is at least // one RandomRay::ray_source_ = Source::create(source_node); @@ -369,6 +370,13 @@ void get_run_parameters(pugi::xml_node node_base) "between 0 and 1"); } } + if (check_for_node(random_ray_node, "adjoint_source")) { + pugi::xml_node adj_source_node = random_ray_node.child("adjoint_source"); + for (pugi::xml_node source_node : adj_source_node.children("source")) { + // Find any local adjoint sources + model::adjoint_sources.push_back(Source::create(source_node)); + } + } } } @@ -1276,6 +1284,16 @@ void read_settings_xml(pugi::xml_node root) break; } } + // If any weight window generators have local FW-CADIS target tallies, + // user-defined adjoint sources cannot be used at the same time. + if (!model::adjoint_sources.empty()) { + for (const auto& wwg : variance_reduction::weight_windows_generators) { + if (!wwg->targets_.empty()) { + fatal_error("Cannot use both user-defined adjoint sources and " + "FW-CADIS target tallies at the same time."); + } + } + } } // Set up weight window checkpoints diff --git a/src/source.cpp b/src/source.cpp index f0b8f48ed..524a72bf0 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -62,6 +62,8 @@ namespace model { vector> external_sources; +vector> adjoint_sources; + DiscreteIndex external_sources_probability; } // namespace model @@ -710,6 +712,7 @@ SourceSite sample_external_source(uint64_t* seed) void free_memory_source() { model::external_sources.clear(); + model::adjoint_sources.clear(); reset_source_rejection_counters(); } diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index c00872e56..0614110cd 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -621,7 +621,7 @@ void WeightWindows::update_weights(const Tally* tally, const std::string& value, } } } else { - // For FW-CADIS, weight windows are inversely proportional to the adjoint + // For (FW-)CADIS, weight windows are inversely proportional to the adjoint // fluxes. We normalize the weight windows across all energy groups. #pragma omp parallel for collapse(2) schedule(static) for (int e = 0; e < e_bins; e++) { @@ -801,6 +801,13 @@ WeightWindowsGenerator::WeightWindowsGenerator(pugi::xml_node node) fatal_error("FW-CADIS can only be run in random ray solver mode."); } FlatSourceDomain::adjoint_ = true; + if (check_for_node(node, "targets")) { + FlatSourceDomain::fw_cadis_local_ = true; + targets_ = get_node_array(node, "targets"); + FlatSourceDomain::fw_cadis_local_targets_.insert( + std::end(FlatSourceDomain::fw_cadis_local_targets_), + std::begin(targets_), std::end(targets_)); + } } else { fatal_error(fmt::format( "Unknown weight window update method '{}' specified", method_string)); diff --git a/tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat b/tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat index 0adfc5488..94e270976 100644 --- a/tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat +++ b/tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true true naive diff --git a/tests/regression_tests/random_ray_adjoint_k_eff/inputs_true.dat b/tests/regression_tests/random_ray_adjoint_k_eff/inputs_true.dat index 073348c41..755afd6c4 100644 --- a/tests/regression_tests/random_ray_adjoint_k_eff/inputs_true.dat +++ b/tests/regression_tests/random_ray_adjoint_k_eff/inputs_true.dat @@ -80,11 +80,13 @@ 100.0 20.0 - - - -1.26 -1.26 -1 1.26 1.26 1 - - + + + + -1.26 -1.26 -1 1.26 1.26 1 + + + true true diff --git a/tests/regression_tests/random_ray_adjoint_local/__init__.py b/tests/regression_tests/random_ray_adjoint_local/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/random_ray_adjoint_local/inputs_true.dat b/tests/regression_tests/random_ray_adjoint_local/inputs_true.dat new file mode 100644 index 000000000..9021d1675 --- /dev/null +++ b/tests/regression_tests/random_ray_adjoint_local/inputs_true.dat @@ -0,0 +1,293 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + + + + + + fixed source + 500 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 800.0 + 100.0 + + + + 0.0 0.0 0.0 35.0 35.0 35.0 + + + + true + + + + + + true + + + + 100.0 1.0 + + + cell + 6 7 + + + + naive + + + 14 14 14 + 0.0 0.0 0.0 + 35.0 35.0 35.0 + + + + + 6 + + + 7 + + + 3 + + + 2 + + + 1 + + + 1 + flux + tracklength + + + 2 + flux + tracklength + + + 3 + flux + tracklength + + + 4 + flux + tracklength + + + 5 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_adjoint_local/results_true.dat b/tests/regression_tests/random_ray_adjoint_local/results_true.dat new file mode 100644 index 000000000..daa948565 --- /dev/null +++ b/tests/regression_tests/random_ray_adjoint_local/results_true.dat @@ -0,0 +1,15 @@ +tally 1: +2.215273E+01 +9.815738E+01 +tally 2: +1.873933E+01 +7.023420E+01 +tally 3: +4.802282E-01 +4.612707E-02 +tally 4: +2.516720E-01 +1.271063E-02 +tally 5: +1.169938E-02 +3.277334E-05 diff --git a/tests/regression_tests/random_ray_adjoint_local/test.py b/tests/regression_tests/random_ray_adjoint_local/test.py new file mode 100644 index 000000000..c11b8e847 --- /dev/null +++ b/tests/regression_tests/random_ray_adjoint_local/test.py @@ -0,0 +1,35 @@ +import os +import openmc + +from openmc.examples import random_ray_three_region_cube_with_detectors + +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_adjoint_local(): + model = random_ray_three_region_cube_with_detectors() + + detector1_cells = model.geometry.get_cells_by_name("detector 1") + detector2_cells = model.geometry.get_cells_by_name("detector 2") + detector_cells = detector1_cells + detector2_cells + + strengths = [1.0] + midpoints = [100.0] + energy_distribution = openmc.stats.Discrete(x=midpoints, p=strengths) + + adj_source = openmc.IndependentSource(energy=energy_distribution, constraints={ + 'domains': detector_cells}, strength=3.14) + + model.settings.random_ray['adjoint'] = True + model.settings.random_ray['adjoint_source'] = adj_source + model.settings.random_ray['volume_estimator'] = 'naive' + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_auto_convert/infinite_medium/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert/infinite_medium/inputs_true.dat index 464c89a5d..86d5ec4ab 100644 --- a/tests/regression_tests/random_ray_auto_convert/infinite_medium/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert/infinite_medium/inputs_true.dat @@ -41,11 +41,13 @@ multi-group - - - -0.63 -0.63 -1.0 0.63 0.63 1.0 - - + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 150.0 diff --git a/tests/regression_tests/random_ray_auto_convert/material_wise/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert/material_wise/inputs_true.dat index 464c89a5d..86d5ec4ab 100644 --- a/tests/regression_tests/random_ray_auto_convert/material_wise/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert/material_wise/inputs_true.dat @@ -41,11 +41,13 @@ multi-group - - - -0.63 -0.63 -1.0 0.63 0.63 1.0 - - + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 150.0 diff --git a/tests/regression_tests/random_ray_auto_convert/stochastic_slab/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert/stochastic_slab/inputs_true.dat index 464c89a5d..86d5ec4ab 100644 --- a/tests/regression_tests/random_ray_auto_convert/stochastic_slab/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert/stochastic_slab/inputs_true.dat @@ -41,11 +41,13 @@ multi-group - - - -0.63 -0.63 -1.0 0.63 0.63 1.0 - - + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 150.0 diff --git a/tests/regression_tests/random_ray_auto_convert_kappa_fission/infinite_medium/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_kappa_fission/infinite_medium/inputs_true.dat index 9f3a827f6..b00935ef3 100644 --- a/tests/regression_tests/random_ray_auto_convert_kappa_fission/infinite_medium/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_kappa_fission/infinite_medium/inputs_true.dat @@ -41,11 +41,13 @@ multi-group - - - -0.63 -0.63 -1.0 0.63 0.63 1.0 - - + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 150.0 diff --git a/tests/regression_tests/random_ray_auto_convert_kappa_fission/material_wise/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_kappa_fission/material_wise/inputs_true.dat index edf68f7e2..472406fa8 100644 --- a/tests/regression_tests/random_ray_auto_convert_kappa_fission/material_wise/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_kappa_fission/material_wise/inputs_true.dat @@ -41,11 +41,13 @@ multi-group - - - -0.63 -0.63 -1.0 0.63 0.63 1.0 - - + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 150.0 diff --git a/tests/regression_tests/random_ray_auto_convert_kappa_fission/stochastic_slab/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_kappa_fission/stochastic_slab/inputs_true.dat index edf68f7e2..472406fa8 100644 --- a/tests/regression_tests/random_ray_auto_convert_kappa_fission/stochastic_slab/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_kappa_fission/stochastic_slab/inputs_true.dat @@ -41,11 +41,13 @@ multi-group - - - -0.63 -0.63 -1.0 0.63 0.63 1.0 - - + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 150.0 diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/inputs_true.dat index 80a166c67..15981f7fa 100644 --- a/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/inputs_true.dat @@ -38,11 +38,13 @@ multi-group - - - -0.63 -0.63 -1.0 0.63 0.63 1.0 - - + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 150.0 diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/inputs_true.dat index 464c89a5d..86d5ec4ab 100644 --- a/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/inputs_true.dat @@ -41,11 +41,13 @@ multi-group - - - -0.63 -0.63 -1.0 0.63 0.63 1.0 - - + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 150.0 diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/inputs_true.dat index 80a166c67..15981f7fa 100644 --- a/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/inputs_true.dat @@ -38,11 +38,13 @@ multi-group - - - -0.63 -0.63 -1.0 0.63 0.63 1.0 - - + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 150.0 diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/inputs_true.dat index 464c89a5d..86d5ec4ab 100644 --- a/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/inputs_true.dat @@ -41,11 +41,13 @@ multi-group - - - -0.63 -0.63 -1.0 0.63 0.63 1.0 - - + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 150.0 diff --git a/tests/regression_tests/random_ray_auto_convert_temperature/infinite_medium/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_temperature/infinite_medium/inputs_true.dat index 08a30176b..c60e6a041 100644 --- a/tests/regression_tests/random_ray_auto_convert_temperature/infinite_medium/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_temperature/infinite_medium/inputs_true.dat @@ -47,11 +47,13 @@ 200.0 400.0 200.0 - - - -0.63 -0.63 -1.0 0.63 0.63 1.0 - - + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 150.0 diff --git a/tests/regression_tests/random_ray_auto_convert_temperature/material_wise/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_temperature/material_wise/inputs_true.dat index 08a30176b..c60e6a041 100644 --- a/tests/regression_tests/random_ray_auto_convert_temperature/material_wise/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_temperature/material_wise/inputs_true.dat @@ -47,11 +47,13 @@ 200.0 400.0 200.0 - - - -0.63 -0.63 -1.0 0.63 0.63 1.0 - - + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 150.0 diff --git a/tests/regression_tests/random_ray_auto_convert_temperature/stochastic_slab/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_temperature/stochastic_slab/inputs_true.dat index 08a30176b..c60e6a041 100644 --- a/tests/regression_tests/random_ray_auto_convert_temperature/stochastic_slab/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_temperature/stochastic_slab/inputs_true.dat @@ -47,11 +47,13 @@ 200.0 400.0 200.0 - - - -0.63 -0.63 -1.0 0.63 0.63 1.0 - - + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 150.0 diff --git a/tests/regression_tests/random_ray_cell_density/eigen/inputs_true.dat b/tests/regression_tests/random_ray_cell_density/eigen/inputs_true.dat index bcf4d0d90..eacd54f83 100644 --- a/tests/regression_tests/random_ray_cell_density/eigen/inputs_true.dat +++ b/tests/regression_tests/random_ray_cell_density/eigen/inputs_true.dat @@ -86,11 +86,13 @@ 100.0 20.0 - - - -1.26 -1.26 -1 1.26 1.26 1 - - + + + + -1.26 -1.26 -1 1.26 1.26 1 + + + true diff --git a/tests/regression_tests/random_ray_cell_density/fs/inputs_true.dat b/tests/regression_tests/random_ray_cell_density/fs/inputs_true.dat index e90f25973..f369bae89 100644 --- a/tests/regression_tests/random_ray_cell_density/fs/inputs_true.dat +++ b/tests/regression_tests/random_ray_cell_density/fs/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true diff --git a/tests/regression_tests/random_ray_cell_temperature/inputs_true.dat b/tests/regression_tests/random_ray_cell_temperature/inputs_true.dat index e8674e6ea..99363ed87 100644 --- a/tests/regression_tests/random_ray_cell_temperature/inputs_true.dat +++ b/tests/regression_tests/random_ray_cell_temperature/inputs_true.dat @@ -89,11 +89,13 @@ 100.0 20.0 - - - -1.26 -1.26 -1 1.26 1.26 1 - - + + + + -1.26 -1.26 -1 1.26 1.26 1 + + + true diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat b/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat index 47325ebd7..11100e88e 100644 --- a/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat +++ b/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat @@ -41,11 +41,13 @@ multi-group - - - -0.63 -0.63 -1.0 0.63 0.63 1.0 - - + + + + -0.63 -0.63 -1.0 0.63 0.63 1.0 + + + 30.0 150.0 diff --git a/tests/regression_tests/random_ray_entropy/settings.xml b/tests/regression_tests/random_ray_entropy/settings.xml index 81deaa775..0d830417b 100644 --- a/tests/regression_tests/random_ray_entropy/settings.xml +++ b/tests/regression_tests/random_ray_entropy/settings.xml @@ -6,11 +6,13 @@ 5 multi-group - - - 0.0 0.0 0.0 100.0 100.0 100.0 - - + + + + 0.0 0.0 0.0 100.0 100.0 100.0 + + + 40.0 400.0 diff --git a/tests/regression_tests/random_ray_fixed_source_domain/cell/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/cell/inputs_true.dat index 9f1987f3a..d650bbaf9 100644 --- a/tests/regression_tests/random_ray_fixed_source_domain/cell/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_domain/cell/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true diff --git a/tests/regression_tests/random_ray_fixed_source_domain/material/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/material/inputs_true.dat index b4f57dbfa..98a51add1 100644 --- a/tests/regression_tests/random_ray_fixed_source_domain/material/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_domain/material/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true diff --git a/tests/regression_tests/random_ray_fixed_source_domain/universe/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/universe/inputs_true.dat index ab91f74e5..20deba664 100644 --- a/tests/regression_tests/random_ray_fixed_source_domain/universe/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_domain/universe/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true diff --git a/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat index 220fa7db6..2268d82c3 100644 --- a/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true linear diff --git a/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat index f8c443085..fe95baa7b 100644 --- a/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true linear_xy diff --git a/tests/regression_tests/random_ray_fixed_source_mesh/flat/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_mesh/flat/inputs_true.dat index c84e544fc..a5632ece9 100644 --- a/tests/regression_tests/random_ray_fixed_source_mesh/flat/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_mesh/flat/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true diff --git a/tests/regression_tests/random_ray_fixed_source_mesh/linear/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_mesh/linear/inputs_true.dat index 05c4846e6..9d22603c6 100644 --- a/tests/regression_tests/random_ray_fixed_source_mesh/linear/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_mesh/linear/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true diff --git a/tests/regression_tests/random_ray_fixed_source_normalization/False/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_normalization/False/inputs_true.dat index 0c870e100..de941f10f 100644 --- a/tests/regression_tests/random_ray_fixed_source_normalization/False/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_normalization/False/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + false diff --git a/tests/regression_tests/random_ray_fixed_source_normalization/True/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_normalization/True/inputs_true.dat index ab91f74e5..20deba664 100644 --- a/tests/regression_tests/random_ray_fixed_source_normalization/True/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_normalization/True/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/flat/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_subcritical/flat/inputs_true.dat index 0c05a71df..943468a10 100644 --- a/tests/regression_tests/random_ray_fixed_source_subcritical/flat/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_subcritical/flat/inputs_true.dat @@ -110,11 +110,13 @@ 40.0 40.0 - - - -1.26 -1.26 -1 1.26 1.26 1 - - + + + + -1.26 -1.26 -1 1.26 1.26 1 + + + false flat diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/inputs_true.dat index a67495bf1..650953c4b 100644 --- a/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/inputs_true.dat @@ -110,11 +110,13 @@ 40.0 40.0 - - - -1.26 -1.26 -1 1.26 1.26 1 - - + + + + -1.26 -1.26 -1 1.26 1.26 1 + + + false linear_xy diff --git a/tests/regression_tests/random_ray_halton_samples/inputs_true.dat b/tests/regression_tests/random_ray_halton_samples/inputs_true.dat index 36d5f6f22..1b86d2dae 100644 --- a/tests/regression_tests/random_ray_halton_samples/inputs_true.dat +++ b/tests/regression_tests/random_ray_halton_samples/inputs_true.dat @@ -80,11 +80,13 @@ 100.0 20.0 - - - -1.26 -1.26 -1 1.26 1.26 1 - - + + + + -1.26 -1.26 -1 1.26 1.26 1 + + + true halton diff --git a/tests/regression_tests/random_ray_k_eff/inputs_true.dat b/tests/regression_tests/random_ray_k_eff/inputs_true.dat index 545bd1d45..72b783344 100644 --- a/tests/regression_tests/random_ray_k_eff/inputs_true.dat +++ b/tests/regression_tests/random_ray_k_eff/inputs_true.dat @@ -80,11 +80,13 @@ 100.0 20.0 - - - -1.26 -1.26 -1 1.26 1.26 1 - - + + + + -1.26 -1.26 -1 1.26 1.26 1 + + + true diff --git a/tests/regression_tests/random_ray_k_eff_mesh/inputs_true.dat b/tests/regression_tests/random_ray_k_eff_mesh/inputs_true.dat index 98badea18..f6e9c8e3e 100644 --- a/tests/regression_tests/random_ray_k_eff_mesh/inputs_true.dat +++ b/tests/regression_tests/random_ray_k_eff_mesh/inputs_true.dat @@ -80,11 +80,13 @@ 100.0 20.0 - - - -1.26 -1.26 -1 1.26 1.26 1 - - + + + + -1.26 -1.26 -1 1.26 1.26 1 + + + true diff --git a/tests/regression_tests/random_ray_linear/linear/inputs_true.dat b/tests/regression_tests/random_ray_linear/linear/inputs_true.dat index a43a66e71..269d9892e 100644 --- a/tests/regression_tests/random_ray_linear/linear/inputs_true.dat +++ b/tests/regression_tests/random_ray_linear/linear/inputs_true.dat @@ -80,11 +80,13 @@ 100.0 20.0 - - - -1.26 -1.26 -1 1.26 1.26 1 - - + + + + -1.26 -1.26 -1 1.26 1.26 1 + + + true linear diff --git a/tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat b/tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat index 7f76f2fd1..217e95516 100644 --- a/tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat +++ b/tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat @@ -80,11 +80,13 @@ 100.0 20.0 - - - -1.26 -1.26 -1 1.26 1.26 1 - - + + + + -1.26 -1.26 -1 1.26 1.26 1 + + + true linear_xy diff --git a/tests/regression_tests/random_ray_low_density/inputs_true.dat b/tests/regression_tests/random_ray_low_density/inputs_true.dat index ab91f74e5..20deba664 100644 --- a/tests/regression_tests/random_ray_low_density/inputs_true.dat +++ b/tests/regression_tests/random_ray_low_density/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true diff --git a/tests/regression_tests/random_ray_point_source_locator/inputs_true.dat b/tests/regression_tests/random_ray_point_source_locator/inputs_true.dat index 088f803bf..b4bd263f5 100644 --- a/tests/regression_tests/random_ray_point_source_locator/inputs_true.dat +++ b/tests/regression_tests/random_ray_point_source_locator/inputs_true.dat @@ -206,11 +206,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true diff --git a/tests/regression_tests/random_ray_s2/inputs_true.dat b/tests/regression_tests/random_ray_s2/inputs_true.dat index aad8ea28b..c0dc6292f 100644 --- a/tests/regression_tests/random_ray_s2/inputs_true.dat +++ b/tests/regression_tests/random_ray_s2/inputs_true.dat @@ -37,11 +37,13 @@ 100.0 400.0 - - - 0.0 -5.0 -5.0 40.0 5.0 5.0 - - + + + + 0.0 -5.0 -5.0 40.0 5.0 5.0 + + + flat s2 diff --git a/tests/regression_tests/random_ray_void/flat/inputs_true.dat b/tests/regression_tests/random_ray_void/flat/inputs_true.dat index aa28e7b68..66390c766 100644 --- a/tests/regression_tests/random_ray_void/flat/inputs_true.dat +++ b/tests/regression_tests/random_ray_void/flat/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true flat diff --git a/tests/regression_tests/random_ray_void/linear/inputs_true.dat b/tests/regression_tests/random_ray_void/linear/inputs_true.dat index e4b2f22fa..45228a039 100644 --- a/tests/regression_tests/random_ray_void/linear/inputs_true.dat +++ b/tests/regression_tests/random_ray_void/linear/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true linear diff --git a/tests/regression_tests/random_ray_volume_estimator/hybrid/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator/hybrid/inputs_true.dat index 8e8a8ed9b..4d1af46b1 100644 --- a/tests/regression_tests/random_ray_volume_estimator/hybrid/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator/hybrid/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true hybrid diff --git a/tests/regression_tests/random_ray_volume_estimator/naive/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator/naive/inputs_true.dat index 1e25b97da..a268d55d0 100644 --- a/tests/regression_tests/random_ray_volume_estimator/naive/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator/naive/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true naive diff --git a/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/inputs_true.dat index 78c162697..777ccaea5 100644 --- a/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true simulation_averaged diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/inputs_true.dat index 47a8a7182..dd11567f6 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true linear hybrid diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/naive/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/naive/inputs_true.dat index 80a9ada4d..6933fba43 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/naive/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator_linear/naive/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true linear naive diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/inputs_true.dat index 4f032a62a..3ccab1d21 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/inputs_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/inputs_true.dat @@ -207,11 +207,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true linear simulation_averaged diff --git a/tests/regression_tests/weightwindows_fw_cadis/inputs_true.dat b/tests/regression_tests/weightwindows_fw_cadis/inputs_true.dat index 5fa6505dd..6bdabfbee 100644 --- a/tests/regression_tests/weightwindows_fw_cadis/inputs_true.dat +++ b/tests/regression_tests/weightwindows_fw_cadis/inputs_true.dat @@ -222,11 +222,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true naive diff --git a/tests/regression_tests/weightwindows_fw_cadis_local/__init__.py b/tests/regression_tests/weightwindows_fw_cadis_local/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/weightwindows_fw_cadis_local/inputs_true.dat b/tests/regression_tests/weightwindows_fw_cadis_local/inputs_true.dat new file mode 100644 index 000000000..ffdd977ec --- /dev/null +++ b/tests/regression_tests/weightwindows_fw_cadis_local/inputs_true.dat @@ -0,0 +1,273 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + + + + + + fixed source + 500 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + + 2 + neutron + 10 + 1 + true + fw_cadis + 1 2 + + + + 7 7 7 + 0.0 0.0 0.0 + 35.0 35.0 35.0 + + + 800.0 + 100.0 + + + + 0.0 0.0 0.0 35.0 35.0 35.0 + + + + true + + + + + + naive + + + 14 14 14 + 0.0 0.0 0.0 + 35.0 35.0 35.0 + + + + + 6 + + + 7 + + + 1 + flux + tracklength + + + 2 + flux + tracklength + + + diff --git a/tests/regression_tests/weightwindows_fw_cadis_local/results_true.dat b/tests/regression_tests/weightwindows_fw_cadis_local/results_true.dat new file mode 100644 index 000000000..5991fc621 --- /dev/null +++ b/tests/regression_tests/weightwindows_fw_cadis_local/results_true.dat @@ -0,0 +1,696 @@ +RegularMesh + ID = 2 + Name = + Dimensions = 3 + Voxels = [7 7 7] + Lower left = [0. 0. 0.] + Upper Right = [np.float64(35.0), np.float64(35.0), np.float64(35.0)] + Width = [5. 5. 5.] +Lower Bounds +1.52e-01 +1.53e-01 +1.78e-01 +1.97e-01 +2.41e-01 +3.94e-01 +2.26e-03 +1.43e-01 +1.58e-01 +1.75e-01 +1.86e-01 +2.06e-01 +4.83e-02 +2.11e-03 +1.31e-01 +1.37e-01 +1.61e-01 +1.76e-01 +2.04e-01 +1.15e-01 +5.04e-03 +1.05e-01 +1.31e-01 +1.49e-01 +1.72e-01 +1.81e-01 +4.24e-02 +5.31e-03 +6.79e-02 +9.98e-02 +1.56e-01 +1.68e-01 +1.78e-01 +1.39e-02 +5.49e-03 +2.69e-03 +6.66e-03 +2.36e-02 +1.65e-02 +1.71e-02 +1.12e-02 +1.94e-03 +1.12e-04 +5.43e-04 +1.23e-03 +1.51e-03 +1.51e-03 +1.31e-03 +1.21e-03 +1.52e-01 +1.60e-01 +1.81e-01 +2.03e-01 +2.16e-01 +2.58e-02 +2.07e-03 +1.45e-01 +1.59e-01 +1.84e-01 +2.07e-01 +2.14e-01 +8.82e-02 +2.92e-03 +1.36e-01 +1.53e-01 +1.61e-01 +1.92e-01 +2.28e-01 +1.82e-01 +2.70e-03 +1.18e-01 +1.42e-01 +1.52e-01 +1.81e-01 +1.98e-01 +2.87e-02 +6.46e-03 +1.00e-01 +1.41e-01 +1.80e-01 +1.73e-01 +2.05e-01 +1.09e-01 +3.29e-03 +5.37e-03 +1.44e-02 +4.53e-02 +2.16e-02 +7.53e-02 +2.38e-02 +1.38e-03 +4.52e-04 +1.11e-03 +1.39e-03 +1.33e-03 +1.26e-03 +1.22e-03 +9.78e-04 +1.83e-01 +1.77e-01 +1.83e-01 +2.03e-01 +2.27e-01 +7.70e-02 +3.55e-03 +1.63e-01 +1.80e-01 +1.87e-01 +1.86e-01 +2.20e-01 +4.43e-02 +2.00e-03 +1.68e-01 +1.60e-01 +1.63e-01 +1.78e-01 +2.12e-01 +9.86e-02 +2.96e-03 +1.56e-01 +1.59e-01 +1.63e-01 +1.72e-01 +1.81e-01 +7.15e-02 +1.77e-03 +1.58e-01 +1.80e-01 +1.87e-01 +1.66e-01 +1.66e-01 +1.22e-02 +2.65e-03 +2.11e-02 +3.01e-02 +4.85e-02 +1.48e-02 +4.67e-02 +5.38e-03 +1.01e-03 +8.10e-04 +8.55e-04 +1.43e-03 +1.63e-03 +1.05e-03 +1.04e-03 +6.84e-04 +2.01e-01 +2.17e-01 +2.26e-01 +2.37e-01 +2.45e-01 +3.25e-02 +2.68e-02 +2.09e-01 +2.09e-01 +2.07e-01 +2.02e-01 +2.15e-01 +2.42e-02 +2.97e-03 +2.01e-01 +1.94e-01 +1.93e-01 +1.80e-01 +1.72e-01 +1.32e-02 +1.54e-03 +1.84e-01 +1.87e-01 +1.63e-01 +1.49e-01 +1.50e-01 +6.42e-02 +1.91e-03 +1.93e-01 +2.15e-01 +1.93e-01 +1.42e-01 +1.22e-01 +7.21e-03 +9.37e-04 +1.67e-02 +5.33e-02 +5.24e-02 +1.13e-02 +1.23e-02 +4.44e-03 +6.14e-04 +7.25e-04 +6.85e-04 +8.30e-04 +9.75e-04 +6.78e-04 +5.52e-04 +5.29e-04 +2.82e-01 +2.73e-01 +2.85e-01 +2.57e-01 +2.70e-01 +2.24e-02 +2.57e-03 +2.52e-01 +2.49e-01 +2.38e-01 +2.53e-01 +2.37e-01 +1.48e-02 +2.30e-03 +2.18e-01 +2.22e-01 +2.20e-01 +1.96e-01 +1.80e-01 +1.93e-02 +1.53e-03 +2.21e-01 +2.39e-01 +1.99e-01 +1.67e-01 +1.45e-01 +1.11e-02 +8.58e-04 +2.57e-01 +2.32e-01 +1.89e-01 +1.26e-01 +8.31e-02 +8.12e-03 +6.25e-04 +8.53e-02 +7.87e-02 +1.53e-02 +1.22e-02 +7.53e-03 +1.75e-03 +3.39e-04 +1.08e-03 +8.97e-04 +6.11e-04 +5.24e-04 +4.33e-04 +2.49e-04 +2.36e-04 +4.78e-01 +1.38e-01 +5.00e-01 +2.99e-02 +3.12e-02 +1.07e-01 +1.42e-03 +3.01e-01 +4.37e-02 +1.79e-01 +2.39e-01 +7.54e-02 +1.13e-02 +1.17e-03 +3.44e-01 +2.30e-01 +5.55e-02 +5.69e-02 +1.70e-02 +8.02e-03 +8.18e-04 +1.77e-01 +6.57e-02 +6.14e-02 +3.20e-02 +1.32e-02 +6.75e-03 +6.17e-04 +3.49e-02 +1.65e-02 +1.95e-02 +8.71e-03 +9.02e-03 +2.07e-03 +3.41e-04 +7.97e-02 +2.60e-02 +1.65e-02 +6.67e-03 +2.61e-03 +5.74e-04 +1.41e-04 +1.62e-03 +1.49e-03 +8.36e-04 +3.95e-04 +2.37e-04 +1.32e-04 +5.59e-05 +1.07e-03 +9.93e-04 +1.63e-03 +6.02e-03 +1.30e-03 +2.86e-03 +2.98e-03 +1.02e-03 +1.31e-03 +1.39e-03 +2.62e-03 +1.69e-03 +2.40e-03 +3.96e-03 +1.26e-03 +1.78e-03 +1.30e-03 +1.06e-03 +1.79e-03 +1.40e-03 +3.18e-03 +1.93e-03 +1.72e-03 +1.78e-03 +8.41e-04 +8.66e-04 +8.00e-04 +1.01e-03 +1.54e-03 +1.30e-03 +1.11e-03 +9.80e-04 +5.02e-04 +3.11e-04 +3.49e-04 +1.24e-03 +1.66e-03 +8.20e-04 +5.29e-04 +3.62e-04 +1.27e-04 +6.03e-05 +6.28e-04 +6.20e-04 +5.01e-04 +4.28e-04 +2.95e-04 +5.38e-05 +6.24e-06 +Upper Bounds +7.61e-01 +7.65e-01 +8.88e-01 +9.87e-01 +1.21e+00 +1.97e+00 +1.13e-02 +7.17e-01 +7.88e-01 +8.74e-01 +9.32e-01 +1.03e+00 +2.41e-01 +1.06e-02 +6.53e-01 +6.86e-01 +8.03e-01 +8.82e-01 +1.02e+00 +5.77e-01 +2.52e-02 +5.23e-01 +6.53e-01 +7.46e-01 +8.58e-01 +9.06e-01 +2.12e-01 +2.66e-02 +3.40e-01 +4.99e-01 +7.82e-01 +8.41e-01 +8.89e-01 +6.94e-02 +2.75e-02 +1.34e-02 +3.33e-02 +1.18e-01 +8.26e-02 +8.54e-02 +5.62e-02 +9.71e-03 +5.62e-04 +2.72e-03 +6.16e-03 +7.54e-03 +7.55e-03 +6.53e-03 +6.05e-03 +7.58e-01 +7.99e-01 +9.04e-01 +1.01e+00 +1.08e+00 +1.29e-01 +1.03e-02 +7.25e-01 +7.95e-01 +9.18e-01 +1.03e+00 +1.07e+00 +4.41e-01 +1.46e-02 +6.80e-01 +7.67e-01 +8.04e-01 +9.61e-01 +1.14e+00 +9.09e-01 +1.35e-02 +5.92e-01 +7.08e-01 +7.60e-01 +9.05e-01 +9.92e-01 +1.43e-01 +3.23e-02 +5.02e-01 +7.06e-01 +9.01e-01 +8.64e-01 +1.02e+00 +5.44e-01 +1.64e-02 +2.68e-02 +7.22e-02 +2.27e-01 +1.08e-01 +3.77e-01 +1.19e-01 +6.91e-03 +2.26e-03 +5.54e-03 +6.95e-03 +6.66e-03 +6.32e-03 +6.08e-03 +4.89e-03 +9.15e-01 +8.87e-01 +9.13e-01 +1.02e+00 +1.13e+00 +3.85e-01 +1.78e-02 +8.13e-01 +9.00e-01 +9.37e-01 +9.28e-01 +1.10e+00 +2.22e-01 +9.99e-03 +8.40e-01 +8.02e-01 +8.17e-01 +8.90e-01 +1.06e+00 +4.93e-01 +1.48e-02 +7.81e-01 +7.93e-01 +8.16e-01 +8.61e-01 +9.05e-01 +3.58e-01 +8.84e-03 +7.91e-01 +9.00e-01 +9.34e-01 +8.31e-01 +8.31e-01 +6.09e-02 +1.33e-02 +1.06e-01 +1.50e-01 +2.43e-01 +7.38e-02 +2.33e-01 +2.69e-02 +5.07e-03 +4.05e-03 +4.28e-03 +7.13e-03 +8.17e-03 +5.26e-03 +5.20e-03 +3.42e-03 +1.01e+00 +1.09e+00 +1.13e+00 +1.18e+00 +1.22e+00 +1.62e-01 +1.34e-01 +1.05e+00 +1.04e+00 +1.03e+00 +1.01e+00 +1.07e+00 +1.21e-01 +1.49e-02 +1.00e+00 +9.68e-01 +9.64e-01 +9.01e-01 +8.62e-01 +6.59e-02 +7.71e-03 +9.19e-01 +9.37e-01 +8.13e-01 +7.43e-01 +7.51e-01 +3.21e-01 +9.55e-03 +9.63e-01 +1.07e+00 +9.67e-01 +7.12e-01 +6.10e-01 +3.60e-02 +4.68e-03 +8.33e-02 +2.66e-01 +2.62e-01 +5.67e-02 +6.16e-02 +2.22e-02 +3.07e-03 +3.63e-03 +3.42e-03 +4.15e-03 +4.88e-03 +3.39e-03 +2.76e-03 +2.65e-03 +1.41e+00 +1.36e+00 +1.42e+00 +1.28e+00 +1.35e+00 +1.12e-01 +1.28e-02 +1.26e+00 +1.24e+00 +1.19e+00 +1.26e+00 +1.18e+00 +7.42e-02 +1.15e-02 +1.09e+00 +1.11e+00 +1.10e+00 +9.79e-01 +9.01e-01 +9.64e-02 +7.63e-03 +1.11e+00 +1.19e+00 +9.95e-01 +8.35e-01 +7.23e-01 +5.53e-02 +4.29e-03 +1.28e+00 +1.16e+00 +9.47e-01 +6.30e-01 +4.16e-01 +4.06e-02 +3.13e-03 +4.27e-01 +3.94e-01 +7.67e-02 +6.12e-02 +3.77e-02 +8.74e-03 +1.70e-03 +5.41e-03 +4.48e-03 +3.06e-03 +2.62e-03 +2.16e-03 +1.25e-03 +1.18e-03 +2.39e+00 +6.90e-01 +2.50e+00 +1.50e-01 +1.56e-01 +5.36e-01 +7.11e-03 +1.50e+00 +2.18e-01 +8.93e-01 +1.20e+00 +3.77e-01 +5.66e-02 +5.84e-03 +1.72e+00 +1.15e+00 +2.77e-01 +2.84e-01 +8.51e-02 +4.01e-02 +4.09e-03 +8.84e-01 +3.28e-01 +3.07e-01 +1.60e-01 +6.60e-02 +3.37e-02 +3.08e-03 +1.74e-01 +8.27e-02 +9.77e-02 +4.36e-02 +4.51e-02 +1.03e-02 +1.71e-03 +3.98e-01 +1.30e-01 +8.25e-02 +3.33e-02 +1.30e-02 +2.87e-03 +7.05e-04 +8.08e-03 +7.43e-03 +4.18e-03 +1.97e-03 +1.18e-03 +6.58e-04 +2.80e-04 +5.36e-03 +4.96e-03 +8.16e-03 +3.01e-02 +6.48e-03 +1.43e-02 +1.49e-02 +5.08e-03 +6.57e-03 +6.95e-03 +1.31e-02 +8.44e-03 +1.20e-02 +1.98e-02 +6.30e-03 +8.90e-03 +6.49e-03 +5.28e-03 +8.94e-03 +7.00e-03 +1.59e-02 +9.64e-03 +8.62e-03 +8.88e-03 +4.21e-03 +4.33e-03 +4.00e-03 +5.04e-03 +7.71e-03 +6.51e-03 +5.56e-03 +4.90e-03 +2.51e-03 +1.55e-03 +1.75e-03 +6.19e-03 +8.31e-03 +4.10e-03 +2.65e-03 +1.81e-03 +6.34e-04 +3.01e-04 +3.14e-03 +3.10e-03 +2.51e-03 +2.14e-03 +1.47e-03 +2.69e-04 +3.12e-05 \ No newline at end of file diff --git a/tests/regression_tests/weightwindows_fw_cadis_local/test.py b/tests/regression_tests/weightwindows_fw_cadis_local/test.py new file mode 100644 index 000000000..abd3f2098 --- /dev/null +++ b/tests/regression_tests/weightwindows_fw_cadis_local/test.py @@ -0,0 +1,42 @@ +import os + +import openmc +from openmc.examples import random_ray_three_region_cube_with_detectors + +from tests.testing_harness import WeightWindowPyAPITestHarness + + +class MGXSTestHarness(WeightWindowPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + + +def test_weight_windows_fw_cadis_local(): + model = random_ray_three_region_cube_with_detectors() + + for tally in list(model.tallies): + if tally.name in {"Source Tally", "Absorber Tally", "Cavity Tally"}: + # leave only the tallies of interest + model.tallies.remove(tally) + + ww_mesh = openmc.RegularMesh() + n = 7 + width = 35.0 + ww_mesh.dimension = (n, n, n) + ww_mesh.lower_left = (0.0, 0.0, 0.0) + ww_mesh.upper_right = (width, width, width) + + wwg = openmc.WeightWindowGenerator( + method="fw_cadis", + targets=model.tallies, + mesh=ww_mesh, + max_realizations=model.settings.batches + ) + model.settings.weight_window_generators = wwg + model.settings.random_ray['volume_estimator'] = 'naive' + + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/inputs_true.dat b/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/inputs_true.dat index ceb89e6e3..a0d84257a 100644 --- a/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/inputs_true.dat +++ b/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/inputs_true.dat @@ -222,11 +222,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true diff --git a/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/inputs_true.dat b/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/inputs_true.dat index c7691e950..62f847858 100644 --- a/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/inputs_true.dat +++ b/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/inputs_true.dat @@ -222,11 +222,13 @@ 500.0 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - + + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + true From e542b2f035e397a647cc3ad0ae39946b1ad81e7b Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Wed, 6 May 2026 16:07:35 +0100 Subject: [PATCH 618/671] Allow Mesh.volumes property for 1D and 2D RegularMesh (#3914) --- openmc/mesh.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openmc/mesh.py b/openmc/mesh.py index 3c3c0a1ac..4129913d8 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -268,10 +268,11 @@ class MeshBase(IDManagerMixin, ABC): return string def _volume_dim_check(self): - if self.n_dimension != 3 or \ - any([d == 0 for d in self.dimension]): - raise RuntimeError(f'Mesh {self.id} is not 3D. ' - 'Volumes cannot be provided.') + if any(d == 0 for d in self.dimension): + raise RuntimeError( + f'Mesh {self.id} has a zero-size dimension. ' + 'Volumes cannot be provided.' + ) @classmethod def from_hdf5(cls, group: h5py.Group): From f3e1066d467a3c12a9c964b624608910bc2121ad Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 7 May 2026 18:50:23 -0500 Subject: [PATCH 619/671] Include mass_attenuation.h5 in package data (#3936) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bf4011344..098487c06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,7 +69,7 @@ include = ['openmc*'] exclude = ['tests*'] [tool.setuptools.package-data] -"openmc.data.dose" = ["**/*.txt"] +"openmc.data.dose" = ["**/*.txt", "*.h5"] "openmc.data" = ["*.txt", "*.DAT", "*.json", "*.h5"] "openmc.lib" = ["libopenmc.dylib", "libopenmc.so"] From d56cda254408971c3f0a79736c46b080ca37f1b7 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 8 May 2026 20:53:12 -0500 Subject: [PATCH 620/671] Implement DecaySpectrum distribution type and utilize in R2S (#3930) Co-authored-by: Copilot --- docs/source/io_formats/settings.rst | 39 +++- docs/source/pythonapi/stats.rst | 1 + include/openmc/chain.h | 2 + include/openmc/distribution.h | 65 ++++++ openmc/deplete/r2s.py | 244 ++++++++++++---------- openmc/stats/univariate.py | 309 ++++++++++++++++++++++++++++ src/chain.cpp | 8 + src/distribution.cpp | 124 +++++++++++ src/finalize.cpp | 2 + src/initialize.cpp | 14 +- src/source.cpp | 30 ++- tests/unit_tests/test_source.py | 69 +++++++ tests/unit_tests/test_stats.py | 135 +++++++++++- 13 files changed, 908 insertions(+), 134 deletions(-) diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index d63376e2b..f8b064a37 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -1058,17 +1058,19 @@ variable and whose sub-elements/attributes are as follows: :type: The type of the distribution. Valid options are "uniform", "discrete", - "tabular", "maxwell", "watt", and "mixture". The "uniform" option produces - variates sampled from a uniform distribution over a finite interval. The - "discrete" option produces random variates that can assume a finite number - of values (i.e., a distribution characterized by a probability mass function). - The "tabular" option produces random variates sampled from a tabulated - distribution where the density function is either a histogram or + "tabular", "maxwell", "watt", "mixture", and "decay_spectrum". The "uniform" + option produces variates sampled from a uniform distribution over a finite + interval. The "discrete" option produces random variates that can assume a + finite number of values (i.e., a distribution characterized by a probability + mass function). The "tabular" option produces random variates sampled from a + tabulated distribution where the density function is either a histogram or linearly-interpolated between tabulated points. The "watt" option produces random variates is sampled from a Watt fission spectrum (only used for energies). The "maxwell" option produce variates sampled from a Maxwell - fission spectrum (only used for energies). The "mixture" option produces samples - from univariate sub-distributions with given probabilities. + fission spectrum (only used for energies). The "mixture" option produces + samples from univariate sub-distributions with given probabilities. The + "decay_spectrum" option produces photon energies sampled from decay photon + spectra in a depletion chain (only used for energies). *Default*: None @@ -1086,6 +1088,10 @@ variable and whose sub-elements/attributes are as follows: :math:`(x,p)` pairs defining the discrete/tabular distribution. All :math:`x` points are given first followed by corresponding :math:`p` points. + For a "decay_spectrum" distribution, ``parameters`` gives the atom densities + in [atom/b-cm] for the nuclides listed in the ``nuclides`` element, in the + same order. + For a "watt" distribution, ``parameters`` should be given as two real numbers :math:`a` and :math:`b` that parameterize the distribution :math:`p(x) dx = c e^{-x/a} \sinh \sqrt{b \, x} dx`. @@ -1115,6 +1121,21 @@ variable and whose sub-elements/attributes are as follows: This sub-element of a ``pair`` element provides information on the corresponding univariate distribution. +:volume: + For a "decay_spectrum" distribution, this attribute specifies the source + region volume in cm\ :sup:`3`. It is used together with atom densities to + determine the absolute photon emission rate. When a source uses a + "decay_spectrum" energy distribution, the source strength is set from this + emission rate. + +:nuclides: + For a "decay_spectrum" distribution, this element specifies a + whitespace-separated list of nuclide names contributing to the decay photon + source. The atom densities for these nuclides are given by the ``parameters`` + element in the same order. Nuclides are resolved against the depletion chain, + and nuclides without decay photon spectra do not contribute to the + distribution. + :bias: This optional element specifies a biased distribution for importance sampling. For continuous distributions, the ``bias`` element should contain another @@ -1300,7 +1321,7 @@ The ```` element specifies the surface flux cosine cutof ```` Element ----------------------------------- -The ```` element specifies the surface flux cosine +The ```` element specifies the surface flux cosine substitution ratio. *Default*: 0.5 diff --git a/docs/source/pythonapi/stats.rst b/docs/source/pythonapi/stats.rst index 203d4fea4..2f2e2835a 100644 --- a/docs/source/pythonapi/stats.rst +++ b/docs/source/pythonapi/stats.rst @@ -22,6 +22,7 @@ Univariate Probability Distributions openmc.stats.Legendre openmc.stats.Mixture openmc.stats.Normal + openmc.stats.DecaySpectrum .. autosummary:: :toctree: generated diff --git a/include/openmc/chain.h b/include/openmc/chain.h index 6f5830358..e01f2a01c 100644 --- a/include/openmc/chain.h +++ b/include/openmc/chain.h @@ -101,6 +101,8 @@ extern vector> chain_nuclides; void read_chain_file_xml(); +void free_memory_chain(); + } // namespace openmc #endif // OPENMC_CHAIN_H diff --git a/include/openmc/distribution.h b/include/openmc/distribution.h index cd81d8801..f319dd19d 100644 --- a/include/openmc/distribution.h +++ b/include/openmc/distribution.h @@ -407,6 +407,71 @@ private: double integral_; //!< Integral of distribution }; +//============================================================================== +// DecaySpectrum — non-owning mixture of decay photon distributions +//============================================================================== + +//! Energy distribution formed by mixing multiple decay photon spectra. +//! +//! Unlike the general Mixture distribution, this class holds non-owning +//! pointers to the component distributions (which live in +//! data::chain_nuclides). Each component is weighted by the activity +//! (atoms * decay_constant) of the corresponding nuclide. + +class DecaySpectrum : public Distribution { +public: + //============================================================================ + // Types, aliases + + struct Sample { + double energy; + double weight; + int parent_nuclide; + }; + + //============================================================================ + // Constructors + + //! Construct from an XML node containing nuclide names and atom densities. + //! + //! Reads child ```` elements with ``name`` and ``density`` + //! attributes, resolves them against the loaded depletion chain, and + //! constructs the mixed distribution. + explicit DecaySpectrum(pugi::xml_node node); + + //============================================================================ + // Methods + + //! Sample a value from the distribution and return the parent nuclide index + //! \param seed Pseudorandom number seed pointer + //! \return (Sampled energy, sample weight, chain nuclide index) + Sample sample_with_parent(uint64_t* seed) const; + + //! Sample a value from the distribution + //! \param seed Pseudorandom number seed pointer + //! \return (sampled value, sample weight) + std::pair sample(uint64_t* seed) const override; + + double integral() const override; + +protected: + //! Sample a value (unbiased) from the distribution + //! \param seed Pseudorandom number seed pointer + //! \return Sampled value + double sample_unbiased(uint64_t* seed) const override; + +private: + //! Initialize decay spectrum sampling data + //! \param nuclide_indices Indices of decay photon emitters in + //! data::chain_nuclides + //! \param atoms Number of atoms for each component. + void init(vector nuclide_indices, const vector& atoms); + + vector nuclide_indices_; //!< Indices of emitting nuclides in the chain + DiscreteIndex di_; //!< Discrete index for component selection + double integral_; //!< Total photon emission rate +}; + } // namespace openmc #endif // OPENMC_DISTRIBUTION_H diff --git a/openmc/deplete/r2s.py b/openmc/deplete/r2s.py index 10d7fdd50..6dbb3adb2 100644 --- a/openmc/deplete/r2s.py +++ b/openmc/deplete/r2s.py @@ -1,5 +1,6 @@ from __future__ import annotations from collections.abc import Sequence +from contextlib import nullcontext import copy from datetime import datetime import json @@ -8,6 +9,7 @@ from pathlib import Path import numpy as np import openmc from . import IndependentOperator, PredictorIntegrator +from .chain import Chain from .microxs import get_microxs_and_flux, write_microxs_hdf5, read_microxs_hdf5 from .results import Results from ..checkvalue import PathLike @@ -149,7 +151,7 @@ class R2SManager: photon_time_indices: Sequence[int] | None = None, output_dir: PathLike | None = None, bounding_boxes: dict[int, openmc.BoundingBox] | None = None, - chain_file: PathLike | None = None, + chain_file: PathLike | Chain | None = None, micro_kwargs: dict | None = None, mat_vol_kwargs: dict | None = None, run_kwargs: dict | None = None, @@ -187,9 +189,10 @@ class R2SManager: Dictionary mapping cell IDs to bounding boxes used for spatial source sampling in cell-based R2S calculations. Required if method is 'cell-based'. - chain_file : PathLike, optional - Path to the depletion chain XML file to use during activation. If - not provided, the default configured chain file will be used. + chain_file : PathLike or openmc.deplete.Chain, optional + Path to the depletion chain XML file or depletion chain object to + use during activation. If not provided, the default configured + chain file will be used. micro_kwargs : dict, optional Additional keyword arguments passed to :func:`openmc.deplete.get_microxs_and_flux` during the neutron @@ -216,6 +219,8 @@ class R2SManager: # consistency (different ranks may have slightly different times) stamp = datetime.now().strftime('%Y-%m-%dT%H-%M-%S') output_dir = Path(comm.bcast(f'r2s_{stamp}')) + else: + output_dir = Path(output_dir) # Set run_kwargs for the neutron transport step if micro_kwargs is None: @@ -226,22 +231,35 @@ class R2SManager: operator_kwargs = {} run_kwargs.setdefault('output', False) micro_kwargs.setdefault('run_kwargs', run_kwargs) - # If a chain file is provided, prefer it for steps 1 and 2 - if chain_file is not None: - micro_kwargs.setdefault('chain_file', chain_file) - operator_kwargs.setdefault('chain_file', chain_file) - self.step1_neutron_transport( - output_dir / 'neutron_transport', mat_vol_kwargs, micro_kwargs - ) - self.step2_activation( - timesteps, source_rates, timestep_units, output_dir / 'activation', - operator_kwargs=operator_kwargs - ) - self.step3_photon_transport( - photon_time_indices, bounding_boxes, output_dir / 'photon_transport', - mat_vol_kwargs=mat_vol_kwargs, run_kwargs=run_kwargs + # DecaySpectrum distributions are resolved in the C++ solver using + # OPENMC_CHAIN_FILE. If a Chain object was passed, write an XML + # representation alongside the R2S outputs. + if isinstance(chain_file, Chain): + output_dir.mkdir(parents=True, exist_ok=True) + chain_path = output_dir / 'chain.xml' + if comm.rank == 0: + chain_file.export_to_xml(chain_path) + comm.barrier() + else: + chain_path = chain_file + + chain_context = ( + openmc.config.patch('chain_file', chain_path) + if chain_path is not None else nullcontext() ) + with chain_context: + self.step1_neutron_transport( + output_dir / 'neutron_transport', mat_vol_kwargs, micro_kwargs + ) + self.step2_activation( + timesteps, source_rates, timestep_units, + output_dir / 'activation', operator_kwargs=operator_kwargs + ) + self.step3_photon_transport( + photon_time_indices, bounding_boxes, output_dir / 'photon_transport', + mat_vol_kwargs=mat_vol_kwargs, run_kwargs=run_kwargs + ) return output_dir @@ -516,45 +534,30 @@ class R2SManager: if different_photon_model: photon_cells = self.photon_model.geometry.get_all_cells() - for time_index in time_indices: - # Create decay photon source - if self.method == 'mesh-based': - self.photon_model.settings.source = \ - self.get_decay_photon_source_mesh(time_index) - else: - sources = [] - results = self.results['depletion_results'] - for cell, original_mat in zip(self.domains, self.results['activation_materials']): - # Skip if the cell is not in the photon model or the - # material has changed - if different_photon_model: - if cell.id not in photon_cells or \ + # Determine eligible work items upfront (independent of time index). + if self.method == 'mesh-based': + work_items = self._get_mesh_work_items() + else: + work_items = [] + for cell, original_mat in zip( + self.domains, self.results['activation_materials']): + if different_photon_model: + if cell.id not in photon_cells or \ cell.fill.id != photon_cells[cell.id].fill.id: - continue + continue + work_items.append((cell, original_mat, bounding_boxes[cell.id])) - # Get bounding box for the cell - bounding_box = bounding_boxes[cell.id] - - # Get activated material composition - activated_mat = results[time_index].get_material(str(original_mat.id)) - - # Create decay photon source source - space = openmc.stats.Box(*bounding_box) - energy = activated_mat.get_decay_photon_energy() - strength = energy.integral() if energy is not None else 0.0 - source = openmc.IndependentSource( - space=space, - energy=energy, - particle='photon', - strength=strength, - constraints={'domains': [cell]} - ) - sources.append(source) - self.photon_model.settings.source = sources + # Ensure photon transport is enabled in settings + self.photon_model.settings.photon_transport = True + for time_index in time_indices: # Convert time_index (which may be negative) to a normal index if time_index < 0: - time_index = len(self.results['depletion_results']) + time_index + time_index += len(self.results['depletion_results']) + + # Build decay photon sources and assign to the photon model + sources = self._create_photon_sources(time_index, work_items) + self.photon_model.settings.source = sources # Run photon transport calculation photon_dir = Path(output_dir) / f'time_{time_index}' @@ -567,58 +570,30 @@ class R2SManager: sp.tallies[tally.id] for tally in self.photon_model.tallies ] - def get_decay_photon_source_mesh( - self, - time_index: int = -1 - ) -> list[openmc.IndependentSource]: - """Create decay photon source for a mesh-based calculation. + def _get_mesh_work_items(self): + """Enumerate mesh-based work items across all meshes. - For each mesh element-material combination across all meshes, an - :class:`~openmc.IndependentSource` is created with a - :class:`~openmc.stats.Box` spatial distribution based on the bounding - box of the material within the mesh element. A material constraint is - also applied so that sampled source sites are limited to the correct - region. - - When the photon transport model is different from the neutron model, the - photon MeshMaterialVolumes is used to determine whether an (element, - material) combination exists in the photon model. - - Parameters - ---------- - time_index : int, optional - Time index for the decay photon source. Default is -1 (last time). + Returns a list of (index_mat, mat_id, bbox) tuples for each eligible + mesh element--material combination, where index_mat is the index into + the activation materials list, mat_id is the material ID, and bbox is + the bounding box for that mesh element--material combination. Returns ------- - list of openmc.IndependentSource - A list of IndependentSource objects for the decay photons, one for - each mesh element-material combination with non-zero source strength. - + list of tuple + Each tuple is (index_mat, mat_id, bbox). """ - mat_dict = self.neutron_model._get_all_materials() - - # List to hold all sources - sources = [] - - # Index in the overall list of activated materials - index_mat = 0 - - # Get various results from previous steps mmv_list = self.results['mesh_material_volumes'] - materials = self.results['activation_materials'] - results = self.results['depletion_results'] photon_mmv_list = self.results.get('mesh_material_volumes_photon') + work_items = [] + index_mat = 0 for mesh_idx, mat_vols in enumerate(mmv_list): photon_mat_vols = photon_mmv_list[mesh_idx] \ if photon_mmv_list is not None else None - # Total number of mesh elements for this mesh n_elements = mat_vols.num_elements - for index_elem in range(n_elements): - # Determine which materials exist in the photon model for this element if photon_mat_vols is not None: photon_materials = { mat_id @@ -626,36 +601,77 @@ class R2SManager: if mat_id is not None } - for mat_id, _, bbox in mat_vols.by_element(index_elem, include_bboxes=True): - # Skip void volume + for mat_id, _, bbox in mat_vols.by_element( + index_elem, include_bboxes=True): if mat_id is None: continue - - # Skip if this material doesn't exist in photon model - if photon_mat_vols is not None and mat_id not in photon_materials: + if photon_mat_vols is not None \ + and mat_id not in photon_materials: index_mat += 1 continue - - # Get activated material composition - original_mat = materials[index_mat] - activated_mat = results[time_index].get_material(str(original_mat.id)) - - # Create decay photon source - energy = activated_mat.get_decay_photon_energy() - if energy is not None: - strength = energy.integral() - space = openmc.stats.Box(*bbox) - sources.append(openmc.IndependentSource( - space=space, - energy=energy, - particle='photon', - strength=strength, - constraints={'domains': [mat_dict[mat_id]]} - )) - - # Increment index of activated material + work_items.append((index_mat, mat_id, bbox)) index_mat += 1 + return work_items + + def _create_photon_sources(self, time_index, work_items): + """Create decay photon sources for a set of regions. + + Builds :class:`openmc.IndependentSource` objects with + :class:`openmc.stats.DecaySpectrum` energy distributions that will be + serialized to XML and resolved against the depletion chain by the C++ + solver. + + Parameters + ---------- + time_index : int + Index into depletion results. + work_items : list of tuple + For mesh-based: list of (index_mat, mat_id, bbox). + For cell-based: list of (cell, original_mat, bbox). + + Returns + ------- + list of openmc.IndependentSource + Photon sources for each activated region. + """ + step_result = self.results['depletion_results'][time_index] + materials = self.results['activation_materials'] + mesh_based = self.method == 'mesh-based' + if mesh_based: + mat_dict = self.neutron_model._get_all_materials() + + sources = [] + for item in work_items: + if mesh_based: + index_mat, domain_id, bbox = item + original_mat = materials[index_mat] + domain = mat_dict[domain_id] + else: + cell, original_mat, bbox = item + domain = cell + + activated_mat = step_result.get_material(str(original_mat.id)) + nuclides = activated_mat.get_nuclide_atom_densities() + if not nuclides: + continue + + # Eliminate nuclides with zero density + nuclides = {nuclide: density for nuclide, density in nuclides.items() + if density > 0} + + energy = openmc.stats.DecaySpectrum(nuclides, activated_mat.volume) + energy.clip(inplace=True) + if not energy.nuclides: + continue + + sources.append(openmc.IndependentSource( + space=openmc.stats.Box(bbox.lower_left, bbox.upper_right), + energy=energy, + particle='photon', + constraints={'domains': [domain]}, + )) + return sources def load_results(self, path: PathLike): diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index 1cf9a1ad5..1b18bcb17 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -2,8 +2,10 @@ from __future__ import annotations from abc import ABC, abstractmethod from collections import defaultdict from collections.abc import Iterable, Sequence +from functools import cache from math import sqrt, pi, exp, log from numbers import Real +from pathlib import Path from warnings import warn import lxml.etree as ET @@ -14,6 +16,7 @@ import scipy import openmc.checkvalue as cv from openmc.data import atomic_mass, NEUTRON_MASS +import openmc.data from .._xml import get_elem_list, get_text from ..mixin import EqualityMixin @@ -124,6 +127,8 @@ class Univariate(EqualityMixin, ABC): return Legendre.from_xml_element(elem) elif distribution == 'mixture': return Mixture.from_xml_element(elem) + elif distribution == 'decay_spectrum': + return DecaySpectrum.from_xml_element(elem) @abstractmethod def _sample_unbiased(self, n_samples: int = 1, seed: int | None = None): @@ -2196,6 +2201,310 @@ class Mixture(Univariate): return new_dist +class DecaySpectrum(Univariate): + """Energy distribution from decay photon spectra of a mixture of nuclides. + + This distribution stores nuclide names, their atom densities, and the volume + of the region. When written to XML and read by the C++ solver, the nuclide + names are resolved against the depletion chain to obtain the decay photon + energy spectra and decay constants. The resulting distribution is a mixture + of per-nuclide photon spectra weighted by absolute activity. The volume is + necessary so that the C++ solver can compute the total photon emission rate + in [photons/s], which is used as the source strength. + + .. versionadded:: 0.15.4 + + Parameters + ---------- + nuclides : dict + Dictionary mapping nuclide name (str) to atom density (float) in units + of [atom/b-cm]. + volume : float + Volume of the source region in [cm³]. Used together with atom densities + to compute the absolute photon emission rate. + + Attributes + ---------- + nuclides : dict + Dictionary mapping nuclide name to atom density in [atom/b-cm]. + volume : float + Volume of the source region in [cm³]. + + """ + + def __init__(self, nuclides: dict[str, float], volume: float): + super().__init__(bias=None) + self._dist_cache = None + self._dist_cache_key = None + self.nuclides = nuclides + self.volume = volume + + def __len__(self): + return len(self.nuclides) + + @property + def nuclides(self): + return self._nuclides + + @nuclides.setter + def nuclides(self, nuclides): + cv.check_type('nuclides', nuclides, dict) + for name, density in nuclides.items(): + cv.check_type('nuclide name', name, str) + cv.check_type(f'atom density for {name}', density, Real) + cv.check_greater_than(f'atom density for {name}', density, 0.0, True) + self._nuclides = dict(nuclides) + self._dist_cache = None + self._dist_cache_key = None + + @property + def volume(self): + return self._volume + + @volume.setter + def volume(self, volume): + cv.check_type('volume', volume, Real) + cv.check_greater_than('volume', volume, 0.0) + self._volume = float(volume) + self._dist_cache = None + self._dist_cache_key = None + + @staticmethod + def _chain_file_cache_key(): + """Return a hashable key for the active depletion chain.""" + chain_file = openmc.config.get('chain_file') + if chain_file is None: + return None + + path = Path(chain_file).resolve() + try: + stat = path.stat() + except OSError: + return (path, None, None) + return (path, stat.st_mtime, stat.st_size) + + def to_distribution(self): + """Convert to a concrete distribution using decay chain data. + + Builds a combined photon energy distribution by looking up each nuclide + in the depletion chain via :func:`openmc.data.decay_photon_energy` and + weighting by absolute atom count (``density * 1e24 * volume``). The + result is cached on the object; the cache is invalidated automatically + when :attr:`nuclides` or :attr:`volume` are reassigned. + + Requires ``openmc.config['chain_file']`` to be set. + + Returns + ------- + openmc.stats.Univariate or None + Combined photon energy distribution, or ``None`` if no nuclide in + :attr:`nuclides` has a photon source in the chain. + + """ + chain_key = self._chain_file_cache_key() + if self._dist_cache is not None and self._dist_cache_key == chain_key: + return self._dist_cache + + dists = [] + weights = [] + for name, density in self.nuclides.items(): + dist = openmc.data.decay_photon_energy(name) + if dist is not None: + dists.append(dist) + weights.append(density * 1e24 * self.volume) + + if not dists: + return None + + self._dist_cache = combine_distributions(dists, weights) + self._dist_cache_key = chain_key + return self._dist_cache + + def to_xml_element(self, element_name: str): + """Return XML representation of the decay photon distribution + + Parameters + ---------- + element_name : str + XML element name + + Returns + ------- + element : lxml.etree._Element + XML element containing decay photon distribution data + + """ + element = ET.Element(element_name) + element.set("type", "decay_spectrum") + element.set("volume", str(self.volume)) + nuclides = ET.SubElement(element, "nuclides") + nuclides.text = ' '.join(self.nuclides) + parameters = ET.SubElement(element, "parameters") + parameters.text = ' '.join(str(density) for density in self.nuclides.values()) + return element + + @classmethod + def from_xml_element(cls, elem: ET.Element): + """Generate decay photon distribution from an XML element + + Parameters + ---------- + elem : lxml.etree._Element + XML element + + Returns + ------- + openmc.stats.DecaySpectrum + Decay photon distribution generated from XML element + + """ + volume = float(elem.get('volume')) + names = get_elem_list(elem, 'nuclides', str) + densities = get_elem_list(elem, 'parameters', float) + nuclides = dict(zip(names, densities)) + return cls(nuclides, volume) + + def _sample_unbiased(self, n_samples=1, seed=None): + dist = self.to_distribution() + if dist is None: + raise RuntimeError( + "DecaySpectrum._sample_unbiased requires chain data but none " + "was found. Ensure openmc.config['chain_file'] is set and the " + "chain contains photon sources for the nuclides present." + ) + return dist.sample(n_samples, seed)[0] + + def integral(self): + """Return integral of the distribution + + Returns the total photon emission rate in [photons/s] by delegating to + :meth:`to_distribution`. Returns ``0.0`` when no chain data is + available (e.g., ``openmc.config['chain_file']`` is not set). + + Returns + ------- + float + Total photon emission rate in [photons/s], or ``0.0`` if chain + data is unavailable. + """ + try: + dist = self.to_distribution() + except Exception: + return 0.0 + if dist is None: + return 0.0 + return dist.integral() + + @staticmethod + @cache + def _photon_integral(nuclide: str, chain_key) -> float | None: + """Return the per-atom photon emission integral for a nuclide""" + dist = openmc.data.decay_photon_energy(nuclide) + return dist.integral() if dist is not None else None + + def clip(self, tolerance: float = 1e-9, inplace: bool = False): + """Remove nuclides with negligible contribution to photon emission. + + Nuclides that are stable or have no photon source in the depletion + chain are removed unconditionally. The remaining nuclides are ranked + by their photon emission rate (proportional to + ``atom_density * decay_constant * photon_yield``) and the least + important are discarded until the cumulative discarded fraction of the + total emission rate exceeds *tolerance*. + + Requires ``openmc.config['chain_file']`` to be set. + + Parameters + ---------- + tolerance : float + Maximum fraction of total photon emission rate that may be + discarded. + inplace : bool + Whether to modify the current object in-place or return a new one. + + Returns + ------- + openmc.stats.DecaySpectrum + Distribution with negligible nuclides removed. + + """ + # Compute per-nuclide emission rate; drop non-emitters + emitting_names = [] + emitting_densities = [] + rates = [] + chain_key = self._chain_file_cache_key() + for name, density in self.nuclides.items(): + integral = DecaySpectrum._photon_integral(name, chain_key) + if integral is None: + continue + emitting_names.append(name) + emitting_densities.append(density) + rates.append(density * self.volume * integral) + + if not emitting_names: + new_nuclides = {} + else: + indices = _intensity_clip(rates, tolerance=tolerance) + new_nuclides = { + emitting_names[i]: emitting_densities[i] for i in indices + } + + if inplace: + self._nuclides = new_nuclides + self._dist_cache = None + self._dist_cache_key = None + return self + return type(self)(new_nuclides, self.volume) + + @property + def support(self): + return (0.0, np.inf) + + def evaluate(self, x): + """Evaluate the probability density at a given value. + + Delegates to the combined distribution built from chain data. Raises + ``NotImplementedError`` if the combined distribution is a + :class:`~openmc.stats.Mixture` (which does not support + ``evaluate()``). + + Parameters + ---------- + x : float + Value at which to evaluate the PDF. + + Returns + ------- + float + Probability density at *x*. + """ + dist = self.to_distribution() + if dist is None: + raise RuntimeError( + "DecaySpectrum.evaluate requires chain data. Ensure " + "openmc.config['chain_file'] is set." + ) + return dist.evaluate(x) + + def mean(self): + """Return the mean of the distribution. + + Delegates to the combined distribution built from chain data. + + Returns + ------- + float + Mean photon energy in [eV]. + """ + dist = self.to_distribution() + if dist is None: + raise RuntimeError( + "DecaySpectrum.mean requires chain data. Ensure " + "openmc.config['chain_file'] is set." + ) + return dist.mean() + + def combine_distributions( dists: Sequence[Discrete | Tabular | Mixture], probs: Sequence[float] diff --git a/src/chain.cpp b/src/chain.cpp index e4d0324d3..3915c016c 100644 --- a/src/chain.cpp +++ b/src/chain.cpp @@ -98,6 +98,8 @@ vector> chain_nuclides; void read_chain_file_xml() { + free_memory_chain(); + char* chain_file_path = std::getenv("OPENMC_CHAIN_FILE"); if (!chain_file_path) { return; @@ -120,4 +122,10 @@ void read_chain_file_xml() } } +void free_memory_chain() +{ + data::chain_nuclides.clear(); + data::chain_nuclide_map.clear(); +} + } // namespace openmc diff --git a/src/distribution.cpp b/src/distribution.cpp index c722d0366..7f5b498ad 100644 --- a/src/distribution.cpp +++ b/src/distribution.cpp @@ -7,7 +7,9 @@ #include // for accumulate #include // for runtime_error #include // for string, stod +#include +#include "openmc/chain.h" #include "openmc/constants.h" #include "openmc/error.h" #include "openmc/math_functions.h" @@ -15,6 +17,10 @@ #include "openmc/random_lcg.h" #include "openmc/xml_interface.h" +namespace { +std::unordered_set decay_spectrum_missing_chain_nuclides; +} + namespace openmc { //============================================================================== @@ -758,6 +764,8 @@ UPtrDist distribution_from_xml(pugi::xml_node node) dist = UPtrDist {new Tabular(node)}; } else if (type == "mixture") { dist = UPtrDist {new Mixture(node)}; + } else if (type == "decay_spectrum") { + dist = UPtrDist {new DecaySpectrum(node)}; } else if (type == "muir") { openmc::fatal_error( "'muir' distributions are now specified using the openmc.stats.muir() " @@ -768,4 +776,120 @@ UPtrDist distribution_from_xml(pugi::xml_node node) return dist; } +//============================================================================== +// DecaySpectrum implementation +//============================================================================== + +DecaySpectrum::DecaySpectrum(pugi::xml_node node) +{ + // Read the region volume [cm^3] needed for absolute emission rate + if (!check_for_node(node, "volume")) + fatal_error("DecaySpectrum: 'volume' attribute is required."); + double volume = std::stod(get_node_value(node, "volume")); + + // Read nuclide names and atom densities from XML + vector nuclide_indices; + vector atoms; + auto names = get_node_array(node, "nuclides"); + auto densities = get_node_array(node, "parameters"); + if (names.size() != densities.size()) { + fatal_error("DecaySpectrum nuclides and parameters must have the same " + "length."); + } + + for (size_t i = 0; i < names.size(); ++i) { + const auto& name = names[i]; + double density = densities[i]; + + // Look up nuclide in the depletion chain + auto it = data::chain_nuclide_map.find(name); + if (it == data::chain_nuclide_map.end()) { + if (decay_spectrum_missing_chain_nuclides.insert(name).second) { + warning("Nuclide '" + name + + "' appears in a DecaySpectrum source but is not present in " + "the depletion chain; it will be ignored."); + } + continue; + } + + int nuclide_index = it->second; + const auto& chain_nuc = data::chain_nuclides[nuclide_index]; + const Distribution* photon_dist = chain_nuc->photon_energy(); + if (!photon_dist) + continue; + + // Skip non-positive densities and warn if negative + if (density <= 0.0) { + if (density < 0.0) { + warning("Nuclide '" + name + + "' has a negative density in a DecaySpectrum source; it will " + "be ignored."); + } + continue; + } + + // atoms = density [atom/b-cm] * 1e24 [b/cm^2] * volume [cm^3] + double atoms_i = density * 1.0e24 * volume; + + nuclide_indices.push_back(nuclide_index); + atoms.push_back(atoms_i); + } + + init(std::move(nuclide_indices), atoms); +} + +void DecaySpectrum::init( + vector nuclide_indices, const vector& atoms) +{ + if (nuclide_indices.size() != atoms.size()) { + fatal_error("DecaySpectrum nuclide index and atoms arrays must have " + "the same length."); + } + + vector probs; + probs.reserve(nuclide_indices.size()); + for (size_t i = 0; i < nuclide_indices.size(); ++i) { + // Distribution integral is in [photons/s/atom]; multiplying by atoms gives + // the total emission rate [photons/s] for this nuclide. + const auto* dist = + data::chain_nuclides[nuclide_indices[i]]->photon_energy(); + probs.push_back(atoms[i] * dist->integral()); + } + + nuclide_indices_ = std::move(nuclide_indices); + integral_ = std::accumulate(probs.begin(), probs.end(), 0.0); + if (nuclide_indices_.empty() || integral_ <= 0.0) { + fatal_error("DecaySpectrum source did not resolve any nuclides with decay " + "photon spectra and positive atom densities. Ensure " + "OPENMC_CHAIN_FILE is set and matches the nuclides in the " + "source definition."); + } + di_.assign(probs); +} + +DecaySpectrum::Sample DecaySpectrum::sample_with_parent(uint64_t* seed) const +{ + size_t idx = di_.sample(seed); + int parent_nuclide = nuclide_indices_[idx]; + const auto* dist = data::chain_nuclides[parent_nuclide]->photon_energy(); + auto [energy, weight] = dist->sample(seed); + return {energy, weight, parent_nuclide}; +} + +std::pair DecaySpectrum::sample(uint64_t* seed) const +{ + auto sample = sample_with_parent(seed); + return {sample.energy, sample.weight}; +} + +double DecaySpectrum::integral() const +{ + return integral_; +} + +double DecaySpectrum::sample_unbiased(uint64_t* seed) const +{ + return sample_with_parent(seed).energy; +} + } // namespace openmc diff --git a/src/finalize.cpp b/src/finalize.cpp index 98f112534..59711dcc3 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -2,6 +2,7 @@ #include "openmc/bank.h" #include "openmc/capi.h" +#include "openmc/chain.h" #include "openmc/cmfd_solver.h" #include "openmc/collision_track.h" #include "openmc/constants.h" @@ -44,6 +45,7 @@ void free_memory() free_memory_photon(); free_memory_settings(); free_memory_thermal(); + free_memory_chain(); library_clear(); nuclides_clear(); free_memory_source(); diff --git a/src/initialize.cpp b/src/initialize.cpp index efb462f5c..991877b24 100644 --- a/src/initialize.cpp +++ b/src/initialize.cpp @@ -405,6 +405,10 @@ bool read_model_xml() write_message( fmt::format("Reading model XML file '{}' ...", model_filename), 5); + // Read chain data before settings so DecaySpectrum source distributions can + // resolve nuclides while sources are constructed. + read_chain_file_xml(); + read_settings_xml(settings_root); // If other XML files are present, display warning @@ -420,9 +424,6 @@ bool read_model_xml() } } - // Read data from chain file - read_chain_file_xml(); - // Read materials and cross sections if (!check_for_node(root, "materials")) { fatal_error(fmt::format( @@ -475,14 +476,15 @@ bool read_model_xml() void read_separate_xml_files() { + // Read chain data before settings so DecaySpectrum source distributions can + // resolve nuclides while sources are constructed. + read_chain_file_xml(); + read_settings_xml(); if (settings::run_mode != RunMode::PLOTTING) { read_cross_sections_xml(); } - // Read data from chain file - read_chain_file_xml(); - read_materials_xml(); read_geometry_xml(); diff --git a/src/source.cpp b/src/source.cpp index 524a72bf0..bee20d5c3 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -354,6 +354,19 @@ IndependentSource::IndependentSource(pugi::xml_node node) : Source(node) if (check_for_node(node, "energy")) { pugi::xml_node node_dist = node.child("energy"); energy_ = distribution_from_xml(node_dist); + + // For decay photon sources, use the absolute photon emission rate in + // [photons/s] as the source strength + if (dynamic_cast(energy_.get())) { + if (strength_ != 1.0) { + warning(fmt::format( + "Source strength of {} is ignored because the source uses a " + "DecaySpectrum energy distribution. The source strength will be " + "set from the DecaySpectrum emission rate.", + strength_)); + } + strength_ = energy_->integral(); + } } else { // Default to a Watt spectrum with parameters 0.988 MeV and 2.249 MeV^-1 energy_ = UPtrDist {new Watt(0.988e6, 2.249e-6)}; @@ -414,6 +427,7 @@ SourceSite IndependentSource::sample(uint64_t* seed) const // Check for monoenergetic source above maximum particle energy auto p = particle_.transport_index(); auto energy_ptr = dynamic_cast(energy_.get()); + auto decay_spectrum = dynamic_cast(energy_.get()); if (energy_ptr) { auto energies = tensor::Tensor(energy_ptr->x().data(), energy_ptr->x().size()); @@ -424,10 +438,18 @@ SourceSite IndependentSource::sample(uint64_t* seed) const } while (true) { - // Sample energy spectrum - auto [E, E_wgt_temp] = energy_->sample(seed); - site.E = E; - E_wgt = E_wgt_temp; + // Sample energy spectrum. For decay photon sources, also get the parent + // nuclide index to store in the source site for tallying purposes. + if (decay_spectrum) { + auto sample = decay_spectrum->sample_with_parent(seed); + site.E = sample.energy; + E_wgt = sample.weight; + site.parent_nuclide = sample.parent_nuclide; + } else { + auto [E, E_wgt_temp] = energy_->sample(seed); + site.E = E; + E_wgt = E_wgt_temp; + } // Resample if energy falls above maximum particle energy if (site.E < data::energy_max[p] && diff --git a/tests/unit_tests/test_source.py b/tests/unit_tests/test_source.py index 6eb03f388..394c09e73 100644 --- a/tests/unit_tests/test_source.py +++ b/tests/unit_tests/test_source.py @@ -1,5 +1,6 @@ from collections import Counter from math import pi +from pathlib import Path import openmc import openmc.lib @@ -91,6 +92,74 @@ def test_point_cloud_strengths(run_in_tmpdir, sphere_box_model): assert sampled_strength == expected_strength, f'Strength incorrect for {positions[i]}' +def test_decay_spectrum_parent_nuclide(run_in_tmpdir): + chain_file = Path('chain_decay_spectrum_parent.xml') + chain_file.write_text(""" + + + + 1000000.0 1.0 + + + + + 2000000.0 1.0 + + + +""") + + inner_sphere = openmc.Sphere(r=10.0) + outer_sphere = openmc.Sphere(r=20.0, boundary_type='vacuum') + + shell_mat = openmc.Material() + shell_mat.add_nuclide('H1', 1.0) + shell_mat.set_density('atom/b-cm', 1.0e-12) + + void_cell = openmc.Cell(region=-inner_sphere) + shell_cell = openmc.Cell(fill=shell_mat, region=+inner_sphere & -outer_sphere) + + model = openmc.Model() + model.geometry = openmc.Geometry([void_cell, shell_cell]) + model.materials = [shell_mat] + model.settings.run_mode = 'fixed source' + model.settings.photon_transport = True + model.settings.particles = 1000 + model.settings.batches = 5 + model.settings.source = openmc.IndependentSource( + particle='photon', + space=openmc.stats.Point((0.0, 0.0, 0.0)), + energy=openmc.stats.DecaySpectrum( + {'ParentA': 1.0, 'ParentB': 1.0}, + volume=1.0 + ) + ) + + tally = openmc.Tally() + tally.filters = [ + openmc.CellFilter([void_cell]), + openmc.ParticleFilter(['photon']), + openmc.EnergyFilter([0.0, 1.5e6, 2.5e6]), + openmc.ParentNuclideFilter(['ParentA', 'ParentB']) + ] + tally.scores = ['flux'] + model.tallies = [tally] + + with openmc.config.patch('chain_file', chain_file): + sp_filename = model.run() + + with openmc.StatePoint(sp_filename) as sp: + tally_out = sp.tallies[tally.id] + mean = tally_out.get_reshaped_data('mean').squeeze() + + assert mean.shape == (2, 2) + assert mean[0, 0] > 0.0 + assert mean[1, 1] > 0.0 + assert mean[0, 1] == 0.0 + assert mean[1, 0] == 0.0 + assert np.count_nonzero(mean) == 2 + + def test_source_file(): filename = 'source.h5' src = openmc.FileSource(path=filename) diff --git a/tests/unit_tests/test_stats.py b/tests/unit_tests/test_stats.py index ca961c8b0..2754cf5d0 100644 --- a/tests/unit_tests/test_stats.py +++ b/tests/unit_tests/test_stats.py @@ -1,10 +1,11 @@ from math import pi +from pathlib import Path import numpy as np import pytest import openmc import openmc.stats -from openmc.stats.univariate import _INTERPOLATION_SCHEMES +from openmc.stats.univariate import _INTERPOLATION_SCHEMES, DecaySpectrum from scipy.integrate import trapezoid from tests.unit_tests import assert_sample_mean @@ -1013,3 +1014,135 @@ def test_fusion_spectrum_invalid(): # Temperature above 100 keV should raise an error with pytest.raises(ValueError): openmc.stats.fusion_neutron_spectrum(101e3, 'DT') + + +@pytest.fixture(autouse=False) +def decay_spectrum_chain(): + """Set chain_file for the duration of a test and clear the _photon_integral + cache so results from a different chain don't bleed across tests.""" + CHAIN_FILE = (Path(__file__).parents[1] / 'chain_simple.xml').resolve() + DecaySpectrum._photon_integral.cache_clear() + with openmc.config.patch('chain_file', CHAIN_FILE): + yield + DecaySpectrum._photon_integral.cache_clear() + + +def test_decay_spectrum_construction(): + nuclides = {'I135': 1.5e-3, 'Xe135': 8.2e-4} + d = openmc.stats.DecaySpectrum(nuclides, volume=100.0) + assert d.nuclides == nuclides + assert d.volume == pytest.approx(100.0) + assert len(d) == 2 + + +def test_decay_spectrum_validation(): + # nuclides must be a dict + with pytest.raises(TypeError): + openmc.stats.DecaySpectrum(['I135'], volume=1.0) + + # densities must be > 0 + with pytest.raises(ValueError): + openmc.stats.DecaySpectrum({'I135': -1.0}, volume=1.0) + + # volume must be > 0 + with pytest.raises(ValueError): + openmc.stats.DecaySpectrum({'I135': 1e-3}, volume=-1.0) + + with pytest.raises(ValueError): + openmc.stats.DecaySpectrum({'I135': 1e-3}, volume=0.0) + + +def test_decay_spectrum_xml_roundtrip(): + nuclides = {'I135': 1.5e-3, 'Xe135': 8.2e-4} + d = openmc.stats.DecaySpectrum(nuclides, volume=100.0) + + elem = d.to_xml_element('energy') + assert elem.get('type') == 'decay_spectrum' + assert float(elem.get('volume')) == pytest.approx(100.0) + assert elem.findtext('nuclides').split() == list(nuclides) + assert [float(x) for x in elem.findtext('parameters').split()] == pytest.approx( + list(nuclides.values())) + + # Round-trip via DecaySpectrum.from_xml_element + d2 = openmc.stats.DecaySpectrum.from_xml_element(elem) + assert d2.nuclides == nuclides + assert d2.volume == pytest.approx(100.0) + + # Round-trip via the Univariate dispatcher + d3 = openmc.stats.Univariate.from_xml_element(elem) + assert isinstance(d3, openmc.stats.DecaySpectrum) + assert d3 == d + + +def test_decay_spectrum_to_distribution(decay_spectrum_chain): + # Single emitting nuclide -> concrete distribution, not None + d = openmc.stats.DecaySpectrum({'I135': 1e-3}, volume=10.0) + dist = d.to_distribution() + assert dist is not None + + # Result is cached on second call + dist2 = d.to_distribution() + assert dist2 is dist + + # Nuclide with no photon source -> None + d_stable = openmc.stats.DecaySpectrum({'Xe136': 1e-3}, volume=10.0) + assert d_stable.to_distribution() is None + + # Mixture of emitters -> non-None combined distribution + d_mix = openmc.stats.DecaySpectrum( + {'I135': 1e-3, 'Xe135': 5e-4}, volume=10.0 + ) + dist_mix = d_mix.to_distribution() + assert dist_mix is not None + + +def test_decay_spectrum_integral(decay_spectrum_chain): + # For an emitting nuclide, integral should be > 0 + d = openmc.stats.DecaySpectrum({'I135': 1e-3}, volume=10.0) + assert d.integral() > 0.0 + + # Proportional to density: doubling density doubles integral + d2 = openmc.stats.DecaySpectrum({'I135': 2e-3}, volume=10.0) + assert d2.integral() == pytest.approx(2.0 * d.integral()) + + # Proportional to volume + d3 = openmc.stats.DecaySpectrum({'I135': 1e-3}, volume=20.0) + assert d3.integral() == pytest.approx(2.0 * d.integral()) + + # Pure non-emitter -> 0.0 + d_stable = openmc.stats.DecaySpectrum({'Xe136': 1e-3}, volume=10.0) + assert d_stable.integral() == pytest.approx(0.0) + + +def test_decay_spectrum_clip(decay_spectrum_chain): + # Stable / non-emitting nuclides are removed unconditionally + d = openmc.stats.DecaySpectrum( + {'I135': 1e-3, 'Xe135': 5e-4, 'Xe136': 1.0, 'Cs135': 1.0}, + volume=10.0, + ) + d_clip = d.clip() + assert 'Xe136' not in d_clip.nuclides + assert 'Cs135' not in d_clip.nuclides + assert 'I135' in d_clip.nuclides + assert 'Xe135' in d_clip.nuclides + # Original is unchanged + assert 'Xe136' in d.nuclides + + # inplace=True modifies and returns the same object + d_same = d.clip(inplace=True) + assert d_same is d + assert 'Xe136' not in d.nuclides + + # A nuclide with negligible emission rate is removed by tolerance clipping. + # U235 has a very small integral (~4e-17 Bq/atom) compared with I135 (~4e-5) + d_tight = openmc.stats.DecaySpectrum( + {'I135': 1e-3, 'U235': 1e-3}, volume=10.0 + ) + d_tight_clip = d_tight.clip(tolerance=1e-9) + assert 'U235' not in d_tight_clip.nuclides + assert 'I135' in d_tight_clip.nuclides + + # All non-emitters -> empty nuclides dict + d_empty = openmc.stats.DecaySpectrum({'Xe136': 1e-3}, volume=10.0) + d_empty.clip(inplace=True) + assert d_empty.nuclides == {} From 195dff6d1d22cc8dd5ed840d846073ff22fed550 Mon Sep 17 00:00:00 2001 From: Hridoy Kabiraj Date: Mon, 18 May 2026 20:20:59 +0600 Subject: [PATCH 621/671] Fix missing volume checks in material activity/decay heat (#3939) --- openmc/material.py | 5 +++++ tests/unit_tests/test_material.py | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/openmc/material.py b/openmc/material.py index 2dbe691f5..86cc3d793 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -1418,6 +1418,9 @@ class Material(IDManagerMixin): if volume is None: volume = self.volume + if units in {'Bq', 'Ci'} and volume is None: + raise ValueError(f"Volume must be set in order to compute activity in '{units}'.") + if units == 'Bq': multiplier = volume elif units == 'Bq/cm3': @@ -1474,6 +1477,8 @@ class Material(IDManagerMixin): if units == 'W': multiplier = volume if volume is not None else self.volume + if multiplier is None: + raise ValueError("Volume must be set in order to compute total decay heat.") elif units == 'W/cm3': multiplier = 1 elif units == 'W/m3': diff --git a/tests/unit_tests/test_material.py b/tests/unit_tests/test_material.py index 911b8867f..89dfc03dd 100644 --- a/tests/unit_tests/test_material.py +++ b/tests/unit_tests/test_material.py @@ -561,6 +561,10 @@ def test_get_activity(): m1.add_element("Fe", 0.7) m1.add_element("Li", 0.3) m1.set_density('g/cm3', 1.5) + with pytest.raises(ValueError, match="Volume must be set"): + m1.get_activity(units='Bq') + with pytest.raises(ValueError, match="Volume must be set"): + m1.get_activity(units='Ci') # activity in Bq/cc and Bq/g should not require volume setting assert m1.get_activity(units='Bq/cm3') == 0 assert m1.get_activity(units='Bq/g') == 0 @@ -619,6 +623,8 @@ def test_get_decay_heat(): m1.add_nuclide("U235", 0.2) m1.add_nuclide("U238", 0.8) m1.set_density('g/cm3', 10.5) + with pytest.raises(ValueError, match="Volume must be set"): + m1.get_decay_heat(units='W') # decay heat in W/cc and W/g should not require volume setting assert m1.get_decay_heat(units='W/cm3') == 0 assert m1.get_decay_heat(units='W/g') == 0 From beed56e6ee44037a1543e7ff3cc8815376634c18 Mon Sep 17 00:00:00 2001 From: charliesheh <59520443+charliesheh@users.noreply.github.com> Date: Mon, 18 May 2026 11:35:56 -0700 Subject: [PATCH 622/671] Check for missing thread count argument for -s/--threads (#3940) --- src/initialize.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/initialize.cpp b/src/initialize.cpp index 991877b24..c04a2fcbd 100644 --- a/src/initialize.cpp +++ b/src/initialize.cpp @@ -300,6 +300,11 @@ int parse_command_line(int argc, char* argv[]) settings::run_mode = RunMode::VOLUME; } else if (arg == "-s" || arg == "--threads") { // Read number of threads + if (i + 1 >= argc) { + std::string msg {"Number of threads not specified."}; + strcpy(openmc_err_msg, msg.c_str()); + return OPENMC_E_INVALID_ARGUMENT; + } i += 1; #ifdef _OPENMP From 0169fd9226786863e499a367d71e5eaae6adabb8 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Tue, 19 May 2026 23:23:10 -0500 Subject: [PATCH 623/671] Shared Secondary Particle Bank (#3863) Co-authored-by: John Tramm Co-authored-by: Claude Opus 4.6 Co-authored-by: Paul Romano Co-authored-by: Copilot --- docs/source/io_formats/settings.rst | 59 +-- include/openmc/bank.h | 8 +- include/openmc/event.h | 13 + include/openmc/particle.h | 3 +- include/openmc/particle_data.h | 34 +- include/openmc/settings.h | 3 +- include/openmc/shared_array.h | 30 +- include/openmc/simulation.h | 33 +- openmc/lib/core.py | 5 +- openmc/settings.py | 43 +- src/bank.cpp | 202 +++++++++- src/event.cpp | 51 ++- src/finalize.cpp | 3 + src/ifp.cpp | 4 +- src/initialize.cpp | 59 ++- src/output.cpp | 8 + src/particle.cpp | 178 ++++---- src/particle_restart.cpp | 23 +- src/physics.cpp | 14 +- src/physics_mg.cpp | 9 +- src/settings.cpp | 22 + src/simulation.cpp | 379 +++++++++++++++--- src/tallies/filter_particle_production.cpp | 2 +- src/tallies/tally_scoring.cpp | 9 +- .../__init__.py | 0 .../inputs_true.dat | 65 +++ .../results_true.dat | 11 + .../test.py | 91 +++++ .../particle_production_fission/__init__.py | 0 .../local/inputs_true.dat | 38 ++ .../local/results_true.dat | 3 + .../shared/inputs_true.dat | 38 ++ .../shared/results_true.dat | 3 + .../particle_production_fission/test.py | 49 +++ .../__init__.py | 0 .../geometry.xml | 9 + .../materials.xml | 9 + .../results_true.dat | 16 + .../settings.xml | 15 + .../test.py | 6 + .../pulse_height/{ => local}/inputs_true.dat | 4 +- .../pulse_height/{ => local}/results_true.dat | 0 .../pulse_height/shared/inputs_true.dat | 40 ++ .../pulse_height/shared/results_true.dat | 201 ++++++++++ tests/regression_tests/pulse_height/test.py | 80 ++-- .../weightwindows/local/inputs_true.dat | 94 +++++ .../weightwindows/local/results_true.dat | 1 + .../weightwindows/results_true.dat | 1 - .../{ => shared}/inputs_true.dat | 1 + .../weightwindows/shared/results_true.dat | 1 + .../survival_biasing/local/inputs_true.dat | 69 ++++ .../{ => local}/results_true.dat | 0 .../{ => shared}/inputs_true.dat | 1 + .../survival_biasing/shared/results_true.dat | 1 + .../weightwindows/survival_biasing/test.py | 28 +- tests/regression_tests/weightwindows/test.py | 28 +- .../weightwindows_pulse_height/__init__.py | 0 .../local/inputs_true.dat | 60 +++ .../local/results_true.dat | 201 ++++++++++ .../shared/inputs_true.dat | 60 +++ .../shared/results_true.dat | 201 ++++++++++ .../weightwindows_pulse_height/test.py | 73 ++++ tests/unit_tests/weightwindows/test.py | 16 +- 63 files changed, 2409 insertions(+), 299 deletions(-) create mode 100644 tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/__init__.py create mode 100644 tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/inputs_true.dat create mode 100644 tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/results_true.dat create mode 100644 tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/test.py create mode 100644 tests/regression_tests/particle_production_fission/__init__.py create mode 100644 tests/regression_tests/particle_production_fission/local/inputs_true.dat create mode 100644 tests/regression_tests/particle_production_fission/local/results_true.dat create mode 100644 tests/regression_tests/particle_production_fission/shared/inputs_true.dat create mode 100644 tests/regression_tests/particle_production_fission/shared/results_true.dat create mode 100644 tests/regression_tests/particle_production_fission/test.py create mode 100644 tests/regression_tests/particle_restart_fixed_shared_secondary/__init__.py create mode 100644 tests/regression_tests/particle_restart_fixed_shared_secondary/geometry.xml create mode 100644 tests/regression_tests/particle_restart_fixed_shared_secondary/materials.xml create mode 100644 tests/regression_tests/particle_restart_fixed_shared_secondary/results_true.dat create mode 100644 tests/regression_tests/particle_restart_fixed_shared_secondary/settings.xml create mode 100644 tests/regression_tests/particle_restart_fixed_shared_secondary/test.py rename tests/regression_tests/pulse_height/{ => local}/inputs_true.dat (95%) rename tests/regression_tests/pulse_height/{ => local}/results_true.dat (100%) create mode 100644 tests/regression_tests/pulse_height/shared/inputs_true.dat create mode 100644 tests/regression_tests/pulse_height/shared/results_true.dat create mode 100644 tests/regression_tests/weightwindows/local/inputs_true.dat create mode 100644 tests/regression_tests/weightwindows/local/results_true.dat delete mode 100644 tests/regression_tests/weightwindows/results_true.dat rename tests/regression_tests/weightwindows/{ => shared}/inputs_true.dat (99%) create mode 100644 tests/regression_tests/weightwindows/shared/results_true.dat create mode 100644 tests/regression_tests/weightwindows/survival_biasing/local/inputs_true.dat rename tests/regression_tests/weightwindows/survival_biasing/{ => local}/results_true.dat (100%) rename tests/regression_tests/weightwindows/survival_biasing/{ => shared}/inputs_true.dat (99%) create mode 100644 tests/regression_tests/weightwindows/survival_biasing/shared/results_true.dat create mode 100644 tests/regression_tests/weightwindows_pulse_height/__init__.py create mode 100644 tests/regression_tests/weightwindows_pulse_height/local/inputs_true.dat create mode 100644 tests/regression_tests/weightwindows_pulse_height/local/results_true.dat create mode 100644 tests/regression_tests/weightwindows_pulse_height/shared/inputs_true.dat create mode 100644 tests/regression_tests/weightwindows_pulse_height/shared/results_true.dat create mode 100644 tests/regression_tests/weightwindows_pulse_height/test.py diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index f8b064a37..1a436a75e 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -741,14 +741,16 @@ pseudo-random number generator. *Default*: 1 --------------------- -```` Element --------------------- +----------------------------------- +```` Element +----------------------------------- -The ``stride`` element is used to specify how many random numbers are allocated -for each source particle history. - - *Default*: 152,917 + The ``shared_secondary_bank`` element indicates whether to use a shared + secondary particle bank. When enabled, secondary particles are collected into + a global bank, sorted for reproducibility, and load-balanced across MPI ranks + between generations. If not specified, the shared secondary bank is enabled + automatically for fixed-source simulations with weight windows active, and + disabled otherwise. .. _source_element: @@ -1156,23 +1158,6 @@ based on constraints. *Default*: 0.05 -------------------------- -```` Element -------------------------- - -The ```` element indicates at what batches a state point file -should be written. A state point file can be used to restart a run or to get -tally results at any batch. The default behavior when using this tag is to -write out the source bank in the state_point file. This behavior can be -customized by using the ```` element. This element has the -following attributes/sub-elements: - - :batches: - A list of integers separated by spaces indicating at what batches a state - point file should be written. - - *Default*: Last batch only - -------------------------- ```` Element -------------------------- @@ -1222,6 +1207,32 @@ attributes/sub-elements: *Default*: false +------------------------- +```` Element +------------------------- + +The ```` element indicates at what batches a state point file +should be written. A state point file can be used to restart a run or to get +tally results at any batch. The default behavior when using this tag is to +write out the source bank in the state_point file. This behavior can be +customized by using the ```` element. This element has the +following attributes/sub-elements: + + :batches: + A list of integers separated by spaces indicating at what batches a state + point file should be written. + + *Default*: Last batch only + +-------------------- +```` Element +-------------------- + +The ``stride`` element is used to specify how many random numbers are allocated +for each source particle history. + + *Default*: 152,917 + ------------------------------ ```` Element ------------------------------ diff --git a/include/openmc/bank.h b/include/openmc/bank.h index c4e940bc8..6abcdd7f1 100644 --- a/include/openmc/bank.h +++ b/include/openmc/bank.h @@ -34,18 +34,24 @@ extern vector> ifp_fission_lifetime_bank; extern vector progeny_per_particle; +extern SharedArray shared_secondary_bank_read; +extern SharedArray shared_secondary_bank_write; + } // namespace simulation //============================================================================== // Non-member functions //============================================================================== -void sort_fission_bank(); +void sort_bank(SharedArray& bank, bool is_fission_bank); void free_memory_bank(); void init_fission_bank(int64_t max); +int64_t synchronize_global_secondary_bank( + SharedArray& shared_secondary_bank); + } // namespace openmc #endif // OPENMC_BANK_H diff --git a/include/openmc/event.h b/include/openmc/event.h index 2d215a10e..48a009f2b 100644 --- a/include/openmc/event.h +++ b/include/openmc/event.h @@ -112,6 +112,19 @@ void process_collision_events(); //! \param n_particles The number of particles in the particle buffer void process_death_events(int64_t n_particles); +//! Process event queues until all are empty. Each iteration processes the +//! longest queue first to maximize vectorization efficiency. +void process_transport_events(); + +//! Initialize secondary particles from a shared secondary bank for +//! event-based transport +// +//! \param n_particles The number of particles to initialize +//! \param offset The offset index in the shared secondary bank +//! \param shared_secondary_bank The shared secondary bank to read from +void process_init_secondary_events(int64_t n_particles, int64_t offset, + const SharedArray& shared_secondary_bank); + } // namespace openmc #endif // OPENMC_EVENT_H diff --git a/include/openmc/particle.h b/include/openmc/particle.h index e75f3785a..8db9721ba 100644 --- a/include/openmc/particle.h +++ b/include/openmc/particle.h @@ -71,7 +71,8 @@ public: void event_advance(); void event_cross_surface(); void event_collide(); - void event_revive_from_secondary(); + void event_revive_from_secondary(const SourceSite& site); + void event_check_limit_and_revive(); void event_death(); //! pulse-height recording diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index 5b632bbce..f72948f6e 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -50,8 +50,11 @@ struct SourceSite { // Extra attributes that don't show up in source written to file int parent_nuclide {-1}; - int64_t parent_id; - int64_t progeny_id; + int64_t parent_id {0}; + int64_t progeny_id {0}; + double wgt_born {1.0}; + double wgt_ww_born {-1.0}; + int64_t n_split {0}; }; struct CollisionTrackSite { @@ -533,14 +536,14 @@ private: uint64_t seeds_[N_STREAMS]; int stream_; - vector secondary_bank_; + vector local_secondary_bank_; // Keep track of how many secondary particles were created in the collision // and what the starting index is in the secondary bank for this particle int n_secondaries_ {0}; int secondary_bank_index_ {0}; - int64_t current_work_; + int64_t current_work_ {0}; vector flux_derivs_; @@ -563,7 +566,9 @@ private: int n_event_ {0}; - int n_split_ {0}; + int64_t n_tracks_ {0}; //!< number of tracks in this particle history + + int64_t n_split_ {0}; double ww_factor_ {0.0}; int64_t n_progeny_ {0}; @@ -693,12 +698,14 @@ public: int& stream() { return stream_; } // secondary particle bank - SourceSite& secondary_bank(int i) { return secondary_bank_[i]; } - const SourceSite& secondary_bank(int i) const { return secondary_bank_[i]; } - decltype(secondary_bank_)& secondary_bank() { return secondary_bank_; } - decltype(secondary_bank_) const& secondary_bank() const + SourceSite& local_secondary_bank(int i) { return local_secondary_bank_[i]; } + const SourceSite& local_secondary_bank(int i) const { - return secondary_bank_; + return local_secondary_bank_[i]; + } + decltype(local_secondary_bank_)& local_secondary_bank() + { + return local_secondary_bank_; } // Number of secondaries created in a collision @@ -747,13 +754,16 @@ public: int& n_event() { return n_event_; } // Number of times variance reduction has caused a particle split - int n_split() const { return n_split_; } - int& n_split() { return n_split_; } + int64_t n_split() const { return n_split_; } + int64_t& n_split() { return n_split_; } // Particle-specific factor for on-the-fly weight window adjustment double ww_factor() const { return ww_factor_; } double& ww_factor() { return ww_factor_; } + // Number of tracks in this particle history + int64_t& n_tracks() { return n_tracks_; } + // Number of progeny produced by this particle int64_t& n_progeny() { return n_progeny_; } diff --git a/include/openmc/settings.h b/include/openmc/settings.h index 0914a0958..3bba040f5 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -98,7 +98,8 @@ extern bool uniform_source_sampling; //!< sample sources uniformly? extern bool ufs_on; //!< uniform fission site method on? extern bool urr_ptables_on; //!< use unresolved resonance prob. tables? extern bool use_decay_photons; //!< use decay photons for D1S -extern "C" bool weight_windows_on; //!< are weight windows are enabled? +extern bool use_shared_secondary_bank; //!< Use shared bank for secondaries +extern "C" bool weight_windows_on; //!< are weight windows are enabled? extern bool weight_window_checkpoint_surface; //!< enable weight window check //!< upon surface crossing? extern bool weight_window_checkpoint_collision; //!< enable weight window check diff --git a/include/openmc/shared_array.h b/include/openmc/shared_array.h index 7e9ef28c5..b309ca3f1 100644 --- a/include/openmc/shared_array.h +++ b/include/openmc/shared_array.h @@ -4,6 +4,8 @@ //! \file shared_array.h //! \brief Shared array data structure +#include // for copy_n + #include "openmc/memory.h" namespace openmc { @@ -30,14 +32,12 @@ public: //! Default constructor. SharedArray() = default; - //! Construct a zero size container with space to hold capacity number of - //! elements. + //! Construct a container with `size` elements and capacity equal to `size`. // - //! \param capacity The number of elements for the container to allocate - //! space for - SharedArray(int64_t capacity) : capacity_(capacity) + //! \param size The number of elements to allocate and initialize + SharedArray(int64_t size) : size_(size), capacity_(size) { - data_ = make_unique(capacity); + data_ = make_unique(size); } //========================================================================== @@ -97,8 +97,26 @@ public: capacity_ = 0; } + //! Push back an element to the array, with capacity and reallocation behavior + //! as if this were a vector. This does not perform any thread safety checks. + //! If the size exceeds the capacity, then the capacity will double just as + //! with a vector. Data will be reallocated and moved to a new pointer and + //! copied in before the new item is appended. Old data will be freed. + void thread_unsafe_append(const T& value) + { + if (size_ == capacity_) { + int64_t new_capacity = capacity_ == 0 ? 8 : 2 * capacity_; + unique_ptr new_data = make_unique(new_capacity); + std::copy_n(data_.get(), size_, new_data.get()); + data_ = std::move(new_data); + capacity_ = new_capacity; + } + data_[size_++] = value; + } + //! Return the number of elements in the container int64_t size() { return size_; } + int64_t size() const { return size_; } //! Resize the container to contain a specified number of elements. This is //! useful in cases where the container is written to in a non-thread safe diff --git a/include/openmc/simulation.h b/include/openmc/simulation.h index 9a6cf1b21..454752cd2 100644 --- a/include/openmc/simulation.h +++ b/include/openmc/simulation.h @@ -49,6 +49,9 @@ extern const RegularMesh* ufs_mesh; extern vector k_generation; extern vector work_index; +extern int64_t + simulation_tracks_completed; //!< Number of tracks completed on this rank + } // namespace simulation //============================================================================== @@ -59,7 +62,7 @@ extern vector work_index; void allocate_banks(); //! Determine number of particles to transport per process -void calculate_work(); +void calculate_work(int64_t n_particles); //! Initialize nuclear data before a simulation void initialize_data(); @@ -70,8 +73,9 @@ void initialize_batch(); //! Initialize a fission generation void initialize_generation(); -//! Full initialization of a particle history -void initialize_history(Particle& p, int64_t index_source); +//! Full initialization of a particle track +void initialize_particle_track( + Particle& p, int64_t index_source, bool is_secondary); //! Finalize a batch //! @@ -92,16 +96,35 @@ void broadcast_results(); void free_memory_simulation(); -//! Simulate a single particle history (and all generated secondary particles, -//! if enabled), from birth to death +//! Compute unique particle ID from a 1-based source index +//! \param index_source 1-based source index within this rank's work +//! \return globally unique particle ID +int64_t compute_particle_id(int64_t index_source); + +//! Compute the transport RNG seed from a particle ID +//! \param particle_id the particle's globally unique ID +//! \return seed value passed to init_particle_seeds() +int64_t compute_transport_seed(int64_t particle_id); + +//! Simulate a single particle history from birth to death, inclusive of any +//! secondary particles. In shared secondary mode, only a single track is +//! transported and secondaries are deposited into a shared bank instead. void transport_history_based_single_particle(Particle& p); //! Simulate all particle histories using history-based parallelism void transport_history_based(); +//! Simulate all particles using history-based parallelism, with a shared +//! secondary bank +void transport_history_based_shared_secondary(); + //! Simulate all particle histories using event-based parallelism void transport_event_based(); +//! Simulate all particles using event-based parallelism, with a shared +//! secondary bank +void transport_event_based_shared_secondary(); + } // namespace openmc #endif // OPENMC_SIMULATION_H diff --git a/openmc/lib/core.py b/openmc/lib/core.py index 00876db99..0ee8c3ff0 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -31,7 +31,10 @@ class _SourceSite(Structure): ('particle', c_int32), ('parent_nuclide', c_int), ('parent_id', c_int64), - ('progeny_id', c_int64)] + ('progeny_id', c_int64), + ('wgt_born', c_double), + ('wgt_ww_born', c_double), + ('n_split', 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 diff --git a/openmc/settings.py b/openmc/settings.py index 1090babda..8120eb073 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -67,6 +67,10 @@ class Settings: (ex: ["(n,fission)", 2, "(n,2n)"] ). (list of str or int) :deposited_E_threshold: Number to define the minimum deposited energy during per collision to trigger banking. (float) + create_delayed_neutrons : bool + Whether delayed neutrons are created in fission. + + .. versionadded:: 0.13.3 create_fission_neutrons : bool Indicate whether fission neutrons should be created or not. cutoff : dict @@ -256,8 +260,15 @@ class Settings: The type of calculation to perform (default is 'eigenvalue') seed : int Seed for the linear congruential pseudorandom number generator - stride : int - Number of random numbers allocated for each source particle history + shared_secondary_bank : bool + Whether to use a shared secondary particle bank. When enabled, + secondary particles are collected into a global bank, sorted for + reproducibility, and load-balanced across MPI ranks between + generations. If not specified, the shared secondary bank is + enabled automatically for fixed-source simulations with weight + windows active, and disabled otherwise. + + .. versionadded:: 0.15.4 source : Iterable of openmc.SourceBase Distribution of source sites in space, angle, and energy source_rejection_fraction : float @@ -277,6 +288,8 @@ class Settings: Options for writing state points. Acceptable keys are: :batches: list of batches at which to write statepoint files + stride : int + Number of random numbers allocated for each source particle history surf_source_read : dict Options for reading surface source points. Acceptable keys are: @@ -373,10 +386,6 @@ class Settings: .. versionadded:: 0.14.0 - create_delayed_neutrons : bool - Whether delayed neutrons are created in fission. - - .. versionadded:: 0.13.3 weight_windows_on : bool Whether weight windows are enabled @@ -482,6 +491,7 @@ class Settings: self._weight_window_generators = cv.CheckedList( WeightWindowGenerator, 'weight window generators') self._weight_windows_on = None + self._shared_secondary_bank = None self._weight_windows_file = None self._weight_window_checkpoints = {} self._max_history_splits = None @@ -1306,6 +1316,15 @@ class Settings: cv.check_type('weight windows on', value, bool) self._weight_windows_on = value + @property + def shared_secondary_bank(self) -> bool: + return self._shared_secondary_bank + + @shared_secondary_bank.setter + def shared_secondary_bank(self, value: bool): + cv.check_type('shared secondary bank', value, bool) + self._shared_secondary_bank = value + @property def weight_window_checkpoints(self) -> dict: return self._weight_window_checkpoints @@ -1924,6 +1943,11 @@ class Settings: elem = ET.SubElement(root, "weight_windows_on") elem.text = str(self._weight_windows_on).lower() + def _create_shared_secondary_bank_subelement(self, root): + if self._shared_secondary_bank is not None: + elem = ET.SubElement(root, "shared_secondary_bank") + elem.text = str(self._shared_secondary_bank).lower() + def _create_weight_window_generators_subelement(self, root, mesh_memo=None): if not self.weight_window_generators: return @@ -2430,6 +2454,11 @@ class Settings: if text is not None: self.weight_windows_on = text in ('true', '1') + def _shared_secondary_bank_from_xml_element(self, root): + text = get_text(root, 'shared_secondary_bank') + if text is not None: + self.shared_secondary_bank = text in ('true', '1') + def _weight_windows_file_from_xml_element(self, root): text = get_text(root, 'weight_windows_file') if text is not None: @@ -2597,6 +2626,7 @@ class Settings: self._create_write_initial_source_subelement(element) self._create_weight_windows_subelement(element, mesh_memo) self._create_weight_windows_on_subelement(element) + self._create_shared_secondary_bank_subelement(element) self._create_weight_window_generators_subelement(element, mesh_memo) self._create_weight_windows_file_element(element) self._create_weight_window_checkpoints_subelement(element) @@ -2714,6 +2744,7 @@ class Settings: settings._write_initial_source_from_xml_element(elem) settings._weight_windows_from_xml_element(elem, meshes) settings._weight_windows_on_from_xml_element(elem) + settings._shared_secondary_bank_from_xml_element(elem) settings._weight_windows_file_from_xml_element(elem) settings._weight_window_generators_from_xml_element(elem, meshes) settings._weight_window_checkpoints_from_xml_element(elem) diff --git a/src/bank.cpp b/src/bank.cpp index 5afdc2eca..5b12b48fd 100644 --- a/src/bank.cpp +++ b/src/bank.cpp @@ -7,6 +7,7 @@ #include "openmc/vector.h" #include +#include #include namespace openmc { @@ -43,6 +44,13 @@ vector> ifp_fission_lifetime_bank; // used to efficiently sort the fission bank after each iteration. vector progeny_per_particle; +// When shared secondary bank mode is enabled, secondaries produced during +// transport are collected in the write bank. When a secondary generation is +// complete, write is moved to read for transport, and a new empty write bank +// is created. This repeats until no secondaries remain. +SharedArray shared_secondary_bank_read; +SharedArray shared_secondary_bank_write; + } // namespace simulation //============================================================================== @@ -60,6 +68,8 @@ void free_memory_bank() simulation::ifp_source_lifetime_bank.clear(); simulation::ifp_fission_delayed_group_bank.clear(); simulation::ifp_fission_lifetime_bank.clear(); + simulation::shared_secondary_bank_read.clear(); + simulation::shared_secondary_bank_write.clear(); } void init_fission_bank(int64_t max) @@ -68,13 +78,13 @@ void init_fission_bank(int64_t max) simulation::progeny_per_particle.resize(simulation::work_per_rank); } -// Performs an O(n) sort on the fission bank, by leveraging +// Performs an O(n) sort on a fission or secondary bank, by leveraging // the parent_id and progeny_id fields of banked particles. See the following // paper for more details: // "Reproducibility and Monte Carlo Eigenvalue Calculations," F.B. Brown and // T.M. Sutton, 1992 ANS Annual Meeting, Transactions of the American Nuclear // Society, Volume 65, Page 235. -void sort_fission_bank() +void sort_bank(SharedArray& bank, bool is_fission_bank) { // Ensure we don't read off the end of the array if we ran with 0 particles if (simulation::progeny_per_particle.size() == 0) { @@ -96,44 +106,200 @@ void sort_fission_bank() vector> sorted_ifp_lifetime_bank; // If there is not enough space, allocate a temporary vector and point to it - if (simulation::fission_bank.size() > - simulation::fission_bank.capacity() / 2) { - sorted_bank_holder.resize(simulation::fission_bank.size()); + if (bank.size() > bank.capacity() / 2) { + sorted_bank_holder.resize(bank.size()); sorted_bank = sorted_bank_holder.data(); } else { // otherwise, point sorted_bank to unused portion of the fission bank - sorted_bank = &simulation::fission_bank[simulation::fission_bank.size()]; + sorted_bank = bank.data() + bank.size(); } - if (settings::ifp_on) { + if (settings::ifp_on && is_fission_bank) { allocate_temporary_vector_ifp( sorted_ifp_delayed_group_bank, sorted_ifp_lifetime_bank); } - // Use parent and progeny indices to sort fission bank - for (int64_t i = 0; i < simulation::fission_bank.size(); i++) { - const auto& site = simulation::fission_bank[i]; - int64_t offset = site.parent_id - 1 - simulation::work_index[mpi::rank]; - int64_t idx = simulation::progeny_per_particle[offset] + site.progeny_id; - if (idx >= simulation::fission_bank.size()) { + // Use parent and progeny indices to sort bank + for (int64_t i = 0; i < bank.size(); i++) { + const auto& site = bank[i]; + if (site.parent_id < 0 || + site.parent_id >= + static_cast(simulation::progeny_per_particle.size())) { + fatal_error(fmt::format("Invalid parent_id {} for banked site (expected " + "range [0, {})).", + site.parent_id, simulation::progeny_per_particle.size())); + } + int64_t idx = + simulation::progeny_per_particle[site.parent_id] + site.progeny_id; + if (idx < 0 || idx >= bank.size()) { fatal_error("Mismatch detected between sum of all particle progeny and " - "shared fission bank size."); + "bank size during sorting."); } sorted_bank[idx] = site; - if (settings::ifp_on) { + if (settings::ifp_on && is_fission_bank) { copy_ifp_data_from_fission_banks( i, sorted_ifp_delayed_group_bank[idx], sorted_ifp_lifetime_bank[idx]); } } // Copy sorted bank into the fission bank - std::copy(sorted_bank, sorted_bank + simulation::fission_bank.size(), - simulation::fission_bank.data()); - if (settings::ifp_on) { + std::copy(sorted_bank, sorted_bank + bank.size(), bank.data()); + if (settings::ifp_on && is_fission_bank) { copy_ifp_data_to_fission_banks( sorted_ifp_delayed_group_bank.data(), sorted_ifp_lifetime_bank.data()); } } +// This function redistributes SourceSite particles across MPI ranks to +// achieve load balancing while preserving the global ordering of particles. +// +// GUARANTEES: +// ----------- +// 1. Global Order Preservation: After redistribution, each rank holds a +// contiguous slice of the original global ordering. For example, if the +// input across 3 ranks was: +// - Rank 0: IDs 0-4 +// - Rank 1: IDs 5-6 +// - Rank 2: IDs 7-200 +// Then after redistribution (assuming ~67 particles per rank): +// - Rank 0: IDs 0-66 (contiguous) +// - Rank 1: IDs 67-133 (contiguous) +// - Rank 2: IDs 134-200 (contiguous) +// The global ordering is always preserved - no rank will ever hold +// non-contiguous ID ranges like "0-4 and 100-200". +// +// 2. Even Load Balancing: Particles are distributed as evenly as possible. +// If total % n_procs != 0, the first 'remainder' ranks each get one extra +// particle (i.e., floor division with remainder distributed to lower +// ranks). This follows the same logic as calculate_work(). +// +// HOW IT WORKS: +// ------------- +// The algorithm uses overlap-based redistribution: +// 1. Each rank's current data occupies a range [cumulative_before[rank], +// cumulative_before[rank+1]) in the global index space. +// 2. Each rank's target data should occupy [cumulative_target[rank], +// cumulative_target[rank+1]) in the same global index space. +// 3. For each pair of (source_rank, dest_rank), we calculate the overlap +// between what source_rank currently has and what dest_rank needs. +// 4. MPI_Alltoallv transfers exactly these overlapping regions, with +// displacements ensuring data lands at the correct position in the +// receiving buffer. +// +// EDGE CASES HANDLED: +// ------------------- +// - Single rank (n_procs == 1): Returns immediately with local size, no MPI. +// - Empty total (all ranks have 0 particles): Returns 0 immediately. +// - Imbalanced input (e.g., one rank has all particles): Works correctly; +// that rank will send portions to all other ranks based on target ranges. +// - Non-divisible totals: First 'remainder' ranks get one extra particle. +int64_t synchronize_global_secondary_bank( + SharedArray& shared_secondary_bank) +{ + // Get current size of local bank + int64_t local_size = shared_secondary_bank.size(); + + if (mpi::n_procs == 1) { + return local_size; + } + +#ifdef OPENMC_MPI + // Gather all sizes to all ranks + vector all_sizes(mpi::n_procs); + MPI_Allgather(&local_size, 1, MPI_INT64_T, all_sizes.data(), 1, MPI_INT64_T, + mpi::intracomm); + + // Calculate total and check for empty case + int64_t total = 0; + for (int64_t size : all_sizes) { + total += size; + } + + // If we don't have any items to distribute, return + if (total == 0) { + return total; + } + + int64_t base_count = total / mpi::n_procs; + int64_t remainder = total % mpi::n_procs; + + // Calculate target size for each rank + // First 'remainder' ranks get base_count + 1, rest get base_count + vector target_sizes(mpi::n_procs); + for (int i = 0; i < mpi::n_procs; ++i) { + target_sizes[i] = base_count + (i < remainder ? 1 : 0); + } + + // Calculate send and receive counts in terms of SourceSite objects + // (not bytes) + vector send_counts(mpi::n_procs, 0); + vector recv_counts(mpi::n_procs, 0); + vector send_displs(mpi::n_procs, 0); + vector recv_displs(mpi::n_procs, 0); + + // Calculate cumulative positions (starting index for each rank in the + // global array) + vector cumulative_before(mpi::n_procs + 1, 0); + vector cumulative_target(mpi::n_procs + 1, 0); + for (int i = 0; i < mpi::n_procs; ++i) { + cumulative_before[i + 1] = cumulative_before[i] + all_sizes[i]; + cumulative_target[i + 1] = cumulative_target[i] + target_sizes[i]; + } + + // Determine send and receive amounts for each rank + int64_t my_start = cumulative_before[mpi::rank]; + int64_t my_end = cumulative_before[mpi::rank + 1]; + int64_t my_target_start = cumulative_target[mpi::rank]; + int64_t my_target_end = cumulative_target[mpi::rank + 1]; + + for (int r = 0; r < mpi::n_procs; ++r) { + // Send: overlap between my current range and rank r's target range + int64_t send_overlap_start = std::max(my_start, cumulative_target[r]); + int64_t send_overlap_end = std::min(my_end, cumulative_target[r + 1]); + if (send_overlap_start < send_overlap_end) { + int64_t count = send_overlap_end - send_overlap_start; + int64_t displ = send_overlap_start - my_start; + if (count > std::numeric_limits::max() || + displ > std::numeric_limits::max()) { + fatal_error("Secondary bank size exceeds MPI_Alltoallv int limit."); + } + send_counts[r] = static_cast(count); + send_displs[r] = static_cast(displ); + } + + // Recv: overlap between rank r's current range and my target range + int64_t recv_overlap_start = + std::max(cumulative_before[r], my_target_start); + int64_t recv_overlap_end = + std::min(cumulative_before[r + 1], my_target_end); + if (recv_overlap_start < recv_overlap_end) { + int64_t count = recv_overlap_end - recv_overlap_start; + int64_t displ = recv_overlap_start - my_target_start; + if (count > std::numeric_limits::max() || + displ > std::numeric_limits::max()) { + fatal_error("Secondary bank size exceeds MPI_Alltoallv int limit."); + } + recv_counts[r] = static_cast(count); + recv_displs[r] = static_cast(displ); + } + } + + // Prepare receive buffer with target size + SharedArray new_bank(target_sizes[mpi::rank]); + + // Perform all-to-all redistribution using the custom MPI type + MPI_Alltoallv(shared_secondary_bank.data(), send_counts.data(), + send_displs.data(), mpi::source_site, new_bank.data(), recv_counts.data(), + recv_displs.data(), mpi::source_site, mpi::intracomm); + + // Replace old bank with redistributed data + shared_secondary_bank = std::move(new_bank); + + return total; +#else + return local_size; +#endif +} + //============================================================================== // C API //============================================================================== diff --git a/src/event.cpp b/src/event.cpp index f33e132d0..2d436bb9d 100644 --- a/src/event.cpp +++ b/src/event.cpp @@ -1,6 +1,9 @@ #include "openmc/event.h" +#include "openmc/bank.h" +#include "openmc/error.h" #include "openmc/material.h" +#include "openmc/settings.h" #include "openmc/simulation.h" #include "openmc/timer.h" @@ -64,7 +67,8 @@ void process_init_events(int64_t n_particles, int64_t source_offset) simulation::time_event_init.start(); #pragma omp parallel for schedule(runtime) for (int64_t i = 0; i < n_particles; i++) { - initialize_history(simulation::particles[i], source_offset + i + 1); + initialize_particle_track( + simulation::particles[i], source_offset + i + 1, false); dispatch_xs_event(i); } simulation::time_event_init.stop(); @@ -136,7 +140,7 @@ void process_surface_crossing_events() int64_t buffer_idx = simulation::surface_crossing_queue[i].idx; Particle& p = simulation::particles[buffer_idx]; p.event_cross_surface(); - p.event_revive_from_secondary(); + p.event_check_limit_and_revive(); if (p.alive()) dispatch_xs_event(buffer_idx); } @@ -155,7 +159,7 @@ void process_collision_events() int64_t buffer_idx = simulation::collision_queue[i].idx; Particle& p = simulation::particles[buffer_idx]; p.event_collide(); - p.event_revive_from_secondary(); + p.event_check_limit_and_revive(); if (p.alive()) dispatch_xs_event(buffer_idx); } @@ -176,4 +180,45 @@ void process_death_events(int64_t n_particles) simulation::time_event_death.stop(); } +void process_transport_events() +{ + while (true) { + int64_t max = std::max({simulation::calculate_fuel_xs_queue.size(), + simulation::calculate_nonfuel_xs_queue.size(), + simulation::advance_particle_queue.size(), + simulation::surface_crossing_queue.size(), + simulation::collision_queue.size()}); + + if (max == 0) { + break; + } else if (max == simulation::calculate_fuel_xs_queue.size()) { + process_calculate_xs_events(simulation::calculate_fuel_xs_queue); + } else if (max == simulation::calculate_nonfuel_xs_queue.size()) { + process_calculate_xs_events(simulation::calculate_nonfuel_xs_queue); + } else if (max == simulation::advance_particle_queue.size()) { + process_advance_particle_events(); + } else if (max == simulation::surface_crossing_queue.size()) { + process_surface_crossing_events(); + } else if (max == simulation::collision_queue.size()) { + process_collision_events(); + } + } +} + +void process_init_secondary_events(int64_t n_particles, int64_t offset, + const SharedArray& shared_secondary_bank) +{ + simulation::time_event_init.start(); +#pragma omp parallel for schedule(runtime) + for (int64_t i = 0; i < n_particles; i++) { + initialize_particle_track(simulation::particles[i], offset + i + 1, true); + const SourceSite& site = shared_secondary_bank[offset + i]; + simulation::particles[i].event_revive_from_secondary(site); + if (simulation::particles[i].alive()) { + dispatch_xs_event(i); + } + } + simulation::time_event_init.stop(); +} + } // namespace openmc diff --git a/src/finalize.cpp b/src/finalize.cpp index 59711dcc3..fd891d9dd 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -149,6 +149,8 @@ int openmc_finalize() settings::uniform_source_sampling = false; settings::ufs_on = false; settings::urr_ptables_on = true; + settings::use_decay_photons = false; + settings::use_shared_secondary_bank = false; settings::verbosity = -1; settings::weight_cutoff = 0.25; settings::weight_survive = 1.0; @@ -219,6 +221,7 @@ int openmc_reset() settings::cmfd_run = false; simulation::n_lost_particles = 0; + simulation::simulation_tracks_completed = 0; return 0; } diff --git a/src/ifp.cpp b/src/ifp.cpp index a5157440d..f0f98b8ef 100644 --- a/src/ifp.cpp +++ b/src/ifp.cpp @@ -32,13 +32,13 @@ void ifp(const Particle& p, int64_t idx) { if (is_beta_effective_or_both()) { const auto& delayed_groups = - simulation::ifp_source_delayed_group_bank[p.current_work() - 1]; + simulation::ifp_source_delayed_group_bank[p.current_work()]; simulation::ifp_fission_delayed_group_bank[idx] = _ifp(p.delayed_group(), delayed_groups); } if (is_generation_time_or_both()) { const auto& lifetimes = - simulation::ifp_source_lifetime_bank[p.current_work() - 1]; + simulation::ifp_source_lifetime_bank[p.current_work()]; simulation::ifp_fission_lifetime_bank[idx] = _ifp(p.lifetime(), lifetimes); } } diff --git a/src/initialize.cpp b/src/initialize.cpp index c04a2fcbd..78b414f78 100644 --- a/src/initialize.cpp +++ b/src/initialize.cpp @@ -161,7 +161,7 @@ void initialize_mpi(MPI_Comm intracomm) // Create bank datatype SourceSite b; - MPI_Aint disp[11]; + MPI_Aint disp[14]; MPI_Get_address(&b.r, &disp[0]); MPI_Get_address(&b.u, &disp[1]); MPI_Get_address(&b.E, &disp[2]); @@ -173,14 +173,35 @@ void initialize_mpi(MPI_Comm intracomm) MPI_Get_address(&b.parent_nuclide, &disp[8]); MPI_Get_address(&b.parent_id, &disp[9]); MPI_Get_address(&b.progeny_id, &disp[10]); - for (int i = 10; i >= 0; --i) { + MPI_Get_address(&b.wgt_born, &disp[11]); + MPI_Get_address(&b.wgt_ww_born, &disp[12]); + MPI_Get_address(&b.n_split, &disp[13]); + for (int i = 13; i >= 0; --i) { disp[i] -= disp[0]; } - int blocks[] {3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1}; - MPI_Datatype types[] {MPI_DOUBLE, MPI_DOUBLE, MPI_DOUBLE, MPI_DOUBLE, - MPI_DOUBLE, MPI_INT, MPI_INT, MPI_INT, MPI_INT, MPI_LONG, MPI_LONG}; - MPI_Type_create_struct(11, blocks, disp, types, &mpi::source_site); + // Block counts for each field + int blocks[] = {3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; + + // Types for each field + MPI_Datatype types[] = { + MPI_DOUBLE, // r (3 doubles) + MPI_DOUBLE, // u (3 doubles) + MPI_DOUBLE, // E + MPI_DOUBLE, // time + MPI_DOUBLE, // wgt + MPI_INT, // delayed_group + MPI_INT, // surf_id + MPI_INT, // particle (enum) + MPI_INT, // parent_nuclide + MPI_INT64_T, // parent_id + MPI_INT64_T, // progeny_id + MPI_DOUBLE, // wgt_born + MPI_DOUBLE, // wgt_ww_born + MPI_INT64_T // n_split + }; + + MPI_Type_create_struct(14, blocks, disp, types, &mpi::source_site); MPI_Type_commit(&mpi::source_site); CollisionTrackSite bc; @@ -366,6 +387,28 @@ int parse_command_line(int argc, char* argv[]) return 0; } +// TODO: Pulse-height tallies require per-history scoring across the full +// particle tree (parent + all descendants). The shared secondary bank +// transports each secondary as an independent Particle, breaking this +// assumption. A proper fix would defer pulse-height scoring: save +// (root_source_id, cell, pht_storage) per particle, then aggregate by +// root_source_id after all secondary generations complete before scoring +// into the histogram. For now, disable shared secondary when pulse-height +// tallies are present. +static void check_pulse_height_compatibility() +{ + if (settings::use_shared_secondary_bank) { + for (const auto& t : model::tallies) { + if (t->type_ == TallyType::PULSE_HEIGHT) { + settings::use_shared_secondary_bank = false; + warning("Pulse-height tallies are not yet compatible with the shared " + "secondary bank. Disabling shared secondary bank."); + break; + } + } + } +} + bool read_model_xml() { std::string model_filename = settings::path_input; @@ -460,6 +503,8 @@ bool read_model_xml() if (check_for_node(root, "tallies")) read_tallies_xml(root.child("tallies")); + check_pulse_height_compatibility(); + // Initialize distribcell_filters prepare_distribcell(); @@ -505,6 +550,8 @@ void read_separate_xml_files() read_tallies_xml(); + check_pulse_height_compatibility(); + // Initialize distribcell_filters prepare_distribcell(); diff --git a/src/output.cpp b/src/output.cpp index f8c0f2a97..725e8d069 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -501,6 +501,14 @@ void print_runtime() show_rate("Calculation Rate (inactive)", speed_inactive); } show_rate("Calculation Rate (active)", speed_active); + + // Display track rate when weight windows are enabled + if (settings::weight_windows_on) { + double speed_tracks = + simulation::simulation_tracks_completed / time_active.elapsed(); + fmt::print( + " {:<33} = {:.6} tracks/second\n", "Track Rate (active)", speed_tracks); + } } //============================================================================== diff --git a/src/particle.cpp b/src/particle.cpp index 46df63cd1..779ae18da 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -94,7 +94,7 @@ bool Particle::create_secondary( // Increment number of secondaries created (for ParticleProductionFilter) n_secondaries()++; - auto& bank = secondary_bank().emplace_back(); + SourceSite bank; bank.particle = type; bank.wgt = wgt; bank.r = r(); @@ -102,12 +102,21 @@ bool Particle::create_secondary( bank.E = settings::run_CE ? E : g(); bank.time = time(); bank_second_E() += bank.E; + bank.parent_id = current_work(); + if (settings::use_shared_secondary_bank) { + bank.progeny_id = n_progeny()++; + } + bank.wgt_born = wgt_born(); + bank.wgt_ww_born = wgt_ww_born(); + bank.n_split = n_split(); + + local_secondary_bank().emplace_back(bank); return true; } void Particle::split(double wgt) { - auto& bank = secondary_bank().emplace_back(); + SourceSite bank; bank.particle = type(); bank.wgt = wgt; bank.r = r(); @@ -122,6 +131,16 @@ void Particle::split(double wgt) int surf_id = model::surfaces[surface_index()]->id_; bank.surf_id = (surface() > 0) ? surf_id : -surf_id; } + + bank.wgt_born = wgt_born(); + bank.wgt_ww_born = wgt_ww_born(); + bank.n_split = n_split(); + bank.parent_id = current_work(); + if (settings::use_shared_secondary_bank) { + bank.progeny_id = n_progeny()++; + } + + local_secondary_bank().emplace_back(bank); } void Particle::from_source(const SourceSite* src) @@ -168,6 +187,10 @@ void Particle::from_source(const SourceSite* src) int index_plus_one = model::surface_map[std::abs(src->surf_id)] + 1; surface() = (src->surf_id > 0) ? index_plus_one : -index_plus_one; } + + wgt_born() = src->wgt_born; + wgt_ww_born() = src->wgt_ww_born; + n_split() = src->n_split; } void Particle::event_calculate_xs() @@ -449,61 +472,72 @@ void Particle::event_collide() #endif } -void Particle::event_revive_from_secondary() +void Particle::event_revive_from_secondary(const SourceSite& site) +{ + // Write final position for the previous track (skip if this is a freshly + // constructed particle with no prior track, e.g., Phase 2 of shared + // secondary transport) + if (write_track() && n_event() > 0) { + write_particle_track(*this); + } + + from_source(&site); + + n_event() = 0; + if (!settings::use_shared_secondary_bank) { + n_tracks()++; + } + bank_second_E() = 0.0; + + // Subtract secondary particle energy from interim pulse-height results. + // In shared secondary mode, this subtraction was already done on the parent + // particle during create_secondary(), so skip it here. + if (!settings::use_shared_secondary_bank && + !model::active_pulse_height_tallies.empty() && this->type().is_photon()) { + // Since the birth cell of the particle has not been set we + // have to determine it before the energy of the secondary particle can be + // removed from the pulse-height of this cell. + if (lowest_coord().cell() == C_NONE) { + bool verbose = settings::verbosity >= 10 || trace(); + if (!exhaustive_find_cell(*this, verbose)) { + mark_as_lost("Could not find the cell containing particle " + + std::to_string(id())); + return; + } + // Set birth cell attribute + if (cell_born() == C_NONE) + cell_born() = lowest_coord().cell(); + + // Initialize last cells from current cell + for (int j = 0; j < n_coord(); ++j) { + cell_last(j) = coord(j).cell(); + } + n_coord_last() = n_coord(); + } + pht_secondary_particles(); + } + + // Enter new particle in particle track file + if (write_track()) + add_particle_track(*this); +} + +void Particle::event_check_limit_and_revive() { // If particle has too many events, display warning and kill it - ++n_event(); + n_event()++; if (n_event() == settings::max_particle_events) { warning("Particle " + std::to_string(id()) + " underwent maximum number of events."); wgt() = 0.0; } - // Check for secondary particles if this particle is dead - if (!alive()) { - // Write final position for this particle - if (write_track()) { - write_particle_track(*this); - } - - // If no secondary particles, break out of event loop - if (secondary_bank().empty()) - return; - - from_source(&secondary_bank().back()); - secondary_bank().pop_back(); - n_event() = 0; - bank_second_E() = 0.0; - - // Subtract secondary particle energy from interim pulse-height results - if (!model::active_pulse_height_tallies.empty() && - this->type().is_photon()) { - // Since the birth cell of the particle has not been set we - // have to determine it before the energy of the secondary particle can be - // removed from the pulse-height of this cell. - if (lowest_coord().cell() == C_NONE) { - bool verbose = settings::verbosity >= 10 || trace(); - if (!exhaustive_find_cell(*this, verbose)) { - mark_as_lost("Could not find the cell containing particle " + - std::to_string(id())); - return; - } - // Set birth cell attribute - if (cell_born() == C_NONE) - cell_born() = lowest_coord().cell(); - - // Initialize last cells from current cell - for (int j = 0; j < n_coord(); ++j) { - cell_last(j) = coord(j).cell(); - } - n_coord_last() = n_coord(); - } - pht_secondary_particles(); - } - - // Enter new particle in particle track file - if (write_track()) - add_particle_track(*this); + // In non-shared-secondary mode, revive from local secondary bank + if (!alive() && !settings::use_shared_secondary_bank && + !local_secondary_bank().empty()) { + SourceSite& site = local_secondary_bank().back(); + event_revive_from_secondary(site); + local_secondary_bank().pop_back(); } } @@ -515,6 +549,7 @@ void Particle::event_death() // Finish particle track output. if (write_track()) { + write_particle_track(*this); finalize_particle_track(*this); } @@ -538,11 +573,17 @@ void Particle::event_death() score_pulse_height_tally(*this, model::active_pulse_height_tallies); } + // Accumulate track count for this particle history + if (!settings::use_shared_secondary_bank) { +#pragma omp atomic + simulation::simulation_tracks_completed += n_tracks(); + } + // Record the number of progeny created by this particle. // This data will be used to efficiently sort the fission bank. - if (settings::run_mode == RunMode::EIGENVALUE) { - int64_t offset = id() - 1 - simulation::work_index[mpi::rank]; - simulation::progeny_per_particle[offset] = n_progeny(); + if (settings::run_mode == RunMode::EIGENVALUE || + settings::use_shared_secondary_bank) { + simulation::progeny_per_particle[current_work()] = n_progeny(); } } @@ -863,28 +904,27 @@ void Particle::write_restart() const write_dataset(file_id, "id", id()); write_dataset(file_id, "type", type().pdg_number()); + // Get source site data for the particle that got lost int64_t i = current_work(); + SourceSite site; if (settings::run_mode == RunMode::EIGENVALUE) { - // take source data from primary bank for eigenvalue simulation - write_dataset(file_id, "weight", simulation::source_bank[i - 1].wgt); - write_dataset(file_id, "energy", simulation::source_bank[i - 1].E); - write_dataset(file_id, "xyz", simulation::source_bank[i - 1].r); - write_dataset(file_id, "uvw", simulation::source_bank[i - 1].u); - write_dataset(file_id, "time", simulation::source_bank[i - 1].time); + site = simulation::source_bank[i]; + } else if (settings::run_mode == RunMode::FIXED_SOURCE && + settings::use_shared_secondary_bank && + i < simulation::shared_secondary_bank_read.size()) { + site = simulation::shared_secondary_bank_read[i]; } else if (settings::run_mode == RunMode::FIXED_SOURCE) { - // re-sample using rng random number seed used to generate source particle - int64_t id = (simulation::total_gen + overall_generation() - 1) * - settings::n_particles + - simulation::work_index[mpi::rank] + i; + // Re-sample using the same seed used to generate the source particle. + // current_work() is 0-indexed, compute_particle_id expects 1-indexed. + int64_t id = compute_transport_seed(compute_particle_id(i + 1)); uint64_t seed = init_seed(id, STREAM_SOURCE); - // re-sample source site - auto site = sample_external_source(&seed); - write_dataset(file_id, "weight", site.wgt); - write_dataset(file_id, "energy", site.E); - write_dataset(file_id, "xyz", site.r); - write_dataset(file_id, "uvw", site.u); - write_dataset(file_id, "time", site.time); + site = sample_external_source(&seed); } + write_dataset(file_id, "weight", site.wgt); + write_dataset(file_id, "energy", site.E); + write_dataset(file_id, "xyz", site.r); + write_dataset(file_id, "uvw", site.u); + write_dataset(file_id, "time", site.time); // Close file file_close(file_id); diff --git a/src/particle_restart.cpp b/src/particle_restart.cpp index f02fcb94b..c226d51ec 100644 --- a/src/particle_restart.cpp +++ b/src/particle_restart.cpp @@ -1,6 +1,7 @@ #include "openmc/particle_restart.h" #include "openmc/array.h" +#include "openmc/bank.h" #include "openmc/constants.h" #include "openmc/hdf5_interface.h" #include "openmc/mgxs_interface.h" @@ -106,20 +107,16 @@ void run_particle_restart() // Set all tallies to 0 for now (just tracking errors) model::tallies.clear(); - // Compute random number seed - int64_t particle_seed; - switch (previous_run_mode) { - case RunMode::EIGENVALUE: - case RunMode::FIXED_SOURCE: - particle_seed = (simulation::total_gen + overall_generation() - 1) * - settings::n_particles + - p.id(); - break; - default: - throw std::runtime_error { - "Unexpected run mode: " + - std::to_string(static_cast(previous_run_mode))}; + // Allocate progeny_per_particle if needed for shared secondary mode + // (event_death() writes to this array). Set current_work to 0 since we + // only have one particle being restarted. + if (settings::use_shared_secondary_bank) { + p.current_work() = 0; + simulation::progeny_per_particle.resize(1, 0); } + + // Compute random number seed + int64_t particle_seed = compute_transport_seed(p.id()); init_particle_seeds(particle_seed, p.seeds()); // Force calculation of cross-sections by setting last energy to zero diff --git a/src/physics.cpp b/src/physics.cpp index 106bd1aa2..892b39479 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -44,7 +44,7 @@ void collision(Particle& p) { // Add to collision counter for particle ++(p.n_collision()); - p.secondary_bank_index() = p.secondary_bank().size(); + p.secondary_bank_index() = p.local_secondary_bank().size(); // Sample reaction for the material the particle is in switch (p.type().pdg_number()) { @@ -127,7 +127,8 @@ void sample_neutron_reaction(Particle& p) // Make sure particle population doesn't grow out of control for // subcritical multiplication problems. - if (p.secondary_bank().size() >= settings::max_secondaries) { + if (p.local_secondary_bank().size() >= settings::max_secondaries && + !settings::use_shared_secondary_bank) { fatal_error( "The secondary particle bank appears to be growing without " "bound. You are likely running a subcritical multiplication problem " @@ -228,7 +229,7 @@ void create_fission_sites(Particle& p, int i_nuclide, const Reaction& rx) } // Set parent and progeny IDs - site.parent_id = p.id(); + site.parent_id = p.current_work(); site.progeny_id = p.n_progeny()++; // Store fission site in bank @@ -253,7 +254,10 @@ void create_fission_sites(Particle& p, int i_nuclide, const Reaction& rx) ifp(p, idx); } } else { - p.secondary_bank().push_back(site); + site.wgt_born = p.wgt_born(); + site.wgt_ww_born = p.wgt_ww_born(); + site.n_split = p.n_split(); + p.local_secondary_bank().push_back(site); p.n_secondaries()++; } @@ -1225,7 +1229,7 @@ void sample_secondary_photons(Particle& p, int i_nuclide) // Tag secondary particle with parent nuclide if (created_photon && settings::use_decay_photons) { - p.secondary_bank().back().parent_nuclide = + p.local_secondary_bank().back().parent_nuclide = rx->products_[i_product].parent_nuclide_; } } diff --git a/src/physics_mg.cpp b/src/physics_mg.cpp index 6991c7365..212ba765c 100644 --- a/src/physics_mg.cpp +++ b/src/physics_mg.cpp @@ -27,7 +27,7 @@ void collision_mg(Particle& p) { // Add to the collision counter for the particle p.n_collision()++; - p.secondary_bank_index() = p.secondary_bank().size(); + p.secondary_bank_index() = p.local_secondary_bank().size(); // Sample the reaction type sample_reaction(p); @@ -179,7 +179,7 @@ void create_fission_sites(Particle& p) } // Set parent and progeny ID - site.parent_id = p.id(); + site.parent_id = p.current_work(); site.progeny_id = p.n_progeny()++; // Store fission site in bank @@ -200,7 +200,10 @@ void create_fission_sites(Particle& p) break; } } else { - p.secondary_bank().push_back(site); + site.wgt_born = p.wgt_born(); + site.wgt_ww_born = p.wgt_ww_born(); + site.n_split = p.n_split(); + p.local_secondary_bank().push_back(site); p.n_secondaries()++; } diff --git a/src/settings.cpp b/src/settings.cpp index bb6991da9..8ae252ae1 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -83,6 +83,7 @@ bool uniform_source_sampling {false}; bool ufs_on {false}; bool urr_ptables_on {true}; bool use_decay_photons {false}; +bool use_shared_secondary_bank {false}; bool weight_windows_on {false}; bool weight_window_checkpoint_surface {false}; bool weight_window_checkpoint_collision {true}; @@ -1320,6 +1321,27 @@ void read_settings_xml(pugi::xml_node root) settings::use_decay_photons = get_node_value_bool(root, "use_decay_photons"); } + + // If weight windows are on, also enable shared secondary bank (unless + // explicitly disabled by user). + if (check_for_node(root, "shared_secondary_bank")) { + bool val = get_node_value_bool(root, "shared_secondary_bank"); + if (val && run_mode == RunMode::EIGENVALUE) { + warning( + "Shared secondary bank is not supported in eigenvalue calculations. " + "Setting will be ignored."); + } else { + settings::use_shared_secondary_bank = val; + } + } else if (settings::weight_windows_on) { + if (run_mode == RunMode::EIGENVALUE) { + warning( + "Shared secondary bank is not supported in eigenvalue calculations. " + "Particle local secondary banks will be used instead."); + } else if (run_mode == RunMode::FIXED_SOURCE) { + settings::use_shared_secondary_bank = true; + } + } } void free_memory_settings() diff --git a/src/simulation.cpp b/src/simulation.cpp index 4fad196a6..89aa9ca0e 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -86,7 +86,7 @@ int openmc_simulation_init() } // Determine how much work each process should do - calculate_work(); + calculate_work(settings::n_particles); // Allocate source, fission and surface source banks. allocate_banks(); @@ -214,6 +214,20 @@ int openmc_simulation_finalize() // Stop timers and show timing statistics simulation::time_finalize.stop(); simulation::time_total.stop(); + +#ifdef OPENMC_MPI + // Reduce track count across ranks for correct reporting. In shared secondary + // bank mode, all ranks already have the global count; in non-shared mode, + // each rank only has its own count. + if (settings::weight_windows_on && !settings::use_shared_secondary_bank) { + int64_t total_tracks; + MPI_Reduce(&simulation::simulation_tracks_completed, &total_tracks, 1, + MPI_INT64_T, MPI_SUM, 0, mpi::intracomm); + if (mpi::master) + simulation::simulation_tracks_completed = total_tracks; + } +#endif + if (mpi::master) { if (settings::solver_type != SolverType::RANDOM_RAY) { if (settings::verbosity >= 6) @@ -254,9 +268,17 @@ int openmc_next_batch(int* status) // Transport loop if (settings::event_based) { - transport_event_based(); + if (settings::use_shared_secondary_bank) { + transport_event_based_shared_secondary(); + } else { + transport_event_based(); + } } else { - transport_history_based(); + if (settings::use_shared_secondary_bank) { + transport_history_based_shared_secondary(); + } else { + transport_history_based(); + } } // Accumulate time for transport @@ -324,6 +346,8 @@ const RegularMesh* ufs_mesh {nullptr}; vector k_generation; vector work_index; +int64_t simulation_tracks_completed {0}; + } // namespace simulation //============================================================================== @@ -547,7 +571,7 @@ void finalize_generation() // If using shared memory, stable sort the fission bank (by parent IDs) // so as to allow for reproducibility regardless of which order particles // are run in. - sort_fission_bank(); + sort_bank(simulation::fission_bank, true); // Distribute fission bank across processors evenly synchronize_bank(); @@ -571,26 +595,35 @@ void finalize_generation() } } -void initialize_history(Particle& p, int64_t index_source) +void sample_source_particle(Particle& p, int64_t index_source) { - // set defaults + // Sample a particle from the source bank if (settings::run_mode == RunMode::EIGENVALUE) { - // set defaults for eigenvalue simulations from primary bank p.from_source(&simulation::source_bank[index_source - 1]); } else if (settings::run_mode == RunMode::FIXED_SOURCE) { // initialize random number seed - int64_t id = (simulation::total_gen + overall_generation() - 1) * - settings::n_particles + - simulation::work_index[mpi::rank] + index_source; + int64_t id = compute_transport_seed(compute_particle_id(index_source)); uint64_t seed = init_seed(id, STREAM_SOURCE); // sample from external source distribution or custom library then set auto site = sample_external_source(&seed); p.from_source(&site); } - p.current_work() = index_source; +} + +void initialize_particle_track( + Particle& p, int64_t index_source, bool is_secondary) +{ + // Note: index_source is 1-based (first particle = 1), but current_work() is + // stored as 0-based for direct use as an array index into + // progeny_per_particle, source_bank, ifp banks, etc. + if (!is_secondary) { + sample_source_particle(p, index_source); + } + + p.current_work() = index_source - 1; // set identifier for particle - p.id() = simulation::work_index[mpi::rank] + index_source; + p.id() = compute_particle_id(index_source); // set progeny count to zero p.n_progeny() = 0; @@ -598,6 +631,9 @@ void initialize_history(Particle& p, int64_t index_source) // Reset particle event counter p.n_event() = 0; + // Initialize track counter (1 for this primary/secondary track) + p.n_tracks() = 1; + // Reset split counter p.n_split() = 0; @@ -611,9 +647,7 @@ void initialize_history(Particle& p, int64_t index_source) std::fill(p.pht_storage().begin(), p.pht_storage().end(), 0); // set random number seed - int64_t particle_seed = - (simulation::total_gen + overall_generation() - 1) * settings::n_particles + - p.id(); + int64_t particle_seed = compute_transport_seed(p.id()); init_particle_seeds(particle_seed, p.seeds()); // set particle trace @@ -627,17 +661,21 @@ void initialize_history(Particle& p, int64_t index_source) p.write_track() = check_track_criteria(p); // Set the particle's initial weight window value. - p.wgt_ww_born() = -1.0; - apply_weight_windows(p); + if (!is_secondary) { + p.wgt_ww_born() = -1.0; + apply_weight_windows(p); + } // Display message if high verbosity or trace is on if (settings::verbosity >= 9 || p.trace()) { write_message("Simulating Particle {}", p.id()); } -// Add particle's starting weight to count for normalizing tallies later + // Add particle's starting weight to count for normalizing tallies later + if (!is_secondary) { #pragma omp atomic - simulation::total_weight += p.wgt(); + simulation::total_weight += p.wgt(); + } // Force calculation of cross-sections by setting last energy to zero if (settings::run_CE) { @@ -655,13 +693,34 @@ int overall_generation() return settings::gen_per_batch * (current_batch - 1) + current_gen; } -void calculate_work() +int64_t compute_particle_id(int64_t index_source) +{ + if (settings::use_shared_secondary_bank) { + return simulation::work_index[mpi::rank] + index_source + + simulation::simulation_tracks_completed; + } else { + return simulation::work_index[mpi::rank] + index_source; + } +} + +int64_t compute_transport_seed(int64_t particle_id) +{ + if (settings::use_shared_secondary_bank) { + return particle_id; + } else { + return (simulation::total_gen + overall_generation() - 1) * + settings::n_particles + + particle_id; + } +} + +void calculate_work(int64_t n_particles) { // Determine minimum amount of particles to simulate on each processor - int64_t min_work = settings::n_particles / mpi::n_procs; + int64_t min_work = n_particles / mpi::n_procs; // Determine number of processors that have one extra particle - int64_t remainder = settings::n_particles % mpi::n_procs; + int64_t remainder = n_particles % mpi::n_procs; int64_t i_bank = 0; simulation::work_index.resize(mpi::n_procs + 1); @@ -816,7 +875,7 @@ void transport_history_based_single_particle(Particle& p) p.event_collide(); } } - p.event_revive_from_secondary(); + p.event_check_limit_and_revive(); } p.event_death(); } @@ -826,11 +885,137 @@ void transport_history_based() #pragma omp parallel for schedule(runtime) for (int64_t i_work = 1; i_work <= simulation::work_per_rank; ++i_work) { Particle p; - initialize_history(p, i_work); + initialize_particle_track(p, i_work, false); transport_history_based_single_particle(p); } } +// The shared secondary bank transport algorithm works in two phases. In the +// first phase, all primary particles are sampled then transported, and their +// secondary particles are deposited into a shared secondary bank. The second +// phase occurs in a loop, where all secondary tracks in the shared secondary +// bank are transported. Any secondary particles generated during this phase are +// deposited back into the shared secondary bank. The shared secondary bank is +// sorted for consistent ordering and load balanced across MPI ranks. This loop +// continues until there are no more secondary tracks left to transport. +void transport_history_based_shared_secondary() +{ + // Clear shared secondary banks from any prior use + simulation::shared_secondary_bank_read.clear(); + simulation::shared_secondary_bank_write.clear(); + + if (mpi::master) { + write_message(fmt::format(" Primary source particles: {}", + settings::n_particles), + 6); + } + + simulation::progeny_per_particle.resize(simulation::work_per_rank); + std::fill(simulation::progeny_per_particle.begin(), + simulation::progeny_per_particle.end(), 0); + + // Phase 1: Transport primary particles and deposit first generation of + // secondaries in the shared secondary bank +#pragma omp parallel + { + vector thread_bank; + +#pragma omp for schedule(runtime) + for (int64_t i = 1; i <= simulation::work_per_rank; i++) { + Particle p; + initialize_particle_track(p, i, false); + transport_history_based_single_particle(p); + for (auto& site : p.local_secondary_bank()) { + thread_bank.push_back(site); + } + } + + // Drain thread-local bank into the shared secondary bank (once per thread) +#pragma omp critical(SharedSecondaryBank) + { + for (auto& site : thread_bank) { + simulation::shared_secondary_bank_write.thread_unsafe_append(site); + } + } + } + + simulation::simulation_tracks_completed += settings::n_particles; + + // Phase 2: Now that the secondary bank has been populated, enter loop over + // all secondary generations + int n_generation_depth = 1; + int64_t alive_secondary = 1; + while (alive_secondary) { + + // Sort the shared secondary bank by parent ID then progeny ID to + // ensure reproducibility. + sort_bank(simulation::shared_secondary_bank_write, false); + + // Synchronize the shared secondary bank amongst all MPI ranks, such + // that each MPI rank has an approximately equal number of secondary + // tracks. Also reports the total number of secondaries alive across + // all MPI ranks. + alive_secondary = synchronize_global_secondary_bank( + simulation::shared_secondary_bank_write); + + // Recalculate work for each MPI rank based on number of alive secondary + // tracks + calculate_work(alive_secondary); + + // Display the number of secondary tracks in this generation. This + // is useful for user monitoring so as to see if the secondary population is + // exploding and to determine how many generations of secondaries are being + // transported. + if (mpi::master) { + write_message(fmt::format(" Secondary generation {:<2} tracks: {}", + n_generation_depth, alive_secondary), + 6); + } + + simulation::shared_secondary_bank_read = + std::move(simulation::shared_secondary_bank_write); + simulation::shared_secondary_bank_write = SharedArray(); + simulation::progeny_per_particle.resize( + simulation::shared_secondary_bank_read.size()); + std::fill(simulation::progeny_per_particle.begin(), + simulation::progeny_per_particle.end(), 0); + + // Transport all secondary tracks from the shared secondary bank +#pragma omp parallel + { + vector thread_bank; + +#pragma omp for schedule(runtime) + for (int64_t i = 1; i <= simulation::shared_secondary_bank_read.size(); + i++) { + Particle p; + initialize_particle_track(p, i, true); + SourceSite& site = simulation::shared_secondary_bank_read[i - 1]; + p.event_revive_from_secondary(site); + transport_history_based_single_particle(p); + for (auto& secondary_site : p.local_secondary_bank()) { + thread_bank.push_back(secondary_site); + } + } + + // Drain thread-local bank into the shared secondary bank (once per + // thread) +#pragma omp critical(SharedSecondaryBank) + { + for (auto& secondary_site : thread_bank) { + simulation::shared_secondary_bank_write.thread_unsafe_append( + secondary_site); + } + } + } // End of transport loop over tracks in shared secondary bank + n_generation_depth++; + simulation::simulation_tracks_completed += alive_secondary; + } // End of loop over secondary generations + + // Reset work so that fission bank etc works correctly + calculate_work(settings::n_particles); +} + void transport_event_based() { int64_t remaining_work = simulation::work_per_rank; @@ -848,33 +1033,7 @@ void transport_event_based() // Initialize all particle histories for this subiteration process_init_events(n_particles, source_offset); - - // Event-based transport loop - while (true) { - // Determine which event kernel has the longest queue - int64_t max = std::max({simulation::calculate_fuel_xs_queue.size(), - simulation::calculate_nonfuel_xs_queue.size(), - simulation::advance_particle_queue.size(), - simulation::surface_crossing_queue.size(), - simulation::collision_queue.size()}); - - // Execute event with the longest queue - if (max == 0) { - break; - } else if (max == simulation::calculate_fuel_xs_queue.size()) { - process_calculate_xs_events(simulation::calculate_fuel_xs_queue); - } else if (max == simulation::calculate_nonfuel_xs_queue.size()) { - process_calculate_xs_events(simulation::calculate_nonfuel_xs_queue); - } else if (max == simulation::advance_particle_queue.size()) { - process_advance_particle_events(); - } else if (max == simulation::surface_crossing_queue.size()) { - process_surface_crossing_events(); - } else if (max == simulation::collision_queue.size()) { - process_collision_events(); - } - } - - // Execute death event for all particles + process_transport_events(); process_death_events(n_particles); // Adjust remaining work and source offset variables @@ -883,4 +1042,122 @@ void transport_event_based() } } +void transport_event_based_shared_secondary() +{ + // Clear shared secondary banks from any prior use + simulation::shared_secondary_bank_read.clear(); + simulation::shared_secondary_bank_write.clear(); + + if (mpi::master) { + write_message(fmt::format(" Primary source particles: {}", + settings::n_particles), + 6); + } + + simulation::progeny_per_particle.resize(simulation::work_per_rank); + std::fill(simulation::progeny_per_particle.begin(), + simulation::progeny_per_particle.end(), 0); + + // Phase 1: Transport primary particles using event-based processing and + // deposit first generation of secondaries in the shared secondary bank + int64_t remaining_work = simulation::work_per_rank; + int64_t source_offset = 0; + + while (remaining_work > 0) { + int64_t n_particles = + std::min(remaining_work, settings::max_particles_in_flight); + + process_init_events(n_particles, source_offset); + process_transport_events(); + process_death_events(n_particles); + + // Collect secondaries from all particle buffers into shared bank + for (int64_t i = 0; i < n_particles; i++) { + for (auto& site : simulation::particles[i].local_secondary_bank()) { + simulation::shared_secondary_bank_write.thread_unsafe_append(site); + } + simulation::particles[i].local_secondary_bank().clear(); + } + + remaining_work -= n_particles; + source_offset += n_particles; + } + + simulation::simulation_tracks_completed += settings::n_particles; + + // Phase 2: Now that the secondary bank has been populated, enter loop over + // all secondary generations + int n_generation_depth = 1; + int64_t alive_secondary = 1; + while (alive_secondary) { + + // Sort the shared secondary bank by parent ID then progeny ID to + // ensure reproducibility. + sort_bank(simulation::shared_secondary_bank_write, false); + + // Synchronize the shared secondary bank amongst all MPI ranks, such + // that each MPI rank has an approximately equal number of secondary + // tracks. + alive_secondary = synchronize_global_secondary_bank( + simulation::shared_secondary_bank_write); + + // Recalculate work for each MPI rank based on number of alive secondary + // tracks + calculate_work(alive_secondary); + + if (mpi::master) { + write_message(fmt::format(" Secondary generation {:<2} tracks: {}", + n_generation_depth, alive_secondary), + 6); + } + + simulation::shared_secondary_bank_read = + std::move(simulation::shared_secondary_bank_write); + simulation::shared_secondary_bank_write = SharedArray(); + simulation::progeny_per_particle.resize( + simulation::shared_secondary_bank_read.size()); + std::fill(simulation::progeny_per_particle.begin(), + simulation::progeny_per_particle.end(), 0); + + // Ensure particle buffer is large enough for this secondary generation + int64_t sec_buffer_length = std::min( + static_cast(simulation::shared_secondary_bank_read.size()), + settings::max_particles_in_flight); + if (sec_buffer_length > + static_cast(simulation::particles.size())) { + init_event_queues(sec_buffer_length); + } + + // Transport secondary tracks using event-based processing + int64_t sec_remaining = simulation::shared_secondary_bank_read.size(); + int64_t sec_offset = 0; + + while (sec_remaining > 0) { + int64_t n_particles = + std::min(sec_remaining, settings::max_particles_in_flight); + + process_init_secondary_events( + n_particles, sec_offset, simulation::shared_secondary_bank_read); + process_transport_events(); + process_death_events(n_particles); + + // Collect secondaries from all particle buffers into shared bank + for (int64_t i = 0; i < n_particles; i++) { + for (auto& site : simulation::particles[i].local_secondary_bank()) { + simulation::shared_secondary_bank_write.thread_unsafe_append(site); + } + simulation::particles[i].local_secondary_bank().clear(); + } + + sec_remaining -= n_particles; + sec_offset += n_particles; + } // End of subiteration loop over secondary tracks + n_generation_depth++; + simulation::simulation_tracks_completed += alive_secondary; + } // End of loop over secondary generations + + // Reset work so that fission bank etc works correctly + calculate_work(settings::n_particles); +} + } // namespace openmc diff --git a/src/tallies/filter_particle_production.cpp b/src/tallies/filter_particle_production.cpp index f8d82fdd9..899809679 100644 --- a/src/tallies/filter_particle_production.cpp +++ b/src/tallies/filter_particle_production.cpp @@ -19,7 +19,7 @@ void ParticleProductionFilter::get_all_bins( // Loop over secondary bank entries for (int bank_idx = start_idx; bank_idx < end_idx; bank_idx++) { - const auto& site = p.secondary_bank(bank_idx); + const auto& site = p.local_secondary_bank(bank_idx); // Find which particle-type slot this secondary belongs to auto it = type_to_index_.find(site.particle.pdg_number()); diff --git a/src/tallies/tally_scoring.cpp b/src/tallies/tally_scoring.cpp index 5dcd07331..b7947afab 100644 --- a/src/tallies/tally_scoring.cpp +++ b/src/tallies/tally_scoring.cpp @@ -945,7 +945,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, if (p.type().is_neutron() && p.fission()) { if (is_generation_time_or_both()) { const auto& lifetimes = - simulation::ifp_source_lifetime_bank[p.current_work() - 1]; + simulation::ifp_source_lifetime_bank[p.current_work()]; if (lifetimes.size() == settings::ifp_n_generation) { score = lifetimes[0] * p.wgt_last(); } @@ -959,7 +959,7 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, if (p.type().is_neutron() && p.fission()) { if (is_beta_effective_or_both()) { const auto& delayed_groups = - simulation::ifp_source_delayed_group_bank[p.current_work() - 1]; + simulation::ifp_source_delayed_group_bank[p.current_work()]; if (delayed_groups.size() == settings::ifp_n_generation) { if (delayed_groups[0] > 0) { score = p.wgt_last(); @@ -985,12 +985,11 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, int ifp_data_size; if (is_beta_effective_or_both()) { ifp_data_size = static_cast( - simulation::ifp_source_delayed_group_bank[p.current_work() - 1] + simulation::ifp_source_delayed_group_bank[p.current_work()] .size()); } else { ifp_data_size = static_cast( - simulation::ifp_source_lifetime_bank[p.current_work() - 1] - .size()); + simulation::ifp_source_lifetime_bank[p.current_work()].size()); } if (ifp_data_size == settings::ifp_n_generation) { score = p.wgt_last(); diff --git a/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/__init__.py b/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/inputs_true.dat b/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/inputs_true.dat new file mode 100644 index 000000000..318fdc7b9 --- /dev/null +++ b/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/inputs_true.dat @@ -0,0 +1,65 @@ + + + + 2g.h5 + + + + + + + + + + + + fixed source + 100 + 2 + 0 + + + 0.0 -1000.0 -1000.0 929.45 1000.0 1000.0 + + + +

    false + + multi-group + + false + + true + + 1 + neutron + 0.0 0.625 20000000.0 + 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 + 2.5 2.5 2.5 2.5 2.5 2.5 2.5 2.5 2.5 2.5 + 3.0 + 10 + 1e-38 + + + 5 1 1 + 0.0 -1000.0 -1000.0 + 929.45 1000.0 1000.0 + + true + 100 + + + + 5 1 1 + 0.0 -1000.0 -1000.0 + 929.45 1000.0 1000.0 + + + 2 + + + 1 + flux + + + diff --git a/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/results_true.dat b/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/results_true.dat new file mode 100644 index 000000000..38ff94417 --- /dev/null +++ b/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/results_true.dat @@ -0,0 +1,11 @@ +tally 1: +1.765369E+04 +3.087787E+08 +1.708316E+04 +2.842002E+08 +9.444106E+03 +7.488341E+07 +2.066528E+03 +2.142445E+06 +8.689619E+02 +5.652099E+05 diff --git a/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/test.py b/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/test.py new file mode 100644 index 000000000..336c8461f --- /dev/null +++ b/tests/regression_tests/mg_fixed_source_ww_fission_shared_secondary/test.py @@ -0,0 +1,91 @@ +import os + +import numpy as np +import openmc +from openmc.examples import slab_mg + +from tests.testing_harness import PyAPITestHarness + + +def create_library(): + # Instantiate the energy group data and file object + groups = openmc.mgxs.EnergyGroups([0.0, 0.625, 20.0e6]) + + mg_cross_sections_file = openmc.MGXSLibrary(groups) + + # Make the base, isotropic data + nu = [2.50, 2.50] + fiss = np.array([0.002817, 0.097]) + capture = [0.008708, 0.02518] + absorption = np.add(capture, fiss) + scatter = np.array( + [[[0.31980, 0.06694], [0.004555, -0.0003972]], + [[0.00000, 0.00000], [0.424100, 0.05439000]]]) + total = [0.33588, 0.54628] + chi = [1., 0.] + + mat_1 = openmc.XSdata('mat_1', groups) + mat_1.order = 1 + mat_1.set_nu_fission(np.multiply(nu, fiss)) + mat_1.set_absorption(absorption) + mat_1.set_scatter_matrix(scatter) + mat_1.set_total(total) + mat_1.set_chi(chi) + mg_cross_sections_file.add_xsdata(mat_1) + + # Write the file + mg_cross_sections_file.export_to_hdf5('2g.h5') + + +class MGXSTestHarness(PyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = '2g.h5' + if os.path.exists(f): + os.remove(f) + + +def test_mg_fixed_source_ww_fission_shared_secondary(): + create_library() + model = slab_mg() + + # Override settings for fixed-source mode with shared secondary bank + model.settings.run_mode = 'fixed source' + model.settings.inactive = 0 + model.settings.batches = 2 + model.settings.particles = 100 + model.settings.create_fission_neutrons = True + model.settings.shared_secondary_bank = True + model.settings.max_history_splits = 100 + + # Add weight windows on a simple 1D mesh + ww_mesh = openmc.RegularMesh() + ww_mesh.lower_left = (0.0, -1000.0, -1000.0) + ww_mesh.upper_right = (929.45, 1000.0, 1000.0) + ww_mesh.dimension = (5, 1, 1) + + # Uniform lower bounds for 2 energy groups, 5 spatial bins + lower_bounds = np.full((2, 5, 1, 1), 0.5) + ww = openmc.WeightWindows( + ww_mesh, + lower_bounds.flatten(), + None, + 5.0, + [0.0, 0.625, 20.0e6], + 'neutron' + ) + model.settings.weight_windows = [ww] + + # Add a flux tally + mesh = openmc.RegularMesh() + mesh.lower_left = (0.0, -1000.0, -1000.0) + mesh.upper_right = (929.45, 1000.0, 1000.0) + mesh.dimension = (5, 1, 1) + + tally = openmc.Tally() + tally.filters = [openmc.MeshFilter(mesh)] + tally.scores = ['flux'] + model.tallies = [tally] + + harness = MGXSTestHarness('statepoint.2.h5', model) + harness.main() diff --git a/tests/regression_tests/particle_production_fission/__init__.py b/tests/regression_tests/particle_production_fission/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/particle_production_fission/local/inputs_true.dat b/tests/regression_tests/particle_production_fission/local/inputs_true.dat new file mode 100644 index 000000000..e93dfc36e --- /dev/null +++ b/tests/regression_tests/particle_production_fission/local/inputs_true.dat @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + fixed source + 100 + 2 + + + 1000000.0 1.0 + + + true + false + + + + neutron + + + neutron + + + 2 1 + events + analog + + + diff --git a/tests/regression_tests/particle_production_fission/local/results_true.dat b/tests/regression_tests/particle_production_fission/local/results_true.dat new file mode 100644 index 000000000..2f5288eba --- /dev/null +++ b/tests/regression_tests/particle_production_fission/local/results_true.dat @@ -0,0 +1,3 @@ +tally 1: +4.570000E+00 +1.047890E+01 diff --git a/tests/regression_tests/particle_production_fission/shared/inputs_true.dat b/tests/regression_tests/particle_production_fission/shared/inputs_true.dat new file mode 100644 index 000000000..b50a39cd0 --- /dev/null +++ b/tests/regression_tests/particle_production_fission/shared/inputs_true.dat @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + fixed source + 100 + 2 + + + 1000000.0 1.0 + + + true + true + + + + neutron + + + neutron + + + 2 1 + events + analog + + + diff --git a/tests/regression_tests/particle_production_fission/shared/results_true.dat b/tests/regression_tests/particle_production_fission/shared/results_true.dat new file mode 100644 index 000000000..5a4dfc516 --- /dev/null +++ b/tests/regression_tests/particle_production_fission/shared/results_true.dat @@ -0,0 +1,3 @@ +tally 1: +5.180000E+00 +1.355140E+01 diff --git a/tests/regression_tests/particle_production_fission/test.py b/tests/regression_tests/particle_production_fission/test.py new file mode 100644 index 000000000..2ad6ca515 --- /dev/null +++ b/tests/regression_tests/particle_production_fission/test.py @@ -0,0 +1,49 @@ +import openmc +import pytest +from openmc.utility_funcs import change_directory + +from tests.testing_harness import PyAPITestHarness + + +@pytest.mark.parametrize("shared_secondary,subdir", [ + (False, "local"), + (True, "shared"), +]) +def test_particle_production_fission(shared_secondary, subdir): + """Fixed-source model with fissionable material to test that + ParticleProductionFilter correctly counts fission-born neutrons, + with both local and shared secondary bank modes.""" + with change_directory(subdir): + openmc.reset_auto_ids() + model = openmc.Model() + + mat = openmc.Material() + mat.set_density('g/cm3', 18.0) + mat.add_nuclide('U235', 1.0) + model.materials.append(mat) + + sph = openmc.Sphere(r=5.0, boundary_type='vacuum') + cell = openmc.Cell(fill=mat, region=-sph) + model.geometry = openmc.Geometry([cell]) + + source = openmc.IndependentSource() + source.energy = openmc.stats.delta_function(1.0e6) + + model.settings.particles = 100 + model.settings.run_mode = 'fixed source' + model.settings.batches = 2 + model.settings.source = source + model.settings.create_fission_neutrons = True + model.settings.shared_secondary_bank = shared_secondary + + # ParticleProductionFilter tracking fission neutron production + ppf = openmc.ParticleProductionFilter(['neutron']) + neutron_filter = openmc.ParticleFilter(['neutron']) + tally = openmc.Tally() + tally.filters = [neutron_filter, ppf] + tally.scores = ['events'] + tally.estimator = 'analog' + model.tallies = [tally] + + harness = PyAPITestHarness('statepoint.2.h5', model) + harness.main() diff --git a/tests/regression_tests/particle_restart_fixed_shared_secondary/__init__.py b/tests/regression_tests/particle_restart_fixed_shared_secondary/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/particle_restart_fixed_shared_secondary/geometry.xml b/tests/regression_tests/particle_restart_fixed_shared_secondary/geometry.xml new file mode 100644 index 000000000..c86e016c6 --- /dev/null +++ b/tests/regression_tests/particle_restart_fixed_shared_secondary/geometry.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/tests/regression_tests/particle_restart_fixed_shared_secondary/materials.xml b/tests/regression_tests/particle_restart_fixed_shared_secondary/materials.xml new file mode 100644 index 000000000..f3851d7ef --- /dev/null +++ b/tests/regression_tests/particle_restart_fixed_shared_secondary/materials.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/tests/regression_tests/particle_restart_fixed_shared_secondary/results_true.dat b/tests/regression_tests/particle_restart_fixed_shared_secondary/results_true.dat new file mode 100644 index 000000000..0c84541f9 --- /dev/null +++ b/tests/regression_tests/particle_restart_fixed_shared_secondary/results_true.dat @@ -0,0 +1,16 @@ +current batch: +4.000000E+00 +current generation: +1.000000E+00 +particle id: +3.241000E+03 +run mode: +fixed source +particle weight: +1.000000E+00 +particle energy: +3.896365E+06 +particle xyz: +8.710681E-01 3.698823E+00 -2.286229E+00 +particle uvw: +-5.882735E-01 4.665422E-01 -6.605093E-01 diff --git a/tests/regression_tests/particle_restart_fixed_shared_secondary/settings.xml b/tests/regression_tests/particle_restart_fixed_shared_secondary/settings.xml new file mode 100644 index 000000000..3a80fb17b --- /dev/null +++ b/tests/regression_tests/particle_restart_fixed_shared_secondary/settings.xml @@ -0,0 +1,15 @@ + + + + fixed source + 12 + 1000 + true + + + + -10 -10 -5 10 10 5 + + + + diff --git a/tests/regression_tests/particle_restart_fixed_shared_secondary/test.py b/tests/regression_tests/particle_restart_fixed_shared_secondary/test.py new file mode 100644 index 000000000..770010900 --- /dev/null +++ b/tests/regression_tests/particle_restart_fixed_shared_secondary/test.py @@ -0,0 +1,6 @@ +from tests.testing_harness import ParticleRestartTestHarness + + +def test_particle_restart_fixed_shared_secondary(): + harness = ParticleRestartTestHarness('particle_4_3241.h5') + harness.main() diff --git a/tests/regression_tests/pulse_height/inputs_true.dat b/tests/regression_tests/pulse_height/local/inputs_true.dat similarity index 95% rename from tests/regression_tests/pulse_height/inputs_true.dat rename to tests/regression_tests/pulse_height/local/inputs_true.dat index 590928e43..50e026bfb 100644 --- a/tests/regression_tests/pulse_height/inputs_true.dat +++ b/tests/regression_tests/pulse_height/local/inputs_true.dat @@ -18,14 +18,12 @@ 100 5 - - 0.0 0.0 0.0 - 1000000.0 1.0 true + false diff --git a/tests/regression_tests/pulse_height/results_true.dat b/tests/regression_tests/pulse_height/local/results_true.dat similarity index 100% rename from tests/regression_tests/pulse_height/results_true.dat rename to tests/regression_tests/pulse_height/local/results_true.dat diff --git a/tests/regression_tests/pulse_height/shared/inputs_true.dat b/tests/regression_tests/pulse_height/shared/inputs_true.dat new file mode 100644 index 000000000..c2aef829c --- /dev/null +++ b/tests/regression_tests/pulse_height/shared/inputs_true.dat @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + fixed source + 100 + 5 + + + 1000000.0 1.0 + + + true + true + + + + 1 + + + 0.0 10000.0 20000.0 30000.0 40000.0 50000.0 60000.0 70000.0 80000.0 90000.0 100000.0 110000.0 120000.0 130000.0 140000.0 150000.0 160000.0 170000.0 180000.0 190000.0 200000.0 210000.0 220000.0 230000.0 240000.0 250000.0 260000.0 270000.0 280000.0 290000.0 300000.0 310000.0 320000.0 330000.0 340000.0 350000.0 360000.0 370000.0 380000.0 390000.0 400000.0 410000.0 420000.0 430000.0 440000.0 450000.0 460000.0 470000.0 480000.0 490000.0 500000.0 510000.0 520000.0 530000.0 540000.0 550000.0 560000.0 570000.0 580000.0 590000.0 600000.0 610000.0 620000.0 630000.0 640000.0 650000.0 660000.0 670000.0 680000.0 690000.0 700000.0 710000.0 720000.0 730000.0 740000.0 750000.0 760000.0 770000.0 780000.0 790000.0 800000.0 810000.0 820000.0 830000.0 840000.0 850000.0 860000.0 870000.0 880000.0 890000.0 900000.0 910000.0 920000.0 930000.0 940000.0 950000.0 960000.0 970000.0 980000.0 990000.0 1000000.0 + + + 1 2 + pulse-height + + + diff --git a/tests/regression_tests/pulse_height/shared/results_true.dat b/tests/regression_tests/pulse_height/shared/results_true.dat new file mode 100644 index 000000000..c57e8ff1c --- /dev/null +++ b/tests/regression_tests/pulse_height/shared/results_true.dat @@ -0,0 +1,201 @@ +tally 1: +4.140000E+00 +3.443000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +3.000000E-02 +5.000000E-04 +2.000000E-02 +4.000000E-04 +2.000000E-02 +4.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.000000E-02 +3.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.000000E-02 +6.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +2.000000E-02 +4.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +3.000000E-02 +5.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +2.000000E-02 +2.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +2.000000E-02 +2.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +3.000000E-02 +5.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-01 +8.600000E-03 diff --git a/tests/regression_tests/pulse_height/test.py b/tests/regression_tests/pulse_height/test.py index 90d960f66..2cbb9152e 100644 --- a/tests/regression_tests/pulse_height/test.py +++ b/tests/regression_tests/pulse_height/test.py @@ -1,53 +1,53 @@ import numpy as np import openmc import pytest +from openmc.utility_funcs import change_directory from tests.testing_harness import PyAPITestHarness -@pytest.fixture -def sphere_model(): - - model = openmc.model.Model() +@pytest.mark.parametrize("shared_secondary,subdir", [ + (False, "local"), + (True, "shared"), +]) +def test_pulse_height(shared_secondary, subdir): + with change_directory(subdir): + openmc.reset_auto_ids() + model = openmc.Model() - # Define materials - NaI = openmc.Material() - NaI.set_density('g/cc', 3.7) - NaI.add_element('Na', 1.0) - NaI.add_element('I', 1.0) + # Define materials + NaI = openmc.Material() + NaI.set_density('g/cc', 3.7) + NaI.add_element('Na', 1.0) + NaI.add_element('I', 1.0) - model.materials = openmc.Materials([NaI]) + model.materials = openmc.Materials([NaI]) - # Define geometry: two spheres in each other - s1 = openmc.Sphere(r=1) - s2 = openmc.Sphere(r=2, boundary_type='vacuum') - inner_sphere = openmc.Cell(name='inner sphere', fill=NaI, region=-s1) - outer_sphere = openmc.Cell(name='outer sphere', region=+s1 & -s2) - model.geometry = openmc.Geometry([inner_sphere, outer_sphere]) + # Define geometry: two spheres in each other + s1 = openmc.Sphere(r=1) + s2 = openmc.Sphere(r=2, boundary_type='vacuum') + inner_sphere = openmc.Cell(name='inner sphere', fill=NaI, region=-s1) + outer_sphere = openmc.Cell(name='outer sphere', region=+s1 & -s2) + model.geometry = openmc.Geometry([inner_sphere, outer_sphere]) - # Define settings - model.settings.run_mode = 'fixed source' - model.settings.batches = 5 - model.settings.particles = 100 - model.settings.photon_transport = True - model.settings.source = openmc.IndependentSource( - space=openmc.stats.Point(), - energy=openmc.stats.Discrete([1e6], [1]), - particle='photon' - ) + # Define settings + model.settings.run_mode = 'fixed source' + model.settings.batches = 5 + model.settings.particles = 100 + model.settings.photon_transport = True + model.settings.shared_secondary_bank = shared_secondary + model.settings.source = openmc.IndependentSource( + energy=openmc.stats.delta_function(1e6), + particle='photon' + ) - # Define tallies - tally = openmc.Tally(name="pht tally") - tally.scores = ['pulse-height'] - cell_filter = openmc.CellFilter(inner_sphere) - energy_filter = openmc.EnergyFilter(np.linspace(0, 1_000_000, 101)) - tally.filters = [cell_filter, energy_filter] - model.tallies = [tally] + # Define tallies + tally = openmc.Tally(name="pht tally") + tally.scores = ['pulse-height'] + cell_filter = openmc.CellFilter(inner_sphere) + energy_filter = openmc.EnergyFilter(np.linspace(0, 1_000_000, 101)) + tally.filters = [cell_filter, energy_filter] + model.tallies = [tally] - return model - - - -def test_pulse_height(sphere_model): - harness = PyAPITestHarness('statepoint.5.h5', sphere_model) - harness.main() + harness = PyAPITestHarness('statepoint.5.h5', model) + harness.main() diff --git a/tests/regression_tests/weightwindows/local/inputs_true.dat b/tests/regression_tests/weightwindows/local/inputs_true.dat new file mode 100644 index 000000000..57dc42ca4 --- /dev/null +++ b/tests/regression_tests/weightwindows/local/inputs_true.dat @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + fixed source + 500 + 2 + + + 0.001 0.001 0.001 + + + 14000000.0 1.0 + + + true + + 2 + neutron + 0.0 0.5 20000000.0 + -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -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.0008758251780046591 -1.0 -1.0 -1.0 -1.0 0.00044494017319042853 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 1.6620271125067025e-05 0.0006278777700923334 3.3816651814344154e-05 -1.0 -1.0 0.0024363004681119066 0.04404124277352227 0.002389900091734746 -1.0 -1.0 0.001096572213298872 0.04862206884590391 0.0011530054432113332 -1.0 -1.0 -1.0 0.00029204950777272335 2.2332845991385424e-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.0001298973206651331 0.0028826281529980655 0.00018941326535850932 -1.0 5.4657521927731274e-05 0.014670839729146354 0.49999999999999994 0.011271060592765664 -1.0 -1.0 0.014846491082523524 0.4897211700090108 0.013284874451810723 -1.0 -1.0 5.14657989105535e-05 0.0010115278438204965 0.0008429411802845685 -1.0 -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.0009344129479768359 -1.0 -1.0 -1.0 0.0024828273390431962 0.04299740304329489 0.0020539079252555113 -1.0 -1.0 0.003034165905819096 0.04870927636605937 0.00313101668086297 -1.0 -1.0 -1.0 6.339910999436737e-05 7.442176086066386e-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 -1.0 -1.0 -1.0 -1.0 -1.0 0.00011276372995589871 0.0002236100157024887 9.56304298913265e-08 -1.0 -1.0 0.0001596955110781239 9.960576598335084e-06 5.3833030623153676e-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 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 4.0453018186074215e-05 4.6013290110121894e-05 2.709738801641897e-05 -1.0 -1.0 -1.0 9.538410811938703e-05 1.992978141244964e-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 -1.0 -1.0 -1.0 -1.0 -1.0 1.0723231851298364e-05 -1.0 -1.0 -1.0 -1.0 0.0008030100296698905 0.017386220476187222 0.0005027758935317528 -1.0 -1.0 0.00103568411326969 0.015609071124009234 0.0007473731339616588 -1.0 -1.0 -1.0 6.979537549963374e-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 -1.0 -1.0 0.0014257756927429828 5.9098070539559466e-05 -1.0 5.0168071459366625e-06 0.005588863190166347 0.5 0.003930588100414563 -1.0 -1.0 0.005163345217476071 0.48250750993887326 0.003510432129522241 -1.0 -1.0 3.371977841720992e-05 0.0006640103276501794 9.554899988419713e-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 -1.0 0.000187872910697387 -1.0 -1.0 -1.0 0.0009134587866163143 0.017081858078684467 0.0002972747357542354 -1.0 -1.0 0.0007973472495507653 0.019300903254809747 0.000902203032280694 -1.0 -1.0 6.170015233787292e-05 1.898984300674969e-05 9.85411583595722e-07 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 1.0173913765490095e-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 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 + -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.008758251780046591 -10.0 -10.0 -10.0 -10.0 0.004449401731904285 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00016620271125067024 0.006278777700923334 0.00033816651814344155 -10.0 -10.0 0.024363004681119065 0.4404124277352227 0.02389900091734746 -10.0 -10.0 0.01096572213298872 0.4862206884590391 0.011530054432113333 -10.0 -10.0 -10.0 0.0029204950777272335 0.00022332845991385425 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.0012989732066513312 0.028826281529980655 0.0018941326535850931 -10.0 0.0005465752192773128 0.14670839729146354 4.999999999999999 0.11271060592765664 -10.0 -10.0 0.14846491082523525 4.897211700090108 0.13284874451810724 -10.0 -10.0 0.000514657989105535 0.010115278438204964 0.008429411802845685 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00934412947976836 -10.0 -10.0 -10.0 0.024828273390431962 0.4299740304329489 0.020539079252555114 -10.0 -10.0 0.03034165905819096 0.48709276366059373 0.0313101668086297 -10.0 -10.0 -10.0 0.0006339910999436737 0.0007442176086066385 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.0011276372995589871 0.002236100157024887 9.56304298913265e-07 -10.0 -10.0 0.001596955110781239 9.960576598335084e-05 0.0005383303062315367 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00040453018186074213 0.00046013290110121896 0.0002709738801641897 -10.0 -10.0 -10.0 0.0009538410811938703 0.00019929781412449641 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00010723231851298364 -10.0 -10.0 -10.0 -10.0 0.008030100296698905 0.17386220476187222 0.0050277589353175285 -10.0 -10.0 0.010356841132696899 0.15609071124009233 0.007473731339616587 -10.0 -10.0 -10.0 0.0006979537549963374 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.014257756927429827 0.0005909807053955946 -10.0 5.016807145936663e-05 0.055888631901663474 5.0 0.039305881004145636 -10.0 -10.0 0.05163345217476071 4.825075099388733 0.03510432129522241 -10.0 -10.0 0.00033719778417209923 0.006640103276501793 0.0009554899988419713 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00187872910697387 -10.0 -10.0 -10.0 0.009134587866163143 0.17081858078684467 0.002972747357542354 -10.0 -10.0 0.007973472495507653 0.19300903254809748 0.009022030322806941 -10.0 -10.0 0.0006170015233787292 0.0001898984300674969 9.85411583595722e-06 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00010173913765490096 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 + 3.0 + 1.5 + 10 + 1e-38 + + + 5 6 7 + -240 -240 -240 + 240 240 240 + + + 2 + photon + 0.0 0.5 20000000.0 + -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -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.0445691096074744e-05 -1.0 -1.0 -1.0 0.00013281328509288383 0.0006267128082339883 -1.0 -1.0 -1.0 -1.0 0.0004209808495056742 5.340221214300681e-05 -1.0 -1.0 -1.0 1.553693492042344e-05 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 3.7096847855980484e-05 -1.0 -1.0 5.41969614042385e-05 0.0007918090215756442 9.763572147337743e-05 -1.0 -1.0 0.0026398319229115233 0.04039871468258457 0.002806654735003014 -1.0 -1.0 0.0027608366482897244 0.040190978087723005 0.0025084416103979975 -1.0 -1.0 9.774868433298412e-05 0.0006126404916879651 0.00014841730774317252 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 9.75342408717107e-06 1.7610849405640072e-05 -1.0 -1.0 0.0001160370359598608 0.002822592083222468 0.0003897177301239376 -1.0 -1.0 0.014965179733172532 0.5 0.011723491704290401 -1.0 2.260493623875525e-05 0.015157364795884922 0.49376993953402387 0.013111419473204967 -1.0 -1.0 0.00014919915366377564 0.002197964829133137 0.0003209711829219673 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 1.4806422867046436e-05 -1.0 -1.0 -1.0 9.207268175745281e-05 0.0007066033561638983 -1.0 -1.0 -1.0 0.003246064903858183 0.03905493028065908 0.0025380574501073605 -1.0 -1.0 0.0032499260211917933 0.04335567738779479 0.0031577214557017056 4.521352809838806e-05 -1.0 0.00018268090756001903 0.0004353654810741932 0.00011273259573092372 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 9.650194311662424e-06 0.00029403373970835075 0.00013506203419326438 -1.0 -1.0 4.5065302725431816e-06 0.00027725323638744237 3.547290864255937e-05 -1.0 -1.0 -1.0 8.786172732576295e-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 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 + -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00020445691096074745 -10.0 -10.0 -10.0 0.0013281328509288383 0.006267128082339883 -10.0 -10.0 -10.0 -10.0 0.004209808495056742 0.0005340221214300681 -10.0 -10.0 -10.0 0.00015536934920423439 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00037096847855980485 -10.0 -10.0 0.000541969614042385 0.007918090215756443 0.0009763572147337743 -10.0 -10.0 0.026398319229115234 0.4039871468258457 0.02806654735003014 -10.0 -10.0 0.027608366482897245 0.40190978087723006 0.025084416103979976 -10.0 -10.0 0.0009774868433298411 0.0061264049168796506 0.0014841730774317252 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 9.753424087171071e-05 0.00017610849405640073 -10.0 -10.0 0.001160370359598608 0.02822592083222468 0.003897177301239376 -10.0 -10.0 0.14965179733172532 5.0 0.11723491704290401 -10.0 0.0002260493623875525 0.15157364795884923 4.937699395340239 0.13111419473204966 -10.0 -10.0 0.0014919915366377564 0.02197964829133137 0.003209711829219673 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 0.00014806422867046437 -10.0 -10.0 -10.0 0.0009207268175745281 0.007066033561638983 -10.0 -10.0 -10.0 0.03246064903858183 0.39054930280659084 0.025380574501073606 -10.0 -10.0 0.03249926021191793 0.4335567738779479 0.03157721455701706 0.0004521352809838806 -10.0 0.0018268090756001904 0.004353654810741932 0.001127325957309237 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 9.650194311662424e-05 0.0029403373970835075 0.0013506203419326437 -10.0 -10.0 4.5065302725431816e-05 0.002772532363874424 0.0003547290864255937 -10.0 -10.0 -10.0 0.0008786172732576295 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 -10.0 + 3.0 + 1.5 + 10 + 1e-38 + + false + + true + true + + 200 + + + + 5 10 15 + -240 -240 -240 + 240 240 240 + + + 1 + + + 0.0 0.5 20000000.0 + + + neutron photon + + + 1 2 3 + flux + + + diff --git a/tests/regression_tests/weightwindows/local/results_true.dat b/tests/regression_tests/weightwindows/local/results_true.dat new file mode 100644 index 000000000..d3dfa119d --- /dev/null +++ b/tests/regression_tests/weightwindows/local/results_true.dat @@ -0,0 +1 @@ +a78972fe3c0dadfc256cfb139c5ca8c7b634738e1895ad2d6286ed5e2567c6dc518e499363908e9058432684148a706a179a39110f703d5322491184a1d0c3e4 \ No newline at end of file diff --git a/tests/regression_tests/weightwindows/results_true.dat b/tests/regression_tests/weightwindows/results_true.dat deleted file mode 100644 index 897089e05..000000000 --- a/tests/regression_tests/weightwindows/results_true.dat +++ /dev/null @@ -1 +0,0 @@ -a4a3ccca43666e2ca1e71800201b152cca20c387b93d67522c5339807348dcee5cada9acbed3238f37e2e86e76b374b06988742f07d4ea1b413e4e75d0c180b1 \ No newline at end of file diff --git a/tests/regression_tests/weightwindows/inputs_true.dat b/tests/regression_tests/weightwindows/shared/inputs_true.dat similarity index 99% rename from tests/regression_tests/weightwindows/inputs_true.dat rename to tests/regression_tests/weightwindows/shared/inputs_true.dat index aa7a0379d..6ec7d12e3 100644 --- a/tests/regression_tests/weightwindows/inputs_true.dat +++ b/tests/regression_tests/weightwindows/shared/inputs_true.dat @@ -64,6 +64,7 @@ 10 1e-38 + true true true diff --git a/tests/regression_tests/weightwindows/shared/results_true.dat b/tests/regression_tests/weightwindows/shared/results_true.dat new file mode 100644 index 000000000..01381db32 --- /dev/null +++ b/tests/regression_tests/weightwindows/shared/results_true.dat @@ -0,0 +1 @@ +27c2d218ecf46161088003fbb39ccd63495f80d6e0b27dbc3b3e845b49b282dabf0eaba4fb619be15c6116fc963ec5ce1fe4197efbe4084972ec761e02e70eb5 \ No newline at end of file diff --git a/tests/regression_tests/weightwindows/survival_biasing/local/inputs_true.dat b/tests/regression_tests/weightwindows/survival_biasing/local/inputs_true.dat new file mode 100644 index 000000000..fe144dc62 --- /dev/null +++ b/tests/regression_tests/weightwindows/survival_biasing/local/inputs_true.dat @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + 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 + + false + + true + true + + + + + 1 + + + 1 + flux + + + diff --git a/tests/regression_tests/weightwindows/survival_biasing/results_true.dat b/tests/regression_tests/weightwindows/survival_biasing/local/results_true.dat similarity index 100% rename from tests/regression_tests/weightwindows/survival_biasing/results_true.dat rename to tests/regression_tests/weightwindows/survival_biasing/local/results_true.dat diff --git a/tests/regression_tests/weightwindows/survival_biasing/inputs_true.dat b/tests/regression_tests/weightwindows/survival_biasing/shared/inputs_true.dat similarity index 99% rename from tests/regression_tests/weightwindows/survival_biasing/inputs_true.dat rename to tests/regression_tests/weightwindows/survival_biasing/shared/inputs_true.dat index d5aa135d4..bafff89c1 100644 --- a/tests/regression_tests/weightwindows/survival_biasing/inputs_true.dat +++ b/tests/regression_tests/weightwindows/survival_biasing/shared/inputs_true.dat @@ -51,6 +51,7 @@ 0.0 0.0 0.0 160.0 160.0 160.0 + true true true diff --git a/tests/regression_tests/weightwindows/survival_biasing/shared/results_true.dat b/tests/regression_tests/weightwindows/survival_biasing/shared/results_true.dat new file mode 100644 index 000000000..11e5ffa77 --- /dev/null +++ b/tests/regression_tests/weightwindows/survival_biasing/shared/results_true.dat @@ -0,0 +1 @@ +d17a437262d3316985fba4b48e21a7fcd5f32ac2d96ef0e66849f567deaeadc1e0f18d5d0bf5e9cab4246dbf823e7f43f249a1f67e928b27ae8c70b89f3cefbf \ 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 index 92604e338..c3a3e91a2 100644 --- a/tests/regression_tests/weightwindows/survival_biasing/test.py +++ b/tests/regression_tests/weightwindows/survival_biasing/test.py @@ -1,14 +1,16 @@ +from pathlib import Path + import pytest import numpy as np - import openmc -from openmc.stats import Discrete, Point +from openmc.utility_funcs import change_directory from tests.testing_harness import HashedPyAPITestHarness -@pytest.fixture -def model(): +def build_model(shared_secondary): + openmc.reset_auto_ids() + # Material w = openmc.Material(name='Tungsten') w.add_element('W', 1.0) @@ -38,13 +40,14 @@ def model(): 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) + source = openmc.IndependentSource(space=space, angle=angle, energy=energy) settings = openmc.Settings() settings.run_mode = 'fixed source' settings.batches = 5 settings.particles = 50 settings.source = source + settings.shared_secondary_bank = shared_secondary model = openmc.Model(geometry=geometry, materials=materials, settings=settings) @@ -61,7 +64,8 @@ def model(): tallies = openmc.Tallies([flux_tally]) model.tallies = tallies - lower_ww_bounds = np.loadtxt('ww_n.txt') + parent_dir = Path(__file__).parent + lower_ww_bounds = np.loadtxt(parent_dir / 'ww_n.txt') weight_windows = openmc.WeightWindows(mesh, lower_ww_bounds, @@ -76,6 +80,12 @@ def model(): return model -def test_weight_windows_with_survival_biasing(model): - harness = HashedPyAPITestHarness('statepoint.5.h5', model) - harness.main() +@pytest.mark.parametrize("shared_secondary,subdir", [ + (False, "local"), + (True, "shared"), +]) +def test_weight_windows_with_survival_biasing(shared_secondary, subdir): + with change_directory(subdir): + model = build_model(shared_secondary) + harness = HashedPyAPITestHarness('statepoint.5.h5', model) + harness.main() diff --git a/tests/regression_tests/weightwindows/test.py b/tests/regression_tests/weightwindows/test.py index c22ca1b75..1d3b063bd 100644 --- a/tests/regression_tests/weightwindows/test.py +++ b/tests/regression_tests/weightwindows/test.py @@ -1,14 +1,17 @@ +from pathlib import Path + import pytest import numpy as np import openmc from openmc.stats import Discrete, Point +from openmc.utility_funcs import change_directory from tests.testing_harness import HashedPyAPITestHarness -@pytest.fixture -def model(): +def build_model(shared_secondary): + openmc.reset_auto_ids() model = openmc.Model() # materials (M4 steel alloy) @@ -43,6 +46,7 @@ def model(): settings.batches = 2 settings.max_history_splits = 200 settings.photon_transport = True + settings.shared_secondary_bank = shared_secondary settings.weight_window_checkpoints = {'surface': True, 'collision': True} space = Point((0.001, 0.001, 0.001)) @@ -71,10 +75,10 @@ def model(): # weight windows - # load pre-generated weight windows - # (created using the same tally as above) - ww_n_lower_bnds = np.loadtxt('ww_n.txt') - ww_p_lower_bnds = np.loadtxt('ww_p.txt') + # load pre-generated weight windows from parent directory + parent_dir = Path(__file__).parent + ww_n_lower_bnds = np.loadtxt(parent_dir / 'ww_n.txt') + ww_p_lower_bnds = np.loadtxt(parent_dir / 'ww_p.txt') # create a mesh matching the one used # to generate the weight windows @@ -104,9 +108,15 @@ def model(): return model -def test_weightwindows(model): - test = HashedPyAPITestHarness('statepoint.2.h5', model) - test.main() +@pytest.mark.parametrize("shared_secondary,subdir", [ + (False, "local"), + (True, "shared"), +]) +def test_weightwindows(shared_secondary, subdir): + with change_directory(subdir): + model = build_model(shared_secondary) + test = HashedPyAPITestHarness('statepoint.2.h5', model) + test.main() def test_wwinp_cylindrical(): diff --git a/tests/regression_tests/weightwindows_pulse_height/__init__.py b/tests/regression_tests/weightwindows_pulse_height/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/weightwindows_pulse_height/local/inputs_true.dat b/tests/regression_tests/weightwindows_pulse_height/local/inputs_true.dat new file mode 100644 index 000000000..4cd24d529 --- /dev/null +++ b/tests/regression_tests/weightwindows_pulse_height/local/inputs_true.dat @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + fixed source + 100 + 5 + + + 1000000.0 1.0 + + + true + + 1 + photon + 0.0 2000000.0 + 0.01 + 0.05 + 3.0 + 10 + 1e-38 + + + 1 1 1 + -2 -2 -2 + 2 2 2 + + false + + true + true + + 50 + + + + 1 + + + 0.0 10000.0 20000.0 30000.0 40000.0 50000.0 60000.0 70000.0 80000.0 90000.0 100000.0 110000.0 120000.0 130000.0 140000.0 150000.0 160000.0 170000.0 180000.0 190000.0 200000.0 210000.0 220000.0 230000.0 240000.0 250000.0 260000.0 270000.0 280000.0 290000.0 300000.0 310000.0 320000.0 330000.0 340000.0 350000.0 360000.0 370000.0 380000.0 390000.0 400000.0 410000.0 420000.0 430000.0 440000.0 450000.0 460000.0 470000.0 480000.0 490000.0 500000.0 510000.0 520000.0 530000.0 540000.0 550000.0 560000.0 570000.0 580000.0 590000.0 600000.0 610000.0 620000.0 630000.0 640000.0 650000.0 660000.0 670000.0 680000.0 690000.0 700000.0 710000.0 720000.0 730000.0 740000.0 750000.0 760000.0 770000.0 780000.0 790000.0 800000.0 810000.0 820000.0 830000.0 840000.0 850000.0 860000.0 870000.0 880000.0 890000.0 900000.0 910000.0 920000.0 930000.0 940000.0 950000.0 960000.0 970000.0 980000.0 990000.0 1000000.0 + + + 1 2 + pulse-height + + + diff --git a/tests/regression_tests/weightwindows_pulse_height/local/results_true.dat b/tests/regression_tests/weightwindows_pulse_height/local/results_true.dat new file mode 100644 index 000000000..c57e8ff1c --- /dev/null +++ b/tests/regression_tests/weightwindows_pulse_height/local/results_true.dat @@ -0,0 +1,201 @@ +tally 1: +4.140000E+00 +3.443000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +3.000000E-02 +5.000000E-04 +2.000000E-02 +4.000000E-04 +2.000000E-02 +4.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.000000E-02 +3.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.000000E-02 +6.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +2.000000E-02 +4.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +3.000000E-02 +5.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +2.000000E-02 +2.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +2.000000E-02 +2.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +3.000000E-02 +5.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-01 +8.600000E-03 diff --git a/tests/regression_tests/weightwindows_pulse_height/shared/inputs_true.dat b/tests/regression_tests/weightwindows_pulse_height/shared/inputs_true.dat new file mode 100644 index 000000000..8bb499a73 --- /dev/null +++ b/tests/regression_tests/weightwindows_pulse_height/shared/inputs_true.dat @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + fixed source + 100 + 5 + + + 1000000.0 1.0 + + + true + + 1 + photon + 0.0 2000000.0 + 0.01 + 0.05 + 3.0 + 10 + 1e-38 + + + 1 1 1 + -2 -2 -2 + 2 2 2 + + true + + true + true + + 50 + + + + 1 + + + 0.0 10000.0 20000.0 30000.0 40000.0 50000.0 60000.0 70000.0 80000.0 90000.0 100000.0 110000.0 120000.0 130000.0 140000.0 150000.0 160000.0 170000.0 180000.0 190000.0 200000.0 210000.0 220000.0 230000.0 240000.0 250000.0 260000.0 270000.0 280000.0 290000.0 300000.0 310000.0 320000.0 330000.0 340000.0 350000.0 360000.0 370000.0 380000.0 390000.0 400000.0 410000.0 420000.0 430000.0 440000.0 450000.0 460000.0 470000.0 480000.0 490000.0 500000.0 510000.0 520000.0 530000.0 540000.0 550000.0 560000.0 570000.0 580000.0 590000.0 600000.0 610000.0 620000.0 630000.0 640000.0 650000.0 660000.0 670000.0 680000.0 690000.0 700000.0 710000.0 720000.0 730000.0 740000.0 750000.0 760000.0 770000.0 780000.0 790000.0 800000.0 810000.0 820000.0 830000.0 840000.0 850000.0 860000.0 870000.0 880000.0 890000.0 900000.0 910000.0 920000.0 930000.0 940000.0 950000.0 960000.0 970000.0 980000.0 990000.0 1000000.0 + + + 1 2 + pulse-height + + + diff --git a/tests/regression_tests/weightwindows_pulse_height/shared/results_true.dat b/tests/regression_tests/weightwindows_pulse_height/shared/results_true.dat new file mode 100644 index 000000000..c57e8ff1c --- /dev/null +++ b/tests/regression_tests/weightwindows_pulse_height/shared/results_true.dat @@ -0,0 +1,201 @@ +tally 1: +4.140000E+00 +3.443000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +3.000000E-02 +5.000000E-04 +2.000000E-02 +4.000000E-04 +2.000000E-02 +4.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.000000E-02 +3.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.000000E-02 +6.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +2.000000E-02 +4.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +3.000000E-02 +5.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +2.000000E-02 +2.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +2.000000E-02 +2.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +3.000000E-02 +5.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-02 +2.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.000000E-01 +8.600000E-03 diff --git a/tests/regression_tests/weightwindows_pulse_height/test.py b/tests/regression_tests/weightwindows_pulse_height/test.py new file mode 100644 index 000000000..b6b49be26 --- /dev/null +++ b/tests/regression_tests/weightwindows_pulse_height/test.py @@ -0,0 +1,73 @@ +import numpy as np +import openmc +import pytest +from openmc.utility_funcs import change_directory + +from tests.testing_harness import PyAPITestHarness + + +@pytest.mark.parametrize("shared_secondary,subdir", [ + (False, "local"), + (True, "shared"), +]) +def test_weightwindows_pulse_height(shared_secondary, subdir): + with change_directory(subdir): + openmc.reset_auto_ids() + model = openmc.Model() + + # Define materials (NaI scintillator) + NaI = openmc.Material() + NaI.set_density('g/cc', 3.7) + NaI.add_element('Na', 1.0) + NaI.add_element('I', 1.0) + + model.materials = openmc.Materials([NaI]) + + # Define geometry: NaI sphere inside vacuum sphere + s1 = openmc.Sphere(r=1) + s2 = openmc.Sphere(r=2, boundary_type='vacuum') + inner_sphere = openmc.Cell(name='inner sphere', fill=NaI, region=-s1) + outer_sphere = openmc.Cell(name='outer sphere', region=+s1 & -s2) + model.geometry = openmc.Geometry([inner_sphere, outer_sphere]) + + # Define settings + model.settings.run_mode = 'fixed source' + model.settings.batches = 5 + model.settings.particles = 100 + model.settings.photon_transport = True + model.settings.shared_secondary_bank = shared_secondary + model.settings.max_history_splits = 50 + model.settings.weight_window_checkpoints = { + 'surface': True, + 'collision': True, + } + model.settings.source = openmc.IndependentSource( + energy=openmc.stats.delta_function(1e6), + particle='photon' + ) + + # Define pulse-height tally + tally = openmc.Tally(name="pht tally") + tally.scores = ['pulse-height'] + cell_filter = openmc.CellFilter(inner_sphere) + energy_filter = openmc.EnergyFilter(np.linspace(0, 1_000_000, 101)) + tally.filters = [cell_filter, energy_filter] + model.tallies = [tally] + + # Define weight windows on a simple mesh covering the geometry + ww_mesh = openmc.RegularMesh() + ww_mesh.lower_left = (-2, -2, -2) + ww_mesh.upper_right = (2, 2, 2) + ww_mesh.dimension = (1, 1, 1) + + # Single energy bin for photons + e_bnds = [0.0, 2e6] + + # Uniform weight window bounds (low enough to trigger some splitting) + lower_bounds = np.array([0.01]) + + ww = openmc.WeightWindows(ww_mesh, lower_bounds, None, 5.0, e_bnds, 'photon') + model.settings.weight_windows = [ww] + + harness = PyAPITestHarness('statepoint.5.h5', model) + harness.main() diff --git a/tests/unit_tests/weightwindows/test.py b/tests/unit_tests/weightwindows/test.py index d6e509522..efa203b51 100644 --- a/tests/unit_tests/weightwindows/test.py +++ b/tests/unit_tests/weightwindows/test.py @@ -122,7 +122,8 @@ def model(): return model -def test_weightwindows(model, wws): +@pytest.mark.parametrize("shared_secondary", [False, True]) +def test_weightwindows(model, wws, shared_secondary): ww_files = ('ww_n.txt', 'ww_p.txt') cwd = Path(__file__).parent.absolute() @@ -131,6 +132,7 @@ def test_weightwindows(model, wws): with cdtemp(filepaths): # run once with variance reduction off model.settings.weight_windows_on = False + model.settings.shared_secondary_bank = shared_secondary analog_sp = model.run() os.rename(analog_sp, 'statepoint.analog.h5') @@ -223,7 +225,8 @@ def test_lower_ww_bounds_shape(): assert ww.lower_ww_bounds.shape == (2, 3, 4, 1) -def test_photon_heating(run_in_tmpdir): +@pytest.mark.parametrize("shared_secondary", [False, True]) +def test_photon_heating(run_in_tmpdir, shared_secondary): water = openmc.Material() water.add_nuclide('H1', 1.0) water.add_nuclide('O16', 2.0) @@ -246,7 +249,8 @@ def test_photon_heating(run_in_tmpdir): model.settings.run_mode = 'fixed source' model.settings.batches = 5 - model.settings.particles = 100 + model.settings.particles = 101 + model.settings.shared_secondary_bank = shared_secondary tally = openmc.Tally() tally.scores = ['heating'] @@ -260,7 +264,11 @@ def test_photon_heating(run_in_tmpdir): with openmc.StatePoint(sp_file) as sp: tally_mean = sp.tallies[tally.id].mean - # these values should be nearly identical + # Note: Our current physics model actually does allow this tally to + # occasionally go slightly negative. However, larger bugs can + # make this more common. We have selected a particle count for + # this test that happens to produce no negative tallies for both + # the shared and non-shared secondary PRNG streams. assert np.all(tally_mean >= 0) From 3c7a030d43d79a9784982bc37a1c9c87cba5a375 Mon Sep 17 00:00:00 2001 From: Hridoy Kabiraj Date: Thu, 21 May 2026 00:49:00 +0600 Subject: [PATCH 624/671] Guard average molar mass and validate materials depletion inputs (#3941) --- openmc/material.py | 13 +++++++- tests/unit_tests/test_materials.py | 49 ++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/openmc/material.py b/openmc/material.py index 86cc3d793..eded9c3fa 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -296,6 +296,8 @@ class Material(IDManagerMixin): mass += nuc.percent # Compute and return the molar mass + if moles == 0.0: + raise ValueError("Material has no nuclides; cannot compute molar mass") return mass / moles @property @@ -2287,7 +2289,7 @@ class Materials(cv.CheckedList): multigroup_fluxes: Sequence[Sequence[float]] Energy-dependent multigroup flux values, where each sublist corresponds to a specific material. Will be normalized so that it sums to 1. - energy_group_structures': Sequence[Sequence[float] | str] + energy_group_structures: Sequence[Sequence[float] | str] Energy group boundaries in [eV] or the name of the group structure. timesteps : iterable of float or iterable of tuple Array of timesteps. Note that values are not cumulative. The units are @@ -2322,6 +2324,11 @@ class Materials(cv.CheckedList): for mat in self: mat.depletable = True + if len(multigroup_fluxes) != len(self): + raise ValueError("multigroup_fluxes length must match number of materials") + if len(energy_group_structures) != len(self): + raise ValueError("energy_group_structures length must match number of materials") + chain = _get_chain(chain_file) # Create MicroXS objects for all materials @@ -2332,6 +2339,10 @@ class Materials(cv.CheckedList): for material, flux, energy in zip( self, multigroup_fluxes, energy_group_structures ): + if material.volume is None: + raise ValueError( + f"Material {material.id} has no volume; cannot deplete" + ) temperature = material.temperature or 293.6 micro_xs = openmc.deplete.MicroXS.from_multigroup_flux( energies=energy, diff --git a/tests/unit_tests/test_materials.py b/tests/unit_tests/test_materials.py index 5a382b777..6620402fd 100644 --- a/tests/unit_tests/test_materials.py +++ b/tests/unit_tests/test_materials.py @@ -1,5 +1,7 @@ from pathlib import Path +import pytest + import openmc from openmc.deplete import Chain @@ -78,3 +80,50 @@ def test_export_duplicate_materials_to_xml(run_in_tmpdir): materials_in = openmc.Materials.from_xml("materials.xml") assert len(materials_in) == 2 + + +def test_materials_deplete_length_mismatch(): + mats = openmc.Materials([openmc.Material()]) + + with pytest.raises(ValueError, match="multigroup_fluxes length"): + mats.deplete( + multigroup_fluxes=[], + energy_group_structures=["VITAMIN-J-42"], + timesteps=[1.0], + source_rates=1.0, + ) + + with pytest.raises(ValueError, match="energy_group_structures length"): + mats.deplete( + multigroup_fluxes=[[1.0]], + energy_group_structures=[], + timesteps=[1.0], + source_rates=1.0, + ) + + +def test_materials_deplete_missing_volume(monkeypatch): + mat = openmc.Material() + mat.add_nuclide("Ni58", 1.0) + mat.set_density("g/cm3", 7.87) + + mats = openmc.Materials([mat]) + + class DummySession: + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + return False + + monkeypatch.setattr(openmc.lib, "TemporarySession", DummySession) + + chain = Path(__file__).parents[1] / "chain_ni.xml" + with pytest.raises(ValueError, match="has no volume"): + mats.deplete( + multigroup_fluxes=[[1.0]], + energy_group_structures=["VITAMIN-J-42"], + timesteps=[1.0], + source_rates=1.0, + chain_file=chain, + ) From 66497e76b11653554676c2ac305a14f764d82c12 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Wed, 20 May 2026 14:01:21 -0500 Subject: [PATCH 625/671] Support writing of hex elements to VTKHDF format. (#3623) Co-authored-by: Paul Romano --- docs/source/usersguide/processing.rst | 10 ++- openmc/mesh.py | 107 ++++++++++++-------------- tests/unit_tests/test_mesh.py | 45 ++++++++++- tests/unit_tests/test_mesh_hexes.exo | 1 + 4 files changed, 102 insertions(+), 61 deletions(-) create mode 120000 tests/unit_tests/test_mesh_hexes.exo diff --git a/docs/source/usersguide/processing.rst b/docs/source/usersguide/processing.rst index fe6ab0826..ed3e8564a 100644 --- a/docs/source/usersguide/processing.rst +++ b/docs/source/usersguide/processing.rst @@ -97,7 +97,15 @@ VTK Mesh File Generation ------------------------ VTK files of OpenMC meshes can be created using the -:meth:`openmc.Mesh.write_data_to_vtk` method. Data can be applied to the +:meth:`openmc.Mesh.write_data_to_vtk` method. This method supports several VTK +formats depending on the mesh type. Structured meshes +(:class:`~openmc.RegularMesh`, :class:`~openmc.RectilinearMesh`, +:class:`~openmc.CylindricalMesh`, and :class:`~openmc.SphericalMesh`) can be +exported to legacy VTK format (``.vtk``). The :class:`~openmc.UnstructuredMesh` +class supports VTK unstructured grid formats (``.vtu``) as well as an HDF5-based +format (``.vtkhdf``) that does not require the ``vtk`` module to write. + +Data can be applied to the elements of the resulting mesh from mesh filter objects. This data can be provided either as a flat array or, in the case of structured meshes (:class:`~openmc.RegularMesh`, :class:`~openmc.RectilinearMesh`, diff --git a/openmc/mesh.py b/openmc/mesh.py index 4129913d8..61effd850 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -2738,7 +2738,8 @@ class UnstructuredMesh(MeshBase): _UNSUPPORTED_ELEM = -1 _LINEAR_TET = 0 _LINEAR_HEX = 1 - _VTK_TETRA = 10 + _VTK_TET = 10 + _VTK_HEX = 12 def __init__(self, filename: PathLike, library: str, mesh_id: int | None = None, name: str = '', length_multiplier: float = 1.0, @@ -3049,7 +3050,9 @@ class UnstructuredMesh(MeshBase): n_skipped += 1 continue else: - raise RuntimeError(f"Invalid element type {elem_type} found") + raise RuntimeError( + f"Invalid element type {elem_type} found in mesh {self.id}" + ) for i, c in enumerate(conn): if c == -1: @@ -3106,35 +3109,42 @@ class UnstructuredMesh(MeshBase): datasets: dict | None = None, volume_normalization: bool = True, ): - def append_dataset(dset, array): - """Convenience function to append data to an HDF5 dataset""" - origLen = dset.shape[0] - dset.resize(origLen + array.shape[0], axis=0) - dset[origLen:] = array + # This writer supports linear tetrahedra and linear hexahedra elements + conn_list = [] # flattened connectivity ids + cell_sizes = [] # number of points per cell + vtk_types = [] # VTK cell types per cell (uint8) + n_skipped = 0 - if self.library != "moab": - raise NotImplementedError("VTKHDF output is only supported for MOAB meshes") - - # the self.connectivity contains arrays of length 8 to support hex - # elements as well, in the case of tetrahedra mesh elements, the - # last 4 values are -1 and are removed - trimmed_connectivity = [] - for cell in self.connectivity: - # Find the index of the first -1 value, if any - first_negative_index = np.where(cell == -1)[0] - if first_negative_index.size > 0: - # Slice the array up to the first -1 value - trimmed_connectivity.append(cell[: first_negative_index[0]]) + for conn, etype in zip(self.connectivity, self.element_types): + if etype == self._LINEAR_TET: + ids = conn[:4] + vtk_types.append(self._VTK_TET) + elif etype == self._LINEAR_HEX: + ids = conn[:8] + vtk_types.append(self._VTK_HEX) + elif etype == self._UNSUPPORTED_ELEM: + n_skipped += 1 + continue else: - # No -1 values, append the whole cell - trimmed_connectivity.append(cell) - trimmed_connectivity = np.array(trimmed_connectivity, dtype="int32").flatten() + raise RuntimeError( + f"Invalid element type {etype} found in mesh {self.id}" + ) + conn_list.extend(ids.tolist()) + cell_sizes.append(len(ids)) - # MOAB meshes supports tet elements only so we know it has 4 points per cell - points_per_cell = 4 + if n_skipped > 0: + warnings.warn( + f"{n_skipped} elements were not written because " + "they are not of type linear tet/hex" + ) - # offsets are the indices of the first point of each cell in the array of points - offsets = np.arange(0, self.n_elements * points_per_cell + 1, points_per_cell) + connectivity = np.asarray(conn_list, dtype=np.int64) + + # Offsets must be length (numCells + 1) with a leading 0 and + # cumulative end-indices thereafter, per VTK's layout + cell_sizes_arr = np.asarray(cell_sizes, dtype=np.int64) + offsets = np.zeros(cell_sizes_arr.size + 1, dtype=np.int64) + np.cumsum(cell_sizes_arr, out=offsets[1:]) for name, data in datasets.items(): if data.shape != self.dimension: @@ -3156,42 +3166,27 @@ class UnstructuredMesh(MeshBase): dtype=h5py.string_dtype("ascii", len(ascii_type)), ) - # create hdf5 file structure - root.create_dataset("NumberOfPoints", (0,), maxshape=(None,), dtype="i8") - root.create_dataset("Types", (0,), maxshape=(None,), dtype="uint8") - root.create_dataset("Points", (0, 3), maxshape=(None, 3), dtype="f") - root.create_dataset( - "NumberOfConnectivityIds", (0,), maxshape=(None,), dtype="i8" - ) - root.create_dataset("NumberOfCells", (0,), maxshape=(None,), dtype="i8") - root.create_dataset("Offsets", (0,), maxshape=(None,), dtype="i8") - root.create_dataset("Connectivity", (0,), maxshape=(None,), dtype="i8") + # Create HDF5 file structure compliant with VTKHDF UnstructuredGrid + n_points = int(len(self.vertices)) + n_cells = int(len(cell_sizes)) + n_conn_ids = int(len(connectivity)) - append_dataset(root["NumberOfPoints"], np.array([len(self.vertices)])) - append_dataset(root["Points"], self.vertices) - append_dataset( - root["NumberOfConnectivityIds"], - np.array([len(trimmed_connectivity)]), - ) - append_dataset(root["Connectivity"], trimmed_connectivity) - append_dataset(root["NumberOfCells"], np.array([self.n_elements])) - append_dataset(root["Offsets"], offsets) - - append_dataset( - root["Types"], np.full(self.n_elements, self._VTK_TETRA, dtype="uint8") - ) + root.create_dataset("NumberOfPoints", data=(n_points,), dtype="i8") + root.create_dataset("NumberOfCells", data=(n_cells,), dtype="i8") + root.create_dataset("NumberOfConnectivityIds", data=(n_conn_ids,), dtype="i8") + root.create_dataset("Points", data=self.vertices.astype(np.float64, copy=False), dtype="f8") + root.create_dataset("Types", data=np.asarray(vtk_types, dtype=np.uint8), dtype="uint8") + root.create_dataset("Offsets", data=offsets.astype("i8"), dtype="i8") + root.create_dataset("Connectivity", data=connectivity.astype("i8"), dtype="i8") cell_data_group = root.create_group("CellData") for name, data in datasets.items(): - - cell_data_group.create_dataset( - name, (0,), maxshape=(None,), dtype="float64", chunks=True - ) - if volume_normalization: data /= self.volumes - append_dataset(cell_data_group[name], data) + cell_data_group.create_dataset( + name, data=data, dtype="float64", chunks=True + ) @classmethod def from_hdf5(cls, group: h5py.Group, mesh_id: int, name: str): diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index 0b28bdfbe..85cb68a83 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -490,13 +490,28 @@ def test_umesh(run_in_tmpdir, simple_umesh, export_type): simple_umesh.write_data_to_vtk(datasets={'mean': ref_data[:-2]}, filename=filename) -@pytest.mark.skipif(not openmc.lib._dagmc_enabled(), reason="DAGMC not enabled.") -def test_write_vtkhdf(request, run_in_tmpdir): +vtkhdf_tests = [ + ( + Path("test_mesh_dagmc_tets.vtk"), + "moab" + ), + ( + Path("test_mesh_hexes.exo"), + "libmesh" + ) +] +@pytest.mark.parametrize('mesh_file, mesh_library', vtkhdf_tests) +def test_write_vtkhdf(mesh_file, mesh_library, request, run_in_tmpdir): """Performs a minimal UnstructuredMesh simulation, reads in the resulting statepoint file and writes the mesh data to vtk and vtkhdf files. It is necessary to read in the unstructured mesh from a statepoint file to ensure it has all the required attributes """ + if mesh_library == 'moab' and not openmc.lib._dagmc_enabled(): + pytest.skip("DAGMC not enabled.") + if mesh_library == 'libmesh' and not openmc.lib._libmesh_enabled(): + pytest.skip("LibMesh not enabled.") + model = openmc.Model() surf1 = openmc.Sphere(r=1000.0, boundary_type="vacuum") @@ -504,8 +519,8 @@ def test_write_vtkhdf(request, run_in_tmpdir): model.geometry = openmc.Geometry([cell1]) umesh = openmc.UnstructuredMesh( - request.path.parent / "test_mesh_dagmc_tets.vtk", - "moab", + request.path.parent / mesh_file, + mesh_library, mesh_id = 1 ) mesh_filter = openmc.MeshFilter(umesh) @@ -514,6 +529,8 @@ def test_write_vtkhdf(request, run_in_tmpdir): mesh_tally = openmc.Tally(name="test_tally") mesh_tally.filters = [mesh_filter] mesh_tally.scores = ["flux"] + if mesh_library == "libmesh": + mesh_tally.estimator = "collision" model.tallies = [mesh_tally] @@ -556,6 +573,26 @@ def test_write_vtkhdf(request, run_in_tmpdir): with h5py.File("test_mesh.vtkhdf", "r"): ... + import vtk + reader = vtk.vtkHDFReader() + reader.SetFileName("test_mesh.vtkhdf") + reader.Update() + + # Get mean from file and make sure it matches original data + num_elements = reader.GetOutput().GetNumberOfCells() + assert num_elements == umesh_from_sp.n_elements + + num_vertices = reader.GetOutput().GetNumberOfPoints() + assert num_vertices == umesh_from_sp.vertices.shape[0] + + arr = reader.GetOutput().GetCellData().GetArray("mean") + mean = np.array([arr.GetTuple1(i) for i in range(my_tally.mean.size)]) + np.testing.assert_almost_equal(mean, my_tally.mean.flatten()/umesh_from_sp.volumes) + + arr = reader.GetOutput().GetCellData().GetArray("std_dev") + std_dev = np.array([arr.GetTuple1(i) for i in range(my_tally.std_dev.size)]) + np.testing.assert_almost_equal(std_dev, my_tally.std_dev.flatten()/umesh_from_sp.volumes) + def test_mesh_get_homogenized_materials(): """Test the get_homogenized_materials method""" diff --git a/tests/unit_tests/test_mesh_hexes.exo b/tests/unit_tests/test_mesh_hexes.exo new file mode 120000 index 000000000..272da92a4 --- /dev/null +++ b/tests/unit_tests/test_mesh_hexes.exo @@ -0,0 +1 @@ +../regression_tests/unstructured_mesh/test_mesh_hexes.exo \ No newline at end of file From 7d09a12606850b37d57cb61372ad2edc4ed327c6 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Wed, 20 May 2026 16:43:21 -0500 Subject: [PATCH 626/671] DAGMC Cell Override Updates (#3888) Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Paul Romano --- docs/source/io_formats/geometry.rst | 61 +++++-- include/openmc/cell.h | 24 +++ include/openmc/dagmc.h | 19 +- openmc/dagmc.py | 230 ++++++++++++++---------- openmc/model/model.py | 2 + src/cell.cpp | 94 ++++++---- src/dagmc.cpp | 227 +++++++++++++++++++----- tests/unit_tests/dagmc/test_model.py | 253 +++++++++++++++++++-------- tests/unit_tests/test_cell.py | 43 +++++ 9 files changed, 689 insertions(+), 264 deletions(-) diff --git a/docs/source/io_formats/geometry.rst b/docs/source/io_formats/geometry.rst index cef3bb79c..60ff0afae 100644 --- a/docs/source/io_formats/geometry.rst +++ b/docs/source/io_formats/geometry.rst @@ -406,24 +406,55 @@ Each ```` element can have the following attributes or sub-eleme *Default*: None - :material_overrides: - This element contains information on material overrides to be applied to the - DAGMC universe. It has the following attributes and sub-elements: + :cell: + Zero or more ```` sub-elements may appear to override properties of + individual DAGMC volumes. Each ```` element supports the following + attributes and sub-elements: - :cell: - Material override information for a single cell. It contains the following - attributes and sub-elements: + :id: + The integer cell ID in the DAGMC geometry to override. Required. - :id: - The cell ID in the DAGMC geometry for which the material override will - apply. + :name: + An optional string label for the cell. - :materials: - A list of material IDs that will apply to instances of the cell. If the - list contains only one ID, it will replace the original material - assignment of all instances of the DAGMC cell. If the list contains more - than one material, each material ID of the list will be assigned to the - various instances of the DAGMC cell. + *Default*: None + + :material: + The material ID to assign to this cell. Use ``void`` for vacuum. Multiple + space-separated IDs may be given to specify a distribmat (distributed + material) assignment. Required. + + :temperature: + Temperature(s) in [K] to assign to the cell. Must be ≥ 0. Multiple + space-separated values may be given. + + *Default*: None + + :density: + Density in [g/cm³] to assign to the cell. Must be > 0. Requires a non-void + material fill. Multiple space-separated values may be given. + + *Default*: None + + :volume: + Volume of the cell in [cm³]. + + .. note:: DAGMC can compute cell volumes exactly from the triangulated + mesh surfaces. Specifying a manual volume risks inconsistency + with that capability. + + *Default*: None + + The following standard ```` attributes are **not** supported inside + ```` and will raise an error if present: ``region``, + ``fill``, ``universe``, ``translation``, ``rotation``. + + .. deprecated:: + The ```` sub-element (containing ```` + children with ````) is deprecated. A deprecation warning is + emitted and the overrides are converted to the ```` format at parse + time. It is an error to specify both ```` and + ```` sub-elements on the same ````. *Default*: None diff --git a/include/openmc/cell.h b/include/openmc/cell.h index 26c34ebd4..1a27ad516 100644 --- a/include/openmc/cell.h +++ b/include/openmc/cell.h @@ -145,6 +145,30 @@ private: bool simple_; //!< Does the region contain only intersections? }; +//============================================================================== +// XML parsing helpers for nodes +//============================================================================== + +//! Parse material IDs from a XML node. +//! \param node XML node containing a "material" attribute or child element +//! \param cell_id Cell ID used in error messages +//! \return Vector of material IDs (MATERIAL_VOID for "void") +vector parse_cell_material_xml(pugi::xml_node node, int32_t cell_id); + +//! Parse temperatures in [K] from a XML node. +//! Validates that all values are non-negative and the list is non-empty. +//! \param node XML node containing a "temperature" attribute or child element +//! \param cell_id Cell ID used in error messages +//! \return Vector of temperatures in [K] +vector parse_cell_temperature_xml(pugi::xml_node node, int32_t cell_id); + +//! Parse densities in [g/cm³] from a XML node. +//! Validates that all values are positive and the list is non-empty. +//! \param node XML node containing a "density" attribute or child element +//! \param cell_id Cell ID used in error messages +//! \return Vector of densities in [g/cm³] +vector parse_cell_density_xml(pugi::xml_node node, int32_t cell_id); + //============================================================================== class Cell { diff --git a/include/openmc/dagmc.h b/include/openmc/dagmc.h index 0e27402a1..858636c0b 100644 --- a/include/openmc/dagmc.h +++ b/include/openmc/dagmc.h @@ -94,6 +94,10 @@ private: class DAGUniverse : public Universe { public: + using MaterialOverrides = std::unordered_map>; + using TemperatureOverrides = std::unordered_map>; + using DensityOverrides = std::unordered_map>; + explicit DAGUniverse(pugi::xml_node node); //! Create a new DAGMC universe @@ -112,6 +116,9 @@ public: //! Initialize the DAGMC accel. data structures, indices, material //! assignments, etc. void initialize(); + void initialize(const MaterialOverrides& material_overrides, + const TemperatureOverrides& temperature_overrides, + const DensityOverrides& density_overrides = {}); //! Reads UWUW materials and returns an ID map void read_uwuw_materials(); @@ -146,7 +153,8 @@ public: //! Assign a material overriding normal assignement to a cell //! \param[in] c The OpenMC cell to which the material is assigned - void override_assign_material(std::unique_ptr& c) const; + void override_assign_material(std::unique_ptr& c, + const MaterialOverrides& material_overrides) const; //! Return the index into the model cells vector for a given DAGMC volume //! handle in the universe @@ -187,7 +195,9 @@ private: void set_id(); //!< Deduce the universe id from model::universes void init_dagmc(); //!< Create and initialise DAGMC pointer void init_metadata(); //!< Create and initialise dagmcMetaData pointer - void init_geometry(); //!< Create cells and surfaces from DAGMC entities + void init_geometry(const MaterialOverrides& material_overrides, + const TemperatureOverrides& temperature_overrides, + const DensityOverrides& density_overrides); std::string filename_; //!< Name of the DAGMC file used to create this universe @@ -201,11 +211,6 @@ private: //!< generate new material IDs for the universe bool has_graveyard_; //!< Indicates if the DAGMC geometry has a "graveyard" //!< volume - std::unordered_map> - material_overrides_; //!< Map of material overrides - //!< keys correspond to the DAGMCCell id - //!< values are a list of material ids used - //!< for the override }; //============================================================================== diff --git a/openmc/dagmc.py b/openmc/dagmc.py index fd4258225..75ead5e43 100644 --- a/openmc/dagmc.py +++ b/openmc/dagmc.py @@ -36,12 +36,6 @@ class DAGMCUniverse(openmc.UniverseBase): auto_mat_ids : bool Set IDs automatically on initialization (True) or report overlaps in ID space between OpenMC and UWUW materials (False) - material_overrides : dict, optional - A dictionary of material overrides. The keys are material name strings - and the values are Iterables of openmc.Material objects. If a material - name is found in the DAGMC file, the material will be replaced with the - openmc.Material object in the value. - Attributes ---------- id : int @@ -78,15 +72,6 @@ class DAGMCUniverse(openmc.UniverseBase): The number of surfaces in the model. .. versionadded:: 0.13.2 - material_overrides : dict - A dictionary of material overrides. Keys are cell IDs; values are - iterables of :class:`openmc.Material` objects. The material assignment - of each DAGMC cell ID key will be replaced with the - :class:`~openmc.Material` object in the value. If the value contains - multiple :class:`~openmc.Material` objects, each Material in the list - will be assigned to the corresponding instance of the cell. - - .. versionadded:: 0.15.1 """ def __init__(self, @@ -94,16 +79,12 @@ class DAGMCUniverse(openmc.UniverseBase): universe_id=None, name='', auto_geom_ids=False, - auto_mat_ids=False, - material_overrides=None): + auto_mat_ids=False): super().__init__(universe_id, name) # Initialize class attributes self.filename = filename self.auto_geom_ids = auto_geom_ids self.auto_mat_ids = auto_mat_ids - self._material_overrides = {} - if material_overrides is not None: - self.material_overrides = material_overrides def __repr__(self): string = super().__repr__() @@ -130,47 +111,17 @@ class DAGMCUniverse(openmc.UniverseBase): @property def material_overrides(self): - return self._material_overrides + raise AttributeError( + "DAGMCUniverse.material_overrides has been removed. Use " + "DAGMCCell objects added via add_cell() to manage per-cell " + "material assignments.") @material_overrides.setter def material_overrides(self, val): - cv.check_type('material overrides', val, Mapping) - for key, value in val.items(): - self.add_material_override(key, value) - - def replace_material_assignment(self, material_name: str, material: openmc.Material): - """Replace a material assignment within the DAGMC universe. - - Replace the material assignment of all cells filled with a material in - the DAGMC universe. The universe must be synchronized in an initialized - Model (see :meth:`~openmc.DAGMCUniverse.sync_dagmc_cells`) before - calling this method. - - .. versionadded:: 0.15.1 - - Parameters - ---------- - material_name : str - Material name to replace - material : openmc.Material - Material to replace the material_name with - - """ - if material_name not in self.material_names: - raise ValueError( - f"No material with name '{material_name}' found in the DAGMC universe") - - if not self.cells: - raise RuntimeError("This DAGMC universe has not been synchronized " - "on an initialized Model.") - - for cell in self.cells.values(): - if cell.fill is None: - continue - if isinstance(cell.fill, openmc.Iterable): - cell.fill = list(map(lambda x: material if x.name == material_name else x, cell.fill)) - else: - cell.fill = material if cell.fill.name == material_name else cell.fill + raise AttributeError( + "DAGMCUniverse.material_overrides has been removed. Use " + "DAGMCCell objects added via add_cell() to manage per-cell " + "material assignments.") def add_material_override(self, key, overrides=None): """Add a material override to the universe. @@ -201,7 +152,10 @@ class DAGMCUniverse(openmc.UniverseBase): if key not in self.cells: raise ValueError(f"Cell ID '{key}' not found in DAGMC universe") - self._material_overrides[key] = overrides + if len(overrides) == 1: + self.cells[key].fill = overrides[0] + else: + self.cells[key].fill = list(overrides) @property def auto_geom_ids(self): @@ -290,12 +244,6 @@ class DAGMCUniverse(openmc.UniverseBase): memo.add(self) - # Ensure that the material overrides are up-to-date - for cell in self.cells.values(): - if cell.fill is None: - continue - self.add_material_override(cell, cell.fill) - # Set xml element values dagmc_element = ET.Element('dagmc_universe') dagmc_element.set('id', str(self.id)) @@ -307,17 +255,10 @@ class DAGMCUniverse(openmc.UniverseBase): if self.auto_mat_ids: dagmc_element.set('auto_mat_ids', 'true') dagmc_element.set('filename', str(self.filename)) - if self._material_overrides: - mat_element = ET.Element('material_overrides') - for key in self._material_overrides: - cell_overrides = ET.Element('cell_override') - cell_overrides.set("id", str(key)) - material_element = ET.Element('material_ids') - material_element.text = ' '.join( - str(t.id) for t in self._material_overrides[key]) - cell_overrides.append(material_element) - mat_element.append(cell_overrides) - dagmc_element.append(mat_element) + if self.cells: + for cell in self.cells.values(): + cell_element = cell.create_xml_subelement(xml_element, memo) + dagmc_element.append(cell_element) xml_element.append(dagmc_element) def bounding_region( @@ -442,7 +383,7 @@ class DAGMCUniverse(openmc.UniverseBase): return out @classmethod - def from_xml_element(cls, elem, mats = None): + def from_xml_element(cls, elem, mats=None): """Generate DAGMC universe from XML element Parameters @@ -471,21 +412,56 @@ class DAGMCUniverse(openmc.UniverseBase): out.auto_geom_ids = bool(get_text(elem, "auto_geom_ids")) out.auto_mat_ids = bool(get_text(elem, "auto_mat_ids")) - el_mat_override = elem.find('material_overrides') - if el_mat_override is not None: - if mats is None: - raise ValueError("Material overrides found in DAGMC universe " - "but no materials were provided to populate " - "the mapping.") - out._material_overrides = {} - for elem in el_mat_override.findall('cell_override'): - cell_id = int(get_text(elem, 'id')) - mat_ids = get_elem_list(elem, "material_ids", str) or [] - mat_objs = [mats[mat_id] for mat_id in mat_ids] - out._material_overrides[cell_id] = mat_objs + has_overrides = elem.find('material_overrides') is not None + has_cells = elem.find('cell') is not None + + if has_overrides and has_cells: + raise ValueError( + "DAGMCUniverse cannot specify both and " + " sub-elements. Use elements only.") + + if has_overrides: + warnings.warn( + "DAGMCUniverse is deprecated and will be " + "removed in a future version. Use nested elements " + "instead.", DeprecationWarning, stacklevel=2) + out._parse_legacy_material_overrides(elem, mats) + elif has_cells: + out._parse_cell_overrides(elem, mats) return out + def _parse_legacy_material_overrides(self, elem, mats): + """Parse the deprecated XML format and populate + the universe with equivalent DAGMCCell objects.""" + if mats is None: + raise ValueError( + "DAGMC material overrides found but no materials were " + "provided to populate the mapping.") + mo_elem = elem.find('material_overrides') + for co_elem in mo_elem.findall('cell_override'): + cell_id = int(get_text(co_elem, 'id')) + mat_ids = co_elem.find('material_ids').text.split() + fill_objs = [mats[mid] for mid in mat_ids] + fill = fill_objs[0] if len(fill_objs) == 1 else fill_objs + if cell_id in self.cells: + raise ValueError( + f"Duplicate DAGMC cell override specified for cell {cell_id}.") + self.add_cell(DAGMCCell(cell_id=cell_id, fill=fill)) + + def _parse_cell_overrides(self, elem, mats): + if mats is None: + raise ValueError("DAGMC cell overrides found in DAGMC universe but " + "no materials were provided to populate the " + "mapping.") + + for cell_elem in elem.findall('cell'): + cell_id = int(get_text(cell_elem, 'id')) + if cell_id in self.cells: + raise ValueError( + f"Duplicate DAGMC cell override specified for cell {cell_id}.") + DAGMCCell.from_xml_element(cell_elem, mats, self) + def _partial_deepcopy(self): """Clone all of the openmc.DAGMCUniverse object's attributes except for its cells, as they are copied within the clone function. This should @@ -565,7 +541,13 @@ class DAGMCUniverse(openmc.UniverseBase): fill = [mats_per_id[mat.id] for mat in dag_cell.fill if mat] else: fill = mats_per_id[dag_cell.fill.id] if dag_cell.fill else None - self.add_cell(openmc.DAGMCCell(cell_id=dag_cell_id, fill=fill)) + name = dag_cell.name + if dag_cell_id in self._cells: + self._cells[dag_cell_id].name = name + self._cells[dag_cell_id].fill = fill + else: + self.add_cell( + openmc.DAGMCCell(cell_id=dag_cell_id, name=name, fill=fill)) @add_plot_params def plot(self, *args, **kwargs): @@ -594,6 +576,14 @@ class DAGMCCell(openmc.Cell): DAG_parent_universe : int The parent universe of the cell. + Notes + ----- + DAGMC geometries are composed of triangulated surfaces, which means cell + volumes can in principle be computed exactly (e.g. via mesh-based + integration). Manually specifying :attr:`volume` overrides any such + calculation and may introduce inconsistencies if the value does not + accurately reflect the true geometric volume. + """ def __init__(self, cell_id=None, name='', fill=None): super().__init__(cell_id, name, fill, None) @@ -625,8 +615,62 @@ class DAGMCCell(openmc.Cell): raise TypeError("plot is not available for DAGMC cells.") def create_xml_subelement(self, xml_element, memo=None): - raise TypeError("create_xml_subelement is not available for DAGMC cells.") + if self.fill_type not in ('void', 'material', 'distribmat'): + raise TypeError("DAGMC cell overrides currently only support " + "material fills.") + if self.temperature is not None and self.fill_type not in ( + 'material', 'distribmat' + ): + raise TypeError("DAGMC cell temperature overrides require a " + "material fill.") + if self.density is not None and self.fill_type not in ('material', 'distribmat'): + raise TypeError("DAGMC cell density overrides require a " + "material fill.") + if any(getattr(self, attr) is not None for attr in ('translation', 'rotation')): + raise TypeError("DAGMC cell overrides do not support translation " + "or rotation.") + return super().create_xml_subelement(xml_element, memo) @classmethod - def from_xml_element(cls, elem, surfaces, materials, get_universe): - raise TypeError("from_xml_element is not available for DAGMC cells.") + def from_xml_element(cls, elem, mats, universe): + """Generate a DAGMCCell from an XML override element. + + Parameters + ---------- + elem : lxml.etree._Element + `` element containing a DAGMC cell property override + mats : dict + Dictionary mapping material ID strings to :class:`openmc.Material` + instances + universe : DAGMCUniverse + Universe to add the parsed cell to. + + Returns + ------- + DAGMCCell + DAGMCCell instance + """ + if not isinstance(universe, DAGMCUniverse): + raise TypeError( + f"universe must be a DAGMCUniverse instance, " + f"got {type(universe).__name__}.") + + cell_id = int(get_text(elem, 'id')) + + # Validate attributes that are unsupported for DAGMC cell overrides + for tag in ('region', 'fill', 'universe'): + if get_text(elem, tag) is not None: + raise ValueError( + f"DAGMC cell {cell_id} override cannot specify '{tag}'.") + for tag in ('translation', 'rotation'): + if get_text(elem, tag) is not None: + raise ValueError( + f"DAGMC cell {cell_id} override does not support " + f"'{tag}'.") + if get_elem_list(elem, 'material', str) is None: + raise ValueError( + f"DAGMC cell {cell_id} must specify a material override.") + + return super().from_xml_element( + elem, surfaces={}, materials=mats, + get_universe=lambda _: universe) diff --git a/openmc/model/model.py b/openmc/model/model.py index c9a24b8b3..11c26d4e7 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -467,6 +467,8 @@ class Model: This method iterates over all DAGMC universes in the geometry and synchronizes their cells with the current material assignments. Requires that the model has been initialized via :meth:`Model.init_lib`. + Synchronized DAGMC cells can then be edited and exported as nested + `` overrides inside each `` element. .. versionadded:: 0.15.1 diff --git a/src/cell.cpp b/src/cell.cpp index ebe28c3d2..06bc29a66 100644 --- a/src/cell.cpp +++ b/src/cell.cpp @@ -340,6 +340,63 @@ void Cell::to_hdf5(hid_t cell_group) const close_group(group); } +//============================================================================== +// XML parsing helpers for nodes +//============================================================================== + +vector parse_cell_material_xml(pugi::xml_node node, int32_t cell_id) +{ + vector mats { + get_node_array(node, "material", true)}; + if (mats.empty()) { + fatal_error(fmt::format( + "An empty material element was specified for cell {}", cell_id)); + } + vector material; + material.reserve(mats.size()); + for (const auto& mat : mats) { + if (mat == "void") { + material.push_back(MATERIAL_VOID); + } else { + material.push_back(std::stoi(mat)); + } + } + return material; +} + +vector parse_cell_temperature_xml(pugi::xml_node node, int32_t cell_id) +{ + auto temperatures = get_node_array(node, "temperature"); + if (temperatures.empty()) { + fatal_error(fmt::format( + "An empty temperature element was specified for cell {}", cell_id)); + } + for (auto T : temperatures) { + if (T < 0) { + fatal_error(fmt::format( + "Cell {} was specified with a negative temperature", cell_id)); + } + } + return temperatures; +} + +vector parse_cell_density_xml(pugi::xml_node node, int32_t cell_id) +{ + auto densities = get_node_array(node, "density"); + if (densities.empty()) { + fatal_error(fmt::format( + "An empty density element was specified for cell {}", cell_id)); + } + for (auto rho : densities) { + if (rho <= 0) { + fatal_error(fmt::format( + "Cell {} was specified with a density less than or equal to zero", + cell_id)); + } + } + return densities; +} + //============================================================================== // CSGCell implementation //============================================================================== @@ -390,26 +447,12 @@ CSGCell::CSGCell(pugi::xml_node cell_node) // universe), more than one material (distribmats), and some materials may // be "void". if (material_present) { - vector mats { - get_node_array(cell_node, "material", true)}; - if (mats.size() > 0) { - material_.reserve(mats.size()); - for (std::string mat : mats) { - if (mat.compare("void") == 0) { - material_.push_back(MATERIAL_VOID); - } else { - material_.push_back(std::stoi(mat)); - } - } - } else { - fatal_error(fmt::format( - "An empty material element was specified for cell {}", id_)); - } + material_ = parse_cell_material_xml(cell_node, id_); } // Read the temperature element which may be distributed like materials. if (check_for_node(cell_node, "temperature")) { - sqrtkT_ = get_node_array(cell_node, "temperature"); + sqrtkT_ = parse_cell_temperature_xml(cell_node, id_); sqrtkT_.shrink_to_fit(); // Make sure this is a material-filled cell. @@ -420,14 +463,6 @@ CSGCell::CSGCell(pugi::xml_node cell_node) id_)); } - // Make sure all temperatures are non-negative. - for (auto T : sqrtkT_) { - if (T < 0) { - fatal_error(fmt::format( - "Cell {} was specified with a negative temperature", id_)); - } - } - // Convert to sqrt(k*T). for (auto& T : sqrtkT_) { T = std::sqrt(K_BOLTZMANN * T); @@ -440,7 +475,7 @@ CSGCell::CSGCell(pugi::xml_node cell_node) // Note: calculating the actual density multiplier is deferred until materials // are finalized. density_mult_ contains the true density in the meantime. if (check_for_node(cell_node, "density")) { - density_mult_ = get_node_array(cell_node, "density"); + density_mult_ = parse_cell_density_xml(cell_node, id_); density_mult_.shrink_to_fit(); // Make sure this is a material-filled cell. @@ -461,15 +496,6 @@ CSGCell::CSGCell(pugi::xml_node cell_node) id_)); } } - - // Make sure all densities are non-negative and greater than zero. - for (auto rho : density_mult_) { - if (rho <= 0) { - fatal_error(fmt::format( - "Cell {} was specified with a density less than or equal to zero", - id_)); - } - } } // Read the region specification. diff --git a/src/dagmc.cpp b/src/dagmc.cpp index 571182fa6..a5cc71b52 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -50,6 +50,10 @@ namespace openmc { DAGUniverse::DAGUniverse(pugi::xml_node node) { + MaterialOverrides material_overrides; + TemperatureOverrides temperature_overrides; + DensityOverrides density_overrides; + if (check_for_node(node, "id")) { id_ = std::stoi(get_node_value(node, "id")); } else { @@ -76,24 +80,77 @@ DAGUniverse::DAGUniverse(pugi::xml_node node) adjust_material_ids_ = get_node_value_bool(node, "auto_mat_ids"); } - // get material assignment overloading - if (check_for_node(node, "material_overrides")) { - auto mat_node = node.child("material_overrides"); - // loop over all subelements (each subelement corresponds to a material) - for (pugi::xml_node cell_node : mat_node.children("cell_override")) { - // Store assignment reference name - int32_t ref_assignment = std::stoi(get_node_value(cell_node, "id")); + // Get material assignment overrides from nested DAGMC cell elements. + if (node.child("cell")) { + for (pugi::xml_node cell_node : node.children("cell")) { + if (!check_for_node(cell_node, "id")) { + fatal_error( + "Must specify id for each DAGMC cell override in ."); + } - // Get mat name for each assignement instances - vector instance_mats = - get_node_array(cell_node, "material_ids"); + int32_t cell_id = std::stoi(get_node_value(cell_node, "id")); - // Store mat name for each instances - material_overrides_.emplace(ref_assignment, instance_mats); + if (check_for_node(cell_node, "region")) { + fatal_error(fmt::format( + "DAGMC cell {} override cannot specify a region.", cell_id)); + } + if (check_for_node(cell_node, "fill")) { + fatal_error(fmt::format( + "DAGMC cell {} override currently only supports material fills.", + cell_id)); + } + if (check_for_node(cell_node, "universe")) { + fatal_error(fmt::format( + "DAGMC cell {} override cannot specify a universe.", cell_id)); + } + if (check_for_node(cell_node, "translation") || + check_for_node(cell_node, "rotation")) { + fatal_error(fmt::format( + "DAGMC cell {} override does not support translation or rotation.", + cell_id)); + } + if (!check_for_node(cell_node, "material")) { + fatal_error(fmt::format( + "DAGMC cell {} override must specify material.", cell_id)); + } + + auto inserted = material_overrides.emplace( + cell_id, parse_cell_material_xml(cell_node, cell_id)); + if (!inserted.second) { + fatal_error(fmt::format( + "Duplicate DAGMC cell override specified for cell {}", cell_id)); + } + + if (check_for_node(cell_node, "temperature")) { + temperature_overrides.emplace( + cell_id, parse_cell_temperature_xml(cell_node, cell_id)); + } + + if (check_for_node(cell_node, "density")) { + density_overrides.emplace( + cell_id, parse_cell_density_xml(cell_node, cell_id)); + } + } + } else if (check_for_node(node, "material_overrides")) { + if (node.child("cell")) { + fatal_error("DAGMCUniverse cannot specify both and " + " sub-elements. Use elements only."); + } + warning("DAGMCUniverse is deprecated. Use nested " + " elements under instead."); + for (pugi::xml_node co : + node.child("material_overrides").children("cell_override")) { + int32_t cell_id = std::stoi(get_node_value(co, "id")); + std::istringstream iss(co.child("material_ids").text().get()); + vector mats; + for (std::string s; iss >> s;) { + mats.push_back(s == "void" ? MATERIAL_VOID : std::stoi(s)); + } + material_overrides.emplace(cell_id, mats); } } - initialize(); + initialize(material_overrides, temperature_overrides, density_overrides); } DAGUniverse::DAGUniverse( @@ -110,9 +167,12 @@ DAGUniverse::DAGUniverse(std::shared_ptr dagmc_ptr, : dagmc_instance_(dagmc_ptr), filename_(filename), adjust_geometry_ids_(auto_geom_ids), adjust_material_ids_(auto_mat_ids) { + MaterialOverrides material_overrides; + TemperatureOverrides temperature_overrides; + DensityOverrides density_overrides; set_id(); init_metadata(); - init_geometry(); + init_geometry(material_overrides, temperature_overrides, density_overrides); } void DAGUniverse::set_id() @@ -130,6 +190,15 @@ void DAGUniverse::set_id() } void DAGUniverse::initialize() +{ + MaterialOverrides material_overrides; + TemperatureOverrides temperature_overrides; + initialize(material_overrides, temperature_overrides); +} + +void DAGUniverse::initialize(const MaterialOverrides& material_overrides, + const TemperatureOverrides& temperature_overrides, + const DensityOverrides& density_overrides) { #ifdef OPENMC_UWUW_ENABLED // read uwuw materials from the .h5m file if present @@ -140,7 +209,7 @@ void DAGUniverse::initialize() init_metadata(); - init_geometry(); + init_geometry(material_overrides, temperature_overrides, density_overrides); } void DAGUniverse::init_dagmc() @@ -176,7 +245,9 @@ void DAGUniverse::init_metadata() MB_CHK_ERR_CONT(rval); } -void DAGUniverse::init_geometry() +void DAGUniverse::init_geometry(const MaterialOverrides& material_overrides, + const TemperatureOverrides& temperature_overrides, + const DensityOverrides& density_overrides) { moab::ErrorCode rval; @@ -202,6 +273,9 @@ void DAGUniverse::init_geometry() : dagmc_instance_->id_by_index(3, c->dag_index()); c->universe_ = this->id_; c->fill_ = C_NONE; // no fill, single universe + if (dagmc_instance_->is_implicit_complement(vol_handle)) { + c->name_ = "implicit complement"; + } auto in_map = model::cell_map.find(c->id_); if (in_map == model::cell_map.end()) { @@ -230,16 +304,68 @@ void DAGUniverse::init_geometry() if (mat_str == "graveyard") { graveyard = vol_handle; } - // material void checks - if (mat_str == "void" || mat_str == "vacuum" || mat_str == "graveyard") { + if (material_overrides.count(c->id_)) { + override_assign_material(c, material_overrides); + } else if (mat_str == "void" || mat_str == "vacuum" || + mat_str == "graveyard") { c->material_.push_back(MATERIAL_VOID); + } else if (uses_uwuw()) { + uwuw_assign_material(vol_handle, c); } else { - if (material_overrides_.count(c->id_)) { - override_assign_material(c); - } else if (uses_uwuw()) { - uwuw_assign_material(vol_handle, c); - } else { - legacy_assign_material(mat_str, c); + legacy_assign_material(mat_str, c); + } + + if (temperature_overrides.count(c->id_)) { + if (c->material_.empty() || c->material_[0] == MATERIAL_VOID) { + fatal_error(fmt::format("DAGMC cell {} was specified with a " + "temperature but no non-void material.", + c->id_)); + } + + c->sqrtkT_.clear(); + const auto& temp_overrides = temperature_overrides.at(c->id_); + c->sqrtkT_.reserve(temp_overrides.size()); + for (auto T : temp_overrides) { + c->sqrtkT_.push_back(std::sqrt(K_BOLTZMANN * T)); + } + + if (settings::verbosity >= 10) { + std::stringstream override_values; + for (size_t i = 0; i < temp_overrides.size(); ++i) { + if (i > 0) { + override_values << " "; + } + override_values << temp_overrides[i]; + } + auto msg = fmt::format("Overriding DAGMC cell {} property " + "'temperature [K]' with value(s): {}", + c->id_, override_values.str()); + write_message(msg, 10); + } + } + + if (density_overrides.count(c->id_)) { + if (c->material_.empty() || c->material_[0] == MATERIAL_VOID) { + fatal_error(fmt::format("DAGMC cell {} was specified with a density " + "but no non-void material.", + c->id_)); + } + // density_mult_ holds the true density until materials are finalized, + // at which point it is converted to a proper multiplier (same as CSG). + c->density_mult_ = density_overrides.at(c->id_); + + if (settings::verbosity >= 10) { + const auto& dens = density_overrides.at(c->id_); + std::stringstream override_values; + for (size_t i = 0; i < dens.size(); ++i) { + if (i > 0) + override_values << " "; + override_values << dens[i]; + } + write_message(fmt::format("Overriding DAGMC cell {} property " + "'density [g/cm³]' with value(s): {}", + c->id_, override_values.str()), + 10); } } @@ -252,18 +378,21 @@ void DAGUniverse::init_geometry() continue; } - // assign cell temperature - const auto& mat = model::materials[model::material_map.at(c->material_[0])]; - if (dagmc_instance_->has_prop(vol_handle, "temp")) { - rval = dagmc_instance_->prop_value(vol_handle, "temp", temp_value); - MB_CHK_ERR_CONT(rval); - double temp = std::stod(temp_value); - c->sqrtkT_.push_back(std::sqrt(K_BOLTZMANN * temp)); - } else if (mat->temperature() > 0.0) { - c->sqrtkT_.push_back(std::sqrt(K_BOLTZMANN * mat->temperature())); - } else { - c->sqrtkT_.push_back( - std::sqrt(K_BOLTZMANN * settings::temperature_default)); + // assign cell temperature if not explicitly overridden + if (c->sqrtkT_.empty()) { + const auto& mat = + model::materials[model::material_map.at(c->material_[0])]; + if (dagmc_instance_->has_prop(vol_handle, "temp")) { + rval = dagmc_instance_->prop_value(vol_handle, "temp", temp_value); + MB_CHK_ERR_CONT(rval); + double temp = std::stod(temp_value); + c->sqrtkT_.push_back(std::sqrt(K_BOLTZMANN * temp)); + } else if (mat->temperature() > 0.0) { + c->sqrtkT_.push_back(std::sqrt(K_BOLTZMANN * mat->temperature())); + } else { + c->sqrtkT_.push_back( + std::sqrt(K_BOLTZMANN * settings::temperature_default)); + } } model::cells.emplace_back(std::move(c)); @@ -630,7 +759,8 @@ void DAGUniverse::uwuw_assign_material( #endif // OPENMC_UWUW_ENABLED } -void DAGUniverse::override_assign_material(std::unique_ptr& c) const +void DAGUniverse::override_assign_material(std::unique_ptr& c, + const MaterialOverrides& material_overrides) const { // if Cell ID matches an override key, use it to override the material // assignment else if UWUW is used, get the material assignment from the DAGMC @@ -638,17 +768,30 @@ void DAGUniverse::override_assign_material(std::unique_ptr& c) const // Notify User that an override is being applied on a DAGMCCell write_message(fmt::format("Applying override for DAGMCCell {}", c->id_), 8); + const auto& mat_overrides = material_overrides.at(c->id_); if (settings::verbosity >= 10) { - auto msg = fmt::format("Assigning DAGMC cell {} material(s) based on " - "override information (see input XML).", - c->id_); + std::stringstream override_values; + for (size_t i = 0; i < mat_overrides.size(); ++i) { + if (i > 0) { + override_values << " "; + } + if (mat_overrides[i] == MATERIAL_VOID) { + override_values << "void"; + } else { + override_values << mat_overrides[i]; + } + } + auto msg = fmt::format("Overriding DAGMC cell {} property 'material' " + "with value(s): {}", + c->id_, override_values.str()); write_message(msg, 10); } // Override the material assignment for each cell instance using the legacy // assignement - for (auto mat_id : material_overrides_.at(c->id_)) { - if (model::material_map.find(mat_id) == model::material_map.end()) { + for (auto mat_id : mat_overrides) { + if (mat_id != MATERIAL_VOID && + model::material_map.find(mat_id) == model::material_map.end()) { fatal_error(fmt::format( "Material with ID '{}' not found for DAGMC cell {}", mat_id, c->id_)); } diff --git a/tests/unit_tests/dagmc/test_model.py b/tests/unit_tests/dagmc/test_model.py index 0917f5b23..5889fa542 100644 --- a/tests/unit_tests/dagmc/test_model.py +++ b/tests/unit_tests/dagmc/test_model.py @@ -70,28 +70,20 @@ def model(request): openmc.reset_auto_ids() -def test_dagmc_replace_material_assignment(model): - mats = {} - - mats["foo"] = openmc.Material(name="foo") - mats["foo"].add_nuclide("H1", 2.0) - mats["foo"].add_element("O", 1.0) - mats["foo"].set_density("g/cm3", 1.0) - mats["foo"].add_s_alpha_beta("c_H_in_H2O") - +def test_dagmc_sync_cell_names(model): + dag_univ = None for univ in model.geometry.get_all_universes().values(): - if not isinstance(univ, openmc.DAGMCUniverse): + if isinstance(univ, openmc.DAGMCUniverse): + dag_univ = univ break - cells_with_41 = [] - for cell in univ.cells.values(): - if cell.fill is None: - continue - if cell.fill.name == "41": - cells_with_41.append(cell.id) - univ.replace_material_assignment("41", mats["foo"]) - for cell_id in cells_with_41: - assert univ.cells[cell_id] == mats["foo"] + assert dag_univ is not None + + for cell_id, cell in dag_univ.cells.items(): + assert cell.name == openmc.lib.cells[cell_id].name + + assert any(cell.name == "implicit complement" + for cell in dag_univ.cells.values()) def test_dagmc_add_material_override_with_id(model): @@ -114,7 +106,7 @@ def test_dagmc_add_material_override_with_id(model): cells_with_41.append(cell.id) univ.add_material_override(cell.id, mats["foo"]) for cell_id in cells_with_41: - assert univ.cells[cell_id] == mats["foo"] + assert univ.cells[cell_id].fill == mats["foo"] def test_dagmc_add_material_override_with_cell(model): @@ -137,7 +129,7 @@ def test_dagmc_add_material_override_with_cell(model): cells_with_41.append(cell.id) univ.add_material_override(cell, mats["foo"]) for cell_id in cells_with_41: - assert univ.cells[cell_id] == mats["foo"] + assert univ.cells[cell_id].fill == mats["foo"] def test_model_differentiate_depletable_with_dagmc(model, run_in_tmpdir): @@ -174,57 +166,20 @@ def test_model_differentiate_with_dagmc(model): assert len(model.materials) == 4*2 + 4 -def test_bad_override_cell_id(model): - for univ in model.geometry.get_all_universes().values(): - if isinstance(univ, openmc.DAGMCUniverse): - break - with pytest.raises(ValueError, match="Cell ID '1' not found in DAGMC universe"): - univ.material_overrides = {1: model.materials[0]} - - -def test_bad_override_type(model): - not_a_dag_cell = openmc.Cell() - for univ in model.geometry.get_all_universes().values(): - if isinstance(univ, openmc.DAGMCUniverse): - break - with pytest.raises(ValueError, match="Unrecognized key type. Must be an integer or openmc.DAGMCCell object"): - univ.material_overrides = {not_a_dag_cell: model.materials[0]} - - -def test_bad_replacement_mat_name(model): - for univ in model.geometry.get_all_universes().values(): - if isinstance(univ, openmc.DAGMCUniverse): - break - with pytest.raises(ValueError, match="No material with name 'not_a_mat' found in the DAGMC universe"): - univ.replace_material_assignment("not_a_mat", model.materials[0]) - - def test_dagmc_xml(model): - # Set the environment - mats = {} - mats["no-void fuel"] = openmc.Material(1, name="no-void fuel") - mats["no-void fuel"].add_nuclide("U235", 0.03) - mats["no-void fuel"].add_nuclide("U238", 0.97) - mats["no-void fuel"].add_nuclide("O16", 2.0) - mats["no-void fuel"].set_density("g/cm3", 10.0) - - mats[5] = openmc.Material(name="41") - mats[5].add_nuclide("H1", 2.0) - mats[5].add_element("O", 1.0) - mats[5].set_density("g/cm3", 1.0) - mats[5].add_s_alpha_beta("c_H_in_H2O") + override_mat = openmc.Material(name="41") + override_mat.add_nuclide("H1", 2.0) + override_mat.add_element("O", 1.0) + override_mat.set_density("g/cm3", 1.0) + override_mat.add_s_alpha_beta("c_H_in_H2O") + model.materials.append(override_mat) for univ in model.geometry.get_all_universes().values(): if isinstance(univ, openmc.DAGMCUniverse): dag_univ = univ break - for k, v in mats.items(): - if isinstance(k, int): - dag_univ.add_material_override(k, v) - model.materials.append(v) - elif isinstance(k, str): - dag_univ.replace_material_assignment(k, v) + dag_univ.add_material_override(5, override_mat) # Tesing the XML subelement generation root = ET.Element('dagmc_universe') @@ -236,12 +191,24 @@ def test_dagmc_xml(model): assert dagmc_ele.get('filename') == str(dag_univ.filename) assert dagmc_ele.get('auto_geom_ids') == str(dag_univ.auto_geom_ids).lower() - override_eles = dagmc_ele.find('material_overrides').findall('cell_override') - assert len(override_eles) == 4 + assert dagmc_ele.find('material_overrides') is None - for i, override_ele in enumerate(override_eles): - cell_id = override_ele.get('id') - assert dag_univ.material_overrides[int(cell_id)][0].id == int(override_ele.find('material_ids').text) + override_elements = dagmc_ele.findall('cell') + assert len(override_elements) == len(dag_univ.cells) + xml_cells = {int(elem.get('id')): elem for elem in override_elements} + for cell_id, cell in dag_univ.cells.items(): + assert cell_id in xml_cells + xml_cell = xml_cells[cell_id] + if cell.fill_type == 'void': + assert xml_cell.get('material') == 'void' + elif cell.fill_type == 'material': + assert xml_cell.get('material') == str(cell.fill.id) + elif cell.fill_type == 'distribmat': + mat_list = xml_cell.find('material').text.split() + expected = ["void" if m is None else str(m.id) for m in cell.fill] + assert mat_list == expected + else: + pytest.fail(f"Unexpected DAGMC cell fill type: {cell.fill_type}") model.export_to_model_xml() @@ -252,7 +219,147 @@ def test_dagmc_xml(model): xml_dagmc_univ = univ break - assert xml_dagmc_univ._material_overrides.keys() == dag_univ._material_overrides.keys() + assert xml_dagmc_univ.cells.keys() == dag_univ.cells.keys() - for xml_mats, model_mats in zip(xml_dagmc_univ._material_overrides.values(), dag_univ._material_overrides.values()): - assert all([xml_mat.id == orig_mat.id for xml_mat, orig_mat in zip(xml_mats, model_mats)]) + for cell_id, cell in dag_univ.cells.items(): + xml_cell = xml_dagmc_univ.cells[cell_id] + assert xml_cell.fill_type == cell.fill_type + if cell.fill_type == 'void': + assert xml_cell.fill is None + elif cell.fill_type == 'material': + assert xml_cell.fill.id == cell.fill.id + elif cell.fill_type == 'distribmat': + xml_ids = [m.id if m is not None else None for m in xml_cell.fill] + model_ids = [m.id if m is not None else None for m in cell.fill] + assert xml_ids == model_ids + else: + pytest.fail(f"Unexpected DAGMC cell fill type: {cell.fill_type}") + + +def test_dagmc_xml_reject_fill_override(): + mats = {'1': openmc.Material(1), 'void': None} + elem = ET.fromstring( + '' + '' + '' + ) + with pytest.raises(ValueError, match="cannot specify 'fill'"): + openmc.DAGMCUniverse.from_xml_element(elem, mats) + + +def test_dagmc_xml_reject_region_override(): + mats = {'1': openmc.Material(1), 'void': None} + elem = ET.fromstring( + '' + '' + '' + ) + with pytest.raises(ValueError, match="cannot specify 'region'"): + openmc.DAGMCUniverse.from_xml_element(elem, mats) + + +def _legacy_xml(cell_overrides): + """Helper to build a with old-format .""" + inner = ''.join( + f'{mids}' + for cid, mids in cell_overrides.items() + ) + return ET.fromstring( + f'' + f'{inner}' + f'' + ) + + +def test_dagmc_xml_legacy_single_material_compat(): + mat = openmc.Material(1) + mats = {'1': mat, 'void': None} + elem = _legacy_xml({3: '1'}) + with pytest.warns(DeprecationWarning, match="deprecated"): + univ = openmc.DAGMCUniverse.from_xml_element(elem, mats) + assert 3 in univ.cells + assert univ.cells[3].fill is mat + + +def test_dagmc_xml_legacy_distribmat_compat(): + mat1, mat2 = openmc.Material(2), openmc.Material(3) + mats = {'2': mat1, '3': mat2, 'void': None} + elem = _legacy_xml({5: '2 3'}) + with pytest.warns(DeprecationWarning): + univ = openmc.DAGMCUniverse.from_xml_element(elem, mats) + assert univ.cells[5].fill_type == 'distribmat' + assert list(univ.cells[5].fill) == [mat1, mat2] + + +def test_dagmc_xml_legacy_void_compat(): + mats = {'void': None} + elem = _legacy_xml({7: 'void'}) + with pytest.warns(DeprecationWarning): + univ = openmc.DAGMCUniverse.from_xml_element(elem, mats) + assert univ.cells[7].fill_type == 'void' + + +def test_dagmc_xml_legacy_both_raises(): + mat = openmc.Material(1) + mats = {'1': mat, 'void': None} + elem = ET.fromstring( + '' + '' + '1' + '' + '' + '' + ) + with pytest.raises(ValueError, match="both"): + openmc.DAGMCUniverse.from_xml_element(elem, mats) + + +def test_dagmc_xml_legacy_deprecation_warning(): + mats = {'1': openmc.Material(1), 'void': None} + elem = _legacy_xml({3: '1'}) + with pytest.warns(DeprecationWarning): + openmc.DAGMCUniverse.from_xml_element(elem, mats) + + +def test_dagmc_xml_legacy_roundtrip(): + """Old-format XML loads correctly and re-exports using the new format.""" + mat = openmc.Material(1) + mats = {'1': mat, 'void': None} + elem = _legacy_xml({3: '1'}) + with pytest.warns(DeprecationWarning): + univ = openmc.DAGMCUniverse.from_xml_element(elem, mats) + + root = ET.Element('geometry') + univ.create_xml_subelement(root) + dagmc_elem = root.find('dagmc_universe') + + assert dagmc_elem.find('material_overrides') is None + cell_elems = dagmc_elem.findall('cell') + assert len(cell_elems) == 1 + assert int(cell_elems[0].get('id')) == 3 + assert cell_elems[0].get('material') == '1' + + +def test_dagmc_xml_temperature_roundtrip(): + mat = openmc.Material(1) + mats = {'1': mat, 'void': None} + + elem = ET.fromstring( + '' + '' + '' + ) + + dag_univ = openmc.DAGMCUniverse.from_xml_element(elem, mats) + assert dag_univ.cells[7].fill.id == 1 + assert dag_univ.cells[7].temperature == pytest.approx(825.0) + + root = ET.Element('geometry') + dag_univ.create_xml_subelement(root) + dagmc_elem = root.find('dagmc_universe') + xml_cell = dagmc_elem.find('cell') + assert xml_cell.get('temperature') == '825.0' + + dag_univ_roundtrip = openmc.DAGMCUniverse.from_xml_element(dagmc_elem, mats) + assert dag_univ_roundtrip.cells[7].fill.id == 1 + assert dag_univ_roundtrip.cells[7].temperature == pytest.approx(825.0) diff --git a/tests/unit_tests/test_cell.py b/tests/unit_tests/test_cell.py index 2baa59f1b..8d39c071b 100644 --- a/tests/unit_tests/test_cell.py +++ b/tests/unit_tests/test_cell.py @@ -374,6 +374,49 @@ def test_rotation_from_xml(rotation): np.testing.assert_allclose(new_cell.rotation, cell.rotation) +def test_dagmccell_from_xml_element(): + """DAGMCCell.from_xml_element parses material, temperature, density, + and volume; rejects unsupported attributes.""" + mat = openmc.Material(1) + mat.add_nuclide('U235', 1.0) + mats = {'1': mat} + # In practice, from_xml_element is always called from DAGMCUniverse + # during XML parsing. A placeholder universe is used here so the test + # can exercise the method directly without a real DAGMC model file. + placeholder_univ = openmc.DAGMCUniverse('model.h5m') + + # material + temperature + density round-trip + xml = '' + cell = openmc.DAGMCCell.from_xml_element(ET.fromstring(xml), mats, placeholder_univ) + assert cell.id == 5 + assert cell.name == 'fuel' + assert cell.fill is mat + assert cell.density == 10.5 + assert cell.temperature == 900.0 + + # volume round-trip + xml = '' + cell = openmc.DAGMCCell.from_xml_element(ET.fromstring(xml), mats, placeholder_univ) + assert cell.volume == 42.0 + + # forbidden: region, fill, universe + for tag, val in [('region', '-1'), ('fill', '2'), ('universe', '0')]: + xml = f'' + with pytest.raises(ValueError, match=tag): + openmc.DAGMCCell.from_xml_element(ET.fromstring(xml), mats, placeholder_univ) + + # forbidden: translation, rotation + for tag, val in [('translation', '1 0 0'), ('rotation', '0 0 90')]: + xml = f'' + with pytest.raises(ValueError, match=tag): + openmc.DAGMCCell.from_xml_element(ET.fromstring(xml), mats, placeholder_univ) + + # missing material raises + xml = '' + with pytest.raises(ValueError, match='material'): + openmc.DAGMCCell.from_xml_element(ET.fromstring(xml), mats, placeholder_univ) + + def test_plot(run_in_tmpdir): zcyl = openmc.ZCylinder() c = openmc.Cell(region=-zcyl) From dfb6c5699c6fb966ca4dfd396d3ccb22f4e28f60 Mon Sep 17 00:00:00 2001 From: Ahnaf Tahmid Chowdhury Date: Fri, 22 May 2026 02:33:56 +0600 Subject: [PATCH 627/671] Modernize CMake packaging (#3653) Co-authored-by: Paul Romano --- CMakeLists.txt | 24 +++++++++++++++------ cmake/OpenMCConfig.cmake.in | 34 +++++++++++++++++++----------- cmake/OpenMCConfigVersion.cmake.in | 11 ---------- 3 files changed, 40 insertions(+), 29 deletions(-) delete mode 100644 cmake/OpenMCConfigVersion.cmake.in diff --git a/CMakeLists.txt b/CMakeLists.txt index 9fe133a22..d5af01610 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -611,9 +611,7 @@ add_custom_command(TARGET libopenmc POST_BUILD #=============================================================================== # Install executable, scripts, manpage, license #=============================================================================== - -configure_file(cmake/OpenMCConfig.cmake.in "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/OpenMCConfig.cmake" @ONLY) -configure_file(cmake/OpenMCConfigVersion.cmake.in "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/OpenMCConfigVersion.cmake" @ONLY) +include(CMakePackageConfigHelpers) set(INSTALL_CONFIGDIR ${CMAKE_INSTALL_LIBDIR}/cmake/OpenMC) install(TARGETS openmc libopenmc @@ -627,10 +625,24 @@ install(EXPORT openmc-targets NAMESPACE OpenMC:: DESTINATION ${INSTALL_CONFIGDIR}) +configure_package_config_file( + "cmake/OpenMCConfig.cmake.in" + "${CMAKE_BINARY_DIR}/${CMAKE_FILES_DIRECTORY}/OpenMCConfig.cmake" + INSTALL_DESTINATION ${INSTALL_CONFIGDIR} +) + +write_basic_package_version_file( + "${CMAKE_BINARY_DIR}/${CMAKE_FILES_DIRECTORY}/OpenMCConfigVersion.cmake" + VERSION ${OPENMC_VERSION} + COMPATIBILITY AnyNewerVersion +) + install(FILES - "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/OpenMCConfig.cmake" - "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/OpenMCConfigVersion.cmake" - DESTINATION ${INSTALL_CONFIGDIR}) + "${CMAKE_BINARY_DIR}/${CMAKE_FILES_DIRECTORY}/OpenMCConfig.cmake" + "${CMAKE_BINARY_DIR}/${CMAKE_FILES_DIRECTORY}/OpenMCConfigVersion.cmake" + DESTINATION "${INSTALL_CONFIGDIR}" +) + install(FILES man/man1/openmc.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) install(FILES LICENSE DESTINATION "${CMAKE_INSTALL_DOCDIR}" RENAME copyright) install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) diff --git a/cmake/OpenMCConfig.cmake.in b/cmake/OpenMCConfig.cmake.in index cc837bba7..0e15060a3 100644 --- a/cmake/OpenMCConfig.cmake.in +++ b/cmake/OpenMCConfig.cmake.in @@ -1,12 +1,18 @@ -get_filename_component(OpenMC_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) +@PACKAGE_INIT@ -# Compute the install prefix from this file's location -get_filename_component(_OPENMC_PREFIX "${OpenMC_CMAKE_DIR}/../../.." ABSOLUTE) +include("${CMAKE_CURRENT_LIST_DIR}/OpenMCConfigVersion.cmake") +include(CMakeFindDependencyMacro) + +# Explicitly calculate prefix if it was not generated above +if(NOT DEFINED PACKAGE_PREFIX_DIR) + get_filename_component(PACKAGE_PREFIX_DIR "${CMAKE_CURRENT_LIST_DIR}/../../.." ABSOLUTE) +endif() + +find_dependency(fmt CONFIG REQUIRED HINTS ${PACKAGE_PREFIX_DIR}) +find_dependency(pugixml CONFIG REQUIRED HINTS ${PACKAGE_PREFIX_DIR}) -find_package(fmt CONFIG REQUIRED HINTS ${_OPENMC_PREFIX}) -find_package(pugixml CONFIG REQUIRED HINTS ${_OPENMC_PREFIX}) if(@OPENMC_USE_DAGMC@) - find_package(DAGMC REQUIRED HINTS @DAGMC_DIR@) + find_dependency(DAGMC REQUIRED HINTS @DAGMC_DIR@) endif() if(@OPENMC_USE_LIBMESH@) @@ -16,20 +22,24 @@ if(@OPENMC_USE_LIBMESH@) pkg_check_modules(LIBMESH REQUIRED @LIBMESH_PC_FILE@>=1.7.0 IMPORTED_TARGET) endif() -find_package(PNG) - -if(NOT TARGET OpenMC::libopenmc) - include("${OpenMC_CMAKE_DIR}/OpenMCTargets.cmake") +if("@PNG_FOUND@") + find_dependency(PNG) endif() if(@OPENMC_USE_MPI@) - find_package(MPI REQUIRED) + find_dependency(MPI REQUIRED) endif() if(@OPENMC_USE_OPENMP@) - find_package(OpenMP REQUIRED) + find_dependency(OpenMP REQUIRED) endif() if(@OPENMC_USE_UWUW@ AND NOT ${DAGMC_BUILD_UWUW}) message(FATAL_ERROR "UWUW is enabled in OpenMC but the DAGMC installation discovered was not configured with UWUW.") endif() + +include("${CMAKE_CURRENT_LIST_DIR}/OpenMCTargets.cmake") + +if(NOT OpenMC_FIND_QUIETLY) + message(STATUS "Found OpenMC: ${PACKAGE_VERSION} (found in ${PACKAGE_PREFIX_DIR})") +endif() diff --git a/cmake/OpenMCConfigVersion.cmake.in b/cmake/OpenMCConfigVersion.cmake.in deleted file mode 100644 index 90d345de4..000000000 --- a/cmake/OpenMCConfigVersion.cmake.in +++ /dev/null @@ -1,11 +0,0 @@ -set(PACKAGE_VERSION "@OPENMC_VERSION@") - -# Check whether the requested PACKAGE_FIND_VERSION is compatible -if("${PACKAGE_VERSION}" VERSION_LESS "${PACKAGE_FIND_VERSION}") - set(PACKAGE_VERSION_COMPATIBLE FALSE) -else() - set(PACKAGE_VERSION_COMPATIBLE TRUE) - if ("${PACKAGE_VERSION}" VERSION_EQUAL "${PACKAGE_FIND_VERSION}") - set(PACKAGE_VERSION_EXACT TRUE) - endif() -endif() From 1914e3eefa7560791056ad9c06bcb542fe19204c Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 29 May 2026 21:53:45 -0500 Subject: [PATCH 628/671] Support endf.Material in `from_endf` methods (#3932) --- openmc/data/angle_distribution.py | 7 +++--- openmc/data/decay.py | 25 +++++++------------- openmc/data/endf.py | 34 +++++++++++++++++++++------ openmc/data/fission_energy.py | 7 +++--- openmc/data/neutron.py | 10 ++++---- openmc/data/photon.py | 22 +++++++---------- openmc/data/reaction.py | 7 +++--- openmc/data/resonance.py | 7 ++++-- openmc/data/resonance_covariance.py | 3 ++- openmc/data/thermal.py | 7 ++---- openmc/deplete/chain.py | 12 +++++----- tests/unit_tests/test_data_decay.py | 22 +++++++++++++++++ tests/unit_tests/test_data_neutron.py | 22 +++++++++++++++++ tests/unit_tests/test_data_photon.py | 24 +++++++++++++++++++ tests/unit_tests/test_data_thermal.py | 12 ++++++++++ tests/unit_tests/test_endf.py | 11 +++++++++ 16 files changed, 166 insertions(+), 66 deletions(-) diff --git a/openmc/data/angle_distribution.py b/openmc/data/angle_distribution.py index e59ffa0c7..778139cc2 100644 --- a/openmc/data/angle_distribution.py +++ b/openmc/data/angle_distribution.py @@ -10,8 +10,8 @@ from openmc.mixin import EqualityMixin from openmc.stats import Univariate, Tabular, Uniform, Legendre from .function import INTERPOLATION_SCHEME from .data import EV_PER_MEV -from .endf import get_head_record, get_cont_record, get_tab1_record, \ - get_list_record, get_tab2_record +from .endf import as_evaluation, get_head_record, get_cont_record, \ + get_tab1_record, get_list_record, get_tab2_record class AngleDistribution(EqualityMixin): @@ -213,7 +213,7 @@ class AngleDistribution(EqualityMixin): Parameters ---------- - ev : openmc.data.endf.Evaluation + ev : openmc.data.endf.Evaluation or endf.Material ENDF evaluation mt : int The MT value of the reaction to get angular distributions for @@ -224,6 +224,7 @@ class AngleDistribution(EqualityMixin): Angular distribution """ + ev = as_evaluation(ev) file_obj = StringIO(ev.section[4, mt]) # Read HEAD record diff --git a/openmc/data/decay.py b/openmc/data/decay.py index ce20a252c..5f95e1281 100644 --- a/openmc/data/decay.py +++ b/openmc/data/decay.py @@ -14,7 +14,8 @@ from openmc.mixin import EqualityMixin from openmc.stats import Discrete, Tabular, Univariate, combine_distributions from .data import gnds_name, zam from .function import INTERPOLATION_SCHEME -from .endf import Evaluation, get_head_record, get_list_record, get_tab1_record +from .endf import ( + as_evaluation, get_head_record, get_list_record, get_tab1_record) # Gives name and (change in A, change in Z) resulting from decay @@ -75,7 +76,7 @@ class FissionProductYields(EqualityMixin): Parameters ---------- - ev_or_filename : str of openmc.data.endf.Evaluation + ev_or_filename : str, openmc.data.endf.Evaluation, or endf.Material ENDF fission product yield evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. @@ -133,11 +134,7 @@ class FissionProductYields(EqualityMixin): return energies, data - # Get evaluation if str is passed - if isinstance(ev_or_filename, Evaluation): - ev = ev_or_filename - else: - ev = Evaluation(ev_or_filename) + ev = as_evaluation(ev_or_filename) # Assign basic nuclide properties self.nuclide = { @@ -164,7 +161,7 @@ class FissionProductYields(EqualityMixin): Parameters ---------- - ev_or_filename : str or openmc.data.endf.Evaluation + ev_or_filename : str, openmc.data.endf.Evaluation, or endf.Material ENDF fission product yield evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. @@ -292,7 +289,7 @@ class Decay(EqualityMixin): Parameters ---------- - ev_or_filename : str of openmc.data.endf.Evaluation + ev_or_filename : str, openmc.data.endf.Evaluation, or endf.Material ENDF radioactive decay data evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. @@ -323,11 +320,7 @@ class Decay(EqualityMixin): """ def __init__(self, ev_or_filename): - # Get evaluation if str is passed - if isinstance(ev_or_filename, Evaluation): - ev = ev_or_filename - else: - ev = Evaluation(ev_or_filename) + ev = as_evaluation(ev_or_filename) file_obj = StringIO(ev.section[8, 457]) @@ -486,7 +479,7 @@ class Decay(EqualityMixin): Parameters ---------- - ev_or_filename : str or openmc.data.endf.Evaluation + ev_or_filename : str, openmc.data.endf.Evaluation, or endf.Material ENDF radioactive decay data evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. @@ -649,5 +642,3 @@ def decay_energy(nuclide: str): warn(f"Chain file '{chain_file}' does not have any decay energy.") return _DECAY_ENERGY.get(nuclide, 0.0) - - diff --git a/openmc/data/endf.py b/openmc/data/endf.py index c50431dc7..edd8afffb 100644 --- a/openmc/data/endf.py +++ b/openmc/data/endf.py @@ -12,7 +12,12 @@ import re from .data import gnds_name from .function import Tabulated1D -from endf.material import _LIBRARY, _SUBLIBRARY, get_materials as get_evaluations +from endf.material import ( + Material, + _LIBRARY, + _SUBLIBRARY, + get_materials as get_evaluations, +) from endf.incident_neutron import SUM_RULES from endf.records import ( float_endf, @@ -44,7 +49,7 @@ class Evaluation: Parameters ---------- - filename_or_obj : str or file-like + filename_or_obj : str, file-like, or endf.Material Path to ENDF file to read or an open file positioned at the start of an ENDF material @@ -64,17 +69,25 @@ class Evaluation: """ def __init__(self, filename_or_obj): + self.section = {} + self.info = {} + self.target = {} + self.projectile = {} + self.reaction_list = [] + + if isinstance(filename_or_obj, Material): + self.section = dict(filename_or_obj.section_text) + self.section_data = filename_or_obj.section_data + self.material = filename_or_obj.MAT + self._read_header() + return + if isinstance(filename_or_obj, (str, PurePath)): fh = open(str(filename_or_obj), 'r') need_to_close = True else: fh = filename_or_obj need_to_close = False - self.section = {} - self.info = {} - self.target = {} - self.projectile = {} - self.reaction_list = [] # Skip TPID record. Evaluators sometimes put in TPID records that are # ill-formated because they lack MF/MT values or put them in the wrong @@ -199,3 +212,10 @@ class Evaluation: self.target['mass_number'], self.target['isomeric_state']) + +def as_evaluation(ev_or_filename): + """Return an object supporting OpenMC's legacy Evaluation interface.""" + if isinstance(ev_or_filename, Evaluation): + return ev_or_filename + else: + return Evaluation(ev_or_filename) diff --git a/openmc/data/fission_energy.py b/openmc/data/fission_energy.py index 3c7998ee2..e4fac087a 100644 --- a/openmc/data/fission_energy.py +++ b/openmc/data/fission_energy.py @@ -5,7 +5,8 @@ from io import StringIO import openmc.checkvalue as cv from openmc.mixin import EqualityMixin from .data import EV_PER_MEV -from .endf import get_cont_record, get_list_record, get_tab1_record, Evaluation +from .endf import ( + as_evaluation, get_cont_record, get_list_record, get_tab1_record) from .function import Function1D, Tabulated1D, Polynomial, sum_functions @@ -195,7 +196,7 @@ class FissionEnergyRelease(EqualityMixin): Parameters ---------- - ev : openmc.data.endf.Evaluation + ev : openmc.data.endf.Evaluation or endf.Material ENDF evaluation incident_neutron : openmc.data.IncidentNeutron Corresponding incident neutron dataset @@ -206,7 +207,7 @@ class FissionEnergyRelease(EqualityMixin): Fission energy release data """ - cv.check_type('evaluation', ev, Evaluation) + ev = as_evaluation(ev) # Check to make sure this ENDF file matches the expected isomer. if ev.target['atomic_number'] != incident_neutron.atomic_number: diff --git a/openmc/data/neutron.py b/openmc/data/neutron.py index 492cdd7f3..f17bb3fe4 100644 --- a/openmc/data/neutron.py +++ b/openmc/data/neutron.py @@ -13,7 +13,8 @@ from . import HDF5_VERSION, HDF5_VERSION_MAJOR from .ace import Library, Table, get_table, get_metadata from .data import ATOMIC_SYMBOL, K_BOLTZMANN, EV_PER_MEV, gnds_name from .endf import ( - Evaluation, SUM_RULES, get_head_record, get_tab1_record, get_evaluations) + Evaluation, SUM_RULES, as_evaluation, get_head_record, get_tab1_record, + get_evaluations) from .fission_energy import FissionEnergyRelease from .function import Tabulated1D, Sum, ResonancesWithBackground from .njoy import make_ace, make_pendf @@ -652,7 +653,7 @@ class IncidentNeutron(EqualityMixin): Parameters ---------- - ev_or_filename : openmc.data.endf.Evaluation or str + ev_or_filename : openmc.data.endf.Evaluation, endf.Material, or str ENDF evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. @@ -666,10 +667,7 @@ class IncidentNeutron(EqualityMixin): Incident neutron continuous-energy data """ - if isinstance(ev_or_filename, Evaluation): - ev = ev_or_filename - else: - ev = Evaluation(ev_or_filename) + ev = as_evaluation(ev_or_filename) atomic_number = ev.target['atomic_number'] mass_number = ev.target['mass_number'] diff --git a/openmc/data/photon.py b/openmc/data/photon.py index bc21b2e56..13cd3ec95 100644 --- a/openmc/data/photon.py +++ b/openmc/data/photon.py @@ -15,7 +15,8 @@ from openmc.mixin import EqualityMixin from . import HDF5_VERSION, HDF5_VERSION_MAJOR from .ace import Table, get_metadata, get_table from .data import ATOMIC_SYMBOL, EV_PER_MEV -from .endf import Evaluation, get_head_record, get_tab1_record, get_list_record +from .endf import ( + as_evaluation, get_head_record, get_tab1_record, get_list_record) from .function import Tabulated1D @@ -272,7 +273,7 @@ class AtomicRelaxation(EqualityMixin): Parameters ---------- - ev_or_filename : str or openmc.data.endf.Evaluation + ev_or_filename : str, openmc.data.endf.Evaluation, or endf.Material ENDF atomic relaxation evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. @@ -282,10 +283,7 @@ class AtomicRelaxation(EqualityMixin): Atomic relaxation data """ - if isinstance(ev_or_filename, Evaluation): - ev = ev_or_filename - else: - ev = Evaluation(ev_or_filename) + ev = as_evaluation(ev_or_filename) # Atomic relaxation data is always MF=28, MT=533 if (28, 533) not in ev.section: @@ -606,10 +604,10 @@ class IncidentPhoton(EqualityMixin): Parameters ---------- - photoatomic : str or openmc.data.endf.Evaluation + photoatomic : str, openmc.data.endf.Evaluation, or endf.Material ENDF photoatomic data evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. - relaxation : str or openmc.data.endf.Evaluation, optional + relaxation : str, openmc.data.endf.Evaluation, or endf.Material, optional ENDF atomic relaxation data evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. @@ -619,10 +617,7 @@ class IncidentPhoton(EqualityMixin): Photon interaction data """ - if isinstance(photoatomic, Evaluation): - ev = photoatomic - else: - ev = Evaluation(photoatomic) + ev = as_evaluation(photoatomic) Z = ev.target['atomic_number'] data = cls(Z) @@ -1071,7 +1066,7 @@ class PhotonReaction(EqualityMixin): Parameters ---------- - ev : openmc.data.endf.Evaluation + ev : openmc.data.endf.Evaluation or endf.Material ENDF photo-atomic interaction data evaluation mt : int The MT value of the reaction to get data for @@ -1082,6 +1077,7 @@ class PhotonReaction(EqualityMixin): Photon reaction data """ + ev = as_evaluation(ev) rx = cls(mt) # Read photon cross section diff --git a/openmc/data/reaction.py b/openmc/data/reaction.py index ab1285b72..44ced5511 100644 --- a/openmc/data/reaction.py +++ b/openmc/data/reaction.py @@ -13,8 +13,8 @@ from .angle_distribution import AngleDistribution from .angle_energy import AngleEnergy from .correlated import CorrelatedAngleEnergy from .data import ATOMIC_SYMBOL, K_BOLTZMANN, EV_PER_MEV -from .endf import get_head_record, get_tab1_record, get_list_record, \ - get_tab2_record, get_cont_record +from .endf import as_evaluation, get_head_record, get_tab1_record, \ + get_list_record, get_tab2_record, get_cont_record from .energy_distribution import EnergyDistribution, LevelInelastic, \ DiscretePhoton from .function import Tabulated1D, Polynomial @@ -1151,7 +1151,7 @@ class Reaction(EqualityMixin): Parameters ---------- - ev : openmc.data.endf.Evaluation + ev : openmc.data.endf.Evaluation or endf.Material ENDF evaluation mt : int The MT value of the reaction to get data for @@ -1162,6 +1162,7 @@ class Reaction(EqualityMixin): Reaction data """ + ev = as_evaluation(ev) rx = Reaction(mt) # Integrated cross section diff --git a/openmc/data/resonance.py b/openmc/data/resonance.py index 31e230df5..52ee7f1a9 100644 --- a/openmc/data/resonance.py +++ b/openmc/data/resonance.py @@ -7,7 +7,9 @@ import pandas as pd import openmc.checkvalue as cv from .data import NEUTRON_MASS -from .endf import get_head_record, get_cont_record, get_tab1_record, get_list_record +from .endf import ( + as_evaluation, get_head_record, get_cont_record, get_tab1_record, + get_list_record) try: from .reconstruct import wave_number, penetration_shift, reconstruct_mlbw, \ reconstruct_slbw, reconstruct_rm @@ -77,7 +79,7 @@ class Resonances: Parameters ---------- - ev : openmc.data.endf.Evaluation + ev : openmc.data.endf.Evaluation or endf.Material ENDF evaluation Returns @@ -86,6 +88,7 @@ class Resonances: Resonance data """ + ev = as_evaluation(ev) file_obj = io.StringIO(ev.section[2, 151]) # Determine whether discrete or continuous representation diff --git a/openmc/data/resonance_covariance.py b/openmc/data/resonance_covariance.py index 709657044..aa86ae33c 100644 --- a/openmc/data/resonance_covariance.py +++ b/openmc/data/resonance_covariance.py @@ -74,7 +74,7 @@ class ResonanceCovariances(Resonances): Parameters ---------- - ev : openmc.data.endf.Evaluation + ev : openmc.data.endf.Evaluation or endf.Material ENDF evaluation resonances : openmc.data.Resonance object openmc.data.Resonanance object generated from the same evaluation @@ -86,6 +86,7 @@ class ResonanceCovariances(Resonances): Resonance covariance data """ + ev = endf.as_evaluation(ev) file_obj = io.StringIO(ev.section[32, 151]) # Determine whether discrete or continuous representation diff --git a/openmc/data/thermal.py b/openmc/data/thermal.py index 54e3a7330..3b910c535 100644 --- a/openmc/data/thermal.py +++ b/openmc/data/thermal.py @@ -1042,7 +1042,7 @@ class ThermalScattering(EqualityMixin): Parameters ---------- - ev_or_filename : openmc.data.endf.Evaluation or str + ev_or_filename : openmc.data.endf.Evaluation, endf.Material, or str ENDF evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. divide_incoherent_elastic : bool @@ -1056,10 +1056,7 @@ class ThermalScattering(EqualityMixin): Thermal scattering data """ - if isinstance(ev_or_filename, endf.Evaluation): - ev = ev_or_filename - else: - ev = endf.Evaluation(ev_or_filename) + ev = endf.as_evaluation(ev_or_filename) # Read incoherent inelastic data assert (7, 4) in ev.section, 'No MF=7, MT=4 found in thermal scattering' diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index 42d4ab07e..a2ff6dea0 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -319,16 +319,16 @@ class Chain: String arguments in ``decay_files``, ``fpy_files``, and ``neutron_files`` will be treated as file names to be read. - Alternatively, :class:`openmc.data.endf.Evaluation` instances - can be included in these arguments. + Alternatively, :class:`openmc.data.endf.Evaluation` or + ``endf.Material`` instances can be included in these arguments. Parameters ---------- - decay_files : list of str or openmc.data.endf.Evaluation + decay_files : list of str, openmc.data.endf.Evaluation, or endf.Material List of ENDF decay sub-library files - fpy_files : list of str or openmc.data.endf.Evaluation + fpy_files : list of str, openmc.data.endf.Evaluation, or endf.Material List of ENDF neutron-induced fission product yield sub-library files - neutron_files : list of str or openmc.data.endf.Evaluation + neutron_files : list of str, openmc.data.endf.Evaluation, or endf.Material List of ENDF neutron reaction sub-library files reactions : iterable of str, optional Transmutation reactions to include in the depletion chain, e.g., @@ -363,7 +363,7 @@ class Chain: print('Processing neutron sub-library files...') reactions = {} for f in neutron_files: - evaluation = openmc.data.endf.Evaluation(f) + evaluation = openmc.data.endf.as_evaluation(f) name = evaluation.gnds_name reactions[name] = {} for mf, mt, nc, mod in evaluation.reaction_list: diff --git a/tests/unit_tests/test_data_decay.py b/tests/unit_tests/test_data_decay.py index de8d90a43..a48c55325 100644 --- a/tests/unit_tests/test_data_decay.py +++ b/tests/unit_tests/test_data_decay.py @@ -100,6 +100,28 @@ def test_fpy(u235_yields): ufloat_close(thermal['I135'], ufloat(0.0292737, 0.000819663)) +def test_decay_from_endf_material(endf_data): + filename = os.path.join(endf_data, 'decay', 'dec-041_Nb_090.endf') + material = openmc.data.endf.get_evaluations(filename)[0] + + data = openmc.data.Decay.from_endf(material) + + assert data.nuclide['name'] == 'Nb90' + assert not data.nuclide['stable'] + assert len(data.modes) == 2 + + +def test_fpy_from_endf_material(endf_data): + filename = os.path.join(endf_data, 'nfy', 'nfy-092_U_235.endf') + material = openmc.data.endf.get_evaluations(filename)[0] + + data = openmc.data.FissionProductYields.from_endf(material) + + assert data.nuclide['name'] == 'U235' + assert data.energies == pytest.approx([0.0253, 500.e3, 1.4e7]) + assert 'I135' in data.cumulative[0] + + def test_sources(ba137m, nb90): # Running .sources twice should give same objects sources = ba137m.sources diff --git a/tests/unit_tests/test_data_neutron.py b/tests/unit_tests/test_data_neutron.py index d43d93ae5..c767f0171 100644 --- a/tests/unit_tests/test_data_neutron.py +++ b/tests/unit_tests/test_data_neutron.py @@ -133,6 +133,28 @@ def test_attributes(pu239): assert pu239.atomic_weight_ratio == pytest.approx(236.9986) +def test_from_endf_material(endf_data): + filename = os.path.join(endf_data, 'neutrons', 'n-001_H_001.endf') + material = openmc.data.endf.get_evaluations(filename)[0] + + data = openmc.data.IncidentNeutron.from_endf(material) + + assert data.name == 'H1' + assert data.atomic_number == 1 + assert data.mass_number == 1 + assert 2 in data.reactions + + +def test_fission_energy_from_endf_material(endf_data): + filename = os.path.join(endf_data, 'neutrons', 'n-092_U_235.endf') + material = openmc.data.endf.get_evaluations(filename)[0] + neutron = openmc.data.IncidentNeutron.from_endf(material) + + data = openmc.data.FissionEnergyRelease.from_endf(material, neutron) + + assert data.fragments(0.0) > 0.0 + + def test_fission_energy(pu239): fer = pu239.fission_energy assert isinstance(fer, openmc.data.FissionEnergyRelease) diff --git a/tests/unit_tests/test_data_photon.py b/tests/unit_tests/test_data_photon.py index 98b180f52..aceb82c3a 100644 --- a/tests/unit_tests/test_data_photon.py +++ b/tests/unit_tests/test_data_photon.py @@ -149,3 +149,27 @@ def test_photodat_only(run_in_tmpdir, endf_data): photoatomic_file = endf_dir / 'photoat' / 'photoat-001_H_000.endf' data = openmc.data.IncidentPhoton.from_endf(photoatomic_file) data.export_to_hdf5('tmp.h5', 'w') + + +def test_from_endf_material(endf_data): + endf_dir = Path(endf_data) + photoatomic_file = endf_dir / 'photoat' / 'photoat-001_H_000.endf' + relaxation_file = endf_dir / 'atomic_relax' / 'atom-001_H_000.endf' + photoatomic = openmc.data.endf.get_evaluations(photoatomic_file)[0] + relaxation = openmc.data.endf.get_evaluations(relaxation_file)[0] + + data = openmc.data.IncidentPhoton.from_endf(photoatomic, relaxation) + + assert data.atomic_number == 1 + assert 502 in data.reactions + assert data.atomic_relaxation.binding_energy['K'] == pytest.approx(13.61) + + +def test_atomic_relaxation_from_endf_material(endf_data): + filename = Path(endf_data) / 'atomic_relax' / 'atom-001_H_000.endf' + material = openmc.data.endf.get_evaluations(filename)[0] + + data = openmc.data.AtomicRelaxation.from_endf(material) + + assert data.binding_energy['K'] == pytest.approx(13.61) + assert data.num_electrons['K'] == pytest.approx(1.0) diff --git a/tests/unit_tests/test_data_thermal.py b/tests/unit_tests/test_data_thermal.py index 7cd63ca71..81ed42b41 100644 --- a/tests/unit_tests/test_data_thermal.py +++ b/tests/unit_tests/test_data_thermal.py @@ -148,6 +148,18 @@ def test_h2o_endf(endf_data): '600K', '650K', '800K'] +def test_from_endf_material(endf_data): + filename = os.path.join(endf_data, 'thermal_scatt', 'tsl-HinH2O.endf') + material = openmc.data.endf.get_evaluations(filename)[0] + + h2o = openmc.data.ThermalScattering.from_endf( + material, divide_incoherent_elastic=True) + + assert not h2o.elastic + assert h2o.atomic_weight_ratio == pytest.approx(0.99917) + assert h2o.temperatures[0] == '294K' + + def test_hzrh_attributes(hzrh): assert hzrh.atomic_weight_ratio == pytest.approx(0.99917) assert hzrh.energy_max == pytest.approx(1.9734) diff --git a/tests/unit_tests/test_endf.py b/tests/unit_tests/test_endf.py index 1d4982054..cdd21d7a6 100644 --- a/tests/unit_tests/test_endf.py +++ b/tests/unit_tests/test_endf.py @@ -2,6 +2,17 @@ from openmc.data import endf from pytest import approx +def test_evaluation_from_material(endf_data): + filename = f'{endf_data}/neutrons/n-001_H_001.endf' + material = endf.get_evaluations(filename)[0] + evaluation = endf.Evaluation(material) + + assert evaluation.material == material.MAT + assert evaluation.gnds_name == 'H1' + assert evaluation.section == material.section_text + assert evaluation.reaction_list == material[1, 451]['section_list'] + + def test_float_endf(): assert endf.float_endf('+3.2146') == approx(3.2146) assert endf.float_endf('.12345') == approx(0.12345) From ddabe1c8c557b3d1228a6f5cb661bac5bb6582e4 Mon Sep 17 00:00:00 2001 From: Jon Shimwell Date: Sat, 30 May 2026 05:25:22 +0200 Subject: [PATCH 629/671] Avoid storing inconsistent sum/sum_sq on summed D1S tallies (#3949) Co-authored-by: shimwell Co-authored-by: Paul Romano --- openmc/deplete/d1s.py | 11 ++++++----- tests/unit_tests/test_d1s.py | 4 ++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/openmc/deplete/d1s.py b/openmc/deplete/d1s.py index bc99fc42d..d85d2e8a7 100644 --- a/openmc/deplete/d1s.py +++ b/openmc/deplete/d1s.py @@ -186,8 +186,6 @@ def apply_time_correction( # Apply TCF, broadcasting to the correct dimensions tcf.shape = (1, -1, 1, 1, 1) - new_tally._sum = tally_sum * tcf - new_tally._sum_sq = tally_sum_sq * (tcf*tcf) new_tally._mean = tally_mean * tcf new_tally._std_dev = tally_std_dev * tcf @@ -196,6 +194,8 @@ def apply_time_correction( if sum_nuclides: # Sum over parent nuclides (note that when combining different bins for # parent nuclide, we can't work directly on sum_sq) + new_tally._sum = None + new_tally._sum_sq = None new_tally._mean = new_tally.mean.sum(axis=1).reshape(shape) new_tally._std_dev = np.linalg.norm(new_tally.std_dev, axis=1).reshape(shape) new_tally._derived = True @@ -203,9 +203,10 @@ def apply_time_correction( # Remove ParentNuclideFilter new_tally.filters.pop(i_filter) else: - # Change shape back to (filter combinations, nuclides, scores) - new_tally._sum.shape = shape - new_tally._sum_sq.shape = shape + # Apply TCF and change shape back to (filter combinations, nuclides, + # scores) + new_tally._sum = (tally_sum * tcf).reshape(shape) + new_tally._sum_sq = (tally_sum_sq * (tcf*tcf)).reshape(shape) new_tally._mean.shape = shape new_tally._std_dev.shape = shape diff --git a/tests/unit_tests/test_d1s.py b/tests/unit_tests/test_d1s.py index 8f3b62f40..49c1b3049 100644 --- a/tests/unit_tests/test_d1s.py +++ b/tests/unit_tests/test_d1s.py @@ -150,3 +150,7 @@ def test_apply_time_correction(run_in_tmpdir): result_summed.get_reshaped_data() result.get_pandas_dataframe() result_summed.get_pandas_dataframe() + + # The summed tally is derived, so sum/sum_sq are None + assert result_summed.sum is None + assert result_summed.sum_sq is None From 46d889613277c9d7df3b312fc27775508aef3a0b Mon Sep 17 00:00:00 2001 From: Bor Kos Date: Sat, 30 May 2026 05:25:38 +0200 Subject: [PATCH 630/671] Pulsed Height Tally in mixed neutron-gamma fields (#3937) Co-authored-by: Bor Kos Co-authored-by: Paul Romano --- src/physics.cpp | 11 + .../local_neutron/inputs_true.dat | 40 ++++ .../local_neutron/results_true.dat | 201 ++++++++++++++++++ .../{local => local_photon}/inputs_true.dat | 2 +- .../{local => local_photon}/results_true.dat | 0 .../shared_neutron/inputs_true.dat | 40 ++++ .../shared_neutron/results_true.dat | 201 ++++++++++++++++++ .../{shared => shared_photon}/inputs_true.dat | 2 +- .../results_true.dat | 0 tests/regression_tests/pulse_height/test.py | 19 +- 10 files changed, 505 insertions(+), 11 deletions(-) create mode 100644 tests/regression_tests/pulse_height/local_neutron/inputs_true.dat create mode 100644 tests/regression_tests/pulse_height/local_neutron/results_true.dat rename tests/regression_tests/pulse_height/{local => local_photon}/inputs_true.dat (97%) rename tests/regression_tests/pulse_height/{local => local_photon}/results_true.dat (100%) create mode 100644 tests/regression_tests/pulse_height/shared_neutron/inputs_true.dat create mode 100644 tests/regression_tests/pulse_height/shared_neutron/results_true.dat rename tests/regression_tests/pulse_height/{shared => shared_photon}/inputs_true.dat (97%) rename tests/regression_tests/pulse_height/{shared => shared_photon}/results_true.dat (100%) diff --git a/src/physics.cpp b/src/physics.cpp index 892b39479..dcc771329 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -1227,6 +1227,17 @@ void sample_secondary_photons(Particle& p, int i_nuclide) // Create the secondary photon bool created_photon = p.create_secondary(wgt, u, E, ParticleType::photon()); + // Pre-add photon energy to pht_storage so pht_secondary_particles() + // subtraction results in net zero + if (created_photon && !model::active_pulse_height_tallies.empty()) { + auto it = std::find(model::pulse_height_cells.begin(), + model::pulse_height_cells.end(), p.lowest_coord().cell()); + if (it != model::pulse_height_cells.end()) { + int index = std::distance(model::pulse_height_cells.begin(), it); + p.pht_storage()[index] += E; + } + } + // Tag secondary particle with parent nuclide if (created_photon && settings::use_decay_photons) { p.local_secondary_bank().back().parent_nuclide = diff --git a/tests/regression_tests/pulse_height/local_neutron/inputs_true.dat b/tests/regression_tests/pulse_height/local_neutron/inputs_true.dat new file mode 100644 index 000000000..e25de58b0 --- /dev/null +++ b/tests/regression_tests/pulse_height/local_neutron/inputs_true.dat @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + fixed source + 100 + 5 + + + 1000000.0 1.0 + + + true + false + + + + 1 + + + 0.0 10000.0 20000.0 30000.0 40000.0 50000.0 60000.0 70000.0 80000.0 90000.0 100000.0 110000.0 120000.0 130000.0 140000.0 150000.0 160000.0 170000.0 180000.0 190000.0 200000.0 210000.0 220000.0 230000.0 240000.0 250000.0 260000.0 270000.0 280000.0 290000.0 300000.0 310000.0 320000.0 330000.0 340000.0 350000.0 360000.0 370000.0 380000.0 390000.0 400000.0 410000.0 420000.0 430000.0 440000.0 450000.0 460000.0 470000.0 480000.0 490000.0 500000.0 510000.0 520000.0 530000.0 540000.0 550000.0 560000.0 570000.0 580000.0 590000.0 600000.0 610000.0 620000.0 630000.0 640000.0 650000.0 660000.0 670000.0 680000.0 690000.0 700000.0 710000.0 720000.0 730000.0 740000.0 750000.0 760000.0 770000.0 780000.0 790000.0 800000.0 810000.0 820000.0 830000.0 840000.0 850000.0 860000.0 870000.0 880000.0 890000.0 900000.0 910000.0 920000.0 930000.0 940000.0 950000.0 960000.0 970000.0 980000.0 990000.0 1000000.0 + + + 1 2 + pulse-height + + + diff --git a/tests/regression_tests/pulse_height/local_neutron/results_true.dat b/tests/regression_tests/pulse_height/local_neutron/results_true.dat new file mode 100644 index 000000000..ffa284d92 --- /dev/null +++ b/tests/regression_tests/pulse_height/local_neutron/results_true.dat @@ -0,0 +1,201 @@ +tally 1: +4.890000E+00 +4.784900E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +6.000000E-02 +1.800000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 diff --git a/tests/regression_tests/pulse_height/local/inputs_true.dat b/tests/regression_tests/pulse_height/local_photon/inputs_true.dat similarity index 97% rename from tests/regression_tests/pulse_height/local/inputs_true.dat rename to tests/regression_tests/pulse_height/local_photon/inputs_true.dat index 50e026bfb..84d033d85 100644 --- a/tests/regression_tests/pulse_height/local/inputs_true.dat +++ b/tests/regression_tests/pulse_height/local_photon/inputs_true.dat @@ -2,7 +2,7 @@ - + diff --git a/tests/regression_tests/pulse_height/local/results_true.dat b/tests/regression_tests/pulse_height/local_photon/results_true.dat similarity index 100% rename from tests/regression_tests/pulse_height/local/results_true.dat rename to tests/regression_tests/pulse_height/local_photon/results_true.dat diff --git a/tests/regression_tests/pulse_height/shared_neutron/inputs_true.dat b/tests/regression_tests/pulse_height/shared_neutron/inputs_true.dat new file mode 100644 index 000000000..b13c53ef8 --- /dev/null +++ b/tests/regression_tests/pulse_height/shared_neutron/inputs_true.dat @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + fixed source + 100 + 5 + + + 1000000.0 1.0 + + + true + true + + + + 1 + + + 0.0 10000.0 20000.0 30000.0 40000.0 50000.0 60000.0 70000.0 80000.0 90000.0 100000.0 110000.0 120000.0 130000.0 140000.0 150000.0 160000.0 170000.0 180000.0 190000.0 200000.0 210000.0 220000.0 230000.0 240000.0 250000.0 260000.0 270000.0 280000.0 290000.0 300000.0 310000.0 320000.0 330000.0 340000.0 350000.0 360000.0 370000.0 380000.0 390000.0 400000.0 410000.0 420000.0 430000.0 440000.0 450000.0 460000.0 470000.0 480000.0 490000.0 500000.0 510000.0 520000.0 530000.0 540000.0 550000.0 560000.0 570000.0 580000.0 590000.0 600000.0 610000.0 620000.0 630000.0 640000.0 650000.0 660000.0 670000.0 680000.0 690000.0 700000.0 710000.0 720000.0 730000.0 740000.0 750000.0 760000.0 770000.0 780000.0 790000.0 800000.0 810000.0 820000.0 830000.0 840000.0 850000.0 860000.0 870000.0 880000.0 890000.0 900000.0 910000.0 920000.0 930000.0 940000.0 950000.0 960000.0 970000.0 980000.0 990000.0 1000000.0 + + + 1 2 + pulse-height + + + diff --git a/tests/regression_tests/pulse_height/shared_neutron/results_true.dat b/tests/regression_tests/pulse_height/shared_neutron/results_true.dat new file mode 100644 index 000000000..ffa284d92 --- /dev/null +++ b/tests/regression_tests/pulse_height/shared_neutron/results_true.dat @@ -0,0 +1,201 @@ +tally 1: +4.890000E+00 +4.784900E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +6.000000E-02 +1.800000E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.000000E-02 +1.000000E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 diff --git a/tests/regression_tests/pulse_height/shared/inputs_true.dat b/tests/regression_tests/pulse_height/shared_photon/inputs_true.dat similarity index 97% rename from tests/regression_tests/pulse_height/shared/inputs_true.dat rename to tests/regression_tests/pulse_height/shared_photon/inputs_true.dat index c2aef829c..608410c50 100644 --- a/tests/regression_tests/pulse_height/shared/inputs_true.dat +++ b/tests/regression_tests/pulse_height/shared_photon/inputs_true.dat @@ -2,7 +2,7 @@ - + diff --git a/tests/regression_tests/pulse_height/shared/results_true.dat b/tests/regression_tests/pulse_height/shared_photon/results_true.dat similarity index 100% rename from tests/regression_tests/pulse_height/shared/results_true.dat rename to tests/regression_tests/pulse_height/shared_photon/results_true.dat diff --git a/tests/regression_tests/pulse_height/test.py b/tests/regression_tests/pulse_height/test.py index 2cbb9152e..371fcbd0f 100644 --- a/tests/regression_tests/pulse_height/test.py +++ b/tests/regression_tests/pulse_height/test.py @@ -6,23 +6,24 @@ from openmc.utility_funcs import change_directory from tests.testing_harness import PyAPITestHarness -@pytest.mark.parametrize("shared_secondary,subdir", [ - (False, "local"), - (True, "shared"), +@pytest.mark.parametrize("shared_secondary,particle", [ + (False, "photon"), + (False, "neutron"), + (True, "photon"), + (True, "neutron") ]) -def test_pulse_height(shared_secondary, subdir): +def test_pulse_height(shared_secondary, particle): + subdir = f"shared_{particle}" if shared_secondary else f"local_{particle}" with change_directory(subdir): openmc.reset_auto_ids() model = openmc.Model() # Define materials NaI = openmc.Material() - NaI.set_density('g/cc', 3.7) + NaI.set_density('g/cm3', 3.7) NaI.add_element('Na', 1.0) NaI.add_element('I', 1.0) - model.materials = openmc.Materials([NaI]) - # Define geometry: two spheres in each other s1 = openmc.Sphere(r=1) s2 = openmc.Sphere(r=2, boundary_type='vacuum') @@ -38,14 +39,14 @@ def test_pulse_height(shared_secondary, subdir): model.settings.shared_secondary_bank = shared_secondary model.settings.source = openmc.IndependentSource( energy=openmc.stats.delta_function(1e6), - particle='photon' + particle=particle ) # Define tallies tally = openmc.Tally(name="pht tally") tally.scores = ['pulse-height'] cell_filter = openmc.CellFilter(inner_sphere) - energy_filter = openmc.EnergyFilter(np.linspace(0, 1_000_000, 101)) + energy_filter = openmc.EnergyFilter(np.linspace(0, 1e6, 101)) tally.filters = [cell_filter, energy_filter] model.tallies = [tally] From 2dd5322fee0373f7810de32c6d4126bc728aa390 Mon Sep 17 00:00:00 2001 From: hugo-barthod Date: Sat, 30 May 2026 07:35:31 +0200 Subject: [PATCH 631/671] Fix undefined variable in openmc.mgxs.library (scatt_mgxs) (#3943) Co-authored-by: Paul Romano --- openmc/mgxs/library.py | 8 + ...at => inputs_true_multiplicity_matrix.dat} | 0 .../inputs_true_scatter_matrix.dat | 217 ++++++++++++++++++ .../mgxs_library_ce_to_mg/test.py | 24 +- 4 files changed, 244 insertions(+), 5 deletions(-) rename tests/regression_tests/mgxs_library_ce_to_mg/{inputs_true.dat => inputs_true_multiplicity_matrix.dat} (100%) create mode 100644 tests/regression_tests/mgxs_library_ce_to_mg/inputs_true_scatter_matrix.dat diff --git a/openmc/mgxs/library.py b/openmc/mgxs/library.py index 78ab3ca19..faa83c048 100644 --- a/openmc/mgxs/library.py +++ b/openmc/mgxs/library.py @@ -1290,6 +1290,14 @@ class Library: 'are ignored since multiplicity or nu-scatter matrices '\ 'were not tallied for ' + xsdata_name warn(msg, RuntimeWarning) + + if 'scatter matrix' in self.mgxs_types: + scatt_mgxs = self.get_mgxs(domain, 'scatter matrix') + elif 'consistent scatter matrix' in self.mgxs_types: + scatt_mgxs = self.get_mgxs(domain, 'consistent scatter matrix') + else: + raise ValueError(f'No scatter matrix found for {xsdata_name}.') + xsdata.set_scatter_matrix_mgxs(scatt_mgxs, temperature=temperature, xs_type=xs_type, nuclide=[nuclide], diff --git a/tests/regression_tests/mgxs_library_ce_to_mg/inputs_true.dat b/tests/regression_tests/mgxs_library_ce_to_mg/inputs_true_multiplicity_matrix.dat similarity index 100% rename from tests/regression_tests/mgxs_library_ce_to_mg/inputs_true.dat rename to tests/regression_tests/mgxs_library_ce_to_mg/inputs_true_multiplicity_matrix.dat diff --git a/tests/regression_tests/mgxs_library_ce_to_mg/inputs_true_scatter_matrix.dat b/tests/regression_tests/mgxs_library_ce_to_mg/inputs_true_scatter_matrix.dat new file mode 100644 index 000000000..6e60f1b19 --- /dev/null +++ b/tests/regression_tests/mgxs_library_ce_to_mg/inputs_true_scatter_matrix.dat @@ -0,0 +1,217 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + + + -0.63 -0.63 -1 0.63 0.63 1 + + + true + + + + + + 7 + + + 0.0 0.625 20000000.0 + + + 0.0 0.625 20000000.0 + + + 3 + + + 8 + + + 9 + + + 43 44 + total + flux + tracklength + + + 43 44 + total + total + tracklength + + + 43 44 + total + flux + tracklength + + + 43 44 + total + absorption + tracklength + + + 43 44 + total + flux + analog + + + 43 44 49 + total + nu-fission + analog + + + 43 44 + total + flux + analog + + + 43 44 49 53 + total + scatter + analog + + + 54 44 + total + flux + tracklength + + + 54 44 + total + total + tracklength + + + 54 44 + total + flux + tracklength + + + 54 44 + total + absorption + tracklength + + + 54 44 + total + flux + analog + + + 54 44 49 + total + nu-fission + analog + + + 54 44 + total + flux + analog + + + 54 44 49 53 + total + scatter + analog + + + 65 44 + total + flux + tracklength + + + 65 44 + total + total + tracklength + + + 65 44 + total + flux + tracklength + + + 65 44 + total + absorption + tracklength + + + 65 44 + total + flux + analog + + + 65 44 49 + total + nu-fission + analog + + + 65 44 + total + flux + analog + + + 65 44 49 53 + total + scatter + analog + + + diff --git a/tests/regression_tests/mgxs_library_ce_to_mg/test.py b/tests/regression_tests/mgxs_library_ce_to_mg/test.py index 075167f58..b68fbd6c3 100644 --- a/tests/regression_tests/mgxs_library_ce_to_mg/test.py +++ b/tests/regression_tests/mgxs_library_ce_to_mg/test.py @@ -9,7 +9,7 @@ from tests.regression_tests import config class MGXSTestHarness(PyAPITestHarness): - def __init__(self, *args, **kwargs): + def __init__(self, *args, scatter_mgxs_type=None, **kwargs): # Generate inputs using parent class routine super().__init__(*args, **kwargs) @@ -19,8 +19,8 @@ class MGXSTestHarness(PyAPITestHarness): # Initialize MGXS Library for a few cross section types self.mgxs_lib = openmc.mgxs.Library(self._model.geometry) self.mgxs_lib.by_nuclide = False - self.mgxs_lib.mgxs_types = ['total', 'absorption', 'nu-fission matrix', - 'nu-scatter matrix', 'multiplicity matrix'] + self.mgxs_lib.mgxs_types = ['total', 'absorption', 'nu-fission matrix'] + self.mgxs_lib.mgxs_types += scatter_mgxs_type self.mgxs_lib.energy_groups = energy_groups self.mgxs_lib.correction = None self.mgxs_lib.legendre_order = 3 @@ -69,9 +69,23 @@ class MGXSTestHarness(PyAPITestHarness): os.remove(f) -def test_mgxs_library_ce_to_mg(): +def test_mgxs_library_ce_to_mg_multiplicity_matrix(): # Set the input set to use the pincell model model = pwr_pin_cell() - harness = MGXSTestHarness('statepoint.10.h5', model) + harness = MGXSTestHarness( + 'statepoint.10.h5', model, + inputs_true='inputs_true_multiplicity_matrix.dat', + scatter_mgxs_type=['nu-scatter matrix', 'multiplicity matrix'] + ) + harness.main() + + +def test_mgxs_library_ce_to_mg_scatter_matrix(): + # Set the input set to use the pincell model + model = pwr_pin_cell() + + harness = MGXSTestHarness('statepoint.10.h5', model, + inputs_true='inputs_true_scatter_matrix.dat', + scatter_mgxs_type=['scatter matrix']) harness.main() From 219d82726fa63e2ae4fc34ef854fae391a185b04 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Sat, 30 May 2026 08:10:05 +0200 Subject: [PATCH 632/671] Allow tracklength estimator for neutron heating (#3915) Co-authored-by: Jon Shimwell Co-authored-by: Paul Romano --- src/tallies/tally.cpp | 20 ++++++++++++++-- tests/unit_tests/test_tally.py | 44 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index 3fe48c1b0..e4ea56e59 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -644,8 +644,24 @@ void Tally::set_scores(const vector& scores) break; case HEATING: - if (settings::photon_transport) - estimator_ = TallyEstimator::COLLISION; + if (settings::photon_transport) { + // Photon heating requires a collision estimator (analog energy + // balance). However, if the tally only scores neutrons, we can keep the + // tracklength estimator since neutron heating uses kerma coefficients + // that support tracklength scoring. + bool neutron_only = false; + for (auto i_filt : filters_) { + auto pf = + dynamic_cast(model::tally_filters[i_filt].get()); + if (pf && pf->particles().size() == 1 && + pf->particles()[0].is_neutron()) { + neutron_only = true; + break; + } + } + if (!neutron_only) + estimator_ = TallyEstimator::COLLISION; + } break; case SCORE_PULSE_HEIGHT: { diff --git a/tests/unit_tests/test_tally.py b/tests/unit_tests/test_tally.py index 52647a36c..7136a621b 100644 --- a/tests/unit_tests/test_tally.py +++ b/tests/unit_tests/test_tally.py @@ -17,3 +17,47 @@ def test_tally_init_args(): assert tally.filters == [filter] assert tally.nuclides == ['U235'] assert tally.estimator == 'tracklength' + + +def test_heating_estimator_with_photon_transport(run_in_tmpdir): + """Test that a neutron-only heating tally uses the tracklength estimator + even when photon transport is enabled. + + Without a neutron particle filter, photon heating requires the collision + estimator (analog energy balance). But neutron heating has kerma + coefficients that support tracklength scoring, so a neutron-only tally + should keep the tracklength estimator. + """ + mat = openmc.Material() + mat.add_nuclide('Si28', 1.0) + mat.set_density('g/cm3', 2.3) + sph = openmc.Sphere(r=10.0, boundary_type='vacuum') + cell = openmc.Cell(fill=mat, region=-sph) + + model = openmc.Model() + model.geometry = openmc.Geometry([cell]) + model.settings.run_mode = 'fixed source' + model.settings.particles = 100 + model.settings.batches = 1 + model.settings.photon_transport = True + model.settings.source = openmc.IndependentSource( + space=openmc.stats.Point() + ) + + neutron_filter = openmc.ParticleFilter(['neutron']) + + # Neutron-only heating — should get tracklength estimator + t_neutron = openmc.Tally(name='heating_neutron') + t_neutron.filters = [neutron_filter] + t_neutron.scores = ['heating'] + + # No particle filter — should get collision estimator + t_all = openmc.Tally(name='heating_all') + t_all.scores = ['heating'] + + model.tallies = [t_neutron, t_all] + + sp_filename = model.run() + with openmc.StatePoint(sp_filename) as sp: + assert sp.tallies[t_neutron.id].estimator == 'tracklength' + assert sp.tallies[t_all.id].estimator == 'collision' From 111eb770668d2f8e39d7fd4524a08df8bf9819f5 Mon Sep 17 00:00:00 2001 From: EdenRochmanSharabi <91745090+EdenRochmanSharabi@users.noreply.github.com> Date: Sat, 30 May 2026 08:18:09 +0200 Subject: [PATCH 633/671] Implement SphericalMesh.get_indices_at_coords (#3919) Co-authored-by: Paul Romano --- openmc/mesh.py | 118 +++++++++++++++++++++++++++------- tests/unit_tests/test_mesh.py | 91 +++++++++++++++++++++++++- 2 files changed, 184 insertions(+), 25 deletions(-) diff --git a/openmc/mesh.py b/openmc/mesh.py index 61effd850..ff8e14577 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -3,7 +3,7 @@ import warnings from abc import ABC, abstractmethod from collections.abc import Iterable, Sequence, Mapping from functools import wraps -from math import pi, sqrt, atan2 +import math from numbers import Integral, Real from pathlib import Path from typing import Protocol @@ -11,12 +11,10 @@ from typing import Protocol import h5py import lxml.etree as ET import numpy as np -from pathlib import Path import openmc import openmc.checkvalue as cv from openmc.checkvalue import PathLike -from openmc.utility_funcs import change_directory from .bounding_box import BoundingBox from ._xml import get_elem_list, get_text from .mixin import IDManagerMixin @@ -291,7 +289,7 @@ class MeshBase(IDManagerMixin, ABC): """ mesh_type = 'regular' if 'type' not in group.keys() else group['type'][()].decode() mesh_id = int(group.name.split('/')[-1].lstrip('mesh ')) - mesh_name = '' if not 'name' in group else group['name'][()].decode() + mesh_name = '' if 'name' not in group else group['name'][()].decode() if mesh_type == 'regular': return RegularMesh.from_hdf5(group, mesh_id, mesh_name) @@ -1903,7 +1901,7 @@ class CylindricalMesh(StructuredMesh): self, r_grid: Sequence[float], z_grid: Sequence[float], - phi_grid: Sequence[float] = (0, 2*pi), + phi_grid: Sequence[float] = (0, 2*math.pi), origin: Sequence[float] = (0., 0., 0.), mesh_id: int | None = None, name: str = '', @@ -1960,7 +1958,7 @@ class CylindricalMesh(StructuredMesh): cv.check_length('mesh phi_grid', grid, 2) cv.check_increasing('mesh phi_grid', grid) grid = np.asarray(grid, dtype=float) - if np.any((grid < 0.0) | (grid > 2*pi)): + if np.any((grid < 0.0) | (grid > 2*math.pi)): raise ValueError("phi_grid values must be in [0, 2π].") self._phi_grid = grid @@ -2028,9 +2026,9 @@ class CylindricalMesh(StructuredMesh): return string def get_indices_at_coords( - self, - coords: Sequence[float] - ) -> tuple[int, int, int]: + self, + coords: Sequence[float] + ) -> tuple[int, int, int]: """Finds the index of the mesh element at the specified coordinates. .. versionadded:: 0.15.0 @@ -2046,7 +2044,7 @@ class CylindricalMesh(StructuredMesh): The r, phi, z indices """ - r_value_from_origin = sqrt((coords[0]-self.origin[0])**2 + (coords[1]-self.origin[1])**2) + r_value_from_origin = math.hypot(coords[0]-self.origin[0], coords[1]-self.origin[1]) if r_value_from_origin < self.r_grid[0] or r_value_from_origin > self.r_grid[-1]: raise ValueError( @@ -2070,13 +2068,13 @@ class CylindricalMesh(StructuredMesh): delta_x = coords[0] - self.origin[0] delta_y = coords[1] - self.origin[1] # atan2 returns values in -pi to +pi range - phi_value = atan2(delta_y, delta_x) + phi_value = math.atan2(delta_y, delta_x) if delta_x < 0 and delta_y < 0: # returned phi_value anticlockwise and negative - phi_value += 2 * pi + phi_value += 2 * math.pi if delta_x > 0 and delta_y < 0: # returned phi_value anticlockwise and negative - phi_value += 2 * pi + phi_value += 2 * math.pi phi_grid_values = np.array(self.phi_grid) @@ -2111,7 +2109,7 @@ class CylindricalMesh(StructuredMesh): dimension: Sequence[int] = (10, 10, 10), mesh_id: int | None = None, name: str = '', - phi_grid_bounds: Sequence[float] = (0.0, 2*pi), + phi_grid_bounds: Sequence[float] = (0.0, 2*math.pi), enclose_domain: bool = False, ) -> CylindricalMesh: """Create CylindricalMesh from a bounding box. @@ -2339,8 +2337,8 @@ class SphericalMesh(StructuredMesh): def __init__( self, r_grid: Sequence[float], - phi_grid: Sequence[float] = (0, 2*pi), - theta_grid: Sequence[float] = (0, pi), + phi_grid: Sequence[float] = (0, 2*math.pi), + theta_grid: Sequence[float] = (0, math.pi), origin: Sequence[float] = (0., 0., 0.), mesh_id: int | None = None, name: str = '', @@ -2397,7 +2395,7 @@ class SphericalMesh(StructuredMesh): cv.check_length('mesh theta_grid', grid, 2) cv.check_increasing('mesh theta_grid', grid) grid = np.asarray(grid, dtype=float) - if np.any((grid < 0.0) | (grid > pi)): + if np.any((grid < 0.0) | (grid > math.pi)): raise ValueError("theta_grid values must be in [0, π].") self._theta_grid = grid @@ -2411,7 +2409,7 @@ class SphericalMesh(StructuredMesh): cv.check_length('mesh phi_grid', grid, 2) cv.check_increasing('mesh phi_grid', grid) grid = np.asarray(grid, dtype=float) - if np.any((grid < 0.0) | (grid > 2*pi)): + if np.any((grid < 0.0) | (grid > 2*math.pi)): raise ValueError("phi_grid values must be in [0, 2π].") self._phi_grid = grid @@ -2483,8 +2481,8 @@ class SphericalMesh(StructuredMesh): dimension: Sequence[int] = (10, 10, 10), mesh_id: int | None = None, name: str = '', - phi_grid_bounds: Sequence[float] = (0.0, 2*pi), - theta_grid_bounds: Sequence[float] = (0.0, pi), + phi_grid_bounds: Sequence[float] = (0.0, 2*math.pi), + theta_grid_bounds: Sequence[float] = (0.0, math.pi), enclose_domain: bool = False, ) -> SphericalMesh: """Create SphericalMesh from a bounding box. @@ -2645,10 +2643,82 @@ 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 get_indices_at_coords( + self, + coords: Sequence[float] + ) -> tuple[int, int, int]: + """Find the mesh cell indices containing the specified coordinates. + + .. versionadded:: 0.15.4 + + Parameters + ---------- + coords : Sequence[float] + Cartesian coordinates of the point as (x, y, z). + + Returns + ------- + tuple[int, int, int] + The r, theta, phi indices. + + Raises + ------ + ValueError + If the coordinates fall outside the mesh grid boundaries. + + """ + dx = coords[0] - self.origin[0] + dy = coords[1] - self.origin[1] + dz = coords[2] - self.origin[2] + + r_value = math.hypot(dx, dy, dz) + + if r_value < self.r_grid[0] or r_value > self.r_grid[-1]: + raise ValueError( + f'The r value {r_value} computed from the specified ' + f'coordinates is outside the r grid range ' + f'[{self.r_grid[0]}, {self.r_grid[-1]}].' + ) + + r_index = int(min( + np.searchsorted(self.r_grid, r_value, side='right') - 1, + len(self.r_grid) - 2 + )) + + if r_value == 0.0: + theta_value = 0.0 + phi_value = 0.0 + else: + theta_value = math.acos(dz / r_value) + phi_value = math.atan2(dy, dx) + if phi_value < 0: + phi_value += 2 * math.pi + + if theta_value < self.theta_grid[0] or theta_value > self.theta_grid[-1]: + raise ValueError( + f'The theta value {theta_value} computed from the specified ' + f'coordinates is outside the theta grid range ' + f'[{self.theta_grid[0]}, {self.theta_grid[-1]}].' + ) + + theta_index = int(min( + np.searchsorted(self.theta_grid, theta_value, side='right') - 1, + len(self.theta_grid) - 2 + )) + + if phi_value < self.phi_grid[0] or phi_value > self.phi_grid[-1]: + raise ValueError( + f'The phi value {phi_value} computed from the specified ' + f'coordinates is outside the phi grid range ' + f'[{self.phi_grid[0]}, {self.phi_grid[-1]}].' + ) + + phi_index = int(min( + np.searchsorted(self.phi_grid, phi_value, side='right') - 1, + len(self.phi_grid) - 2 + )) + + return (r_index, theta_index, phi_index) def require_statepoint_data(func): diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index 85cb68a83..9b1469fc5 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -1,4 +1,4 @@ -from math import pi +from math import pi, sqrt from tempfile import TemporaryDirectory from pathlib import Path import itertools @@ -1065,3 +1065,92 @@ def test_rectilinear_mesh_get_indices_at_coords(): mesh.get_indices_at_coords([0.5, -0.5, 110.]) with pytest.raises(ValueError): mesh.get_indices_at_coords([0.5, -20., 110.]) + + +def test_SphericalMesh_get_indices_at_coords(): + """Test get_indices_at_coords method for SphericalMesh""" + + # Basic mesh with default phi and theta grids (single angular bin) + mesh = openmc.SphericalMesh(r_grid=(0, 5, 10)) + + assert mesh.get_indices_at_coords([3, 0, 0]) == (0, 0, 0) + assert mesh.get_indices_at_coords([0, 0, 3]) == (0, 0, 0) + assert mesh.get_indices_at_coords([0, 0, -3]) == (0, 0, 0) + assert mesh.get_indices_at_coords([7, 0, 0]) == (1, 0, 0) + assert mesh.get_indices_at_coords([10, 0, 0]) == (1, 0, 0) + + # Out-of-bounds r + with pytest.raises(ValueError): + mesh.get_indices_at_coords([11, 0, 0]) + + mesh2 = openmc.SphericalMesh(r_grid=(2, 5, 10)) + with pytest.raises(ValueError): + mesh2.get_indices_at_coords([1, 0, 0]) + + # Multi-bin angular grids: use points clearly inside bins + mesh3 = openmc.SphericalMesh( + r_grid=(0, 5, 10), + theta_grid=(0, pi/4, pi/2, pi), + phi_grid=(0, pi/2, pi, 3*pi/2, 2*pi) + ) + + # Near z-axis: theta~0 -> bin 0 + assert mesh3.get_indices_at_coords([0.01, 0, 3]) == (0, 0, 0) + + # theta in (0, pi/4) -> bin 0: [1, 0, 2] theta=arccos(2/sqrt(5))~0.46 + assert mesh3.get_indices_at_coords([1, 0, 2]) == (0, 0, 0) + + # theta in (pi/4, pi/2) -> bin 1: [2, 0, 1] theta=arccos(1/sqrt(5))~1.107 + assert mesh3.get_indices_at_coords([2, 0, 1]) == (0, 1, 0) + + # theta in (pi/2, pi) -> bin 2: [1, 0, -2] theta=arccos(-2/sqrt(5))~2.034 + assert mesh3.get_indices_at_coords([1, 0, -2]) == (0, 2, 0) + + # phi in (pi/2, pi) -> bin 1: [-1, 1, 0.5] + assert mesh3.get_indices_at_coords([-1, 1, 0.5]) == (0, 1, 1) + + # phi in (pi, 3*pi/2) -> bin 2: [-1, -1, 0.5] + assert mesh3.get_indices_at_coords([-1, -1, 0.5]) == (0, 1, 2) + + # phi in (3*pi/2, 2*pi) -> bin 3: [1, -1, 0.5] + assert mesh3.get_indices_at_coords([1, -1, 0.5]) == (0, 1, 3) + + # Non-default origin + mesh4 = openmc.SphericalMesh( + r_grid=(0, 5, 10), + origin=(100, 200, 300) + ) + + assert mesh4.get_indices_at_coords([103, 200, 300]) == (0, 0, 0) + assert mesh4.get_indices_at_coords([100, 200, 307]) == (1, 0, 0) + + with pytest.raises(ValueError): + mesh4.get_indices_at_coords([111, 200, 300]) + + # Degenerate case: point at origin with r_grid starting at 0 + mesh5 = openmc.SphericalMesh(r_grid=(0, 5)) + assert mesh5.get_indices_at_coords([0, 0, 0]) == (0, 0, 0) + + # Out-of-bounds theta: restricted theta grid + mesh6 = openmc.SphericalMesh( + r_grid=(0, 10), + theta_grid=(0, pi/4) + ) + with pytest.raises(ValueError): + mesh6.get_indices_at_coords([5, 0, 0]) # theta=pi/2 > pi/4 + + # Out-of-bounds phi: restricted phi grid + mesh7 = openmc.SphericalMesh( + r_grid=(0, 10), + phi_grid=(0, pi/2) + ) + with pytest.raises(ValueError): + mesh7.get_indices_at_coords([-5, 0, 0]) # phi=pi > pi/2 + + # Diagonal point: verify r, theta, phi all computed correctly + r = 6.0 + val = r / sqrt(3) + result = mesh3.get_indices_at_coords([val, val, val]) + assert result[0] == 1 # r=6 in second bin [5, 10] + assert result[1] == 1 # theta=arccos(1/sqrt(3))~0.955, in (pi/4, pi/2) + assert result[2] == 0 # phi=pi/4, in [0, pi/2) From db322f2c5dff390bf906c6aed6bd885924d778d5 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 8 Jun 2026 13:22:32 -0500 Subject: [PATCH 634/671] Add section in depletion user's guide about comparing to other codes (#3955) --- docs/source/usersguide/depletion.rst | 39 ++++++++++++++++++++++++ openmc/model/model.py | 44 ++++++++++++++-------------- tests/unit_tests/test_lib.py | 4 +-- 3 files changed, 63 insertions(+), 24 deletions(-) diff --git a/docs/source/usersguide/depletion.rst b/docs/source/usersguide/depletion.rst index 261900ce6..9a22adf01 100644 --- a/docs/source/usersguide/depletion.rst +++ b/docs/source/usersguide/depletion.rst @@ -449,3 +449,42 @@ to transfer xenon from one material to another, you'd use:: ... integrator.add_transfer_rate(mat1, ['Xe'], 0.1, destination_material=mat2) + +Comparing to Other Codes +======================== + +Comparing depletion results from OpenMC with those from another code, such as +MCNP or Serpent, requires more than constructing equivalent transport models. +At each depletion step, differences in the transport solution, nuclear data, +reaction rate normalization, and numerical integration can all affect the +result. Small differences can also accumulate over successive depletion steps. + +For a meaningful comparison, align as many of the following inputs and methods +as possible: + +- Geometry and material definitions and associated physical properties such as + temperature +- Neutron cross section library (e.g., ENDF/B-VIII.0) +- Treatment of thermal scattering and unresolved resonance probability tables +- Neutron reactions accounted for in the depletion chain +- Decay data in the depletion chain +- Isomeric branching ratios for reactions in the depletion chain +- Fission product yields in the depletion chain +- Fission product yield interpolation method + (``CoupledOperator(fission_yield_mode=...)``) +- Reaction rate normalization, including fission Q values + (``CoupledOperator(fission_q=...)``) +- Depletion integration method (``PredictorIntegrator``, ``CECMIntegrator``, + etc.) and time-step sizes + +When comparing to codes that use ACE format cross sections, it is recommended to +directly convert the ACE files to HDF5 format using functionality from the +:mod:`openmc.data` module (see :ref:`create_xs_library`). Some of the +LANL-distributed ACE libraries used with MCNP have also been converted to HDF5 +format and are available for download at https://openmc.org/data. + +Even after these choices have been aligned, exact agreement should not be +expected. Codes may use different approximations or numerical methods that +cannot be configured identically. When investigating a discrepancy, first +compare transport results and one-group reaction rates at the initial time, then +compare changes over subsequent timesteps. diff --git a/openmc/model/model.py b/openmc/model/model.py index 11c26d4e7..56c0e7a49 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -265,22 +265,22 @@ class Model: denom_tally = openmc.Tally(name='IFP denominator') denom_tally.scores = ['ifp-denominator'] self.tallies.append(denom_tally) - - # TODO: This should also be incorporated into lower-level calls in + + # TODO: This should also be incorporated into lower-level calls in # settings.py, but it requires information about the tallies currently # on the active Model def _assign_fw_cadis_tally_IDs(self): - # Verify that all tallies assigned as targets on WeightWindowGenerators - # exist within model.tallies. If this is the case, convert the .targets + # Verify that all tallies assigned as targets on WeightWindowGenerators + # exist within model.tallies. If this is the case, convert the .targets # attribute of each WeightWindowGenerator to a sequence of tally IDs. if len(self.settings.weight_window_generators) == 0: return - + # List of valid tally IDs reference_tally_ids = np.asarray([tal.id for tal in self.tallies]) - + for wwg in self.settings.weight_window_generators: - # Only proceeds if the "targets" attribute is an openmc.Tallies, + # Only proceeds if the "targets" attribute is an openmc.Tallies, # which means it hasn't been checked against model.tallies. if isinstance(wwg.targets, openmc.Tallies): id_vec = [] @@ -291,7 +291,7 @@ class Model: if tal == reference_tal: id_next = reference_tal.id break - + if id_next == None: raise RuntimeError( f'Local FW-CADIS target tally {tal.id} not found on model.tallies!') @@ -1750,8 +1750,8 @@ class Model: def _auto_generate_mgxs_lib( model: openmc.model.model, groups: openmc.mgxs.EnergyGroups, - correction: str | none, - directory: pathlike, + correction: str | None, + directory: PathLike, ) -> openmc.mgxs.Library: """ Automatically generate a multi-group cross section libray from a model @@ -1958,7 +1958,7 @@ class Model: # Set materials on the model model.materials = [material] - if temperature != None: + if temperature is not None: model.materials[-1].temperature = temperature # Settings @@ -1985,7 +1985,7 @@ class Model: mgxs_lib = Model._auto_generate_mgxs_lib( model, groups, correction, directory) - if temperature != None: + if temperature is not None: return mgxs_lib.get_xsdata(domain=material, xsdata_name=name, temperature=temperature) else: @@ -2058,12 +2058,12 @@ class Model: ) temp_settings = {} - if temperature_settings == None: + if temperature_settings is None: temp_settings = self.settings.temperature else: temp_settings = temperature_settings - if temperatures == None: + if temperatures is None: mgxs_sets = [] for material in self.materials: xs_data = Model._isothermal_infinite_media_mgxs( @@ -2236,7 +2236,7 @@ class Model: model = openmc.Model() model.geometry = stoch_geom - if temperature != None: + if temperature is not None: for material in model.geometry.get_all_materials().values(): material.temperature = temperature @@ -2260,7 +2260,7 @@ class Model: model, groups, correction, directory) # Fetch all of the isothermal results. - if temperature != None: + if temperature is not None: return { mat.name : mgxs_lib.get_xsdata(domain=mat, xsdata_name=mat.name, temperature=temperature) @@ -2346,12 +2346,12 @@ class Model: ) temp_settings = {} - if temperature_settings == None: + if temperature_settings is None: temp_settings = self.settings.temperature else: temp_settings = temperature_settings - if temperatures == None: + if temperatures is None: mgxs_sets = Model._isothermal_stochastic_slab_mgxs( geo, groups, @@ -2444,7 +2444,7 @@ class Model: model = copy.deepcopy(input_model) model.tallies = openmc.Tallies() - if temperature != None: + if temperature is not None: for material in model.geometry.get_all_materials().values(): material.temperature = temperature @@ -2460,7 +2460,7 @@ class Model: model, groups, correction, directory) # Fetch all of the isothermal results. - if temperature != None: + if temperature is not None: return { mat.name : mgxs_lib.get_xsdata(domain=mat, xsdata_name=mat.name, temperature=temperature) @@ -2515,12 +2515,12 @@ class Model: entries in openmc.Settings.temperature_settings. """ temp_settings = {} - if temperature_settings == None: + if temperature_settings is None: temp_settings = self.settings.temperature else: temp_settings = temperature_settings - if temperatures == None: + if temperatures is None: mgxs_sets = Model._isothermal_materialwise_mgxs( self, groups, diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index 51e648dcf..6926bc11b 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -251,9 +251,9 @@ def test_material(lib_init): m.name = "Not hot borated water" assert m.name == "Not hot borated water" - assert m.depletable == False + assert not m.depletable m.depletable = True - assert m.depletable == True + assert m.depletable def test_properties_density(lib_init): From 4f6a25e00aea8fe66e55bd182932193bf83061de Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 8 Jun 2026 16:08:08 -0500 Subject: [PATCH 635/671] Introduce new C API function for slice plots (#3806) --- include/openmc/capi.h | 5 + include/openmc/plot.h | 114 +++++---- openmc/lib/plot.py | 358 ++++++++++++++-------------- openmc/model/model.py | 155 ++++++++++-- src/plot.cpp | 202 +++++++++++++++- tests/unit_tests/test_lib.py | 36 ++- tests/unit_tests/test_slice_data.py | 168 +++++++++++++ 7 files changed, 755 insertions(+), 283 deletions(-) create mode 100644 tests/unit_tests/test_slice_data.py diff --git a/include/openmc/capi.h b/include/openmc/capi.h index 911654d31..6b78145a4 100644 --- a/include/openmc/capi.h +++ b/include/openmc/capi.h @@ -123,8 +123,13 @@ int openmc_new_filter(const char* type, int32_t* index); int openmc_next_batch(int* status); int openmc_nuclide_name(int index, const char** name); int openmc_plot_geometry(); +// Deprecated; use openmc_slice_data. int openmc_id_map(const void* slice, int32_t* data_out); +// Deprecated; use openmc_slice_data. int openmc_property_map(const void* slice, double* data_out); +int openmc_slice_data(const double origin[3], const double u_span[3], + const double v_span[3], const size_t pixels[2], bool show_overlaps, int level, + int32_t filter_index, int32_t* geom_data, double* property_data); int openmc_get_plot_index(int32_t id, int32_t* index); int openmc_plot_get_id(int32_t index, int32_t* id); int openmc_plot_set_id(int32_t index, int32_t id); diff --git a/include/openmc/plot.h b/include/openmc/plot.h index 6c00fc2f6..f97d31384 100644 --- a/include/openmc/plot.h +++ b/include/openmc/plot.h @@ -18,6 +18,8 @@ #include "openmc/position.h" #include "openmc/random_lcg.h" #include "openmc/ray.h" +#include "openmc/tallies/filter.h" +#include "openmc/tallies/filter_match.h" #include "openmc/xml_interface.h" namespace openmc { @@ -148,10 +150,11 @@ public: struct IdData { // Constructor - IdData(size_t h_res, size_t v_res); + IdData(size_t h_res, size_t v_res, bool include_filter = false); // Methods - void set_value(size_t y, size_t x, const GeometryState& p, int level); + void set_value(size_t y, size_t x, const Particle& p, int level, + Filter* filter = nullptr, FilterMatch* match = nullptr); void set_overlap(size_t y, size_t x); // Members @@ -160,16 +163,34 @@ struct IdData { struct PropertyData { // Constructor - PropertyData(size_t h_res, size_t v_res); + PropertyData(size_t h_res, size_t v_res, bool include_filter = false); // Methods - void set_value(size_t y, size_t x, const GeometryState& p, int level); + void set_value(size_t y, size_t x, const Particle& p, int level, + Filter* filter = nullptr, FilterMatch* match = nullptr); void set_overlap(size_t y, size_t x); // Members tensor::Tensor data_; //!< 2D array of temperature & density data }; +struct RasterData { + // Constructor + RasterData(size_t h_res, size_t v_res, bool include_filter = false); + + // Methods + void set_value(size_t y, size_t x, const Particle& p, int level, + Filter* filter = nullptr, FilterMatch* match = nullptr); + void set_overlap(size_t y, size_t x); + + // Members + tensor::Tensor + id_data_; //!< [v_res, h_res, 3 or 4]: cell, instance, mat, [filter_bin] + tensor::Tensor + property_data_; //!< [v_res, h_res, 2]: temperature, density + bool include_filter_; //!< Whether filter bin index is included +}; + //=============================================================================== // Plot class //=============================================================================== @@ -177,7 +198,7 @@ struct PropertyData { class SlicePlotBase { public: template - T get_map() const; + T get_map(int32_t filter_index = -1) const; enum class PlotBasis { xy = 1, xz = 2, yz = 3 }; @@ -188,70 +209,65 @@ public: // Members public: - Position origin_; //!< Plot origin in geometry - Position width_; //!< Plot width in geometry - PlotBasis basis_; //!< Plot basis (XY/XZ/YZ) - array pixels_; //!< Plot size in pixels - bool slice_color_overlaps_; //!< Show overlapping cells? - int slice_level_ {-1}; //!< Plot universe level + Position origin_; //!< Plot origin in geometry + Direction u_span_; //!< Full-width span vector in geometry + Direction v_span_; //!< Full-height span vector in geometry + array pixels_; //!< Plot size in pixels + bool show_overlaps_; //!< Show overlapping cells? + int slice_level_ {-1}; //!< Plot universe level private: }; template -T SlicePlotBase::get_map() const +T SlicePlotBase::get_map(int32_t filter_index) const { size_t width = pixels_[0]; size_t height = pixels_[1]; - // get pixel size - double in_pixel = (width_[0]) / static_cast(width); - double out_pixel = (width_[1]) / static_cast(height); - - // size data array - T data(width, height); - - // setup basis indices and initial position centered on pixel - int in_i, out_i; - Position xyz = origin_; - switch (basis_) { - case PlotBasis::xy: - in_i = 0; - out_i = 1; - break; - case PlotBasis::xz: - in_i = 0; - out_i = 2; - break; - case PlotBasis::yz: - in_i = 1; - out_i = 2; - break; - default: - UNREACHABLE(); + // Determine if filter is being used + bool include_filter = (filter_index >= 0); + Filter* filter = nullptr; + if (include_filter) { + filter = model::tally_filters[filter_index].get(); } - // set initial position - xyz[in_i] = origin_[in_i] - width_[0] / 2. + in_pixel / 2.; - xyz[out_i] = origin_[out_i] + width_[1] / 2. - out_pixel / 2.; + // size data array + T data(width, height, include_filter); - // arbitrary direction - Direction dir = {1. / std::sqrt(2.), 1. / std::sqrt(2.), 0.0}; + // compute pixel steps and top-left pixel center + Direction u_step = u_span_ / static_cast(width); + Direction v_step = v_span_ / static_cast(height); + + Position start = + origin_ - 0.5 * u_span_ + 0.5 * v_span_ + 0.5 * u_step - 0.5 * v_step; + + // Validate that span vectors define a valid plane + Position cross = u_span_.cross(v_span_); + if (cross.norm() == 0.0) { + fatal_error("Slice span vectors are invalid (zero area)."); + } + + // Use an arbitrary direction that is not aligned with any coordinate axis. + // The direction has no physical meaning for plotting but is used by + // Surface::sense() to break ties when a pixel is coincident with a surface. + Direction dir = {1.0 / std::sqrt(2.0), 1.0 / std::sqrt(2.0), 0.0}; #pragma omp parallel { - GeometryState p; - p.r() = xyz; + Particle p; + p.r() = start; p.u() = dir; p.coord(0).universe() = model::root_universe; int level = slice_level_; int j {}; + FilterMatch match; #pragma omp for for (int y = 0; y < height; y++) { - p.r()[out_i] = xyz[out_i] - out_pixel * y; + Position row = start - v_step * static_cast(y); for (int x = 0; x < width; x++) { - p.r()[in_i] = xyz[in_i] + in_pixel * x; + p.r() = row + u_step * static_cast(x); p.n_coord() = 1; // local variables bool found_cell = exhaustive_find_cell(p); @@ -260,9 +276,9 @@ T SlicePlotBase::get_map() const j = level; } if (found_cell) { - data.set_value(y, x, p, j); + data.set_value(y, x, p, j, filter, &match); } - if (slice_color_overlaps_ && check_cell_overlap(p, false)) { + if (show_overlaps_ && check_cell_overlap(p, false)) { data.set_overlap(y, x); } } // inner for @@ -297,6 +313,8 @@ public: void print_info() const override; PlotType type_; //!< Plot type (Slice/Voxel) + Position width_; //!< Axis-aligned width from plot.xml + PlotBasis basis_; //!< Basis from plot.xml for slice plots int meshlines_width_; //!< Width of lines added to the plot int index_meshlines_mesh_ {-1}; //!< Index of the mesh to draw on the plot RGBColor meshlines_color_; //!< Color of meshlines on the plot diff --git a/openmc/lib/plot.py b/openmc/lib/plot.py index 90af80d5b..44d6ac273 100644 --- a/openmc/lib/plot.py +++ b/openmc/lib/plot.py @@ -9,6 +9,7 @@ from .core import _FortranObjectWithID from .error import _error_handler import numpy as np +import warnings class _Position(Structure): @@ -51,218 +52,209 @@ class _Position(Structure): return f"({self.x}, {self.y}, {self.z})" -class _PlotBase(Structure): - """A structure defining a 2-D geometry slice with underlying c-types +def _extract_slice_data_args(plot): + """Convert a legacy plot-like object into slice_data keyword arguments.""" + try: + kwargs = { + 'origin': tuple(plot.origin), + 'width': (plot.width, plot.height), + 'basis': plot.basis, + 'pixels': (plot.h_res, plot.v_res), + 'show_overlaps': getattr(plot, 'color_overlaps', False), + 'level': getattr(plot, 'level', -1), + } + except AttributeError as exc: + raise TypeError( + "plot must be a legacy plot-like object with origin, width, " + "height, basis, h_res, and v_res attributes." + ) from exc + return kwargs - C-Type Attributes - ----------------- - origin_ : openmc.lib.plot._Position - A position defining the origin of the plot. - width_ : openmc.lib.plot._Position - The width of the plot along the x, y, and z axes, respectively - basis_ : c_int - The axes basis of the plot view. - pixels_ : c_size_t[3] - The resolution of the plot in the horizontal and vertical dimensions - color_overlaps_ : c_bool - Whether to assign unique IDs (-3) to overlapping regions. - level_ : c_int - The universe level for the plot view - Attributes +_dll.openmc_slice_data.argtypes = [ + POINTER(c_double * 3), # origin + POINTER(c_double * 3), # u_span + POINTER(c_double * 3), # v_span + POINTER(c_size_t * 2), # pixels + c_bool, # show_overlaps + c_int, # level + c_int32, # filter_index + POINTER(c_int32), # geom_data + POINTER(c_double), # property_data (can be None) +] +_dll.openmc_slice_data.restype = c_int +_dll.openmc_slice_data.errcheck = _error_handler + + +def slice_data(origin, width=None, basis='xy', u_span=None, v_span=None, + pixels=None, show_overlaps=False, level=-1, filter=None, + include_properties=True): + """Generate a 2D raster of geometry and property data for plotting. + + Parameters ---------- - origin : tuple or list of ndarray - Origin (center) of the plot - width : float - The horizontal dimension of the plot in geometry units (cm) - height : float - The vertical dimension of the plot in geometry units (cm) - basis : string - One of {'xy', 'xz', 'yz'} indicating the horizontal and vertical - axes of the plot. - h_res : int - The horizontal resolution of the plot in pixels - v_res : int - The vertical resolution of the plot in pixels - level : int - The universe level for the plot (default: -1 -> all universes shown) + origin : sequence of float + Center position of the plot [x, y, z] + width : sequence of float + Width of the plot [horizontal, vertical]. Mutually exclusive with + u_span/v_span. + basis : {'xy', 'xz', 'yz'} or int + Plot basis. Ignored if u_span/v_span are provided. + u_span : sequence of float, optional + Full-width span vector for the horizontal axis (3 values). Mutually + exclusive with width. + v_span : sequence of float, optional + Full-height span vector for the vertical axis (3 values). Mutually + exclusive with width. + pixels : sequence of int + Number of pixels [horizontal, vertical] + show_overlaps : bool, optional + Whether to detect overlapping cells + level : int, optional + Universe level (-1 for deepest) + filter : openmc.lib.Filter, optional + Filter for bin index lookup + include_properties : bool, optional + Whether to compute temperature/density + + Returns + ------- + geom_data : numpy.ndarray + Array of shape (v_res, h_res, 3) or (v_res, h_res, 4) with int32 dtype. + Contains [cell_id, cell_instance, material_id] when no filter is provided, + or [cell_id, cell_instance, material_id, filter_bin] when a filter is provided. + property_data : numpy.ndarray or None + Array of shape (v_res, h_res, 2) with float64 dtype containing + [temperature, density], or None if include_properties=False """ - _fields_ = [('origin_', _Position), - ('width_', _Position), - ('basis_', c_int), - ('pixels_', 3*c_size_t), - ('color_overlaps_', c_bool), - ('level_', c_int)] + if pixels is None: + raise ValueError("pixels must be specified.") + if len(pixels) != 2: + raise ValueError("pixels must be a length-2 sequence.") - def __init__(self): - self.level_ = -1 - self.basis_ = 1 - self.color_overlaps_ = False + if width is not None and (u_span is not None or v_span is not None): + raise ValueError("width is mutually exclusive with u_span/v_span.") - @property - def origin(self): - return self.origin_ - - @origin.setter - def origin(self, origin): - self.origin_.x = origin[0] - self.origin_.y = origin[1] - self.origin_.z = origin[2] - - @property - def width(self): - return self.width_.x - - @width.setter - def width(self, width): - self.width_.x = width - - @property - def height(self): - return self.width_.y - - @height.setter - def height(self, height): - self.width_.y = height - - @property - def basis(self): - if self.basis_ == 1: - return 'xy' - elif self.basis_ == 2: - return 'xz' - elif self.basis_ == 3: - return 'yz' - - raise ValueError(f"Plot basis {self.basis_} is invalid") - - @basis.setter - def basis(self, basis): + if u_span is not None or v_span is not None: + if u_span is None or v_span is None: + raise ValueError("Both u_span and v_span must be provided.") + u_span = np.asarray(u_span, dtype=float) + v_span = np.asarray(v_span, dtype=float) + if u_span.shape != (3,) or v_span.shape != (3,): + raise ValueError("u_span and v_span must be length-3 sequences.") + u_norm = np.linalg.norm(u_span) + v_norm = np.linalg.norm(v_span) + if u_norm == 0.0 or v_norm == 0.0: + raise ValueError("u_span and v_span must be non-zero vectors.") + dot = float(np.dot(u_span, v_span)) + ortho_tol = 1.0e-10 * u_norm * v_norm + if abs(dot) > ortho_tol: + raise ValueError("u_span and v_span must be orthogonal.") + else: + if width is None: + raise ValueError("width must be provided when u_span/v_span are not set.") + if len(width) != 2: + raise ValueError("width must be a length-2 sequence.") + basis_map = {'xy': 1, 'xz': 2, 'yz': 3} if isinstance(basis, str): - valid_bases = ('xy', 'xz', 'yz') basis = basis.lower() - if basis not in valid_bases: + if basis not in basis_map: raise ValueError(f"{basis} is not a valid plot basis.") - - if basis == 'xy': - self.basis_ = 1 - elif basis == 'xz': - self.basis_ = 2 - elif basis == 'yz': - self.basis_ = 3 - return - - if isinstance(basis, int): - valid_bases = (1, 2, 3) - if basis not in valid_bases: + basis = basis_map[basis] + elif isinstance(basis, int): + if basis not in basis_map.values(): raise ValueError(f"{basis} is not a valid plot basis.") - self.basis_ = basis - return + else: + raise ValueError(f"{basis} is not a valid plot basis.") - raise ValueError(f"{basis} of type {type(basis)} is an invalid plot basis") + if basis == 1: + u_span = np.array([width[0], 0.0, 0.0], dtype=float) + v_span = np.array([0.0, width[1], 0.0], dtype=float) + elif basis == 2: + u_span = np.array([width[0], 0.0, 0.0], dtype=float) + v_span = np.array([0.0, 0.0, width[1]], dtype=float) + else: + u_span = np.array([0.0, width[0], 0.0], dtype=float) + v_span = np.array([0.0, 0.0, width[1]], dtype=float) - @property - def h_res(self): - return self.pixels_[0] + origin = np.asarray(origin, dtype=float) + if origin.shape != (3,): + raise ValueError("origin must be a length-3 sequence.") - @h_res.setter - def h_res(self, h_res): - self.pixels_[0] = h_res + # Prepare ctypes arrays + origin_arr = (c_double * 3)(*origin) + u_span_arr = (c_double * 3)(*u_span) + v_span_arr = (c_double * 3)(*v_span) + pixels_arr = (c_size_t * 2)(*pixels) - @property - def v_res(self): - return self.pixels_[1] + # Get internal filter index from filter ID if filter is provided + if filter is not None: + filter_index = c_int32() + _dll.openmc_get_filter_index(filter.id, filter_index) + filter_index = filter_index.value + else: + filter_index = -1 - @v_res.setter - def v_res(self, v_res): - self.pixels_[1] = v_res + # Allocate output arrays with dynamic size based on filter + n_geom_fields = 4 if filter is not None else 3 + geom_data = np.zeros((pixels[1], pixels[0], n_geom_fields), dtype=np.int32) + if include_properties: + property_data = np.zeros((pixels[1], pixels[0], 2), dtype=np.float64) + prop_ptr = property_data.ctypes.data_as(POINTER(c_double)) + else: + property_data = None + prop_ptr = None - @property - def level(self): - return int(self.level_) + _dll.openmc_slice_data( + origin_arr, + u_span_arr, + v_span_arr, + pixels_arr, + show_overlaps, + level, + filter_index, + geom_data.ctypes.data_as(POINTER(c_int32)), + prop_ptr + ) - @level.setter - def level(self, level): - self.level_ = level - - @property - def color_overlaps(self): - return self.color_overlaps_ - - @color_overlaps.setter - def color_overlaps(self, color_overlaps): - self.color_overlaps_ = color_overlaps - - def __repr__(self): - out_str = ["-----", - "Plot:", - "-----", - f"Origin: {self.origin}", - f"Width: {self.width}", - f"Height: {self.height}", - f"Basis: {self.basis}", - f"HRes: {self.h_res}", - f"VRes: {self.v_res}", - f"Color Overlaps: {self.color_overlaps}", - f"Level: {self.level}"] - return '\n'.join(out_str) - - -_dll.openmc_id_map.argtypes = [POINTER(_PlotBase), POINTER(c_int32)] -_dll.openmc_id_map.restype = c_int -_dll.openmc_id_map.errcheck = _error_handler + return geom_data, property_data def id_map(plot): + """Deprecated compatibility wrapper for geometry ID maps. + + This function is kept for compatibility and will be removed in a future + release. Use `slice_data(..., include_properties=False)` instead. """ - Generate a 2-D map of cell and material IDs. Used for in-memory image - generation. + warnings.warn( + "openmc.lib.id_map is deprecated and will be removed in a future " + "release; use openmc.lib.slice_data(..., include_properties=False).", + FutureWarning, + ) - Parameters - ---------- - plot : openmc.lib.plot._PlotBase - Object describing the slice of the model to be generated - - Returns - ------- - id_map : numpy.ndarray - A NumPy array with shape (vertical pixels, horizontal pixels, 3) of - OpenMC property ids with dtype int32. The last dimension of the array - contains, in order, cell IDs, cell instances, and material IDs. - - """ - img_data = np.zeros((plot.v_res, plot.h_res, 3), - dtype=np.dtype('int32')) - _dll.openmc_id_map(plot, img_data.ctypes.data_as(POINTER(c_int32))) - return img_data - - -_dll.openmc_property_map.argtypes = [POINTER(_PlotBase), POINTER(c_double)] -_dll.openmc_property_map.restype = c_int -_dll.openmc_property_map.errcheck = _error_handler + kwargs = _extract_slice_data_args(plot) + geom_data, _ = slice_data(include_properties=False, **kwargs) + return geom_data[:, :, :3] def property_map(plot): + """Deprecated compatibility wrapper for temperature/density maps. + + This function is kept for compatibility and will be removed in a future + release. Use `slice_data(..., include_properties=True)` instead. """ - Generate a 2-D map of cell temperatures and material densities. Used for - in-memory image generation. + warnings.warn( + "openmc.lib.property_map is deprecated and will be removed in a " + "future release; use openmc.lib.slice_data(..., " + "include_properties=True).", + FutureWarning, + ) - Parameters - ---------- - plot : openmc.lib.plot._PlotBase - Object describing the slice of the model to be generated - - Returns - ------- - property_map : numpy.ndarray - A NumPy array with shape (vertical pixels, horizontal pixels, 2) of - OpenMC property ids with dtype float - - """ - prop_data = np.zeros((plot.v_res, plot.h_res, 2)) - _dll.openmc_property_map(plot, prop_data.ctypes.data_as(POINTER(c_double))) + kwargs = _extract_slice_data_args(plot) + _, prop_data = slice_data(include_properties=True, **kwargs) return prop_data + _dll.openmc_get_plot_index.argtypes = [c_int32, POINTER(c_int32)] _dll.openmc_get_plot_index.restype = c_int _dll.openmc_get_plot_index.errcheck = _error_handler diff --git a/openmc/model/model.py b/openmc/model/model.py index 56c0e7a49..d927b65ae 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -292,7 +292,7 @@ class Model: id_next = reference_tal.id break - if id_next == None: + if id_next is None: raise RuntimeError( f'Local FW-CADIS target tally {tal.id} not found on model.tallies!') else: @@ -1127,28 +1127,148 @@ class Model: array contains cell IDs, cell instances, and material IDs (in that order). """ + ids, _ = self.slice_data( + origin=origin, + width=width, + pixels=pixels, + basis=basis, + show_overlaps=color_overlaps, + level=-1, + include_properties=False, + **init_kwargs, + ) + return ids + + def slice_data( + self, + origin: Sequence[float] | None = None, + width: Sequence[float] | None = None, + pixels: int | Sequence[int] = 40000, + basis: str = 'xy', + u_span: Sequence[float] | None = None, + v_span: Sequence[float] | None = None, + show_overlaps: bool = False, + level: int = -1, + filter: openmc.Filter | None = None, + include_properties: bool = True, + **init_kwargs + ) -> tuple[np.ndarray, np.ndarray | None]: + """Generate geometry and property data for a 2D plot slice. + + This method combines the functionality of :meth:`id_map` and property + mapping into a single call, avoiding duplicate geometry lookups. It also + supports filter bin index lookup for tally visualization. + + .. versionadded:: 0.16.0 + + Parameters + ---------- + origin : Sequence[float], optional + Origin of the plot. If unspecified, this argument defaults to the + center of the bounding box if the bounding box does not contain inf + values for the provided basis, otherwise (0.0, 0.0, 0.0). + width : Sequence[float], optional + Width of the plot. If unspecified, this argument defaults to the + width of the bounding box if the bounding box does not contain inf + values for the provided basis, otherwise (10.0, 10.0). + pixels : int | Sequence[int], optional + If an iterable of ints is provided then this directly sets the + number of pixels to use in each basis direction. If a single int is + provided then this sets the total number of pixels in the plot and + the number of pixels in each basis direction is calculated from this + total and the image aspect ratio based on the width argument. + basis : {'xy', 'yz', 'xz'}, optional + Basis of the plot. + u_span : Sequence[float], optional + Full-width span vector for an oriented slice (3 values). Mutually + exclusive with width. + v_span : Sequence[float], optional + Full-height span vector for an oriented slice (3 values). Mutually + exclusive with width. + show_overlaps : bool, optional + Whether to identify and assign unique IDs (-3) to overlapping + regions. If False, overlapping regions will be assigned the ID of + the lowest-numbered cell that occupies that region. Defaults to + False. + level : int, optional + Universe level to plot (-1 for deepest). Defaults to -1. + filter : openmc.Filter, optional + If provided, the information for each pixel also includes an index + in the filter corresponding to the pixel position. + include_properties : bool, optional + Whether to include temperature/density data. Defaults to True. + **init_kwargs + Keyword arguments passed to :meth:`Model.init_lib`. + + Returns + ------- + geom_data : numpy.ndarray + Shape (v_res, h_res, 3) or (v_res, h_res, 4) int32 array. Contains + [cell_id, cell_instance, material_id] when no filter, or [cell_id, + cell_instance, material_id, filter_bin] with filter. + property_data : numpy.ndarray or None + Shape (v_res, h_res, 2) float64 array with [temperature, density], + or None if include_properties=False. + """ import openmc.lib - origin, width, pixels = self._set_plot_defaults( - origin, width, pixels, basis) + if width is not None and (u_span is not None or v_span is not None): + raise ValueError("width is mutually exclusive with u_span/v_span.") - # initialize the openmc.lib.plot._PlotBase object - plot_obj = openmc.lib.plot._PlotBase() - plot_obj.origin = origin - plot_obj.width = width[0] - plot_obj.height = width[1] - plot_obj.h_res = pixels[0] - plot_obj.v_res = pixels[1] - plot_obj.basis = basis - plot_obj.color_overlaps = color_overlaps + if u_span is not None or v_span is not None: + if u_span is None or v_span is None: + raise ValueError("Both u_span and v_span must be provided.") + if origin is None: + origin = (0.0, 0.0, 0.0) + if isinstance(pixels, int): + u_norm = np.linalg.norm(u_span) + v_norm = np.linalg.norm(v_span) + aspect_ratio = u_norm / v_norm + pixels_y = math.sqrt(pixels / aspect_ratio) + pixels = (int(pixels / pixels_y), int(pixels_y)) + else: + origin, width, pixels = self._set_plot_defaults( + origin, width, pixels, basis) # Silence output by default. Also set arguments to start in volume # calculation mode to avoid loading cross sections init_kwargs.setdefault('output', False) init_kwargs.setdefault('args', ['-c']) + # If filter does not already appear in the model, temporarily add a + # tally with the filter + original_length = len(self.tallies) + if filter is not None: + filter_ids = {f.id for t in self.tallies for f in t.filters} + if filter.id not in filter_ids: + # Create temporary tally while preserving ID assignment + next_id = openmc.Tally.next_id + temp_tally = openmc.Tally() + temp_tally.filters = [filter] + temp_tally.scores = ['flux'] + self.tallies.append(temp_tally) + openmc.Tally.used_ids.remove(temp_tally.id) + openmc.Tally.next_id = next_id + with openmc.lib.TemporarySession(self, **init_kwargs): - return openmc.lib.id_map(plot_obj) + geom_data, property_data = openmc.lib.slice_data( + origin=origin, + width=width, + basis=basis, + u_span=u_span, + v_span=v_span, + pixels=pixels, + show_overlaps=show_overlaps, + level=level, + filter=filter, + include_properties=include_properties, + ) + + # If filter was temporarily added, remove it + if len(self.tallies) > original_length: + self.tallies.pop() + + return geom_data, property_data @add_plot_params def plot( @@ -1216,13 +1336,14 @@ class Model: "openmc.config before plotting.") break - # Get ID map from the C API - id_map = self.id_map( + # Get plot IDs from the C API + id_map, _ = self.slice_data( origin=origin, width=width, pixels=pixels, basis=basis, - color_overlaps=show_overlaps + show_overlaps=show_overlaps, + include_properties=False, ) # Generate colors if not provided @@ -1748,7 +1869,7 @@ class Model: @staticmethod def _auto_generate_mgxs_lib( - model: openmc.model.model, + model: openmc.model.Model, groups: openmc.mgxs.EnergyGroups, correction: str | None, directory: PathLike, diff --git a/src/plot.cpp b/src/plot.cpp index 707d53dc2..b9a1136af 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -33,6 +33,7 @@ #include "openmc/settings.h" #include "openmc/simulation.h" #include "openmc/string_utils.h" +#include "openmc/tallies/filter.h" namespace openmc { @@ -44,10 +45,12 @@ constexpr int PLOT_LEVEL_LOWEST {-1}; //!< lower bound on plot universe level constexpr int32_t NOT_FOUND {-2}; constexpr int32_t OVERLAP {-3}; -IdData::IdData(size_t h_res, size_t v_res) : data_({v_res, h_res, 3}, NOT_FOUND) +IdData::IdData(size_t h_res, size_t v_res, bool /*include_filter*/) + : data_({v_res, h_res, 3}, NOT_FOUND) {} -void IdData::set_value(size_t y, size_t x, const GeometryState& p, int level) +void IdData::set_value(size_t y, size_t x, const Particle& p, int level, + Filter* /*filter*/, FilterMatch* /*match*/) { // set cell data if (p.n_coord() <= level) { @@ -64,7 +67,6 @@ void IdData::set_value(size_t y, size_t x, const GeometryState& p, int level) Cell* c = model::cells.at(p.lowest_coord().cell()).get(); if (p.material() == MATERIAL_VOID) { data_(y, x, 2) = MATERIAL_VOID; - return; } else if (c->type_ == Fill::MATERIAL) { Material* m = model::materials.at(p.material()).get(); data_(y, x, 2) = m->id_; @@ -77,12 +79,12 @@ void IdData::set_overlap(size_t y, size_t x) data_(y, x, k) = OVERLAP; } -PropertyData::PropertyData(size_t h_res, size_t v_res) +PropertyData::PropertyData(size_t h_res, size_t v_res, bool /*include_filter*/) : data_({v_res, h_res, 2}, NOT_FOUND) {} -void PropertyData::set_value( - size_t y, size_t x, const GeometryState& p, int level) +void PropertyData::set_value(size_t y, size_t x, const Particle& p, int level, + Filter* /*filter*/, FilterMatch* /*match*/) { Cell* c = model::cells.at(p.lowest_coord().cell()).get(); data_(y, x, 0) = (p.sqrtkT() * p.sqrtkT()) / K_BOLTZMANN; @@ -97,6 +99,74 @@ void PropertyData::set_overlap(size_t y, size_t x) data_(y, x) = OVERLAP; } +//============================================================================== +// RasterData implementation +//============================================================================== + +RasterData::RasterData(size_t h_res, size_t v_res, bool include_filter) + : id_data_({v_res, h_res, include_filter ? 4u : 3u}, NOT_FOUND), + property_data_({v_res, h_res, 2}, static_cast(NOT_FOUND)), + include_filter_(include_filter) +{} + +void RasterData::set_value(size_t y, size_t x, const Particle& p, int level, + Filter* filter, FilterMatch* match) +{ + // set cell data + if (p.n_coord() <= level) { + id_data_(y, x, 0) = NOT_FOUND; + id_data_(y, x, 1) = NOT_FOUND; + } else { + id_data_(y, x, 0) = model::cells.at(p.coord(level).cell())->id_; + id_data_(y, x, 1) = level == p.n_coord() - 1 + ? p.cell_instance() + : cell_instance_at_level(p, level); + } + + // set material data + Cell* c = model::cells.at(p.lowest_coord().cell()).get(); + if (p.material() == MATERIAL_VOID) { + id_data_(y, x, 2) = MATERIAL_VOID; + } else if (c->type_ == Fill::MATERIAL) { + Material* m = model::materials.at(p.material()).get(); + id_data_(y, x, 2) = m->id_; + } + + // set filter index (only if filter is being used) + if (include_filter_ && filter) { + filter->get_all_bins(p, TallyEstimator::COLLISION, *match); + if (match->bins_.empty()) { + id_data_(y, x, 3) = -1; + } else { + id_data_(y, x, 3) = match->bins_[0]; + } + match->bins_.clear(); + match->weights_.clear(); + } + + // set temperature (in K) + property_data_(y, x, 0) = (p.sqrtkT() * p.sqrtkT()) / K_BOLTZMANN; + + // set density (g/cm³) + if (c->type_ != Fill::UNIVERSE && p.material() != MATERIAL_VOID) { + Material* m = model::materials.at(p.material()).get(); + property_data_(y, x, 1) = m->density_gpcc_; + } +} + +void RasterData::set_overlap(size_t y, size_t x) +{ + // Set cell, instance, and material to OVERLAP, but preserve filter bin + id_data_(y, x, 0) = OVERLAP; + id_data_(y, x, 1) = OVERLAP; + id_data_(y, x, 2) = OVERLAP; + // Note: id_data_(y, x, 3) is NOT overwritten - preserves filter bin for tally + // plotting + + property_data_(y, x, 0) = OVERLAP; + property_data_(y, x, 1) = OVERLAP; +} + //============================================================================== // Global variables //============================================================================== @@ -450,6 +520,22 @@ void Plot::set_width(pugi::xml_node plot_node) if (pl_width.size() == 2) { width_.x = pl_width[0]; width_.y = pl_width[1]; + switch (basis_) { + case PlotBasis::xy: + u_span_ = {width_.x, 0.0, 0.0}; + v_span_ = {0.0, width_.y, 0.0}; + break; + case PlotBasis::xz: + u_span_ = {width_.x, 0.0, 0.0}; + v_span_ = {0.0, 0.0, width_.y}; + break; + case PlotBasis::yz: + u_span_ = {0.0, width_.x, 0.0}; + v_span_ = {0.0, 0.0, width_.y}; + break; + default: + UNREACHABLE(); + } } else { fatal_error( fmt::format(" must be length 2 in slice plot {}", id())); @@ -765,7 +851,7 @@ Plot::Plot(pugi::xml_node plot_node, PlotType type) set_width(plot_node); set_meshlines(plot_node); slice_level_ = level_; // Copy level employed in SlicePlotBase::get_map - slice_color_overlaps_ = color_overlaps_; + show_overlaps_ = color_overlaps_; } //============================================================================== @@ -862,23 +948,39 @@ void Plot::draw_mesh_lines(ImageData& data) const rgb = meshlines_color_; int ax1, ax2; + Position expected_u {}; + Position expected_v {}; switch (basis_) { case PlotBasis::xy: ax1 = 0; ax2 = 1; + expected_u = {width_[0], 0.0, 0.0}; + expected_v = {0.0, width_[1], 0.0}; break; case PlotBasis::xz: ax1 = 0; ax2 = 2; + expected_u = {width_[0], 0.0, 0.0}; + expected_v = {0.0, 0.0, width_[1]}; break; case PlotBasis::yz: ax1 = 1; ax2 = 2; + expected_u = {0.0, width_[0], 0.0}; + expected_v = {0.0, 0.0, width_[1]}; break; default: UNREACHABLE(); } + // Meshlines rely on axis-aligned indexing in global coordinates. + constexpr double rel_tol {1e-12}; + double span_tol = rel_tol * (1.0 + u_span_.norm() + v_span_.norm()); + if ((u_span_ - expected_u).norm() > span_tol || + (v_span_ - expected_v).norm() > span_tol) { + fatal_error("Meshlines are only supported for axis-aligned slice plots."); + } + Position ll_plot {origin_}; Position ur_plot {origin_}; @@ -1008,11 +1110,11 @@ void Plot::create_voxel() const voxel_init(file_id, &(dims[0]), &dspace, &dset, &memspace); SlicePlotBase pltbase; - pltbase.width_ = width_; pltbase.origin_ = origin_; - pltbase.basis_ = PlotBasis::xy; + pltbase.u_span_ = {width_.x, 0.0, 0.0}; + pltbase.v_span_ = {0.0, width_.y, 0.0}; pltbase.pixels() = pixels(); - pltbase.slice_color_overlaps_ = color_overlaps_; + pltbase.show_overlaps_ = color_overlaps_; ProgressBar pb; for (int z = 0; z < pixels()[2]; z++) { @@ -1794,6 +1896,12 @@ void PhongRay::on_intersection() extern "C" int openmc_id_map(const void* plot, int32_t* data_out) { + static bool warned {false}; + if (!warned) { + warning("openmc_id_map is deprecated and will be removed in a future " + "release. Use openmc_slice_data."); + warned = true; + } auto plt = reinterpret_cast(plot); if (!plt) { @@ -1801,7 +1909,7 @@ extern "C" int openmc_id_map(const void* plot, int32_t* data_out) return OPENMC_E_INVALID_ARGUMENT; } - if (plt->slice_color_overlaps_ && model::overlap_check_count.size() == 0) { + if (plt->show_overlaps_ && model::overlap_check_count.size() == 0) { model::overlap_check_count.resize(model::cells.size()); } @@ -1815,14 +1923,20 @@ extern "C" int openmc_id_map(const void* plot, int32_t* data_out) extern "C" int openmc_property_map(const void* plot, double* data_out) { + static bool warned {false}; + if (!warned) { + warning("openmc_property_map is deprecated and will be removed in a future " + "release. Use openmc_slice_data."); + warned = true; + } auto plt = reinterpret_cast(plot); if (!plt) { - set_errmsg("Invalid slice pointer passed to openmc_id_map"); + set_errmsg("Invalid slice pointer passed to openmc_property_map"); return OPENMC_E_INVALID_ARGUMENT; } - if (plt->slice_color_overlaps_ && model::overlap_check_count.size() == 0) { + if (plt->show_overlaps_ && model::overlap_check_count.size() == 0) { model::overlap_check_count.resize(model::cells.size()); } @@ -1834,6 +1948,68 @@ extern "C" int openmc_property_map(const void* plot, double* data_out) return 0; } +extern "C" int openmc_slice_data(const double origin[3], const double u_span[3], + const double v_span[3], const size_t pixels[2], bool color_overlaps, + int level, int32_t filter_index, int32_t* geom_data, double* property_data) +{ + // Validate span vectors + Direction u_span_pos {u_span[0], u_span[1], u_span[2]}; + Direction v_span_pos {v_span[0], v_span[1], v_span[2]}; + double u_norm = u_span_pos.norm(); + double v_norm = v_span_pos.norm(); + if (u_norm == 0.0 || v_norm == 0.0) { + set_errmsg("Slice span vectors must be non-zero."); + return OPENMC_E_INVALID_ARGUMENT; + } + + constexpr double ORTHO_REL_TOL = 1e-10; + double dot = u_span_pos.dot(v_span_pos); + if (std::abs(dot) > ORTHO_REL_TOL * u_norm * v_norm) { + set_errmsg("Slice span vectors must be orthogonal."); + return OPENMC_E_INVALID_ARGUMENT; + } + + // Validate filter index if provided + if (filter_index >= 0) { + if (int err = verify_filter(filter_index)) + return err; + } + + // Initialize overlap check vector if needed + if (color_overlaps && model::overlap_check_count.size() == 0) { + model::overlap_check_count.resize(model::cells.size()); + } + + try { + // Create a temporary SlicePlotBase object to reuse get_map logic + SlicePlotBase plot_params; + plot_params.origin_ = Position {origin[0], origin[1], origin[2]}; + plot_params.u_span_ = u_span_pos; + plot_params.v_span_ = v_span_pos; + plot_params.pixels_[0] = pixels[0]; + plot_params.pixels_[1] = pixels[1]; + plot_params.show_overlaps_ = color_overlaps; + plot_params.slice_level_ = level; + + // Use get_map to generate data + auto data = plot_params.get_map(filter_index); + + // Copy geometry data + std::copy(data.id_data_.begin(), data.id_data_.end(), geom_data); + + // Copy property data if requested + if (property_data != nullptr) { + std::copy( + data.property_data_.begin(), data.property_data_.end(), property_data); + } + } catch (const std::exception& e) { + set_errmsg(e.what()); + return OPENMC_E_UNASSIGNED; + } + + return 0; +} + extern "C" int openmc_get_plot_index(int32_t id, int32_t* index) { auto it = model::plot_map.find(id); diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index 6926bc11b..f62306e83 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -895,22 +895,23 @@ def test_load_nuclide(lib_init): openmc.lib.load_nuclide('Pu3') +class LegacySlicePlot: + origin = (0.0, 0.0, 0.0) + width = 1.26 + height = 1.26 + basis = 'xy' + h_res = 3 + v_res = 3 + level = -1 + + def test_id_map(lib_init): expected_ids = np.array([[(3, 0, 3), (2, 0, 2), (3, 0, 3)], [(2, 0, 2), (1, 0, 1), (2, 0, 2)], [(3, 0, 3), (2, 0, 2), (3, 0, 3)]], dtype='int32') - # create a plot object - s = openmc.lib.plot._PlotBase() - s.width = 1.26 - s.height = 1.26 - s.v_res = 3 - s.h_res = 3 - s.origin = (0.0, 0.0, 0.0) - s.basis = 'xy' - s.level = -1 - - ids = openmc.lib.plot.id_map(s) + with pytest.warns(FutureWarning, match="deprecated"): + ids = openmc.lib.id_map(LegacySlicePlot()) assert np.array_equal(expected_ids, ids) @@ -920,17 +921,8 @@ def test_property_map(lib_init): [ (293.6, 6.55), (293.6, 10.29769), (293.6, 6.55)], [(293.6, 0.740582), (293.6, 6.55), (293.6, 0.740582)]], dtype='float') - # create a plot object - s = openmc.lib.plot._PlotBase() - s.width = 1.26 - s.height = 1.26 - s.v_res = 3 - s.h_res = 3 - s.origin = (0.0, 0.0, 0.0) - s.basis = 'xy' - s.level = -1 - - properties = openmc.lib.plot.property_map(s) + with pytest.warns(FutureWarning, match="deprecated"): + properties = openmc.lib.property_map(LegacySlicePlot()) assert np.allclose(expected_properties, properties, atol=1e-04) diff --git a/tests/unit_tests/test_slice_data.py b/tests/unit_tests/test_slice_data.py new file mode 100644 index 000000000..cc5fb0474 --- /dev/null +++ b/tests/unit_tests/test_slice_data.py @@ -0,0 +1,168 @@ +import numpy as np +import openmc +from openmc.examples import pwr_pin_cell + + +def test_slice_data_basic(run_in_tmpdir): + """Test basic slice_data functionality.""" + model = pwr_pin_cell() + geom_data, prop_data = model.slice_data( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(100, 100), + basis='xy' + ) + + # Without filter, should have 3 fields + assert geom_data.shape == (100, 100, 3) + assert geom_data.dtype == np.int32 + assert prop_data.shape == (100, 100, 2) + assert prop_data.dtype == np.float64 + + # Check we have valid geometry + assert np.any(geom_data[:, :, 0] >= 0) # Valid cell IDs + assert np.any(prop_data[:, :, 0] > 0) # Valid temperatures + + +def test_slice_data_no_properties(run_in_tmpdir): + """Test slice_data without property data.""" + model = pwr_pin_cell() + geom_data, prop_data = model.slice_data( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(50, 50), + include_properties=False + ) + + # Without filter, should have 3 fields + assert geom_data.shape == (50, 50, 3) + assert prop_data is None + + +def test_slice_data_with_filter(run_in_tmpdir): + """Test slice_data with a cell filter.""" + model = pwr_pin_cell() + cell_ids = [c.id for c in model.geometry.get_all_cells().values()] + cell_filter = openmc.CellFilter(cell_ids) + + geom_data, _ = model.slice_data( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(50, 50), + filter=cell_filter, + include_properties=False + ) + + # With filter, should have 4 fields + assert geom_data.shape == (50, 50, 4) + + # Filter bin index should be populated where cells exist + filter_bins = geom_data[:, :, 3] + valid_cells = geom_data[:, :, 0] >= 0 + assert np.any(filter_bins[valid_cells] >= 0) + + +def test_slice_data_overlaps(run_in_tmpdir): + """Test slice_data with overlap detection.""" + model = pwr_pin_cell() + geom_data, _ = model.slice_data( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(50, 50), + show_overlaps=True, + include_properties=False + ) + + # Without filter, should have 3 fields + assert geom_data.shape == (50, 50, 3) + # Check for overlap markers (-3) if any exist + # Note: This test may pass without finding overlaps if geometry is correct + + +def test_slice_data_overlaps_with_filter(run_in_tmpdir): + """Test that overlaps don't overwrite filter bin data.""" + model = pwr_pin_cell() + cell_ids = [c.id for c in model.geometry.get_all_cells().values()] + cell_filter = openmc.CellFilter(cell_ids) + + geom_data, _ = model.slice_data( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(50, 50), + filter=cell_filter, + show_overlaps=True, + include_properties=False + ) + + assert geom_data.shape == (50, 50, 4) + + # If any overlaps exist, verify filter bin is still valid (not -3) + overlap_pixels = geom_data[:, :, 0] == -3 + if np.any(overlap_pixels): + # Filter bins at overlap locations should NOT be -3 + filter_bins_at_overlaps = geom_data[overlap_pixels, 3] + assert not np.all(filter_bins_at_overlaps == -3), \ + "Filter bins should be preserved even where overlaps are detected" + + +def test_slice_data_different_bases(run_in_tmpdir): + """Test slice_data with different basis planes.""" + model = pwr_pin_cell() + + for basis in ['xy', 'xz', 'yz']: + geom_data, prop_data = model.slice_data( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(25, 25), + basis=basis + ) + + assert geom_data.shape == (25, 25, 3) + assert prop_data.shape == (25, 25, 2) + + +def test_slice_data_oriented_spans(run_in_tmpdir): + """Test slice_data with oriented span vectors.""" + model = pwr_pin_cell() + + geom_data, prop_data = model.slice_data( + origin=(0, 0, 0), + u_span=(1.0, 0.0, 0.0), + v_span=(0.0, 0.0, 1.0), + pixels=(25, 25) + ) + + assert geom_data.shape == (25, 25, 3) + assert prop_data.shape == (25, 25, 2) + + +def test_slice_data_level(run_in_tmpdir): + """Test slice_data with specific universe level.""" + model = pwr_pin_cell() + geom_data, _ = model.slice_data( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(50, 50), + level=0, # Root universe only + include_properties=False + ) + + assert geom_data.shape == (50, 50, 3) + + +def test_id_map_reverted(run_in_tmpdir): + """Test that id_map returns 3D array without filter support.""" + model = pwr_pin_cell() + id_data = model.id_map( + origin=(0, 0, 0), + width=(1.0, 1.0), + pixels=(50, 50), + basis='xy' + ) + + # Should have 3 fields (cell_id, cell_instance, material_id) + assert id_data.shape == (50, 50, 3) + assert id_data.dtype == np.int32 + + # Check valid data + assert np.any(id_data[:, :, 0] >= 0) # Valid cell IDs From ea6ba328c9960a9e1b1cc85ab3b4f1ddd91ed3a1 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Mon, 8 Jun 2026 18:37:34 -0500 Subject: [PATCH 636/671] Fix collision track feature for photon transport (#3946) Co-authored-by: Paul Romano --- docs/source/io_formats/collision_track.rst | 4 +- docs/source/io_formats/settings.rst | 31 +++--- docs/source/usersguide/settings.rst | 5 + include/openmc/constants.h | 2 +- include/openmc/nuclide.h | 3 + src/collision_track.cpp | 12 ++- .../case_1_Reactions/collision_track_true.h5 | Bin 0 -> 2960 bytes .../case_1_Reactions/inputs_true.dat | 6 +- .../case_1_Reactions/results_true.dat | 2 - .../case_2_Cell_ID/collision_track_true.h5 | Bin 0 -> 9744 bytes .../case_2_Cell_ID/inputs_true.dat | 6 +- .../case_2_Cell_ID/results_true.dat | 2 - .../collision_track_true.h5 | Bin 0 -> 9744 bytes .../case_3_Material_ID/inputs_true.dat | 6 +- .../case_3_Material_ID/results_true.dat | 2 - .../case_4_Nuclide_ID/collision_track_true.h5 | Bin 0 -> 9232 bytes .../case_4_Nuclide_ID/inputs_true.dat | 6 +- .../case_4_Nuclide_ID/results_true.dat | 2 - .../collision_track_true.h5 | Bin 0 -> 9744 bytes .../case_5_Universe_ID/inputs_true.dat | 6 +- .../case_5_Universe_ID/results_true.dat | 2 - .../collision_track_true.h5 | Bin 0 -> 6160 bytes .../inputs_true.dat | 6 +- .../results_true.dat | 2 - .../collision_track_true.h5 | Bin 0 -> 6032 bytes .../inputs_true.dat | 6 +- .../results_true.dat | 2 - .../case_8_2threads/inputs_true.dat | 57 ---------- .../case_8_2threads/results_true.dat | 2 - .../regression_tests/collision_track/test.py | 51 ++------- tests/testing_harness.py | 97 +++++++++++------- tests/unit_tests/test_collision_track.py | 43 +++++++- 32 files changed, 167 insertions(+), 196 deletions(-) create mode 100644 tests/regression_tests/collision_track/case_1_Reactions/collision_track_true.h5 delete mode 100644 tests/regression_tests/collision_track/case_1_Reactions/results_true.dat create mode 100644 tests/regression_tests/collision_track/case_2_Cell_ID/collision_track_true.h5 delete mode 100644 tests/regression_tests/collision_track/case_2_Cell_ID/results_true.dat create mode 100644 tests/regression_tests/collision_track/case_3_Material_ID/collision_track_true.h5 delete mode 100644 tests/regression_tests/collision_track/case_3_Material_ID/results_true.dat create mode 100644 tests/regression_tests/collision_track/case_4_Nuclide_ID/collision_track_true.h5 delete mode 100644 tests/regression_tests/collision_track/case_4_Nuclide_ID/results_true.dat create mode 100644 tests/regression_tests/collision_track/case_5_Universe_ID/collision_track_true.h5 delete mode 100644 tests/regression_tests/collision_track/case_5_Universe_ID/results_true.dat create mode 100644 tests/regression_tests/collision_track/case_6_deposited_energy_threshold/collision_track_true.h5 delete mode 100644 tests/regression_tests/collision_track/case_6_deposited_energy_threshold/results_true.dat create mode 100644 tests/regression_tests/collision_track/case_7_all_parameters_used_together/collision_track_true.h5 delete mode 100644 tests/regression_tests/collision_track/case_7_all_parameters_used_together/results_true.dat delete mode 100644 tests/regression_tests/collision_track/case_8_2threads/inputs_true.dat delete mode 100644 tests/regression_tests/collision_track/case_8_2threads/results_true.dat diff --git a/docs/source/io_formats/collision_track.rst b/docs/source/io_formats/collision_track.rst index 8e1e00ffb..4123fdac6 100644 --- a/docs/source/io_formats/collision_track.rst +++ b/docs/source/io_formats/collision_track.rst @@ -10,7 +10,7 @@ may also be written after each batch when multiple files are requested (``collision_track.N.h5``) or when the run is performed in parallel. The file contains the information needed to reconstruct each recorded collision. -The current revision of the collision track file format is 1.1. +The current revision of the collision track file format is 1.2. **/** @@ -33,7 +33,7 @@ The current revision of the collision track file format is 1.1. - ``event_mt`` (*int*) -- ENDF MT number identifying the reaction. - ``delayed_group`` (*int*) -- Delayed neutron group index (non-zero for delayed events). - ``cell_id`` (*int*) -- ID of the cell in which the collision occurred. - - ``nuclide_id`` (*int*) -- ZA identifier of the nuclide (ZZZAAAM format). + - ``nuclide_id`` (*int*) -- PDG number of the nuclide (100ZZZAAAM). - ``material_id`` (*int*) -- ID of the material containing the collision site. - ``universe_id`` (*int*) -- ID of the universe containing the collision site. - ``n_collision`` (*int*) -- Collision counter for the particle history. diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 1a436a75e..fb0215916 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -98,6 +98,11 @@ sub-elements: A list of strings representing the nuclide, to define specific define specific target nuclide collisions to be banked. + .. note:: + Electron and positron collision-track events are not associated with + a specific nuclide. If a ``nuclides`` entry is specified, these events + are omitted. + *Default*: None :reactions: @@ -606,30 +611,30 @@ found in the :ref:`random ray user guide `. *Default*: None :adjoint_source: - Specifies an adjoint fixed source for adjoint transport simulations, and - follows the format for :ref:`source_element`. The distributions which make - up the adjoint source are subject to the same restrictions as forward + Specifies an adjoint fixed source for adjoint transport simulations, and + follows the format for :ref:`source_element`. The distributions which make + up the adjoint source are subject to the same restrictions as forward fixed sources in Random Ray mode. *Default*: None - + :adjoint: - Specifies whether to perform adjoint transport. The default is 'False', + Specifies whether to perform adjoint transport. The default is 'False', corresponding to forward transport. *Default*: None - + :volume_estimator: - Specifies choice of volume estimator for the random ray solver. Options + Specifies choice of volume estimator for the random ray solver. Options are 'naive', 'simulation_averaged', or 'hybrid'. The default is 'hybrid'. *Default*: None :volume_normalized_flux_tallies: - Specifies whether to normalize flux tallies by volume (bool). The - default is 'False'. When enabled, flux tallies will be reported in units - of cm/cm^3. When disabled, flux tallies will be reported in units of cm - (i.e., total distance traveled by neutrons in the spatial tally + Specifies whether to normalize flux tallies by volume (bool). The + default is 'False'. When enabled, flux tallies will be reported in units + of cm/cm^3. When disabled, flux tallies will be reported in units of cm + (i.e., total distance traveled by neutrons in the spatial tally region). *Default*: None @@ -1757,11 +1762,11 @@ mesh-based weight windows. The ratio of the lower to upper weight window bounds. *Default*: 5.0 - + For FW-CADIS: :targets: - A sequence of IDs corresponding to the tallies which cover phase + A sequence of IDs corresponding to the tallies which cover phase space regions of interest for local variance reduction. *Default*: None diff --git a/docs/source/usersguide/settings.rst b/docs/source/usersguide/settings.rst index 8ac07f3c8..e8514b856 100644 --- a/docs/source/usersguide/settings.rst +++ b/docs/source/usersguide/settings.rst @@ -792,6 +792,11 @@ collision_track.h5 file at the end of the simulation. The file contains 300 recorded collisions that occurred in materials with IDs 1 or 2, involving fission or (n,2n) reactions on the nuclides U-238 or O-16, within cells with IDs 5 and 12. + +.. note:: + Electron and positron collision-track events are not associated with a + specific nuclide. If a ``nuclides`` entry is specified, these events are omitted. + The file can be read using :func:`openmc.read_collision_track_file`. The example below shows how to extract the data from the collision_track feature and displays the fields stored in the file: diff --git a/include/openmc/constants.h b/include/openmc/constants.h index 0b425a673..7baed25c5 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -35,7 +35,7 @@ constexpr array VERSION_VOXEL {2, 0}; constexpr array VERSION_MGXS_LIBRARY {1, 0}; constexpr array VERSION_PROPERTIES {1, 1}; constexpr array VERSION_WEIGHT_WINDOWS {1, 0}; -constexpr array VERSION_COLLISION_TRACK {1, 1}; +constexpr array VERSION_COLLISION_TRACK {1, 2}; // ============================================================================ // ADJUSTABLE PARAMETERS diff --git a/include/openmc/nuclide.h b/include/openmc/nuclide.h index 7a8b2acad..ae39a53dd 100644 --- a/include/openmc/nuclide.h +++ b/include/openmc/nuclide.h @@ -84,6 +84,9 @@ public: double collapse_rate(int MT, double temperature, span energy, span flux) const; + //! Return a ParticleType object representing this nuclide + ParticleType particle_type() const { return {Z_, A_, metastable_}; } + //============================================================================ // Data members std::string name_; //!< Name of nuclide, e.g. "U235" diff --git a/src/collision_track.cpp b/src/collision_track.cpp index 03cbc32b7..2e749007f 100644 --- a/src/collision_track.cpp +++ b/src/collision_track.cpp @@ -200,8 +200,13 @@ void collision_track_record(Particle& particle) return; int cell_id = model::cells[cell_index]->id_; - const auto* nuclide_ptr = data::nuclides[particle.event_nuclide()].get(); - std::string nuclide = nuclide_ptr->name_; + std::string nuclide {}; + int nuclide_id = 0; + if (particle.event_nuclide() != NUCLIDE_NONE) { + const auto* nuclide_ptr = data::nuclides[particle.event_nuclide()].get(); + nuclide = nuclide_ptr->name_; + nuclide_id = nuclide_ptr->particle_type().pdg_number(); + } int universe_id = model::universes[particle.lowest_coord().universe()]->id_; double delta_E = particle.E_last() - particle.E(); int material_index = particle.material(); @@ -224,8 +229,7 @@ void collision_track_record(Particle& particle) site.event_mt = particle.event_mt(); site.delayed_group = particle.delayed_group(); site.cell_id = cell_id; - site.nuclide_id = - 10000 * nuclide_ptr->Z_ + 10 * nuclide_ptr->A_ + nuclide_ptr->metastable_; + site.nuclide_id = nuclide_id; site.material_id = material_id; site.universe_id = universe_id; site.n_collision = particle.n_collision(); diff --git a/tests/regression_tests/collision_track/case_1_Reactions/collision_track_true.h5 b/tests/regression_tests/collision_track/case_1_Reactions/collision_track_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..c315462134217a7b6f4f23b5831d8097fe92d024 GIT binary patch literal 2960 zcmeHJ&1(};5PzEw*tV%nDnxBB!Gm6UP^efeWFvGXDr#vX7OJFiv&L;Uo3P!)MlXU0 z6+A>i1hFV0*grrJK~0PxMJl58AS&oVdb3sVB$fC+<4dw>yo!BccjnFfelzcLvX{a` z`#Rfu+5u{!2wcF6RradTR_GuW)@S|+st8ohP&wxyiU1=b-)3s}()a-+-)^fv~)H7)^ML&N@KlJUwT$3zW33Yg1a_(_`!H4Co8yE0^5QYkn>w&i;r@+rcK& zMB1%SoRSBcaG2SSYiTKb=0gnBMA~uBBrJ>$H{md|9aqh%h80bb01$|a10&c-{$9mZ zJ(gGF(S(`KW*|0VgXc{=S5b96s>Q2yh{y3d&oQ!!uEkYE!|gZ^w9$EPDrTvs7Q;Ca zbixMDn|N;4&`?>aCZ_3mc#Y0;jcB!EVa(|oWMhZtW@4tLDLOtE2&p&1Yb6nRs`NJZ=W1J zd}K&~5y)5oDADhmwZ^{gYg~b?pLcy-{PevTf0y)4b^esjeH(vr@M2k-k<0J*tlX6j zl&;)5`f|CbZR*+L+xA)#pFTcz^Zo;gMbhpCl@sN~AnSKi#Si83$GJhYUx(o1gHH%< zRv(n8q5o>sR-U~YAC*_7!j0k5{PRb}or7aJSMLw$!sWin7nPFqW>vX9a%ZV{^+V*^ k!tN#M%fxK%S8$=4f7a=K80dQ2GC#_P^7rDb0BoT0Hw#|g3IG5A literal 0 HcmV?d00001 diff --git a/tests/regression_tests/collision_track/case_1_Reactions/inputs_true.dat b/tests/regression_tests/collision_track/case_1_Reactions/inputs_true.dat index 7533616c0..005a56020 100644 --- a/tests/regression_tests/collision_track/case_1_Reactions/inputs_true.dat +++ b/tests/regression_tests/collision_track/case_1_Reactions/inputs_true.dat @@ -38,9 +38,9 @@ eigenvalue - 100 + 80 5 - 1 + 4 -2.0 -2.0 -2.0 2.0 2.0 2.0 @@ -51,7 +51,7 @@ (n,fission) 101 - 300 + 100 1 diff --git a/tests/regression_tests/collision_track/case_1_Reactions/results_true.dat b/tests/regression_tests/collision_track/case_1_Reactions/results_true.dat deleted file mode 100644 index d4d1d1e5a..000000000 --- a/tests/regression_tests/collision_track/case_1_Reactions/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_2_Cell_ID/collision_track_true.h5 b/tests/regression_tests/collision_track/case_2_Cell_ID/collision_track_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..850bba8415ef0534522e94487a08473e1fae182b GIT binary patch literal 9744 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 E1#}!hp#T5? literal 0 HcmV?d00001 diff --git a/tests/regression_tests/collision_track/case_2_Cell_ID/inputs_true.dat b/tests/regression_tests/collision_track/case_2_Cell_ID/inputs_true.dat index 55fb835de..8d46c3d3b 100644 --- a/tests/regression_tests/collision_track/case_2_Cell_ID/inputs_true.dat +++ b/tests/regression_tests/collision_track/case_2_Cell_ID/inputs_true.dat @@ -38,9 +38,9 @@ eigenvalue - 100 + 80 5 - 1 + 4 -2.0 -2.0 -2.0 2.0 2.0 2.0 @@ -51,7 +51,7 @@ 22 - 300 + 100 1 diff --git a/tests/regression_tests/collision_track/case_2_Cell_ID/results_true.dat b/tests/regression_tests/collision_track/case_2_Cell_ID/results_true.dat deleted file mode 100644 index d4d1d1e5a..000000000 --- a/tests/regression_tests/collision_track/case_2_Cell_ID/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_3_Material_ID/collision_track_true.h5 b/tests/regression_tests/collision_track/case_3_Material_ID/collision_track_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..777d91f088810687d7762eaba930c31b7caffe2a GIT binary patch literal 9744 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 w>p+y eigenvalue - 100 + 80 5 - 1 + 4 -2.0 -2.0 -2.0 2.0 2.0 2.0 @@ -51,7 +51,7 @@ 1 - 300 + 100 1 diff --git a/tests/regression_tests/collision_track/case_3_Material_ID/results_true.dat b/tests/regression_tests/collision_track/case_3_Material_ID/results_true.dat deleted file mode 100644 index d4d1d1e5a..000000000 --- a/tests/regression_tests/collision_track/case_3_Material_ID/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_4_Nuclide_ID/collision_track_true.h5 b/tests/regression_tests/collision_track/case_4_Nuclide_ID/collision_track_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..527705297feba53a95173def72577be38f8954f8 GIT binary patch literal 9232 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 N=I?(5e+d%)zX2?eSMmS= literal 0 HcmV?d00001 diff --git a/tests/regression_tests/collision_track/case_4_Nuclide_ID/inputs_true.dat b/tests/regression_tests/collision_track/case_4_Nuclide_ID/inputs_true.dat index 8960dde5c..6202bcacc 100644 --- a/tests/regression_tests/collision_track/case_4_Nuclide_ID/inputs_true.dat +++ b/tests/regression_tests/collision_track/case_4_Nuclide_ID/inputs_true.dat @@ -38,9 +38,9 @@ eigenvalue - 100 + 80 5 - 1 + 4 -2.0 -2.0 -2.0 2.0 2.0 2.0 @@ -51,7 +51,7 @@ O16 U235 - 300 + 100 1 diff --git a/tests/regression_tests/collision_track/case_4_Nuclide_ID/results_true.dat b/tests/regression_tests/collision_track/case_4_Nuclide_ID/results_true.dat deleted file mode 100644 index d4d1d1e5a..000000000 --- a/tests/regression_tests/collision_track/case_4_Nuclide_ID/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_5_Universe_ID/collision_track_true.h5 b/tests/regression_tests/collision_track/case_5_Universe_ID/collision_track_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..65419d5ead9739ee3000454f40eeafe20497d129 GIT binary patch literal 9744 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 eigenvalue - 100 + 80 5 - 1 + 4 -2.0 -2.0 -2.0 2.0 2.0 2.0 @@ -52,7 +52,7 @@ 22 77 - 300 + 100 1 diff --git a/tests/regression_tests/collision_track/case_5_Universe_ID/results_true.dat b/tests/regression_tests/collision_track/case_5_Universe_ID/results_true.dat deleted file mode 100644 index d4d1d1e5a..000000000 --- a/tests/regression_tests/collision_track/case_5_Universe_ID/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/collision_track_true.h5 b/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/collision_track_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..507665a60fdac4d894511918d0492b9509172c8e GIT binary patch literal 6160 zcmeHKc|4U_AAc^cu7)Bdv}qI?`&x+eoDe3W)Fd?_Zd}XND@zo~v}&0q&CoIvm8rBZ zS`5xjs!5VHrA-N0(?+(u&vWndm^%+%-ap^ZJLi7x{hf1u%lG_#=XZYR-0cpwbLB>= zj>O2u;b2mjys%6E^ioY;Su{hBX}%ZLVNrA&MNdeGIxNB>$)nT>>mvJVk|KVJh+vBr z+Sy@*ByFFj6dQAp5%oCq#6R@^Dgq96(3N!9o$^|ZqJL6(Mh!8akf0#m8eRzBGc45G zC(v`HH$RZ%(fK)x791v3pz@WO*Rq(%5(|^TIK*caFNhl!wVF$tRU|s2i7$=ISQjRX z;X+Xsp({(AjtFijxoS+FNN_13M@__Mi7e1=(v+Mf`z(wt#8}G~iW7a&Nn=rZ6GYP( zO(ijTHmR#b9K$$*hEn{Yh!SyLNy}@85~TPFYJl*JY&d4}6=u>)=toh!q0aC?y3v&G z->VZfDMkj;4X5~`$cPCuy8p{Q62(*43>ZE!hXcjOmr|a^6lRPE(hcJU3v(W4$TEim z=|=j63B??k!i@1iy4(mZKg=^&WPp$t76*o&_)z>@`ErB2qqx4FexV`Zs|le+;(`P8 z#dUqSK|!88-@ZCDfVgl53F^B{b@i@AEP){X_xa0{1z>8^ihACMEOc!*%S5mF723M# zo0ax^FM8($jKcJMT-P??e%8+gJsQmTGkCCa?V)vSnwZL;;a+{x5397QbG=074R_PM#W!$ifXQ{=pAvm4sG!FIM=OZ0F$CUGVNOQ z;K|CV+hcAm=)`#T5?`w1@FRQjo^b8k&{)~M4FTHKU`@ss zqssd=$Z;YoKP}`YIG?j;;kNo(WMBDO{dxFwxIw~VY0XA;cCpw+d{Cjy0Kth)E{7$XW9L$3N^@PiAwb2!?i&6x0W2K%|&QL z;@L%4lPT6v+jiqy2L0yT|@@WZV|HhQFy6joI#jEgXFtJ^2{X`gl|=YBjTXu5-@>rX^@7J&iSnBYhV=a}S)= zhfmubEdRa{cb@3K*V&Z4?(#e2s-h#WzKtc2U2Dg8I@k!HxslYDefQ9|(+0dko!h`| zWqb1Pf)?Q9^@Z0GGY9UQ-pbd@w}#)1d+6-~jfagtkAc53WM;-`cS%8k{dt4TRaM~U zF73!k=?y4p;!eKx;bJuYlef9Ujuxc&;0+k#nvFWA##x&*xWY$LcIT3GZJ?okNvvCh z=CJWq82CmB??xT{7aQMq&8phar7E+N=eK za&Lg>J=(G#_Tw-%*5b{+w-(ST`jq=A34>wd|LsyC)-rom)WL7<Ib_4o8*X;C-(Cf%NHsWBavlZm$@lO?BG=zo@S-pvm zP5a>wcK(?DpUm>oZ&Uk>4!r(-@dvqj@WxKr({+3YICxj%c9Q-xge@<-*LkHFa9nG5 zNGLR-tjM{Nq5Hpu^2xeI@ln%Z_U)Jp>nhFq;gjb~P4ZB``m4@}e}Rpk=H%=maG)em z`v$uh1zRC+A2Q@z%!`=32W%5uP zy1&#a@$xnr(tq*}BJ2Ms8knLJ7MDDcHDe3(iu2AkpYKBFrB<*cnlzI=dQC+(-G4tIAi=Mf36c zk1Xm?=k4k8n;ul7n*}K)(4kK=9Y{;?jZ}z&~oT;q01b^rrhMN0nU;O5Kf= zWY;;sJfjrlFYl&7pT^D|zQ(Yh|IGR$P2iIIPy5fN)}i=O+CA=Q4_I=kt>(3A8E~4q zeX>`mB(4|sL_6qIA~+#&ZsV@fX3%!|1xH4+qA&jo?dy&l-);#N*J!!8YYspECo eigenvalue - 100 + 80 5 - 1 + 4 -2.0 -2.0 -2.0 2.0 2.0 2.0 @@ -51,7 +51,7 @@ 550000.0 - 300 + 100 1 diff --git a/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/results_true.dat b/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/results_true.dat deleted file mode 100644 index d4d1d1e5a..000000000 --- a/tests/regression_tests/collision_track/case_6_deposited_energy_threshold/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_7_all_parameters_used_together/collision_track_true.h5 b/tests/regression_tests/collision_track/case_7_all_parameters_used_together/collision_track_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..b7ff67c48e1c0261d34de2a9a61179e569671da2 GIT binary patch literal 6032 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 literal 0 HcmV?d00001 diff --git a/tests/regression_tests/collision_track/case_7_all_parameters_used_together/inputs_true.dat b/tests/regression_tests/collision_track/case_7_all_parameters_used_together/inputs_true.dat index 005d9feb2..1449c3596 100644 --- a/tests/regression_tests/collision_track/case_7_all_parameters_used_together/inputs_true.dat +++ b/tests/regression_tests/collision_track/case_7_all_parameters_used_together/inputs_true.dat @@ -38,9 +38,9 @@ eigenvalue - 100 + 80 5 - 1 + 4 -2.0 -2.0 -2.0 2.0 2.0 2.0 @@ -56,7 +56,7 @@ 1 11 U238 U235 H1 U234 100000.0 - 300 + 100 1 diff --git a/tests/regression_tests/collision_track/case_7_all_parameters_used_together/results_true.dat b/tests/regression_tests/collision_track/case_7_all_parameters_used_together/results_true.dat deleted file mode 100644 index d4d1d1e5a..000000000 --- a/tests/regression_tests/collision_track/case_7_all_parameters_used_together/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/case_8_2threads/inputs_true.dat b/tests/regression_tests/collision_track/case_8_2threads/inputs_true.dat deleted file mode 100644 index 514932c1a..000000000 --- a/tests/regression_tests/collision_track/case_8_2threads/inputs_true.dat +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - eigenvalue - 100 - 5 - 1 - - - -2.0 -2.0 -2.0 2.0 2.0 2.0 - - - true - - - - 200 - - 1 - - diff --git a/tests/regression_tests/collision_track/case_8_2threads/results_true.dat b/tests/regression_tests/collision_track/case_8_2threads/results_true.dat deleted file mode 100644 index d4d1d1e5a..000000000 --- a/tests/regression_tests/collision_track/case_8_2threads/results_true.dat +++ /dev/null @@ -1,2 +0,0 @@ -k-combined: -5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/collision_track/test.py b/tests/regression_tests/collision_track/test.py index 00e1e3de4..2f9de9ac0 100644 --- a/tests/regression_tests/collision_track/test.py +++ b/tests/regression_tests/collision_track/test.py @@ -59,28 +59,13 @@ TODO: """ -import os - import openmc -import openmc.lib import pytest from tests.testing_harness import CollisionTrackTestHarness from tests.regression_tests import config -@pytest.fixture(scope="function") -def two_threads(monkeypatch): - """Set the number of OMP threads to 2 for the test.""" - monkeypatch.setenv("OMP_NUM_THREADS", "2") - - -@pytest.fixture(scope="function") -def single_process(monkeypatch): - """Set the number of MPI process to 1 for the test.""" - monkeypatch.setitem(config, "mpi_np", "1") - - @pytest.fixture(scope="module") def model_1(): """Cylindrical core contained in a first box which is contained in a larger box. @@ -181,9 +166,9 @@ def model_1(): # ============================================================================= model.settings = openmc.Settings() - model.settings.particles = 100 + model.settings.particles = 80 model.settings.batches = 5 - model.settings.inactive = 1 + model.settings.inactive = 4 model.settings.seed = 1 bounds = [ @@ -203,19 +188,19 @@ def model_1(): @pytest.mark.parametrize( "folder, model_name, parameter", - [("case_1_Reactions", "model_1", {"max_collisions": 300, "reactions": ["(n,fission)", 101]}), + [("case_1_Reactions", "model_1", {"max_collisions": 100, "reactions": ["(n,fission)", 101]}), ("case_2_Cell_ID", "model_1", { - "max_collisions": 300, "cell_ids": [22]}), + "max_collisions": 100, "cell_ids": [22]}), ("case_3_Material_ID", "model_1", { - "max_collisions": 300, "material_ids": [1]}), + "max_collisions": 100, "material_ids": [1]}), ("case_4_Nuclide_ID", "model_1", { - "max_collisions": 300, "nuclides": ["O16", "U235"]}), + "max_collisions": 100, "nuclides": ["O16", "U235"]}), ("case_5_Universe_ID", "model_1", { - "max_collisions": 300, "cell_ids": [22], "universe_ids": [77]}), + "max_collisions": 100, "cell_ids": [22], "universe_ids": [77]}), ("case_6_deposited_energy_threshold", "model_1", { - "max_collisions": 300, "deposited_E_threshold": 5.5e5}), + "max_collisions": 100, "deposited_E_threshold": 5.5e5}), ("case_7_all_parameters_used_together", "model_1", { - "max_collisions": 300, + "max_collisions": 100, "reactions": ["elastic", 18, "(n,disappear)"], "material_ids": [1, 11], "universe_ids": [77], @@ -235,21 +220,3 @@ def test_collision_track_several_cases( "statepoint.5.h5", model=model, workdir=folder ) harness.main() - - -@pytest.mark.skipif(config["event"], reason="Results from history-based mode.") -def test_collision_track_2threads(model_1, two_threads, single_process): - # This test checks that the `max_collisions` setting is honored: - # no collisions beyond the specified limit should be recorded. - # - # For the result to be reproducible, the number of threads and - # the transport mode (history vs. event) must remain fixed. - assert os.environ["OMP_NUM_THREADS"] == "2" - assert config["mpi_np"] == "1" - model_1.settings.collision_track = { - "max_collisions": 200 - } - harness = CollisionTrackTestHarness( - "statepoint.5.h5", model=model_1, workdir="case_8_2threads" - ) - harness.main() diff --git a/tests/testing_harness.py b/tests/testing_harness.py index 1ad91b7a8..8d156bd64 100644 --- a/tests/testing_harness.py +++ b/tests/testing_harness.py @@ -546,19 +546,19 @@ class CollisionTrackTestHarness(PyAPITestHarness): def _test_output_created(self): """Make sure collision_track.h5 has also been created.""" - super()._test_output_created() if self._model.settings.collision_track: assert os.path.exists( "collision_track.h5" ), "collision_track file has not been created." - def _compare_output(self): + def _compare_results(self): """Compare collision_track.h5 files.""" if self._model.settings.collision_track: collision_track_true = self._return_collision_track_data( "collision_track_true.h5") collision_track_test = self._return_collision_track_data( "collision_track.h5") + assert collision_track_true.shape == collision_track_test.shape np.testing.assert_allclose( collision_track_true, collision_track_test, rtol=1e-07) @@ -582,15 +582,18 @@ class CollisionTrackTestHarness(PyAPITestHarness): def _overwrite_results(self): """Also add the 'collision_track.h5' file during overwriting.""" - super()._overwrite_results() if os.path.exists("collision_track.h5"): shutil.copyfile("collision_track.h5", "collision_track_true.h5") + def _write_results(self, results_string): + # The result file for this test are written by the OpenMC executable itself + pass + @staticmethod def _return_collision_track_data(filepath): """ - Read a collision_track file and return a sorted array composed - of flatten arrays of collision information. + Read a collision_track file and return a sorted array composed of + flattened collision records. Parameters ---------- @@ -600,42 +603,58 @@ class CollisionTrackTestHarness(PyAPITestHarness): Returns ------- data : np.array - Sorted array composed of flatten arrays of collision_track data for - each collision information + Sorted array composed of flattened collision-track records. """ - data = [] - keys = [] - - # Read source file source = openmc.read_collision_track_file(filepath) - for src in source: - r = src['r'] - u = src['u'] - e = src['E'] - de = src['dE'] - time = src['time'] - wgt = src['wgt'] - delayed_group = src['delayed_group'] - cell_id = src['cell_id'] - nuclide_id = src['nuclide_id'] - material_id = src['material_id'] - universe_id = src['universe_id'] - n_collision = src['n_collision'] - event_mt = src['event_mt'] - key = ( - f"{r[0]:.10e} {r[1]:.10e} {r[2]:.10e} {u[0]:.10e} {u[1]:.10e} {u[2]:.10e}" - f"{e:.10e} {de:.10e} {time:.10e} {wgt:.10e} {event_mt} {delayed_group} {cell_id}" - f"{nuclide_id} {material_id} {universe_id} {n_collision} " - ) - keys.append(key) - values = [*r, *u, e, de, time, wgt, event_mt, - delayed_group, cell_id, nuclide_id, material_id, - universe_id, n_collision] - assert len(values) == 17 - data.append(values) + columns = [ + source['r']['x'], + source['r']['y'], + source['r']['z'], + source['u']['x'], + source['u']['y'], + source['u']['z'], + source['E'], + source['dE'], + source['time'], + source['wgt'], + source['event_mt'], + source['delayed_group'], + source['cell_id'], + source['nuclide_id'], + source['material_id'], + source['universe_id'], + source['n_collision'], + source['particle'], + source['parent_id'], + source['progeny_id'], + ] + data = np.column_stack(columns) - data = np.array(data) - keys = np.array(keys) - sorted_idx = np.argsort(keys, kind='stable') + # Sort by the complete record, prioritizing stable integer identifiers + # before floating-point fields. This removes dependence on the order in + # which threads append otherwise reproducible collision records. + sort_columns = [ + source['parent_id'], + source['progeny_id'], + source['n_collision'], + source['particle'], + source['cell_id'], + source['material_id'], + source['universe_id'], + source['nuclide_id'], + source['event_mt'], + source['delayed_group'], + source['r']['x'], + source['r']['y'], + source['r']['z'], + source['u']['x'], + source['u']['y'], + source['u']['z'], + source['E'], + source['dE'], + source['time'], + source['wgt'], + ] + sorted_idx = np.lexsort(tuple(reversed(sort_columns))) return data[sorted_idx] diff --git a/tests/unit_tests/test_collision_track.py b/tests/unit_tests/test_collision_track.py index 25344a3bd..96676e2cd 100644 --- a/tests/unit_tests/test_collision_track.py +++ b/tests/unit_tests/test_collision_track.py @@ -34,6 +34,7 @@ def geometry(): {"max_collisions": 200, "mcpl": True} ], + ids=str ) def test_xml_serialization(parameter, run_in_tmpdir): """Check that the different use cases can be written and read in XML.""" @@ -45,7 +46,7 @@ def test_xml_serialization(parameter, run_in_tmpdir): assert read_settings.collision_track == parameter -@pytest.fixture(scope="module") +@pytest.fixture def model(): """Simple hydrogen sphere divided in two hemispheres by a z-plane to form 2 cells.""" @@ -127,3 +128,43 @@ def test_format_similarity(run_in_tmpdir, model): np.testing.assert_allclose(data_h5, data_mcpl, rtol=1e-05) # tolerance not that low due to the strings that is saved in MCPL, # not enough precision! + + +def test_photon_particles(run_in_tmpdir, model): + """Test that the collision track can be used to track photon particles.""" + model.settings.collision_track = {"max_collisions": 200, "cell_ids": [1, 2]} + + model.settings.source = openmc.IndependentSource( + space=openmc.stats.Box(*model.geometry.bounding_box), + energy=openmc.stats.delta_function(1e5), + particle='photon' + ) + model.run() + + with h5py.File("collision_track.h5", "r") as f: + source = f["collision_track_bank"] + + assert len(source) < 200 + + allowed_particles = (openmc.ParticleType.PHOTON, openmc.ParticleType.ELECTRON) + + for point in source: + particle_type = openmc.ParticleType(point['particle']) + assert particle_type in allowed_particles + + if particle_type == openmc.ParticleType.ELECTRON: + assert point['nuclide_id'] == 0 + + +def test_collision_track_two_threads(model, run_in_tmpdir): + # This test checks that the `max_collisions` setting is honored: + # no collisions beyond the specified limit should be recorded. + # + # The exact set of events in the capped bank is not reproducible with + # multiple threads because the bank stores whichever thread appends first + # until capacity is reached. + model.settings.collision_track = {"max_collisions": 200} + model.run(threads=2, particles=500) + + collision_track = openmc.read_collision_track_hdf5("collision_track.h5") + assert len(collision_track) == 200 From 998565808ea2c4e85fcf45686de4f5eefac6b300 Mon Sep 17 00:00:00 2001 From: Logan Harbour Date: Mon, 8 Jun 2026 21:07:21 -0600 Subject: [PATCH 637/671] Use non-deprecated methods for getting libMesh node points (#3963) --- src/mesh.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mesh.cpp b/src/mesh.cpp index 5ab7ac398..181af8466 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -3670,7 +3670,7 @@ Position LibMesh::sample_element(int32_t bin, uint64_t* seed) const // Get tet vertex coordinates from LibMesh std::array tet_verts; for (int i = 0; i < elem.n_nodes(); i++) { - auto node_ref = elem.node_ref(i); + const auto& node_ref = elem.node_ref(i); tet_verts[i] = {node_ref(0), node_ref(1), node_ref(2)}; } // Samples position within tet using Barycentric coordinates @@ -3700,7 +3700,7 @@ int LibMesh::n_vertices() const Position LibMesh::vertex(int vertex_id) const { - const auto node_ref = m_->node_ref(vertex_id); + const auto& node_ref = m_->node_ref(vertex_id); if (length_multiplier_ > 0.0) { return length_multiplier_ * Position(node_ref(0), node_ref(1), node_ref(2)); } else { From 02eb999af1e30b6a61c092baeebe2329ac5719c8 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Tue, 9 Jun 2026 06:09:25 +0300 Subject: [PATCH 638/671] fix tmate start only on successful tests (#3954) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8c14279b..c3da79c04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -177,7 +177,7 @@ jobs: - name: Setup tmate debug session continue-on-error: true - if: ${{ contains(env.COMMIT_MESSAGE, '[gha-debug]') }} + if: ${{ failure() && contains(env.COMMIT_MESSAGE, '[gha-debug]') }} uses: mxschmitt/action-tmate@v3 timeout-minutes: 10 From 09ee8308d089c6e4be8e025edc619b9d650a3acd Mon Sep 17 00:00:00 2001 From: stetsonschott Date: Wed, 17 Jun 2026 20:33:40 -0500 Subject: [PATCH 639/671] Replaced 'C0' by elemental carbon in `openmc/examples.py'. (#3974) --- openmc/examples.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openmc/examples.py b/openmc/examples.py index 90a0bffe7..6895bd94b 100644 --- a/openmc/examples.py +++ b/openmc/examples.py @@ -150,7 +150,7 @@ def pwr_core() -> openmc.Model: rpv_steel.add_nuclide('Ni60', 0.0026776, 'wo') rpv_steel.add_nuclide('Mn55', 0.01, 'wo') rpv_steel.add_nuclide('Cr52', 0.002092475, 'wo') - rpv_steel.add_nuclide('C0', 0.0025, 'wo') + rpv_steel.add_element('C', 0.0025, 'wo') rpv_steel.add_nuclide('Cu63', 0.0013696, 'wo') lower_rad_ref = openmc.Material(6, name='Lower radial reflector') @@ -1618,4 +1618,4 @@ def random_ray_three_region_cube_with_detectors() -> openmc.Model: model.settings = settings model.tallies = tallies - return model \ No newline at end of file + return model From 608a1c3386db6af88d70d35cc2202d3955aea30b Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 23 Jun 2026 02:00:47 -0500 Subject: [PATCH 640/671] Fix several issues related to independent operator depletion (#3977) --- openmc/deplete/independent_operator.py | 4 ++ openmc/deplete/microxs.py | 63 ++++++++++++++++++------ tests/unit_tests/test_deplete_microxs.py | 60 ++++++++++++++++++++++ 3 files changed, 112 insertions(+), 15 deletions(-) diff --git a/openmc/deplete/independent_operator.py b/openmc/deplete/independent_operator.py index c12863956..bdfc32763 100644 --- a/openmc/deplete/independent_operator.py +++ b/openmc/deplete/independent_operator.py @@ -339,8 +339,12 @@ class IndependentOperator(OpenMCOperator): for i_nuc in nuc_index: nuc = self.nuc_ind_map[i_nuc] + if nuc not in xs._index_nuc: + continue for i_rx in react_index: rx = self.rx_ind_map[i_rx] + if rx not in xs._index_rx: + continue # Determine reaction rate by multiplying xs in [b] by flux # in [n-cm/src] to give [(reactions/src)*b-cm/atom] diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index 687cf646f..42bb958ca 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -84,7 +84,12 @@ def get_microxs_and_flux( reactions listed in the depletion chain file are used. energies : iterable of float or str Energy group boundaries in [eV] or the name of the group structure. - If left as None energies will default to [0.0, 100e6] + If left as None, no energy filter is applied to the flux tally. When + `reaction_rate_mode` is "direct", these boundaries define the output + flux and microscopic cross section energy group structure. When + `reaction_rate_mode` is "flux", these boundaries define the multigroup + flux tally used to collapse continuous-energy cross sections; returned + fluxes and microscopic cross sections are one-group. reaction_rate_mode : {"direct", "flux"}, optional The "direct" method tallies reaction rates directly (per energy group). The "flux" method tallies a multigroup flux spectrum and then @@ -110,7 +115,9 @@ def get_microxs_and_flux( 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". + over one energy bin spanning the full `energies` range. Supported keys: + "nuclides", "reactions". If "reactions" are specified without + "nuclides", all selected nuclides are used. Returns ------- @@ -139,10 +146,14 @@ def get_microxs_and_flux( nuclides = [nuc.name for nuc in chain.nuclides if nuc.name in nuclides_with_data] - # Set up the reaction rate and flux tallies + # Set up the reaction rate and flux tallies. When energies are omitted, no + # energy filter is needed for the transport calculation. A one-group energy + # range is still needed later if flux collapse is requested. + collapse_energies = energies if energies is None: - energies = [0.0, 100.0e6] - if isinstance(energies, str): + energy_filter = None + collapse_energies = [0.0, 100.0e6] + elif isinstance(energies, str): energy_filter = openmc.EnergyFilter.from_group_structure(energies) else: energy_filter = openmc.EnergyFilter(energies) @@ -172,8 +183,11 @@ def get_microxs_and_flux( 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', [])) + if rr_reactions: + rr_nuclides = list(opts.get('nuclides', nuclides)) + else: + rr_nuclides = list(opts.get('nuclides', [])) # Keep only requested pairs within overall sets if rr_nuclides: rr_nuclides = [n for n in rr_nuclides if n in set(nuclides)] @@ -182,7 +196,7 @@ def get_microxs_and_flux( # Use 1-group energy filter for RR in flux mode has_rr = bool(rr_nuclides and rr_reactions) - if has_rr and reaction_rate_mode == 'flux': + if has_rr and reaction_rate_mode == 'flux' and energy_filter is not None: rr_energy_filter = openmc.EnergyFilter( [energy_filter.values[0], energy_filter.values[-1]]) else: @@ -194,14 +208,18 @@ def get_microxs_and_flux( model.tallies = [] for i, domain_filter in enumerate(domain_filters): flux_tally = openmc.Tally(name=f'MicroXS flux {i}') - flux_tally.filters = [domain_filter, energy_filter] + flux_tally.filters = [domain_filter] + if energy_filter is not None: + flux_tally.filters.append(energy_filter) flux_tally.scores = ['flux'] model.tallies.append(flux_tally) flux_tallies.append(flux_tally) if has_rr: rr_tally = openmc.Tally(name=f'MicroXS RR {i}') - rr_tally.filters = [domain_filter, rr_energy_filter] + rr_tally.filters = [domain_filter] + if rr_energy_filter is not None: + rr_tally.filters.append(rr_energy_filter) rr_tally.nuclides = rr_nuclides rr_tally.multiply_density = False rr_tally.scores = rr_reactions @@ -255,8 +273,12 @@ def get_microxs_and_flux( all_flux_arrays = [] for flux_tally in flux_tallies: # Get flux values and make energy groups last dimension - flux = flux_tally.get_reshaped_data() # (domains, groups, 1, 1) - flux = np.moveaxis(flux, 1, -1) # (domains, 1, 1, groups) + flux = flux_tally.get_reshaped_data() + if energy_filter is None: + flux = flux[..., np.newaxis] # (domains, 1, 1, groups) + else: + # (domains, groups, 1, 1) -> (domains, 1, 1, groups) + flux = np.moveaxis(flux, 1, -1) all_flux_arrays.append(flux) fluxes.extend(flux.squeeze((1, 2))) @@ -266,8 +288,15 @@ def get_microxs_and_flux( for flux_arr, rr_tally in zip(all_flux_arrays, rr_tallies): flux = flux_arr # Get reaction rates and make energy groups last dimension - reaction_rates = rr_tally.get_reshaped_data() # (domains, groups, nuclides, reactions) - reaction_rates = np.moveaxis(reaction_rates, 1, -1) # (domains, nuclides, reactions, groups) + reaction_rates = rr_tally.get_reshaped_data() + if rr_energy_filter is None: + # (domains, nuclides, reactions) -> + # (domains, nuclides, reactions, groups) + reaction_rates = reaction_rates[..., np.newaxis] + else: + # (domains, groups, nuclides, reactions) -> + # (domains, nuclides, reactions, groups) + reaction_rates = np.moveaxis(reaction_rates, 1, -1) # If RR is 1-group, sum flux over groups if reaction_rate_mode == "flux": @@ -279,16 +308,20 @@ def get_microxs_and_flux( direct_micros.extend( 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': + # Compute flux-collapsed microscopic XS flux_micros = [MicroXS.from_multigroup_flux( - energies=energies, + energies=collapse_energies, multigroup_flux=flux_i, chain_file=chain_file, nuclides=nuclides, reactions=reactions ) for flux_i in fluxes] + # We need to return one-group fluxes to match the microscopic cross + # sections, which are always one-group by virtue of the collapse + fluxes = [flux.sum(keepdims=True) for flux in fluxes] + # Decide which micros to use and merge if needed if reaction_rate_mode == 'flux' and rr_tallies: micros = [m1.merge(m2) for m1, m2 in zip(flux_micros, direct_micros)] diff --git a/tests/unit_tests/test_deplete_microxs.py b/tests/unit_tests/test_deplete_microxs.py index 26529e6ce..0b1937fac 100644 --- a/tests/unit_tests/test_deplete_microxs.py +++ b/tests/unit_tests/test_deplete_microxs.py @@ -179,6 +179,66 @@ def test_hybrid_tally_setup(): assert ef.values[0] == pytest.approx(energies[0]) assert ef.values[-1] == pytest.approx(energies[-1]) + +def _simple_model(): + model = openmc.Model() + mat = openmc.Material(components={'H1': 1.0, 'H2': 1.0}, + density=5.0, density_units='g/cm3') + sphere = openmc.Sphere(r=10.0, boundary_type='vacuum') + cell = openmc.Cell(region=-sphere, fill=mat) + model.geometry = openmc.Geometry([cell]) + model.settings.particles = 100 + model.settings.batches = 5 + model.settings.run_mode = 'fixed source' + return model, mat + + +def test_hybrid_tally_defaults_to_all_nuclides(run_in_tmpdir): + energies = [0., 0.625, 2.0e7] + kwargs = { + 'nuclides': ['H1', 'H2'], + 'reactions': ['(n,2n)', '(n,gamma)'], + 'energies': energies, + 'reaction_rate_mode': 'flux', + 'chain_file': CHAIN_FILE, + } + + model, mat = _simple_model() + default_fluxes, default_micros = get_microxs_and_flux( + model, [mat], reaction_rate_opts={'reactions': ['(n,2n)']}, **kwargs + ) + + model, mat = _simple_model() + explicit_fluxes, explicit_micros = get_microxs_and_flux( + model, [mat], + reaction_rate_opts={ + 'nuclides': ['H1', 'H2'], + 'reactions': ['(n,2n)'] + }, + **kwargs + ) + + np.testing.assert_allclose(default_fluxes[0], explicit_fluxes[0]) + np.testing.assert_allclose(default_micros[0].data, explicit_micros[0].data) + assert default_micros[0].nuclides == explicit_micros[0].nuclides + assert default_micros[0].reactions == explicit_micros[0].reactions + + +def test_flux_mode_returns_one_group_flux(run_in_tmpdir): + model, mat = _simple_model() + fluxes, micros = get_microxs_and_flux( + model, [mat], + nuclides=['H1'], + reactions=['(n,2n)'], + energies=[0., 0.625, 2.0e7], + reaction_rate_mode='flux', + chain_file=CHAIN_FILE, + ) + + assert fluxes[0].shape == (1,) + assert micros[0].data.shape == (1, 1, 1) + assert fluxes[0][0] > 0.0 + # --------------------------------------------------------------------------- # Tests for MicroXS.merge() # --------------------------------------------------------------------------- From 4d6244d93c78daafdb4db19bdff37fd204306666 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Thu, 25 Jun 2026 00:09:16 +0300 Subject: [PATCH 641/671] Fix for numpy 2.5.0 (#3981) --- tests/regression_tests/mg_temperature/build_2g.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/regression_tests/mg_temperature/build_2g.py b/tests/regression_tests/mg_temperature/build_2g.py index 1fb723449..bca87364e 100644 --- a/tests/regression_tests/mg_temperature/build_2g.py +++ b/tests/regression_tests/mg_temperature/build_2g.py @@ -227,7 +227,7 @@ def analytical_solution_2g_therm(xsmin, xsmax=None, wgt=1.0): L = np.array([sa[0] + ss12, 0.0, -ss12, sa[1]]).reshape(2, 2) Q = np.array([nsf[0], nsf[1], 0.0, 0.0]).reshape(2, 2) arr = np.linalg.inv(L).dot(Q) - return np.amax(np.linalg.eigvals(arr)) + return np.amax(np.linalg.eigvals(arr).real) def build_inf_model(xsnames, xslibname, temperature, tempmethod='nearest'): From d06fdee93a3b6b52a22d90bcee2f6382f6171ee6 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Tue, 30 Jun 2026 16:38:09 +0200 Subject: [PATCH 642/671] Preserve user material names in convert_to_multigroup and avoid overwriting material with same name bug fix (#3984) --- openmc/model/model.py | 17 +++++++---- .../infinite_medium/inputs_true.dat | 10 +++---- .../material_wise/inputs_true.dat | 10 +++---- .../stochastic_slab/inputs_true.dat | 10 +++---- .../infinite_medium/inputs_true.dat | 10 +++---- .../material_wise/inputs_true.dat | 10 +++---- .../stochastic_slab/inputs_true.dat | 10 +++---- .../infinite_medium/model/inputs_true.dat | 10 +++---- .../infinite_medium/user/inputs_true.dat | 10 +++---- .../stochastic_slab/model/inputs_true.dat | 10 +++---- .../stochastic_slab/user/inputs_true.dat | 10 +++---- .../infinite_medium/inputs_true.dat | 10 +++---- .../material_wise/inputs_true.dat | 10 +++---- .../stochastic_slab/inputs_true.dat | 10 +++---- .../inputs_true.dat | 10 +++---- tests/unit_tests/test_model.py | 29 +++++++++++++++++++ 16 files changed, 111 insertions(+), 75 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index d927b65ae..328494288 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -2772,12 +2772,15 @@ class Model: self.settings.run_mode = original_run_mode break - # Make sure all materials have a name, and that the name is a valid HDF5 - # dataset name + # Temporarily replace each material's name with a unique, valid HDF5 + # dataset name (its name plus ID) for use as its MGXS library entry + # and macroscopic. The ID keeps the name unique even when materials + # share a name; the original names are restored at the end. + original_names = [material.name for material in self.materials] for material in self.materials: - if not material.name or not material.name.strip(): - material.name = f"material {material.id}" - material.name = re.sub(r'[^a-zA-Z0-9]', '_', material.name) + base = material.name if material.name and material.name.strip() \ + else "material" + material.name = re.sub(r'[^a-zA-Z0-9]', '_', base) + f"_{material.id}" # If needed, generate the needed MGXS data library file if not Path(mgxs_path).is_file() or overwrite_mgxs_library: @@ -2809,6 +2812,10 @@ class Model: self.settings.energy_mode = 'multi-group' + # Restore the user's original material names. + for material, name in zip(self.materials, original_names): + material.name = name + def convert_to_random_ray(self): """Convert a multigroup model to use random ray. diff --git a/tests/regression_tests/random_ray_auto_convert/infinite_medium/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert/infinite_medium/inputs_true.dat index 86d5ec4ab..81f8c98ca 100644 --- a/tests/regression_tests/random_ray_auto_convert/infinite_medium/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert/infinite_medium/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert/material_wise/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert/material_wise/inputs_true.dat index 86d5ec4ab..81f8c98ca 100644 --- a/tests/regression_tests/random_ray_auto_convert/material_wise/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert/material_wise/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert/stochastic_slab/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert/stochastic_slab/inputs_true.dat index 86d5ec4ab..81f8c98ca 100644 --- a/tests/regression_tests/random_ray_auto_convert/stochastic_slab/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert/stochastic_slab/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert_kappa_fission/infinite_medium/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_kappa_fission/infinite_medium/inputs_true.dat index b00935ef3..48b0e8256 100644 --- a/tests/regression_tests/random_ray_auto_convert_kappa_fission/infinite_medium/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_kappa_fission/infinite_medium/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert_kappa_fission/material_wise/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_kappa_fission/material_wise/inputs_true.dat index 472406fa8..be738c3e0 100644 --- a/tests/regression_tests/random_ray_auto_convert_kappa_fission/material_wise/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_kappa_fission/material_wise/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert_kappa_fission/stochastic_slab/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_kappa_fission/stochastic_slab/inputs_true.dat index 472406fa8..be738c3e0 100644 --- a/tests/regression_tests/random_ray_auto_convert_kappa_fission/stochastic_slab/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_kappa_fission/stochastic_slab/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/inputs_true.dat index 15981f7fa..d2d8289ff 100644 --- a/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/model/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/inputs_true.dat index 86d5ec4ab..81f8c98ca 100644 --- a/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/infinite_medium/user/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/inputs_true.dat index 15981f7fa..d2d8289ff 100644 --- a/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/model/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/inputs_true.dat index 86d5ec4ab..81f8c98ca 100644 --- a/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_source_energy/stochastic_slab/user/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert_temperature/infinite_medium/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_temperature/infinite_medium/inputs_true.dat index c60e6a041..c49020d55 100644 --- a/tests/regression_tests/random_ray_auto_convert_temperature/infinite_medium/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_temperature/infinite_medium/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert_temperature/material_wise/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_temperature/material_wise/inputs_true.dat index c60e6a041..c49020d55 100644 --- a/tests/regression_tests/random_ray_auto_convert_temperature/material_wise/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_temperature/material_wise/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_auto_convert_temperature/stochastic_slab/inputs_true.dat b/tests/regression_tests/random_ray_auto_convert_temperature/stochastic_slab/inputs_true.dat index c60e6a041..c49020d55 100644 --- a/tests/regression_tests/random_ray_auto_convert_temperature/stochastic_slab/inputs_true.dat +++ b/tests/regression_tests/random_ray_auto_convert_temperature/stochastic_slab/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat b/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat index 11100e88e..0ea8c0177 100644 --- a/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat +++ b/tests/regression_tests/random_ray_diagonal_stabilization/inputs_true.dat @@ -2,17 +2,17 @@ mgxs.h5 - + - + - + - + - + diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 9234b2d27..028a83b0b 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -1038,3 +1038,32 @@ def test_id_map_to_rgb(): ) # Check that overlap region is green assert np.allclose(rgb_overlap[5:, 5:], [0.0, 1.0, 0.0]) + + +def test_convert_to_multigroup_preserves_material_names(run_in_tmpdir): + """convert_to_multigroup leaves the user's material names unchanged and keys + the MGXS library by a unique sanitised name + id, so distinct materials that + share a name do not collapse to a single cross section.""" + a = openmc.Material(name="Steel Plate #1") + a.add_element("Fe", 1.0) + a.set_density("g/cm3", 7.9) + b = openmc.Material(name="Steel Plate #1") # same name, distinct material + b.add_element("Fe", 1.0) + b.set_density("g/cm3", 7.9) + + s1 = openmc.Sphere(r=1.0) + s2 = openmc.Sphere(r=2.0, boundary_type="vacuum") + c1 = openmc.Cell(fill=a, region=-s1) + c2 = openmc.Cell(fill=b, region=+s1 & -s2) + model = openmc.Model(openmc.Geometry([c1, c2]), openmc.Materials([a, b])) + + # Pre-create the library so MGXS generation (and transport) is skipped. + Path("mgxs.h5").touch() + model.convert_to_multigroup(method="material_wise", mgxs_path="mgxs.h5") + + # User names are preserved, not sanitised or de-duplicated. + assert [m.name for m in model.materials] == ["Steel Plate #1", "Steel Plate #1"] + # Each material reads a unique, sanitised library entry (name + id). + macro = [m._macroscopic for m in model.materials] + assert macro == [f"Steel_Plate__1_{a.id}", f"Steel_Plate__1_{b.id}"] + assert len(set(macro)) == 2 From f01852411dcb12ade9b0c2493555303e0227f25b Mon Sep 17 00:00:00 2001 From: John Tramm Date: Wed, 1 Jul 2026 15:35:27 -0500 Subject: [PATCH 643/671] Random Ray Forward Flux Save in Adjoint Mode (#3962) Co-authored-by: Claude Opus 4.8 (1M context) --- docs/source/usersguide/random_ray.rst | 13 ++- include/openmc/constants.h | 1 + .../openmc/random_ray/flat_source_domain.h | 5 +- .../openmc/random_ray/random_ray_simulation.h | 2 +- src/output.cpp | 9 +- src/random_ray/flat_source_domain.cpp | 18 ++-- src/random_ray/random_ray_simulation.cpp | 87 ++++++++----------- src/settings.cpp | 2 +- src/simulation.cpp | 10 ++- src/state_point.cpp | 11 ++- src/weight_windows.cpp | 4 +- tests/testing_harness.py | 3 +- 12 files changed, 95 insertions(+), 70 deletions(-) diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index d35aff83b..06d4e50ed 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -1105,10 +1105,15 @@ external source is present in the problem. Simulation settings (e.g., number of rays, batches, etc.) will be identical for both calculations. At the conclusion of the run, all results (e.g., tallies, plots, etc.) will be derived from the adjoint flux rather than the forward flux but are not labeled -any differently. The initial forward flux solution will not be stored or -available in the final statepoint file. Those wishing to do analysis requiring -both the forward and adjoint solutions will need to run two separate -simulations and load both statepoint files. +any differently. When an initial forward solve is performed (i.e., when no +user-specified adjoint source is present), its output files are also written to +disk with a ``forward`` infix, so they are not overwritten by the subsequent +adjoint solve. This applies to the statepoint, ``tallies.out``, and any voxel +plots, e.g., ``statepoint.forward.N.h5`` and ``tallies.forward.out``; the +adjoint solve keeps the usual file names. This allows analyses requiring both +the forward and adjoint solutions to be performed from a single run. When +generating FW-CADIS weight windows, no weight window file is written for the +forward solve, as only the final adjoint-derived weight windows are meaningful. .. note:: Use of the automated diff --git a/include/openmc/constants.h b/include/openmc/constants.h index 7baed25c5..26b4a224c 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -368,6 +368,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, S2 }; +enum class RandomRaySolve { FORWARD, FORWARD_FOR_ADJOINT, ADJOINT }; //============================================================================== // Geometry Constants diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index 6f51af34d..09414fd44 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -76,7 +76,10 @@ public: //---------------------------------------------------------------------------- // Static Data members static bool volume_normalized_flux_tallies_; - static bool adjoint_; // If the user wants outputs based on the adjoint flux + // If the user wants outputs based on the adjoint flux + static bool adjoint_requested_; + // The solve currently being executed + static RandomRaySolve solve_; static bool fw_cadis_local_; static double diagonal_stabilization_rho_; // Adjusts strength of diagonal stabilization diff --git a/include/openmc/random_ray/random_ray_simulation.h b/include/openmc/random_ray/random_ray_simulation.h index ccd2cbe47..e186c549f 100644 --- a/include/openmc/random_ray/random_ray_simulation.h +++ b/include/openmc/random_ray/random_ray_simulation.h @@ -22,7 +22,7 @@ public: void apply_fixed_sources_and_mesh_domains(); void prepare_fw_fixed_sources_adjoint(); void prepare_local_fixed_sources_adjoint(); - void prepare_adjoint_simulation(bool fw_adjoint); + void prepare_adjoint_simulation(bool from_forward); void simulate(); void output_simulation_results() const; void instability_check( diff --git a/src/output.cpp b/src/output.cpp index 725e8d069..04a8caa60 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -621,8 +621,15 @@ void write_tallies() if (model::tallies.empty()) return; + // Tag tallies.out written during the forward solve of an adjoint run + const char* forward = + (FlatSourceDomain::solve_ == RandomRaySolve::FORWARD_FOR_ADJOINT) + ? "forward." + : ""; + // Set filename for tallies_out - std::string filename = fmt::format("{}tallies.out", settings::path_output); + std::string filename = + fmt::format("{}tallies.{}out", settings::path_output, forward); // Open the tallies.out file. std::ofstream tallies_out; diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 06c6ef14d..895e0663e 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -30,7 +30,8 @@ namespace openmc { RandomRayVolumeEstimator FlatSourceDomain::volume_estimator_ { RandomRayVolumeEstimator::HYBRID}; bool FlatSourceDomain::volume_normalized_flux_tallies_ {false}; -bool FlatSourceDomain::adjoint_ {false}; +bool FlatSourceDomain::adjoint_requested_ {false}; +RandomRaySolve FlatSourceDomain::solve_ {RandomRaySolve::FORWARD}; bool FlatSourceDomain::fw_cadis_local_ {false}; double FlatSourceDomain::diagonal_stabilization_rho_ {1.0}; std::unordered_map>> @@ -556,7 +557,7 @@ double FlatSourceDomain::compute_fixed_source_normalization_factor() const // If we are in adjoint mode of a fixed source problem, the external // source is already normalized, such that all resulting fluxes are // also normalized. - if (adjoint_) { + if (solve_ == RandomRaySolve::ADJOINT) { return 1.0; } @@ -795,6 +796,12 @@ void FlatSourceDomain::output_to_vtk() const double z_delta = width.z / Nz; std::string filename = openmc_plot->path_plot(); + // Tag plots written during the forward solve of an adjoint run + if (solve_ == RandomRaySolve::FORWARD_FOR_ADJOINT) { + auto dot = filename.find_last_of('.'); + filename = filename.substr(0, dot) + ".forward" + filename.substr(dot); + } + // Perform sanity checks on file size uint64_t bytes = Nx * Ny * Nz * (negroups_ + 1 + 1 + 1) * sizeof(float); write_message(5, "Processing plot {}: {}... (Estimated size is {} MB)", @@ -1002,9 +1009,10 @@ void FlatSourceDomain::output_to_vtk() const void FlatSourceDomain::apply_external_source_to_source_region( int src_idx, SourceRegionHandle& srh) { - auto s = (adjoint_ && !model::adjoint_sources.empty()) - ? model::adjoint_sources[src_idx].get() - : model::external_sources[src_idx].get(); + auto s = + (solve_ == RandomRaySolve::ADJOINT && !model::adjoint_sources.empty()) + ? model::adjoint_sources[src_idx].get() + : model::external_sources[src_idx].get(); auto is = dynamic_cast(s); auto discrete = dynamic_cast(is->energy()); double strength_factor = is->strength(); diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index 24650afb8..00fff99a7 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -189,7 +189,7 @@ void validate_random_ray_inputs() // Validate adjoint sources /////////////////////////////////////////////////////////////////// - if (FlatSourceDomain::adjoint_ && !model::adjoint_sources.empty()) { + if (FlatSourceDomain::adjoint_requested_ && !model::adjoint_sources.empty()) { for (int i = 0; i < model::adjoint_sources.size(); i++) { Source* s = model::adjoint_sources[i].get(); @@ -289,7 +289,8 @@ void openmc_finalize_random_ray() { FlatSourceDomain::volume_estimator_ = RandomRayVolumeEstimator::HYBRID; FlatSourceDomain::volume_normalized_flux_tallies_ = false; - FlatSourceDomain::adjoint_ = false; + FlatSourceDomain::adjoint_requested_ = false; + FlatSourceDomain::solve_ = RandomRaySolve::FORWARD; FlatSourceDomain::fw_cadis_local_ = false; FlatSourceDomain::fw_cadis_local_targets_.clear(); FlatSourceDomain::mesh_domain_map_.clear(); @@ -356,19 +357,17 @@ void RandomRaySimulation::prepare_local_fixed_sources_adjoint() } } -void RandomRaySimulation::prepare_adjoint_simulation(bool fw_adjoint) +void RandomRaySimulation::prepare_adjoint_simulation(bool from_forward) { reset_timers(); if (mpi::master) header("ADJOINT FLUX SOLVE", 3); - if (fw_adjoint) { - // Forward simulation has already been run; - // Configure the domain for adjoint simulation and - // re-initialize OpenMC general data structures - FlatSourceDomain::adjoint_ = true; - + if (from_forward) { + // The forward solve has already run. Re-initialize OpenMC's general data + // structures for the adjoint solve and derive the adjoint source from the + // forward flux. openmc_simulation_init(); prepare_fw_fixed_sources_adjoint(); @@ -603,7 +602,8 @@ void RandomRaySimulation::print_results_random_ray( } fmt::print(" Volume Estimator Type = {}\n", estimator); - std::string adjoint_true = (FlatSourceDomain::adjoint_) ? "ON" : "OFF"; + std::string adjoint_true = + (FlatSourceDomain::solve_ == RandomRaySolve::ADJOINT) ? "ON" : "OFF"; fmt::print(" Adjoint Flux Mode = {}\n", adjoint_true); std::string shape; @@ -675,60 +675,49 @@ void RandomRaySimulation::print_results_random_ray( void openmc_run_random_ray() { - ////////////////////////////////////////////////////////// - // Run forward simulation - ////////////////////////////////////////////////////////// + using namespace openmc; - // Check if adjoint calculation is needed, and if local adjoint source(s) - // are present. If an adjoint calculation is needed and no sources are - // specified, we will run a forward calculation first to calculate adjoint - // sources for global variance reduction, then perform an adjoint - // calculation later. - bool adjoint_needed = openmc::FlatSourceDomain::adjoint_; - bool fw_adjoint = openmc::model::adjoint_sources.empty() && adjoint_needed; + // Determine which solves to run. If adjoint results are requested and no + // user-defined adjoint source is present, an initial forward solve is needed + // to construct the adjoint source from the forward flux (FW-CADIS). If the + // user has defined an adjoint source, the forward solve is skipped and only + // the adjoint solve is run. + const bool run_adjoint = FlatSourceDomain::adjoint_requested_; + const bool have_adjoint_source = !model::adjoint_sources.empty(); + const bool run_forward = !(run_adjoint && have_adjoint_source); - // If we're going to do an adjoint simulation with forward-weighted adjoint - // sources afterwards, report that this is the initial forward flux solve. - if (!adjoint_needed || fw_adjoint) { - // Configure the domain for forward simulation - openmc::FlatSourceDomain::adjoint_ = false; - - if (adjoint_needed && openmc::mpi::master) - openmc::header("FORWARD FLUX SOLVE", 3); + // Set the initial solve type + if (!run_forward) { + FlatSourceDomain::solve_ = RandomRaySolve::ADJOINT; + } else if (run_adjoint) { + FlatSourceDomain::solve_ = RandomRaySolve::FORWARD_FOR_ADJOINT; } else { - // Configure domain for adjoint simulation (later) - openmc::FlatSourceDomain::adjoint_ = true; + FlatSourceDomain::solve_ = RandomRaySolve::FORWARD; } // Initialize OpenMC general data structures openmc_simulation_init(); // Validate that inputs meet requirements for random ray mode - if (openmc::mpi::master) - openmc::validate_random_ray_inputs(); + if (mpi::master) + validate_random_ray_inputs(); // Initialize Random Ray Simulation Object - openmc::RandomRaySimulation sim; + RandomRaySimulation sim; - if (!adjoint_needed || fw_adjoint) { - // Initialize fixed sources, if present + // Run the forward solve + if (run_forward) { + // When an adjoint solve follows, report this as the initial forward solve + if (run_adjoint && mpi::master) + header("FORWARD FLUX SOLVE", 3); sim.apply_fixed_sources_and_mesh_domains(); - - // Execute random ray simulation sim.simulate(); } - ////////////////////////////////////////////////////////// - // Run adjoint simulation (if enabled) - ////////////////////////////////////////////////////////// - - if (!adjoint_needed) { - return; + // Run the adjoint solve + if (run_adjoint) { + FlatSourceDomain::solve_ = RandomRaySolve::ADJOINT; + sim.prepare_adjoint_simulation(run_forward); + sim.simulate(); } - - // Setup for adjoint simulation - sim.prepare_adjoint_simulation(fw_adjoint); - - // Execute random ray simulation - sim.simulate(); } diff --git a/src/settings.cpp b/src/settings.cpp index 8ae252ae1..58bced940 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -322,7 +322,7 @@ void get_run_parameters(pugi::xml_node node_base) get_node_value_bool(random_ray_node, "volume_normalized_flux_tallies"); } if (check_for_node(random_ray_node, "adjoint")) { - FlatSourceDomain::adjoint_ = + FlatSourceDomain::adjoint_requested_ = get_node_value_bool(random_ray_node, "adjoint"); } if (check_for_node(random_ray_node, "sample_method")) { diff --git a/src/simulation.cpp b/src/simulation.cpp index 89aa9ca0e..900a383ab 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -16,6 +16,7 @@ #include "openmc/particle.h" #include "openmc/photon.h" #include "openmc/random_lcg.h" +#include "openmc/random_ray/flat_source_domain.h" #include "openmc/settings.h" #include "openmc/source.h" #include "openmc/state_point.h" @@ -200,9 +201,12 @@ int openmc_simulation_finalize() if (settings::output_tallies && mpi::master) write_tallies(); - // If weight window generators are present in this simulation, - // write a weight windows file - if (variance_reduction::weight_windows_generators.size() > 0) { + // If weight window generators are present in this simulation, write a + // weight windows file. This is skipped during the forward solve of an + // adjoint (FW-CADIS) run, where only the adjoint-derived weight windows + // are meaningful. + if (variance_reduction::weight_windows_generators.size() > 0 && + FlatSourceDomain::solve_ != RandomRaySolve::FORWARD_FOR_ADJOINT) { openmc_weight_windows_export(); } diff --git a/src/state_point.cpp b/src/state_point.cpp index da1c141a2..02c68bf60 100644 --- a/src/state_point.cpp +++ b/src/state_point.cpp @@ -22,6 +22,7 @@ #include "openmc/nuclide.h" #include "openmc/output.h" #include "openmc/particle_type.h" +#include "openmc/random_ray/flat_source_domain.h" #include "openmc/settings.h" #include "openmc/simulation.h" #include "openmc/tallies/derivative.h" @@ -46,9 +47,15 @@ extern "C" int openmc_statepoint_write(const char* filename, bool* write_source) // Determine width for zero padding int w = std::to_string(settings::n_max_batches).size(); + // Tag statepoints written during the forward solve of an adjoint run + const char* forward = + (FlatSourceDomain::solve_ == RandomRaySolve::FORWARD_FOR_ADJOINT) + ? "forward." + : ""; + // Set filename for state point - filename_ = fmt::format("{0}statepoint.{1:0{2}}.h5", settings::path_output, - simulation::current_batch, w); + filename_ = fmt::format("{0}statepoint.{3}{1:0{2}}.h5", + settings::path_output, simulation::current_batch, w, forward); } // If a file name was specified, ensure it has .h5 file extension diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index 0614110cd..39e46026c 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -791,7 +791,7 @@ WeightWindowsGenerator::WeightWindowsGenerator(pugi::xml_node node) if (method_string == "magic") { method_ = WeightWindowUpdateMethod::MAGIC; if (settings::solver_type == SolverType::RANDOM_RAY && - FlatSourceDomain::adjoint_) { + FlatSourceDomain::adjoint_requested_) { fatal_error("Random ray weight window generation with MAGIC cannot be " "done in adjoint mode."); } @@ -800,7 +800,7 @@ WeightWindowsGenerator::WeightWindowsGenerator(pugi::xml_node node) if (settings::solver_type != SolverType::RANDOM_RAY) { fatal_error("FW-CADIS can only be run in random ray solver mode."); } - FlatSourceDomain::adjoint_ = true; + FlatSourceDomain::adjoint_requested_ = true; if (check_for_node(node, "targets")) { FlatSourceDomain::fw_cadis_local_ = true; targets_ = get_node_array(node, "targets"); diff --git a/tests/testing_harness.py b/tests/testing_harness.py index 8d156bd64..c1977556d 100644 --- a/tests/testing_harness.py +++ b/tests/testing_harness.py @@ -143,7 +143,8 @@ class TestHarness: def _cleanup(self): """Delete statepoints, tally, and test files.""" output = glob.glob('statepoint.*.h5') - output += ['tallies.out', 'results_test.dat', 'summary.h5'] + output += ['tallies.out', 'tallies.forward.out'] + output += ['results_test.dat', 'summary.h5'] output += glob.glob('volume_*.h5') for f in output: if os.path.exists(f): From 24fdb84edccb1bd4fe59c35fb082dab74ed982ec Mon Sep 17 00:00:00 2001 From: John Tramm Date: Thu, 2 Jul 2026 11:53:49 -0500 Subject: [PATCH 644/671] Tally 32-bit Overflow Fix (#3960) Co-authored-by: Claude Opus 4.8 (1M context) Co-authored-by: Paul Romano --- include/openmc/random_ray/source_region.h | 6 ++--- include/openmc/tallies/tally.h | 8 +++---- include/openmc/tallies/tally_scoring.h | 2 +- src/mesh.cpp | 22 ++++++++++++++---- src/random_ray/flat_source_domain.cpp | 2 +- src/tallies/tally.cpp | 6 ++--- src/tallies/tally_scoring.cpp | 18 +++++++-------- src/tallies/trigger.cpp | 4 ++-- tests/cpp_unit_tests/test_tally.cpp | 27 ++++++++++++++++++++++- 9 files changed, 67 insertions(+), 28 deletions(-) diff --git a/include/openmc/random_ray/source_region.h b/include/openmc/random_ray/source_region.h index 65f2e6bc4..1d2bbe1e8 100644 --- a/include/openmc/random_ray/source_region.h +++ b/include/openmc/random_ray/source_region.h @@ -51,10 +51,10 @@ inline void hash_combine(size_t& seed, const size_t v) // every iteration. struct TallyTask { int tally_idx; - int filter_idx; + int64_t filter_idx; int score_idx; int score_type; - TallyTask(int tally_idx, int filter_idx, int score_idx, int score_type) + TallyTask(int tally_idx, int64_t filter_idx, int score_idx, int score_type) : tally_idx(tally_idx), filter_idx(filter_idx), score_idx(score_idx), score_type(score_type) {} @@ -690,7 +690,7 @@ private: // Private Methods // Helper function for indexing - inline int index(int64_t sr, int g) const { return sr * negroups_ + g; } + inline int64_t index(int64_t sr, int g) const { return sr * negroups_ + g; } }; } // namespace openmc diff --git a/include/openmc/tallies/tally.h b/include/openmc/tallies/tally.h index 66e6ff764..ae604b732 100644 --- a/include/openmc/tallies/tally.h +++ b/include/openmc/tallies/tally.h @@ -97,9 +97,9 @@ public: //! Given already-set filters, set the stride lengths void set_strides(); - int32_t strides(int i) const { return strides_[i]; } + int64_t strides(int i) const { return strides_[i]; } - int32_t n_filter_bins() const { return n_filter_bins_; } + int64_t n_filter_bins() const { return n_filter_bins_; } bool multiply_density() const { return multiply_density_; } @@ -184,9 +184,9 @@ private: vector filters_; //!< Filter indices in global filters array //! Index strides assigned to each filter to support 1D indexing. - vector strides_; + vector strides_; - int32_t n_filter_bins_ {0}; + int64_t n_filter_bins_ {0}; //! Whether to multiply by atom density for reaction rates bool multiply_density_ {true}; diff --git a/include/openmc/tallies/tally_scoring.h b/include/openmc/tallies/tally_scoring.h index d1aed2831..4303ddb7a 100644 --- a/include/openmc/tallies/tally_scoring.h +++ b/include/openmc/tallies/tally_scoring.h @@ -41,7 +41,7 @@ public: FilterBinIter& operator++(); - int index_ {1}; + int64_t index_ {1}; double weight_ {1.}; vector& filter_matches_; diff --git a/src/mesh.cpp b/src/mesh.cpp index 181af8466..89d1ff24b 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -6,7 +6,8 @@ #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 +#include // for accumulate #include #ifdef _MSC_VER @@ -1080,13 +1081,26 @@ int StructuredMesh::get_bin(Position r) const int StructuredMesh::n_bins() const { - return std::accumulate( - shape_.begin(), shape_.begin() + n_dimension_, 1, std::multiplies<>()); + // Bin indices are stored as 32-bit ints in the tally system. + int64_t n = 1; + for (int i = 0; i < n_dimension_; ++i) + n *= shape_[i]; + if (n > std::numeric_limits::max()) { + fatal_error(fmt::format( + "Mesh {} has too many bins ({}) for 32-bit tally indexing", id_, n)); + } + return static_cast(n); } int StructuredMesh::n_surface_bins() const { - return 4 * n_dimension_ * n_bins(); + // Surface bin indices are stored as 32-bit ints in the tally system. + int64_t n = static_cast(n_bins()) * 4 * n_dimension_; + if (n > std::numeric_limits::max()) { + fatal_error(fmt::format( + "Mesh {} has too many surface bins ({}) for tally indexing", id_, n)); + } + return static_cast(n); } tensor::Tensor StructuredMesh::count_sites( diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 895e0663e..83128fdaa 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -725,7 +725,7 @@ void FlatSourceDomain::random_ray_tally() for (int i = 0; i < model::tallies.size(); i++) { Tally& tally {*model::tallies[i]}; #pragma omp parallel for - for (int bin = 0; bin < tally.n_filter_bins(); bin++) { + for (int64_t bin = 0; bin < tally.n_filter_bins(); bin++) { for (int score_idx = 0; score_idx < tally.n_scores(); score_idx++) { auto score_type = tally.scores_[score_idx]; if (score_type == SCORE_FLUX) { diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index e4ea56e59..085633247 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -512,7 +512,7 @@ void Tally::set_strides() // longest stride. auto n = filters_.size(); strides_.resize(n, 0); - int stride = 1; + int64_t stride = 1; for (int i = n - 1; i >= 0; --i) { strides_[i] = stride; stride *= model::tally_filters[filters_[i]]->n_bins(); @@ -887,7 +887,7 @@ 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 (int64_t 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) { double val = results_(i, j, TallyResult::VALUE) * norm; @@ -902,7 +902,7 @@ void Tally::accumulate() } else { #pragma omp parallel for // filter bins (specific cell, energy bins) - for (int i = 0; i < results_.shape(0); ++i) { + for (int64_t 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) { double val = results_(i, j, TallyResult::VALUE) * norm; diff --git a/src/tallies/tally_scoring.cpp b/src/tallies/tally_scoring.cpp index b7947afab..7bce6ec13 100644 --- a/src/tallies/tally_scoring.cpp +++ b/src/tallies/tally_scoring.cpp @@ -158,7 +158,7 @@ void score_fission_delayed_dg(int i_tally, int d_bin, double score, dg_match.bins_[i_bin] = d_bin; // Determine the filter scoring index - auto filter_index = 0; + int64_t filter_index = 0; double filter_weight = 1.; for (auto i = 0; i < tally.filters().size(); ++i) { auto i_filt = tally.filters(i); @@ -449,7 +449,7 @@ void score_fission_eout(Particle& p, int i_tally, int i_score, int score_bin) (score_bin == SCORE_PROMPT_NU_FISSION && g == 0)) { // Find the filter scoring index for this filter combination - int filter_index = 0; + int64_t filter_index = 0; double filter_weight = 1.0; for (auto j = 0; j < tally.filters().size(); ++j) { auto i_filt = tally.filters(j); @@ -497,7 +497,7 @@ void score_fission_eout(Particle& p, int i_tally, int i_score, int score_bin) } else { // Find the filter index and weight for this filter combination - int filter_index = 0; + int64_t filter_index = 0; double filter_weight = 1.; for (auto j = 0; j < tally.filters().size(); ++j) { auto i_filt = tally.filters(j); @@ -578,8 +578,8 @@ double get_nuclide_xs(const Particle& p, int i_nuclide, int score_bin) //! collision estimator. void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, - int filter_index, double filter_weight, int i_nuclide, double atom_density, - double flux) + int64_t filter_index, double filter_weight, int i_nuclide, + double atom_density, double flux) { Tally& tally {*model::tallies[i_tally]}; @@ -1112,8 +1112,8 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, //! is not used for analog tallies. void score_general_ce_analog(Particle& p, int i_tally, int start_index, - int filter_index, double filter_weight, int i_nuclide, double atom_density, - double flux) + int64_t filter_index, double filter_weight, int i_nuclide, + double atom_density, double flux) { Tally& tally {*model::tallies[i_tally]}; @@ -1615,8 +1615,8 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index, //! argument is really just used for filter weights. void score_general_mg(Particle& p, int i_tally, int start_index, - int filter_index, double filter_weight, int i_nuclide, double atom_density, - double flux) + int64_t filter_index, double filter_weight, int i_nuclide, + double atom_density, double flux) { auto& tally {*model::tallies[i_tally]}; diff --git a/src/tallies/trigger.cpp b/src/tallies/trigger.cpp index 6c54edd5e..7515f01a9 100644 --- a/src/tallies/trigger.cpp +++ b/src/tallies/trigger.cpp @@ -28,7 +28,7 @@ KTrigger keff_trigger; //============================================================================== std::pair get_tally_uncertainty( - int i_tally, int score_index, int filter_index) + int i_tally, int score_index, int64_t filter_index) { const auto& tally {model::tallies[i_tally]}; @@ -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 (int64_t filter_index = 0; filter_index < results.shape(0); ++filter_index) { // Compute the tally uncertainty metrics. auto uncert_pair = diff --git a/tests/cpp_unit_tests/test_tally.cpp b/tests/cpp_unit_tests/test_tally.cpp index 964d30cc4..03322e764 100644 --- a/tests/cpp_unit_tests/test_tally.cpp +++ b/tests/cpp_unit_tests/test_tally.cpp @@ -1,3 +1,4 @@ +#include "openmc/tallies/filter_energy.h" #include "openmc/tallies/tally.h" #include @@ -46,10 +47,34 @@ TEST_CASE("Test add/set_filter") REQUIRE(model::filter_map[cell_filter->id()] == tally->filters(0)); REQUIRE(model::filter_map[particle_filter->id()] == tally->filters(1)); - // set filters with a duplicate filter, should only add the filter to the tally once + // set filters with a duplicate filter, should only add the filter to the + // tally once filters = {cell_filter, cell_filter}; tally->set_filters(filters); REQUIRE(tally->filters().size() == 1); REQUIRE(model::filter_map[cell_filter->id()] == tally->filters(0)); +} +// Regression test for 64-bit tally filter-bin counts (mesh x groups > 2^31). +TEST_CASE("Tally filter-bin count does not overflow 32 bits") +{ + // Two energy filters whose bin counts multiply to 2.5e9, above INT32_MAX. + constexpr int64_t bins_per_filter = 50000; + + // Only the bin count matters here, so the edge values are an arbitrary ramp. + std::vector edges(bins_per_filter + 1); + for (int64_t i = 0; i < bins_per_filter + 1; ++i) + edges[i] = static_cast(i); + + Tally* tally = Tally::create(); + for (int i = 0; i < 2; ++i) { + Filter* filter = Filter::create("energy"); + dynamic_cast(filter)->set_bins(edges); + tally->add_filter(filter); + } + tally->set_strides(); + + // set_strides() previously accumulated this product in a 32-bit int. + REQUIRE(tally->n_filter_bins() == bins_per_filter * bins_per_filter); + REQUIRE(tally->n_filter_bins() > 2147483647); } \ No newline at end of file From 3247587d4920fa3c9a4e3e3da79adc20e23eb1f7 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 2 Jul 2026 11:54:12 -0500 Subject: [PATCH 645/671] Ensure photon cross sections are loaded when FileSource contains photons (#3988) --- src/source.cpp | 9 ++++++++- tests/unit_tests/test_source_file.py | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/source.cpp b/src/source.cpp index bee20d5c3..1e783b623 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -523,9 +523,16 @@ void FileSource::load_sites_from_file(const std::string& path) file_close(file_id); } - // Make sure particles in source file have valid types + // Make sure particles in source file have valid types. If any particle is a + // photon, electron, or positron, enable photon transport so that the + // appropriate cross sections are loaded. for (const auto& site : this->sites_) { validate_particle_type(site.particle, "FileSource"); + if (site.particle == ParticleType::photon() || + site.particle == ParticleType::electron() || + site.particle == ParticleType::positron()) { + settings::photon_transport = true; + } } } diff --git a/tests/unit_tests/test_source_file.py b/tests/unit_tests/test_source_file.py index 3e84fbe51..f19ea74a6 100644 --- a/tests/unit_tests/test_source_file.py +++ b/tests/unit_tests/test_source_file.py @@ -145,3 +145,27 @@ def test_source_file_transport(run_in_tmpdir): # Try running OpenMC model.run() + + +def test_source_file_photon_transport(run_in_tmpdir): + # Create a source file containing a photon. Note that photon_transport is + # not explicitly enabled in the settings -- it should be turned on + # automatically because the source file contains a photon. + particle = openmc.SourceParticle(E=1.0e6, particle='photon') + openmc.write_source_file([particle], 'photon_source.h5') + + # Create simple model to use the photon source file + model = openmc.Model() + al = openmc.Material() + al.add_element('Al', 1.0) + al.set_density('g/cm3', 2.7) + sph = openmc.Sphere(r=10.0, boundary_type='vacuum') + cell = openmc.Cell(fill=al, region=-sph) + model.geometry = openmc.Geometry([cell]) + model.settings.source = openmc.FileSource(path='photon_source.h5') + model.settings.particles = 10 + model.settings.batches = 3 + model.settings.run_mode = 'fixed source' + + # Running OpenMC should succeed + model.run() From df6f94300f6f9013100073962ffc417f4c32c754 Mon Sep 17 00:00:00 2001 From: Jon Shimwell Date: Thu, 2 Jul 2026 20:04:02 +0200 Subject: [PATCH 646/671] Turn the weight window game off for a zero or negative lower bound (#3990) Co-authored-by: John Tramm Co-authored-by: shimwell --- include/openmc/weight_windows.h | 8 ++- tests/regression_tests/weightwindows/test.py | 55 ++++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/include/openmc/weight_windows.h b/include/openmc/weight_windows.h index a5d404133..d0b385d16 100644 --- a/include/openmc/weight_windows.h +++ b/include/openmc/weight_windows.h @@ -52,8 +52,12 @@ struct WeightWindow { double weight_cutoff {DEFAULT_WEIGHT_CUTOFF}; int max_split {10}; - //! Whether the weight window is in a valid state - bool is_valid() const { return lower_weight >= 0.0; } + //! Whether the weight window is in a valid state. A non-positive lower + //! bound indicates that no weight window information exists at this + //! location (generators mark such cells with -1, and a lower bound of zero + //! conventionally turns the weight window game off in a cell, as in MCNP + //! wwinp files), in which case no weight window game is played. + bool is_valid() const { return lower_weight > 0.0; } //! Adjust the weight window by a constant factor void scale(double factor) diff --git a/tests/regression_tests/weightwindows/test.py b/tests/regression_tests/weightwindows/test.py index 1d3b063bd..c5b6bd889 100644 --- a/tests/regression_tests/weightwindows/test.py +++ b/tests/regression_tests/weightwindows/test.py @@ -119,6 +119,61 @@ def test_weightwindows(shared_secondary, subdir): test.main() +def test_zero_bound_windows_play_no_game(tmp_path): + # A weight window lower bound of zero means no weight window information + # exists there (MCNP wwinp files use zero to turn the game off in a cell), + # so transport must proceed as if weight windows were disabled. Previously, + # zero-bound windows demanded a split at every checkpoint (weight/0 -> + # max_split), multiplying the particle population until terminated by the + # split or weight cutoff limits. + model = build_model(False) + for ww in model.settings.weight_windows: + ww.lower_ww_bounds = np.zeros_like(ww.lower_ww_bounds) + ww.upper_ww_bounds = np.zeros_like(ww.upper_ww_bounds) + sp_zero = model.run(cwd=tmp_path / 'zero_windows') + + model.settings.weight_windows_on = False + sp_off = model.run(cwd=tmp_path / 'windows_off') + + with openmc.StatePoint(sp_zero) as sp: + flux_zero = list(sp.tallies.values())[0].mean + with openmc.StatePoint(sp_off) as sp: + flux_off = list(sp.tallies.values())[0].mean + + np.testing.assert_allclose(flux_zero, flux_off, rtol=1e-12) + + +def test_zero_and_negative_bounds_equivalent(tmp_path): + # Zero and negative lower bounds both mean that no weight window + # information exists in a cell (generators mark such cells with -1, and + # MCNP wwinp files use zero), so they must produce identical transport. + # Unlike the all-zero case above, here particles are born under valid + # windows and encounter the no-information region in flight; previously a + # zero lower bound in that situation demanded a split at every checkpoint + # in the cell (weight/0 -> max_split), multiplying the particle population, + # while -1 played no game. + def run_with(bound_value, subdir): + model = build_model(False) + for ww in model.settings.weight_windows: + lb = np.array(ww.lower_ww_bounds, copy=True) + ub = np.array(ww.upper_ww_bounds, copy=True) + lb[3:, :, :, :] = bound_value + ub[3:, :, :, :] = bound_value + ww.lower_ww_bounds = lb + ww.upper_ww_bounds = ub + return model.run(cwd=tmp_path / subdir) + + sp_zero = run_with(0.0, 'zero_region') + sp_negative = run_with(-1.0, 'negative_region') + + with openmc.StatePoint(sp_zero) as sp: + flux_zero = list(sp.tallies.values())[0].mean + with openmc.StatePoint(sp_negative) as sp: + flux_negative = list(sp.tallies.values())[0].mean + + np.testing.assert_allclose(flux_zero, flux_negative, rtol=1e-12) + + def test_wwinp_cylindrical(): ww = openmc.WeightWindowsList.from_wwinp('ww_n_cyl.txt')[0] From 97e04c464a279500bc740a1151fef5c629796e89 Mon Sep 17 00:00:00 2001 From: Matteo Zammataro <103496190+MatteoZammataro@users.noreply.github.com> Date: Fri, 3 Jul 2026 01:51:10 +0200 Subject: [PATCH 647/671] Add ambient dose coefficients (H*(10)) from ICRP74 (#3256) Co-authored-by: matteo.zammataro Co-authored-by: Paul Romano --- openmc/data/dose/dose.py | 129 +++++++++++++++-------- openmc/data/dose/icrp74/neutrons_H10.txt | 50 +++++++++ openmc/data/dose/icrp74/photons_H10.txt | 28 +++++ tests/unit_tests/test_data_dose.py | 27 ++++- 4 files changed, 187 insertions(+), 47 deletions(-) create mode 100644 openmc/data/dose/icrp74/neutrons_H10.txt create mode 100644 openmc/data/dose/icrp74/photons_H10.txt diff --git a/openmc/data/dose/dose.py b/openmc/data/dose/dose.py index d49043b0a..87e0cf9ae 100644 --- a/openmc/data/dose/dose.py +++ b/openmc/data/dose/dose.py @@ -4,50 +4,76 @@ import numpy as np import openmc.checkvalue as cv -_FILES = { - ('icrp74', 'neutron'): Path('icrp74') / 'neutrons.txt', - ('icrp74', 'photon'): Path('icrp74') / 'photons.txt', - ('icrp116', 'electron'): Path('icrp116') / 'electrons.txt', - ('icrp116', 'helium'): Path('icrp116') / 'helium_ions.txt', - ('icrp116', 'mu-'): Path('icrp116') / 'negative_muons.txt', - ('icrp116', 'pi-'): Path('icrp116') / 'negative_pions.txt', - ('icrp116', 'neutron'): Path('icrp116') / 'neutrons.txt', - ('icrp116', 'photon'): Path('icrp116') / 'photons.txt', - ('icrp116', 'photon kerma'): Path('icrp116') / 'photons_kerma.txt', - ('icrp116', 'mu+'): Path('icrp116') / 'positive_muons.txt', - ('icrp116', 'pi+'): Path('icrp116') / 'positive_pions.txt', - ('icrp116', 'positron'): Path('icrp116') / 'positrons.txt', - ('icrp116', 'proton'): Path('icrp116') / 'protons.txt', +_FULL_GEOMETRIES = ('AP', 'PA', 'LLAT', 'RLAT', 'ROT', 'ISO') +_LIMITED_GEOMETRIES = ('AP', 'PA', 'ISO') + +_TABLES = { + ('icrp74', 'effective', 'neutron'): ( + Path('icrp74') / 'neutrons.txt', _FULL_GEOMETRIES), + ('icrp74', 'effective', 'photon'): ( + Path('icrp74') / 'photons.txt', _FULL_GEOMETRIES), + ('icrp116', 'effective', 'electron'): ( + Path('icrp116') / 'electrons.txt', _LIMITED_GEOMETRIES), + ('icrp116', 'effective', 'helium'): ( + Path('icrp116') / 'helium_ions.txt', _LIMITED_GEOMETRIES), + ('icrp116', 'effective', 'mu-'): ( + Path('icrp116') / 'negative_muons.txt', _LIMITED_GEOMETRIES), + ('icrp116', 'effective', 'pi-'): ( + Path('icrp116') / 'negative_pions.txt', _LIMITED_GEOMETRIES), + ('icrp116', 'effective', 'neutron'): ( + Path('icrp116') / 'neutrons.txt', _FULL_GEOMETRIES), + ('icrp116', 'effective', 'photon'): ( + Path('icrp116') / 'photons.txt', _FULL_GEOMETRIES), + ('icrp116', 'effective', 'photon kerma'): ( + Path('icrp116') / 'photons_kerma.txt', _FULL_GEOMETRIES), + ('icrp116', 'effective', 'mu+'): ( + Path('icrp116') / 'positive_muons.txt', _LIMITED_GEOMETRIES), + ('icrp116', 'effective', 'pi+'): ( + Path('icrp116') / 'positive_pions.txt', _LIMITED_GEOMETRIES), + ('icrp116', 'effective', 'positron'): ( + Path('icrp116') / 'positrons.txt', _LIMITED_GEOMETRIES), + ('icrp116', 'effective', 'proton'): ( + Path('icrp116') / 'protons.txt', _FULL_GEOMETRIES), + ('icrp74', 'ambient', 'neutron'): ( + Path('icrp74') / 'neutrons_H10.txt', None), + ('icrp74', 'ambient', 'photon'): ( + Path('icrp74') / 'photons_H10.txt', None), } _DOSE_TABLES = {} -def _load_dose_icrp(data_source: str, particle: str): - """Load effective dose tables from text files. +def _load_dose_table(data_source: str, dose_quantity: str, particle: str): + """Load dose tables from text files. Parameters ---------- data_source : {'icrp74', 'icrp116'} The dose conversion data source to use + dose_quantity : {'effective', 'ambient'} + Dose quantity to load. 'ambient' corresponds to ambient dose + equivalent H*(10). particle : {'neutron', 'photon', 'photon kerma', 'electron', 'positron'} Incident particle """ - path = Path(__file__).parent / _FILES[data_source, particle] + key = (data_source, dose_quantity, particle) + path = Path(__file__).parent / _TABLES[key][0] data = np.loadtxt(path, skiprows=3, encoding='utf-8') data[:, 0] *= 1e6 # Change energies to eV - _DOSE_TABLES[data_source, particle] = data + _DOSE_TABLES[key] = data -def dose_coefficients(particle, geometry='AP', data_source='icrp116'): - """Return effective dose conversion coefficients. +def dose_coefficients( + particle, geometry='AP', data_source='icrp116', dose_quantity='effective' +): + """Return dose conversion coefficients. - This function provides fluence (and air kerma) to effective or ambient dose - (H*(10)) conversion coefficients for various types of external exposures - based on values in ICRP publications. Corrected values found in a - corrigendum are used rather than the values in the original report. - Available libraries include `ICRP Publication 74 + This function provides fluence (and air kerma) to effective dose or ambient + dose equivalent (H*(10)) conversion coefficients for various types of + external exposures based on values in ICRP publications. Corrected values + found in a corrigendum are used rather than the values in the original + report. Available libraries include `ICRP Publication 74 ` and `ICRP Publication 116 `. @@ -63,45 +89,58 @@ def dose_coefficients(particle, geometry='AP', data_source='icrp116'): particle : {'neutron', 'photon', 'photon kerma', 'electron', 'positron'} Incident particle geometry : {'AP', 'PA', 'LLAT', 'RLAT', 'ROT', 'ISO'} - Irradiation geometry assumed. Refer to ICRP-116 (Section 3.2) for the - meaning of the options here. + Irradiation geometry assumed for effective dose coefficients. Refer to + ICRP-116 (Section 3.2) for the meaning of the options here. This + argument does not apply when ``dose_quantity`` is 'ambient'. data_source : {'icrp74', 'icrp116'} - The data source for the effective dose conversion coefficients. + The data source for the dose conversion coefficients. + dose_quantity : {'effective', 'ambient'} + Dose quantity to return. 'effective' returns effective dose + coefficients; 'ambient' returns ambient dose equivalent (H*(10)) + coefficients. Returns ------- energy : numpy.ndarray Energies at which dose conversion coefficients are given dose_coeffs : numpy.ndarray - Effective dose coefficients in [pSv cm^2] at provided energies. For - 'photon kerma', the coefficients are given in [Sv/Gy]. + Dose coefficients in [pSv cm^2] at provided energies. For 'photon + kerma', the coefficients are given in [Sv/Gy]. """ - cv.check_value('geometry', geometry, {'AP', 'PA', 'LLAT', 'RLAT', 'ROT', 'ISO'}) + cv.check_value('geometry', geometry, _FULL_GEOMETRIES) cv.check_value('data_source', data_source, {'icrp74', 'icrp116'}) + cv.check_value('dose_quantity', dose_quantity, {'effective', 'ambient'}) - if (data_source, particle) not in _FILES: - available_particles = sorted({p for (ds, p) in _FILES if ds == data_source}) + key = (data_source, dose_quantity, particle) + if key not in _TABLES: + available_particles = sorted( + p for ds, dq, p in _TABLES + if ds == data_source and dq == dose_quantity + ) msg = ( - f"'{particle}' has no dose data in data source {data_source}. " - f"Available particles for {data_source} are: {available_particles}" + f"'{particle}' has no {dose_quantity} dose data in data source " + f"{data_source}. Available particles for {data_source} " + f"with dose quantity {dose_quantity} are: {available_particles}" ) raise ValueError(msg) - elif (data_source, particle) not in _DOSE_TABLES: - _load_dose_icrp(data_source, particle) + elif key not in _DOSE_TABLES: + _load_dose_table(data_source, dose_quantity, particle) - # Get all data for selected particle - data = _DOSE_TABLES[data_source, particle] + data = _DOSE_TABLES[key] + columns = _TABLES[key][1] - # Determine index for selected geometry - if particle in ('neutron', 'photon', 'proton', 'photon kerma'): - columns = ('AP', 'PA', 'LLAT', 'RLAT', 'ROT', 'ISO') + if columns is None: + if geometry != 'AP': + raise ValueError( + "Irradiation geometry is not defined for ambient dose " + "equivalent coefficients. Use the default geometry='AP'." + ) + index = 0 else: - columns = ('AP', 'PA', 'ISO') - index = columns.index(geometry) + index = columns.index(geometry) - # Pull out energy and dose from table energy = data[:, 0].copy() dose_coeffs = data[:, index + 1].copy() return energy, dose_coeffs diff --git a/openmc/data/dose/icrp74/neutrons_H10.txt b/openmc/data/dose/icrp74/neutrons_H10.txt new file mode 100644 index 000000000..fe0036cbc --- /dev/null +++ b/openmc/data/dose/icrp74/neutrons_H10.txt @@ -0,0 +1,50 @@ +Neutrons: Ambient per fluence, in units of pSv cm², for monoenergetic particles incident. + +Energy (MeV) Dose + 1.00E-09 6.60 + 1.00E-08 9.00 + 2.53E-08 10.6 + 1.00E-07 12.9 + 2.00E-07 13.5 + 5.00E-07 13.6 + 1.00E-06 13.3 + 2.00E-06 12.9 + 5.00E-06 12.0 + 1.00E-05 11.3 + 2.00E-05 10.6 + 5.00E-05 9.90 + 1.00E-04 9.40 + 2.00E-04 8.90 + 5.00E-04 8.30 + 1.00E-03 7.90 + 2.00E-03 7.70 + 5.00E-03 8.00 + 1.00E-02 10.5 + 2.00E-02 16.6 + 3.00E-02 23.7 + 5.00E-02 41.1 + 7.00E-02 60.0 + 1.00E-01 88.0 + 1.50E-01 132 + 2.00E-01 170 + 3.00E-01 233 + 5.00E-01 322 + 7.00E-01 375 + 9.00E-01 400 + 1 416 + 1.2 425 + 2 420 + 3 412 + 4 408 + 5 405 + 6 400 + 7 405 + 8 409 + 9 420 + 10 440 + 12 480 + 14 520 + 15 540 + 16 555 + 18 570 + 20 600 \ No newline at end of file diff --git a/openmc/data/dose/icrp74/photons_H10.txt b/openmc/data/dose/icrp74/photons_H10.txt new file mode 100644 index 000000000..66031b2f0 --- /dev/null +++ b/openmc/data/dose/icrp74/photons_H10.txt @@ -0,0 +1,28 @@ +Photons: Ambient dose (H*10) per fluence, in units of pSv cm² + +Energy (MeV) Dose +0.010 0.061 +0.015 0.83 +0.020 1.05 +0.030 0.81 +0.040 0.64 +0.050 0.55 +0.060 0.51 +0.080 0.53 +0.100 0.61 +0.150 0.89 +0.200 1.20 +0.300 1.80 +0.400 2.38 +0.500 2.93 +0.600 3.44 +0.800 4.38 +1 5.20 +1.5 6.90 +2 8.60 +3 11.1 +4 13.4 +5 15.5 +6 17.6 +8 21.6 +10 25.6 \ No newline at end of file diff --git a/tests/unit_tests/test_data_dose.py b/tests/unit_tests/test_data_dose.py index 4f1880014..32be5eb89 100644 --- a/tests/unit_tests/test_data_dose.py +++ b/tests/unit_tests/test_data_dose.py @@ -34,6 +34,20 @@ def test_dose_coefficients(): assert energy[-1] == approx(20.0e6) assert dose[-1] == approx(338.0) + energy, dose = dose_coefficients( + 'neutron', data_source='icrp74', dose_quantity='ambient') + assert energy[0] == approx(1e-3) + assert dose[0] == approx(6.60) + assert energy[-1] == approx(20.0e6) + assert dose[-1] == approx(600) + + energy, dose = dose_coefficients( + 'photon', data_source='icrp74', dose_quantity='ambient') + assert energy[0] == approx(0.01e6) + assert dose[0] == approx(0.061) + assert energy[-1] == approx(10e6) + assert dose[-1] == approx(25.6) + # Invalid particle/geometry should raise an exception with raises(ValueError): dose_coefficients('slime', 'LAT') @@ -41,6 +55,14 @@ def test_dose_coefficients(): dose_coefficients('neutron', 'ZZ') with raises(ValueError): dose_coefficients('neutron', data_source='icrp7000') + with raises(ValueError): + dose_coefficients('neutron', dose_quantity='banana') + with raises(ValueError): + dose_coefficients( + 'neutron', data_source='icrp116', dose_quantity='ambient') + with raises(ValueError): + dose_coefficients( + 'neutron', 'ISO', data_source='icrp74', dose_quantity='ambient') with raises(ValueError) as excinfo: dose_coefficients("photons", data_source="icrp116") expected_particles = [ @@ -57,7 +79,8 @@ def test_dose_coefficients(): "proton", ] expected_msg = ( - "'photons' has no dose data in data source icrp116. " - f"Available particles for icrp116 are: {expected_particles}" + "'photons' has no effective dose data in data source icrp116. " + "Available particles for icrp116 with dose quantity effective are: " + f"{expected_particles}" ) assert str(excinfo.value) == expected_msg From 66359e5dd8382b81c508458fa64922100627fa89 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 3 Jul 2026 15:52:19 -0500 Subject: [PATCH 648/671] Fix surface tally crash on lattice crossings (#3993) Co-authored-by: GuySten --- src/particle.cpp | 3 +- tests/unit_tests/test_surface_flux.py | 44 +++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/particle.cpp b/src/particle.cpp index 779ae18da..65ff2ad61 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -338,13 +338,14 @@ void Particle::event_cross_surface() boundary().lattice_translation()[2] != 0) { // Particle crosses lattice boundary + int i_lattice = coord(boundary().coord_level() - 1).lattice(); bool verbose = settings::verbosity >= 10 || trace(); cross_lattice(*this, boundary(), verbose); event() = TallyEvent::LATTICE; // Score cell to cell partial currents if (!model::active_surface_tallies.empty()) { - auto& lat {*model::lattices[lowest_coord().lattice()]}; + auto& lat {*model::lattices[i_lattice]}; bool is_valid; Direction normal = lat.get_normal(boundary().lattice_translation(), is_valid); diff --git a/tests/unit_tests/test_surface_flux.py b/tests/unit_tests/test_surface_flux.py index 4e067ffe2..e4419252e 100644 --- a/tests/unit_tests/test_surface_flux.py +++ b/tests/unit_tests/test_surface_flux.py @@ -98,6 +98,50 @@ def test_surface_filter_flux_angled(two_cell_model, run_in_tmpdir): assert flux_mean == pytest.approx(1.0 / mu) +def test_surface_tally_during_lattice_crossing(run_in_tmpdir): + openmc.reset_auto_ids() + model = openmc.Model() + + xmin = openmc.XPlane(-1.0, boundary_type="vacuum") + xmax = openmc.XPlane(1.0, boundary_type="vacuum") + ymin = openmc.YPlane(-1.0, boundary_type="vacuum") + ymax = openmc.YPlane(1.0, boundary_type="vacuum") + zmin = openmc.ZPlane(-1.0, boundary_type="vacuum") + zmax = openmc.ZPlane(1.0, boundary_type="vacuum") + + inner_cell = openmc.Cell() + inner_univ = openmc.Universe(cells=[inner_cell]) + + tile_cell = openmc.Cell(fill=inner_univ) + tile_univ = openmc.Universe(cells=[tile_cell]) + + lattice = openmc.RectLattice() + lattice.lower_left = (-1.0, -1.0) + lattice.pitch = (1.0, 2.0) + lattice.universes = [[tile_univ, tile_univ]] + + root_cell = openmc.Cell( + fill=lattice, region=+xmin & -xmax & +ymin & -ymax & +zmin & -zmax) + model.geometry = openmc.Geometry([root_cell]) + + src = openmc.IndependentSource() + src.space = openmc.stats.Point((-0.5, 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 = 5 + model.settings.source = src + + current_tally = openmc.Tally() + current_tally.filters = [openmc.SurfaceFilter(xmax)] + current_tally.scores = ['current'] + model.tallies = [current_tally] + + model.run(apply_tally_results=True) + assert current_tally.mean.flat[0] == pytest.approx(1.0) + + 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 From e3bc615172ffa5ad825d4221c2f22edbe231fbba Mon Sep 17 00:00:00 2001 From: Matteo Zammataro <103496190+MatteoZammataro@users.noreply.github.com> Date: Fri, 3 Jul 2026 23:39:17 +0200 Subject: [PATCH 649/671] Fix numerical cancellation in RectLattice::distance for large pitch values (#3853) Co-authored-by: matteo.zammataro Co-authored-by: Paul Romano --- include/openmc/math_functions.h | 12 +++ src/lattice.cpp | 33 ++++++--- src/math_functions.cpp | 8 ++ tests/cpp_unit_tests/test_math.cpp | 21 ++++++ .../lattice_large_pitch/__init__.py | 0 .../lattice_large_pitch/inputs_true.dat | 51 +++++++++++++ .../lattice_large_pitch/results_true.dat | 73 +++++++++++++++++++ .../lattice_large_pitch/test.py | 70 ++++++++++++++++++ 8 files changed, 256 insertions(+), 12 deletions(-) create mode 100644 tests/regression_tests/lattice_large_pitch/__init__.py create mode 100644 tests/regression_tests/lattice_large_pitch/inputs_true.dat create mode 100644 tests/regression_tests/lattice_large_pitch/results_true.dat create mode 100644 tests/regression_tests/lattice_large_pitch/test.py diff --git a/include/openmc/math_functions.h b/include/openmc/math_functions.h index dcc0e21fe..f322ad28a 100644 --- a/include/openmc/math_functions.h +++ b/include/openmc/math_functions.h @@ -233,5 +233,17 @@ void get_energy_index( double standard_normal_cdf(double z); +//============================================================================== +//! Return true if two floating-point values are approximately equal within a +//! combined relative and absolute tolerance. +//! +//! \param a first floating point value +//! \param b second floating point value +//! \param rel_tol relative tolerance +//! \param abs_tol absolute tolerance +//! \return true if a and b are approximately equal, false otherwise +//============================================================================== +bool isclose(double a, double b, double rel_tol, double abs_tol); + } // namespace openmc #endif // OPENMC_MATH_FUNCTIONS_H diff --git a/src/lattice.cpp b/src/lattice.cpp index e799a340e..9e56c58e8 100644 --- a/src/lattice.cpp +++ b/src/lattice.cpp @@ -10,6 +10,7 @@ #include "openmc/geometry.h" #include "openmc/geometry_aux.h" #include "openmc/hdf5_interface.h" +#include "openmc/math_functions.h" #include "openmc/string_utils.h" #include "openmc/vector.h" #include "openmc/xml_interface.h" @@ -260,25 +261,33 @@ std::pair> RectLattice::distance( // Determine the oncoming edge. double x0 {copysign(0.5 * pitch_[0], u.x)}; double y0 {copysign(0.5 * pitch_[1], u.y)}; - double z0; - double d = std::min( - u.x != 0.0 ? (x0 - x) / u.x : INFTY, u.y != 0.0 ? (y0 - y) / u.y : INFTY); + // Evaluate the distance to each oncoming edge independently. Comparing these + // distances directly (rather than reconstructing the crossing position) + // avoids the floating-point cancellation that occurs for large pitches. + double dx = u.x != 0.0 ? (x0 - x) / u.x : INFTY; + double dy = u.y != 0.0 ? (y0 - y) / u.y : INFTY; + double dz = INFTY; if (is_3d_) { - z0 = copysign(0.5 * pitch_[2], u.z); - d = std::min(d, u.z != 0.0 ? (z0 - z) / u.z : INFTY); + double z0 {copysign(0.5 * pitch_[2], u.z)}; + dz = u.z != 0.0 ? (z0 - z) / u.z : INFTY; } - // Determine which lattice boundaries are being crossed + // The distance to the nearest lattice boundary is the smallest axial + // distance. + double d = std::min({dx, dy, dz}); + + // Determine which lattice boundaries are being crossed. The axis attaining + // the minimum is exactly equal to d, so at least one translation is always + // set for a finite crossing; a near-equal second axis indicates a corner + // crossing. array lattice_trans = {0, 0, 0}; - if (u.x != 0.0 && std::abs(x + u.x * d - x0) < FP_PRECISION) + if (isclose(d, dx, FP_COINCIDENT, FP_PRECISION)) lattice_trans[0] = copysign(1, u.x); - if (u.y != 0.0 && std::abs(y + u.y * d - y0) < FP_PRECISION) + if (isclose(d, dy, FP_COINCIDENT, FP_PRECISION)) lattice_trans[1] = copysign(1, u.y); - if (is_3d_) { - if (u.z != 0.0 && std::abs(z + u.z * d - z0) < FP_PRECISION) - lattice_trans[2] = copysign(1, u.z); - } + if (is_3d_ && isclose(d, dz, FP_COINCIDENT, FP_PRECISION)) + lattice_trans[2] = copysign(1, u.z); return {d, lattice_trans}; } diff --git a/src/math_functions.cpp b/src/math_functions.cpp index 81f08fa04..3befa8bc2 100644 --- a/src/math_functions.cpp +++ b/src/math_functions.cpp @@ -959,4 +959,12 @@ void get_energy_index( } } +// Return true if two floating-point values are approximately equal within a +// combined relative and absolute tolerance. +bool isclose(double a, double b, double rel_tol, double abs_tol) +{ + return std::abs(a - b) <= + std::max(rel_tol * std::max(std::abs(a), std::abs(b)), abs_tol); +} + } // namespace openmc diff --git a/tests/cpp_unit_tests/test_math.cpp b/tests/cpp_unit_tests/test_math.cpp index 1ad7c4b70..983322e4b 100644 --- a/tests/cpp_unit_tests/test_math.cpp +++ b/tests/cpp_unit_tests/test_math.cpp @@ -355,3 +355,24 @@ TEST_CASE("Test broaden_wmp_polynomials") REQUIRE_THAT(ref_val, Catch::Matchers::Approx(test_val)); } } + +TEST_CASE("Test isclose") +{ + using openmc::isclose; + + // Identical values are always close, regardless of tolerances. + REQUIRE(isclose(1.0, 1.0, 0.0, 0.0)); + REQUIRE(isclose(0.0, 0.0, 0.0, 0.0)); + + // Absolute tolerance governs comparisons near zero. + REQUIRE(isclose(0.0, 1e-15, 0.0, 1e-14)); + REQUIRE_FALSE(isclose(0.0, 1e-13, 0.0, 1e-14)); + + // Relative tolerance scales with the magnitude of the operands. + REQUIRE(isclose(1.0e12, 1.0e12 + 1.0, 1e-12, 0.0)); + REQUIRE_FALSE(isclose(1.0e12, 1.0e12 + 10.0, 1e-12, 0.0)); + + // The looser of the two tolerances wins. + REQUIRE(isclose(1.0, 1.0 + 1e-13, 0.0, 1e-12)); + REQUIRE(isclose(1.0e6, 1.0e6 + 1e-4, 1e-9, 0.0)); +} diff --git a/tests/regression_tests/lattice_large_pitch/__init__.py b/tests/regression_tests/lattice_large_pitch/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/lattice_large_pitch/inputs_true.dat b/tests/regression_tests/lattice_large_pitch/inputs_true.dat new file mode 100644 index 000000000..4dd4f0bd7 --- /dev/null +++ b/tests/regression_tests/lattice_large_pitch/inputs_true.dat @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + 100.0 100.0 + 2 + 3 3 + -150.0 -150.0 + +1 2 1 +2 1 2 +1 2 1 + + + + + + + + fixed source + 1000 + 10 + + + + 6 6 + -150.0 -150.0 + 150.0 150.0 + + + 1 + + + 1 + flux + + + diff --git a/tests/regression_tests/lattice_large_pitch/results_true.dat b/tests/regression_tests/lattice_large_pitch/results_true.dat new file mode 100644 index 000000000..58c4e306d --- /dev/null +++ b/tests/regression_tests/lattice_large_pitch/results_true.dat @@ -0,0 +1,73 @@ +tally 1: +1.749265E+01 +3.275925E+01 +4.159430E+01 +1.799568E+02 +7.027454E+01 +4.967568E+02 +6.789716E+01 +4.652258E+02 +4.272935E+01 +1.861129E+02 +2.073342E+01 +4.374279E+01 +4.028297E+01 +1.641642E+02 +6.734263E+01 +4.627171E+02 +1.037657E+02 +1.078682E+03 +1.108394E+02 +1.240927E+03 +7.236770E+01 +5.302034E+02 +4.356361E+01 +1.921990E+02 +6.731536E+01 +4.615977E+02 +9.850684E+01 +9.810272E+02 +5.164863E+02 +2.670336E+04 +5.097770E+02 +2.604770E+04 +1.064847E+02 +1.137536E+03 +7.052986E+01 +5.003220E+02 +7.065046E+01 +5.036723E+02 +1.044890E+02 +1.103250E+03 +5.121544E+02 +2.632389E+04 +5.065331E+02 +2.570752E+04 +1.044794E+02 +1.101249E+03 +6.569142E+01 +4.361120E+02 +4.739812E+01 +2.313289E+02 +7.402284E+01 +5.591689E+02 +1.066905E+02 +1.149521E+03 +1.038665E+02 +1.086813E+03 +7.160008E+01 +5.217744E+02 +4.219705E+01 +1.816100E+02 +2.089838E+01 +4.464899E+01 +4.154271E+01 +1.745866E+02 +7.081253E+01 +5.049997E+02 +6.673273E+01 +4.509038E+02 +4.184624E+01 +1.771546E+02 +1.853898E+01 +3.538608E+01 diff --git a/tests/regression_tests/lattice_large_pitch/test.py b/tests/regression_tests/lattice_large_pitch/test.py new file mode 100644 index 000000000..0f409badd --- /dev/null +++ b/tests/regression_tests/lattice_large_pitch/test.py @@ -0,0 +1,70 @@ +"""Regression test for rectangular lattices with large pitch values. + +Large pitches used to trigger a segmentation fault in ``RectLattice::distance`` +because the boundary-crossing check compared a reconstructed crossing position +against an absolute tolerance that did not scale with the geometry (see #3852). +This test transports particles across a lattice with a large pitch to ensure the +crossing logic remains robust. + +""" + +import openmc +import pytest + +from tests.testing_harness import PyAPITestHarness + + +@pytest.fixture +def model(): + model = openmc.Model() + + # Large pitch that previously caused floating-point cancellation + pitch = 100.0 + n = 3 + + air = openmc.Material() + air.set_density('g/cm3', 0.001) + air.add_nuclide('N14', 1.0) + metal = openmc.Material() + metal.set_density('g/cm3', 7.0) + metal.add_nuclide('Fe56', 1.0) + + metal_cell = openmc.Cell(fill=metal) + metal_uni = openmc.Universe(cells=[metal_cell]) + air_cell = openmc.Cell(fill=air) + air_uni = openmc.Universe(cells=[air_cell]) + + lattice = openmc.RectLattice() + lattice.lower_left = (-pitch*n/2, -pitch*n/2) + lattice.pitch = (pitch, pitch) + lattice.outer = air_uni + lattice.universes = [ + [metal_uni, air_uni, metal_uni], + [air_uni, metal_uni, air_uni], + [metal_uni, air_uni, metal_uni], + ] + + box = openmc.model.RectangularPrism(pitch*n, pitch*n, boundary_type='vacuum') + root_cell = openmc.Cell(region=-box, fill=lattice) + model.geometry = openmc.Geometry([root_cell]) + + model.settings.run_mode = 'fixed source' + model.settings.batches = 10 + model.settings.particles = 1000 + + mesh = openmc.RegularMesh() + mesh.dimension = (6, 6) + mesh.lower_left = (-pitch*n/2, -pitch*n/2) + mesh.upper_right = (pitch*n/2, pitch*n/2) + mesh_filter = openmc.MeshFilter(mesh) + tally = openmc.Tally(tally_id=1) + tally.filters = [mesh_filter] + tally.scores = ['flux'] + model.tallies = [tally] + + return model + + +def test_lattice_large_pitch(model): + harness = PyAPITestHarness('statepoint.10.h5', model) + harness.main() From 3fcb9692be2dd920e773c04bfab8151b5f773549 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Sat, 4 Jul 2026 22:20:03 -0500 Subject: [PATCH 650/671] Make sure output is treated consistently in R2SManager (#3994) --- openmc/deplete/microxs.py | 3 ++- openmc/deplete/r2s.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index 42bb958ca..7049f7d31 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -233,7 +233,8 @@ def get_microxs_and_flux( model.export_to_model_xml() comm.barrier() # Reinitialize with tallies - openmc.lib.init(intracomm=comm) + output = run_kwargs.get('output', True) if run_kwargs else True + openmc.lib.init(intracomm=comm, output=output) with TemporaryDirectory() as temp_dir: # Indicate to run in temporary directory unless being executed through diff --git a/openmc/deplete/r2s.py b/openmc/deplete/r2s.py index 6dbb3adb2..d41e230ee 100644 --- a/openmc/deplete/r2s.py +++ b/openmc/deplete/r2s.py @@ -347,7 +347,7 @@ class R2SManager: # Run neutron transport and get fluxes and micros. Run via openmc.lib to # maintain a consistent parallelism strategy with the activation step. - with TemporarySession(): + with TemporarySession(output=False): self.results['fluxes'], self.results['micros'] = get_microxs_and_flux( self.neutron_model, domains, **micro_kwargs) From 0c6b3fb83570a03d8217fd723ad8071c7704735c Mon Sep 17 00:00:00 2001 From: viktormai <127278660+viktormai@users.noreply.github.com> Date: Mon, 6 Jul 2026 12:47:22 -0500 Subject: [PATCH 651/671] Overlap detection for plotter (#3969) Co-authored-by: Paul Romano --- include/openmc/geometry.h | 38 +++++++- include/openmc/plot.h | 14 +-- openmc/lib/plot.py | 28 ++++++ src/geometry.cpp | 37 ++++++-- src/plot.cpp | 40 +++++++-- .../test_slice_data_overlap_info.py | 87 +++++++++++++++++++ 6 files changed, 225 insertions(+), 19 deletions(-) create mode 100644 tests/unit_tests/test_slice_data_overlap_info.py diff --git a/include/openmc/geometry.h b/include/openmc/geometry.h index 107cc7d1f..43cf9bb58 100644 --- a/include/openmc/geometry.h +++ b/include/openmc/geometry.h @@ -3,9 +3,12 @@ #include #include +#include +#include #include "openmc/array.h" #include "openmc/constants.h" +#include "openmc/random_ray/source_region.h" // For hash_combine #include "openmc/vector.h" namespace openmc { @@ -13,6 +16,34 @@ namespace openmc { class BoundaryInfo; class GeometryState; +//============================================================================== +//! OverlapKey to store cell and universe data of a single overlap, along with +//! a functor for hashing an OverlapKey into an unordered_map. +//============================================================================== + +struct OverlapKey { + int universe_id; + int cell1_id; + int cell2_id; + + bool operator==(const OverlapKey& other) const + { + return universe_id == other.universe_id && cell1_id == other.cell1_id && + cell2_id == other.cell2_id; + } +}; + +struct OverlapKeyHash { + std::size_t operator()(const OverlapKey& k) const + { + size_t seed = 0; + hash_combine(seed, k.universe_id); + hash_combine(seed, k.cell1_id); + hash_combine(seed, k.cell2_id); + return seed; + } +}; + //============================================================================== // Global variables //============================================================================== @@ -24,6 +55,10 @@ extern "C" int n_coord_levels; //!< Number of CSG coordinate levels extern vector overlap_check_count; +// Overlap data structures get cleared every slice_data run +extern vector overlap_keys; +extern std::unordered_map overlap_key_index; + } // namespace model //============================================================================== @@ -38,8 +73,7 @@ inline bool coincident(double d1, double d2) //============================================================================== //! Check for overlapping cells at a particle's position. //============================================================================== - -bool check_cell_overlap(GeometryState& p, bool error = true); +int check_cell_overlap(GeometryState& p, bool error = true); //============================================================================== //! Get the cell instance for a particle at the specified universe level diff --git a/include/openmc/plot.h b/include/openmc/plot.h index f97d31384..ba8ac84a1 100644 --- a/include/openmc/plot.h +++ b/include/openmc/plot.h @@ -5,6 +5,7 @@ #include #include #include +#include #include "openmc/tensor.h" #include "pugixml.hpp" @@ -155,7 +156,7 @@ struct IdData { // Methods void set_value(size_t y, size_t x, const Particle& p, int level, Filter* filter = nullptr, FilterMatch* match = nullptr); - void set_overlap(size_t y, size_t x); + void set_overlap(size_t y, size_t x, int overlap_idx); // Members tensor::Tensor data_; //!< 2D array of cell & material ids @@ -168,7 +169,7 @@ struct PropertyData { // Methods void set_value(size_t y, size_t x, const Particle& p, int level, Filter* filter = nullptr, FilterMatch* match = nullptr); - void set_overlap(size_t y, size_t x); + void set_overlap(size_t y, size_t x, int overlap_idx); // Members tensor::Tensor data_; //!< 2D array of temperature & density data @@ -181,7 +182,7 @@ struct RasterData { // Methods void set_value(size_t y, size_t x, const Particle& p, int level, Filter* filter = nullptr, FilterMatch* match = nullptr); - void set_overlap(size_t y, size_t x); + void set_overlap(size_t y, size_t x, int overlap_idx); // Members tensor::Tensor @@ -278,8 +279,11 @@ T SlicePlotBase::get_map(int32_t filter_index) const if (found_cell) { data.set_value(y, x, p, j, filter, &match); } - if (show_overlaps_ && check_cell_overlap(p, false)) { - data.set_overlap(y, x); + if (show_overlaps_) { + int overlap_idx = check_cell_overlap(p, false); + if (overlap_idx >= 0) { + data.set_overlap(y, x, overlap_idx); + } } } // inner for } diff --git a/openmc/lib/plot.py b/openmc/lib/plot.py index 44d6ac273..0f2e6c19a 100644 --- a/openmc/lib/plot.py +++ b/openmc/lib/plot.py @@ -255,6 +255,34 @@ def property_map(plot): return prop_data +_dll.openmc_slice_data_overlap_count.argtypes = [POINTER(c_size_t)] +_dll.openmc_slice_data_overlap_count.restype = c_int +_dll.openmc_slice_data_overlap_count.errcheck = _error_handler + +_dll.openmc_slice_data_overlap_info.argtypes = [c_size_t, POINTER(c_int32)] +_dll.openmc_slice_data_overlap_info.restype = c_int +_dll.openmc_slice_data_overlap_info.errcheck = _error_handler + + +# Python wrappings for overlap functions +def slice_data_overlap_count(): + count = c_size_t() + _dll.openmc_slice_data_overlap_count(count) + return count.value + + +def slice_data_overlap_info(): + n = slice_data_overlap_count() + overlap_info = np.empty(n * 3, dtype=np.int32) + + if n > 0: + _dll.openmc_slice_data_overlap_info( + n, + overlap_info.ctypes.data_as(POINTER(c_int32)), + ) + return overlap_info, n + + _dll.openmc_get_plot_index.argtypes = [c_int32, POINTER(c_int32)] _dll.openmc_get_plot_index.restype = c_int _dll.openmc_get_plot_index.errcheck = _error_handler diff --git a/src/geometry.cpp b/src/geometry.cpp index ddb61385f..bf654f4f9 100644 --- a/src/geometry.cpp +++ b/src/geometry.cpp @@ -26,16 +26,22 @@ int n_coord_levels; vector overlap_check_count; +vector overlap_keys; +std::unordered_map overlap_key_index; + } // namespace model //============================================================================== // Non-member functions //============================================================================== -bool check_cell_overlap(GeometryState& p, bool error) +int check_cell_overlap(GeometryState& p, bool error) { int n_coord = p.n_coord(); + // If no overlap found, return a nonphysical index + int overlap_index = -1; + // Loop through each coordinate level for (int j = 0; j < n_coord; j++) { Universe& univ = *model::universes[p.coord(j).universe()]; @@ -44,21 +50,40 @@ bool check_cell_overlap(GeometryState& p, bool error) for (auto index_cell : univ.cells_) { Cell& c = *model::cells[index_cell]; if (c.contains(p.coord(j).r(), p.coord(j).u(), p.surface())) { +#pragma omp atomic + ++model::overlap_check_count[index_cell]; if (index_cell != p.coord(j).cell()) { if (error) { fatal_error( fmt::format("Overlapping cells detected: {}, {} on universe {}", c.id_, model::cells[p.coord(j).cell()]->id_, univ.id_)); } - return true; + + // With no fatal error (plotter is calling), now adds overlaps and + // ensures order does not matter when making overlap key + int cell_a = model::cells[index_cell]->id_; + int cell_b = model::cells[p.coord(j).cell()]->id_; + int a = std::min(cell_a, cell_b); + int b = std::max(cell_a, cell_b); + OverlapKey key {univ.id_, a, b}; +#pragma omp critical(overlap_key_update) + { + auto it = model::overlap_key_index.find(key); + if (it != model::overlap_key_index.end()) { + overlap_index = it->second; // already exists, reuse index + } else { + int idx = int(model::overlap_keys.size()); + model::overlap_keys.push_back(key); + model::overlap_key_index[key] = idx; + overlap_index = idx; + } + } + break; } -#pragma omp atomic - ++model::overlap_check_count[index_cell]; } } } - - return false; + return overlap_index; } //============================================================================== diff --git a/src/plot.cpp b/src/plot.cpp index b9a1136af..d26589a79 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -73,7 +73,7 @@ void IdData::set_value(size_t y, size_t x, const Particle& p, int level, } } -void IdData::set_overlap(size_t y, size_t x) +void IdData::set_overlap(size_t y, size_t x, int /*overlap_idx*/) { for (size_t k = 0; k < data_.shape(2); ++k) data_(y, x, k) = OVERLAP; @@ -94,7 +94,7 @@ void PropertyData::set_value(size_t y, size_t x, const Particle& p, int level, } } -void PropertyData::set_overlap(size_t y, size_t x) +void PropertyData::set_overlap(size_t y, size_t x, int /*overlap_idx*/) { data_(y, x) = OVERLAP; } @@ -154,12 +154,12 @@ void RasterData::set_value(size_t y, size_t x, const Particle& p, int level, } } -void RasterData::set_overlap(size_t y, size_t x) +void RasterData::set_overlap(size_t y, size_t x, int overlap_idx) { // Set cell, instance, and material to OVERLAP, but preserve filter bin id_data_(y, x, 0) = OVERLAP; id_data_(y, x, 1) = OVERLAP; - id_data_(y, x, 2) = OVERLAP; + id_data_(y, x, 2) = OVERLAP - overlap_idx - 1; // Note: id_data_(y, x, 3) is NOT overwritten - preserves filter bin for tally // plotting @@ -1991,10 +1991,12 @@ extern "C" int openmc_slice_data(const double origin[3], const double u_span[3], plot_params.show_overlaps_ = color_overlaps; plot_params.slice_level_ = level; + // Clear overlap data structures on new slice call + model::overlap_keys.clear(); + model::overlap_key_index.clear(); + // Use get_map to generate data auto data = plot_params.get_map(filter_index); - - // Copy geometry data std::copy(data.id_data_.begin(), data.id_data_.end(), geom_data); // Copy property data if requested @@ -2010,6 +2012,32 @@ extern "C" int openmc_slice_data(const double origin[3], const double u_span[3], return 0; } +// Gets the number of overlaps that we need data for +extern "C" int openmc_slice_data_overlap_count(size_t* count) +{ + if (!count) { + set_errmsg("Null pointer passed for overlap count."); + return OPENMC_E_INVALID_ARGUMENT; + } + *count = model::overlap_keys.size(); + + return 0; +} + +// Plotter pre-allocates array size based on what is returned with +// overlap_count; populates an array of size 3*count +extern "C" int openmc_slice_data_overlap_info( + size_t count, int32_t* overlap_info) +{ + for (size_t i = 0; i < count; ++i) { + overlap_info[i * 3] = model::overlap_keys[i].universe_id; + overlap_info[i * 3 + 1] = model::overlap_keys[i].cell1_id; + overlap_info[i * 3 + 2] = model::overlap_keys[i].cell2_id; + } + + return 0; +} + extern "C" int openmc_get_plot_index(int32_t id, int32_t* index) { auto it = model::plot_map.find(id); diff --git a/tests/unit_tests/test_slice_data_overlap_info.py b/tests/unit_tests/test_slice_data_overlap_info.py new file mode 100644 index 000000000..c8e415002 --- /dev/null +++ b/tests/unit_tests/test_slice_data_overlap_info.py @@ -0,0 +1,87 @@ +import pytest +import numpy as np +import openmc +import openmc.lib + +# Sentinel value matching _OVERLAP in plotmodel.py and OVERLAP in plot.cpp +_OVERLAP = -3 + + +@pytest.fixture(scope='module') +def overlap_model(): + openmc.reset_auto_ids() + + # Three cylinders: cyl1 and cyl2 overlap near x=0, cyl2 and cyl3 overlap + # near x=4. This gives us two spatially distinct overlap regions in one model. + mat1 = openmc.Material(components={'H1': 1.0}) + mat2 = openmc.Material(components={'H1': 1.0}) + mat3 = openmc.Material(components={'H1': 1.0}) + + # cyl1 and cyl2 overlap on the left, cyl2 and cyl3 overlap on the right + cyl1 = openmc.ZCylinder(x0=-2.0, r=2.5) + cyl2 = openmc.ZCylinder(x0=0.0, r=2.5) + cyl3 = openmc.ZCylinder(x0=2.0, r=2.5) + boundary = openmc.Sphere(r=20.0, boundary_type='vacuum') + cell1 = openmc.Cell(region=-cyl1, fill=mat1) + cell2 = openmc.Cell(region=-cyl2, fill=mat2) + cell3 = openmc.Cell(region=-cyl3, fill=mat3) + cell_outside = openmc.Cell(region=+cyl1 & +cyl2 & +cyl3 & -boundary) + geometry = openmc.Geometry([cell1, cell2, cell3, cell_outside]) + + settings = openmc.Settings() + settings.run_mode = 'fixed source' + settings.particles = 100 + settings.batches = 1 + model = openmc.Model(geometry=geometry, settings=settings) + + with openmc.lib.TemporarySession(model, args=['-s', '1']): + yield + + +def run_slice(origin=(0.0, 0.0, 0.0), width=(10.0, 6.0), show_overlaps=True): + # Helper that runs a slice over a region covering both overlap zones + geom_data, _ = openmc.lib.slice_data( + origin=origin, + width=width, + basis='xy', + pixels=(100, 60), + show_overlaps=show_overlaps, + include_properties=False, + ) + return geom_data + + +def test_overlaps_enabled(overlap_model): + # Run a single slice with overlap detection enabled and check all + # expected properties in one pass. + geom_data = run_slice() + overlap_info, n = openmc.lib.slice_data_overlap_info() + mat_ids = geom_data[:, :, 2] + + # mat_ids should contain values more negative than _OVERLAP; RasterData + # encodes each unique overlap as OVERLAP - overlap_idx - 1 into slot 2. + assert np.any(mat_ids < _OVERLAP) + + # overlap_keys should have 2 entries for the two distinct overlapping + # cylinder pairs in this model. + assert n == 2, f"Expected exactly 2 overlap entries, got {n}" + + # Each entry is a (universe_id, cell1_id, cell2_id) triple; verify values. + for i in range(n): + universe_id = int(overlap_info[i * 3]) + cell1_id = int(overlap_info[i * 3 + 1]) + cell2_id = int(overlap_info[i * 3 + 2]) + assert universe_id == 1 + assert cell1_id in {1, 2, 3} + assert cell2_id in {1, 2, 3} + assert cell1_id != cell2_id + + +def test_overlaps_disabled(overlap_model): + # With show_overlaps=False, set_overlap is never called and overlap_keys + # is never written to, so the image and map should both be clean. + geom_data = run_slice(show_overlaps=False) + _, n = openmc.lib.slice_data_overlap_info() + + assert not np.any(geom_data[:, :, 2] < _OVERLAP) + assert n == 0 From 8684506269d29f129a28702a519467af8524ecb4 Mon Sep 17 00:00:00 2001 From: Andrew Davis Date: Tue, 7 Jul 2026 15:58:19 +0100 Subject: [PATCH 652/671] This fixes compile isuees found with GCC 16.1.1 and FMT version (#4000) Co-authored-by: Paul Romano --- include/openmc/error.h | 4 ++-- src/distribution_spatial.cpp | 8 +++++--- src/weight_windows.cpp | 3 ++- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/include/openmc/error.h b/include/openmc/error.h index d73795aee..1f6e15f49 100644 --- a/include/openmc/error.h +++ b/include/openmc/error.h @@ -64,14 +64,14 @@ void write_message( int level, const std::string& message, const Params&... fmt_args) { if (settings::verbosity >= level) { - write_message(fmt::format(message, fmt_args...)); + write_message(fmt::format(fmt::runtime(message), fmt_args...)); } } template void write_message(const std::string& message, const Params&... fmt_args) { - write_message(fmt::format(message, fmt_args...)); + write_message(fmt::format(fmt::runtime(message), fmt_args...)); } #ifdef OPENMC_MPI diff --git a/src/distribution_spatial.cpp b/src/distribution_spatial.cpp index ba8658f10..5a3452422 100644 --- a/src/distribution_spatial.cpp +++ b/src/distribution_spatial.cpp @@ -262,9 +262,11 @@ std::pair SphericalIndependent::sample(uint64_t* seed) const MeshSpatial::MeshSpatial(pugi::xml_node node) { - if (get_node_value(node, "type", true, true) != "mesh") { - fatal_error(fmt::format( - "Incorrect spatial type '{}' for a MeshSpatial distribution")); + auto spatial_type = get_node_value(node, "type", true, true); + if (spatial_type != "mesh") { + fatal_error( + fmt::format("Incorrect spatial type '{}' for a MeshSpatial distribution", + spatial_type)); } // No in-tet distributions implemented, could include distributions for the diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index 39e46026c..0d268ce17 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -837,7 +837,8 @@ WeightWindowsGenerator::WeightWindowsGenerator(pugi::xml_node node) ratio_)); if (ratio_ <= 1.0) fatal_error(fmt::format("Invalid weight window ratio '{}' (<= 1.0) " - "specified for weight window generation")); + "specified for weight window generation", + ratio_)); // create a matching weight windows object auto wws = WeightWindows::create(); From 8b15ee3915dc59ff51742d0307181d2303512a6a Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 8 Jul 2026 11:25:21 -0500 Subject: [PATCH 653/671] Add Jon Shimwell to technical committee (#4001) --- docs/source/devguide/contributing.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/devguide/contributing.rst b/docs/source/devguide/contributing.rst index cda031389..464bff2aa 100644 --- a/docs/source/devguide/contributing.rst +++ b/docs/source/devguide/contributing.rst @@ -112,6 +112,7 @@ The TC consists of the following individuals: - `Patrick Shriwise `_ - `Adam Nelson `_ - `Benoit Forget `_ +- `Jonathan Shimwell `_ The Project Lead is Paul Romano. From 3cdb67e50d0724f672f1a4227dc2a5ab5a71e062 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 9 Jul 2026 04:47:55 -0500 Subject: [PATCH 654/671] Store slice overlap indices in cell ID field (#4002) --- openmc/lib/plot.py | 26 ++++++++++++++++--- src/plot.cpp | 10 +++---- .../test_slice_data_overlap_info.py | 18 +++++++------ 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/openmc/lib/plot.py b/openmc/lib/plot.py index 0f2e6c19a..471ae688b 100644 --- a/openmc/lib/plot.py +++ b/openmc/lib/plot.py @@ -265,22 +265,40 @@ _dll.openmc_slice_data_overlap_info.errcheck = _error_handler # Python wrappings for overlap functions -def slice_data_overlap_count(): +def slice_data_overlap_count() -> int: + """Return the number of unique overlaps from the last slice plot. + + Returns + ------- + int + Number of unique overlapping cell pairs detected by the most recent + :func:`slice_data` call with overlap checking enabled. + """ count = c_size_t() _dll.openmc_slice_data_overlap_count(count) return count.value -def slice_data_overlap_info(): +def slice_data_overlap_info() -> np.ndarray: + """Return identifying information for overlaps from the last slice plot. + + Returns + ------- + numpy.ndarray + Array of shape ``(n, 3)`` with int32 dtype, where ``n`` is the number + of unique overlaps detected by the most recent :func:`slice_data` call + with overlap checking enabled. Each row contains ``[universe_id, + cell1_id, cell2_id]``. + """ n = slice_data_overlap_count() - overlap_info = np.empty(n * 3, dtype=np.int32) + overlap_info = np.empty((n, 3), dtype=np.int32) if n > 0: _dll.openmc_slice_data_overlap_info( n, overlap_info.ctypes.data_as(POINTER(c_int32)), ) - return overlap_info, n + return overlap_info _dll.openmc_get_plot_index.argtypes = [c_int32, POINTER(c_int32)] diff --git a/src/plot.cpp b/src/plot.cpp index d26589a79..06c2b550a 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -156,12 +156,12 @@ void RasterData::set_value(size_t y, size_t x, const Particle& p, int level, void RasterData::set_overlap(size_t y, size_t x, int overlap_idx) { - // Set cell, instance, and material to OVERLAP, but preserve filter bin - id_data_(y, x, 0) = OVERLAP; + // Set cell, instance, and material to OVERLAP, but preserve filter bin for + // tally plotting. Cell encodes the overlap index as a negative number so that + // it can be used to look up overlap information in the plotter. + id_data_(y, x, 0) = OVERLAP - overlap_idx - 1; id_data_(y, x, 1) = OVERLAP; - id_data_(y, x, 2) = OVERLAP - overlap_idx - 1; - // Note: id_data_(y, x, 3) is NOT overwritten - preserves filter bin for tally - // plotting + id_data_(y, x, 2) = OVERLAP; property_data_(y, x, 0) = OVERLAP; property_data_(y, x, 1) = OVERLAP; diff --git a/tests/unit_tests/test_slice_data_overlap_info.py b/tests/unit_tests/test_slice_data_overlap_info.py index c8e415002..e5f08245d 100644 --- a/tests/unit_tests/test_slice_data_overlap_info.py +++ b/tests/unit_tests/test_slice_data_overlap_info.py @@ -55,12 +55,13 @@ def test_overlaps_enabled(overlap_model): # Run a single slice with overlap detection enabled and check all # expected properties in one pass. geom_data = run_slice() - overlap_info, n = openmc.lib.slice_data_overlap_info() - mat_ids = geom_data[:, :, 2] + overlap_info = openmc.lib.slice_data_overlap_info() + n = overlap_info.shape[0] + cell_ids = geom_data[:, :, 0] - # mat_ids should contain values more negative than _OVERLAP; RasterData + # cell_ids should contain values more negative than _OVERLAP; RasterData # encodes each unique overlap as OVERLAP - overlap_idx - 1 into slot 2. - assert np.any(mat_ids < _OVERLAP) + assert np.any(cell_ids < _OVERLAP) # overlap_keys should have 2 entries for the two distinct overlapping # cylinder pairs in this model. @@ -68,9 +69,9 @@ def test_overlaps_enabled(overlap_model): # Each entry is a (universe_id, cell1_id, cell2_id) triple; verify values. for i in range(n): - universe_id = int(overlap_info[i * 3]) - cell1_id = int(overlap_info[i * 3 + 1]) - cell2_id = int(overlap_info[i * 3 + 2]) + universe_id = int(overlap_info[i, 0]) + cell1_id = int(overlap_info[i, 1]) + cell2_id = int(overlap_info[i, 2]) assert universe_id == 1 assert cell1_id in {1, 2, 3} assert cell2_id in {1, 2, 3} @@ -81,7 +82,8 @@ def test_overlaps_disabled(overlap_model): # With show_overlaps=False, set_overlap is never called and overlap_keys # is never written to, so the image and map should both be clean. geom_data = run_slice(show_overlaps=False) - _, n = openmc.lib.slice_data_overlap_info() + overlap_info = openmc.lib.slice_data_overlap_info() + n = overlap_info.shape[0] assert not np.any(geom_data[:, :, 2] < _OVERLAP) assert n == 0 From 5203640549e690672c1bea3a1aa9ebe6c4f47121 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Thu, 9 Jul 2026 23:55:32 +0200 Subject: [PATCH 655/671] Further input validation for plot methods (#4004) --- openmc/model/model.py | 14 +++++++++++++- tests/unit_tests/test_model.py | 21 +++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index 328494288..b166137f2 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -21,7 +21,8 @@ import openmc import openmc._xml as xml from openmc.dummy_comm import DummyCommunicator from openmc.executor import _process_CLI_arguments -from openmc.checkvalue import check_type, check_value, PathLike +from openmc.checkvalue import (check_type, check_value, check_greater_than, + check_length, PathLike) from openmc.exceptions import InvalidIDError from openmc.plots import add_plot_params, _BASIS_INDICES, id_map_to_rgb from openmc.utility_funcs import change_directory @@ -1050,6 +1051,13 @@ class Model: pixels: int | Sequence[int], basis: str ): + if isinstance(pixels, int): + check_greater_than('pixels', pixels, 0) + else: + check_length('pixels', pixels, 2) + for p in pixels: + check_greater_than('pixels', p, 0) + x, y, _ = _BASIS_INDICES[basis] bb = self.bounding_box @@ -1301,7 +1309,11 @@ class Model: import matplotlib.pyplot as plt check_type('n_samples', n_samples, int | None) + if n_samples is not None: + check_greater_than('n_samples', n_samples, 0, equality=True) check_type('plane_tolerance', plane_tolerance, Real) + check_greater_than('plane_tolerance', plane_tolerance, 0.0) + if legend_kwargs is None: legend_kwargs = {} legend_kwargs.setdefault('bbox_to_anchor', (1.05, 1)) diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 028a83b0b..52ac43117 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -660,6 +660,27 @@ def test_model_plot(): plt.close('all') +def test_model_plot_invalid_inputs(): + surface = openmc.Sphere(r=10.0, boundary_type="vacuum") + cell = openmc.Cell(region=-surface) + model = openmc.Model(openmc.Geometry([cell])) + + with pytest.raises(ValueError): + model.plot(n_samples=-1) + with pytest.raises(TypeError): + model.plot(n_samples=1.5) + with pytest.raises(ValueError): + model.plot(plane_tolerance=0.0) + with pytest.raises(TypeError): + model.plot(plane_tolerance='1') + with pytest.raises(ValueError): + model.plot(pixels=-1) + with pytest.raises(ValueError): + model.plot(pixels=(0, 100)) + with pytest.raises(ValueError): + model.plot(pixels=(100,)) + + def test_model_id_map_initialization(run_in_tmpdir): model = openmc.examples.pwr_assembly() model.init_lib(output=False) From e73d8048da30ad15ebf0afc0ad02bbe6a26eec22 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Fri, 10 Jul 2026 01:45:09 +0300 Subject: [PATCH 656/671] Support cell densities per instances in plots (#4006) Co-authored-by: Paul Romano --- openmc/lib/plot.py | 10 ++++++++-- src/plot.cpp | 7 ++----- tests/unit_tests/test_lib.py | 16 ++++++++++++++++ 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/openmc/lib/plot.py b/openmc/lib/plot.py index 471ae688b..196682394 100644 --- a/openmc/lib/plot.py +++ b/openmc/lib/plot.py @@ -87,7 +87,7 @@ _dll.openmc_slice_data.errcheck = _error_handler def slice_data(origin, width=None, basis='xy', u_span=None, v_span=None, - pixels=None, show_overlaps=False, level=-1, filter=None, + pixels=None, show_overlaps=False, level=None, filter=None, include_properties=True): """Generate a 2D raster of geometry and property data for plotting. @@ -111,7 +111,7 @@ def slice_data(origin, width=None, basis='xy', u_span=None, v_span=None, show_overlaps : bool, optional Whether to detect overlapping cells level : int, optional - Universe level (-1 for deepest) + Universe level (None for deepest) filter : openmc.lib.Filter, optional Filter for bin index lookup include_properties : bool, optional @@ -127,6 +127,12 @@ def slice_data(origin, width=None, basis='xy', u_span=None, v_span=None, Array of shape (v_res, h_res, 2) with float64 dtype containing [temperature, density], or None if include_properties=False """ + # Set deepest level as default + if level is None: + level = -1 + if not isinstance(level, int): + raise TypeError("level must be an integer.") + if pixels is None: raise ValueError("pixels must be specified.") if len(pixels) != 2: diff --git a/src/plot.cpp b/src/plot.cpp index 06c2b550a..96df1c5be 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -88,10 +88,7 @@ void PropertyData::set_value(size_t y, size_t x, const Particle& p, int level, { Cell* c = model::cells.at(p.lowest_coord().cell()).get(); data_(y, x, 0) = (p.sqrtkT() * p.sqrtkT()) / K_BOLTZMANN; - if (c->type_ != Fill::UNIVERSE && p.material() != MATERIAL_VOID) { - Material* m = model::materials.at(p.material()).get(); - data_(y, x, 1) = m->density_gpcc_; - } + data_(y, x, 1) = c->density(p.cell_instance()); } void PropertyData::set_overlap(size_t y, size_t x, int /*overlap_idx*/) @@ -150,7 +147,7 @@ void RasterData::set_value(size_t y, size_t x, const Particle& p, int level, // set density (g/cm³) if (c->type_ != Fill::UNIVERSE && p.material() != MATERIAL_VOID) { Material* m = model::materials.at(p.material()).get(); - property_data_(y, x, 1) = m->density_gpcc_; + property_data_(y, x, 1) = c->density(p.cell_instance()); } } diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index f62306e83..4cfae0df2 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -926,6 +926,22 @@ def test_property_map(lib_init): assert np.allclose(expected_properties, properties, atol=1e-04) +def test_slice_data(lib_init): + expected_properties = np.array( + [[(293.6, 0.740582), (293.6, 6.55), (293.6, 0.740582)], + [ (293.6, 6.55), (293.6, 10.29769), (293.6, 6.55)], + [(293.6, 0.740582), (293.6, 6.55), (293.6, 0.740582)]], dtype='float') + origin = (0.0, 0.0, 0.0) + _, properties = openmc.lib.slice_data( + origin, + width=(1.26, 1.26), + basis='xy', + pixels=(3, 3), + include_properties=True + ) + assert np.allclose(expected_properties, properties, atol=1e-04) + + def test_solid_raytrace_plot(lib_init, pincell_model): # Ensure plot mapping can be accessed and grows after allocation n0 = len(openmc.lib.plots) From 3c3ebba98b491b9c07f1ec60a67d46bfc4c2d8de Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 9 Jul 2026 18:59:33 -0500 Subject: [PATCH 657/671] Turn on weight windows if `weight_windows_file` is specified (#4007) --- src/settings.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/settings.cpp b/src/settings.cpp index 58bced940..15a7b9c27 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -1247,6 +1247,7 @@ void read_settings_xml(pugi::xml_node root) // read weight windows from file if (check_for_node(root, "weight_windows_file")) { weight_windows_file = get_node_value(root, "weight_windows_file"); + weight_windows_on = true; } // read settings for weight windows value, this will override From 7c408f6a100a8885b85df10df19589a1a9b1dce8 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 10 Jul 2026 02:48:11 -0500 Subject: [PATCH 658/671] Replace Ben Forget with John Tramm on technical committee (#4008) --- docs/source/devguide/contributing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/devguide/contributing.rst b/docs/source/devguide/contributing.rst index 464bff2aa..5199c9438 100644 --- a/docs/source/devguide/contributing.rst +++ b/docs/source/devguide/contributing.rst @@ -111,8 +111,8 @@ The TC consists of the following individuals: - `Paul Romano `_ - `Patrick Shriwise `_ - `Adam Nelson `_ -- `Benoit Forget `_ - `Jonathan Shimwell `_ +- `John Tramm `_ The Project Lead is Paul Romano. From 216651b69edfb8c012aa4812b094a1c8a53f36c9 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 10 Jul 2026 09:22:16 -0500 Subject: [PATCH 659/671] Shared secondary bank performance optimizations (#3995) --- include/openmc/particle_data.h | 6 +-- src/particle.cpp | 25 +++++++++--- src/random_lcg.cpp | 72 +++++++++++++++++++++------------- src/simulation.cpp | 17 +++++--- 4 files changed, 77 insertions(+), 43 deletions(-) diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index f72948f6e..44e82fd23 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -582,10 +582,8 @@ public: // Methods and accessors // Cross section caches - NuclideMicroXS& neutron_xs(int i) - { - return neutron_xs_[i]; - } // Microscopic neutron cross sections + // Microscopic neutron cross sections + NuclideMicroXS& neutron_xs(int i) { return neutron_xs_[i]; } const NuclideMicroXS& neutron_xs(int i) const { return neutron_xs_[i]; } // Microscopic photon cross sections diff --git a/src/particle.cpp b/src/particle.cpp index 65ff2ad61..10e8f213d 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -554,15 +554,30 @@ void Particle::event_death() finalize_particle_track(*this); } -// Contribute tally reduction variables to global accumulator + // Contribute tally reduction variables to global accumulator + const auto k_absorption = keff_tally_absorption(); + const auto k_collision = keff_tally_collision(); + const auto k_tracklength = keff_tally_tracklength(); + const auto leakage = keff_tally_leakage(); + + if (settings::run_mode == RunMode::EIGENVALUE) { + if (k_absorption != 0.0) { #pragma omp atomic - global_tally_absorption += keff_tally_absorption(); + global_tally_absorption += k_absorption; + } + if (k_collision != 0.0) { #pragma omp atomic - global_tally_collision += keff_tally_collision(); + global_tally_collision += k_collision; + } + if (k_tracklength != 0.0) { #pragma omp atomic - global_tally_tracklength += keff_tally_tracklength(); + global_tally_tracklength += k_tracklength; + } + } + if (leakage != 0.0) { #pragma omp atomic - global_tally_leakage += keff_tally_leakage(); + global_tally_leakage += leakage; + } // Reset particle tallies once accumulated keff_tally_absorption() = 0.0; diff --git a/src/random_lcg.cpp b/src/random_lcg.cpp index 29457569b..f2a81fc1c 100644 --- a/src/random_lcg.cpp +++ b/src/random_lcg.cpp @@ -12,6 +12,45 @@ constexpr uint64_t prn_mult {6364136223846793005ULL}; // multiplication constexpr uint64_t prn_add {1442695040888963407ULL}; // additive factor, c uint64_t prn_stride {DEFAULT_STRIDE}; // stride between particles +namespace { + +struct SkipAheadCoefficients { + uint64_t multiplier; + uint64_t increment; +}; + +SkipAheadCoefficients future_seed_coefficients(uint64_t n) +{ + // The algorithm here to determine the parameters used to skip ahead is + // described in F. Brown, "Random Number Generation with Arbitrary Stride," + // Trans. Am. Nucl. Soc. (Nov. 1994). This algorithm is able to skip ahead in + // O(log2(N)) operations instead of O(N). Basically, it computes parameters G + // and C which can then be used to find x_N = G*x_0 + C mod 2^M. + + // Initialize constants + uint64_t g {prn_mult}; + uint64_t c {prn_add}; + uint64_t g_new {1}; + uint64_t c_new {0}; + + while (n > 0) { + // Check if the least significant bit is 1. + if (n & 1) { + g_new *= g; + c_new = c_new * g + c; + } + c *= (g + 1); + g *= g; + + // Move bits right, dropping least significant bit. + n >>= 1; + } + + return {g_new, c_new}; +} + +} // namespace + //============================================================================== // PRN //============================================================================== @@ -69,9 +108,10 @@ uint64_t init_seed(int64_t id, int offset) void init_particle_seeds(int64_t id, uint64_t* seeds) { + auto [multiplier, increment] = + future_seed_coefficients(static_cast(id) * prn_stride); for (int i = 0; i < N_STREAMS; i++) { - seeds[i] = - future_seed(static_cast(id) * prn_stride, master_seed + i); + seeds[i] = multiplier * (master_seed + i) + increment; } } @@ -90,33 +130,9 @@ void advance_prn_seed(int64_t n, uint64_t* seed) uint64_t future_seed(uint64_t n, uint64_t seed) { - // The algorithm here to determine the parameters used to skip ahead is - // described in F. Brown, "Random Number Generation with Arbitrary Stride," - // Trans. Am. Nucl. Soc. (Nov. 1994). This algorithm is able to skip ahead in - // O(log2(N)) operations instead of O(N). Basically, it computes parameters G - // and C which can then be used to find x_N = G*x_0 + C mod 2^M. - - // Initialize constants - uint64_t g {prn_mult}; - uint64_t c {prn_add}; - uint64_t g_new {1}; - uint64_t c_new {0}; - - while (n > 0) { - // Check if the least significant bit is 1. - if (n & 1) { - g_new *= g; - c_new = c_new * g + c; - } - c *= (g + 1); - g *= g; - - // Move bits right, dropping least significant bit. - n >>= 1; - } - // With G and C, we can now find the new seed. - return g_new * seed + c_new; + auto [multiplier, increment] = future_seed_coefficients(n); + return multiplier * seed + increment; } //============================================================================== diff --git a/src/simulation.cpp b/src/simulation.cpp index 900a383ab..cfc2b43e6 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -886,11 +886,14 @@ void transport_history_based_single_particle(Particle& p) void transport_history_based() { -#pragma omp parallel for schedule(runtime) - for (int64_t i_work = 1; i_work <= simulation::work_per_rank; ++i_work) { +#pragma omp parallel + { Particle p; - initialize_particle_track(p, i_work, false); - transport_history_based_single_particle(p); +#pragma omp for schedule(runtime) + for (int64_t i_work = 1; i_work <= simulation::work_per_rank; ++i_work) { + initialize_particle_track(p, i_work, false); + transport_history_based_single_particle(p); + } } } @@ -923,15 +926,16 @@ void transport_history_based_shared_secondary() #pragma omp parallel { vector thread_bank; + Particle p; #pragma omp for schedule(runtime) for (int64_t i = 1; i <= simulation::work_per_rank; i++) { - Particle p; initialize_particle_track(p, i, false); transport_history_based_single_particle(p); for (auto& site : p.local_secondary_bank()) { thread_bank.push_back(site); } + p.local_secondary_bank().clear(); } // Drain thread-local bank into the shared secondary bank (once per thread) @@ -988,11 +992,11 @@ void transport_history_based_shared_secondary() #pragma omp parallel { vector thread_bank; + Particle p; #pragma omp for schedule(runtime) for (int64_t i = 1; i <= simulation::shared_secondary_bank_read.size(); i++) { - Particle p; initialize_particle_track(p, i, true); SourceSite& site = simulation::shared_secondary_bank_read[i - 1]; p.event_revive_from_secondary(site); @@ -1000,6 +1004,7 @@ void transport_history_based_shared_secondary() for (auto& secondary_site : p.local_secondary_bank()) { thread_bank.push_back(secondary_site); } + p.local_secondary_bank().clear(); } // Drain thread-local bank into the shared secondary bank (once per From 7256d5046ae0b7ba05455c115f4f2f958313f786 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Fri, 10 Jul 2026 17:02:14 +0200 Subject: [PATCH 660/671] extra checks for pixels (#4009) Co-authored-by: Jonathan Shimwell Co-authored-by: Paul Romano --- openmc/model/model.py | 18 ++++++++++++------ openmc/plots.py | 14 +++++++------- tests/unit_tests/test_model.py | 2 ++ 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index b166137f2..c437d2033 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -34,6 +34,15 @@ class ModelModifier(Protocol): ... +def _check_pixels(pixels: int | Sequence[int]) -> None: + if isinstance(pixels, Integral): + check_greater_than('pixels', pixels, 0) + else: + check_length('pixels', pixels, 2) + for p in pixels: + check_greater_than('pixels', p, 0) + + class Model: """Model container. @@ -1051,12 +1060,7 @@ class Model: pixels: int | Sequence[int], basis: str ): - if isinstance(pixels, int): - check_greater_than('pixels', pixels, 0) - else: - check_length('pixels', pixels, 2) - for p in pixels: - check_greater_than('pixels', p, 0) + _check_pixels(pixels) x, y, _ = _BASIS_INDICES[basis] @@ -1220,6 +1224,8 @@ class Model: """ import openmc.lib + _check_pixels(pixels) + if width is not None and (u_span is not None or v_span is not None): raise ValueError("width is mutually exclusive with u_span/v_span.") diff --git a/openmc/plots.py b/openmc/plots.py index aeece7acf..531184095 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -383,7 +383,7 @@ def id_map_to_rgb( """ # Initialize RGB array with white background (values between 0 and 1 for matplotlib) img = np.ones(id_map.shape, dtype=float) - + # Get the appropriate index based on color_by if color_by == 'cell': id_index = 0 # Cell IDs are in the first channel @@ -391,14 +391,14 @@ def id_map_to_rgb( id_index = 2 # Material IDs are in the third channel else: raise ValueError("color_by must be either 'cell' or 'material'") - + # Get all unique IDs in the plot unique_ids = np.unique(id_map[:, :, id_index]) - + # Generate default colors if not provided if colors is None: colors = {} - + # Convert colors dict to use IDs as keys color_map = {} for key, color in colors.items(): @@ -406,13 +406,13 @@ def id_map_to_rgb( color_map[key.id] = color else: color_map[key] = color - + # Generate random colors for IDs not in color_map rng = np.random.RandomState(1) for uid in unique_ids: if uid > 0 and uid not in color_map: color_map[uid] = rng.randint(0, 256, (3,)) - + # Apply colors to each pixel for uid in unique_ids: if uid == -1: # Background/void @@ -432,7 +432,7 @@ def id_map_to_rgb( rgb = color mask = id_map[:, :, id_index] == uid img[mask] = np.array(rgb) / 255.0 - + return img class PlotBase(IDManagerMixin): diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 52ac43117..f60c458b6 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -679,6 +679,8 @@ def test_model_plot_invalid_inputs(): model.plot(pixels=(0, 100)) with pytest.raises(ValueError): model.plot(pixels=(100,)) + with pytest.raises(ValueError): + model.slice_data(u_span=(2, 0, 0), v_span=(0, 2, 0), pixels=-1) def test_model_id_map_initialization(run_in_tmpdir): From e783e014715cfb03df44616fc06e8ac5168f0df1 Mon Sep 17 00:00:00 2001 From: Eden <91745090+EdenRochmanSharabi@users.noreply.github.com> Date: Sat, 11 Jul 2026 08:18:26 +0200 Subject: [PATCH 661/671] Add chain parameter to Material.get_activity for half-life data (#3957) Co-authored-by: Paul Romano --- openmc/data/data.py | 60 +++++++++++++++++--- openmc/deplete/results.py | 35 ++++++++++-- openmc/material.py | 32 ++++++++--- tests/unit_tests/test_data_decay.py | 22 +++---- tests/unit_tests/test_data_misc.py | 33 ++++++++++- tests/unit_tests/test_deplete_resultslist.py | 31 ++++++++++ tests/unit_tests/test_material.py | 31 +++++++++- 7 files changed, 209 insertions(+), 35 deletions(-) diff --git a/openmc/data/data.py b/openmc/data/data.py index c22e54e7d..6e4840990 100644 --- a/openmc/data/data.py +++ b/openmc/data/data.py @@ -1,14 +1,23 @@ +from __future__ import annotations + import itertools import json import os import re from pathlib import Path from math import sqrt, log +from typing import TYPE_CHECKING, Literal from warnings import warn from endf.data import (ATOMIC_NUMBER, ATOMIC_SYMBOL, ELEMENT_SYMBOL, EV_PER_MEV, K_BOLTZMANN, gnds_name, zam) +import openmc +from openmc.checkvalue import PathLike + +if TYPE_CHECKING: + from openmc.deplete import Chain + gnds_name.__module__ = __name__ zam.__module__ = __name__ @@ -296,25 +305,47 @@ def atomic_weight(element): raise ValueError(f"No naturally-occurring isotopes for element '{element}'.") -def half_life(isotope): +def half_life( + isotope: str, + chain_file: Literal[False] | None | PathLike | Chain = False +) -> float | None: """Return half-life of isotope in seconds or None if isotope is stable - Half-life values are from the `ENDF/B-VIII.0 decay sublibrary - `_. + By default, half-life values are from the `ENDF/B-VIII.0 decay sublibrary + `_. A depletion chain can + also be used as the source of half-life values. .. versionadded:: 0.13.1 + .. versionchanged:: 0.15.4 + Added the ``chain_file`` argument. + Parameters ---------- isotope : str Name of isotope, e.g., 'Pu239' + chain_file : False, None, PathLike, or openmc.deplete.Chain, optional + Source of half-life values. If ``False``, only ENDF/B-VIII.0 data is + used. If ``None``, the chain specified by + ``openmc.config['chain_file']`` is used when available. If a path or + :class:`openmc.deplete.Chain` is given, that chain is used. For ``None`` + or an explicit chain, nuclides absent from the chain fall back to + ENDF/B-VIII.0 data. Returns ------- - float - Half-life of isotope in [s] + float or None + Half-life of isotope in [s], or None if the isotope is stable """ + if chain_file is not False: + if chain_file is not None or openmc.config.get('chain_file') is not None: + # Local import avoids a circular dependency + from openmc.deplete.chain import _get_chain + chain = _get_chain(chain_file) + if isotope in chain: + return chain[isotope].half_life + global _HALF_LIFE if not _HALF_LIFE: # Load ENDF/B-VIII.0 data from JSON file @@ -324,7 +355,10 @@ def half_life(isotope): return _HALF_LIFE.get(isotope.lower()) -def decay_constant(isotope): +def decay_constant( + isotope: str, + chain_file: Literal[False] | None | PathLike | Chain = False +) -> float: """Return decay constant of isotope in [s^-1] Decay constants are based on half-life values from the @@ -333,10 +367,20 @@ def decay_constant(isotope): .. versionadded:: 0.13.1 + .. versionchanged:: 0.15.4 + Added the ``chain_file`` argument. + Parameters ---------- isotope : str Name of isotope, e.g., 'Pu239' + chain_file : False, None, PathLike, or openmc.deplete.Chain, optional + Source of half-life values. If ``False``, only ENDF/B-VIII.0 data is + used. If ``None``, the chain specified by + ``openmc.config['chain_file']`` is used when available. If a path or + :class:`openmc.deplete.Chain` is given, that chain is used. For ``None`` + or an explicit chain, nuclides absent from the chain fall back to + ENDF/B-VIII.0 data. Returns ------- @@ -348,7 +392,7 @@ def decay_constant(isotope): openmc.data.half_life """ - t = half_life(isotope) + t = half_life(isotope, chain_file) return _LOG_TWO / t if t else 0.0 @@ -496,5 +540,3 @@ def isotopes(element: str) -> list[tuple[str, float]]: result.append(kv) return result - - diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index adb0d3dbc..0cf0141d2 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -1,12 +1,17 @@ +from __future__ import annotations + import numbers import bisect import math from collections.abc import Iterable +from typing import Literal from warnings import warn import h5py import numpy as np +import openmc +from .chain import Chain, _get_chain from .stepresult import StepResult, VERSION_RESULTS import openmc.checkvalue as cv from openmc.data import atomic_mass, AVOGADRO @@ -103,7 +108,8 @@ class Results(list): mat: Material | str, units: str = "Bq/cm3", by_nuclide: bool = False, - volume: float | None = None + volume: float | None = None, + chain_file: Literal[False] | None | PathLike | Chain = None ) -> tuple[np.ndarray, np.ndarray | list[dict]]: """Get activity of material over time. @@ -115,22 +121,31 @@ class Results(list): Material object or material id to evaluate units : {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3', 'Bq/m3'} Specifies the type of activity to return, options include total - activity [Bq], specific [Bq/g, Bq/kg] or volumetric activity [Bq/cm3]. + activity [Bq], specific [Bq/g, Bq/kg] or volumetric activity + [Bq/cm3]. by_nuclide : bool Specifies if the activity should be returned for the material as a whole or per nuclide. Default is False. volume : float, optional Volume of the material. If not passed, defaults to using the :attr:`Material.volume` attribute. + chain_file : False, None, PathLike, or openmc.deplete.Chain, optional + Source of half-life values. If ``False``, only ENDF/B-VIII.0 data is + used. If ``None``, the chain specified by + ``openmc.config['chain_file']`` is used when available. If a path or + :class:`openmc.deplete.Chain` is given, that chain is used. For + ``None`` or an explicit chain, nuclides absent from the chain fall + back to ENDF/B-VIII.0 data. + + .. versionadded:: 0.15.4 Returns ------- times : numpy.ndarray Array of times in [s] activities : numpy.ndarray or List[dict] - Array of total activities if by_nuclide = False (default) - or list of dictionaries of activities by nuclide if - by_nuclide = True. + Array of total activities if by_nuclide = False (default) or list of + dictionaries of activities by nuclide if by_nuclide = True. """ if isinstance(mat, Material): @@ -140,6 +155,13 @@ class Results(list): else: raise TypeError('mat should be of type openmc.Material or str') + if chain_file is not False: + if chain_file is None: + if openmc.config.get('chain_file') is not None: + chain_file = _get_chain(None) + else: + chain_file = _get_chain(chain_file) + times = np.empty_like(self, dtype=float) if by_nuclide: activities = [None] * len(self) @@ -149,7 +171,8 @@ class Results(list): # Evaluate activity for each depletion time for i, result in enumerate(self): times[i] = result.time[0] - activities[i] = result.get_material(mat_id).get_activity(units, by_nuclide, volume) + activities[i] = result.get_material(mat_id).get_activity( + units, by_nuclide, volume, chain_file=chain_file) return times, activities diff --git a/openmc/material.py b/openmc/material.py index eded9c3fa..3a92efda2 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -8,7 +8,7 @@ from pathlib import Path import re import sys import tempfile -from typing import Sequence, Dict +from typing import TYPE_CHECKING, Literal, Sequence, Dict import warnings import lxml.etree as ET @@ -28,6 +28,9 @@ 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 +if TYPE_CHECKING: + from openmc.deplete import Chain + # Units for density supported by OpenMC DENSITY_UNITS = ('g/cm3', 'g/cc', 'kg/m3', 'atom/b-cm', 'atom/cm3', 'sum', @@ -1385,8 +1388,13 @@ class Material(IDManagerMixin): return densities - def get_activity(self, units: str = 'Bq/cm3', by_nuclide: bool = False, - volume: float | None = None) -> dict[str, float] | float: + def get_activity( + self, + units: str = 'Bq/cm3', + by_nuclide: bool = False, + volume: float | None = None, + chain_file: Literal[False] | None | PathLike | Chain = None + ) -> dict[str, float] | float: """Return the activity of the material or each nuclide within. .. versionadded:: 0.13.1 @@ -1405,13 +1413,22 @@ class Material(IDManagerMixin): :attr:`Material.volume` attribute. .. versionadded:: 0.13.3 + chain_file : False, None, PathLike, or openmc.deplete.Chain, optional + Source of half-life values. If ``False``, only ENDF/B-VIII.0 data is + used. If ``None``, the chain specified by + ``openmc.config['chain_file']`` is used when available. If a path or + :class:`openmc.deplete.Chain` is given, that chain is used. For + ``None`` or an explicit chain, nuclides absent from the chain fall + back to ENDF/B-VIII.0 data. + + .. versionadded:: 0.15.4 Returns ------- Union[dict, float] - If by_nuclide is True then a dictionary whose keys are nuclide - names and values are activity is returned. Otherwise the activity - of the material is returned as a float. + If by_nuclide is True then a dictionary whose keys are nuclide names + and values are activity is returned. Otherwise the activity of the + material is returned as a float. """ cv.check_value('units', units, {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3', 'Bq/m3', 'Ci', 'Ci/m3'}) @@ -1440,7 +1457,8 @@ class Material(IDManagerMixin): activity = {} for nuclide, atoms_per_bcm in self.get_nuclide_atom_densities().items(): - inv_seconds = openmc.data.decay_constant(nuclide) + inv_seconds = openmc.data.decay_constant( + nuclide, chain_file=chain_file) activity[nuclide] = inv_seconds * 1e24 * atoms_per_bcm * multiplier return activity if by_nuclide else sum(activity.values()) diff --git a/tests/unit_tests/test_data_decay.py b/tests/unit_tests/test_data_decay.py index a48c55325..f81f857e0 100644 --- a/tests/unit_tests/test_data_decay.py +++ b/tests/unit_tests/test_data_decay.py @@ -155,16 +155,16 @@ def test_decay_photon_energy(): with pytest.raises(DataError): openmc.data.decay_photon_energy('I135') - # Set chain file to simple chain - openmc.config['chain_file'] = Path(__file__).parents[1] / "chain_simple.xml" + # Temporarily Set chain file to simple chain + with openmc.config.patch('chain_file', Path(__file__).parents[1] / 'chain_simple.xml'): - # Check strength of I135 source and presence of specific spectral line - src = openmc.data.decay_photon_energy('I135') - assert isinstance(src, openmc.stats.Discrete) - assert src.integral() == pytest.approx(3.920996223799345e-05) - assert 1260409. in src.x + # Check strength of I135 source and presence of specific spectral line + src = openmc.data.decay_photon_energy('I135') + assert isinstance(src, openmc.stats.Discrete) + assert src.integral() == pytest.approx(3.920996223799345e-05) + assert 1260409. in src.x - # Check Xe135 source, which should be tabular - src = openmc.data.decay_photon_energy('Xe135') - assert isinstance(src, openmc.stats.Tabular) - assert src.integral() == pytest.approx(2.076506258964966e-05) + # Check Xe135 source, which should be tabular + src = openmc.data.decay_photon_energy('Xe135') + assert isinstance(src, openmc.stats.Tabular) + assert src.integral() == pytest.approx(2.076506258964966e-05) diff --git a/tests/unit_tests/test_data_misc.py b/tests/unit_tests/test_data_misc.py index 14db68913..320d09eda 100644 --- a/tests/unit_tests/test_data_misc.py +++ b/tests/unit_tests/test_data_misc.py @@ -7,6 +7,7 @@ from pathlib import Path import numpy as np import pytest import openmc.data +from openmc.deplete import Chain, Nuclide def test_data_library(tmpdir): @@ -134,7 +135,8 @@ def test_zam(): with pytest.raises(ValueError): openmc.data.zam('Am242-m1') -def test_half_life(): + +def test_half_life(tmp_path): assert openmc.data.half_life('H2') is None assert openmc.data.half_life('U235') == pytest.approx(2.22102e16) assert openmc.data.half_life('Am242') == pytest.approx(57672.0) @@ -143,3 +145,32 @@ def test_half_life(): assert openmc.data.decay_constant('U235') == pytest.approx(log(2.0)/2.22102e16) assert openmc.data.decay_constant('Am242') == pytest.approx(log(2.0)/57672.0) assert openmc.data.decay_constant('Am242_m1') == pytest.approx(log(2.0)/4449622000.0) + + # Create minimal chain with H3 and Am242 to test half-life and decay + # constant retrieval from chain file + chain = Chain() + h3 = Nuclide("H3") + h3.half_life = 1.0 + chain.add_nuclide(h3) + am242 = Nuclide("Am242") + chain.add_nuclide(am242) + + assert openmc.data.half_life('H3', chain_file=chain) == 1.0 + assert openmc.data.decay_constant('H3', chain_file=chain) == pytest.approx(log(2.0)) + + # Nuclides that are present but stable in the chain should not fall back to + # ENDF/B-VIII.0 data. + assert openmc.data.half_life('Am242', chain_file=chain) is None + assert openmc.data.decay_constant('Am242', chain_file=chain) == 0.0 + + # Nuclides missing from the chain fall back to ENDF/B-VIII.0 data. + assert openmc.data.half_life('U235', chain_file=chain) == pytest.approx(2.22102e16) + + chain_path = tmp_path / "chain.xml" + chain.export_to_xml(chain_path) + assert openmc.data.half_life('H3', chain_file=chain_path) == 1.0 + + endf_h3 = openmc.data.half_life('H3') + with openmc.config.patch('chain_file', chain_path): + assert openmc.data.half_life('H3', chain_file=None) == 1.0 + assert openmc.data.half_life('H3', chain_file=False) == endf_h3 diff --git a/tests/unit_tests/test_deplete_resultslist.py b/tests/unit_tests/test_deplete_resultslist.py index 39c532c54..bc1a078b0 100644 --- a/tests/unit_tests/test_deplete_resultslist.py +++ b/tests/unit_tests/test_deplete_resultslist.py @@ -6,6 +6,7 @@ from pathlib import Path import numpy as np import pytest +import openmc import openmc.deplete @@ -38,6 +39,36 @@ def test_get_activity(res): np.testing.assert_allclose(a_xe135, a_xe135_ref) +def test_get_activity_chain_file(res, tmp_path): + """Tests evaluating activity with chain half-life data""" + _, a_endf = res.get_activity("1", by_nuclide=True, chain_file=False) + xe135_endf = np.array([a["Xe135"] for a in a_endf]) + + chain = openmc.deplete.Chain() + xe135 = openmc.deplete.Nuclide("Xe135") + xe135.half_life = openmc.data.half_life("Xe135") / 2.0 + chain.add_nuclide(xe135) + + t_chain, a_chain = res.get_activity("1", by_nuclide=True, chain_file=chain) + xe135_chain = np.array([a["Xe135"] for a in a_chain]) + + t_ref = np.array([0.0, 1296000.0, 2592000.0, 3888000.0]) + np.testing.assert_allclose(t_chain, t_ref) + np.testing.assert_allclose(xe135_chain, 2.0 * xe135_endf) + + chain_path = tmp_path / "chain.xml" + chain.export_to_xml(chain_path) + with openmc.config.patch('chain_file', chain_path): + _, a_config = res.get_activity("1", by_nuclide=True) + xe135_config = np.array([a["Xe135"] for a in a_config]) + np.testing.assert_allclose(xe135_config, xe135_chain) + + stable_chain = openmc.deplete.Chain() + stable_chain.add_nuclide(openmc.deplete.Nuclide("Xe135")) + _, a_stable = res.get_activity("1", by_nuclide=True, chain_file=stable_chain) + assert all(a["Xe135"] == 0.0 for a in a_stable) + + def test_get_atoms(res): """Tests evaluating single nuclide concentration.""" t, n = res.get_atoms("1", "Xe135") diff --git a/tests/unit_tests/test_material.py b/tests/unit_tests/test_material.py index 89dfc03dd..12d363376 100644 --- a/tests/unit_tests/test_material.py +++ b/tests/unit_tests/test_material.py @@ -7,7 +7,7 @@ import numpy as np import openmc from openmc.data import decay_photon_energy -from openmc.deplete import Chain +from openmc.deplete import Chain, Nuclide import openmc.examples import openmc.model import openmc.stats @@ -614,6 +614,35 @@ def test_get_activity(): assert m4.get_activity(units='Ci/m3') == pytest.approx(ci/m3) +def test_get_activity_chain_file(tmp_path): + m = openmc.Material() + m.add_nuclide("H3", 1.0) + m.set_density('g/cm3', 1.0) + + chain = Chain() + h3 = Nuclide("H3") + h3.half_life = 1.0 + chain.add_nuclide(h3) + + atoms_per_bcm = m.get_nuclide_atom_densities()["H3"] + expected = np.log(2.0) * 1e24 * atoms_per_bcm + + assert m.get_activity(chain_file=chain) == pytest.approx(expected) + + chain_path = tmp_path / "chain.xml" + chain.export_to_xml(chain_path) + assert m.get_activity(chain_file=chain_path) == pytest.approx(expected) + + endf_activity = m.get_activity(chain_file=False) + with openmc.config.patch('chain_file', chain_path): + assert m.get_activity() == pytest.approx(expected) + assert m.get_activity(chain_file=False) == pytest.approx(endf_activity) + + stable_chain = Chain() + stable_chain.add_nuclide(Nuclide("H3")) + assert m.get_activity(chain_file=stable_chain) == 0.0 + + def test_get_decay_heat(): # Set chain file for testing openmc.config['chain_file'] = Path(__file__).parents[1] / 'chain_simple.xml' From 243c2495333c4bc15aaa345a2ba33330a66f2caf Mon Sep 17 00:00:00 2001 From: Lewis Gross <43077972+lewisgross1296@users.noreply.github.com> Date: Wed, 15 Jul 2026 08:57:53 -0500 Subject: [PATCH 662/671] normalize new_u after periodic crossing to prevent unnormalized direction (#4015) --- src/boundary_condition.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/boundary_condition.cpp b/src/boundary_condition.cpp index 5bbda4830..d166f1247 100644 --- a/src/boundary_condition.cpp +++ b/src/boundary_condition.cpp @@ -38,6 +38,9 @@ void VacuumBC::handle_particle(Particle& p, const Surface& surf) const void ReflectiveBC::handle_particle(Particle& p, const Surface& surf) const { Direction u = surf.reflect(p.r(), p.u(), &p); + + // normalize reflected u to ensure no floating point error leads to + // unnormalized directions u /= u.norm(); // Handle the effects of the surface albedo on the particle's weight. @@ -53,6 +56,9 @@ void ReflectiveBC::handle_particle(Particle& p, const Surface& surf) const void WhiteBC::handle_particle(Particle& p, const Surface& surf) const { Direction u = surf.diffuse_reflect(p.r(), p.u(), p.current_seed()); + + // normalize outgoing u to ensure no floating point error leads to + // unnormalized directions u /= u.norm(); // Handle the effects of the surface albedo on the particle's weight. @@ -241,6 +247,10 @@ void RotationalPeriodicBC::handle_particle( new_u[axis_1_idx_] = cos_theta * u[axis_1_idx_] - sin_theta * u[axis_2_idx_]; new_u[axis_2_idx_] = sin_theta * u[axis_1_idx_] + cos_theta * u[axis_2_idx_]; + // normalize new_u to ensure no floating point error leads to unnormalized + // directions + new_u /= new_u.norm(); + // Handle the effects of the surface albedo on the particle's weight. BoundaryCondition::handle_albedo(p, surf); From 16cde42ff4a9261c0f4c76b10d64f1d8906bef0d Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 15 Jul 2026 16:00:42 -0500 Subject: [PATCH 663/671] Enable parent-nuclide tally breakdowns in R2S calculations (#4013) Co-authored-by: GuySten <62616591+GuySten@users.noreply.github.com> --- docs/source/usersguide/decay_sources.rst | 24 ++- openmc/deplete/r2s.py | 183 +++++++++++++++++------ tests/unit_tests/test_r2s.py | 120 ++++++++++++++- 3 files changed, 273 insertions(+), 54 deletions(-) diff --git a/docs/source/usersguide/decay_sources.rst b/docs/source/usersguide/decay_sources.rst index 21981fdaa..19f1e5d5f 100644 --- a/docs/source/usersguide/decay_sources.rst +++ b/docs/source/usersguide/decay_sources.rst @@ -132,8 +132,9 @@ can be run:: r2s.run(timesteps, source_rates, bounding_boxes=bounding_boxes) If not specified otherwise, a photon transport calculation is run at each time -in the depletion schedule. That means in the case above, we would see three -photon transport calculations. To specify specific times at which photon +in the depletion schedule for which a decay photon source exists. Times without +a decay photon source, such as the initial state of a model containing only +stable nuclides, are omitted. To specify particular times at which photon transport calculations should be run, pass the ``photon_time_indices`` argument. For example, if we wanted to run a photon transport calculation only on the last time (after the 5 hour decay), we would run:: @@ -141,6 +142,19 @@ time (after the 5 hour decay), we would run:: r2s.run(timesteps, source_rates, bounding_boxes=bounding_boxes, photon_time_indices=[2]) +To attribute photon tally results to their parent radionuclides, set +``by_parent_nuclide=True``. This automatically adds a +:class:`openmc.ParentNuclideFilter` to every photon tally that does not already +have one. The filter bins are the union of radionuclides contributing to the +prepared decay photon sources. The resulting bins can be used directly when +inspecting the tally results:: + + r2s.run(timesteps, source_rates, bounding_boxes=bounding_boxes, + photon_time_indices=[2], by_parent_nuclide=True) + + photon_tally = r2s.results['photon_tallies'][2][0] + tally_by_parent = photon_tally.get_pandas_dataframe() + After an R2S calculation has been run, the :class:`~openmc.deplete.R2SManager` instance will have a ``results`` dictionary that allows you to directly access results from each of the steps. It will also write out all the output files into @@ -148,12 +162,13 @@ a directory that is named "r2s_/". The ``output_dir`` argument to the :meth:`~openmc.deplete.R2SManager.run` method enables you to override the default output directory name if desired. -The :meth:`~openmc.deplete.R2SManager.run` method actually runs three +The :meth:`~openmc.deplete.R2SManager.run` method actually runs four lower-level methods under the hood:: r2s.step1_neutron_transport(...) r2s.step2_activation(...) - r2s.step3_photon_transport(...) + r2s.step3_photon_source(...) + r2s.step4_photon_transport(...) For users looking for more control over the calculation, these lower-level methods can be used in lieu of the :meth:`openmc.deplete.R2SManager.run` method. @@ -255,4 +270,3 @@ relevant tallies. This can be done with the aid of the # Apply time correction factors tally = d1s.apply_time_correction(dose_tally, factors, time_index) - diff --git a/openmc/deplete/r2s.py b/openmc/deplete/r2s.py index d41e230ee..6c19531e7 100644 --- a/openmc/deplete/r2s.py +++ b/openmc/deplete/r2s.py @@ -4,6 +4,7 @@ from contextlib import nullcontext import copy from datetime import datetime import json +from numbers import Integral from pathlib import Path import numpy as np @@ -156,6 +157,7 @@ class R2SManager: mat_vol_kwargs: dict | None = None, run_kwargs: dict | None = None, operator_kwargs: dict | None = None, + by_parent_nuclide: bool = False, ): """Run the R2S calculation. @@ -179,7 +181,7 @@ class R2SManager: timesteps. For example, if two timesteps are specified, the array of times would contain three entries, and [2] would indicate computing photon results at the last time. A value of None indicates to run - photon transport for each time. + photon transport at each time that has a decay photon source. output_dir : PathLike, optional Path to directory where R2S calculation outputs will be saved. If not provided, a timestamped directory 'r2s_YYYY-MM-DDTHH-MM-SS' is @@ -207,6 +209,11 @@ class R2SManager: operator_kwargs : dict, optional Additional keyword arguments passed to :class:`openmc.deplete.IndependentOperator`. + by_parent_nuclide : bool, optional + Whether to score photon tallies separately for each parent + radionuclide. A :class:`~openmc.ParentNuclideFilter` is added to + tallies that do not already contain one, with bins determined from + the prepared decay photon sources. Returns ------- @@ -256,9 +263,13 @@ class R2SManager: timesteps, source_rates, timestep_units, output_dir / 'activation', operator_kwargs=operator_kwargs ) - self.step3_photon_transport( + self.step3_photon_source( photon_time_indices, bounding_boxes, output_dir / 'photon_transport', - mat_vol_kwargs=mat_vol_kwargs, run_kwargs=run_kwargs + mat_vol_kwargs=mat_vol_kwargs, + ) + self.step4_photon_transport( + output_dir / 'photon_transport', run_kwargs=run_kwargs, + by_parent_nuclide=by_parent_nuclide, ) return output_dir @@ -438,23 +449,20 @@ class R2SManager: # Get depletion results self.results['depletion_results'] = Results(output_path) - def step3_photon_transport( + def step3_photon_source( self, time_indices: Sequence[int] | None = None, bounding_boxes: dict[int, openmc.BoundingBox] | None = None, output_dir: PathLike = 'photon_transport', mat_vol_kwargs: dict | None = None, - run_kwargs: dict | None = None, ): - """Run the photon transport step. + """Create decay photon sources. - This step performs photon transport calculations using decay photon - sources created from the activated materials. For each specified time, - it creates appropriate photon sources and runs a transport calculation. - In mesh-based mode, the sources are created using the mesh material - volumes, while in cell-based mode, they are created using bounding boxes - for each cell. This step will populate the 'photon_tallies' key in the - results dictionary. + This step creates decay photon sources from the activated materials for + each specified time. In mesh-based mode, the sources are created using + mesh material volumes, while in cell-based mode, they are created using + bounding boxes for each cell. This step will populate the + 'photon_sources' key in the results dictionary. Parameters ---------- @@ -464,40 +472,54 @@ class R2SManager: timesteps. For example, if two timesteps are specified, the array of times would contain three entries, and [2] would indicate computing photon results at the last time. A value of None indicates to run - photon transport for each time. + photon transport at each time that has a decay photon source. bounding_boxes : dict[int, openmc.BoundingBox], optional Dictionary mapping cell IDs to bounding boxes used for spatial source sampling in cell-based R2S calculations. Required if method is 'cell-based'. output_dir : PathLike, optional - Path to directory where photon transport outputs will be saved. + Path to directory where photon source outputs will be saved. mat_vol_kwargs : dict, optional Additional keyword arguments passed to :meth:`openmc.MeshBase.material_volumes`. - run_kwargs : dict, optional - Additional keyword arguments passed to :meth:`openmc.Model.run` - during the photon transport step. By default, output is disabled. """ + # Do not retain sources from an earlier successful call if this source + # preparation attempt fails. + self.results.pop('photon_sources', None) + # TODO: Automatically determine bounding box for each cell if bounding_boxes is None and self.method == 'cell-based': raise ValueError("bounding_boxes must be provided for cell-based " "R2S calculations.") - # Set default run arguments if not provided - if run_kwargs is None: - run_kwargs = {} - run_kwargs.setdefault('output', False) - - # Write out JSON file with tally IDs that can be used for loading - # results output_dir = Path(output_dir) output_dir.mkdir(parents=True, exist_ok=True) - # Get default time indices if not provided + # Determine and validate time indices before preparing source data. + n_steps = len(self.results['depletion_results']) + implicit_time_indices = time_indices is None if time_indices is None: - n_steps = len(self.results['depletion_results']) time_indices = list(range(n_steps)) + else: + time_indices = list(time_indices) + if not time_indices: + raise ValueError('time_indices must contain at least one index') + + normalized_indices = [] + for index in time_indices: + if isinstance(index, bool) or not isinstance(index, Integral): + raise TypeError('time_indices must contain only integers') + index = int(index) + if index < -n_steps or index >= n_steps: + raise IndexError( + f'Photon time index {index} is out of range for ' + f'{n_steps} depletion results') + normalized_index = index % n_steps + normalized_indices.append(normalized_index) + + # Remove duplicates while preserving order + time_indices = list(dict.fromkeys(normalized_indices)) # Check whether the photon model is different neutron_univ = self.neutron_model.geometry.root_universe @@ -523,13 +545,6 @@ class R2SManager: self.results['mesh_material_volumes_photon'] = photon_mmv_list - if comm.rank == 0: - tally_ids = [tally.id for tally in self.photon_model.tallies] - with open(output_dir / 'tally_ids.json', 'w') as f: - json.dump(tally_ids, f) - - self.results['photon_tallies'] = {} - # Get dictionary of cells in the photon model if different_photon_model: photon_cells = self.photon_model.geometry.get_all_cells() @@ -547,16 +562,100 @@ class R2SManager: continue work_items.append((cell, original_mat, bounding_boxes[cell.id])) - # Ensure photon transport is enabled in settings + # Create decay photon sources for each time index + photon_sources = { + time_index: self._create_photon_sources(time_index, work_items) + for time_index in time_indices + } + + # Determine if any times have no decay photon sources. If the user + # didn't specify any specific time indices, remove those times from the + # photon_sources dictionary. If the user did specify time indices, raise + # an error if any of those times have no decay photon sources. + empty_indices = [ + time_index for time_index, sources in photon_sources.items() + if not sources + ] + if implicit_time_indices: + for time_index in empty_indices: + del photon_sources[time_index] + if not photon_sources: + raise RuntimeError( + 'No decay photon sources were found at any depletion time') + elif empty_indices: + indices = ', '.join(str(index) for index in empty_indices) + raise RuntimeError( + f'No decay photon source was found for requested time ' + f'indices: {indices}') + + self.results['photon_sources'] = photon_sources + + def step4_photon_transport( + self, + output_dir: PathLike = 'photon_transport', + run_kwargs: dict | None = None, + by_parent_nuclide: bool = False, + ): + """Run photon transport using prepared decay photon sources. + + This step runs a photon transport calculation for each source list + created by :meth:`step3_photon_source`. It will populate the + 'photon_tallies' key in the results dictionary. + + Parameters + ---------- + output_dir : PathLike, optional + Path to directory where photon transport outputs will be saved. + run_kwargs : dict, optional + Additional keyword arguments passed to :meth:`openmc.Model.run`. + By default, output is disabled. + by_parent_nuclide : bool, optional + Whether to score photon tallies separately for each parent + radionuclide. A :class:`~openmc.ParentNuclideFilter` is added to + tallies that do not already contain one, with bins determined from + the prepared decay photon sources. + """ + if 'photon_sources' not in self.results: + raise RuntimeError( + 'Photon sources must be created with step3_photon_source ' + 'before running photon transport.') + photon_sources = self.results['photon_sources'] + if not photon_sources: + raise RuntimeError( + 'No decay photon sources are available for transport') + + if by_parent_nuclide: + radionuclides = sorted({ + nuclide + for sources in photon_sources.values() + for source in sources + for nuclide in source.energy.nuclides + }) + + if radionuclides: + parent_filter = openmc.ParentNuclideFilter(radionuclides) + for tally in self.photon_model.tallies: + if not tally.contains_filter(openmc.ParentNuclideFilter): + tally.filters.append(parent_filter) + + if run_kwargs is None: + run_kwargs = {} + run_kwargs.setdefault('output', False) + + output_dir = Path(output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + if comm.rank == 0: + tally_ids = [tally.id for tally in self.photon_model.tallies] + with open(output_dir / 'tally_ids.json', 'w') as f: + json.dump(tally_ids, f) + + self.results['photon_tallies'] = {} + + # Ensure photon transport is enabled in settings. self.photon_model.settings.photon_transport = True - for time_index in time_indices: - # Convert time_index (which may be negative) to a normal index - if time_index < 0: - time_index += len(self.results['depletion_results']) - - # Build decay photon sources and assign to the photon model - sources = self._create_photon_sources(time_index, work_items) + for time_index, sources in photon_sources.items(): self.photon_model.settings.source = sources # Run photon transport calculation diff --git a/tests/unit_tests/test_r2s.py b/tests/unit_tests/test_r2s.py index 266bd4976..5e617919d 100644 --- a/tests/unit_tests/test_r2s.py +++ b/tests/unit_tests/test_r2s.py @@ -33,8 +33,8 @@ def simple_model_and_mesh(): # Simple settings with a point source settings = openmc.Settings() - settings.batches = 10 - settings.particles = 1000 + settings.batches = 2 + settings.particles = 250 settings.run_mode = 'fixed source' settings.source = openmc.IndependentSource() model = openmc.Model(geometry, settings=settings) @@ -46,6 +46,16 @@ def simple_model_and_mesh(): return model, (c1, c2), mesh +@pytest.fixture +def source_stage_manager(simple_model_and_mesh): + model, (c1, c2), _ = simple_model_and_mesh + r2s = R2SManager(model, [c1, c2]) + r2s.results['depletion_results'] = [None, None] + r2s.results['activation_materials'] = [c1.fill, c2.fill] + bounding_boxes = {c1.id: c1.bounding_box, c2.id: c2.bounding_box} + return r2s, bounding_boxes + + def test_r2s_mesh_expected_output(simple_model_and_mesh, tmp_path): model, (c1, c2), mesh = simple_model_and_mesh @@ -59,9 +69,9 @@ def test_r2s_mesh_expected_output(simple_model_and_mesh, tmp_path): outdir = r2s.run( timesteps=[(1.0, 'd')], source_rates=[1.0], - photon_time_indices=[1], output_dir=tmp_path, chain_file=chain, + micro_kwargs={'nuclides': ['Ni58'], 'reactions': ['(n,p)']}, ) # Check directories and files exist @@ -73,7 +83,8 @@ def test_r2s_mesh_expected_output(simple_model_and_mesh, tmp_path): assert (act / 'depletion_results.h5').exists() pt = Path(outdir) / 'photon_transport' assert (pt / 'tally_ids.json').exists() - assert (pt / 'time_1' / 'statepoint.10.h5').exists() + assert not (pt / 'time_0').exists() + assert (pt / 'time_1' / 'statepoint.2.h5').exists() # Basic results structure checks assert len(r2s.results['fluxes']) == 2 @@ -82,6 +93,8 @@ def test_r2s_mesh_expected_output(simple_model_and_mesh, tmp_path): assert len(r2s.results['mesh_material_volumes'][0]) == 2 assert len(r2s.results['activation_materials']) == 2 assert len(r2s.results['depletion_results']) == 2 + assert list(r2s.results['photon_sources']) == [1] + assert r2s.results['photon_sources'][1] # Check activation materials amats = r2s.results['activation_materials'] @@ -125,6 +138,7 @@ def test_r2s_multi_mesh(simple_model_and_mesh, tmp_path): photon_time_indices=[1], output_dir=tmp_path, chain_file=chain, + micro_kwargs={'nuclides': ['Ni58'], 'reactions': ['(n,p)']}, ) # Check that per-mesh MMV files were written @@ -137,7 +151,7 @@ def test_r2s_multi_mesh(simple_model_and_mesh, tmp_path): assert (act / 'depletion_results.h5').exists() pt = Path(outdir) / 'photon_transport' assert (pt / 'tally_ids.json').exists() - assert (pt / 'time_1' / 'statepoint.10.h5').exists() + assert (pt / 'time_1' / 'statepoint.2.h5').exists() # Two meshes, each with 1 element containing both materials → # 2 element-material combinations per mesh, 4 total @@ -167,6 +181,9 @@ def test_r2s_multi_mesh(simple_model_and_mesh, tmp_path): def test_r2s_cell_expected_output(simple_model_and_mesh, tmp_path): model, (c1, c2), _ = simple_model_and_mesh + tally = openmc.Tally() + tally.scores = ['flux'] + model.tallies = [tally] # Use cell-based domains r2s = R2SManager(model, [c1, c2]) @@ -180,9 +197,11 @@ def test_r2s_cell_expected_output(simple_model_and_mesh, tmp_path): timesteps=[(1.0, 'd')], source_rates=[1.0], photon_time_indices=[1], + by_parent_nuclide=True, output_dir=tmp_path, bounding_boxes=bounding_boxes, - chain_file=chain + chain_file=chain, + micro_kwargs={'nuclides': ['Ni58'], 'reactions': ['(n,p)']}, ) # Check directories and files exist @@ -193,13 +212,15 @@ def test_r2s_cell_expected_output(simple_model_and_mesh, tmp_path): assert (act / 'depletion_results.h5').exists() pt = Path(outdir) / 'photon_transport' assert (pt / 'tally_ids.json').exists() - assert (pt / 'time_1' / 'statepoint.10.h5').exists() + assert (pt / 'time_1' / 'statepoint.2.h5').exists() # Basic results structure checks assert len(r2s.results['fluxes']) == 2 assert len(r2s.results['micros']) == 2 assert len(r2s.results['activation_materials']) == 2 assert len(r2s.results['depletion_results']) == 2 + assert r2s.photon_model.tallies[0].contains_filter( + openmc.ParentNuclideFilter) # Check activation materials amats = r2s.results['activation_materials'] @@ -217,3 +238,88 @@ def test_r2s_cell_expected_output(simple_model_and_mesh, tmp_path): assert len(r2s_loaded.results['micros']) == 2 assert len(r2s_loaded.results['activation_materials']) == 2 assert len(r2s_loaded.results['depletion_results']) == 2 + + +def test_step4_requires_photon_sources(simple_model_and_mesh, tmp_path): + model, (c1, c2), _ = simple_model_and_mesh + r2s = R2SManager(model, [c1, c2]) + output_dir = tmp_path / 'photon' + + with pytest.raises(RuntimeError, match='step3_photon_source'): + r2s.step4_photon_transport(output_dir) + + r2s.results['photon_sources'] = {} + with pytest.raises(RuntimeError, match='No decay photon sources'): + r2s.step4_photon_transport(output_dir) + + assert not output_dir.exists() + + +def test_default_photon_times_skip_empty_sources( + source_stage_manager, tmp_path, monkeypatch +): + r2s, bounding_boxes = source_stage_manager + source = object() + sources_by_time = {0: [], 1: [source]} + monkeypatch.setattr( + r2s, '_create_photon_sources', + lambda time_index, work_items: sources_by_time[time_index]) + + r2s.step3_photon_source( + bounding_boxes=bounding_boxes, output_dir=tmp_path) + + assert r2s.results['photon_sources'] == {1: [source]} + + +def test_explicit_empty_photon_source_fails( + source_stage_manager, tmp_path, monkeypatch +): + r2s, bounding_boxes = source_stage_manager + source = object() + sources_by_time = {0: [], 1: [source]} + monkeypatch.setattr( + r2s, '_create_photon_sources', + lambda time_index, work_items: sources_by_time[time_index]) + r2s.results['photon_sources'] = {99: [source]} + + with pytest.raises(RuntimeError, match='requested time indices: 0'): + r2s.step3_photon_source( + [0, 1], bounding_boxes, output_dir=tmp_path) + + assert 'photon_sources' not in r2s.results + + +def test_default_photon_times_require_a_source( + source_stage_manager, tmp_path, monkeypatch +): + r2s, bounding_boxes = source_stage_manager + monkeypatch.setattr( + r2s, '_create_photon_sources', + lambda time_index, work_items: []) + + with pytest.raises(RuntimeError, match='at any depletion time'): + r2s.step3_photon_source( + bounding_boxes=bounding_boxes, output_dir=tmp_path) + + assert 'photon_sources' not in r2s.results + + +@pytest.mark.parametrize( + ('time_indices', 'exception'), + [ + ([], ValueError), + ([2], IndexError), + ([-3], IndexError), + ([1.0], TypeError), + ], +) +def test_photon_time_index_validation( + source_stage_manager, tmp_path, time_indices, exception +): + r2s, bounding_boxes = source_stage_manager + + with pytest.raises(exception): + r2s.step3_photon_source( + time_indices, bounding_boxes, output_dir=tmp_path) + + assert 'photon_sources' not in r2s.results From c55c5788125015bfdb621c5e2a822c037b3f4f41 Mon Sep 17 00:00:00 2001 From: Ethan Peterson Date: Thu, 16 Jul 2026 10:54:56 -0400 Subject: [PATCH 664/671] Native parametric tokamak source (#3999) Co-authored-by: Paul Romano --- docs/source/io_formats/settings.rst | 80 +++- docs/source/pythonapi/base.rst | 1 + docs/source/usersguide/settings.rst | 47 ++ include/openmc/math_functions.h | 14 + include/openmc/source.h | 136 ++++++ openmc/source.py | 430 ++++++++++++++++++- src/distribution.cpp | 11 +- src/math_functions.cpp | 42 ++ src/source.cpp | 474 ++++++++++++++++++++- tests/cpp_unit_tests/test_distribution.cpp | 25 ++ tests/cpp_unit_tests/test_math.cpp | 15 + tests/unit_tests/test_source_tokamak.py | 263 ++++++++++++ 12 files changed, 1524 insertions(+), 14 deletions(-) create mode 100644 tests/unit_tests/test_source_tokamak.py diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index fb0215916..7091757f2 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -777,9 +777,9 @@ attributes/sub-elements: *Default*: 1.0 :type: - Indicator of source type. One of ``independent``, ``file``, ``compiled``, or - ``mesh``. The type of the source will be determined by this attribute if it - is present. + Indicator of source type. One of ``independent``, ``file``, ``compiled``, + ``mesh``, or ``tokamak``. The type of the source will be determined by this + attribute if it is present. :particle: The source particle type, specified as a PDG number or a string alias (e.g., @@ -1015,6 +1015,80 @@ attributes/sub-elements: mesh element and follows the format for :ref:`source_element`. The number of ```` sub-elements should correspond to the number of mesh elements. + For a source with ``type="tokamak"``, the spatial distribution is described by + a Miller-style flux-surface parameterization and the following sub-elements + are used instead of the ``space`` element: + + :major_radius: + The major radius :math:`R_0` of the plasma in [cm]. + + :minor_radius: + The minor radius :math:`a` of the plasma in [cm]. Must be smaller than + ``major_radius``. + + :elongation: + The plasma elongation :math:`\kappa` (must be > 0). + + :triangularity: + The plasma triangularity :math:`\delta` (must be in [-1, 1]). Negative + values describe negative-triangularity plasmas. + + :shafranov_shift: + The Shafranov shift :math:`\Delta` in [cm] (must be >= 0 and less than + ``minor_radius``/2). + + :r_over_a: + A list of normalized minor-radius grid points :math:`r/a`. Must be strictly + increasing, start at 0, and end at 1. + + :emission_density: + A list of neutron emission densities :math:`S(r)` evaluated at each + ``r_over_a`` grid point (arbitrary units, must be non-negative). Only the + shape matters, since the profile is normalized internally. Values are + interpolated linearly between grid points and the profile is refined on an + internal grid for radial sampling. Must have the same length as + ``r_over_a`` and contain at least one positive value. + + :phi_start: + The starting toroidal angle in [rad]. + + *Default*: 0.0 + + :phi_extent: + The toroidal angle extent in [rad]. The source is sampled uniformly in + :math:`[\phi_\text{start},\ \phi_\text{start} + \phi_\text{extent}]`. + + *Default*: :math:`2\pi` + + :n_alpha: + The number of poloidal-angle grid points used to build the sampling CDFs + (must be > 2). Larger values reduce discretization bias; values below 51 + produce a warning. + + *Default*: 101 + + :vertical_shift: + A vertical shift of the plasma center in [cm]. + + *Default*: 0.0 + + :energy: + For a tokamak source, one or more ``energy`` sub-elements specify the + neutron energy distribution(s). Either a single distribution is given (used + at all radii) or exactly one distribution per ``r_over_a`` grid point is + given, in which case the energy is sampled from one of the two + distributions bracketing the sampled radius, selected stochastically with + probability proportional to the proximity of the radius to each grid point + (stochastic interpolation). Each follows the format of a univariate + probability distribution (see :ref:`univariate`). + + :time: + An optional ``time`` sub-element specifying the time distribution of source + particles, following the format of a univariate probability distribution + (see :ref:`univariate`). + + *Default*: particles are born at :math:`t=0` + .. note:: Biased sampling can be applied to the spatial and energy distributions of a source by using the ```` sub-element (see :ref:`univariate` for details on how to specify bias distributions). diff --git a/docs/source/pythonapi/base.rst b/docs/source/pythonapi/base.rst index b3911c8b3..f06a1eba4 100644 --- a/docs/source/pythonapi/base.rst +++ b/docs/source/pythonapi/base.rst @@ -26,6 +26,7 @@ Simulation Settings openmc.FileSource openmc.CompiledSource openmc.MeshSource + openmc.TokamakSource openmc.SourceParticle openmc.VolumeCalculation openmc.Settings diff --git a/docs/source/usersguide/settings.rst b/docs/source/usersguide/settings.rst index e8514b856..6faeb59e1 100644 --- a/docs/source/usersguide/settings.rst +++ b/docs/source/usersguide/settings.rst @@ -291,6 +291,53 @@ example, the following would generate a photon source:: For a full list of all classes related to statistical distributions, see :ref:`pythonapi_stats`. +Tokamak Plasma Sources +---------------------- + +For fusion applications, the :class:`openmc.TokamakSource` class provides a +native parametric neutron source for tokamak plasmas. Rather than specifying +spatial, angular, and energy distributions separately, the source is defined by +the plasma geometry (using a `Miller-style flux-surface parameterization +`_) and a radial emission profile. Source +sites are sampled directly from the plasma volume without rejection. + +The plasma shape is described by the major radius :math:`R_0`, minor radius +:math:`a`, elongation :math:`\kappa`, triangularity :math:`\delta`, and +Shafranov shift :math:`\Delta`. The neutron birth profile is given as an +emission density :math:`S(r/a)` tabulated on a normalized minor-radius grid that +runs from 0 (magnetic axis) to 1 (last closed flux surface); only the shape of +the profile matters, since it is normalized internally. The emission density is +linearly interpolated between the supplied points and refined internally for +radial sampling. For example:: + + import numpy as np + + r_over_a = np.linspace(0.0, 1.0, 50) + emission = (1.0 - r_over_a**2)**2 # peaked on-axis profile + + source = openmc.TokamakSource( + major_radius=620.0, # cm + minor_radius=200.0, # cm + elongation=1.8, + triangularity=0.45, + shafranov_shift=10.0, # cm + r_over_a=r_over_a, + emission_density=emission, + energy=openmc.stats.muir(e0=14.08e6, m_rat=5.0, kt=20000.0), + ) + + settings.source = source + +The ``energy`` argument accepts either a single +:class:`~openmc.stats.Univariate` distribution applied at all radii, or a +sequence with one distribution per ``r_over_a`` grid point to model a +radially-varying neutron spectrum (energies are then sampled by stochastic +interpolation between the two distributions bracketing the sampled radius). A +time distribution can be given with the ``time`` argument; by default, particles +are born at :math:`t=0`. The toroidal extent can be restricted with +``phi_start`` and ``phi_extent`` to model a sector of the plasma, and +``vertical_shift`` translates the plasma center along the z-axis. + File-based Sources ------------------ diff --git a/include/openmc/math_functions.h b/include/openmc/math_functions.h index f322ad28a..d3bccca7b 100644 --- a/include/openmc/math_functions.h +++ b/include/openmc/math_functions.h @@ -213,6 +213,20 @@ double exprel(double x); //! \return log(1+x)/x without loss of precision near 0 double log1prel(double x); +//! Evaluate the cylindrical Bessel function of the first kind J_n(x) +//! +//! Uses std::cyl_bessel_j where available (e.g., libstdc++). On standard +//! library implementations lacking the C++17 special math functions (e.g., +//! libc++ on Apple Clang/LLVM), falls back to the ascending power series, +//! which converges to machine precision for the small arguments (|x| <= 2) +//! used in OpenMC. Unlike std::cyl_bessel_j, negative arguments are handled +//! via the parity relation J_n(-x) = (-1)^n J_n(x). +//! +//! \param n Non-negative integer order of the Bessel function +//! \param x Real argument +//! \return J_n(x) +double cyl_bessel_j(int n, double x); + //! Helper function to get index and interpolation function on an incident //! energy grid //! diff --git a/include/openmc/source.h b/include/openmc/source.h index e307b1ed2..51b54a1d1 100644 --- a/include/openmc/source.h +++ b/include/openmc/source.h @@ -7,9 +7,12 @@ #include #include #include +#include // for pair #include "pugixml.hpp" +#include "openmc/array.h" +#include "openmc/distribution.h" #include "openmc/distribution_multi.h" #include "openmc/distribution_spatial.h" #include "openmc/memory.h" @@ -260,6 +263,139 @@ private: vector> sources_; //!< Source distributions }; +//============================================================================== +//! Parametric tokamak plasma neutron source +//! +//! This source samples neutron positions from a tokamak plasma geometry using +//! Miller-style flux surface parameterization with user-specified emission +//! profiles and energy distributions. +//! +//! Flux surface parameterization: +//! R = R0 + r*cos(alpha + delta*sin(alpha)) + Delta*(1 - (r/a)^2) +//! Z = kappa * r * sin(alpha) +//! +//! The sampling algorithm: +//! 1. Sample minor radius r from precomputed CDF of S(r) * Jacobian +//! 2. Sample poloidal angle alpha from conditional P(alpha|r) using mixture +//! of precomputed CDFs weighted by functions of r +//! 3. Sample energy and time from user-provided distribution(s) +//! 4. Sample isotropic direction +//! 5. Sample toroidal angle phi uniformly in [phi_start, phi_start + +//! phi_extent] +//! 6. Transform (r, alpha, phi) to Cartesian (x, y, z), applying the optional +//! vertical shift of the plasma center +//! +//! The user provides the emission density S(r) directly (e.g., from transport +//! codes like TRANSP, ASTRA, etc.), allowing full flexibility in reaction +//! physics calculations. S(r) is a profile in arbitrary units sampled on the +//! r_over_a grid; only its shape matters, since it is normalized internally. +//! Energy distributions can be specified as either a single distribution for +//! all r, or one distribution per radial point. +//============================================================================== + +class TokamakSource : public Source { +public: + // Constructors + explicit TokamakSource(pugi::xml_node node); + + //! Sample from the tokamak source distribution + //! \param[inout] seed Pseudorandom seed pointer + //! \return Sampled site + SourceSite sample(uint64_t* seed) const override; + +private: + //========================================================================== + // Private methods + + //! Precompute data structures for efficient sampling + void precompute_sampling_distributions(); + + //! Sample minor radius from marginal CDF + //! \param seed Pseudorandom seed pointer + //! \return Sampled r/a value + double sample_r_over_a(uint64_t* seed) const; + + //! Sample poloidal angle given r using mixture of precomputed CDFs + //! \param r_norm Normalized minor radius r/a + //! \param seed Pseudorandom seed pointer + //! \return Sampled poloidal angle alpha [rad] + double sample_poloidal_angle(double r_norm, uint64_t* seed) const; + + //! Compute the k-th mixture weight w_k(r) * I_hat_k for poloidal sampling + //! \param k Basis function index (0-5) + //! \param r Normalized minor radius r/a + //! \return Mixture weight for component k + double mixture_weight(int k, double r) const; + + //! Sample energy from the distribution(s) + //! \param r_norm Normalized minor radius r/a (for distribution selection) + //! \param seed Pseudorandom seed pointer + //! \return (Sampled energy [eV], importance weight) + std::pair sample_energy(double r_norm, uint64_t* seed) const; + + //! Transform from flux coordinates (r, alpha, phi) to Cartesian (x, y, z) + //! \param r Minor radius [cm] + //! \param alpha Poloidal angle [rad] + //! \param phi Toroidal angle [rad] + //! \return Position in Cartesian coordinates [cm] + Position flux_to_cartesian(double r, double alpha, double phi) const; + + //========================================================================== + // Data members + + // Emission profile (input) + vector r_over_a_; //!< Normalized minor radius grid points + vector emission_density_; //!< Emission density S(r) at grid points + + // Energy distribution(s): either 1 for all r, or one per r point + vector> energy_dists_; + + // Time distribution (defaults to a delta distribution at t=0) + UPtrDist time_; + + // Angular distribution (isotropic) + UPtrAngle angle_; + + // Tokamak geometry parameters + double major_radius_; //!< Major radius R0 [cm] + double minor_radius_; //!< Minor radius a [cm] + double elongation_; //!< Elongation kappa + double triangularity_; //!< Triangularity delta + double shafranov_shift_; //!< Shafranov shift Delta [cm] + double vertical_shift_; //!< Vertical shift of plasma center [cm] + + // Normalized geometry parameters (precomputed for efficiency) + double epsilon_; //!< Inverse aspect ratio a/R0 + double delta_tilde_; //!< Normalized Shafranov shift Delta/a + + // Toroidal angle bounds + double phi_start_; //!< Starting toroidal angle [rad] + double phi_extent_; //!< Toroidal angle extent [rad] + + // Precomputed distribution for radial sampling + unique_ptr radial_dist_; + + // Coefficients of the radial geometric polynomial: A*r - B*r^2 - C*r^3 + // Also used as the analytical normalization for poloidal mixture weights + double radial_poly_a_; //!< 1 + ε·Δ̃ + double radial_poly_b_; //!< (3/8)·c₁·ε + double radial_poly_c_; //!< 2·ε·Δ̃ + + // Precomputed Bernstein basis functions for poloidal angle sampling. + // Using the factorization f(r_tilde, alpha) = R_tilde x J_tilde where: + // R_tilde = b0*(1-r)^2 + 2*b1*r*(1-r) + b2*r^2 (Bernstein quadratic) + // J_tilde = b3*(1-r) + b4*r (Bernstein linear) + // The product gives 6 non-negative basis functions g_k(alpha) with + // weights w_k(r_tilde) that are products of Bernstein polynomials. + // Distributions are tabulated on [0, pi] exploiting up-down symmetry. + static constexpr int N_POLOIDAL_BASIS = 6; //!< Number of basis functions + int n_alpha_; //!< Number of poloidal angle grid points + array, N_POLOIDAL_BASIS> + poloidal_dists_; //!< Distributions for each basis function g_k(alpha) + array + poloidal_integrals_; //!< Integrals of g_k(alpha) over [0, pi] +}; + //============================================================================== // Functions //============================================================================== diff --git a/openmc/source.py b/openmc/source.py index a11cfd6c8..5947540d6 100644 --- a/openmc/source.py +++ b/openmc/source.py @@ -1,7 +1,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from collections.abc import Iterable, Sequence -from numbers import Real +from numbers import Integral, Real from pathlib import Path import warnings from typing import Any @@ -46,7 +46,7 @@ class SourceBase(ABC): Attributes ---------- - type : {'independent', 'file', 'compiled', 'mesh'} + type : {'independent', 'file', 'compiled', 'mesh', 'tokamak'} Indicator of source type. strength : float Strength of the source @@ -203,6 +203,8 @@ class SourceBase(ABC): return FileSource.from_xml_element(elem) elif source_type == 'mesh': return MeshSource.from_xml_element(elem, meshes) + elif source_type == 'tokamak': + return TokamakSource.from_xml_element(elem) else: raise ValueError( f'Source type {source_type} is not recognized') @@ -896,6 +898,430 @@ class FileSource(SourceBase): return cls(**kwargs) +class TokamakSource(SourceBase): + r"""A source representing neutron emission from a tokamak plasma. + + This source samples neutron positions from a tokamak plasma geometry using + Miller-style flux surface parameterization. The user provides an emission + profile S(r/a) as a function of normalized minor radius, along with one or + more energy distributions. + + The flux surface parameterization is + + .. math:: + + \begin{aligned} + R &= R_0 + r \cos\left(\alpha + \delta \sin\alpha\right) + + \Delta \left[1 - \left(\frac{r}{a}\right)^2\right] \\ + Z &= Z_\mathrm{shift} + \kappa r \sin\alpha + \end{aligned} + + where :math:`R_0` is major radius, :math:`a` is minor radius, + :math:`\kappa` is elongation, :math:`\delta` is triangularity, + :math:`\Delta` is the Shafranov shift, and :math:`Z_\mathrm{shift}` is + the vertical shift. + + .. versionadded:: 0.15.4 + + Parameters + ---------- + major_radius : float + Major radius R0 in [cm] + minor_radius : float + Minor radius a in [cm] + elongation : float + Plasma elongation κ (must be > 0) + triangularity : float + Plasma triangularity δ (must be in [-1, 1]) + shafranov_shift : float + Shafranov shift Δ in [cm] (must be >= 0 and < a/2) + r_over_a : numpy.ndarray + Normalized minor radius grid points, must start at 0 and end at 1 + emission_density : numpy.ndarray + Emission density S(r) at each r/a point (arbitrary units, must be >= 0). + Values are linearly interpolated between grid points and refined on an + internal grid for radial sampling. Must have the same length as + ``r_over_a`` and contain at least one positive value. + energy : openmc.stats.Univariate or Sequence[openmc.stats.Univariate] + Energy distribution(s). Either a single distribution used at all radii, + or one distribution per ``r_over_a`` grid point. When one distribution + per grid point is given, the energy of a sampled particle is drawn from + one of the two distributions bracketing its sampled radius, selected + stochastically with probability proportional to the proximity of the + radius to each grid point (stochastic interpolation). + time : openmc.stats.Univariate, optional + Time distribution of the source. If None, particles are born at + :math:`t=0`, matching the default behavior of + :class:`openmc.IndependentSource`. + phi_start : float + Starting toroidal angle in [rad] (default: 0) + phi_extent : float + Toroidal angle extent in [rad] (default: 2π) + n_alpha : int + Number of poloidal angle grid points for CDF sampling (default: 101) + vertical_shift : float + Vertical shift of the plasma center in [cm] (default: 0) + strength : float + Strength of the source (default: 1.0) + constraints : dict + Constraints on sampled source particles. See :class:`SourceBase` for + valid keys and values. + + Attributes + ---------- + major_radius : float + Major radius R0 in [cm] + minor_radius : float + Minor radius a in [cm] + elongation : float + Plasma elongation κ + triangularity : float + Plasma triangularity δ + shafranov_shift : float + Shafranov shift Δ in [cm] + r_over_a : numpy.ndarray + Normalized minor radius grid points + emission_density : numpy.ndarray + Emission density S(r) at each r/a point + energy : list of openmc.stats.Univariate + Energy distribution(s) + time : openmc.stats.Univariate or None + Time distribution of the source + phi_start : float + Starting toroidal angle in [rad] + phi_extent : float + Toroidal angle extent in [rad] + n_alpha : int + Number of poloidal angle grid points + vertical_shift : float + Vertical shift of the plasma center in [cm] + strength : float + Strength of the source + type : str + Indicator of source type: 'tokamak' + constraints : dict + Constraints on sampled source particles + + """ + + def __init__( + self, + major_radius: float, + minor_radius: float, + elongation: float, + triangularity: float, + shafranov_shift: float, + r_over_a: Sequence[float], + emission_density: Sequence[float], + energy: Univariate | Sequence[Univariate], + time: Univariate | None = None, + phi_start: float = 0.0, + phi_extent: float = 2.0 * np.pi, + n_alpha: int = 101, + vertical_shift: float = 0.0, + strength: float = 1.0, + constraints: dict[str, Any] | None = None + ): + super().__init__(strength=strength, constraints=constraints) + self.major_radius = major_radius + self.minor_radius = minor_radius + self.elongation = elongation + self.triangularity = triangularity + self.shafranov_shift = shafranov_shift + self.r_over_a = r_over_a + self.emission_density = emission_density + self.phi_start = phi_start + self.phi_extent = phi_extent + self.n_alpha = n_alpha + self.vertical_shift = vertical_shift + self.energy = energy + self.time = time + + self._validate() + + def _validate(self): + """Validate relationships between tokamak source parameters.""" + if self.minor_radius >= self.major_radius: + raise ValueError( + f"minor_radius ({self.minor_radius}) must be smaller than " + f"major_radius ({self.major_radius})") + if self.shafranov_shift >= 0.5 * self.minor_radius: + raise ValueError( + f"shafranov_shift ({self.shafranov_shift}) must be smaller " + f"than half the minor_radius ({0.5 * self.minor_radius})") + if len(self.emission_density) != len(self.r_over_a): + raise ValueError( + f"emission_density (length {len(self.emission_density)}) must " + f"have the same length as r_over_a (length {len(self.r_over_a)})") + if not np.any(self.emission_density > 0.0): + raise ValueError("emission_density must contain a positive value") + if len(self.energy) not in (1, len(self.r_over_a)): + raise ValueError( + f"Number of energy distributions ({len(self.energy)}) must be " + f"either 1 or equal to the number of r_over_a grid points " + f"({len(self.r_over_a)})") + + @property + def type(self) -> str: + return "tokamak" + + @property + def major_radius(self) -> float: + return self._major_radius + + @major_radius.setter + def major_radius(self, value: float): + cv.check_type('major radius', value, Real) + cv.check_greater_than('major radius', value, 0.0) + self._major_radius = value + + @property + def minor_radius(self) -> float: + return self._minor_radius + + @minor_radius.setter + def minor_radius(self, value: float): + cv.check_type('minor radius', value, Real) + cv.check_greater_than('minor radius', value, 0.0) + self._minor_radius = value + + @property + def elongation(self) -> float: + return self._elongation + + @elongation.setter + def elongation(self, value: float): + cv.check_type('elongation', value, Real) + cv.check_greater_than('elongation', value, 0.0) + self._elongation = value + + @property + def triangularity(self) -> float: + return self._triangularity + + @triangularity.setter + def triangularity(self, value: float): + cv.check_type('triangularity', value, Real) + cv.check_greater_than('triangularity', value, -1.0, equality=True) + cv.check_less_than('triangularity', value, 1.0, equality=True) + self._triangularity = value + + @property + def shafranov_shift(self) -> float: + return self._shafranov_shift + + @shafranov_shift.setter + def shafranov_shift(self, value: float): + cv.check_type('Shafranov shift', value, Real) + cv.check_greater_than('Shafranov shift', value, 0.0, equality=True) + self._shafranov_shift = value + + @property + def r_over_a(self) -> np.ndarray: + return self._r_over_a + + @r_over_a.setter + def r_over_a(self, value: Sequence[float]): + value = np.asarray(value, dtype=float) + if value.ndim != 1 or len(value) < 2: + raise ValueError("r_over_a must be a 1-D array with at least 2 points") + if value[0] != 0.0: + raise ValueError("r_over_a must start at 0") + if value[-1] != 1.0: + raise ValueError("r_over_a must end at 1") + if not np.all(np.diff(value) > 0): + raise ValueError("r_over_a must be strictly increasing") + self._r_over_a = value + + @property + def emission_density(self) -> np.ndarray: + return self._emission_density + + @emission_density.setter + def emission_density(self, value: Sequence[float]): + value = np.asarray(value, dtype=float) + if value.ndim != 1: + raise ValueError("emission_density must be a 1-D array") + if np.any(value < 0): + raise ValueError("emission_density values cannot be negative") + self._emission_density = value + + @property + def energy(self) -> list[Univariate]: + return self._energy + + @energy.setter + def energy(self, value: Univariate | Sequence[Univariate]): + if isinstance(value, Univariate): + self._energy = [value] + else: + cv.check_iterable_type('energy distributions', value, Univariate) + self._energy = list(value) + + @property + def time(self) -> Univariate | None: + return self._time + + @time.setter + def time(self, value: Univariate | None): + if value is not None: + cv.check_type('time distribution', value, Univariate) + self._time = value + + @property + def phi_start(self) -> float: + return self._phi_start + + @phi_start.setter + def phi_start(self, value: float): + cv.check_type('phi_start', value, Real) + self._phi_start = value + + @property + def phi_extent(self) -> float: + return self._phi_extent + + @phi_extent.setter + def phi_extent(self, value: float): + cv.check_type('phi_extent', value, Real) + cv.check_greater_than('phi_extent', value, 0.0) + cv.check_less_than('phi_extent', value, 2.0 * np.pi, equality=True) + self._phi_extent = value + + @property + def n_alpha(self) -> int: + return self._n_alpha + + @n_alpha.setter + def n_alpha(self, value: int): + cv.check_type('n_alpha', value, Integral) + cv.check_greater_than('n_alpha', value, 2) + if value < 51: + warnings.warn( + "n_alpha values below 51 may introduce noticeable " + "discretization bias in tokamak source sampling", stacklevel=2) + self._n_alpha = value + + @property + def vertical_shift(self) -> float: + return self._vertical_shift + + @vertical_shift.setter + def vertical_shift(self, value: float): + cv.check_type('vertical shift', value, Real) + self._vertical_shift = value + + def populate_xml_element(self, element): + """Add necessary tokamak source information to an XML element + + Returns + ------- + element : lxml.etree._Element + XML element containing source data + + """ + self._validate() + + # Geometry parameters + ET.SubElement(element, "major_radius").text = str(self.major_radius) + ET.SubElement(element, "minor_radius").text = str(self.minor_radius) + ET.SubElement(element, "elongation").text = str(self.elongation) + ET.SubElement(element, "triangularity").text = str(self.triangularity) + ET.SubElement(element, "shafranov_shift").text = str(self.shafranov_shift) + + # Toroidal angle bounds + ET.SubElement(element, "phi_start").text = str(self.phi_start) + ET.SubElement(element, "phi_extent").text = str(self.phi_extent) + + # Poloidal sampling resolution + ET.SubElement(element, "n_alpha").text = str(self.n_alpha) + + # Vertical shift + if self.vertical_shift != 0.0: + ET.SubElement(element, "vertical_shift").text = str(self.vertical_shift) + + # Emission profile + ET.SubElement(element, "r_over_a").text = ' '.join(str(r) for r in self.r_over_a) + ET.SubElement(element, "emission_density").text = ' '.join(str(s) for s in self.emission_density) + + # Energy distribution(s) + for dist in self.energy: + element.append(dist.to_xml_element('energy')) + + # Time distribution + if self.time is not None: + element.append(self.time.to_xml_element('time')) + + @classmethod + def from_xml_element(cls, elem: ET.Element) -> TokamakSource: + """Generate tokamak source from an XML element + + Parameters + ---------- + elem : lxml.etree._Element + XML element + + Returns + ------- + openmc.TokamakSource + Source generated from XML element + + """ + # Read geometry parameters + major_radius = float(get_text(elem, 'major_radius')) + minor_radius = float(get_text(elem, 'minor_radius')) + elongation = float(get_text(elem, 'elongation')) + triangularity = float(get_text(elem, 'triangularity')) + shafranov_shift = float(get_text(elem, 'shafranov_shift')) + + # Read optional parameters + phi_start_text = get_text(elem, 'phi_start') + phi_start = float(phi_start_text) if phi_start_text else 0.0 + + phi_extent_text = get_text(elem, 'phi_extent') + phi_extent = float(phi_extent_text) if phi_extent_text else 2.0 * np.pi + + n_alpha_text = get_text(elem, 'n_alpha') + n_alpha = int(n_alpha_text) if n_alpha_text else 101 + + vertical_shift_text = get_text(elem, 'vertical_shift') + vertical_shift = float(vertical_shift_text) if vertical_shift_text else 0.0 + + # Read emission profile + r_over_a = np.array([float(x) for x in get_text(elem, 'r_over_a').split()]) + emission_density = np.array([float(x) for x in get_text(elem, 'emission_density').split()]) + + # Read energy distributions + energy = [Univariate.from_xml_element(e) for e in elem.findall('energy')] + if len(energy) == 1: + energy = energy[0] + + # Read time distribution + time_elem = elem.find('time') + time = Univariate.from_xml_element(time_elem) if time_elem is not None else None + + # Read constraints and strength + constraints = cls._get_constraints(elem) + strength_text = get_text(elem, 'strength') + strength = float(strength_text) if strength_text else 1.0 + + return cls( + major_radius=major_radius, + minor_radius=minor_radius, + elongation=elongation, + triangularity=triangularity, + shafranov_shift=shafranov_shift, + r_over_a=r_over_a, + emission_density=emission_density, + energy=energy, + time=time, + phi_start=phi_start, + phi_extent=phi_extent, + n_alpha=n_alpha, + vertical_shift=vertical_shift, + strength=strength, + constraints=constraints + ) class SourceParticle: diff --git a/src/distribution.cpp b/src/distribution.cpp index 7f5b498ad..cbdd13127 100644 --- a/src/distribution.cpp +++ b/src/distribution.cpp @@ -552,14 +552,9 @@ double Tabular::sample_unbiased(uint64_t* seed) const double c = prn(seed); // Find first CDF bin which is above the sampled value - double c_i = c_[0]; - int i; - std::size_t n = c_.size(); - for (i = 0; i < n - 1; ++i) { - if (c <= c_[i + 1]) - break; - c_i = c_[i + 1]; - } + auto c_iter = std::lower_bound(c_.begin() + 1, c_.end(), c); + int i = std::distance(c_.begin(), c_iter) - 1; + double c_i = c_[i]; // Determine bounding PDF values double x_i = x_[i]; diff --git a/src/math_functions.cpp b/src/math_functions.cpp index 3befa8bc2..ddacc2bd9 100644 --- a/src/math_functions.cpp +++ b/src/math_functions.cpp @@ -1,5 +1,7 @@ #include "openmc/math_functions.h" +#include // for numeric_limits + #include "openmc/external/Faddeeva.hh" #include "openmc/constants.h" @@ -944,6 +946,46 @@ double log1prel(double x) } } +double cyl_bessel_j(int n, double x) +{ + // Handle negative arguments via the parity relation + // J_n(-x) = (-1)^n J_n(x); std::cyl_bessel_j has a domain error for x < 0. + double sign = 1.0; + if (x < 0.0) { + x = -x; + if (n % 2 == 1) + sign = -1.0; + } + +#if defined(__cpp_lib_math_special_functions) && \ + __cpp_lib_math_special_functions >= 201603L + return sign * std::cyl_bessel_j(static_cast(n), x); +#else + // Ascending power series (e.g., Abramowitz & Stegun eq. 9.1.10): + // J_n(x) = sum_{m=0}^inf (-1)^m / (m! (m+n)!) * (x/2)^(2m+n) + // The term ratio is -(x/2)^2 / (m*(m+n)), so for |x| <= 2 the series + // converges to machine precision within ~20 terms. + double half_x = 0.5 * x; + + // First term: (x/2)^n / n! + double term = 1.0; + for (int k = 1; k <= n; ++k) { + term *= half_x / k; + } + + double sum = term; + double neg_half_x_sq = -half_x * half_x; + for (int m = 1; m <= 50; ++m) { + term *= neg_half_x_sq / (m * (m + n)); + sum += term; + if (std::abs(term) <= + std::numeric_limits::epsilon() * std::abs(sum)) + break; + } + return sign * sum; +#endif +} + // Helper function to get index and interpolation function on an incident energy // grid void get_energy_index( diff --git a/src/source.cpp b/src/source.cpp index 1e783b623..12bbdf84e 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -4,7 +4,9 @@ #define HAS_DYNAMIC_LINKING #endif -#include // for move +#include // for max +#include // for sin, cos, abs +#include // for move #ifdef HAS_DYNAMIC_LINKING #include // for dlopen, dlsym, dlclose, dlerror @@ -16,17 +18,20 @@ #include "openmc/bank.h" #include "openmc/capi.h" #include "openmc/cell.h" +#include "openmc/constants.h" #include "openmc/container_util.h" #include "openmc/error.h" #include "openmc/file_utils.h" #include "openmc/geometry.h" #include "openmc/hdf5_interface.h" #include "openmc/material.h" +#include "openmc/math_functions.h" #include "openmc/mcpl_interface.h" #include "openmc/memory.h" #include "openmc/message_passing.h" #include "openmc/mgxs_interface.h" #include "openmc/nuclide.h" +#include "openmc/random_dist.h" #include "openmc/random_lcg.h" #include "openmc/search.h" #include "openmc/settings.h" @@ -100,6 +105,8 @@ unique_ptr Source::create(pugi::xml_node node) return make_unique(node); } else if (source_type == "mesh") { return make_unique(node); + } else if (source_type == "tokamak") { + return make_unique(node); } else { fatal_error(fmt::format("Invalid source type '{}' found.", source_type)); } @@ -673,6 +680,471 @@ SourceSite MeshSource::sample(uint64_t* seed) const return source(element)->sample_with_constraints(seed); } +//============================================================================== +// TokamakSource implementation +//============================================================================== + +TokamakSource::TokamakSource(pugi::xml_node node) : Source(node) +{ + // Read geometry parameters + major_radius_ = std::stod(get_node_value(node, "major_radius")); + minor_radius_ = std::stod(get_node_value(node, "minor_radius")); + elongation_ = std::stod(get_node_value(node, "elongation")); + triangularity_ = std::stod(get_node_value(node, "triangularity")); + shafranov_shift_ = std::stod(get_node_value(node, "shafranov_shift")); + + // Read optional vertical shift + if (check_for_node(node, "vertical_shift")) { + vertical_shift_ = std::stod(get_node_value(node, "vertical_shift")); + } else { + vertical_shift_ = 0.0; + } + + // Read optional toroidal angle bounds + if (check_for_node(node, "phi_start")) { + phi_start_ = std::stod(get_node_value(node, "phi_start")); + } else { + phi_start_ = 0.0; + } + if (check_for_node(node, "phi_extent")) { + phi_extent_ = std::stod(get_node_value(node, "phi_extent")); + } else { + phi_extent_ = 2.0 * PI; + } + if (check_for_node(node, "n_alpha")) { + n_alpha_ = std::stoi(get_node_value(node, "n_alpha")); + } else { + n_alpha_ = 101; // Default + } + + // Read emission profile + r_over_a_ = get_node_array(node, "r_over_a"); + emission_density_ = get_node_array(node, "emission_density"); + + // Read energy distribution(s) + for (auto energy_node : node.children("energy")) { + energy_dists_.push_back(distribution_from_xml(energy_node)); + } + + // Read optional time distribution; default to a delta distribution at t=0 + // for the same behavior as IndependentSource + if (check_for_node(node, "time")) { + time_ = distribution_from_xml(node.child("time")); + } else { + double T[] {0.0}; + double p[] {1.0}; + time_ = UPtrDist {new Discrete {T, p, 1}}; + } + + // Validate inputs + if (emission_density_.size() != r_over_a_.size()) { + fatal_error("TokamakSource: emission_density and r_over_a must have the " + "same length."); + } + if (r_over_a_.size() < 2) { + fatal_error( + "TokamakSource: At least 2 radial points are required for profiles."); + } + if (r_over_a_.front() != 0.0) { + fatal_error("TokamakSource: r_over_a must start at 0."); + } + if (r_over_a_.back() != 1.0) { + fatal_error("TokamakSource: r_over_a must end at 1."); + } + for (size_t i = 1; i < r_over_a_.size(); ++i) { + if (r_over_a_[i] <= r_over_a_[i - 1]) { + fatal_error("TokamakSource: r_over_a must be strictly increasing."); + } + } + for (size_t i = 0; i < emission_density_.size(); ++i) { + if (emission_density_[i] < 0.0) { + fatal_error("TokamakSource: emission_density values cannot be negative."); + } + } + if (major_radius_ <= 0.0) { + fatal_error("TokamakSource: major_radius must be > 0."); + } + if (minor_radius_ <= 0.0) { + fatal_error("TokamakSource: minor_radius must be > 0."); + } + if (minor_radius_ >= major_radius_) { + fatal_error("TokamakSource: minor_radius must be less than major_radius."); + } + if (elongation_ <= 0.0) { + fatal_error("TokamakSource: elongation must be > 0."); + } + if (triangularity_ < -1.0 || triangularity_ > 1.0) { + fatal_error("TokamakSource: triangularity must be in the range [-1, 1]."); + } + if (shafranov_shift_ < 0.0) { + fatal_error("TokamakSource: shafranov_shift must be >= 0."); + } + if (shafranov_shift_ >= 0.5 * minor_radius_) { + fatal_error("TokamakSource: shafranov_shift must be less than half the " + "minor radius."); + } + if (phi_extent_ <= 0.0 || phi_extent_ > 2.0 * PI) { + fatal_error("TokamakSource: phi_extent must be > 0 and <= 2*pi."); + } + if (n_alpha_ <= 2) { + fatal_error("TokamakSource: n_alpha must be > 2."); + } + if (n_alpha_ < 51) { + warning("TokamakSource: n_alpha values below 51 may introduce noticeable " + "discretization bias in source sampling."); + } + if (energy_dists_.empty()) { + fatal_error("TokamakSource: At least one energy distribution is required."); + } + if (energy_dists_.size() != 1 && energy_dists_.size() != r_over_a_.size()) { + fatal_error("TokamakSource: energy distributions must be either 1 (for all " + "r) or match the number of r_over_a points."); + } + + // Compute normalized geometry parameters + epsilon_ = minor_radius_ / major_radius_; + delta_tilde_ = shafranov_shift_ / minor_radius_; + + // Initialize isotropic angular distribution + angle_ = UPtrAngle {new Isotropic()}; + + precompute_sampling_distributions(); +} + +void TokamakSource::precompute_sampling_distributions() +{ + // Use precomputed normalized geometry parameters + double eps = epsilon_; // Inverse aspect ratio (a/R0) + double Dt = delta_tilde_; // Normalized Shafranov shift (Delta/a) + double delta = triangularity_; + + //========================================================================== + // RADIAL CDF (computed first since it's simpler and sampled first) + //========================================================================== + // The marginal radial PDF is obtained by analytically integrating the joint + // distribution f(r_tilde, alpha) over alpha. The result is: + // + // p(r_tilde) ~ S(r_tilde) * [(1 + eps*Dt)*r_tilde + // - (3/8)*c1*eps*r_tilde^2 + // - 2*eps*Dt*r_tilde^3] + // + // where the Bessel function coefficients are: + // c0 = J_0(delta) + J_2(delta) + // c1 = (J_1(2*delta) + J_3(2*delta)) / c0 + // + // For delta -> 0, c0 -> 1 and c1 -> 0, giving the circular cross-section + // limit. + + // Compute Bessel function coefficients. openmc::cyl_bessel_j handles + // negative arguments (negative triangularity) via the parity relation + // J_n(-x) = (-1)^n * J_n(x). + double J0_d = cyl_bessel_j(0, delta); + double J2_d = cyl_bessel_j(2, delta); + double J1_2d = cyl_bessel_j(1, 2.0 * delta); + double J3_2d = cyl_bessel_j(3, 2.0 * delta); + double c0 = J0_d + J2_d; + double c1 = (J1_2d + J3_2d) / c0; + + // Coefficients for the radial polynomial: A*r - B*r^2 - C*r^3 + radial_poly_a_ = 1.0 + eps * Dt; + radial_poly_b_ = 0.375 * c1 * eps; // 3/8 * c1 * eps + radial_poly_c_ = 2.0 * eps * Dt; + + // Build a refined radial grid that retains the user-specified grid points. + // The emission density is interpreted as linear-linear between those points. + constexpr int MIN_SUBINTERVALS = 8; + constexpr double MAX_GRID_SPACING = 1.0e-3; + vector radial_grid {r_over_a_.front()}; + vector radial_emission {emission_density_.front()}; + for (size_t i = 1; i < r_over_a_.size(); ++i) { + double r_lo = r_over_a_[i - 1]; + double r_hi = r_over_a_[i]; + double s_lo = emission_density_[i - 1]; + double s_hi = emission_density_[i]; + int n_subintervals = std::max(MIN_SUBINTERVALS, + static_cast(std::ceil((r_hi - r_lo) / MAX_GRID_SPACING))); + for (int j = 1; j <= n_subintervals; ++j) { + double t = static_cast(j) / n_subintervals; + radial_grid.push_back(r_lo + t * (r_hi - r_lo)); + radial_emission.push_back(s_lo + t * (s_hi - s_lo)); + } + } + + vector radial_pdf(radial_grid.size()); + for (size_t i = 0; i < radial_grid.size(); ++i) { + double r = radial_grid[i]; + // p(r) ~ S(r) * [A*r - B*r^2 - C*r^3] + double geometric_factor = + radial_poly_a_ * r - radial_poly_b_ * r * r - radial_poly_c_ * r * r * r; + radial_pdf[i] = radial_emission[i] * std::max(0.0, geometric_factor); + } + + // Check that the refined profile contains positive probability mass before + // constructing the normalized tabular distribution. + double total = 0.0; + for (size_t i = 1; i < radial_grid.size(); ++i) { + total += 0.5 * (radial_pdf[i - 1] + radial_pdf[i]) * + (radial_grid[i] - radial_grid[i - 1]); + } + if (total <= 0.0) { + fatal_error( + "TokamakSource: Integrated emission density is zero or negative. " + "Check emission_density profile."); + } + radial_dist_ = make_unique(radial_grid.data(), radial_pdf.data(), + radial_grid.size(), Interpolation::lin_lin); + + //========================================================================== + // POLOIDAL CDFs (for conditional sampling of alpha given r) + //========================================================================== + // The conditional distribution P(alpha | r) is a mixture: + // P(alpha | r) ~ sum_k w_k(r) * I_hat_k * p_k(alpha) + // where: + // - w_k(r) are the "dynamic" Bernstein weight functions (depend on r) + // - I_hat_k are the "static" normalized integrals (precomputed constants) + // - p_k(alpha) are the normalized basis distributions (precomputed CDFs) + // + // The static weights I_hat_k = I_k / (2*pi*c0) are: + // I_hat_0 = 1 + eps*Dt + // I_hat_1 = 1 + eps*Dt - (3/16)*c1*eps + // I_hat_2 = 1 - (3/8)*c1*eps + // I_hat_3 = 1 + eps*Dt + // I_hat_4 = 1 + (1/2)*eps*Dt - (3/16)*c1*eps + // I_hat_5 = 1 - eps*Dt - (3/8)*c1*eps + + // Compute static weights analytically + poloidal_integrals_[0] = 1.0 + eps * Dt; + poloidal_integrals_[1] = 1.0 + eps * Dt - 0.1875 * c1 * eps; // 3/16 = 0.1875 + poloidal_integrals_[2] = 1.0 - 0.375 * c1 * eps; // 3/8 = 0.375 + poloidal_integrals_[3] = 1.0 + eps * Dt; + poloidal_integrals_[4] = 1.0 + 0.5 * eps * Dt - 0.1875 * c1 * eps; + poloidal_integrals_[5] = 1.0 - eps * Dt - 0.375 * c1 * eps; + + // Build the alpha grid on [0, pi] (half domain due to up-down symmetry) + int n_alpha = n_alpha_; + vector alpha_grid(n_alpha); + double dalpha = PI / (n_alpha - 1); + for (int i = 0; i < n_alpha; ++i) { + alpha_grid[i] = i * dalpha; + } + + // Compute basis function values g_k(alpha) for tabular distributions + // Using Bernstein form: + // R_tilde = b0*(1-r)^2 + 2*b1*r*(1-r) + b2*r^2 + // J_tilde = b3*(1-r) + b4*r + // with: + // b0(alpha) = 1 + eps*Dt + // b1(alpha) = b0 + (eps/2)*cos(psi), psi = alpha + delta*sin(alpha) + // b2(alpha) = 1 + eps*cos(psi) + // b3(alpha) = cos(delta*sin(alpha)) + // + (delta/4)*(cos(alpha - delta*sin(alpha)) + // - cos(3*alpha + delta*sin(alpha))) + // b4(alpha) = b3(alpha) - 2*Dt*cos(alpha) + + array, N_POLOIDAL_BASIS> basis; + for (int k = 0; k < N_POLOIDAL_BASIS; ++k) { + basis[k].resize(n_alpha); + } + + for (int i = 0; i < n_alpha; ++i) { + double alpha = alpha_grid[i]; + double sin_alpha = std::sin(alpha); + double cos_alpha = std::cos(alpha); + double delta_sin_alpha = delta * sin_alpha; + double psi = alpha + delta_sin_alpha; + double cos_psi = std::cos(psi); + + // Bernstein coefficients b0-b4 + double b0 = 1.0 + eps * Dt; + double b1 = b0 + 0.5 * eps * cos_psi; + double b2 = 1.0 + eps * cos_psi; + double b3 = + std::cos(delta_sin_alpha) + 0.25 * delta * + (std::cos(alpha - delta_sin_alpha) - + std::cos(3.0 * alpha + delta_sin_alpha)); + double b4 = b3 - 2.0 * Dt * cos_alpha; + + // 6 basis functions g_k(alpha) = b_i * b_j + basis[0][i] = b0 * b3; // w0 = (1-r)^3 + basis[1][i] = b1 * b3; // w1 = 2*r*(1-r)^2 + basis[2][i] = b2 * b3; // w2 = r^2*(1-r) + basis[3][i] = b0 * b4; // w3 = r*(1-r)^2 + basis[4][i] = b1 * b4; // w4 = 2*r^2*(1-r) + basis[5][i] = b2 * b4; // w5 = r^3 + } + + // Build a linear-linear distribution for each basis function p_k(alpha) + for (int k = 0; k < N_POLOIDAL_BASIS; ++k) { + poloidal_dists_[k] = make_unique( + alpha_grid.data(), basis[k].data(), n_alpha, Interpolation::lin_lin); + } +} + +double TokamakSource::sample_r_over_a(uint64_t* seed) const +{ + return radial_dist_->sample(seed).first; +} + +double TokamakSource::mixture_weight(int k, double r) const +{ + double s = 1.0 - r; + switch (k) { + case 0: + return s * s * s * poloidal_integrals_[0]; + case 1: + return 2.0 * r * s * s * poloidal_integrals_[1]; + case 2: + return r * r * s * poloidal_integrals_[2]; + case 3: + return r * s * s * poloidal_integrals_[3]; + case 4: + return 2.0 * r * r * s * poloidal_integrals_[4]; + case 5: + return r * r * r * poloidal_integrals_[5]; + default: + UNREACHABLE(); + } +} + +double TokamakSource::sample_poloidal_angle(double r_norm, uint64_t* seed) const +{ + // Sample from the conditional distribution P(alpha | r_tilde) using + // mixture sampling with 6 precomputed basis distributions. + // + // The conditional is: P(alpha | r) ~ sum_k w_k(r) * I_hat_k * p_k(alpha) + // where: + // - w_k(r) are the "dynamic" Bernstein weight functions + // - I_hat_k are the "static" normalized integrals (precomputed in + // poloidal_integrals_) + // - p_k(alpha) are the normalized, precomputed basis distributions + // + // The normalization sum_k w_k(r) * I_hat_k equals the radial geometric + // polynomial evaluated at r, which is known analytically. + // + // Algorithm: + // 1. Compute total from analytical normalization + // 2. Lazily evaluate mixture weights with early exit to select component k + // 3. Sample alpha from the selected basis distribution + + // Analytical normalization: sum_k w_k(r) * I_hat_k + double total = + radial_poly_a_ - radial_poly_b_ * r_norm - radial_poly_c_ * r_norm * r_norm; + double xi = prn(seed) * total; + + // Sample component via lazy evaluation with early exit + // Order optimized for peaked emission profiles: 0, 1, 4, 5, 3, 2 + constexpr int order[] = {0, 1, 4, 5, 3, 2}; + double cumsum = 0.0; + int component = order[N_POLOIDAL_BASIS - 1]; + for (int i = 0; i < N_POLOIDAL_BASIS; ++i) { + cumsum += mixture_weight(order[i], r_norm); + if (xi < cumsum) { + component = order[i]; + break; + } + } + + // Sample alpha from [0, pi] + double alpha = poloidal_dists_[component]->sample(seed).first; + + // Exploit up-down symmetry: randomly flip to [pi, 2*pi] with 50% probability + // This is equivalent to flipping the sign of Z in the final position + if (prn(seed) >= 0.5) { + alpha = 2.0 * PI - alpha; + } + return alpha; +} + +std::pair TokamakSource::sample_energy( + double r_norm, uint64_t* seed) const +{ + if (energy_dists_.size() == 1) { + // Single distribution for all r + return energy_dists_[0]->sample(seed); + } + + // Multiple distributions: stochastic selection between bracketing r points + // Find the interval containing r_norm + size_t i = lower_bound_index(r_over_a_.begin(), r_over_a_.end(), r_norm); + + // Handle boundary cases + if (i >= energy_dists_.size() - 1) { + return energy_dists_.back()->sample(seed); + } + + // Stochastic interpolation: randomly select one of the two bracketing + // distributions based on distance to each + double t = (r_norm - r_over_a_[i]) / (r_over_a_[i + 1] - r_over_a_[i]); + size_t idx = (prn(seed) < t) ? i + 1 : i; + return energy_dists_[idx]->sample(seed); +} + +Position TokamakSource::flux_to_cartesian( + double r, double alpha, double phi) const +{ + // Flux surface parameterization: + // R = R0 + r*cos(alpha + delta*sin(alpha)) + Delta*(1 - (r/a)^2) + // Z = kappa * r * sin(alpha) + // x = R * cos(phi) + // y = R * sin(phi) + // z = Z + + double psi = alpha + triangularity_ * std::sin(alpha); + double r_over_a_sq = (r * r) / (minor_radius_ * minor_radius_); + + double R = + major_radius_ + r * std::cos(psi) + shafranov_shift_ * (1.0 - r_over_a_sq); + double Z = elongation_ * r * std::sin(alpha); + + double x = R * std::cos(phi); + double y = R * std::sin(phi); + double z = Z; + + return {x, y, z}; +} + +SourceSite TokamakSource::sample(uint64_t* seed) const +{ + SourceSite site; + site.particle = ParticleType::neutron(); + site.wgt = 1.0; + site.delayed_group = 0; + + // 1. Sample r/a from radial CDF + double r_norm = sample_r_over_a(seed); + double r = r_norm * minor_radius_; + + // 2. Sample poloidal angle from conditional distribution P(alpha|r) + double alpha = sample_poloidal_angle(r_norm, seed); + + // 3. Sample toroidal angle uniformly in [phi_start, phi_start + phi_extent] + double phi = phi_start_ + phi_extent_ * prn(seed); + + // 4. Convert to Cartesian coordinates + site.r = flux_to_cartesian(r, alpha, phi); + + // 4a. Apply vertical shift if non-zero + if (vertical_shift_ != 0.0) { + site.r.z += vertical_shift_; + } + + // 5. Sample isotropic direction + site.u = angle_->sample(seed).first; + + // 6. Sample energy from distribution(s), applying the importance weight so + // that biased distributions are handled correctly + auto [E, E_wgt] = sample_energy(r_norm, seed); + site.E = E; + + // 7. Sample particle creation time + auto [time, time_wgt] = time_->sample(seed); + site.time = time; + + site.wgt *= E_wgt * time_wgt; + + return site; +} + //============================================================================== // Non-member functions //============================================================================== diff --git a/tests/cpp_unit_tests/test_distribution.cpp b/tests/cpp_unit_tests/test_distribution.cpp index e46088a63..479e7c8a7 100644 --- a/tests/cpp_unit_tests/test_distribution.cpp +++ b/tests/cpp_unit_tests/test_distribution.cpp @@ -82,6 +82,31 @@ TEST_CASE("Test alias sampling method for pugixml constructor") } } +TEST_CASE("Test sampling a large linear-linear tabular distribution") +{ + constexpr int n_points = 10001; + constexpr int n_samples = 200000; + openmc::vector x(n_points); + openmc::vector p(n_points); + for (int i = 0; i < n_points; ++i) { + x[i] = static_cast(i) / (n_points - 1); + p[i] = 2.0 * x[i]; + } + + openmc::Tabular dist( + x.data(), p.data(), n_points, openmc::Interpolation::lin_lin); + uint64_t seed = openmc::init_seed(0, 0); + + double mean = 0.0; + for (int i = 0; i < n_samples; ++i) { + mean += dist.sample(&seed).first; + } + mean /= n_samples; + + // The normalized PDF is 2x on [0, 1], which has a mean of 2/3. + REQUIRE_THAT(mean, Catch::Matchers::WithinAbs(2.0 / 3.0, 0.003)); +} + TEST_CASE("Test construction of SpatialBox with parameters") { openmc::Position ll {-1, -2, -3}; diff --git a/tests/cpp_unit_tests/test_math.cpp b/tests/cpp_unit_tests/test_math.cpp index 983322e4b..7467aa1fe 100644 --- a/tests/cpp_unit_tests/test_math.cpp +++ b/tests/cpp_unit_tests/test_math.cpp @@ -46,6 +46,21 @@ TEST_CASE("Test t_percentile") } } +TEST_CASE("Test cylindrical Bessel functions") +{ + constexpr double x = 2.0; + constexpr double expected[] {0.22389077914123567, 0.57672480775687339, + 0.35283402861563773, 0.12894324947440205}; + + for (int n = 0; n < 4; ++n) { + REQUIRE_THAT(openmc::cyl_bessel_j(n, x), + Catch::Matchers::WithinRel(expected[n], 1.0e-14)); + REQUIRE_THAT(openmc::cyl_bessel_j(n, -x), + Catch::Matchers::WithinRel( + (n % 2 == 0 ? 1.0 : -1.0) * expected[n], 1.0e-14)); + } +} + TEST_CASE("Test calc_pn") { // The reference solutions come from scipy.special.eval_legendre diff --git a/tests/unit_tests/test_source_tokamak.py b/tests/unit_tests/test_source_tokamak.py new file mode 100644 index 000000000..f926c2007 --- /dev/null +++ b/tests/unit_tests/test_source_tokamak.py @@ -0,0 +1,263 @@ +import numpy as np +import pytest + +import openmc +import openmc.stats + +from tests.unit_tests import assert_sample_mean + + +def make_source(**kwargs): + """Build a valid TokamakSource, overriding defaults via kwargs.""" + r_over_a = np.linspace(0.0, 1.0, 10) + params = dict( + major_radius=620.0, + minor_radius=200.0, + elongation=1.8, + triangularity=0.45, + shafranov_shift=10.0, + r_over_a=r_over_a, + emission_density=(1.0 - r_over_a**2), + energy=openmc.stats.muir(e0=14.08e6, m_rat=5.0, kt=2.0e4), + ) + params.update(kwargs) + return openmc.TokamakSource(**params) + + +def test_tokamak_source_roundtrip(): + src = make_source( + phi_start=0.1, phi_extent=np.pi, n_alpha=51, vertical_shift=5.0, + strength=2.0, time=openmc.stats.Uniform(0.0, 1e-6)) + + elem = src.to_xml_element() + assert elem.get('type') == 'tokamak' + + new = openmc.SourceBase.from_xml_element(elem) + assert isinstance(new, openmc.TokamakSource) + assert new.major_radius == src.major_radius + assert new.minor_radius == src.minor_radius + assert new.elongation == src.elongation + assert new.triangularity == src.triangularity + assert new.shafranov_shift == src.shafranov_shift + assert new.phi_start == src.phi_start + assert new.phi_extent == src.phi_extent + assert new.n_alpha == src.n_alpha + assert new.vertical_shift == src.vertical_shift + assert new.strength == src.strength + np.testing.assert_allclose(new.r_over_a, src.r_over_a) + np.testing.assert_allclose(new.emission_density, src.emission_density) + assert len(new.energy) == 1 + assert isinstance(new.time, openmc.stats.Uniform) + assert new.time.a == src.time.a + assert new.time.b == src.time.b + + +def test_tokamak_source_default_time(): + src = make_source() + assert src.time is None + + new = openmc.SourceBase.from_xml_element(src.to_xml_element()) + assert new.time is None + + with pytest.raises(TypeError): + make_source(time=1.0) + + +def test_tokamak_source_multiple_energies(): + r_over_a = np.linspace(0.0, 1.0, 5) + energies = [openmc.stats.muir(e0=14.08e6, m_rat=5.0, kt=kt) + for kt in (1.0e4, 1.5e4, 2.0e4, 2.5e4, 3.0e4)] + src = make_source(r_over_a=r_over_a, + emission_density=np.ones_like(r_over_a), + energy=energies) + assert len(src.energy) == len(r_over_a) + + new = openmc.SourceBase.from_xml_element(src.to_xml_element()) + assert len(new.energy) == len(r_over_a) + + +@pytest.mark.parametrize("kwargs, match", [ + (dict(minor_radius=700.0), "smaller than major_radius"), + (dict(shafranov_shift=150.0), "half the minor_radius"), + (dict(emission_density=np.ones(5)), "same length as r_over_a"), + (dict(energy=[openmc.stats.muir(14.08e6, 5.0, 2.0e4)] * 2), + "Number of energy distributions"), + (dict(r_over_a=np.linspace(0.1, 1.0, 10)), "must start at 0"), + (dict(r_over_a=np.linspace(0.0, 0.9, 10)), "must end at 1"), + (dict(emission_density=-np.linspace(0.0, 1.0, 10)), "cannot be negative"), + (dict(emission_density=np.zeros(10)), "must contain a positive value"), +]) +def test_tokamak_source_invalid(kwargs, match): + with pytest.raises(ValueError, match=match): + make_source(**kwargs) + + +@pytest.mark.parametrize("value", [-1.5, 1.5]) +def test_tokamak_source_invalid_triangularity(value): + with pytest.raises(ValueError): + make_source(triangularity=value) + + +def test_tokamak_source_invalid_n_alpha(): + with pytest.raises(ValueError): + make_source(n_alpha=2) + + +@pytest.mark.parametrize(("attribute", "value", "match"), [ + ("minor_radius", 700.0, "smaller than major_radius"), + ("shafranov_shift", 150.0, "half the minor_radius"), + ("emission_density", np.ones(5), "same length as r_over_a"), + ("energy", [openmc.stats.delta_function(1.0)] * 2, + "Number of energy distributions"), +]) +def test_tokamak_source_mutation_validation(attribute, value, match): + src = make_source() + setattr(src, attribute, value) + with pytest.raises(ValueError, match=match): + src.to_xml_element() + + +@pytest.mark.parametrize(("emission_density", "expected_mean"), [ + ([1.0, 1.0], 2.0 / 3.0), + ([1.0, 0.0], 0.5), +]) +def test_tokamak_source_radial_sampling( + run_in_tmpdir, emission_density, expected_mean +): + """Check radial sampling for profiles on the coarsest valid grid.""" + major_radius = 620.0 + minor_radius = 200.0 + src = make_source( + major_radius=major_radius, + minor_radius=minor_radius, + elongation=1.0, + triangularity=0.0, + shafranov_shift=0.0, + r_over_a=[0.0, 1.0], + emission_density=emission_density, + energy=openmc.stats.delta_function(14.07e6), + ) + + sphere = openmc.Sphere(r=2000.0, boundary_type='vacuum') + model = openmc.Model( + geometry=openmc.Geometry([openmc.Cell(region=-sphere)]), + settings=openmc.Settings( + particles=100, batches=1, run_mode='fixed source', source=src), + ) + + sites = model.sample_external_source(20_000) + xyz = np.array([site.r for site in sites]) + major_r = np.hypot(xyz[:, 0], xyz[:, 1]) + r_over_a = np.hypot(major_r - major_radius, xyz[:, 2]) / minor_radius + assert_sample_mean(r_over_a, expected_mean) + + +def test_tokamak_source_poloidal_sampling(run_in_tmpdir): + """Check linear-linear poloidal sampling on a coarse internal grid.""" + major_radius = 620.0 + minor_radius = 200.0 + with pytest.warns(UserWarning, match="below 51"): + src = make_source( + major_radius=major_radius, + minor_radius=minor_radius, + elongation=1.0, + triangularity=0.0, + shafranov_shift=0.0, + emission_density=np.ones(10), + n_alpha=3, + energy=openmc.stats.delta_function(14.07e6), + ) + + sphere = openmc.Sphere(r=2000.0, boundary_type='vacuum') + model = openmc.Model( + geometry=openmc.Geometry([openmc.Cell(region=-sphere)]), + settings=openmc.Settings( + particles=100, batches=1, run_mode='fixed source', source=src), + ) + + sites = model.sample_external_source(100_000) + xyz = np.array([site.r for site in sites]) + major_r = np.hypot(xyz[:, 0], xyz[:, 1]) + + # With three alpha points, linear interpolation of cos(alpha) on [0, pi] + # gives 1 - 2*alpha/pi. Integrating the resulting density gives this mean. + expected_R = ( + major_radius + + 2.0 * minor_radius**2 / (np.pi**2 * major_radius) + ) + assert_sample_mean(major_r, expected_R) + + +@pytest.mark.flaky(reruns=1) +def test_tokamak_source_sampling(run_in_tmpdir): + """Exercise the compiled C++ sampling path and check invariants. + + Sampled moments are compared against direct numerical quadrature of the + exact source density S(r)*R*|J|, where the Jacobian J of the flux-surface + map is computed from analytic partial derivatives. This check is + independent of the Bernstein-mixture factorization used by the + implementation. + """ + R0, a, kappa, delta = 620.0, 200.0, 1.8, -0.5 + shafranov, zshift = 40.0, 25.0 + phi_start, phi_extent = 0.5, np.pi / 2 + + # Fine grids to make discretization error negligible relative to + # statistical uncertainty + r_over_a = np.linspace(0.0, 1.0, 200) + src = make_source( + major_radius=R0, minor_radius=a, elongation=kappa, + triangularity=delta, shafranov_shift=shafranov, vertical_shift=zshift, + r_over_a=r_over_a, emission_density=1.0 - r_over_a**2, + phi_start=phi_start, phi_extent=phi_extent, n_alpha=201, + energy=openmc.stats.delta_function(14.07e6)) + + sphere = openmc.Sphere(r=2000.0, boundary_type='vacuum') + cell = openmc.Cell(region=-sphere) + settings = openmc.Settings( + particles=100, batches=1, run_mode='fixed source', source=src) + model = openmc.Model(geometry=openmc.Geometry([cell]), settings=settings) + + n_samples = 20_000 + sites = model.sample_external_source(n_samples) + + xyz = np.array([s.r for s in sites]) + R = np.hypot(xyz[:, 0], xyz[:, 1]) + z = xyz[:, 2] + + # Energy, weight, and time invariants + assert np.all([s.E == 14.07e6 for s in sites]) + assert np.all([s.wgt == 1.0 for s in sites]) + assert np.all([s.time == 0.0 for s in sites]) + + # Toroidal angle within the requested sector + phi = np.arctan2(xyz[:, 1], xyz[:, 0]) + assert phi.min() >= phi_start + assert phi.max() <= phi_start + phi_extent + + # Positions bounded by the last closed flux surface + assert R.min() >= R0 - a + assert R.max() <= R0 + a + shafranov + assert np.abs(z - zshift).max() <= kappa * a + + # Up-down symmetry about the vertical shift + assert_sample_mean(z, zshift) + + # Reference moments by 2D quadrature of the exact density + r = np.linspace(0.0, 1.0, 1001)[:, np.newaxis] # r/a + alpha = np.linspace(0.0, 2 * np.pi, 2001)[np.newaxis, :] + psi = alpha + delta * np.sin(alpha) + R_map = R0 + a * r * np.cos(psi) + shafranov * (1.0 - r**2) + Z_map = kappa * a * r * np.sin(alpha) + dR_dr = a * np.cos(psi) - 2.0 * shafranov * r + dR_da = -a * r * np.sin(psi) * (1.0 + delta * np.cos(alpha)) + dZ_dr = kappa * a * np.sin(alpha) * np.ones_like(psi) + dZ_da = kappa * a * r * np.cos(alpha) + jac = np.abs(dR_dr * dZ_da - dR_da * dZ_dr) + dens = (1.0 - r**2) * R_map * jac + norm = dens.sum() + expected_R = (R_map * dens).sum() / norm + expected_z2 = (Z_map**2 * dens).sum() / norm + + assert_sample_mean(R, expected_R) + assert_sample_mean((z - zshift)**2, expected_z2) From f1fb6721f05a0f0eeda95670e9e96386bc45a445 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 16 Jul 2026 20:25:18 -0500 Subject: [PATCH 665/671] Performance optimizations for shared secondary bank (#4011) --- include/openmc/shared_array.h | 26 +++++++ src/simulation.cpp | 142 +++++++++++++++++++++++++--------- 2 files changed, 130 insertions(+), 38 deletions(-) diff --git a/include/openmc/shared_array.h b/include/openmc/shared_array.h index b309ca3f1..2829a2eb9 100644 --- a/include/openmc/shared_array.h +++ b/include/openmc/shared_array.h @@ -114,6 +114,32 @@ public: data_[size_++] = value; } + //! Increase the size of the container by count elements without assigning + //! values to the new elements. Existing elements are preserved if the + //! container needs to grow. This does not perform any thread safety checks. + // + //! \param count The number of elements to append + //! \return The starting index of the appended range + int64_t extend_uninitialized(int64_t count) + { + int64_t offset = size_; + int64_t new_size = size_ + count; + if (new_size > capacity_) { + int64_t new_capacity = capacity_ == 0 ? 8 : capacity_; + while (new_capacity < new_size) { + new_capacity *= 2; + } + unique_ptr new_data = make_unique(new_capacity); + if (size_ > 0) { + std::copy_n(data_.get(), size_, new_data.get()); + } + data_ = std::move(new_data); + capacity_ = new_capacity; + } + size_ = new_size; + return offset; + } + //! Return the number of elements in the container int64_t size() { return size_; } int64_t size() const { return size_; } diff --git a/src/simulation.cpp b/src/simulation.cpp index cfc2b43e6..03f40a726 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -12,6 +12,7 @@ #include "openmc/material.h" #include "openmc/message_passing.h" #include "openmc/nuclide.h" +#include "openmc/openmp_interface.h" #include "openmc/output.h" #include "openmc/particle.h" #include "openmc/photon.h" @@ -41,6 +42,7 @@ #include #include +#include #include //============================================================================== @@ -354,6 +356,94 @@ int64_t simulation_tracks_completed {0}; } // namespace simulation +namespace { + +//! Collect thread-local secondary banks into the shared secondary bank in +//! sorted order. +//! +//! \param thread_banks Secondary banks produced by each OpenMP thread +void collect_sorted_history_secondary_banks( + vector>& thread_banks) +{ + // Count the total number of all secondary sites produced + int64_t n_collected = 0; + for (const auto& bank : thread_banks) { + n_collected += bank.size(); + } + + // Count the expected number of progeny from per-parent progeny counts + int64_t n_progeny = 0; + for (int64_t count : simulation::progeny_per_particle) { + n_progeny += count; + } + + if (n_collected != n_progeny) { + fatal_error("Mismatch detected between sum of all particle progeny and " + "secondary bank size during collection."); + } + + // Convert per-parent progeny counts to offsets into the sorted bank + std::exclusive_scan(simulation::progeny_per_particle.begin(), + simulation::progeny_per_particle.end(), + simulation::progeny_per_particle.begin(), 0); + + // Allocate the shared bank once for the complete generation + simulation::shared_secondary_bank_write.resize(0); + simulation::shared_secondary_bank_write.extend_uninitialized(n_progeny); + + // Place each secondary according to its parent and progeny identifiers + for (const auto& bank : thread_banks) { + for (const auto& site : bank) { + if (site.parent_id < 0 || + site.parent_id >= + static_cast(simulation::progeny_per_particle.size())) { + fatal_error(fmt::format("Invalid parent_id {} for banked site " + "(expected range [0, {})).", + site.parent_id, simulation::progeny_per_particle.size())); + } + int64_t idx = + simulation::progeny_per_particle[site.parent_id] + site.progeny_id; + if (idx < 0 || idx >= n_progeny) { + fatal_error("Mismatch detected between sum of all particle progeny and " + "secondary bank size during collection."); + } + simulation::shared_secondary_bank_write[idx] = site; + } + } +} + +//! Collect particle-local secondary banks into the shared secondary bank. +//! +//! \param n_particles Number of particles in the active event-based buffer +void collect_event_secondary_banks(int64_t n_particles) +{ + // Compute offsets for each particle's local secondary bank. + vector offsets(n_particles); + int64_t total = 0; + for (int64_t i = 0; i < n_particles; ++i) { + offsets[i] = total; + total += simulation::particles[i].local_secondary_bank().size(); + } + + // Extend the shared bank once for all collected secondaries + int64_t bank_offset = + simulation::shared_secondary_bank_write.extend_uninitialized(total); + + // Copy each local bank into its assigned range and clear the local storage +#pragma omp parallel for schedule(static) + for (int64_t i = 0; i < n_particles; ++i) { + auto& local_bank = simulation::particles[i].local_secondary_bank(); + if (!local_bank.empty()) { + std::copy(local_bank.cbegin(), local_bank.cend(), + simulation::shared_secondary_bank_write.data() + bank_offset + + offsets[i]); + local_bank.clear(); + } + } +} + +} // namespace + //============================================================================== // Non-member functions //============================================================================== @@ -921,11 +1011,13 @@ void transport_history_based_shared_secondary() std::fill(simulation::progeny_per_particle.begin(), simulation::progeny_per_particle.end(), 0); + vector> thread_banks(num_threads()); + // Phase 1: Transport primary particles and deposit first generation of // secondaries in the shared secondary bank #pragma omp parallel { - vector thread_bank; + auto& thread_bank = thread_banks[thread_num()]; Particle p; #pragma omp for schedule(runtime) @@ -937,15 +1029,9 @@ void transport_history_based_shared_secondary() } p.local_secondary_bank().clear(); } - - // Drain thread-local bank into the shared secondary bank (once per thread) -#pragma omp critical(SharedSecondaryBank) - { - for (auto& site : thread_bank) { - simulation::shared_secondary_bank_write.thread_unsafe_append(site); - } - } } + collect_sorted_history_secondary_banks(thread_banks); + thread_banks.clear(); simulation::simulation_tracks_completed += settings::n_particles; @@ -955,10 +1041,6 @@ void transport_history_based_shared_secondary() int64_t alive_secondary = 1; while (alive_secondary) { - // Sort the shared secondary bank by parent ID then progeny ID to - // ensure reproducibility. - sort_bank(simulation::shared_secondary_bank_write, false); - // Synchronize the shared secondary bank amongst all MPI ranks, such // that each MPI rank has an approximately equal number of secondary // tracks. Also reports the total number of secondaries alive across @@ -987,11 +1069,12 @@ void transport_history_based_shared_secondary() simulation::shared_secondary_bank_read.size()); std::fill(simulation::progeny_per_particle.begin(), simulation::progeny_per_particle.end(), 0); + thread_banks.resize(num_threads()); // Transport all secondary tracks from the shared secondary bank #pragma omp parallel { - vector thread_bank; + auto& thread_bank = thread_banks[thread_num()]; Particle p; #pragma omp for schedule(runtime) @@ -1006,17 +1089,12 @@ void transport_history_based_shared_secondary() } p.local_secondary_bank().clear(); } - - // Drain thread-local bank into the shared secondary bank (once per - // thread) -#pragma omp critical(SharedSecondaryBank) - { - for (auto& secondary_site : thread_bank) { - simulation::shared_secondary_bank_write.thread_unsafe_append( - secondary_site); - } - } } // End of transport loop over tracks in shared secondary bank + simulation::shared_secondary_bank_write = + std::move(simulation::shared_secondary_bank_read); + simulation::shared_secondary_bank_read = SharedArray(); + collect_sorted_history_secondary_banks(thread_banks); + thread_banks.clear(); n_generation_depth++; simulation::simulation_tracks_completed += alive_secondary; } // End of loop over secondary generations @@ -1080,13 +1158,7 @@ void transport_event_based_shared_secondary() process_transport_events(); process_death_events(n_particles); - // Collect secondaries from all particle buffers into shared bank - for (int64_t i = 0; i < n_particles; i++) { - for (auto& site : simulation::particles[i].local_secondary_bank()) { - simulation::shared_secondary_bank_write.thread_unsafe_append(site); - } - simulation::particles[i].local_secondary_bank().clear(); - } + collect_event_secondary_banks(n_particles); remaining_work -= n_particles; source_offset += n_particles; @@ -1150,13 +1222,7 @@ void transport_event_based_shared_secondary() process_transport_events(); process_death_events(n_particles); - // Collect secondaries from all particle buffers into shared bank - for (int64_t i = 0; i < n_particles; i++) { - for (auto& site : simulation::particles[i].local_secondary_bank()) { - simulation::shared_secondary_bank_write.thread_unsafe_append(site); - } - simulation::particles[i].local_secondary_bank().clear(); - } + collect_event_secondary_banks(n_particles); sec_remaining -= n_particles; sec_offset += n_particles; From d3bc1669d3933d04898fa51e2030a8413b7f59bd Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Fri, 17 Jul 2026 15:40:16 +0300 Subject: [PATCH 666/671] Access element subshell map data only if it has atomic relaxation data (#4020) --- src/physics.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/physics.cpp b/src/physics.cpp index dcc771329..4a8b4d368 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -359,8 +359,8 @@ void sample_photon_reaction(Particle& p) // Allow electrons to fill orbital and produce Auger electrons and // fluorescent photons. Since Compton subshell data does not match atomic // relaxation data, use the mapping between the data to find the subshell - if (settings::atomic_relaxation && i_shell >= 0 && - element.subshell_map_[i_shell] >= 0) { + if (settings::atomic_relaxation && element.has_atomic_relaxation_ && + i_shell >= 0 && element.subshell_map_[i_shell] >= 0) { element.atomic_relaxation(element.subshell_map_[i_shell], p); } From 796ae384b80480bda03b33bfa85a90cb510d1b50 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Fri, 17 Jul 2026 16:18:53 +0300 Subject: [PATCH 667/671] Fix not reseting filter_matches corrupt pulse height results (#4019) Co-authored-by: Paul Romano --- src/tallies/tally_scoring.cpp | 34 +++++++-------- tests/unit_tests/test_pulse_height.py | 59 +++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 16 deletions(-) create mode 100644 tests/unit_tests/test_pulse_height.py diff --git a/src/tallies/tally_scoring.cpp b/src/tallies/tally_scoring.cpp index 7bce6ec13..241cbf008 100644 --- a/src/tallies/tally_scoring.cpp +++ b/src/tallies/tally_scoring.cpp @@ -2730,6 +2730,9 @@ void score_pulse_height_tally(Particle& p, const vector& tallies) int orig_cell = p.coord(0).cell(); double orig_E_last = p.E_last(); + // Set particle in top level + p.n_coord() = 1; + for (auto i_tally : tallies) { auto& tally {*model::tallies[i_tally]}; @@ -2740,7 +2743,6 @@ void score_pulse_height_tally(Particle& p, const vector& tallies) for (auto cell_id : cells) { // Temporarily change cell of particle - p.n_coord() = 1; p.coord(0).cell() = cell_id; // Determine index of cell in model::pulse_height_cells @@ -2756,20 +2758,20 @@ void score_pulse_height_tally(Particle& p, const vector& tallies) // 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; + if (filter_iter != end) { - // 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; + tally.results_(filter_index, score_index, TallyResult::VALUE) += + filter_weight; + } } } @@ -2777,10 +2779,10 @@ void score_pulse_height_tally(Particle& p, const vector& tallies) for (auto& match : p.filter_matches()) match.bins_present_ = false; } - // Restore cell/energy - p.n_coord() = orig_n_coord; - p.coord(0).cell() = orig_cell; - p.E_last() = orig_E_last; } + // Restore cell/energy + p.n_coord() = orig_n_coord; + p.coord(0).cell() = orig_cell; + p.E_last() = orig_E_last; } } // namespace openmc diff --git a/tests/unit_tests/test_pulse_height.py b/tests/unit_tests/test_pulse_height.py new file mode 100644 index 000000000..1f27cc6f2 --- /dev/null +++ b/tests/unit_tests/test_pulse_height.py @@ -0,0 +1,59 @@ +import numpy as np +import pytest +import openmc + + +@pytest.fixture +def model(): + openmc.reset_auto_ids() + model = openmc.Model() + + # Define materials + NaI = openmc.Material() + NaI.set_density('g/cm3', 3.7) + NaI.add_element('Na', 1.0) + NaI.add_element('I', 1.0) + + # Define geometry: two spheres in each other + s1 = openmc.Sphere(r=1) + s2 = openmc.Sphere(r=2, boundary_type='vacuum') + inner_sphere = openmc.Cell(name='inner sphere', fill=NaI, region=-s1) + outer_sphere = openmc.Cell(name='outer sphere', fill=NaI, region=+s1 & -s2) + model.geometry = openmc.Geometry([inner_sphere, outer_sphere]) + + # Define settings + model.settings.run_mode = 'fixed source' + model.settings.batches = 1 + model.settings.particles = 10000 + model.settings.photon_transport = True + model.settings.source = openmc.IndependentSource( + energy=openmc.stats.delta_function(1e6), + particle='photon' + ) + + # Define tallies + energy_filter = openmc.EnergyFilter([1e3, 1e7]) + + tally1 = openmc.Tally() + tally1.scores = ['pulse-height'] + cell_filter1 = openmc.CellFilter([inner_sphere, outer_sphere]) + tally1.filters = [cell_filter1, energy_filter] + + tally2 = openmc.Tally() + tally2.scores = ['pulse-height'] + cell_filter2 = openmc.CellFilter([outer_sphere, inner_sphere]) + tally2.filters = [cell_filter2, energy_filter] + + model.tallies = [tally1, tally2] + return model + + +def test_pulse_height(model, run_in_tmpdir): + sp_path = model.run() + sp = openmc.StatePoint(sp_path) + t1 = sp.tallies[1].mean.squeeze() + t2 = sp.tallies[2].mean.squeeze() + + np.testing.assert_array_equal(t1, t2[::-1]) + + From db673b9acb400d15be3df98ea0492486ff820866 Mon Sep 17 00:00:00 2001 From: Joffrey Dorville <54550047+JoffreyDorville@users.noreply.github.com> Date: Fri, 17 Jul 2026 10:47:40 -0500 Subject: [PATCH 668/671] Automatic C++ doc generation (#3950) Co-authored-by: Paul Romano --- .gitignore | 1 + .readthedocs.yaml | 2 ++ docs/Makefile | 1 + docs/doxygen/Doxyfile | 13 +++++++++ docs/source/capi/index.rst | 43 +++++++---------------------- docs/source/conf.py | 13 ++++++++- docs/source/devguide/docbuild.rst | 5 ++++ docs/source/io_formats/geometry.rst | 6 ++-- include/openmc/capi.h | 29 ++++++++++++++++++- pyproject.toml | 1 + 10 files changed, 76 insertions(+), 38 deletions(-) create mode 100644 docs/doxygen/Doxyfile diff --git a/.gitignore b/.gitignore index dd8dfb14a..32ef7919a 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ examples/**/*.xml # Documentation builds docs/build +docs/doxygen/xml docs/source/_images/*.pdf docs/source/_images/*.aux docs/source/pythonapi/generated/ diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 61301bdb4..7594ed5f2 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -7,6 +7,8 @@ build: jobs: post_checkout: - git fetch --unshallow || true + - cd docs/doxygen && doxygen && cd - + sphinx: configuration: docs/source/conf.py diff --git a/docs/Makefile b/docs/Makefile index a93338df3..e320432a2 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -45,6 +45,7 @@ help: clean: -rm -rf $(BUILDDIR)/* -rm -rf source/pythonapi/generated/ + -rm -rf doxygen/xml html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html diff --git a/docs/doxygen/Doxyfile b/docs/doxygen/Doxyfile new file mode 100644 index 000000000..75702eff7 --- /dev/null +++ b/docs/doxygen/Doxyfile @@ -0,0 +1,13 @@ +# Doxyfile 1.9.1 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project. + +# Difference with default Doxyfile 1.9.1 +PROJECT_NAME = OpenMC +QUIET = YES +WARN_IF_UNDOCUMENTED = NO +INPUT = ../../include/openmc/capi.h +GENERATE_HTML = NO +GENERATE_LATEX = NO +GENERATE_XML = YES diff --git a/docs/source/capi/index.rst b/docs/source/capi/index.rst index 1777e795a..e924aa6e2 100644 --- a/docs/source/capi/index.rst +++ b/docs/source/capi/index.rst @@ -46,43 +46,20 @@ Type Definitions Functions --------- -.. c:function:: int openmc_calculate_volumes() +.. + Once documentation is complete in capi.h, use: + .. doxygenfile:: capi.h + to populate this documentation without using + .. doxygenfunction:: + for every function. - Run a stochastic volume calculation +.. doxygenfunction:: openmc_calculate_volumes - :return: Return status (negative if an error occurred) - :rtype: int +.. doxygenfunction:: openmc_cell_get_fill -.. c:function:: int openmc_cell_get_fill(int32_t index, int* type, int32_t** indices, int32_t* n) +.. doxygenfunction:: openmc_cell_get_id - Get the fill for a cell - - :param int32_t index: Index in the cells array - :param int* type: Type of the fill - :param int32_t** indices: Array of material indices for cell - :param int32_t* n: Length of indices array - :return: Return status (negative if an error occurred) - :rtype: int - -.. c:function:: int openmc_cell_get_id(int32_t index, int32_t* id) - - Get the ID of a cell - - :param int32_t index: Index in the cells array - :param int32_t* id: ID of the cell - :return: Return status (negative if an error occurred) - :rtype: int - -.. c:function:: int openmc_cell_get_temperature(int32_t index, const int32_t* instance, double* T) - - Get the temperature of a cell - - :param int32_t index: Index in the cells array - :param int32_t* instance: Which instance of the cell. If a null pointer is passed, the temperature - of the first instance is returned. - :param double* T: temperature of the cell - :return: Return status (negative if an error occurred) - :rtype: int +.. doxygenfunction:: openmc_cell_get_temperature .. c:function:: int openmc_cell_get_density(int32_t index, const int32_t* instance, double* density) diff --git a/docs/source/conf.py b/docs/source/conf.py index 3a2e7fb93..61740239b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -11,7 +11,10 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import os +from pathlib import Path +import subprocess +import sys # Determine if we're on Read the Docs server on_rtd = os.environ.get('READTHEDOCS', None) == 'True' @@ -37,6 +40,7 @@ sys.path.insert(0, os.path.abspath('../..')) # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ + 'breathe', 'sphinx.ext.autodoc', 'sphinx.ext.napoleon', 'sphinx.ext.autosummary', @@ -47,6 +51,8 @@ extensions = [ ] if not on_rtd: extensions.append('sphinxcontrib.rsvgconverter') + doxygen_dir = Path(__file__).parents[1] / 'doxygen' + subprocess.run(['doxygen'], cwd=doxygen_dir, check=True) # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -117,6 +123,11 @@ pygments_style = 'tango' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] +# -- Options breathe + doxygen ------------------------------------------------- + +breathe_projects = {"OpenMC": "../doxygen/xml"} +breathe_default_project = "OpenMC" +breathe_domain_by_file_pattern = {"*capi.h": "c"} # -- Options for HTML output --------------------------------------------------- diff --git a/docs/source/devguide/docbuild.rst b/docs/source/devguide/docbuild.rst index f723db06e..cda0307ca 100644 --- a/docs/source/devguide/docbuild.rst +++ b/docs/source/devguide/docbuild.rst @@ -14,6 +14,11 @@ Python API. That is, from the root directory of the OpenMC repository: python -m pip install ".[docs]" +The OpenMC documentation also uses Doxygen to automatically generate its +C/C++ API documentation directly from the docstrings available in the source +code. You will need to have a working installation of Doxygen to generate the +documentation locally. + ----------------------------------- Building Documentation as a Webpage ----------------------------------- diff --git a/docs/source/io_formats/geometry.rst b/docs/source/io_formats/geometry.rst index 60ff0afae..5cd18bef1 100644 --- a/docs/source/io_formats/geometry.rst +++ b/docs/source/io_formats/geometry.rst @@ -425,13 +425,13 @@ Each ```` element can have the following attributes or sub-eleme material) assignment. Required. :temperature: - Temperature(s) in [K] to assign to the cell. Must be ≥ 0. Multiple - space-separated values may be given. + Temperature(s) in [K] to assign to the cell. Must be greater than or equal + to 0. Multiple space-separated values may be given. *Default*: None :density: - Density in [g/cm³] to assign to the cell. Must be > 0. Requires a non-void + Density in [g/cm³] to assign to the cell. Must be greater than 0. Requires a non-void material fill. Multiple space-separated values may be given. *Default*: None diff --git a/include/openmc/capi.h b/include/openmc/capi.h index 6b78145a4..9f6987d74 100644 --- a/include/openmc/capi.h +++ b/include/openmc/capi.h @@ -9,14 +9,41 @@ extern "C" { #endif +//! Run a stochastic volume calculation +// +//! \return Status (negative if an error occurred) int openmc_calculate_volumes(); + int openmc_cell_filter_get_bins( int32_t index, const int32_t** cells, int32_t* n); + +//! Get the fill for a cell +// +//! \param index Index in the cells array +//! \param type Type of the fill +//! \param indices Array of material indices for cell +//! \param n Length of indices array +//! \return Status (negative if an error occurred) int openmc_cell_get_fill( int32_t index, int* type, int32_t** indices, int32_t* n); + +//! Get the ID of a cell +// +//! \param index Index in the cells array +//! \param id ID of the cell +//! \return Status (negative if an error occurred) int openmc_cell_get_id(int32_t index, int32_t* id); + +//! Get the temperature of a cell +// +//! \param index Index in the cells array +//! \param instance Which instance of the cell. If a null pointer is +//! passed, the temperature of the first instance is returned. +//! \param T temperature of the cell +//!\return Status (negative if an error occurred) int openmc_cell_get_temperature( int32_t index, const int32_t* instance, double* T); + int openmc_cell_get_density( int32_t index, const int32_t* instance, double* rho); int openmc_cell_get_translation(int32_t index, double xyz[]); @@ -280,7 +307,7 @@ int openmc_zernike_filter_set_params( int openmc_particle_filter_get_bins(int32_t idx, int32_t bins[]); //! Sets the mesh and energy grid for CMFD reweight -//! \param[in] meshtyally_id id of CMFD Mesh Tally +//! \param[in] meshtally_id id of CMFD Mesh Tally //! \param[in] cmfd_indices indices storing spatial and energy dimensions of //! CMFD problem \param[in] norm CMFD normalization factor void openmc_initialize_mesh_egrid( diff --git a/pyproject.toml b/pyproject.toml index 098487c06..c769fb7d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ dependencies = [ [project.optional-dependencies] depletion-mpi = ["mpi4py"] docs = [ + "breathe", "sphinx", "sphinxcontrib-katex", "sphinx-numfig", From 05d01274a7ae5f999300843c6604a6cddf2b6546 Mon Sep 17 00:00:00 2001 From: Mazeyar Moeini Feizabadi Date: Sun, 19 Jul 2026 21:49:48 +0200 Subject: [PATCH 669/671] Add analytic tests for ray-traced intersection distances (#4014) --- CMakeLists.txt | 2 +- tests/cpp_unit_tests/CMakeLists.txt | 1 + tests/cpp_unit_tests/test_ray.cpp | 117 ++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 tests/cpp_unit_tests/test_ray.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d5af01610..61d2cc6c5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -532,7 +532,7 @@ else() endif() if(OPENMC_USE_DAGMC) - target_compile_definitions(libopenmc PRIVATE OPENMC_DAGMC_ENABLED) + target_compile_definitions(libopenmc PUBLIC OPENMC_DAGMC_ENABLED) target_link_libraries(libopenmc dagmc-shared) if(OPENMC_USE_UWUW) diff --git a/tests/cpp_unit_tests/CMakeLists.txt b/tests/cpp_unit_tests/CMakeLists.txt index ce7e539ea..3eb88f023 100644 --- a/tests/cpp_unit_tests/CMakeLists.txt +++ b/tests/cpp_unit_tests/CMakeLists.txt @@ -6,6 +6,7 @@ set(TEST_NAMES test_math test_mcpl_stat_sum test_mesh + test_ray test_region test_tensor # Add additional unit test files here diff --git a/tests/cpp_unit_tests/test_ray.cpp b/tests/cpp_unit_tests/test_ray.cpp new file mode 100644 index 000000000..c043c4d25 --- /dev/null +++ b/tests/cpp_unit_tests/test_ray.cpp @@ -0,0 +1,117 @@ +#include +#include + +#include +#include +#include + +#include "openmc/geometry.h" +#include "openmc/geometry_aux.h" +#include "openmc/ray.h" +#include "openmc/settings.h" +#include "openmc/surface.h" + +namespace { + +using Catch::Matchers::WithinAbs; + +constexpr double DISTANCE_TOLERANCE = 1.0e-12; + +class SphericalShellFixture { +public: + SphericalShellFixture() + : run_mode_(openmc::settings::run_mode), + root_universe_(openmc::model::root_universe), + n_coord_levels_(openmc::model::n_coord_levels) + { + openmc::settings::run_mode = openmc::RunMode::PLOTTING; + + constexpr auto geometry = R"( + + + + + + + )"; + + auto result = document_.load_string(geometry); + REQUIRE(result); + openmc::read_geometry_xml(document_.document_element()); + openmc::finalize_geometry(); + openmc::finalize_cell_densities(); + } + + ~SphericalShellFixture() + { + openmc::free_memory_geometry(); + openmc::free_memory_surfaces(); + openmc::model::universe_level_counts.clear(); + openmc::model::root_universe = root_universe_; + openmc::model::n_coord_levels = n_coord_levels_; + openmc::settings::run_mode = run_mode_; + } + +private: + pugi::xml_document document_; + openmc::RunMode run_mode_; + int root_universe_; + int n_coord_levels_; +}; + +class RecordingRay : public openmc::Ray { +public: + using Ray::Ray; + + void on_intersection() override + { + intersections.emplace_back( + traversal_distance_, boundary().surface_index() + 1); + } + + openmc::vector> intersections; +}; + +} // namespace + +TEST_CASE_METHOD(SphericalShellFixture, "Trace a single chord through a sphere") +{ + constexpr double impact_parameter = 1.5; + const double half_chord = + std::sqrt(4.0 - impact_parameter * impact_parameter); + + RecordingRay ray({-3.0, impact_parameter, 0.0}, {1.0, 0.0, 0.0}); + ray.trace(); + + REQUIRE(ray.intersections.size() == 2); + CHECK(ray.intersections[0].second == 2); + CHECK_THAT(ray.intersections[0].first, WithinAbs(0.0, DISTANCE_TOLERANCE)); + CHECK(ray.intersections[1].second == 2); + CHECK_THAT(ray.intersections[1].first, + WithinAbs(2.0 * half_chord, DISTANCE_TOLERANCE)); +} + +TEST_CASE_METHOD( + SphericalShellFixture, "Trace both segments of a spherical shell") +{ + constexpr double impact_parameter = 0.5; + const double outer = std::sqrt(4.0 - impact_parameter * impact_parameter); + const double inner = std::sqrt(1.0 - impact_parameter * impact_parameter); + + RecordingRay ray({-3.0, impact_parameter, 0.0}, {1.0, 0.0, 0.0}); + ray.trace(); + + REQUIRE(ray.intersections.size() == 4); + CHECK(ray.intersections[0].second == 2); + CHECK_THAT(ray.intersections[0].first, WithinAbs(0.0, DISTANCE_TOLERANCE)); + CHECK(ray.intersections[1].second == 1); + CHECK_THAT( + ray.intersections[1].first, WithinAbs(outer - inner, DISTANCE_TOLERANCE)); + CHECK(ray.intersections[2].second == 1); + CHECK_THAT( + ray.intersections[2].first, WithinAbs(outer + inner, DISTANCE_TOLERANCE)); + CHECK(ray.intersections[3].second == 2); + CHECK_THAT( + ray.intersections[3].first, WithinAbs(2.0 * outer, DISTANCE_TOLERANCE)); +} From 61c8a59cff0806384ee7c2662af9c995362a30d6 Mon Sep 17 00:00:00 2001 From: GuySten <62616591+GuySten@users.noreply.github.com> Date: Mon, 20 Jul 2026 17:52:30 +0300 Subject: [PATCH 670/671] Override NJOY default damage energy threshold (#4022) Co-authored-by: Paul Romano --- openmc/data/njoy.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openmc/data/njoy.py b/openmc/data/njoy.py index ddc68cc37..7c99637f0 100644 --- a/openmc/data/njoy.py +++ b/openmc/data/njoy.py @@ -259,14 +259,14 @@ broadr / %%%%%%%%%%%%%%%%%%%%%%% Doppler broaden XS %%%%%%%%%%%%%%%%%%%%%%%%%%%% _TEMPLATE_HEATR = """ heatr / %%%%%%%%%%%%%%%%%%%%%%%%% Add heating kerma %%%%%%%%%%%%%%%%%%%%%%%%%%%% {nendf} {nheatr_in} {nheatr} / -{mat} 4 0 0 0 / +{mat} 4 0 0 0 0 {ed}/ 302 318 402 444 / """ _TEMPLATE_HEATR_LOCAL = """ heatr / %%%%%%%%%%%%%%%%% Add heating kerma (local photons) %%%%%%%%%%%%%%%%%%%% {nendf} {nheatr_in} {nheatr_local} / -{mat} 4 0 0 1 / +{mat} 4 0 0 1 0 {ed}/ 302 318 402 444 / """ @@ -411,7 +411,7 @@ def make_pendf(filename, pendf='pendf', **kwargs): def make_ace(filename, temperatures=None, acer=True, xsdir=None, output_dir=None, pendf=False, error=0.001, broadr=True, heatr=True, gaspr=True, purr=True, evaluation=None, - smoothing=True, **kwargs): + smoothing=True, displacement_energy=None, **kwargs): """Generate incident neutron ACE file from an ENDF file File names can be passed to @@ -463,6 +463,9 @@ def make_ace(filename, temperatures=None, acer=True, xsdir=None, indicates which evaluation should be used. smoothing : bool, optional If the smoothing option (ACER card 6) is on (True) or off (False). + displacement_energy : float, optional + Threshold displacement energy in [eV]. Used in HEATR to calculate + damage-energy cross section. When None, use NJOY defaults. **kwargs Keyword arguments passed to :func:`openmc.data.njoy.run` @@ -515,6 +518,7 @@ def make_ace(filename, temperatures=None, acer=True, xsdir=None, # heatr if heatr: + ed = displacement_energy if displacement_energy is not None else 0 nheatr_in = nlast nheatr_local = nheatr_in + 1 tapeout[nheatr_local] = (output_dir / "heatr_local") if heatr is True \ From 852f92780c9c82dae7f5fbcfff91165510719f90 Mon Sep 17 00:00:00 2001 From: Satoshi Misumi <18492524+lzpel@users.noreply.github.com> Date: Wed, 22 Jul 2026 00:45:41 +0900 Subject: [PATCH 671/671] Fix M_PI being unavailable where _USE_MATH_DEFINES comes too late (#4023) Co-authored-by: Paul Romano --- examples/custom_source/source_ring.cpp | 1 + .../parameterized_source_ring.cpp | 1 + src/external/quartic_solver.cpp | 2 +- src/mesh.cpp | 13 ++++++------- src/plot.cpp | 3 +-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/custom_source/source_ring.cpp b/examples/custom_source/source_ring.cpp index 5ab531a4a..bce6c27eb 100644 --- a/examples/custom_source/source_ring.cpp +++ b/examples/custom_source/source_ring.cpp @@ -1,3 +1,4 @@ +#define _USE_MATH_DEFINES #include // for M_PI #include // for unique_ptr diff --git a/examples/parameterized_custom_source/parameterized_source_ring.cpp b/examples/parameterized_custom_source/parameterized_source_ring.cpp index 9756e1b97..3aabf3581 100644 --- a/examples/parameterized_custom_source/parameterized_source_ring.cpp +++ b/examples/parameterized_custom_source/parameterized_source_ring.cpp @@ -1,3 +1,4 @@ +#define _USE_MATH_DEFINES #include #include #include diff --git a/src/external/quartic_solver.cpp b/src/external/quartic_solver.cpp index 915020ffa..71bdb25c3 100644 --- a/src/external/quartic_solver.cpp +++ b/src/external/quartic_solver.cpp @@ -1,5 +1,5 @@ -#include #define _USE_MATH_DEFINES // to make M_PI declared in Intel and MSVC compilers +#include #include #include #include diff --git a/src/mesh.cpp b/src/mesh.cpp index 89d1ff24b..b05560d2a 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -1,11 +1,10 @@ #include "openmc/mesh.h" #include // for copy, equal, min, min_element #include -#include // for uint64_t -#include // for memcpy -#define _USE_MATH_DEFINES // to make M_PI declared in Intel and MSVC compilers -#include // for ceil -#include // for size_t +#include // for ceil +#include // for size_t +#include // for uint64_t +#include // for memcpy #include #include // for accumulate #include @@ -1829,7 +1828,7 @@ StructuredMesh::MeshIndex CylindricalMesh::get_indices( } else { mapped_r[1] = std::atan2(r.y, r.x); if (mapped_r[1] < 0) - mapped_r[1] += 2 * M_PI; + mapped_r[1] += 2 * PI; } MeshIndex idx = StructuredMesh::get_indices(mapped_r, in_mesh); @@ -2123,7 +2122,7 @@ StructuredMesh::MeshIndex SphericalMesh::get_indices( mapped_r[1] = std::acos(r.z / mapped_r.x); mapped_r[2] = std::atan2(r.y, r.x); if (mapped_r[2] < 0) - mapped_r[2] += 2 * M_PI; + mapped_r[2] += 2 * PI; } MeshIndex idx = StructuredMesh::get_indices(mapped_r, in_mesh); diff --git a/src/plot.cpp b/src/plot.cpp index 96df1c5be..5c4eb9460 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -1,7 +1,6 @@ #include "openmc/plot.h" #include -#define _USE_MATH_DEFINES // to make M_PI declared in Intel and MSVC compilers #include #include #include @@ -1328,7 +1327,7 @@ std::pair RayTracePlot::get_pixel_ray( int horiz, int vert) const { // Compute field of view in radians - constexpr double DEGREE_TO_RADIAN = M_PI / 180.0; + constexpr double DEGREE_TO_RADIAN = PI / 180.0; double horiz_fov_radians = horizontal_field_of_view_ * DEGREE_TO_RADIAN; double p0 = static_cast(pixels()[0]); double p1 = static_cast(pixels()[1]);

    gACz3xS$)7QvAJ4q~sq5iSLU?Z+ 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 diff --git a/tests/regression_tests/surface_source_write/case-05/results_true.dat b/tests/regression_tests/surface_source_write/case-05/results_true.dat index 8979eb554..d4d1d1e5a 100644 --- a/tests/regression_tests/surface_source_write/case-05/results_true.dat +++ b/tests/regression_tests/surface_source_write/case-05/results_true.dat @@ -1,2 +1,2 @@ k-combined: -5.169123E-02 9.144814E-03 +5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/surface_source_write/case-05/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-05/surface_source_true.h5 index 1a2f54d765811bd10a2758e20a11b7ea1f242ae5..ee00af3895c349d60d786af1aedef39e05e24d20 100644 GIT binary patch delta 16296 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�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 ZGW~B#GSJ)n@9qo(Vl%43UYmve{vZ9g!(IRY diff --git a/tests/regression_tests/surface_source_write/case-07/results_true.dat b/tests/regression_tests/surface_source_write/case-07/results_true.dat index 8979eb554..d4d1d1e5a 100644 --- a/tests/regression_tests/surface_source_write/case-07/results_true.dat +++ b/tests/regression_tests/surface_source_write/case-07/results_true.dat @@ -1,2 +1,2 @@ k-combined: -5.169123E-02 9.144814E-03 +5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/surface_source_write/case-07/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-07/surface_source_true.h5 index f84a5c4c5b6d8cb4ad3f720e40580f6309998d57..60527c99bc259a790c1f71ca36c7a00bef7fdf3f 100644 GIT binary patch delta 16301 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 delta 16328 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 diff --git a/tests/regression_tests/surface_source_write/case-08/results_true.dat b/tests/regression_tests/surface_source_write/case-08/results_true.dat index 8979eb554..d4d1d1e5a 100644 --- a/tests/regression_tests/surface_source_write/case-08/results_true.dat +++ b/tests/regression_tests/surface_source_write/case-08/results_true.dat @@ -1,2 +1,2 @@ k-combined: -5.169123E-02 9.144814E-03 +5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/surface_source_write/case-08/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-08/surface_source_true.h5 index 1fd302a7cd429beaee0a203ec8e5f1da09cb09d9..3088597c08e1b4af24b1004f8d62e66bd18d6631 100644 GIT binary patch delta 16492 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 delta 16528 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 P!D9IFE&iy17We-E%q{pF diff --git a/tests/regression_tests/surface_source_write/case-09/results_true.dat b/tests/regression_tests/surface_source_write/case-09/results_true.dat index 8979eb554..d4d1d1e5a 100644 --- a/tests/regression_tests/surface_source_write/case-09/results_true.dat +++ b/tests/regression_tests/surface_source_write/case-09/results_true.dat @@ -1,2 +1,2 @@ k-combined: -5.169123E-02 9.144814E-03 +5.642735E-02 1.494035E-02 diff --git a/tests/regression_tests/surface_source_write/case-09/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-09/surface_source_true.h5 index af832f060cbfbfd6f25fd6cd2049a5bfef55e307..81cdbfb0fe8919edb309daee668fd689fc71507b 100644 GIT binary patch delta 15744 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